kopia lustrzana https://github.com/nolanlawson/pinafore
Porównaj commity
396 Commity
Autor | SHA1 | Data |
---|---|---|
Nolan Lawson | 8f61ea75ce | |
Nolan Lawson | 5889b404cb | |
Nolan Lawson | 794d9ca74e | |
Nolan Lawson | 72a07ac40d | |
Nolan Lawson | ed9a9f6539 | |
Arnaldo Gabriel | 452b34b3b4 | |
Thomas Preece | fd4bb4d864 | |
vitalyster | c426b7fe31 | |
Noelia Ruiz Martínez | c2851ce104 | |
Nolan Lawson | 2578d0964d | |
Noelia Ruiz Martínez | ff53fcab10 | |
Nolan Lawson | 750235cd8f | |
Nolan Lawson | b5cad87aaf | |
Nick Colley | a85ff62d48 | |
Nick Colley | e06f63684e | |
Nick Colley | f81778d37f | |
Nick Colley | 746298a1f7 | |
Nolan Lawson | 02f1dad098 | |
Nick Colley | 3edfed971f | |
Noelia Ruiz Martínez | d71430f86d | |
Noelia Ruiz Martínez | 6124c948de | |
Nolan Lawson | 774aa7a21c | |
Nolan Lawson | 276c6e7bea | |
Nolan Lawson | f61054a3d5 | |
Nolan Lawson | b1dc43a9c9 | |
Nolan Lawson | 040462f5b5 | |
Thomas Broyer | f5f3395a53 | |
Nick Colley | 3fb152ac7c | |
Daniel Soohan Park | 97e3b04f1f | |
Scott Feeney | 3c32b48e29 | |
Noelia Ruiz Martínez | 4a6907bbdc | |
Thomas Broyer | d31c800806 | |
Nolan Lawson | 380d2a0d45 | |
Nolan Lawson | 7fdbd72f13 | |
dependabot[bot] | 62b30f6d99 | |
Nolan Lawson | 6d6eb59f41 | |
James Teh | 30b00667f2 | |
Nick Colley | da28e98cfb | |
Nick Colley | 7417e89f78 | |
James Teh | 815438172e | |
James Teh | 8fc9d5c728 | |
Scott Feeney | a775bd9193 | |
Nolan Lawson | edb7e7b442 | |
Maxime Le Conte des Floris | 3c857d74b8 | |
Ringtail Software | 5eb7183048 | |
Nolan Lawson | a3f41917c7 | |
Nolan Lawson | 098da30f2a | |
Nolan Lawson | abc39ef982 | |
Nick Colley | b543399e0a | |
Nolan Lawson | fda00fc87c | |
Nick Colley | 0e4523a37d | |
Nolan Lawson | 4fb8f37db7 | |
Nolan Lawson | fac42a91a0 | |
Nolan Lawson | b50b9dc40b | |
Nick Colley | bc664e5ca1 | |
Marco Zehe | fa41fe7649 | |
Nolan Lawson | 53803db5be | |
James Teh | 8792d912bc | |
James Teh | a447b9535e | |
Nolan Lawson | 6b1533c947 | |
Nolan Lawson | 347dab4e29 | |
Nolan Lawson | fdec7b2b3d | |
Сергей Ворон | 7f86a94414 | |
Nolan Lawson | 302845866a | |
Nolan Lawson | f875e65c49 | |
Nolan Lawson | 85bc6ba372 | |
Nolan Lawson | 00b6d31f0c | |
Nolan Lawson | 035ab9cbff | |
Nolan Lawson | ad73918fa8 | |
Nolan Lawson | d57ab7238f | |
Nolan Lawson | fb5478cd06 | |
Nolan Lawson | 52880a4689 | |
Nolan Lawson | 2131ababf3 | |
Nolan Lawson | a318746961 | |
Nolan Lawson | 601c3e40c9 | |
Nolan Lawson | ff6e1dc6fc | |
Nolan Lawson | 4273666ce5 | |
Nolan Lawson | 6f4eb98397 | |
Nolan Lawson | 3c59069490 | |
dependabot[bot] | 55189e840b | |
Nolan Lawson | e2d5b5928d | |
Nolan Lawson | 6fde4e0b90 | |
Nolan Lawson | 6ebd6a6a01 | |
Gavin Mogan | 36ead0406d | |
Nolan Lawson | 1de26d4b06 | |
Nolan Lawson | f10e9dbcf3 | |
Nolan Lawson | 1c6387a0a4 | |
Nolan Lawson | 0fd7154ed4 | |
Nolan Lawson | 7ea387bc4c | |
Nolan Lawson | 68d756ca34 | |
Nolan Lawson | 6edb6df17d | |
Nolan Lawson | ed38cad661 | |
Nolan Lawson | 19e466aa90 | |
Nolan Lawson | f301fc59f6 | |
Nolan Lawson | 8fb4c40275 | |
dependabot[bot] | 87cb80bf18 | |
dependabot[bot] | 46682296d6 | |
dependabot[bot] | 99d4cb4d8c | |
dependabot[bot] | 9eb8fc1f1d | |
dependabot[bot] | b152359428 | |
Nolan Lawson | a211763d98 | |
Nolan Lawson | 69bb849508 | |
Nolan Lawson | 5fd8d0ac23 | |
Nolan Lawson | 78687479df | |
tobi | b312b3b485 | |
Nolan Lawson | b2d900f078 | |
Nolan Lawson | 9282d7099d | |
Nolan Lawson | 135c51a856 | |
Nolan Lawson | 1a7bbe19a2 | |
Nolan Lawson | c67be9acc2 | |
Alexander Yakovlev | 2e9afd711f | |
Nolan Lawson | 54a11778da | |
Nolan Lawson | 2a53bd3f80 | |
Nolan Lawson | 6794514916 | |
Nolan Lawson | 8685e4f603 | |
Nolan Lawson | 58d81a25ad | |
Nolan Lawson | 7d13f27d6c | |
Nolan Lawson | 30ad0becb5 | |
Rylan Cates | ce03460b86 | |
Matthew Connelly | 05a3b2d31f | |
Nolan Lawson | e04b1da754 | |
Nolan Lawson | 3e2fd130e0 | |
dependabot[bot] | 49723fa91e | |
Nolan Lawson | a9119fa53f | |
hellojaccc | 10ed291950 | |
Nolan Lawson | 00c6aa1843 | |
dependabot[bot] | 6e42e9f2b0 | |
Nolan Lawson | f2d752bfc2 | |
Nolan Lawson | fd6bb63450 | |
Nolan Lawson | e15f4523ba | |
Nolan Lawson | 5ecf8b8ab9 | |
Nolan Lawson | 8b8246c59f | |
Nolan Lawson | cac792a830 | |
dependabot[bot] | dc8b7c93f3 | |
Nolan Lawson | c57e3a2e7e | |
Nolan Lawson | 331f6e8803 | |
Nolan Lawson | 67f4a1ab2f | |
Nolan Lawson | f3c5e7de5f | |
Nolan Lawson | 58ff6beb26 | |
Nolan Lawson | 0df4b724ca | |
dependabot[bot] | fdf4110dad | |
Nolan Lawson | 54b3042042 | |
Nolan Lawson | 21678ec78e | |
Nolan Lawson | 368775e220 | |
Nolan Lawson | 9d5157f15c | |
Nolan Lawson | a1e105ccef | |
Nolan Lawson | 344a23fddd | |
Nolan Lawson | 32b1be96a9 | |
Nolan Lawson | da4d32c1e7 | |
Nolan Lawson | 821b785e6b | |
Nolan Lawson | d30f7f4b1a | |
Nolan Lawson | d84f4604ad | |
Nolan Lawson | 374b8b251e | |
Nolan Lawson | 16e66346d7 | |
Nolan Lawson | c5de673990 | |
Nolan Lawson | b3ab427ac0 | |
Nolan Lawson | f012369d72 | |
Nolan Lawson | 992c5efd7e | |
Nolan Lawson | b31a72f850 | |
Nolan Lawson | 7bc9c3f263 | |
Nolan Lawson | f13e5be3a0 | |
Nolan Lawson | 658a9736e1 | |
Nolan Lawson | 0c455c35c9 | |
Nolan Lawson | b241ea18ac | |
Nolan Lawson | cbdbb05926 | |
Nolan Lawson | c692a1850b | |
Nolan Lawson | e0827be8c8 | |
Nolan Lawson | a166dccb59 | |
Nolan Lawson | 7255221c5c | |
Nolan Lawson | e2813ae428 | |
dependabot[bot] | aa9878d1a9 | |
Nolan Lawson | 284d812367 | |
Nolan Lawson | 0861b22c85 | |
Nolan Lawson | c4fbf34a27 | |
Nolan Lawson | 8205b6a2a6 | |
Nolan Lawson | 9937b6f3cc | |
Nolan Lawson | 75de31f7c7 | |
Nolan Lawson | c4e8d772dd | |
Nolan Lawson | 69e3582157 | |
Nolan Lawson | 3971f9a636 | |
dependabot[bot] | f9ac31465d | |
Nolan Lawson | f7ea5d98ad | |
Nolan Lawson | 566cf6cd78 | |
Nolan Lawson | 85a5874876 | |
Nolan Lawson | 66fc202b5c | |
Nolan Lawson | ad9609738b | |
Nolan Lawson | 3a91ad75b8 | |
Dylan Staley | 11fca7b792 | |
Nolan Lawson | 7a28bd2d88 | |
Nolan Lawson | bb7ebb04bc | |
Nolan Lawson | c815292b0b | |
Nolan Lawson | d0c9be0c09 | |
Nolan Lawson | 69ef9f2798 | |
Nolan Lawson | 3c307a47fc | |
Nolan Lawson | b48db404ad | |
Nolan Lawson | 081df2bf82 | |
Calvin Walton | 1aa06bc041 | |
Nolan Lawson | d044e12aee | |
Nolan Lawson | 65733ce68a | |
Nolan Lawson | 751ed299f6 | |
Nolan Lawson | 237ac836c0 | |
Nolan Lawson | 75458a3410 | |
Nolan Lawson | 66cfc342f0 | |
Nolan Lawson | eba2b1cd74 | |
Nolan Lawson | 746c341fda | |
Nolan Lawson | 3bf744d2c5 | |
Nolan Lawson | fd321720f2 | |
Nolan Lawson | 40cb793e81 | |
Nolan Lawson | 98815714ba | |
Nolan Lawson | a7fb2e68dd | |
Nolan Lawson | c3fb1e2038 | |
Nolan Lawson | c4e73106cf | |
Nolan Lawson | 02019e9251 | |
Nolan Lawson | 193db0aa15 | |
Nolan Lawson | cf0f1d884a | |
Nolan Lawson | 5e7440aaee | |
Nolan Lawson | 5e61a8582b | |
Nolan Lawson | 4adc8ff748 | |
Nolan Lawson | 3271344c76 | |
Nolan Lawson | 9a5ce33efb | |
Nolan Lawson | a6c9d41f46 | |
Nolan Lawson | 987e5827b0 | |
Nolan Lawson | 88ccfdad6e | |
Nolan Lawson | f22b1bf328 | |
Nolan Lawson | a2dcbcdcda | |
Nolan Lawson | 1f2ce30fd4 | |
Nolan Lawson | 7d96876aca | |
Nolan Lawson | 650751d343 | |
Nolan Lawson | 8f63cc479c | |
Timo Tijhof | 5573f7cf32 | |
Marco Zehe | b9496c9bca | |
Nolan Lawson | 8c09ede2d4 | |
Nolan Lawson | a21a889f5f | |
Nolan Lawson | 67a338be17 | |
Nolan Lawson | ef3f107d82 | |
Nolan Lawson | b0c694b1bd | |
Nolan Lawson | b2583277eb | |
Nolan Lawson | 2c34527411 | |
Nolan Lawson | e507618451 | |
Nolan Lawson | 0b3ccdb6a2 | |
Nolan Lawson | 533360e32f | |
Nolan Lawson | e3d3249a20 | |
Nolan Lawson | ba3b76f769 | |
Nolan Lawson | c209fb23d5 | |
Nolan Lawson | c1e9cab238 | |
Nolan Lawson | 68178ce40e | |
Nolan Lawson | 9cf8f8b516 | |
Nolan Lawson | 2ecd8a39a9 | |
Nolan Lawson | ddd565c708 | |
Nolan Lawson | b451093ece | |
Nolan Lawson | 5b04db8442 | |
Nolan Lawson | 1e974824e1 | |
Nolan Lawson | cad8201692 | |
Nolan Lawson | 63790021a9 | |
Nolan Lawson | 760b7f6cd4 | |
Nolan Lawson | c3d25b88cf | |
Nolan Lawson | 6fdbedd594 | |
Nolan Lawson | 1afd14b7ac | |
Nolan Lawson | 09b5474e22 | |
Nolan Lawson | 2585b55479 | |
Nolan Lawson | 456dac73b5 | |
Nolan Lawson | a3d0c87e27 | |
Nolan Lawson | c909b0d9d9 | |
Nolan Lawson | c833680ecc | |
Nolan Lawson | 4218c4ce64 | |
Nolan Lawson | 6d5bb0e39e | |
Nolan Lawson | eba90b3122 | |
Nolan Lawson | 46a4774a61 | |
Marco Zehe | 96d84134b4 | |
Nolan Lawson | fc96d7137d | |
Nolan Lawson | a3e970fe7a | |
Nolan Lawson | eff9d6c52e | |
Nolan Lawson | 67feaef844 | |
Nolan Lawson | 0afaef350a | |
Nolan Lawson | a028a7e880 | |
dependabot[bot] | 2de875795b | |
Nolan Lawson | 6433a9c644 | |
Nolan Lawson | 0022286b46 | |
Nolan Lawson | 583285a09c | |
Nolan Lawson | 5bf5e1d36e | |
Nolan Lawson | 69aad56421 | |
Nolan Lawson | d3ce112f60 | |
Nolan Lawson | a124ba9dc8 | |
Nolan Lawson | f2e51bbbfe | |
Nolan Lawson | de2c58be6a | |
Nolan Lawson | ac08a53014 | |
Nolan Lawson | 006f0deee8 | |
Nolan Lawson | 01ba161520 | |
Nolan Lawson | 9cb16ea91c | |
Nolan Lawson | 870fa0e93c | |
Nolan Lawson | 742a76b4dd | |
Nolan Lawson | 57b75ade1b | |
Timo Tijhof | 9acb3faac8 | |
Nolan Lawson | f09e1bd975 | |
Nolan Lawson | ef3cecae74 | |
Nolan Lawson | 04b56f5dc5 | |
Nolan Lawson | 620dfafd09 | |
Nolan Lawson | 37711ee17e | |
Nolan Lawson | 7803bdf797 | |
Nolan Lawson | 295bd18e05 | |
Nolan Lawson | e3b3382c01 | |
Nolan Lawson | 35818250c0 | |
Nolan Lawson | f6ba607493 | |
Nolan Lawson | f1907a8315 | |
Nolan Lawson | c683a4b85d | |
Nolan Lawson | fd7c22345c | |
Nolan Lawson | 7445a16e3c | |
Emilia Michanek | 60a146eb40 | |
Nolan Lawson | 978afb8753 | |
Nolan Lawson | 6adad8e4a9 | |
Nolan Lawson | 07f23c5990 | |
Nolan Lawson | 430ab4db4c | |
Nolan Lawson | 55b9c8d3b8 | |
Nolan Lawson | 40e9b44adc | |
Nolan Lawson | 4d1a72bb98 | |
Nolan Lawson | 1466371909 | |
Nolan Lawson | 2f41494a9a | |
Nolan Lawson | 08c021bc56 | |
Nolan Lawson | 5a9e5ae8bc | |
charlag | 2113ab3d46 | |
Nolan Lawson | 4e8a60ddef | |
Nolan Lawson | c081bf67dc | |
Nolan Lawson | 36cf9fd56d | |
Nolan Lawson | 07deb122f3 | |
Nolan Lawson | 2812e4521e | |
Nolan Lawson | 7de0023d17 | |
shine | c86d2b5088 | |
Nolan Lawson | cc8c605828 | |
Nolan Lawson | d40cd429e0 | |
Nolan Lawson | bd09718cf6 | |
Alex | 780db2be22 | |
Nolan Lawson | f1606706c4 | |
Nolan Lawson | b8fef16a92 | |
Nolan Lawson | 518691b8b2 | |
Nolan Lawson | 60dc4c4e7b | |
Nolan Lawson | f6f9b7d294 | |
Nolan Lawson | c5c6b6a14b | |
Nolan Lawson | 30819c6c47 | |
Nolan Lawson | 8c8934a8c6 | |
Nolan Lawson | ad066e39b1 | |
Nolan Lawson | 55ded5c234 | |
Nolan Lawson | f17096a8ac | |
Nolan Lawson | 44c1b6feb5 | |
charlag | 5e7c8003db | |
Nolan Lawson | 8bbe372fda | |
Nolan Lawson | eb436de9c3 | |
Nolan Lawson | 1371175bce | |
Nolan Lawson | 85ce93177b | |
Nolan Lawson | 949fb5f8a4 | |
Nolan Lawson | ec8e872f8d | |
Nolan Lawson | 3476b9dc7e | |
Nolan Lawson | d26470592c | |
Nolan Lawson | ceff1f1f8f | |
Nolan Lawson | 1fc14107c8 | |
Nolan Lawson | 4fc41affe4 | |
Nolan Lawson | bedb636182 | |
Nolan Lawson | f080148aad | |
Nolan Lawson | a790004be7 | |
Nolan Lawson | c98b198e60 | |
Nolan Lawson | ea1315858d | |
Nolan Lawson | beade4aec3 | |
Nolan Lawson | cc62000b21 | |
Nolan Lawson | 836b0e341f | |
Nolan Lawson | 9e09ba6ca1 | |
Nolan Lawson | 3f7aa7fb00 | |
Nolan Lawson | c610a259d5 | |
Nolan Lawson | f5eb5fc50b | |
Nolan Lawson | 1df3b506e9 | |
Nolan Lawson | dacd9dcc5b | |
Alex | a4820a2792 | |
Nolan Lawson | 511d16694c | |
Nolan Lawson | 5c4b44e3dd | |
Nolan Lawson | 4b4cee3662 | |
Nolan Lawson | 5348b3ccce | |
Nolan Lawson | 6447326f5c | |
Nolan Lawson | 00b9b1c216 | |
Nolan Lawson | 4fe7a818ab | |
Nolan Lawson | 7c8cd271ca | |
Nolan Lawson | 2f602e5a58 | |
Nolan Lawson | a78c7c6992 | |
greenkeeper[bot] | 3e63323c27 | |
Nolan Lawson | 0c300f8e70 | |
Nolan Lawson | 4ad7de8e8d | |
Nolan Lawson | 0ce47f0379 | |
Nolan Lawson | 5f6c5d89d1 | |
Nolan Lawson | e1532ed9d1 | |
Nolan Lawson | ae3bd2bda2 | |
Nolan Lawson | 06a403df28 | |
Nolan Lawson | a4a9cb7962 | |
Nolan Lawson | 1f0d67fcc4 | |
greenkeeper[bot] | bfb1da6bd0 | |
greenkeeper[bot] | ee5d911d49 | |
greenkeeper[bot] | 94fd351638 | |
Nolan Lawson | f6f2da2748 | |
Nolan Lawson | 767fbccaff | |
Nolan Lawson | 55d6de2de6 |
|
@ -12,9 +12,9 @@ tests
|
|||
/src/template.html
|
||||
/static/*.css
|
||||
/static/icons.svg
|
||||
/static/robots.txt
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-mart-all.json
|
||||
/static/emoji-*.json
|
||||
/static/manifest.json
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
/now.json
|
||||
/vercel.json
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
name: Read-only e2e tests
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.2
|
||||
env:
|
||||
POSTGRES_USER: pinafore
|
||||
POSTGRES_PASSWORD: pinafore
|
||||
POSTGRES_DB: pinafore_development
|
||||
POSTGRES_HOST: 127.0.0.1
|
||||
POSTGRES_PORT: 5432
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
redis:
|
||||
image: redis:5
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0.4'
|
||||
- name: Cache Mastodon bundler
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bundle-vendor-cache
|
||||
# cache based on masto version implicitly defined in mastodon-config.js
|
||||
key: masto-bundler-v3-${{ hashFiles('bin/mastodon-config.js') }}
|
||||
- name: Cache Mastodon's and our yarn
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/yarn
|
||||
# cache based on our version and masto version implicitly defined in mastodon-config.js
|
||||
# because we share the yarn cache
|
||||
key: masto-yarn-v1-${{ hashFiles('yarn.lock') }}-${{ hashFiles('bin/mastodon-config.js') }}
|
||||
- name: Install Mastodon system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
ffmpeg \
|
||||
fonts-noto-color-emoji \
|
||||
imagemagick \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
libprotobuf-dev \
|
||||
postgresql-contrib \
|
||||
protobuf-compiler
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn clone-mastodon
|
||||
- name: Move bundler cache so Mastodon can find it
|
||||
run: if [ -d ~/.bundle-vendor-cache ]; then mkdir -p ./mastodon/vendor && mv ~/.bundle-vendor-cache ./mastodon/vendor/bundle; fi
|
||||
- name: Read-only e2e tests
|
||||
run: yarn test-in-ci-suite0
|
||||
- name: Move bundler cache so GitHub Actions can find it
|
||||
run: mv ./mastodon/vendor/bundle ~/.bundle-vendor-cache
|
|
@ -0,0 +1,65 @@
|
|||
name: Read-write e2e tests
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.2
|
||||
env:
|
||||
POSTGRES_USER: pinafore
|
||||
POSTGRES_PASSWORD: pinafore
|
||||
POSTGRES_DB: pinafore_development
|
||||
POSTGRES_HOST: 127.0.0.1
|
||||
POSTGRES_PORT: 5432
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
redis:
|
||||
image: redis:5
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0.4'
|
||||
- name: Cache Mastodon bundler
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bundle-vendor-cache
|
||||
# cache based on masto version implicitly defined in mastodon-config.js
|
||||
key: masto-bundler-v3-${{ hashFiles('bin/mastodon-config.js') }}
|
||||
- name: Cache Mastodon's and our yarn
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/yarn
|
||||
# cache based on our version and masto version implicitly defined in mastodon-config.js
|
||||
# because we share the yarn cache
|
||||
key: masto-yarn-v1-${{ hashFiles('yarn.lock') }}-${{ hashFiles('bin/mastodon-config.js') }}
|
||||
- name: Install Mastodon system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
ffmpeg \
|
||||
fonts-noto-color-emoji \
|
||||
imagemagick \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
libprotobuf-dev \
|
||||
postgresql-contrib \
|
||||
protobuf-compiler
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn clone-mastodon
|
||||
- name: Move bundler cache so Mastodon can find it
|
||||
run: if [ -d ~/.bundle-vendor-cache ]; then mkdir -p ./mastodon/vendor && mv ~/.bundle-vendor-cache ./mastodon/vendor/bundle; fi
|
||||
- name: Read-write e2e tests
|
||||
run: yarn test-in-ci-suite1
|
||||
- name: Move bundler cache so GitHub Actions can find it
|
||||
run: mv ./mastodon/vendor/bundle ~/.bundle-vendor-cache
|
|
@ -0,0 +1,17 @@
|
|||
name: Unit tests
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn lint
|
||||
- run: yarn test-vercel-json
|
||||
- run: yarn test-unit
|
|
@ -6,9 +6,13 @@
|
|||
/src/template.html
|
||||
/static/*.css
|
||||
/static/icons.svg
|
||||
/static/robots.txt
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-mart-all.json
|
||||
/static/emoji-*.json
|
||||
/static/manifest.json
|
||||
/static/TwemojiCountryFlags.woff2
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
/now.json
|
||||
|
||||
.now
|
||||
.vercel
|
||||
/webpack/*.cjs
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
_
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
lint-staged
|
||||
run-s build-vercel-json
|
||||
git add vercel.json
|
79
.travis.yml
79
.travis.yml
|
@ -1,79 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
dist: xenial
|
||||
sudo: false
|
||||
services:
|
||||
- redis-server
|
||||
- postgresql
|
||||
addons:
|
||||
chrome: stable
|
||||
postgresql: "10"
|
||||
apt:
|
||||
packages:
|
||||
- autoconf
|
||||
- bison
|
||||
- build-essential
|
||||
- ffmpeg
|
||||
- file
|
||||
- g++
|
||||
- gcc
|
||||
- imagemagick
|
||||
- libffi-dev
|
||||
- libgdbm-dev
|
||||
- libgdbm-dev
|
||||
- libicu-dev
|
||||
- libidn11-dev
|
||||
- libncurses5-dev
|
||||
- libpq-dev
|
||||
- libprotobuf-dev
|
||||
- libreadline6-dev
|
||||
- libssl-dev
|
||||
- libxml2-dev
|
||||
- libxslt1-dev
|
||||
- libyaml-dev
|
||||
- pkg-config
|
||||
- postgresql-10
|
||||
- postgresql-client-10
|
||||
- postgresql-contrib-10
|
||||
- protobuf-compiler
|
||||
- redis-server
|
||||
- redis-tools
|
||||
- zlib1g-dev
|
||||
before_install:
|
||||
- npm install -g greenkeeper-lockfile@1
|
||||
- psql -d template1 -U postgres -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;"
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- ./bin/setup-mastodon-in-travis.sh
|
||||
install: yarn # don't allow yarn --pure-lockfile because Greenkeeper needs to update
|
||||
before_script:
|
||||
- yarn run lint
|
||||
- greenkeeper-lockfile-update
|
||||
after_script:
|
||||
- greenkeeper-lockfile-upload
|
||||
- rm -f /home/travis/.rvm/gems/ruby-*/bin/posix-spawn-benchmark # file seems generated every time, breaks travis cache
|
||||
script: travis_retry yarn run $COMMAND
|
||||
env:
|
||||
global:
|
||||
- ESM_DISABLE_CACHE=1 # this cache causes travis to rebuild its cache every single time
|
||||
- TERSER_DISABLE_CACHE=1 # ditto
|
||||
- secure: "iR11lD+OAyTJdNoK67esDOrd34HKQboJo0DrVL4xqwoAwQGmNX7posBtcj0TOwVAdHfd5S80xlhWlMebrtHWR9oMtdcBXusWnehZVRB0WE89n8enkJCbAxn5uMpcEpKpDHTcfR/Gbxf2sw15dTy0PrW/ldiZXWf7wybJEBGbrEP7QI8oy3VHzmKpSyjRpN/hnSlgxskVnfIMKPp43D+705Ka7aMJNTWZ5dAdKdIjQWX6j6jlqx6Vl+qIq7td3DTZA9A5ft8HxaWC27F1bbd52PdRY2h8Ii3Ps+n8Q8uZK1KJPp9t3pPk+PmYINu2715ArukRk73kahnFadBQLhytn95FLiyKOLj+ajBNo+o3KIQDs3qRj8gkXpkJpuLAPgwABOEVWuLh9y+oa33IDYVzyESRVtXqbbwgziYVjNQCozP1Nt9+Wyh4YHfdOjEEMdlVlkwlyzPfaLAqVBusEphnaF/vx7itdVvxOMQYNcSRoBrAgciN4ng0GZHi5P85DMXnNV41r+d3JK5JEYZD/gpSja5cPUlpPlkXsKiElG3fEoO2D/Uc8rR9Cu84XiJiJQjP91QuWagfdhgqM4YOndt2YukiGzRzDMjTx1BzSW4S11RQGWzZrv06zmDLXTlnUAMEm1/Exo4L6VfgRvyFmgM0LAT+IceVEHbOKC/Hnd8Y3mo="
|
||||
- secure: "E9t4zTDdPX9I4XgeC5zJEy+mIM2s0MFPpNfJ/mc5q/JX+gQkHSNkE/32NKgfSce85v33kWWxiPK4qorgX4B+v/MkK6kVQ2HSC7p4XttlQucvSICh/hYSM21WnH7g7DRNj3mDWHWEAwQuquaxVLfGWL60M/svnKG9MOoewos9iDj9AvANm6J09DjRmLuqmDV+VL+cV6ZL/SvUZ5Ervkgs/s3nXHEMse9rKMH/6/KncTGGPRolqyx86XSU6/XtRKX2+bEdiOaIxUYYvjcHJZTKxpSelPpEHjoUfWCM2CG3WyjtYkLF/1Romh8Ft4pnz+iiTzN3eWaT2ralOFvW30oB3cKbDcFb6LDGfXYw7v+XIORc79Ehcb2XlweEymf7fPhbx+7bkCfudRCMLw6OUWXoh66BBjOh2gcQYQ2+3U8KV7YKl/ZQHYb722wE6rN0YvJ6zGriWomDuV1smdyu4teo4lY2oVUUUGflyz2HWxnjVbqWizw4k69TNIcEEQ8j8YgdXMUxNMUOJoCu0c3Lnd8J1BeU/7LX87c54/oCMjEivnsENGIC/EUgAoXDi3L0y7HzHgaDs112p5zjspJsSSON/T4E2uyyb2RpjBY4Ghl43a/RDAlv1gUFtvbanphg3PEGMfG7B2gxk9Z/v5J9pUP/NtsspmT2MvTHZXtH/44XPEU="
|
||||
matrix:
|
||||
include:
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
||||
- env: COMMAND=test-unit
|
||||
- env: COMMAND=deploy-all-travis
|
||||
allow_failures:
|
||||
- env: COMMAND=deploy-all-travis
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^greenkeeper/.*$/
|
||||
cache:
|
||||
yarn: true
|
||||
bundler: true
|
||||
directories:
|
||||
- /home/travis/.rvm/
|
|
@ -7,6 +7,7 @@
|
|||
/static/*.css
|
||||
/static/icons.svg
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-mart-all.json
|
||||
/static/emoji-*.json
|
||||
/static/manifest.json
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
|
@ -1,10 +1,16 @@
|
|||
# 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).
|
||||
This document contains a list of _breaking changes_ for Pinafore. For a full changelog, see [GitHub releases](https://github.com/nolanlawson/pinafore/releases).
|
||||
|
||||
## 2.0.0
|
||||
|
||||
For self-hosters, the new minimum Node.js versions are v12.20+, v14.14+, or v16.0+ [due to native ES Modules](https://github.com/nolanlawson/pinafore/pull/2064).
|
||||
|
||||
Please check your Node version using `node --version` and update as necessary.
|
||||
|
||||
## 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:
|
||||
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`.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
For full release notes, see [GitHub releases](https://github.com/nolanlawson/pinafore/releases).
|
||||
|
||||
For breaking changes, see [BREAKING_CHANGES.md](https://github.com/nolanlawson/pinafore/blob/master/BREAKING_CHANGES.md).
|
|
@ -14,7 +14,7 @@ To run a dev server with hot reloading:
|
|||
|
||||
Now it's running at `localhost:4002`.
|
||||
|
||||
**Linux users:** for file changes to work,
|
||||
**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).
|
||||
|
||||
|
@ -38,8 +38,8 @@ running on `localhost:3000`.
|
|||
### Running integration tests
|
||||
|
||||
The integration tests require running Mastodon itself,
|
||||
meaning the [Mastodon development guide](https://docs.joinmastodon.org/development/overview/)
|
||||
is relevant here. In particular, you'll need a recent
|
||||
meaning the [Mastodon development guide](https://docs.joinmastodon.org/dev/setup/)
|
||||
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:
|
||||
|
@ -96,7 +96,7 @@ 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
|
||||
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".
|
||||
|
@ -120,12 +120,13 @@ or
|
|||
|
||||
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_OR_BRANCH` 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
|
||||
2. Update the `GIT_TAG` in `mastodon-config.js` to whatever you want
|
||||
3. If the Ruby version changed (check Mastodon's `.ruby-version`), install it and update `RUBY_VERSION` in `mastodon-config.js` as well as the Ruby version in `.github/workflows`.
|
||||
4. Run `yarn run-mastodon`
|
||||
5. Run `yarn backup-mastodon-data` to overwrite the data in `fixtures/`
|
||||
6. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
|
||||
7. Commit all changed files
|
||||
8. Run `rm -fr mastodon/` and `yarn run run-mastodon` to confirm everything's working
|
||||
|
||||
Check `mastodon.log` if you have any issues.
|
||||
|
||||
|
@ -137,12 +138,6 @@ updating the `fixtures/` should make that a no-op.
|
|||
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`.
|
||||
|
||||
## Legacy build
|
||||
|
||||
Pinafore also offers a "legacy" build designed for older browsers. To build this version, use:
|
||||
|
||||
LEGACY=1 yarn build
|
||||
|
||||
## Debug build
|
||||
|
||||
To disable minification in a production build (for debugging purposes), you can run:
|
||||
|
@ -158,52 +153,19 @@ The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicl
|
|||
|
||||
This is also available locally after `yarn run build` at `.sapper/client/report.html`.
|
||||
|
||||
## Codebase overview
|
||||
## Deploying
|
||||
|
||||
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
||||
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
||||
This section only applies to `dev.pinafore.social` and `pinafore.social`, not if you're hosting your own version of
|
||||
Pinafore.
|
||||
|
||||
### Prebuild process
|
||||
The site uses [Vercel](https://vercel.com). The `master` branch publishes to `dev.pinafore.social` and the `production`
|
||||
branch deploys to `pinafore.social`.
|
||||
|
||||
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.
|
||||
## Architecture
|
||||
|
||||
### Lots of small files
|
||||
See [Architecture.md](https://github.com/nolanlawson/pinafore/blob/master/docs/Architecture.md).
|
||||
|
||||
Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and
|
||||
code-splitting, as well as avoiding circular dependencies.
|
||||
## Internationalization
|
||||
|
||||
### Preact is loaded dynamically
|
||||
See [Internationalization.md](https://github.com/nolanlawson/pinafore/blob/master/docs/Internationalization.md).
|
||||
|
||||
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 Preact 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.
|
||||
|
|
21
Dockerfile
21
Dockerfile
|
@ -1,23 +1,16 @@
|
|||
# Using Alpine to keep the images smaller
|
||||
FROM alpine:latest
|
||||
# Change to using the official NodeJS Alpine container
|
||||
FROM node:16-alpine
|
||||
|
||||
# Pushing all files into image
|
||||
WORKDIR /app
|
||||
ADD . /app
|
||||
COPY . /app
|
||||
|
||||
# Install updates and NodeJS+Dependencies
|
||||
RUN apk add --update --no-cache --virtual build-dependencies git python build-base clang \
|
||||
# Install updates and NodeJS+Dependencies
|
||||
&& apk add --update --no-cache nodejs npm \
|
||||
# Install yarn
|
||||
&& npm i yarn -g \
|
||||
# Install Pinafore
|
||||
&& yarn --production --pure-lockfile \
|
||||
&& yarn build \
|
||||
&& yarn cache clean \
|
||||
&& rm -rf ./src \
|
||||
# Cleanup
|
||||
&& apk del build-dependencies
|
||||
RUN yarn --production --pure-lockfile && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf ./src ./docs ./tests ./bin
|
||||
|
||||
# Expose port 4002
|
||||
EXPOSE 4002
|
||||
|
|
35
README.md
35
README.md
|
@ -1,4 +1,6 @@
|
|||
# Pinafore [![Build Status](https://travis-ci.org/nolanlawson/pinafore.svg)](https://travis-ci.org/nolanlawson/pinafore) [![Greenkeeper badge](https://badges.greenkeeper.io/nolanlawson/pinafore.svg)](https://greenkeeper.io/)
|
||||
# Pinafore [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/)
|
||||
|
||||
_**Note:** Pinafore is unmaintained. Read [this](https://nolanlawson.com/2023/01/09/retiring-pinafore/). Original documentation follows._
|
||||
|
||||
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
|
||||
|
||||
|
@ -6,7 +8,7 @@ Pinafore is available at [pinafore.social](https://pinafore.social). Beta releas
|
|||
|
||||
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 [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
|
||||
For updates and support, follow [@pinafore@fosstodon.org](https://fosstodon.org/@pinafore).
|
||||
|
||||
## Browser support
|
||||
|
||||
|
@ -31,12 +33,12 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
|||
- Progressive Web App features
|
||||
- Multi-instance support
|
||||
- Support latest versions of Chrome, Edge, Firefox, and Safari
|
||||
- Support non-Mastodon instances (e.g. Pleroma) as well as possible
|
||||
- Internationalization
|
||||
|
||||
### Secondary / possible future goals
|
||||
|
||||
- Support for Pleroma or other non-Mastodon backends
|
||||
- Serve as an alternative frontend tied to a particular instance
|
||||
- Support for non-English languages (i18n)
|
||||
- Offline search
|
||||
|
||||
### Non-goals
|
||||
|
@ -52,7 +54,7 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
|||
|
||||
## Building
|
||||
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com).
|
||||
|
||||
To build Pinafore for production, first install dependencies:
|
||||
|
||||
|
@ -75,6 +77,14 @@ To build a Docker image for production:
|
|||
|
||||
Now Pinafore is running at `localhost:4002`.
|
||||
|
||||
### docker-compose
|
||||
|
||||
Alternatively, use docker-compose to build and serve the image for production:
|
||||
|
||||
docker-compose up --build -d
|
||||
|
||||
The image will build and start, then detach from the terminal running at `localhost:4002`.
|
||||
|
||||
### Updating
|
||||
|
||||
To keep your version of Pinafore up to date, you can use `git` to check out the latest tag:
|
||||
|
@ -83,19 +93,20 @@ To keep your version of Pinafore up to date, you can use `git` to check out the
|
|||
|
||||
### Exporting
|
||||
|
||||
Pinafore is a static site. When you run `yarn build`, static files will be
|
||||
Pinafore is a static site. When you run `yarn build`, static files will be
|
||||
written to `__sapper__/export`.
|
||||
|
||||
In theory you could host these static files yourself (e.g. using nginx or Apache), but
|
||||
it's not recommended, because:
|
||||
It is _not_ recommended to directly expose these files when self-hosting. Instead, you should use `node server.js` (e.g. with an
|
||||
nginx or Apache proxy in front). This adds several things you don't get from the raw static files:
|
||||
|
||||
- You'd have to set the [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers yourself,
|
||||
which are an important security feature.
|
||||
- Some routes are dynamic and need to be routed to the correct static file.
|
||||
- [CSP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (important for security)
|
||||
- Certain dynamic routes (less important because of Service Worker managing routing, but certain things could break if Service Workers are disabled in the user's browser)
|
||||
|
||||
Having an [nginx config generator](https://github.com/nolanlawson/pinafore/issues/1878) is currently an open issue.
|
||||
|
||||
## Developing and testing
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/nolanlawson/pinafore/blob/master/CONTRIBUTING.md) for
|
||||
See [CONTRIBUTING.md](https://github.com/nolanlawson/pinafore/blob/master/CONTRIBUTING.md) for
|
||||
how to run Pinafore in dev mode and run tests.
|
||||
|
||||
## Changelog
|
||||
|
|
|
@ -1,29 +1,83 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import CleanCSS from 'clean-css'
|
||||
import { LOCALE } from '../src/routes/_static/intl.js'
|
||||
import { getIntl, trimWhitespace } from './getIntl.js'
|
||||
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const readFile = promisify(fs.readFile)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
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')
|
||||
// Try 'en-US' first, then 'en' if that doesn't exist
|
||||
const PREFERRED_LOCALES = [LOCALE, LOCALE.split('-')[0]]
|
||||
|
||||
// emojibase seems like the most "neutral" shortcodes, but cldr is available in every language
|
||||
const PREFERRED_SHORTCODES = ['emojibase', 'cldr']
|
||||
|
||||
async function getEmojiI18nFile (locale, shortcode) {
|
||||
const filename = path.resolve(__dirname,
|
||||
'../node_modules/emoji-picker-element-data',
|
||||
locale,
|
||||
shortcode,
|
||||
'data.json')
|
||||
try {
|
||||
return JSON.parse(await readFile(filename, 'utf8'))
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
|
||||
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 getFirstExistingEmojiI18nFile () {
|
||||
for (const locale of PREFERRED_LOCALES) {
|
||||
for (const shortcode of PREFERRED_SHORTCODES) {
|
||||
const json = await getEmojiI18nFile(locale, shortcode)
|
||||
if (json) {
|
||||
return json
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildEmojiI18nFile () {
|
||||
const json = await getFirstExistingEmojiI18nFile()
|
||||
|
||||
if (!json) {
|
||||
throw new Error(`Couldn't find i18n data for locale ${LOCALE}. Is it supported in emoji-picker-element-data?`)
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
path.resolve(__dirname, `../static/emoji-${LOCALE}.json`),
|
||||
JSON.stringify(json),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
async function buildManifestJson () {
|
||||
const template = await readFile(path.resolve(__dirname, '../src/build/manifest.json'), 'utf8')
|
||||
// replace {@intl.foo}
|
||||
const output = template
|
||||
.replace(/{intl\.([^}]+)}/g, (match, p1) => trimWhitespace(getIntl(p1)))
|
||||
|
||||
await writeFile(
|
||||
path.resolve(__dirname, '../static/manifest.json'),
|
||||
JSON.stringify(JSON.parse(output)), // minify json
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
async function buildFlagEmojiFile () {
|
||||
await copyFile(path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/country-flag-emoji-polyfill/dist/TwemojiCountryFlags.woff2'
|
||||
), path.resolve(
|
||||
__dirname, '../static/TwemojiCountryFlags.woff2'
|
||||
))
|
||||
}
|
||||
|
||||
async function main () {
|
||||
await Promise.all([
|
||||
compileThirdPartyCss(),
|
||||
compileThirdPartyJson()
|
||||
buildEmojiI18nFile(),
|
||||
buildManifestJson(),
|
||||
buildFlagEmojiFile()
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,12 @@ 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 babel from 'rollup-plugin-babel'
|
||||
import { themes } from '../src/routes/_static/themes'
|
||||
import terserOptions from './terserOptions'
|
||||
import { themes } from '../src/routes/_static/themes.js'
|
||||
import terserOptions from './terserOptions.js'
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
|
||||
const themeColors = Object.fromEntries(themes.map(_ => ([_.name, _.color])))
|
||||
|
||||
export async function buildInlineScript () {
|
||||
const inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
|
||||
|
@ -21,15 +19,15 @@ export async function buildInlineScript () {
|
|||
input: inlineScriptPath,
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.LEGACY': JSON.stringify(process.env.LEGACY),
|
||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
||||
values: {
|
||||
'process.browser': true,
|
||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
||||
},
|
||||
preventAssignment: true
|
||||
}),
|
||||
process.env.LEGACY && babel({
|
||||
runtimeHelpers: true,
|
||||
presets: ['@babel/preset-env']
|
||||
}),
|
||||
!process.env.DEBUG && terser(terserOptions)
|
||||
// TODO: can't disable terser at all, it causes the CSP checksum to stop working
|
||||
// because the HTML gets minified as some point so the checksums don't match.
|
||||
terser({ ...terserOptions, mangle: !process.env.DEBUG })
|
||||
]
|
||||
})
|
||||
const { output } = await bundle.generate({
|
||||
|
@ -40,10 +38,10 @@ export async function buildInlineScript () {
|
|||
const { code, map } = output[0]
|
||||
|
||||
const fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
|
||||
const checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
|
||||
const checksum = crypto.createHash('sha256').update(fullCode, 'utf8').digest('base64')
|
||||
|
||||
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
|
||||
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
|
||||
`export default ${JSON.stringify(checksum)}`, 'utf8')
|
||||
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
|
||||
map.toString(), 'utf8')
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import sass from 'node-sass'
|
||||
import sass from 'sass'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import cssDedoupe from 'css-dedoupe'
|
||||
import { TextDecoder } from 'text-encoding'
|
||||
import textEncodingPackage from 'text-encoding'
|
||||
const { TextDecoder } = textEncodingPackage
|
||||
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const readdir = promisify(fs.readdir)
|
||||
const render = promisify(sass.render.bind(sass))
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
|
||||
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
||||
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
|
||||
|
@ -16,7 +17,7 @@ const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
|||
const assetsDir = path.join(__dirname, '../static')
|
||||
|
||||
async function renderCss (file) {
|
||||
return (await render({ file, outputStyle: 'compressed' })).css
|
||||
return sass.renderSync({ file, outputStyle: 'compressed' }).css
|
||||
}
|
||||
|
||||
async function compileGlobalSass () {
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import svgs from './svgs'
|
||||
import svgs from './svgs.js'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import SVGO from 'svgo'
|
||||
import $ from 'cheerio'
|
||||
import { optimize } from 'svgo'
|
||||
import cheerioPackage from 'cheerio'
|
||||
|
||||
const svgo = new SVGO()
|
||||
const { default: $ } = cheerioPackage
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const readFile = promisify(fs.readFile)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
async function readSvg (svg) {
|
||||
const filepath = path.join(__dirname, '../', svg.src)
|
||||
const content = await readFile(filepath, 'utf8')
|
||||
const optimized = (await svgo.optimize(content))
|
||||
const optimized = (await optimize(content, { multipass: true }))
|
||||
const $optimized = $(optimized.data)
|
||||
const $path = $optimized.find('path, circle').removeAttr('fill')
|
||||
const viewBox = $optimized.attr('viewBox') || `0 0 ${$optimized.attr('width')} ${$optimized.attr('height')}`
|
||||
const $symbol = $('<symbol></symbol>')
|
||||
.attr('id', svg.id)
|
||||
.attr('viewBox', $optimized.attr('viewBox'))
|
||||
.attr('viewBox', viewBox)
|
||||
.append($path)
|
||||
return $.xml($symbol)
|
||||
}
|
||||
|
|
|
@ -2,14 +2,20 @@ 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'
|
||||
import { buildSass } from './build-sass.js'
|
||||
import { buildInlineScript } from './build-inline-script.js'
|
||||
import { buildSvg } from './build-svg.js'
|
||||
import { performance } from 'perf_hooks'
|
||||
import { debounce } from '../src/routes/_thirdparty/lodash/timers.js'
|
||||
import applyIntl from '../webpack/svelte-intl-loader.js'
|
||||
import { LOCALE } from '../src/routes/_static/intl.js'
|
||||
import rtlDetectPackage from 'rtl-detect'
|
||||
|
||||
const { getLangDir } = rtlDetectPackage
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
const LOCALE_DIRECTION = getLangDir(LOCALE)
|
||||
const DEBOUNCE = 500
|
||||
|
||||
const builders = [
|
||||
|
@ -77,8 +83,8 @@ function doWatch () {
|
|||
}
|
||||
|
||||
async function buildAll () {
|
||||
const start = now()
|
||||
const html = (await Promise.all(partials.map(async partial => {
|
||||
const start = performance.now()
|
||||
let html = (await Promise.all(partials.map(async partial => {
|
||||
if (typeof partial === 'string') {
|
||||
return partial
|
||||
}
|
||||
|
@ -88,8 +94,11 @@ async function buildAll () {
|
|||
return partial.result
|
||||
}))).join('')
|
||||
|
||||
html = applyIntl(html)
|
||||
.replace('{process.env.LOCALE}', LOCALE)
|
||||
.replace('{process.env.LOCALE_DIRECTION}', LOCALE_DIRECTION)
|
||||
await writeFile(path.resolve(__dirname, '../src/template.html'), html, 'utf8')
|
||||
const end = now()
|
||||
const end = performance.now()
|
||||
console.log(`Built template.html in ${(end - start).toFixed(2)}ms`)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
// create the now.json file
|
||||
// create the vercel.json file
|
||||
// Unfortunately this has to be re-run periodically, as AFAICT there is no way to
|
||||
// give Zeit a script and tell them to run that, instead of using a static now.json file.
|
||||
// give Zeit a script and tell them to run that, instead of using a static vercel.json file.
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import { routes } from '../__sapper__/service-worker'
|
||||
import cloneDeep from 'lodash-es/cloneDeep'
|
||||
import inlineScriptChecksum from '../src/inline-script/checksum'
|
||||
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums'
|
||||
import { routes } from '../__sapper__/service-worker.js'
|
||||
import { cloneDeep } from '../src/routes/_utils/lodash-lite.js'
|
||||
import inlineScriptChecksum from '../src/inline-script/checksum.js'
|
||||
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums.js'
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
const JSON_TEMPLATE = {
|
||||
|
@ -17,15 +18,11 @@ const JSON_TEMPLATE = {
|
|||
env: {
|
||||
NODE_ENV: 'production'
|
||||
},
|
||||
builds: [
|
||||
{
|
||||
src: 'package.json',
|
||||
use: '@now/static-build',
|
||||
config: {
|
||||
distDir: '__sapper__/export'
|
||||
}
|
||||
}
|
||||
],
|
||||
github: {
|
||||
silent: true
|
||||
},
|
||||
buildCommand: 'yarn build',
|
||||
outputDirectory: '__sapper__/export',
|
||||
routes: [
|
||||
{
|
||||
src: '^/service-worker\\.js$',
|
||||
|
@ -41,13 +38,19 @@ const JSON_TEMPLATE = {
|
|||
dest: 'client/$1'
|
||||
},
|
||||
{
|
||||
src: '^/client/.*\\.(js|css|map|LICENSE)$',
|
||||
src: '^/client/.*\\.(js|css|map|LICENSE|txt)$',
|
||||
headers: {
|
||||
'cache-control': 'public,max-age=31536000,immutable'
|
||||
}
|
||||
},
|
||||
{
|
||||
src: '^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp)$',
|
||||
src: '^/.*\\.(png|jpe?g)$',
|
||||
headers: {
|
||||
'cache-control': 'public,max-age=31536000,immutable'
|
||||
}
|
||||
},
|
||||
{
|
||||
src: '^/.*\\.(css|json|svg|map|txt|gz|webapp|woff|woff2)$',
|
||||
headers: {
|
||||
'cache-control': 'public,max-age=3600'
|
||||
}
|
||||
|
@ -55,17 +58,38 @@ const JSON_TEMPLATE = {
|
|||
]
|
||||
}
|
||||
|
||||
const SCRIPT_CHECKSUMS = [inlineScriptChecksum]
|
||||
.concat(sapperInlineScriptChecksums)
|
||||
.map(_ => `'sha256-${_}'`)
|
||||
.join(' ')
|
||||
|
||||
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=(),interest-cohort=()'
|
||||
|
||||
const HTML_HEADERS = {
|
||||
'cache-control': 'public,max-age=3600',
|
||||
'content-security-policy': 'script-src \'self\' ' +
|
||||
`${[inlineScriptChecksum].concat(sapperInlineScriptChecksums).map(_ => `'sha256-${_}'`).join(' ')}; ` +
|
||||
'worker-src \'self\'; style-src \'self\' \'unsafe-inline\'; frame-src \'none\'; object-src \'none\'; manifest-src \'self\'',
|
||||
'content-security-policy': [
|
||||
"default-src 'self'",
|
||||
`script-src 'self' ${SCRIPT_CHECKSUMS}`,
|
||||
"worker-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' * data: blob:",
|
||||
"media-src 'self' *",
|
||||
"connect-src 'self' * data: blob:",
|
||||
"frame-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
"object-src 'none'",
|
||||
"manifest-src 'self'",
|
||||
"form-action 'self'", // we need form-action for the Web Share Target API
|
||||
"base-uri 'self'"
|
||||
].join(';'),
|
||||
'referrer-policy': 'no-referrer',
|
||||
'strict-transport-security': 'max-age=15552000; includeSubDomains',
|
||||
'permissions-policy': PERMISSIONS_POLICY,
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-download-options': 'noopen',
|
||||
'x-frame-options': 'SAMEORIGIN',
|
||||
'x-xss-protection': '1; mode=block'
|
||||
'x-frame-options': 'DENY',
|
||||
'x-xss-protection': '1; mode=block',
|
||||
'cross-origin-opener-policy': 'same-origin'
|
||||
}
|
||||
|
||||
async function main () {
|
||||
|
@ -102,7 +126,7 @@ async function main () {
|
|||
headers: cloneDeep(HTML_HEADERS)
|
||||
})
|
||||
|
||||
await writeFile(path.resolve(__dirname, '../now.json'), JSON.stringify(json, null, ' '), 'utf8')
|
||||
await writeFile(path.resolve(__dirname, '../vercel.json'), JSON.stringify(json, null, ' '), 'utf8')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
|
@ -0,0 +1,32 @@
|
|||
import { promisify } from 'util'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { envFile, GIT_TAG, GIT_URL, RUBY_VERSION } from './mastodon-config.js'
|
||||
import esMain from 'es-main'
|
||||
|
||||
const exec = childProcessPromise.exec
|
||||
const stat = promisify(fs.stat)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const dir = __dirname
|
||||
|
||||
const mastodonDir = path.join(dir, '../mastodon')
|
||||
|
||||
export default async function cloneMastodon () {
|
||||
try {
|
||||
await stat(mastodonDir)
|
||||
} catch (e) {
|
||||
console.log('Cloning mastodon...')
|
||||
await exec(`git clone --single-branch --branch ${GIT_TAG} ${GIT_URL} "${mastodonDir}"`)
|
||||
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||
await writeFile(path.join(dir, '../mastodon/.ruby-version'), RUBY_VERSION, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
if (esMain(import.meta)) {
|
||||
cloneMastodon().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Designed to be run before yarn build, and then tested with test-vercel-json-unchanged.sh
|
||||
|
||||
cp ./vercel.json /tmp/vercel-old.json
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [ "$TRAVIS_BRANCH" = master -a "$TRAVIS_PULL_REQUEST" = false ]; then
|
||||
yarn run deploy-dev
|
||||
fi
|
|
@ -1,41 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
# need to build to update now.json
|
||||
yarn run build
|
||||
|
||||
# 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 --scope 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
|
|
@ -0,0 +1,39 @@
|
|||
import { get } from '../src/routes/_utils/lodash-lite.js'
|
||||
import { DEFAULT_LOCALE, LOCALE } from '../src/routes/_static/intl.js'
|
||||
|
||||
import enUS from '../src/intl/en-US.js'
|
||||
import fr from '../src/intl/fr.js'
|
||||
import de from '../src/intl/de.js'
|
||||
import es from '../src/intl/es.js'
|
||||
|
||||
// TODO: make it so we don't have to explicitly list these out
|
||||
const locales = {
|
||||
'en-US': enUS,
|
||||
fr,
|
||||
de,
|
||||
es
|
||||
}
|
||||
|
||||
const intl = locales[LOCALE]
|
||||
const defaultIntl = locales[DEFAULT_LOCALE]
|
||||
|
||||
export function warningOrError (message) { // avoid crashing the whole server on `yarn dev`
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error(message)
|
||||
}
|
||||
console.warn(message)
|
||||
return '(Placeholder intl string)'
|
||||
}
|
||||
|
||||
export function getIntl (path) {
|
||||
path = path.split('.')
|
||||
const res = get(intl, path, get(defaultIntl, path))
|
||||
if (typeof res !== 'string') {
|
||||
return warningOrError('Unknown intl string: ' + JSON.stringify(path))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export function trimWhitespace (str) {
|
||||
return str.trim().replace(/\s+/g, ' ')
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { promisify } from 'util'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { DB_NAME, DB_PASS, DB_USER, mastodonDir, env } from './mastodon-config.js'
|
||||
import mkdirp from 'mkdirp'
|
||||
import esMain from 'es-main'
|
||||
|
||||
const exec = childProcessPromise.exec
|
||||
const stat = promisify(fs.stat)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
const dir = __dirname
|
||||
|
||||
async function setupMastodonDatabase () {
|
||||
console.log('Setting up mastodon database...')
|
||||
try {
|
||||
await exec(`psql -d template1 -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;"`)
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
} catch (e) { /* ignore */ }
|
||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
const dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
|
||||
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
const tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
|
||||
const systemDir = path.join(mastodonDir, 'public/system')
|
||||
await mkdirp(systemDir)
|
||||
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
||||
}
|
||||
|
||||
async function installMastodonDependencies () {
|
||||
const cwd = mastodonDir
|
||||
const installCommands = [
|
||||
'gem install bundler -v 2.3.26 --no-document',
|
||||
'gem install foreman -v 0.87.2 --no-document',
|
||||
'bundle config set --local frozen \'true\'',
|
||||
'bundle install',
|
||||
'yarn --pure-lockfile'
|
||||
]
|
||||
|
||||
const installedFile = path.join(mastodonDir, 'installed.txt')
|
||||
try {
|
||||
await stat(installedFile)
|
||||
console.log('Already installed Mastodon dependencies')
|
||||
} catch (e) {
|
||||
console.log('Installing Mastodon dependencies...')
|
||||
for (const cmd of installCommands) {
|
||||
console.log(cmd)
|
||||
await exec(cmd, { cwd, env })
|
||||
}
|
||||
await writeFile(installedFile, '', 'utf8')
|
||||
}
|
||||
await exec('bundle exec rails db:migrate', { cwd, env })
|
||||
}
|
||||
|
||||
export default async function installMastodon () {
|
||||
console.log('Installing Mastodon...')
|
||||
await setupMastodonDatabase()
|
||||
await installMastodonDependencies()
|
||||
}
|
||||
|
||||
if (esMain(import.meta)) {
|
||||
installMastodon().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import path from 'path'
|
||||
|
||||
export const DB_NAME = 'pinafore_development'
|
||||
export const DB_USER = 'pinafore'
|
||||
export const DB_PASS = 'pinafore'
|
||||
export const DB_PORT = process.env.PGPORT || 5432
|
||||
export const DB_HOST = '127.0.0.1'
|
||||
|
||||
export const envFile = `
|
||||
PAPERCLIP_SECRET=foo
|
||||
SECRET_KEY_BASE=bar
|
||||
OTP_SECRET=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
|
||||
DB_HOST=${DB_HOST}
|
||||
DB_PORT=${DB_PORT}
|
||||
DB_USER=${DB_USER}
|
||||
DB_NAME=${DB_NAME}
|
||||
DB_PASS=${DB_PASS}
|
||||
BIND=0.0.0.0
|
||||
`
|
||||
|
||||
export const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
||||
export const GIT_TAG = 'v4.0.2'
|
||||
|
||||
export const RUBY_VERSION = '3.0.4'
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
export const mastodonDir = path.join(__dirname, '../mastodon')
|
||||
|
||||
export const env = Object.assign({}, process.env, {
|
||||
RAILS_ENV: 'development',
|
||||
NODE_ENV: 'development',
|
||||
BUNDLE_PATH: path.join(mastodonDir, 'vendor/bundle'),
|
||||
DB_NAME,
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
DB_HOST,
|
||||
DB_PORT
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import times from 'lodash-es/times'
|
||||
import { times } from '../src/routes/_utils/lodash-lite.js'
|
||||
|
||||
function unrollThread (user, prefix, privacy, thread) {
|
||||
const res = []
|
||||
|
@ -9,11 +9,11 @@ function unrollThread (user, prefix, privacy, thread) {
|
|||
}
|
||||
for (const key of Object.keys(node)) {
|
||||
res.push({
|
||||
user: user,
|
||||
user,
|
||||
post: {
|
||||
internalId: prefix + key,
|
||||
text: key,
|
||||
privacy: privacy,
|
||||
privacy,
|
||||
inReplyTo: parentKey && (prefix + parentKey)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { actions } from './mastodon-data'
|
||||
import { users } from '../tests/users'
|
||||
import { postStatus } from '../src/routes/_api/statuses'
|
||||
import { followAccount } from '../src/routes/_api/follow'
|
||||
import { favoriteStatus } from '../src/routes/_api/favorite'
|
||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||
import { actions } from './mastodon-data.js'
|
||||
import { users } from '../tests/users.js'
|
||||
import { postStatus } from '../src/routes/_api/statuses.js'
|
||||
import { followAccount } from '../src/routes/_api/follow.js'
|
||||
import { favoriteStatus } from '../src/routes/_api/favorite.js'
|
||||
import { reblogStatus } from '../src/routes/_api/reblog.js'
|
||||
import fetch from 'node-fetch'
|
||||
import FileApi from 'file-api'
|
||||
import { pinStatus } from '../src/routes/_api/pin'
|
||||
import { submitMedia } from '../tests/submitMedia'
|
||||
import { pinStatus } from '../src/routes/_api/pin.js'
|
||||
import { submitMedia } from '../tests/submitMedia.js'
|
||||
|
||||
global.File = FileApi.File
|
||||
global.FormData = FileApi.FormData
|
||||
|
@ -17,10 +17,10 @@ export async function restoreMastodonData () {
|
|||
console.log('Restoring mastodon data...')
|
||||
const internalIdsToIds = {}
|
||||
for (const action of actions) {
|
||||
if (!action.post) {
|
||||
// If the action is a boost, favorite, etc., then it needs to
|
||||
if (!action.post || /@/.test(action.post.text)) {
|
||||
// If the action is a boost, favorite, mention, 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))
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
}
|
||||
console.log(JSON.stringify(action))
|
||||
const accessToken = users[action.user].accessToken
|
||||
|
|
|
@ -1,115 +1,22 @@
|
|||
import { restoreMastodonData } from './restore-mastodon-data'
|
||||
import { promisify } from 'util'
|
||||
import { restoreMastodonData } from './restore-mastodon-data.js'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start'
|
||||
import mkdirp from 'mkdirp'
|
||||
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start.js'
|
||||
import cloneMastodon from './clone-mastodon.js'
|
||||
import installMastodon from './install-mastodon.js'
|
||||
import { mastodonDir, env } from './mastodon-config.js'
|
||||
|
||||
const exec = childProcessPromise.exec
|
||||
const spawn = childProcessPromise.spawn
|
||||
const stat = promisify(fs.stat)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const dir = __dirname
|
||||
|
||||
const GIT_URL = 'https://github.com/nolanlawson/mastodon.git'
|
||||
const GIT_TAG_OR_COMMIT = '824ddcdbe'
|
||||
const GIT_BRANCH = 'v2.9.2-with-fuubar-fix'
|
||||
|
||||
const DB_NAME = 'pinafore_development'
|
||||
const DB_USER = 'pinafore'
|
||||
const DB_PASS = 'pinafore'
|
||||
const DB_PORT = process.env.PGPORT || 5432
|
||||
const DB_HOST = '127.0.0.1'
|
||||
|
||||
const envFile = `
|
||||
PAPERCLIP_SECRET=foo
|
||||
SECRET_KEY_BASE=bar
|
||||
OTP_SECRET=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
|
||||
DB_HOST=${DB_HOST}
|
||||
DB_PORT=${DB_PORT}
|
||||
DB_USER=${DB_USER}
|
||||
DB_NAME=${DB_NAME}
|
||||
DB_PASS=${DB_PASS}
|
||||
`
|
||||
|
||||
const mastodonDir = path.join(dir, '../mastodon')
|
||||
|
||||
let childProc
|
||||
|
||||
async function cloneMastodon () {
|
||||
try {
|
||||
await stat(mastodonDir)
|
||||
} catch (e) {
|
||||
console.log('Cloning mastodon...')
|
||||
await exec(`git clone --single-branch --branch ${GIT_BRANCH} ${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_OR_COMMIT}`, { cwd: mastodonDir })
|
||||
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
async function setupMastodonDatabase () {
|
||||
console.log('Setting up mastodon database...')
|
||||
try {
|
||||
await exec(`psql -d template1 -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;"`)
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
} catch (e) { /* ignore */ }
|
||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
const dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
|
||||
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
const tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
|
||||
const systemDir = path.join(mastodonDir, 'public/system')
|
||||
await mkdirp(systemDir)
|
||||
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
||||
}
|
||||
|
||||
async function runMastodon () {
|
||||
console.log('Running mastodon...')
|
||||
const env = Object.assign({}, process.env, {
|
||||
RAILS_ENV: 'development',
|
||||
NODE_ENV: 'development',
|
||||
DB_NAME,
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
DB_HOST,
|
||||
DB_PORT
|
||||
})
|
||||
const cwd = mastodonDir
|
||||
const cmds = [
|
||||
'gem install bundler foreman',
|
||||
'bundle install',
|
||||
'bundle exec rails db:migrate',
|
||||
'yarn --pure-lockfile'
|
||||
]
|
||||
|
||||
const installedFile = path.join(mastodonDir, 'installed.txt')
|
||||
try {
|
||||
await stat(installedFile)
|
||||
console.log('Already installed Mastodon')
|
||||
} catch (e) {
|
||||
console.log('Installing Mastodon...')
|
||||
for (const cmd of cmds) {
|
||||
console.log(cmd)
|
||||
await exec(cmd, { cwd, env })
|
||||
}
|
||||
await writeFile(installedFile, '', 'utf8')
|
||||
}
|
||||
const promise = spawn('foreman', ['start'], { cwd, env })
|
||||
const log = fs.createWriteStream('mastodon.log', { flags: 'a' })
|
||||
// don't bother writing to mastodon.log in CI; we can't read the file anyway
|
||||
const logFile = process.env.CI ? '/dev/null' : 'mastodon.log'
|
||||
const log = fs.createWriteStream(logFile, { flags: 'a' })
|
||||
childProc = promise.childProcess
|
||||
childProc.stdout.pipe(log)
|
||||
childProc.stderr.pipe(log)
|
||||
|
@ -123,7 +30,7 @@ async function runMastodon () {
|
|||
|
||||
async function main () {
|
||||
await cloneMastodon()
|
||||
await setupMastodonDatabase()
|
||||
await installMastodon()
|
||||
await runMastodon()
|
||||
await waitForMastodonApiToStart()
|
||||
await restoreMastodonData()
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$COMMAND" = deploy-all-travis || "$COMMAND" = test-unit ]]; then
|
||||
exit 0 # no need to setup mastodon in this case
|
||||
fi
|
||||
|
||||
# install ruby
|
||||
source "$HOME/.rvm/scripts/rvm"
|
||||
rvm install 2.6.1
|
||||
rvm use 2.6.1
|
||||
|
||||
# check versions
|
||||
ruby --version
|
||||
node --version
|
||||
yarn --version
|
||||
postgres --version
|
||||
redis-server --version
|
||||
ffmpeg -version
|
|
@ -1,6 +1,8 @@
|
|||
module.exports = [
|
||||
export default [
|
||||
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
|
||||
{ id: 'fa-arrow-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/arrow-left.svg' },
|
||||
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
|
||||
{ id: 'fa-bell-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell-o.svg' },
|
||||
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },
|
||||
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
|
||||
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg', inline: true },
|
||||
|
@ -22,6 +24,7 @@ module.exports = [
|
|||
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
|
||||
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
|
||||
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg', inline: true },
|
||||
{ id: 'fa-comment', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comment.svg' },
|
||||
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg', inline: true },
|
||||
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
|
||||
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
|
||||
|
@ -55,5 +58,6 @@ module.exports = [
|
|||
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
|
||||
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' },
|
||||
{ id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' },
|
||||
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' }
|
||||
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' },
|
||||
{ id: 'fa-bookmark', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg' }
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module.exports = {
|
||||
export default {
|
||||
ecma: 8,
|
||||
mangle: true,
|
||||
compress: {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# In CI, we need to make sure the vercel.json file is built correctly,
|
||||
# or else it will mess up the deployment to Vercel
|
||||
|
||||
if ! diff -q /tmp/vercel-old.json ./vercel.json &>/dev/null; then
|
||||
diff /tmp/vercel-old.json ./vercel.json
|
||||
echo "vercel.json changed, run yarn build and make sure everything looks okay"
|
||||
exit 1
|
||||
fi
|
|
@ -1,5 +1,6 @@
|
|||
import fetch from 'node-fetch'
|
||||
import { actions } from './mastodon-data'
|
||||
import { actions } from './mastodon-data.js'
|
||||
import esMain from 'es-main'
|
||||
|
||||
const numStatuses = actions
|
||||
.map(_ => _.post || _.boost)
|
||||
|
@ -26,7 +27,7 @@ async function waitForMastodonData () {
|
|||
console.log('Mastodon data populated')
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (esMain(import.meta)) {
|
||||
waitForMastodonData().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import fetch from 'node-fetch'
|
||||
import esMain from 'es-main'
|
||||
|
||||
export async function waitForMastodonUiToStart () {
|
||||
while (true) {
|
||||
|
@ -30,7 +31,7 @@ export async function waitForMastodonApiToStart () {
|
|||
console.log('Mastodon API started up')
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (esMain(import.meta)) {
|
||||
Promise.resolve()
|
||||
.then(waitForMastodonApiToStart)
|
||||
.then(waitForMastodonUiToStart).catch(err => {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
version: "3"
|
||||
services:
|
||||
pinafore:
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: pinafore:latest
|
||||
ports:
|
||||
- 4002:4002
|
|
@ -5,10 +5,11 @@ This guide is for instance admins who may be having trouble with Pinafore compat
|
|||
|
||||
By default, [Mastodon allows cross-origin access to the `/api` endpoint](https://github.com/tootsuite/mastodon/blob/50529cbceb84e611bca497624a7a4c38113e5135/config/initializers/cors.rb#L15-L20). Thus Pinafore should "just work" for most Mastodon servers.
|
||||
|
||||
If the nginx/Apache settings have been changed, though, then Pinafore might not be able to connect to an instance. To check if the instance is supported, run this command:
|
||||
If the nginx/Apache settings have been changed, though, then Pinafore might not be able to connect to an instance. To check if the instance is supported, run this command (replacing `myinstance.com` with your instance URL):
|
||||
|
||||
```bash
|
||||
curl -sLv example.com/api/v1/instance -o /dev/null
|
||||
curl -sLv -H 'Origin: https://pinafore.social' -o /dev/null \
|
||||
myinstance.com/api/v1/instance
|
||||
```
|
||||
|
||||
If you see this in the output:
|
||||
|
@ -19,7 +20,7 @@ Access-Control-Allow-Origin: *
|
|||
|
||||
Then Pinafore should work as expected!
|
||||
|
||||
Otherwise, if the instance admin would like to whitelist only certain websites (including Pinafore) to work with CORS, then they will need to make sure that the server echoes:
|
||||
Otherwise, if the instance admin would like to whitelist only certain websites (including Pinafore) to work with CORS, then they will need to make sure that the server echoes:
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: https://pinafore.social
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# Architecture
|
||||
|
||||
This document describes some things about the codebase that are worth knowing if you're trying to contribute.
|
||||
Basically think of it as a "lay of the land" as well as "weird unusual stuff that may surprise you."
|
||||
|
||||
## Overview
|
||||
|
||||
Pinafore uses [SvelteJS](https://svelte.technology) v2 and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
||||
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
||||
|
||||
## Why Svelte v2 / Sapper ?
|
||||
|
||||
There is [no upgrade path from Svelte v2 to v3](https://github.com/sveltejs/svelte/issues/2462). Doing so would require manually migrating every component over. And in the end, it would probably not change the UX (user experience) of Pinafore – only the DX (developer experience).
|
||||
|
||||
Similarly, [Sapper would need to be migrated to SvelteKit](https://kit.svelte.dev/docs/migrating). Since Pinafore generates static files, there is probably not much benefit in moving from Sapper to SvelteKit.
|
||||
|
||||
For this reason, Pinafore has been stuck on Svelte v2 and Sapper for a long time. Migrating it is not something I've considered. The [v2 Svelte docs](https://v2.svelte.dev/) are still online, and share many similarities with Svelte v3.
|
||||
|
||||
## 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.
|
||||
|
||||
## emoji-picker-element is loaded as a third-party bundle
|
||||
|
||||
`emoji-picker-element` uses Svelte 3, whereas we use Svelte 2. So it's just imported
|
||||
as a bundled custom element, not as a Svelte component.
|
||||
|
||||
## 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.
|
|
@ -0,0 +1,19 @@
|
|||
# Internationalization
|
||||
|
||||
To contribute or change translations for Pinafore, look in the [src/intl](https://github.com/nolanlawson/pinafore/tree/master/src/intl) directory. Create a new file or edit an existing file based on its [two-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and optionally, a region. For instance, `en-US.js` is American English, and `fr.js` is French.
|
||||
|
||||
The default is `en-US.js`, and any strings not defined in a language file will fall back to the strings from that file.
|
||||
|
||||
There is also an `intl/emoji-picker` directory, which contains translations for [emoji-picker-element](https://github.com/nolanlawson/emoji-picker-element)
|
||||
(which already comes with English built-in).
|
||||
|
||||
Note that internationalization is currently experimental. Client-side locale switching is not supported – when you build
|
||||
the instance of Pinafore, it is either one language or another. To build in a particular language, use (for example):
|
||||
|
||||
LOCALE=fr yarn build
|
||||
|
||||
or
|
||||
|
||||
LOCALE=fr yarn dev
|
||||
|
||||
To host a localized version of Pinafore using Vercel, you can see this example: [buildCommand in vercel.json for Spanish](https://github.com/nvdaes/vercelPinafore/blob/45c70fb2088fe5f2380a729dab83e6f3ab4e6291/vercel.json#L9).
|
181
package.json
181
package.json
|
@ -1,129 +1,138 @@
|
|||
{
|
||||
"name": "pinafore",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "1.15.8",
|
||||
"version": "2.6.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0 || ^20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
||||
"dev": "run-s build-template-html build-assets serve-dev",
|
||||
"dev": "run-s before-build serve-dev",
|
||||
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
|
||||
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
|
||||
"before-build": "run-s build-template-html build-assets",
|
||||
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper dev",
|
||||
"before-build": "run-p build-template-html build-assets build-webpack-config",
|
||||
"build": "cross-env NODE_ENV=production run-s build-steps",
|
||||
"build-steps": "run-s before-build sapper-export build-now-json",
|
||||
"sapper-build": "sapper build",
|
||||
"build-steps": "run-s before-build sapper-export build-vercel-json",
|
||||
"sapper-build": "cross-env WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper build",
|
||||
"start": "node server.js",
|
||||
"build-and-start": "run-s build start",
|
||||
"build-template-html": "node -r esm ./bin/build-template-html.js",
|
||||
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
|
||||
"build-assets": "node -r esm ./bin/build-assets.js",
|
||||
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
|
||||
"build-template-html": "node ./bin/build-template-html.js",
|
||||
"build-template-html-watch": "node ./bin/build-template-html.js --watch",
|
||||
"build-assets": "node ./bin/build-assets.js",
|
||||
"build-webpack-config": "rollup -c ./webpack/rollup.config.js",
|
||||
"clone-mastodon": "node ./bin/clone-mastodon.js",
|
||||
"install-mastodon": "node ./bin/install-mastodon.js",
|
||||
"run-mastodon": "node ./bin/run-mastodon.js",
|
||||
"test": "cross-env BROWSER=chrome:headless run-s test-browser",
|
||||
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
|
||||
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
|
||||
"test-browser-suite0": "run-p --race run-mastodon build-and-start test-mastodon-suite0",
|
||||
"test-mastodon-suite0": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite0",
|
||||
"test-browser-suite1": "run-p --race run-mastodon build-and-start test-mastodon-suite1",
|
||||
"test-mastodon-suite1": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite1",
|
||||
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
|
||||
"testcafe-suite0": "cross-env-shell testcafe -c 4 $BROWSER tests/spec/0*",
|
||||
"testcafe-suite0": "cross-env-shell testcafe -c 2 $BROWSER tests/spec/0*",
|
||||
"testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*",
|
||||
"test-unit": "mocha -r esm -r bin/browser-shim.js tests/unit/",
|
||||
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
|
||||
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
|
||||
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
|
||||
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
|
||||
"deploy-all-travis": "./bin/deploy-all-travis.sh",
|
||||
"test-unit": "NODE_ENV=test mocha -r bin/browser-shim.js tests/unit/",
|
||||
"test-in-ci-suite0": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite0",
|
||||
"test-in-ci-suite1": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite1",
|
||||
"test-vercel-json": "run-s test-vercel-json-copy build test-vercel-json-test",
|
||||
"test-vercel-json-copy": "./bin/copy-vercel-json.sh",
|
||||
"test-vercel-json-test": "./bin/test-vercel-json-unchanged.sh",
|
||||
"wait-for-mastodon-to-start": "node bin/wait-for-mastodon-to-start.js",
|
||||
"wait-for-mastodon-data": "node bin/wait-for-mastodon-data.js",
|
||||
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
|
||||
"sapper-export": "cross-env PORT=22939 sapper export",
|
||||
"sapper-export": "cross-env PORT=22939 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper export",
|
||||
"print-export-info": "node ./bin/print-export-info.js",
|
||||
"export-steps": "run-s before-build sapper-export print-export-info",
|
||||
"export": "cross-env NODE_ENV=production run-s export-steps",
|
||||
"now-build": "run-s export",
|
||||
"build-now-json": "node -r esm ./bin/build-now-json.js"
|
||||
"build-vercel-json": "node bin/build-vercel-json.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.8.6",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.6",
|
||||
"@babel/runtime": "^7.8.4",
|
||||
"@rollup/plugin-replace": "^2.3.0",
|
||||
"@webcomponents/custom-elements": "^1.4.0",
|
||||
"arrow-key-navigation": "^1.1.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"blurhash": "^1.1.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"@formatjs/intl-listformat": "^7.1.3",
|
||||
"@formatjs/intl-locale": "^3.0.7",
|
||||
"@formatjs/intl-pluralrules": "^5.1.4",
|
||||
"@formatjs/intl-relativetimeformat": "^11.1.4",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^14.1.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@stdlib/utils-noop": "^0.0.13",
|
||||
"arrow-key-navigation": "^1.2.0",
|
||||
"blurhash": "^1.1.5",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^3.3.1",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"clean-css": "^4.2.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.0",
|
||||
"country-flag-emoji-polyfill": "^0.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-dedoupe": "^0.1.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"emoji-mart": "nolanlawson/emoji-mart#8bb6fb6",
|
||||
"emoji-regex": "^8.0.0",
|
||||
"encoding": "^0.1.12",
|
||||
"emoji-picker-element": "^1.13.1",
|
||||
"emoji-picker-element-data": "^1.3.0",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"encoding": "^0.1.13",
|
||||
"es-main": "^1.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"esm": "^3.2.25",
|
||||
"events-light": "^1.0.5",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.18.2",
|
||||
"file-api": "^0.10.4",
|
||||
"file-drop-element": "0.2.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"form-data": "^3.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"indexeddb-getall-shim": "^1.3.6",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"intl": "^1.2.5",
|
||||
"file-drop-element": "^1.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"focus-visible": "^5.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"format-message-interpret": "^6.2.4",
|
||||
"format-message-parse": "^6.2.4",
|
||||
"glob": "^7.2.0",
|
||||
"is-emoji-supported": "^0.0.5",
|
||||
"li": "^1.3.0",
|
||||
"localstorage-memory": "^1.0.3",
|
||||
"lodash-es": "^4.17.15",
|
||||
"lodash-webpack-plugin": "^0.11.5",
|
||||
"mkdirp": "^1.0.3",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-fetch": "^2.6.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-any": "^3.0.0",
|
||||
"p-any": "^4.0.0",
|
||||
"page-lifecycle": "^0.1.2",
|
||||
"performance-now": "^2.1.0",
|
||||
"pinch-zoom-element": "^1.1.1",
|
||||
"preact": "^10.3.3",
|
||||
"promise-worker": "^2.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"rollup": "^2.0.0",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-terser": "^5.2.0",
|
||||
"sapper": "nolanlawson/sapper#for-pinafore-14",
|
||||
"rollup": "^2.67.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rtl-detect": "^1.0.4",
|
||||
"safari-14-idb-fix": "^1.0.4",
|
||||
"sapper": "nolanlawson/sapper#for-pinafore-26",
|
||||
"sass": "^1.56.1",
|
||||
"stringz": "^2.1.0",
|
||||
"svelte": "^2.16.1",
|
||||
"svelte-extras": "^2.0.2",
|
||||
"svelte-loader": "^2.13.6",
|
||||
"svelte-transitions": "^1.2.0",
|
||||
"svgo": "^1.3.2",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"tesseract.js": "^2.0.2",
|
||||
"tesseract.js-core": "^2.0.0",
|
||||
"svgo": "^2.8.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"tesseract.js": "^2.1.5",
|
||||
"tesseract.js-core": "^2.2.0",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"worker-loader": "^2.0.0"
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assert": "^2.0.0",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"mocha": "^7.1.0",
|
||||
"now": "^17.0.4",
|
||||
"standard": "^14.3.1",
|
||||
"testcafe": "^1.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"globby": "^13.1.2",
|
||||
"husky": "^8.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"mocha": "^10.1.0",
|
||||
"standard": "^17.0.0",
|
||||
"testcafe": "^1.20.1"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"webpack/*.cjs"
|
||||
],
|
||||
"globals": [
|
||||
"AbortController",
|
||||
"Blob",
|
||||
|
@ -133,6 +142,7 @@
|
|||
"Element",
|
||||
"Event",
|
||||
"FormData",
|
||||
"HTMLElement",
|
||||
"IDBKeyRange",
|
||||
"IDBObjectStore",
|
||||
"Image",
|
||||
|
@ -163,6 +173,7 @@
|
|||
"matchMedia",
|
||||
"performance",
|
||||
"postMessage",
|
||||
"queueMicrotask",
|
||||
"requestAnimationFrame",
|
||||
"requestIdleCallback",
|
||||
"self",
|
||||
|
@ -173,12 +184,6 @@
|
|||
"mode": "auto",
|
||||
"cjs": "vars"
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"sapper",
|
||||
"svelte"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nolanlawson/pinafore.git"
|
||||
|
@ -189,5 +194,13 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/nolanlawson/pinafore/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nolanlawson/pinafore#readme"
|
||||
"homepage": "https://github.com/nolanlawson/pinafore#readme",
|
||||
"lint-staged": {
|
||||
"*.js": "standard --fix",
|
||||
"*.html": "standard --fix --plugin html 'src/routes/**/*.html'"
|
||||
},
|
||||
"volta": {
|
||||
"node": "14.21.1",
|
||||
"yarn": "1.22.19"
|
||||
}
|
||||
}
|
||||
|
|
17
server.js
17
server.js
|
@ -1,15 +1,22 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import compression from 'compression'
|
||||
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
const compression = require('compression')
|
||||
const { routes: nowRoutes } = require('./now.json')
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
|
||||
// JSON files not supported in ESM yet
|
||||
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-import-json
|
||||
const vercelJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'vercel.json'), 'utf8'))
|
||||
|
||||
const { routes: rawRoutes } = vercelJson
|
||||
|
||||
const { PORT = 4002 } = process.env
|
||||
const app = express()
|
||||
const exportDir = path.resolve(__dirname, '__sapper__/export')
|
||||
|
||||
const routes = nowRoutes.map(({ src, headers, dest }) => ({
|
||||
const routes = rawRoutes.map(({ src, headers, dest }) => ({
|
||||
regex: new RegExp(src),
|
||||
headers,
|
||||
dest
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
{
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#4169e1",
|
||||
"name": "{intl.longAppName}",
|
||||
"short_name": "{intl.appName}",
|
||||
"description": "{intl.appDescription}",
|
||||
"categories": [
|
||||
"social"
|
||||
],
|
||||
"display": "standalone",
|
||||
"start_url": "/?pwa=true",
|
||||
"share_target": {
|
||||
"action": "/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "file",
|
||||
"accept": [
|
||||
"audio/*",
|
||||
"image/*",
|
||||
"video/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-44.png",
|
||||
"sizes": "44x44",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-50.png",
|
||||
"sizes": "50x50",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-150.png",
|
||||
"sizes": "150x150",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-48-maskable.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-72-maskable.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-96-maskable.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-144-maskable.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-192-maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-512-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "{intl.newStatus}",
|
||||
"url": "/?pwa=true&compose=true",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-shortcut-fa-pencil.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "{intl.notifications}",
|
||||
"url": "/notifications?pwa=true",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-shortcut-fa-bell.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "screenshot-540-720-1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720"
|
||||
},
|
||||
{
|
||||
"src": "screenshot-540-720-2.png",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "540x720"
|
||||
},
|
||||
{
|
||||
"src": "screenshot-540-720-3.png",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "540x720"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,33 +1,93 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="{process.env.LOCALE}" dir="{process.env.LOCALE_DIRECTION}">
|
||||
<head>
|
||||
<meta charset='utf-8' >
|
||||
<meta name="viewport" content="width=device-width, viewport-fit=cover">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta id='theThemeColor' name='theme-color' content='#4169e1' >
|
||||
<meta name="description" content="An alternative web client for Mastodon, focused on speed and simplicity." >
|
||||
<meta name="description" content="{intl.appDescription}" >
|
||||
|
||||
%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" href="/apple-icon.png" >
|
||||
<!-- both of these *-web-app-capable are required, for Chrome on Android and Safari on iOS
|
||||
https://developers.google.com/web/fundamentals/native-hardware/fullscreen/ -->
|
||||
<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" >
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="{intl.appName}">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<!-- splashscreen for iOS -->
|
||||
<link href="/iphone5_splash.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/iphone6_splash.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/iphoneplus_splash.png" media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
|
||||
<link href="/iphonex_splash.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
|
||||
<link href="/iphonexr_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/iphonexsmax_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
|
||||
<link href="/ipad_splash.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/ipadpro1_splash.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/ipadpro3_splash.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link href="/ipadpro2_splash.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
|
||||
<link rel="me" href="https://fosstodon.org/@pinafore">
|
||||
|
||||
<!-- inline CSS -->
|
||||
|
||||
<style id="theBottomNavStyle" media="only x">
|
||||
:root {
|
||||
--nav-top: calc(100dvh - var(--nav-total-height));
|
||||
--nav-bottom: initial;
|
||||
--main-content-pad-top: 0px;
|
||||
--main-content-pad-bottom: var(--main-content-pad-vertical);
|
||||
--toast-gap-bottom: var(--nav-total-height);
|
||||
--fab-gap-top: 0px;
|
||||
}
|
||||
@supports not (height: 1dvh) {
|
||||
/* In browsers that don't support dvh, use the large-small-dynamic-viewport-units-polyfill */
|
||||
:root {
|
||||
--nav-top: calc((100 * var(--1dvh)) - var(--nav-total-height));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style id="theGrayscaleStyle" media="only x">
|
||||
/* Firefox doesn't seem to like applying filter: grayscale() to
|
||||
* the entire body, so we apply individually.
|
||||
*/
|
||||
img, svg, video,
|
||||
input[type="checkbox"], input[type="radio"],
|
||||
.inline-emoji, .theme-preview, .emoji-mart-emoji, .emoji-mart-skin {
|
||||
.inline-emoji, .theme-preview, .account-profile {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style id="theFocusVisiblePolyfillStyle" media="only x">
|
||||
/* polyfill */
|
||||
/* Note we have to use [data-focus-visible-added] rather than .focus-visible because
|
||||
* Svelte overrides classes */
|
||||
.js-focus-visible :focus:not([data-focus-visible-added]) {
|
||||
outline: none !important; /* important to win the specificity war */
|
||||
}
|
||||
.js-focus-visible :focus:not([data-focus-visible-added]).focus-after::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style id="theFocusVisibleStyle" media="only x">
|
||||
/* standard version */
|
||||
:focus:not(:focus-visible) {
|
||||
outline: none !important; /* important to win the specificity war */
|
||||
}
|
||||
:focus:not(:focus-visible).focus-after::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style id="theCenterNavStyle" media="only x">
|
||||
@media (min-width: 992px) {
|
||||
.main-nav-ul {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.hidden-from-ssr {
|
||||
|
@ -61,6 +121,9 @@
|
|||
<!-- LoadingMask.html gets rendered here -->
|
||||
<div id="loading-mask" aria-hidden="true"></div>
|
||||
|
||||
<!-- announceAriaLivePolite.js gets rendered here -->
|
||||
<div id="theAriaLive" class="sr-only" aria-live="polite"></div>
|
||||
|
||||
<!-- inline SVG -->
|
||||
|
||||
<!-- Sapper creates a <script> tag containing `templates/client.js`
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import './routes/_thirdparty/regenerator-runtime/runtime.js'
|
||||
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'
|
||||
import './routes/_utils/forceOnline'
|
||||
import './routes/_utils/serviceWorkerClient.js'
|
||||
import './routes/_utils/historyEvents.js'
|
||||
import './routes/_utils/loadingMask.js'
|
||||
import './routes/_utils/forceOnline.js'
|
||||
import { mark, stop } from './routes/_utils/marks.js'
|
||||
import { loadPolyfills } from './routes/_utils/polyfills/loadPolyfills.js'
|
||||
import { loadNonCriticalPolyfills } from './routes/_utils/polyfills/loadNonCriticalPolyfills.js'
|
||||
import idbReady from 'safari-14-idb-fix/dist/esm'
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
console.log('init()')
|
||||
Promise.all([idbReady(), loadPolyfills()]).then(() => {
|
||||
mark('sapperStart')
|
||||
sapper.start({ target: document.querySelector('#sapper') })
|
||||
stop('sapperStart')
|
||||
/* no await */ loadNonCriticalPolyfills()
|
||||
})
|
||||
|
||||
console.log('process.env.NODE_ENV', process.env.NODE_ENV)
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
if (import.meta.webpackHot) {
|
||||
import.meta.webpackHot.accept()
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
||||
// the build process and write it to checksum.js.
|
||||
|
||||
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
|
||||
import { basename } from '../routes/_api/utils'
|
||||
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
|
||||
import { storeLite } from '../routes/_store/storeLite'
|
||||
import { isIOSPre12Point2 } from '../routes/_utils/userAgent/isIOSPre12Point2'
|
||||
import { isMac } from '../routes/_utils/userAgent/isMac'
|
||||
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine.js'
|
||||
import { basename } from '../routes/_api/utils.js'
|
||||
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut.js'
|
||||
import { storeLite } from '../routes/_store/storeLite.js'
|
||||
import { isIOSPre12Point2 } from '../routes/_utils/userAgent/isIOSPre12Point2.js'
|
||||
import { isMac } from '../routes/_utils/userAgent/isMac.js'
|
||||
|
||||
window.__themeColors = process.env.THEME_COLORS
|
||||
|
||||
|
@ -16,9 +16,11 @@ const {
|
|||
currentInstance,
|
||||
instanceThemes,
|
||||
disableCustomScrollbars,
|
||||
bottomNav,
|
||||
enableGrayscale,
|
||||
pushSubscription,
|
||||
loggedInInstancesInOrder
|
||||
loggedInInstancesInOrder,
|
||||
centerNav
|
||||
} = storeLite.get()
|
||||
|
||||
const theme = (instanceThemes && instanceThemes[currentInstance]) || DEFAULT_THEME
|
||||
|
@ -32,12 +34,13 @@ if (currentInstance) {
|
|||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
if (theme !== INLINE_THEME) {
|
||||
if (theme !== INLINE_THEME || enableGrayscale) {
|
||||
// switch theme ASAP to minimize flash of default theme
|
||||
switchToTheme(theme, enableGrayscale)
|
||||
}
|
||||
|
||||
if (enableGrayscale) {
|
||||
// set the grayscale style on every img, svg, etc.
|
||||
document.getElementById('theGrayscaleStyle')
|
||||
.setAttribute('media', 'all') // enables the style
|
||||
}
|
||||
|
@ -52,6 +55,16 @@ if (disableCustomScrollbars) {
|
|||
.setAttribute('media', 'only x') // disables the style
|
||||
}
|
||||
|
||||
if (bottomNav) {
|
||||
document.getElementById('theBottomNavStyle')
|
||||
.setAttribute('media', 'all') // enables the style
|
||||
}
|
||||
|
||||
if (centerNav) {
|
||||
document.getElementById('theCenterNavStyle')
|
||||
.setAttribute('media', 'all') // enables the style
|
||||
}
|
||||
|
||||
// hack to make the scrollbars rounded only on macOS
|
||||
if (isMac()) {
|
||||
document.documentElement.style.setProperty('--scrollbar-border-radius', '50px')
|
||||
|
|
|
@ -0,0 +1,620 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'Ein alternativer Web Client für Mastodon, der auf Geschwindigkeit und einfache Bedienung ausgelegt ist.',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore ist ein Web Client für
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
der für Geschwindigkeit und einfache Bedienung konzipiert wurde.
|
||||
</p>
|
||||
<p>
|
||||
Lies den
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">einführenden Blogbeitrag</a> (auf englisch)
|
||||
oder lege los, indem Du Dich bei einer Instanz anmeldest:
|
||||
</p>`,
|
||||
logIn: 'Anmelden',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore ist
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">quelloffene Software</a>,
|
||||
erstellt von <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
und verteilt unter der
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL Lizenz</a>.
|
||||
Hier ist die <a href="/settings/about#privacy-policy" rel="prefetch">Datenschutzerklärung</a>.
|
||||
</p>
|
||||
`,
|
||||
// Generic UI
|
||||
loading: 'Wird geladen',
|
||||
okay: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
alert: 'Hinweis',
|
||||
close: 'Schließen',
|
||||
error: 'Fehler: {error}',
|
||||
errorShort: 'Fehler:',
|
||||
// Relative timestamps
|
||||
justNow: 'gerade eben',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(aktuelle Seite)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(eine Benachrichtigung)}
|
||||
other {({count} Benachrichtigungen)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(eine Followeranfrage)}
|
||||
other {({count} Followeranfragen)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Blockierte Benutzer',
|
||||
bookmarks: 'Lesezeichen',
|
||||
directMessages: 'Direktnachrichten',
|
||||
favorites: 'Favoriten',
|
||||
federated: 'Föderiert',
|
||||
home: 'Startseite',
|
||||
local: 'Lokal',
|
||||
notifications: 'Benachrichtigungen',
|
||||
mutedUsers: 'Stummgeschaltete Benutzer',
|
||||
pinnedStatuses: 'Angeheftete Tröts',
|
||||
followRequests: 'Followeranfragen',
|
||||
followRequestsLabel: `Followeranfragen {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'Liste',
|
||||
search: 'Suchen',
|
||||
pageHeader: 'Seitenkopf',
|
||||
goBack: 'Zurückgehen',
|
||||
back: 'Zurück',
|
||||
profile: 'Profil',
|
||||
federatedTimeline: 'Föderierte Zeitleiste',
|
||||
localTimeline: 'Lokale Zeitleiste',
|
||||
// community page
|
||||
community: 'Community',
|
||||
pinnableTimelines: 'Zeitleisten, die angeheftet werden können',
|
||||
timelines: 'Zeitleisten',
|
||||
lists: 'Listen',
|
||||
instanceSettings: 'Einstellungen der Instanz',
|
||||
notificationMentions: 'Benachrichtigungen über Erwähnungen',
|
||||
profileWithMedia: 'Profil mit Medien',
|
||||
profileWithReplies: 'Profil mit Antworten',
|
||||
hashtag: 'Hashtag',
|
||||
// not logged in
|
||||
profileNotLoggedIn: 'Hier erscheint Deine Benutzerzeitleiste, wenn Du Dich anmeldest.',
|
||||
bookmarksNotLoggedIn: 'Hier erscheinen Deine Lesezeichen, wenn Du Dich anmeldest.',
|
||||
directMessagesNotLoggedIn: 'Hier erscheinen Deine Direktnachrichten, wenn Du Dich anmeldest.',
|
||||
favoritesNotLoggedIn: 'Hier erscheinen Deine Favoriten, wenn Du Dich anmeldest.',
|
||||
federatedTimelineNotLoggedIn: 'Hier erscheint Deine föderierte Zeitleiste, wenn Du Dich anmeldest.',
|
||||
localTimelineNotLoggedIn: 'Hier erscheint Deine lokale Zeitleiste, wenn Du Dich anmeldest.',
|
||||
searchNotLoggedIn: 'Du kannst eine Suche ausführen, sobald Du bei einer Instanz angemeldet bist.',
|
||||
communityNotLoggedIn: 'Hier erscheinen Community-Optionen, wenn Du Dich anmeldest.',
|
||||
listNotLoggedIn: 'Hier erscheint eine Liste, wenn Du Dich anmeldest.',
|
||||
notificationsNotLoggedIn: 'Hier erscheinen Deine Benachrichtigungen, wenn Du Dich anmeldest.',
|
||||
notificationMentionsNotLoggedIn: 'Hier erscheinen Deine Benachrichtigungen zu Erwähnungen, wenn Du Dich anmeldest.',
|
||||
statusNotLoggedIn: 'Hier erscheint der Faden zu einem Tröt, wenn Du Dich anmeldest.',
|
||||
tagNotLoggedIn: 'Hier erscheinen Tröts zu einem hashtag, wenn Du Dich anmeldest.',
|
||||
// Notification subpages
|
||||
filters: 'Filter',
|
||||
all: 'Alle',
|
||||
mentions: 'Erwähnungen',
|
||||
// Follow requests
|
||||
approve: 'Genehmigen',
|
||||
reject: 'Ablehnen',
|
||||
// Hotkeys
|
||||
hotkeys: 'Tastenkürzel',
|
||||
global: 'Global',
|
||||
timeline: 'Zeitleiste',
|
||||
media: 'Medien',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> gehe zum nächsten fokussierbaren Element</li>
|
||||
<li><kbd>←</kbd> gehe zum vorherigen fokussierbaren Element</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {oder <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
um die Spalten umzuschalten
|
||||
</li>
|
||||
<li><kbd>7</kbd> oder <kbd>c</kbd> zum Erstellen eines neuen Tröts</li>
|
||||
<li><kbd>s</kbd> oder <kbd>/</kbd> zum Suchen</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> zur Startseite gehen</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> zu den Benachrichtigungen gehen</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> zur lokalen zeitleiste gehen</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> zur föderierten Zeitleiste gehen</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> zur Community-Seite gehen</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> zur Seite mit Direktnachrichten gehen</li>
|
||||
<li><kbd>h</kbd> oder <kbd>?</kbd> zum Umschalten des Hilfe-Dialogs</li>
|
||||
<li><kbd>Rückschritttaste</kbd> zurückgehen, Dialogfelder schließen</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> oder <kbd>↓</kbd> nächsten Tröt ansteuern</li>
|
||||
<li><kbd>k</kbd> oder <kbd>↑</kbd> den vorherigen Tröt ansteuern</li>
|
||||
<li><kbd>.</kbd> neue Tröts anzeigen und nach oben scrollen</li>
|
||||
<li><kbd>o</kbd> Tröt öffnen</li>
|
||||
<li><kbd>f</kbd> Tröt favorisieren</li>
|
||||
<li><kbd>b</kbd> Tröt boosten</li>
|
||||
<li><kbd>r</kbd> auf Tröt antworten</li>
|
||||
<li><kbd>i</kbd> Bilder, Videos oder Audio öffnen</li>
|
||||
<li><kbd>y</kbd> sensible Medieninhalte zeigen oder verbergen</li>
|
||||
<li><kbd>m</kbd> den Verfasser erwähnen</li>
|
||||
<li><kbd>p</kbd> das Profil des Verfassers öffnen</li>
|
||||
<li><kbd>l</kbd> den Link des aktuellen Tröts in einem neuen Tab öffnen</li>
|
||||
<li><kbd>x</kbd> den Text hinter der Inhaltswarnung anzeigen oder verbergen</li>
|
||||
<li><kbd>z</kbd> den Text hinter der Inhaltswarnung für alle in dieser Unterhaltung anzeigen oder verbergen</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> zum nächsten oder vorherigen Inhalt gehen</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Aktuell)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(Angeheftete Seite)}
|
||||
other {(Nicht angeheftete Seite)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Hefte {label} an',
|
||||
// Status composition
|
||||
composeStatus: 'Tröt erstellen',
|
||||
postStatus: 'Tröt!',
|
||||
contentWarning: 'Inhaltswarnung',
|
||||
dropToUpload: 'Fallenlassen zum Hochladen',
|
||||
invalidFileType: 'Ungültiger Dateityp',
|
||||
composeLabel: "Was gibt's Neues?",
|
||||
autocompleteDescription: 'Autovervollständigungsergebnisse verfügbar. Drücke die Pfeiltasten auf und ab und dann Eingabe zum Auswählen.',
|
||||
mediaUploads: 'Medien-Uploads',
|
||||
edit: 'Bearbeiten',
|
||||
delete: 'Löschen',
|
||||
description: 'Beschreibung',
|
||||
descriptionLabel: 'Beschreibe Bilder oder Videos für Menschen mit Seheinschränkungen, oder Audio- oder Videoinhalte für Menschen mit Höreinschränkungen',
|
||||
markAsSensitive: 'Medien als sensibel kennzeichnen',
|
||||
// Polls
|
||||
createPoll: 'Umfrage erstellen',
|
||||
removePollChoice: 'Auswahl {index} entfernen',
|
||||
pollChoiceLabel: 'Auswahl {index}',
|
||||
multipleChoice: 'Mehrfachauswahl',
|
||||
pollDuration: 'Dauer der Umfrage',
|
||||
fiveMinutes: '5 Minuten',
|
||||
thirtyMinutes: '30 Minuten',
|
||||
oneHour: '1 Stunde',
|
||||
sixHours: '6 Stunden',
|
||||
oneDay: '1 Tag',
|
||||
threeDays: '3 Tage',
|
||||
sevenDays: '7 Tage',
|
||||
addEmoji: 'Emoji einfügen',
|
||||
addMedia: 'Medien einfügen (Bilder, video, audio)',
|
||||
addPoll: 'Umfrage hinzufügen',
|
||||
removePoll: 'Umfrage entfernen',
|
||||
postPrivacyLabel: 'Privatsphäre anpassen (aktuell {label})',
|
||||
addContentWarning: 'Inhaltswarnung hinzufügen',
|
||||
removeContentWarning: 'Inhaltswarnung entfernen',
|
||||
altLabel: 'Beschreibe für Menschen mit Seheinschränkungen',
|
||||
extractText: 'Text aus Bild ermitteln',
|
||||
extractingText: 'Erkenne Text',
|
||||
extractingTextCompletion: 'Erkenne Text ({percent}% abgeschlossen)…',
|
||||
unableToExtractText: 'Es konnte kein Text erkannt werden.',
|
||||
// Account options
|
||||
followAccount: 'Folge {account}',
|
||||
unfollowAccount: 'Entfolge {account}',
|
||||
blockAccount: 'Blockiere {account}',
|
||||
unblockAccount: 'Blockieren von {account} aufheben',
|
||||
muteAccount: 'Schalte {account} stumm',
|
||||
unmuteAccount: 'Hebe Stummschaltung von {account} auf',
|
||||
showReblogsFromAccount: 'Boosts von {account} anzeigen',
|
||||
hideReblogsFromAccount: 'Boosts von {account} verbergen',
|
||||
showDomain: 'Verbergen von {domain} aufheben',
|
||||
hideDomain: 'Verbirg {domain}',
|
||||
reportAccount: 'Melde {account}',
|
||||
mentionAccount: 'Erwähne {account}',
|
||||
copyLinkToAccount: 'Kopiere Link zu account',
|
||||
copiedToClipboard: 'In Zwischenablage kopiert',
|
||||
// Media dialog
|
||||
navigateMedia: 'Durch Medieninhalte navigieren',
|
||||
showPreviousMedia: 'Vorherigen Medieninhalt zeigen',
|
||||
showNextMedia: 'Nächsten Medieninhalt zeigen',
|
||||
enterPinchZoom: 'Modus Ziehen zum Zoomen',
|
||||
exitPinchZoom: 'Modus Ziehen zum Zoomen beenden',
|
||||
showMedia: `Zeige {index, select,
|
||||
1 {ersten}
|
||||
2 {zweiten}
|
||||
3 {dritten}
|
||||
other {vierten}
|
||||
} Medieninhalt {current, select,
|
||||
true {(aktuell)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Vorschau (Hauptausschnitt)',
|
||||
enterFocalPoint: 'Koordinaten des Hauptausschnitts des Medieninhalts eingeben (X, Y)',
|
||||
muteNotifications: 'Auch Benachrichtigungen stummschalten',
|
||||
muteAccountConfirm: '{account} stummschalten?',
|
||||
mute: 'Stummschalten',
|
||||
unmute: 'Stummschaltung aufheben',
|
||||
zoomOut: 'Herauszoomen',
|
||||
zoomIn: 'Hineinzoomen',
|
||||
// Reporting
|
||||
reportingLabel: 'Du machst eine Meldung von {account} an die Moderatoren von {instance}.',
|
||||
additionalComments: 'Zusätzliche Kommentare',
|
||||
forwardDescription: 'Auch an die Moderatoren von {instance} weiterleiten?',
|
||||
forwardLabel: 'An {instance} weiterleiten',
|
||||
unableToLoadStatuses: 'Kann neueste Tröts nicht laden: {error}',
|
||||
report: 'Melden',
|
||||
noContent: '(Keine Inhalte)',
|
||||
noStatuses: 'Keine Tröts zum Melden vorhanden',
|
||||
// Status options
|
||||
unpinFromProfile: 'Vom Profil abheften',
|
||||
pinToProfile: 'An Profil anheften',
|
||||
muteConversation: 'Unterhaltung stummschalten',
|
||||
unmuteConversation: 'Stummschaltung der Unterhaltung aufheben',
|
||||
bookmarkStatus: 'Tröt als Lesezeichen speichern',
|
||||
unbookmarkStatus: 'Tröt aus Lesezeichen entfernen',
|
||||
deleteAndRedraft: 'Löschen und neu eingeben',
|
||||
reportStatus: 'Tröt melden',
|
||||
shareStatus: 'Tröt teilen',
|
||||
copyLinkToStatus: 'Link zum Tröt kopieren',
|
||||
// Account profile
|
||||
profileForAccount: 'Profil für {account}',
|
||||
statisticsAndMoreOptions: 'Statistiken und weitere Optionen',
|
||||
statuses: 'Tröts',
|
||||
follows: 'Folgt',
|
||||
followers: 'Folgende',
|
||||
moreOptions: 'Weitere Optionen',
|
||||
followersLabel: 'Gefolgt von {count}',
|
||||
followingLabel: 'Folgt {count}',
|
||||
followLabel: `Folgen {requested, select,
|
||||
true {(Followeranfrage gestellt)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Entfolgen {requested, select,
|
||||
true {(Followeranfrage gestellt)}
|
||||
other {}
|
||||
}`,
|
||||
unblock: 'Blockade aufheben',
|
||||
nameAndFollowing: 'Name und folgt',
|
||||
clickToSeeAvatar: 'Klicke zum Anzeigen des Avatars',
|
||||
opensInNewWindow: '{label} (öffnet in neuem Fenster)',
|
||||
blocked: 'Blockiert',
|
||||
domainHidden: 'Domain verborgen',
|
||||
muted: 'Stummgeschaltet',
|
||||
followsYou: 'Folgt Dir',
|
||||
avatarForAccount: 'Avatar für {account}',
|
||||
fields: 'Felder',
|
||||
accountHasMoved: '{account} ist umgezogen:',
|
||||
profilePageForAccount: 'Profilseite für {account}',
|
||||
// About page
|
||||
about: 'Über',
|
||||
aboutApp: 'Über Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore ist
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">freie und quelloffene Software</a>
|
||||
erstellt von
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
und verteilt unter der
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Datenschutzerklärung</h2>
|
||||
|
||||
<p>
|
||||
Pinafore speichert keine persönlichen Informationen auf seinen Servern,
|
||||
einschließlich, aber nicht beschränkt auf, Namen, E-Mail-Adressen,
|
||||
IP-Adressen, Beiträgen, und Fotos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore ist eine statische Seite. Alle Daten werden lokal in Ihrem Browser gespeichert und mit den Instanzen des Fediversums geteilt, zu denen Sie sich verbinden.
|
||||
</p>
|
||||
|
||||
<h2>Mitwirkende</h2>
|
||||
|
||||
<p>
|
||||
Icons provided by <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Logo thanks to "sailboat" by Gregor Cresnar from
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Einstellungen',
|
||||
general: 'Allgemein',
|
||||
generalSettings: 'Allgemeine Einstellungen',
|
||||
showSensitive: 'Sensible Inhalte standardmäßig anzeigen',
|
||||
showPlain: 'Eine graue Fläche für sensible Inhalte anzeigen',
|
||||
allSensitive: 'Alle Medien als sensibel behandeln',
|
||||
largeMedia: 'Große eingebettete Bilder und Videos anzeigen',
|
||||
autoplayGifs: 'Animierte Gifs automatisch abspielen',
|
||||
hideCards: 'Linkvorschauen verbergen',
|
||||
underlineLinks: 'Links in Tröts und Profilen unterstreichen',
|
||||
accessibility: 'Barrierefreiheit',
|
||||
reduceMotion: 'Bewegung in Animationen der Oberfläche reduzieren',
|
||||
disableTappable: 'Berührungsempfindlichkeit auf ganzem Tröt deaktivieren',
|
||||
removeEmoji: 'Emoji aus Anzeigenamen der Benutzer entfernen',
|
||||
shortAria: 'Verkürzte aria-label für Artikel verwenden',
|
||||
theme: 'Design',
|
||||
themeForInstance: 'Design für {instance}',
|
||||
disableCustomScrollbars: 'Angepasste Rollbalken deaktivieren',
|
||||
preferences: 'Vorlieben',
|
||||
hotkeySettings: 'Einstellungen für Tastenkürzel',
|
||||
disableHotkeys: 'Alle Tastenkürzel deaktivieren',
|
||||
leftRightArrows: 'Linke und rechte Pfeiltasten schalten den Fokus um anstatt der Spalten oder Medien',
|
||||
guide: 'Anleitung',
|
||||
reload: 'Neu laden',
|
||||
// Wellness settings
|
||||
wellness: 'Wohlbefinden',
|
||||
wellnessSettings: 'Einstellungen für ein gutes Wohlbefinden',
|
||||
wellnessDescription: `Die Einstellungen fürs Wohlbefinden dienen dazu, die süchtig machenden oder Angst induzierenden Aspekte von Social Media zu reduzieren.
|
||||
Nimm hier Einstellungen vor, die Dir gut tun.`,
|
||||
enableAll: 'Alle einschalten',
|
||||
metrics: 'Messungen',
|
||||
hideFollowerCount: 'Verbirg Anzahl Folgender (ab 10 gedeckelt)',
|
||||
hideReblogCount: 'Verbirg Anzahl der Boosts',
|
||||
hideFavoriteCount: 'Verbirg Anzahl Favorisierungen',
|
||||
hideUnread: 'Verbirg Anzeige ungelesener Benachrichtigungen (z.B. den roten Punkt)',
|
||||
ui: 'Benutzeroberfläche',
|
||||
grayscaleMode: 'Graustufenmodus',
|
||||
wellnessFooter: `Diese Einstellungen basieren zum Teil auf Richtlinien des
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'Du kannst die Einstellungen für Benachrichtigungen in den',
|
||||
filterNotificationsText: 'Instanzeinstellungen',
|
||||
filterNotificationsPost: 'anpassen.',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: '',
|
||||
disableInfiniteScrollText: 'Unendliches Scrollen',
|
||||
disableInfiniteScrollDescription: 'Wenn unendliches Scrollen deaktiviert ist, erscheinen neue Tröts nicht automatisch am unteren oder oberen Ende der zeitleiste. Stattdessen kannst Du weitere Inhalte durch dafür vorgesehene Schaltflächen nachladen.',
|
||||
disableInfiniteScrollPost: 'deaktivieren',
|
||||
// Instance settings
|
||||
loggedInAs: 'Eingeloggt als',
|
||||
homeTimelineFilters: 'Filter für Startzeitleiste',
|
||||
notificationFilters: 'Filter für Benachrichtigungen',
|
||||
pushNotifications: 'Push-Benachrichtigungen',
|
||||
// Add instance page
|
||||
storageError: 'Es sieht so aus als ob Pinafore lokal keine Daten speichern kann. Ist Dein Browser im privaten Modus oder blockiert Cookies? Pinafore speichert alle Daten lokal und braucht zum ordnungsgemäßen Betrieb LocalStorage und IndexedDB.',
|
||||
javaScriptError: 'Du musst zum Einloggen javaScript einschalten.',
|
||||
enterInstanceName: 'Namen der Instanz eingeben',
|
||||
instanceColon: 'Instanz:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: 'Hast Du noch keine',
|
||||
getAnInstanceText: 'instanz',
|
||||
getAnInstanceDescription: 'Eine Instanz ist Deine Heimat auf Mastodon, wie z.B. mastodon.social oder cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: 'Tritt Mastodon bei!',
|
||||
instancesYouveLoggedInTo: 'Instanzen, in denen Du angemeldet bist:',
|
||||
addAnotherInstance: 'Eine weitere Instanz hinzufügen',
|
||||
youreNotLoggedIn: 'Du bist bei keiner Instanz angemeldet.',
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(jetzige Instanz)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Melde Dich bei einer Instanz an',
|
||||
logInToAnInstancePost: 'um Pinafore zu verwenden.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Immer einen',
|
||||
showRingText: 'Fokusring',
|
||||
showRingDescription: 'Der Fokusring ist der Rahmen, der um das fokussierte Element angezeigt wird, wenn Du mit der Tastatur navigierst. Standardmäßig wird er nicht angezeigt, wenn Du die maus oder einen Touchscreen verwendest. Hier kannst du einstellen, dass er immer angezeigt wird.',
|
||||
showRingPost: 'anzeigen',
|
||||
instances: 'Instanzen',
|
||||
addInstance: 'Instanz hinzufügen',
|
||||
homeTimelineFilterSettings: 'Einstellungen für die Filterung der Startzeitleiste',
|
||||
showReblogs: 'Boosts zeigen',
|
||||
showReplies: 'Antworten zeigen',
|
||||
switchOrLogOut: 'Zu dieser Instanz wechseln oder sich von ihr abmelden',
|
||||
switchTo: 'Zu dieser Instanz wechseln',
|
||||
switchToInstance: 'Zu Instanz wechseln',
|
||||
switchToNameOfInstance: 'Wechsle zu {instance}',
|
||||
logOut: 'Abmelden',
|
||||
logOutOfInstanceConfirm: 'Von {instance} abmelden?',
|
||||
notificationFilterSettings: 'Einstellungen für die Filterung von Benachrichtigungen',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: 'Dein Browser unterstützt keine Push-Benachrichtigungen.',
|
||||
deniedPush: 'Du hast es abgelehnt, Push-Benachrichtigungen anzuzeigen.',
|
||||
pushNotificationsNote: 'Beachte, dass Du nur für jeweils eine Instanz Push-Benachrichtigungen anzeigen lassen kannst.',
|
||||
pushSettings: 'Einstellungen für Push-Benachrichtigungen',
|
||||
newFollowers: 'Neue Folgende',
|
||||
reblogs: 'Boosts',
|
||||
pollResults: 'Umfrageergebnisse',
|
||||
needToReauthenticate: 'Du musst Dich neu anmelden, um die Push-Benachrichtigung einschalten zu können. Jetzt von {instance} abmelden?',
|
||||
failedToUpdatePush: 'Fehler beim Aktualisieren der Einstellungen für Push-Benachrichtigungen: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Wähle ein Design',
|
||||
darkBackground: 'Dunkler Hintergrund',
|
||||
lightBackground: 'Heller Hintergrund',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(standard)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Animiertes Gif: {description}',
|
||||
showImage: `Zeige {animated, select,
|
||||
true {animiert}
|
||||
other {}
|
||||
} image: {description}`,
|
||||
playVideoOrAudio: `Wiedergabe von {audio, select,
|
||||
true {audio}
|
||||
other {video}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} folgt Dir jetzt, {account}',
|
||||
reblogCountsHidden: 'Anzahl Boosts verborgen',
|
||||
favoriteCountsHidden: 'Anzahl Favorisierungen verborgen',
|
||||
rebloggedTimes: `Geboostet {count, plural,
|
||||
one {einmal}
|
||||
other {{count} mal}
|
||||
}`,
|
||||
favoritedTimes: `Favorisiert {count, plural,
|
||||
one {einmal}
|
||||
other {{count} mal}
|
||||
}`,
|
||||
pinnedStatus: 'Angehefteter Tröt',
|
||||
rebloggedYou: 'hat Deinen Tröt geboostet',
|
||||
favoritedYou: 'hat Deinen Tröt favorisiert',
|
||||
followedYou: 'folgt Dir jetzt',
|
||||
pollYouCreatedEnded: 'Eine von Dir erstellte Umfrage ist beendet',
|
||||
pollYouVotedEnded: 'Eine Umfrage, an der Du teilgenommen hast, ist beendet',
|
||||
reblogged: 'geboostet',
|
||||
showSensitiveMedia: 'Sensible Inhalte zeigen',
|
||||
hideSensitiveMedia: 'Sensible Inhalte verbergen',
|
||||
clickToShowSensitive: 'Sensible Inhalte. Klicke zum Anzeigen.',
|
||||
longPost: 'Langer Beitrag',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} hat Deinen Tröt geboostet',
|
||||
accountFavoritedYou: '{account} hat Deinen Tröt favorisiert',
|
||||
rebloggedByAccount: 'geboostet von {account}',
|
||||
contentWarningContent: 'Inhaltswarnung: {spoiler}',
|
||||
hasMedia: 'hat Medien',
|
||||
hasPoll: 'hat Umfrage',
|
||||
shortStatusLabel: '{privacy} Tröt von {account}',
|
||||
// Privacy types
|
||||
public: 'Öffentlich',
|
||||
unlisted: 'Nicht gelistet',
|
||||
followersOnly: 'Nur Folgende',
|
||||
direct: 'Direkt',
|
||||
// Themes
|
||||
themeRoyal: 'Royal',
|
||||
themeScarlet: 'Scarlet',
|
||||
themeSeafoam: 'Seafoam',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Oaken',
|
||||
themeMajesty: 'Majesty',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Grayscale',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalt',
|
||||
themeSorcery: 'Sorcery',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Pitch Black',
|
||||
themeDarkGrayscale: 'Dark Grayscale',
|
||||
// Polls
|
||||
voteOnPoll: 'In dieser Umfrage abstimmen',
|
||||
pollChoices: 'Auswahlmöglichkeiten',
|
||||
vote: 'Abstimmen',
|
||||
pollDetails: 'Einzelheiten zur Umfrage',
|
||||
refresh: 'Aktualisieren',
|
||||
expires: 'Endet',
|
||||
expired: 'Beendet',
|
||||
voteCount: `{count, plural,
|
||||
one {eine Stimme}
|
||||
other {{count} Stimmen}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - Klicke, um Unterhaltung anzuzeigen',
|
||||
showMore: 'Zeige mehr',
|
||||
showLess: 'Zeige weniger',
|
||||
closeReply: 'Antwort schließen',
|
||||
cannotReblogFollowersOnly: 'Kann nicht geboostet werden, da nur Folgende',
|
||||
cannotReblogDirectMessage: 'Kann nicht geboostet werden, da dies eine Direktnachricht ist',
|
||||
reblog: 'Boost',
|
||||
reply: 'Antworten',
|
||||
replyToThread: 'Auf Unterhaltung antworten',
|
||||
favorite: 'Favorisieren',
|
||||
unfavorite: 'Favorisieren aufheben',
|
||||
// timeline
|
||||
loadingMore: 'Lade weitere',
|
||||
loadMore: 'Lade weitere',
|
||||
showCountMore: 'Zeige {count} weitere',
|
||||
nothingToShow: 'Nichts zum anzeigen.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Seite für Tröt-Unterhaltung',
|
||||
status: 'Tröt',
|
||||
// toast messages
|
||||
blockedAccount: 'Account blockiert',
|
||||
unblockedAccount: 'Blockade des Accounts aufgehoben',
|
||||
unableToBlock: 'Konnte Account nicht blockieren: {error}',
|
||||
unableToUnblock: 'Konnte Blockade des Accounts nicht aufheben: {error}',
|
||||
bookmarkedStatus: 'Tröt als Lesezeichen gespeichert',
|
||||
unbookmarkedStatus: 'Tröt aus Lesezeichen entfernt',
|
||||
unableToBookmark: 'Konnte kein lesezeichen setzen: {error}',
|
||||
unableToUnbookmark: 'Konnte Lesezeichen nicht entfernen: {error}',
|
||||
cannotPostOffline: 'Du kannst nicht senden, wenn Du offline bist',
|
||||
unableToPost: 'Konnte Tröt nicht posten: {error}',
|
||||
statusDeleted: 'Tröt gelöscht',
|
||||
unableToDelete: 'Konnte Tröt nicht löschen: {error}',
|
||||
cannotFavoriteOffline: 'Du kannst nicht favorisieren, wenn Du offline bist',
|
||||
cannotUnfavoriteOffline: 'Du kannst Favorisierung nicht zurücknehmen, wenn Du offline bist',
|
||||
unableToFavorite: 'Konnte nicht favorisieren: {error}',
|
||||
unableToUnfavorite: 'Konnte Favorisierung nicht aufheben: {error}',
|
||||
followedAccount: 'folge jetzt diesem Account',
|
||||
unfollowedAccount: 'Folge diesem Account nicht mehr',
|
||||
unableToFollow: 'Konnte dem Account nicht folgen: {error}',
|
||||
unableToUnfollow: 'Konnte den Account nicht entfolgen: {error}',
|
||||
accessTokenRevoked: 'Das Zugriffstoken wurde zurückgezogen, von {instance} abgemeldet',
|
||||
loggedOutOfInstance: 'Von {instance} abgemeldet',
|
||||
failedToUploadMedia: 'Konnte Medien nicht hochladen: {error}',
|
||||
mutedAccount: 'Account stummgeschaltet',
|
||||
unmutedAccount: 'Stummschaltung von Account aufgehoben',
|
||||
unableToMute: 'Konnte Account nicht stummschalten: {error}',
|
||||
unableToUnmute: 'Konnte Stummschaltung von Account nicht aufheben: {error}',
|
||||
mutedConversation: 'Unterhaltung stummgeschaltet',
|
||||
unmutedConversation: 'Stummschaltung der Unterhaltung aufgehoben',
|
||||
unableToMuteConversation: 'Konnte Unterhaltung nicht stummschalten: {error}',
|
||||
unableToUnmuteConversation: 'Konnte Stummschaltung der Unterhaltung nicht aufheben: {error}',
|
||||
unpinnedStatus: 'Tröt abgeheftet',
|
||||
unableToPinStatus: 'Konnte Tröt nicht anheften: {error}',
|
||||
unableToUnpinStatus: 'Konnte Tröt nicht abheften: {error}',
|
||||
unableToRefreshPoll: 'Konnte Umfrage nicht aktualisieren: {error}',
|
||||
unableToVoteInPoll: 'Konte in der Umfrage nicht abstimmen: {error}',
|
||||
cannotReblogOffline: 'Du kannst nicht boosten, wenn Du offline bist.',
|
||||
cannotUnreblogOffline: 'Du kannst einen Boost nicht aufheben, wenn Du offline bist.',
|
||||
failedToReblog: 'Boosten fehlgeschlagen: {error}',
|
||||
failedToUnreblog: 'Aufheben des Boosts fehlgeschlagen: {error}',
|
||||
submittedReport: 'Meldung übermittelt',
|
||||
failedToReport: 'Übermittlung der Meldung fehlgeschlagen: {error}',
|
||||
approvedFollowRequest: 'Folgeanfrage genehmigt',
|
||||
rejectedFollowRequest: 'Folgeanfrage abgelehnt',
|
||||
unableToApproveFollowRequest: 'Konnte Folgeanfrage nicht genehmigen: {error}',
|
||||
unableToRejectFollowRequest: 'Konnte Folgeanfrage nicht ablehnen: {error}',
|
||||
searchError: 'Fehler bei der Suche: {error}',
|
||||
hidDomain: 'Domain verborgen',
|
||||
unhidDomain: 'Domain nicht mehr verborgen',
|
||||
unableToHideDomain: 'Konnte Domain nicht verbergen: {error}',
|
||||
unableToUnhideDomain: 'Konnte Verbergen der Domain nicht aufheben: {error}',
|
||||
showingReblogs: 'Zeige Boosts an',
|
||||
hidingReblogs: 'Verberge Boosts',
|
||||
unableToShowReblogs: 'Kann Boosts nicht anzeigen: {error}',
|
||||
unableToHideReblogs: 'Kann Boosts nicht verbergen: {error}',
|
||||
unableToShare: 'Teilen fehlgeschlagen: {error}',
|
||||
showingOfflineContent: 'Anforderung übers Internet fehlgeschlagen. Zeige Offline-Inhalte an.',
|
||||
youAreOffline: 'Du scheinst keine Verbindung zum Internet zu haben. Du kanst weiterhin Tröts lesen, solange Du offline bist.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'Update der App verfügbar'
|
||||
}
|
|
@ -0,0 +1,699 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'An alternative web client for Mastodon, focused on speed and simplicity.',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore is a web client for
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
designed for speed and simplicity.
|
||||
</p>
|
||||
<p>
|
||||
Read the
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">introductory blog post</a>,
|
||||
or get started by logging in to an instance:
|
||||
</p>`,
|
||||
logIn: 'Log in',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore is
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">open-source software</a>
|
||||
created by
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
and distributed under the
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>.
|
||||
Here is the <a href="/settings/about#privacy-policy" rel="prefetch">privacy policy</a>.
|
||||
</p>
|
||||
`,
|
||||
// Manifest
|
||||
longAppName: 'Pinafore for Mastodon',
|
||||
newStatus: 'New toot',
|
||||
// Generic UI
|
||||
loading: 'Loading',
|
||||
okay: 'OK',
|
||||
cancel: 'Cancel',
|
||||
alert: 'Alert',
|
||||
close: 'Close',
|
||||
error: 'Error: {error}',
|
||||
errorShort: 'Error:',
|
||||
// Relative timestamps
|
||||
justNow: 'just now',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(current page)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(1 notification)}
|
||||
other {({count} notifications)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(1 follow request)}
|
||||
other {({count} follow requests)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Blocked users',
|
||||
bookmarks: 'Bookmarks',
|
||||
directMessages: 'Direct messages',
|
||||
favorites: 'Favorites',
|
||||
federated: 'Federated',
|
||||
home: 'Home',
|
||||
local: 'Local',
|
||||
notifications: 'Notifications',
|
||||
mutedUsers: 'Muted users',
|
||||
pinnedStatuses: 'Pinned toots',
|
||||
followRequests: 'Follow requests',
|
||||
followRequestsLabel: `Follow requests {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'List',
|
||||
search: 'Search',
|
||||
pageHeader: 'Page header',
|
||||
goBack: 'Go back',
|
||||
back: 'Back',
|
||||
profile: 'Profile',
|
||||
federatedTimeline: 'Federated timeline',
|
||||
localTimeline: 'Local timeline',
|
||||
// community page
|
||||
community: 'Community',
|
||||
pinnableTimelines: 'Pinnable timelines',
|
||||
timelines: 'Timelines',
|
||||
lists: 'Lists',
|
||||
instanceSettings: 'Instance settings',
|
||||
notificationMentions: 'Notification mentions',
|
||||
profileWithMedia: 'Profile with media',
|
||||
profileWithReplies: 'Profile with replies',
|
||||
hashtag: 'Hashtag',
|
||||
// not logged in
|
||||
profileNotLoggedIn: 'A user timeline will appear here when logged in.',
|
||||
bookmarksNotLoggedIn: 'Your bookmarks will appear here when logged in.',
|
||||
directMessagesNotLoggedIn: 'Your direct messages will appear here when logged in.',
|
||||
favoritesNotLoggedIn: 'Your favorites will appear here when logged in.',
|
||||
federatedTimelineNotLoggedIn: 'Your federated timeline will appear here when logged in.',
|
||||
localTimelineNotLoggedIn: 'Your local timeline will appear here when logged in.',
|
||||
searchNotLoggedIn: 'You can search once logged in to an instance.',
|
||||
communityNotLoggedIn: 'Community options appear here when logged in.',
|
||||
listNotLoggedIn: 'A list will appear here when logged in.',
|
||||
notificationsNotLoggedIn: 'Your notifications will appear here when logged in.',
|
||||
notificationMentionsNotLoggedIn: 'Your notification mentions will appear here when logged in.',
|
||||
statusNotLoggedIn: 'A toot thread will appear here when logged in.',
|
||||
tagNotLoggedIn: 'A hashtag timeline will appear here when logged in.',
|
||||
// Notification subpages
|
||||
filters: 'Filters',
|
||||
all: 'All',
|
||||
mentions: 'Mentions',
|
||||
// Follow requests
|
||||
approve: 'Approve',
|
||||
reject: 'Reject',
|
||||
// Hotkeys
|
||||
hotkeys: 'Hotkeys',
|
||||
global: 'Global',
|
||||
timeline: 'Timeline',
|
||||
media: 'Media',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> to go to the next focusable element</li>
|
||||
<li><kbd>←</kbd> to go to the previous focusable element</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {or <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
to switch columns
|
||||
</li>
|
||||
<li><kbd>7</kbd> or <kbd>c</kbd> to compose a new toot</li>
|
||||
<li><kbd>s</kbd> or <kbd>/</kbd> to search</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> to go home</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> to go to notifications</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> to go to the local timeline</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> to go to the federated timeline</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> to go to the community page</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> to go to the direct messages page</li>
|
||||
<li><kbd>h</kbd> or <kbd>?</kbd> to toggle the help dialog</li>
|
||||
<li><kbd>Backspace</kbd> to go back, close dialogs</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> or <kbd>↓</kbd> to activate the next toot</li>
|
||||
<li><kbd>k</kbd> or <kbd>↑</kbd> to activate the previous toot</li>
|
||||
<li><kbd>.</kbd> to show more and scroll to top</li>
|
||||
<li><kbd>o</kbd> to open</li>
|
||||
<li><kbd>f</kbd> to favorite</li>
|
||||
<li><kbd>b</kbd> to boost</li>
|
||||
<li><kbd>r</kbd> to reply</li>
|
||||
<li><kbd>Escape</kbd> to close reply</li>
|
||||
<li><kbd>a</kbd> to bookmark</li>
|
||||
<li><kbd>i</kbd> to open images, video, or audio</li>
|
||||
<li><kbd>y</kbd> to show or hide sensitive media</li>
|
||||
<li><kbd>m</kbd> to mention the author</li>
|
||||
<li><kbd>p</kbd> to open the author's profile</li>
|
||||
<li><kbd>l</kbd> to open the card's link in a new tab</li>
|
||||
<li><kbd>x</kbd> to show or hide text behind content warning</li>
|
||||
<li><kbd>z</kbd> to show or hide all content warnings in a thread</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> to go to next or previous</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Current)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(Pinned page)}
|
||||
other {(Unpinned page)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Pin {label}',
|
||||
// Status composition
|
||||
composeStatus: 'Compose toot',
|
||||
postStatus: 'Toot!',
|
||||
contentWarning: 'Content warning',
|
||||
dropToUpload: 'Drop to upload',
|
||||
invalidFileType: 'Invalid file type',
|
||||
composeLabel: "What's on your mind?",
|
||||
autocompleteDescription: 'When autocomplete results are available, press up or down arrows and enter to select.',
|
||||
mediaUploads: 'Media uploads',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
description: 'Description',
|
||||
descriptionLabel: 'Describe for visually impaired (image, video) or auditorily impaired (audio, video) people',
|
||||
markAsSensitive: 'Mark media as sensitive',
|
||||
// Polls
|
||||
createPoll: 'Create poll',
|
||||
removePollChoice: 'Remove choice {index}',
|
||||
pollChoiceLabel: 'Choice {index}',
|
||||
multipleChoice: 'Multiple choice',
|
||||
pollDuration: 'Poll duration',
|
||||
fiveMinutes: '5 minutes',
|
||||
thirtyMinutes: '30 minutes',
|
||||
oneHour: '1 hour',
|
||||
sixHours: '6 hours',
|
||||
twelveHours: '12 hours',
|
||||
oneDay: '1 day',
|
||||
threeDays: '3 days',
|
||||
sevenDays: '7 days',
|
||||
never: 'Never',
|
||||
addEmoji: 'Insert emoji',
|
||||
addMedia: 'Add media (images, video, audio)',
|
||||
addPoll: 'Add poll',
|
||||
removePoll: 'Remove poll',
|
||||
postPrivacyLabel: 'Adjust privacy (currently {label})',
|
||||
addContentWarning: 'Add content warning',
|
||||
removeContentWarning: 'Remove content warning',
|
||||
altLabel: 'Describe for visually impaired people',
|
||||
extractText: 'Extract text from image',
|
||||
extractingText: 'Extracting text…',
|
||||
extractingTextCompletion: 'Extracting text ({percent}% complete)…',
|
||||
unableToExtractText: 'Unable to extract text.',
|
||||
// Account options
|
||||
followAccount: 'Follow {account}',
|
||||
unfollowAccount: 'Unfollow {account}',
|
||||
blockAccount: 'Block {account}',
|
||||
unblockAccount: 'Unblock {account}',
|
||||
muteAccount: 'Mute {account}',
|
||||
unmuteAccount: 'Unmute {account}',
|
||||
showReblogsFromAccount: 'Show boosts from {account}',
|
||||
hideReblogsFromAccount: 'Hide boosts from {account}',
|
||||
showDomain: 'Unhide {domain}',
|
||||
hideDomain: 'Hide {domain}',
|
||||
reportAccount: 'Report {account}',
|
||||
mentionAccount: 'Mention {account}',
|
||||
copyLinkToAccount: 'Copy link to account',
|
||||
copiedToClipboard: 'Copied to clipboard',
|
||||
// Media dialog
|
||||
navigateMedia: 'Navigate media items',
|
||||
showPreviousMedia: 'Show previous media',
|
||||
showNextMedia: 'Show next media',
|
||||
enterPinchZoom: 'Pinch-zoom mode',
|
||||
exitPinchZoom: 'Exit pinch-zoom mode',
|
||||
showMedia: `Show {index, select,
|
||||
1 {first}
|
||||
2 {second}
|
||||
3 {third}
|
||||
other {fourth}
|
||||
} media {current, select,
|
||||
true {(current)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Preview (focal point)',
|
||||
enterFocalPoint: 'Enter the focal point (X, Y) for this media',
|
||||
muteNotifications: 'Mute notifications as well',
|
||||
muteAccountConfirm: 'Mute {account}?',
|
||||
mute: 'Mute',
|
||||
unmute: 'Unmute',
|
||||
zoomOut: 'Zoom out',
|
||||
zoomIn: 'Zoom in',
|
||||
// Reporting
|
||||
reportingLabel: 'You are reporting {account} to the moderators of {instance}.',
|
||||
additionalComments: 'Additional comments',
|
||||
forwardDescription: 'Forward to the moderators of {instance} as well?',
|
||||
forwardLabel: 'Forward to {instance}',
|
||||
unableToLoadStatuses: 'Unable to load recent toots: {error}',
|
||||
report: 'Report',
|
||||
noContent: '(No content)',
|
||||
noStatuses: 'No toots to report',
|
||||
// Status options
|
||||
unpinFromProfile: 'Unpin from profile',
|
||||
pinToProfile: 'Pin to profile',
|
||||
muteConversation: 'Mute conversation',
|
||||
unmuteConversation: 'Unmute conversation',
|
||||
bookmarkStatus: 'Bookmark toot',
|
||||
unbookmarkStatus: 'Unbookmark toot',
|
||||
deleteAndRedraft: 'Delete and redraft',
|
||||
reportStatus: 'Report toot',
|
||||
shareStatus: 'Share toot',
|
||||
copyLinkToStatus: 'Copy link to toot',
|
||||
// Account profile
|
||||
profileForAccount: 'Profile for {account}',
|
||||
statisticsAndMoreOptions: 'Stats and more options',
|
||||
statuses: 'Toots',
|
||||
follows: 'Follows',
|
||||
followers: 'Followers',
|
||||
moreOptions: 'More options',
|
||||
followersLabel: 'Followed by {count}',
|
||||
followingLabel: 'Follows {count}',
|
||||
followLabel: `Follow {requested, select,
|
||||
true {(follow requested)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Unfollow {requested, select,
|
||||
true {(follow requested)}
|
||||
other {}
|
||||
}`,
|
||||
notify: 'Subscribe to {account}',
|
||||
denotify: 'Unsubscribe from {account}',
|
||||
subscribedAccount: 'Subscribed to account',
|
||||
unsubscribedAccount: 'Unsubscribed from account',
|
||||
unblock: 'Unblock',
|
||||
nameAndFollowing: 'Name and following',
|
||||
clickToSeeAvatar: 'Click to see avatar',
|
||||
opensInNewWindow: '{label} (opens in new window)',
|
||||
blocked: 'Blocked',
|
||||
domainHidden: 'Domain hidden',
|
||||
muted: 'Muted',
|
||||
followsYou: 'Follows you',
|
||||
avatarForAccount: 'Avatar for {account}',
|
||||
fields: 'Fields',
|
||||
accountHasMoved: '{account} has moved:',
|
||||
profilePageForAccount: 'Profile page for {account}',
|
||||
// About page
|
||||
about: 'About',
|
||||
aboutApp: 'About Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore is
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">free and open-source software</a>
|
||||
created by
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
and distributed under the
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Privacy Policy</h2>
|
||||
|
||||
<p>
|
||||
Pinafore does not store any personal information on its servers,
|
||||
including but not limited to names, email addresses,
|
||||
IP addresses, posts, and photos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore is a static site. All data is stored locally in your browser and shared with the fediverse
|
||||
instance(s) you connect to.
|
||||
</p>
|
||||
|
||||
<h2>Credits</h2>
|
||||
|
||||
<p>
|
||||
Icons provided by <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Logo thanks to "sailboat" by Gregor Cresnar from
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Settings',
|
||||
general: 'General',
|
||||
generalSettings: 'General settings',
|
||||
showSensitive: 'Show sensitive media by default',
|
||||
showAllSpoilers: 'Expand content warnings by default',
|
||||
showPlain: 'Show a plain gray color for sensitive media',
|
||||
allSensitive: 'Treat all media as sensitive',
|
||||
largeMedia: 'Show large inline images and videos',
|
||||
autoplayGifs: 'Autoplay animated GIFs',
|
||||
hideCards: 'Hide link preview cards',
|
||||
underlineLinks: 'Underline links in toots and profiles',
|
||||
accessibility: 'Accessibility',
|
||||
reduceMotion: 'Reduce motion in UI animations',
|
||||
disableTappable: 'Disable tappable area on entire toot',
|
||||
removeEmoji: 'Remove emoji from user display names',
|
||||
shortAria: 'Use short article ARIA labels',
|
||||
theme: 'Theme',
|
||||
themeForInstance: 'Theme for {instance}',
|
||||
disableCustomScrollbars: 'Disable custom scrollbars',
|
||||
bottomNav: 'Place the navigation bar at the bottom of the screen',
|
||||
centerNav: 'Center the navigation bar',
|
||||
preferences: 'Preferences',
|
||||
hotkeySettings: 'Hotkey settings',
|
||||
disableHotkeys: 'Disable all hotkeys',
|
||||
leftRightArrows: 'Left/right arrow keys change focus rather than columns/media',
|
||||
guide: 'Guide',
|
||||
reload: 'Reload',
|
||||
// Wellness settings
|
||||
wellness: 'Wellness',
|
||||
wellnessSettings: 'Wellness settings',
|
||||
wellnessDescription: `Wellness settings are designed to reduce the addictive or anxiety-inducing aspects of social media.
|
||||
Choose any options that work well for you.`,
|
||||
enableAll: 'Enable all',
|
||||
metrics: 'Metrics',
|
||||
hideFollowerCount: 'Hide follower counts (capped at 10)',
|
||||
hideReblogCount: 'Hide boost counts',
|
||||
hideFavoriteCount: 'Hide favorite counts',
|
||||
hideUnread: 'Hide unread notifications count (i.e. the red dot)',
|
||||
// The quality that makes something seem important or interesting because it seems to be happening now
|
||||
immediacy: 'Immediacy',
|
||||
showAbsoluteTimestamps: 'Show absolute timestamps (e.g. "March 3rd") instead of relative timestamps (e.g. "5 minutes ago")',
|
||||
ui: 'UI',
|
||||
grayscaleMode: 'Grayscale mode',
|
||||
wellnessFooter: `These settings are partly based on guidelines from the
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'You can filter or disable notifications in the',
|
||||
filterNotificationsText: 'instance settings',
|
||||
filterNotificationsPost: '',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: 'Disable',
|
||||
disableInfiniteScrollText: 'infinite scroll',
|
||||
disableInfiniteScrollDescription: `When infinite scroll is disabled, new toots will not automatically appear at
|
||||
the bottom or top of the timeline. Instead, buttons will allow you to
|
||||
load more content on demand.`,
|
||||
disableInfiniteScrollPost: '',
|
||||
// Instance settings
|
||||
loggedInAs: 'Logged in as',
|
||||
homeTimelineFilters: 'Home timeline filters',
|
||||
notificationFilters: 'Notification filters',
|
||||
pushNotifications: 'Push notifications',
|
||||
// Add instance page
|
||||
storageError: `It seems Pinafore cannot store data locally. Is your browser in private mode
|
||||
or blocking cookies? Pinafore stores all data locally, and requires LocalStorage and
|
||||
IndexedDB to work correctly.`,
|
||||
javaScriptError: 'You must enable JavaScript to log in.',
|
||||
enterInstanceName: 'Enter instance name',
|
||||
instanceColon: 'Instance:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: "Don't have an",
|
||||
getAnInstanceText: 'instance',
|
||||
getAnInstanceDescription: 'An instance is your Mastodon home server, such as mastodon.social or cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: 'Join Mastodon!',
|
||||
instancesYouveLoggedInTo: "Instances you've logged in to:",
|
||||
addAnotherInstance: 'Add another instance',
|
||||
youreNotLoggedIn: "You're not logged in to any instances.",
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(current instance)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Log in to an instance',
|
||||
logInToAnInstancePost: 'to start using Pinafore.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Always show',
|
||||
showRingText: 'focus ring',
|
||||
showRingDescription: `The focus ring is the outline showing the currently focused element. By default, it's only
|
||||
shown when using the keyboard (not mouse or touch), but you may choose to always show it.`,
|
||||
showRingPost: '',
|
||||
instances: 'Instances',
|
||||
addInstance: 'Add instance',
|
||||
homeTimelineFilterSettings: 'Home timeline filter settings',
|
||||
showReblogs: 'Show boosts',
|
||||
showReplies: 'Show replies',
|
||||
switchOrLogOut: 'Switch to or log out of this instance',
|
||||
switchTo: 'Switch to this instance',
|
||||
switchToInstance: 'Switch to instance',
|
||||
switchToNameOfInstance: 'Switch to {instance}',
|
||||
logOut: 'Log out',
|
||||
logOutOfInstanceConfirm: 'Log out of {instance}?',
|
||||
notificationFilterSettings: 'Notification filter settings',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: "Your browser doesn't support push notifications.",
|
||||
deniedPush: 'You have denied permission to show notifications.',
|
||||
pushNotificationsNote: 'Note that you can only have push notifications for one instance at a time.',
|
||||
pushSettings: 'Push notification settings',
|
||||
newFollowers: 'New followers',
|
||||
reblogs: 'Boosts',
|
||||
pollResults: 'Poll results',
|
||||
subscriptions: 'Subscribed toots',
|
||||
needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?',
|
||||
failedToUpdatePush: 'Failed to update push notification settings: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Choose a theme',
|
||||
darkBackground: 'Dark background',
|
||||
lightBackground: 'Light background',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(default)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Animated image: {description}',
|
||||
showImage: `Show {animated, select,
|
||||
true {animated}
|
||||
other {}
|
||||
} image: {description}`,
|
||||
playVideoOrAudio: `Play {audio, select,
|
||||
true {audio}
|
||||
other {video}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} followed you, {account}',
|
||||
accountSignedUp: '{name} signed up, {account}',
|
||||
accountRequestedFollow: '{name} requested to follow you, {account}',
|
||||
accountReported: '{name} filed a report, {account}',
|
||||
reblogCountsHidden: 'Boost counts hidden',
|
||||
favoriteCountsHidden: 'Favorite counts hidden',
|
||||
rebloggedTimes: `Boosted {count, plural,
|
||||
one {1 time}
|
||||
other {{count} times}
|
||||
}`,
|
||||
favoritedTimes: `Favorited {count, plural,
|
||||
one {1 time}
|
||||
other {{count} times}
|
||||
}`,
|
||||
pinnedStatus: 'Pinned toot',
|
||||
rebloggedYou: 'boosted your toot',
|
||||
favoritedYou: 'favorited your toot',
|
||||
followedYou: 'followed you',
|
||||
edited: 'edited their toot',
|
||||
requestedFollow: 'requested to follow you',
|
||||
reported: 'filed a report',
|
||||
signedUp: 'signed up',
|
||||
posted: 'posted',
|
||||
pollYouCreatedEnded: 'A poll you created has ended',
|
||||
pollYouVotedEnded: 'A poll you voted on has ended',
|
||||
reblogged: 'boosted',
|
||||
favorited: 'favorited',
|
||||
unreblogged: 'unboosted',
|
||||
unfavorited: 'unfavorited',
|
||||
showSensitiveMedia: 'Show sensitive media',
|
||||
hideSensitiveMedia: 'Hide sensitive media',
|
||||
clickToShowSensitive: 'Sensitive content. Click to show.',
|
||||
longPost: 'Long post',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} boosted your toot',
|
||||
accountFavoritedYou: '{account} favorited your toot',
|
||||
accountEdited: '{account} edited their toot',
|
||||
rebloggedByAccount: 'Boosted by {account}',
|
||||
contentWarningContent: 'Content warning: {spoiler}',
|
||||
hasMedia: 'has media',
|
||||
hasPoll: 'has poll',
|
||||
shortStatusLabel: '{privacy} toot by {account}',
|
||||
// Privacy types
|
||||
public: 'Public',
|
||||
unlisted: 'Unlisted',
|
||||
followersOnly: 'Followers-only',
|
||||
direct: 'Direct',
|
||||
// Themes
|
||||
themeRoyal: 'Royal',
|
||||
themeScarlet: 'Scarlet',
|
||||
themeSeafoam: 'Seafoam',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Oaken',
|
||||
themeMajesty: 'Majesty',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Grayscale',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalt',
|
||||
themeSorcery: 'Sorcery',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Pitch Black',
|
||||
themeDarkGrayscale: 'Dark Grayscale',
|
||||
// Polls
|
||||
voteOnPoll: 'Vote on poll',
|
||||
pollChoices: 'Poll choices',
|
||||
vote: 'Vote',
|
||||
pollDetails: 'Poll details',
|
||||
refresh: 'Refresh',
|
||||
expires: 'Ends',
|
||||
expired: 'Ended',
|
||||
voteCount: `{count, plural,
|
||||
one {1 vote}
|
||||
other {{count} votes}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - click to show thread',
|
||||
showMore: 'Show more',
|
||||
showLess: 'Show less',
|
||||
closeReply: 'Close reply',
|
||||
cannotReblogFollowersOnly: 'Cannot be boosted because this is followers-only',
|
||||
cannotReblogDirectMessage: 'Cannot be boosted because this is a direct message',
|
||||
reblog: 'Boost',
|
||||
reply: 'Reply',
|
||||
replyToThread: 'Reply to thread',
|
||||
favorite: 'Favorite',
|
||||
unfavorite: 'Unfavorite',
|
||||
// timeline
|
||||
loadingMore: 'Loading more…',
|
||||
loadMore: 'Load more',
|
||||
showCountMore: 'Show {count} more',
|
||||
nothingToShow: 'Nothing to show.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Toot thread page',
|
||||
status: 'Toot',
|
||||
// toast messages
|
||||
blockedAccount: 'Blocked account',
|
||||
unblockedAccount: 'Unblocked account',
|
||||
unableToBlock: 'Unable to block account: {error}',
|
||||
unableToUnblock: 'Unable to unblock account: {error}',
|
||||
bookmarkedStatus: 'Bookmarked toot',
|
||||
unbookmarkedStatus: 'Unbookmarked toot',
|
||||
unableToBookmark: 'Unable to bookmark: {error}',
|
||||
unableToUnbookmark: 'Unable to unbookmark: {error}',
|
||||
cannotPostOffline: 'You cannot post while offline',
|
||||
unableToPost: 'Unable to post toot: {error}',
|
||||
statusDeleted: 'Toot deleted',
|
||||
unableToDelete: 'Unable to delete toot: {error}',
|
||||
cannotFavoriteOffline: 'You cannot favorite while offline',
|
||||
cannotUnfavoriteOffline: 'You cannot unfavorite while offline',
|
||||
unableToFavorite: 'Unable to favorite: {error}',
|
||||
unableToUnfavorite: 'Unable to unfavorite: {error}',
|
||||
followedAccount: 'Followed account',
|
||||
unfollowedAccount: 'Unfollowed account',
|
||||
unableToFollow: 'Unable to follow account: {error}',
|
||||
unableToUnfollow: 'Unable to unfollow account: {error}',
|
||||
accessTokenRevoked: 'The access token was revoked, logged out of {instance}',
|
||||
loggedOutOfInstance: 'Logged out of {instance}',
|
||||
failedToUploadMedia: 'Failed to upload media: {error}',
|
||||
mutedAccount: 'Muted account',
|
||||
unmutedAccount: 'Unmuted account',
|
||||
unableToMute: 'Unable to mute account: {error}',
|
||||
unableToUnmute: 'Unable to unmute account: {error}',
|
||||
mutedConversation: 'Muted conversation',
|
||||
unmutedConversation: 'Unmuted conversation',
|
||||
unableToMuteConversation: 'Unable to mute conversation: {error}',
|
||||
unableToUnmuteConversation: 'Unable to unmute conversation: {error}',
|
||||
unpinnedStatus: 'Unpinned toot',
|
||||
unableToPinStatus: 'Unable to pin toot: {error}',
|
||||
unableToUnpinStatus: 'Unable to unpin toot: {error}',
|
||||
unableToRefreshPoll: 'Unable to refresh poll: {error}',
|
||||
unableToVoteInPoll: 'Unable to vote in poll: {error}',
|
||||
cannotReblogOffline: 'You cannot boost while offline.',
|
||||
cannotUnreblogOffline: 'You cannot unboost while offline.',
|
||||
failedToReblog: 'Failed to boost: {error}',
|
||||
failedToUnreblog: 'Failed to unboost: {error}',
|
||||
submittedReport: 'Submitted report',
|
||||
failedToReport: 'Failed to report: {error}',
|
||||
approvedFollowRequest: 'Approved follow request',
|
||||
rejectedFollowRequest: 'Rejected follow request',
|
||||
unableToApproveFollowRequest: 'Unable to approve follow request: {error}',
|
||||
unableToRejectFollowRequest: 'Unable to reject follow request: {error}',
|
||||
searchError: 'Error during search: {error}',
|
||||
hidDomain: 'Hid domain',
|
||||
unhidDomain: 'Unhid domain',
|
||||
unableToHideDomain: 'Unable to hide domain: {error}',
|
||||
unableToUnhideDomain: 'Unable to unhide domain: {error}',
|
||||
showingReblogs: 'Showing boosts',
|
||||
hidingReblogs: 'Hiding boosts',
|
||||
unableToShowReblogs: 'Unable to show boosts: {error}',
|
||||
unableToHideReblogs: 'Unable to hide boosts: {error}',
|
||||
unableToShare: 'Unable to share: {error}',
|
||||
unableToSubscribe: 'Unable to subscribe: {error}',
|
||||
unableToUnsubscribe: 'Unable to unsubscribe: {error}',
|
||||
showingOfflineContent: 'Internet request failed. Showing offline content.',
|
||||
youAreOffline: 'You seem to be offline. You can still read toots while offline.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'App update available.',
|
||||
// Word/phrase filters
|
||||
wordFilters: 'Word filters',
|
||||
noFilters: 'You don\'t have any word filters.',
|
||||
wordOrPhrase: 'Word or phrase',
|
||||
contexts: 'Contexts',
|
||||
addFilter: 'Add filter',
|
||||
editFilter: 'Edit filter',
|
||||
filterHome: 'Home and lists',
|
||||
filterNotifications: 'Notifications',
|
||||
filterPublic: 'Public timelines',
|
||||
filterThread: 'Conversations',
|
||||
filterAccount: 'Profiles',
|
||||
filterUnknown: 'Unknown',
|
||||
expireAfter: 'Expire after',
|
||||
whereToFilter: 'Where to filter',
|
||||
irreversible: 'Irreversible',
|
||||
wholeWord: 'Whole word',
|
||||
save: 'Save',
|
||||
updatedFilter: 'Updated filter',
|
||||
createdFilter: 'Created filter',
|
||||
failedToModifyFilter: 'Failed to modify filter: {error}',
|
||||
deletedFilter: 'Deleted filter',
|
||||
required: 'Required',
|
||||
// Dialogs
|
||||
profileOptions: 'Profile options',
|
||||
copyLink: 'Copy link',
|
||||
emoji: 'Emoji',
|
||||
editMedia: 'Edit media',
|
||||
shortcutHelp: 'Shortcut help',
|
||||
statusOptions: 'Status options',
|
||||
confirm: 'Confirm',
|
||||
closeDialog: 'Close dialog',
|
||||
postPrivacy: 'Post privacy',
|
||||
homeOnInstance: 'Home on {instance}',
|
||||
statusesTimelineOnInstance: 'Statuses: {timeline} timeline on {instance}',
|
||||
statusesHashtag: 'Statuses: #{hashtag} hashtag',
|
||||
statusesThread: 'Statuses: thread',
|
||||
statusesAccountTimeline: 'Statuses: account timeline',
|
||||
statusesList: 'Statuses: list',
|
||||
notificationsOnInstance: 'Notifications on {instance}'
|
||||
}
|
|
@ -0,0 +1,696 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'Un cliente web alternativo para Mastodon, centrado en la velocidad y la sencillez.',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore es un cliente web para
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
diseñado para ser rápido y sencillo.
|
||||
</p>
|
||||
<p>
|
||||
Lee el
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">artículo introductorio en el blog</a>,
|
||||
o comienza iniciando sesión en una instancia:
|
||||
</p>`,
|
||||
logIn: 'Iniciar sesión',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore es
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">software de código abierto</a>
|
||||
creado por
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
y distribuido bajo la
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">Licencia AGPL</a>.
|
||||
Aquí está la <a href="/settings/about#privacy-policy" rel="prefetch">política de privacidad</a>.
|
||||
</p>
|
||||
`,
|
||||
// Manifest
|
||||
longAppName: 'Pinafore para Mastodon',
|
||||
newStatus: 'Nuevo toot',
|
||||
// Generic UI
|
||||
loading: 'Cargando',
|
||||
okay: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
alert: 'Alerta',
|
||||
close: 'Cerrar',
|
||||
error: 'Error: {error}',
|
||||
errorShort: 'Error:',
|
||||
// Relative timestamps
|
||||
justNow: 'ahora mismo',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(página actual)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(1 notificación)}
|
||||
other {({count} notificaciones)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(1 solicitud de seguimiento)}
|
||||
other {({count} solicitudes de seguimiento)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Usuarios bloqueados',
|
||||
bookmarks: 'Marcadores',
|
||||
directMessages: 'Mensajes directos',
|
||||
favorites: 'Favoritos',
|
||||
federated: 'Federada',
|
||||
home: 'Inicio',
|
||||
local: 'Local',
|
||||
notifications: 'Notificaciones',
|
||||
mutedUsers: 'Usuarios silenciados',
|
||||
pinnedStatuses: 'Toots fijados',
|
||||
followRequests: 'Solicitudes de seguimiento',
|
||||
followRequestsLabel: `Solicitudes de seguimiento {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'Lista',
|
||||
search: 'Buscar',
|
||||
pageHeader: 'Encabezado de página',
|
||||
goBack: 'Retroceder',
|
||||
back: 'Atrás',
|
||||
profile: 'Perfil',
|
||||
federatedTimeline: 'Cronología federada',
|
||||
localTimeline: 'Cronología local',
|
||||
// community page
|
||||
community: 'Comunidad',
|
||||
pinnableTimelines: 'Cronologías que puedes fijar',
|
||||
timelines: 'Cronologías',
|
||||
lists: 'Listas',
|
||||
instanceSettings: 'Opciones para instancia',
|
||||
notificationMentions: 'Notificación de menciones',
|
||||
profileWithMedia: 'Perfil con multimedia',
|
||||
profileWithReplies: 'Perfil con respuestas',
|
||||
hashtag: 'Hashtag',
|
||||
// not logged in
|
||||
profileNotLoggedIn: 'Aquí se mostrará una cronología de usuario cuando hayas iniciado sesión.',
|
||||
bookmarksNotLoggedIn: 'Tus marcadores se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
directMessagesNotLoggedIn: 'Tus mensajes directos se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
favoritesNotLoggedIn: 'Tus favoritos se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
federatedTimelineNotLoggedIn: 'Tu cronología federada se mostrará aquí cuando hayas iniciado sesión.',
|
||||
localTimelineNotLoggedIn: 'Tu cronología localse mostrará aquí cuando hayas iniciado sesión.',
|
||||
searchNotLoggedIn: 'Puedes buscar una vez que inicias sesión en una instancia.',
|
||||
communityNotLoggedIn: 'Las opciones para comunidad se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
listNotLoggedIn: 'Aquí se mostrará una lista cuando hayas iniciado sesión.',
|
||||
notificationsNotLoggedIn: 'Tus notificaciones se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
notificationMentionsNotLoggedIn: 'Las notificaciones de tus menciones se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
statusNotLoggedIn: 'Aquí se mostrará un hilo de toots cuando hayas iniciado sesión.',
|
||||
tagNotLoggedIn: 'Aquí se mostrará una cronología de hashtags cuando hayas iniciado sesión.',
|
||||
// Notification subpages
|
||||
filters: 'Filtros',
|
||||
all: 'Todo',
|
||||
mentions: 'Menciones',
|
||||
// Follow requests
|
||||
approve: 'Aceptar',
|
||||
reject: 'Rechazar',
|
||||
// Hotkeys
|
||||
hotkeys: 'Atajos de teclado',
|
||||
global: 'Globales',
|
||||
timeline: 'Cronología',
|
||||
media: 'Multimedia',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> para ir al elemento enfocable siguiente</li>
|
||||
<li><kbd>←</kbd> para ir al elemento enfocable anterior</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {o <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
para cambiar de columna
|
||||
</li>
|
||||
<li><kbd>7</kbd> o <kbd>c</kbd> para redactar un nuevo toot</li>
|
||||
<li><kbd>s</kbd> o <kbd>/</kbd> para buscar</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> para ir a inicio</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> para ir a notificaciones</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> to para ir a la cronología local</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> para ir a la cronología federada</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> para ir a la página comunidad</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> para ir a la página de mensajes directos</li>
|
||||
<li><kbd>h</kbd> o <kbd>?</kbd> para abrir o cerrar el diálogo de ayuda</li>
|
||||
<li><kbd>Backspace</kbd> para retroceder, cerrar diálogos</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> o <kbd>↓</kbd> para activar el toot siguiente</li>
|
||||
<li><kbd>k</kbd> o <kbd>↑</kbd> para activar el toot anterior</li>
|
||||
<li><kbd>.</kbd> para mostrar más y desplazarse al principio</li>
|
||||
<li><kbd>o</kbd> para abrir</li>
|
||||
<li><kbd>f</kbd> para marcar como favorito</li>
|
||||
<li><kbd>b</kbd> para reenviar</li>
|
||||
<li><kbd>r</kbd> para responder</li>
|
||||
<li><kbd>Escape</kbd> para cerrar respuesta</li>
|
||||
<li><kbd>a</kbd> para marcador</li>
|
||||
<li><kbd>i</kbd> para abrir imágenes, vídeo o audio</li>
|
||||
<li><kbd>y</kbd> para mostrar u ocultar multimedia sensible</li>
|
||||
<li><kbd>m</kbd> para mencionar al autor</li>
|
||||
<li><kbd>p</kbd> para abrir el perfil del autor</li>
|
||||
<li><kbd>l</kbd> para abrir el enlace de la publicación en una nueva pestaña</li>
|
||||
<li><kbd>x</kbd> para mostrar u ocultar el texto tras una advertencia de contenido</li>
|
||||
<li><kbd>z</kbd> para mostrar u ocultar todas las advertencias de contenido en un hilo</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> para ir a siguiente o anterior</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Actual)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(página fijada)}
|
||||
other {(Página no fijada)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Fijar {label}',
|
||||
// Status composition
|
||||
composeStatus: 'Redactar toot',
|
||||
postStatus: 'Toot!',
|
||||
contentWarning: 'Advertencia de contenido',
|
||||
dropToUpload: 'Soltar para subir',
|
||||
invalidFileType: 'Tipo de fichero no válido',
|
||||
composeLabel: '¿En qué estás pensando?',
|
||||
autocompleteDescription: 'Cuando haya disponibles resultados de autocompletado, pulsa las flechas arriba o abajo y enter para seleccionar.',
|
||||
mediaUploads: 'Subidas multimedia',
|
||||
edit: 'Editar',
|
||||
delete: 'Borrar',
|
||||
description: 'Descripción',
|
||||
descriptionLabel: 'Describir para las personas con discapacidad visual (imagen, vídeo) o con discapacidad auditiva (audio, vídeo)',
|
||||
markAsSensitive: 'Marcar multimedia como sensible',
|
||||
// Polls
|
||||
createPoll: 'Crear encuesta',
|
||||
removePollChoice: 'Eliminar opción {index}',
|
||||
pollChoiceLabel: 'Opción {index}',
|
||||
multipleChoice: 'Selección múltiple',
|
||||
pollDuration: 'Duración de la encuesta',
|
||||
fiveMinutes: '5 minutos',
|
||||
thirtyMinutes: '30 minutos',
|
||||
oneHour: '1 hora',
|
||||
sixHours: '6 horas',
|
||||
twelveHours: '12 horas',
|
||||
oneDay: '1 día',
|
||||
threeDays: '3 días',
|
||||
sevenDays: '7 días',
|
||||
never: 'Nunca',
|
||||
addEmoji: 'Insertar emoji',
|
||||
addMedia: 'Añadir multimedia (imágenes, vídeo, audio)',
|
||||
addPoll: 'Añadir encuesta',
|
||||
removePoll: 'Eliminar encuesta',
|
||||
postPrivacyLabel: 'Ajustar privacidad (actualmente {label})',
|
||||
addContentWarning: 'Añadir advertencia de contenido',
|
||||
removeContentWarning: 'Eliminar advertencia de contenido',
|
||||
altLabel: 'Describir para las personas con discapacidad visual',
|
||||
extractText: 'Extraer texto de imagen',
|
||||
extractingText: 'Extrayendo texto…',
|
||||
extractingTextCompletion: 'Extrayendo texto ({percent}% completado)…',
|
||||
unableToExtractText: 'No se puede extraer texto.',
|
||||
// Account options
|
||||
followAccount: 'Seguir a {account}',
|
||||
unfollowAccount: 'Dejar de seguir a {account}',
|
||||
blockAccount: 'Bloquear a {account}',
|
||||
unblockAccount: 'Desbloquear a {account}',
|
||||
muteAccount: 'Silenciar a {account}',
|
||||
unmuteAccount: 'Dejar de silenciar a Unmute {account}',
|
||||
showReblogsFromAccount: 'Mostrar toots reenviados por {account}',
|
||||
hideReblogsFromAccount: 'Ocultar toots reenviados por {account}',
|
||||
showDomain: 'Dejar de ocultar {domain}',
|
||||
hideDomain: 'Ocultar {domain}',
|
||||
reportAccount: 'Denunciar a {account}',
|
||||
mentionAccount: 'Mencionar a {account}',
|
||||
copyLinkToAccount: 'Copiar enlace a cuenta',
|
||||
copiedToClipboard: 'Copiado al portapapeles',
|
||||
// Media dialog
|
||||
navigateMedia: 'Navegar por elementos multimedia',
|
||||
showPreviousMedia: 'Mostrar multimedia anterior',
|
||||
showNextMedia: 'Mostrar multimedia siguiente',
|
||||
enterPinchZoom: 'Modo pinch-zoom',
|
||||
exitPinchZoom: 'Salir del modo pinch-zoom',
|
||||
showMedia: `Mostrar {index, select,
|
||||
1 {primer}
|
||||
2 {segundo}
|
||||
3 {tercero}
|
||||
other {cuarto}
|
||||
} multimedia {current, select,
|
||||
true {(actual)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Previsualizar (punto focal)',
|
||||
enterFocalPoint: 'Introducir el punto focal (X, Y) para este multimedia',
|
||||
muteNotifications: 'Silenciar también las notificaciones',
|
||||
muteAccountConfirm: '¿Silenciar a {account}?',
|
||||
mute: 'Silenciar',
|
||||
unmute: 'Dejar de silenciar',
|
||||
zoomOut: 'Alejar',
|
||||
zoomIn: 'Acercar',
|
||||
// Reporting
|
||||
reportingLabel: 'Estás denunciando a {account} a los moderadores de {instance}.',
|
||||
additionalComments: 'Comentarios adicionales',
|
||||
forwardDescription: '?Reenviar también a los moderadores de {instance}?',
|
||||
forwardLabel: 'Reenviar a {instance}',
|
||||
unableToLoadStatuses: 'No se pueden cargar los toots recientes: {error}',
|
||||
report: 'Denunciar',
|
||||
noContent: '(Sin contenido)',
|
||||
noStatuses: 'No hay toots para denunciar',
|
||||
// Status options
|
||||
unpinFromProfile: 'Dejar de fijar en el perfil',
|
||||
pinToProfile: 'Fijar en el perfil',
|
||||
muteConversation: 'Silenciar conversación',
|
||||
unmuteConversation: 'Dejar de silenciar conversación',
|
||||
bookmarkStatus: 'Poner marcador al toot',
|
||||
unbookmarkStatus: 'Quitar marcador al toot',
|
||||
deleteAndRedraft: 'Borrar y volver a redactar',
|
||||
reportStatus: 'Denunciar toot',
|
||||
shareStatus: 'Compartir toot',
|
||||
copyLinkToStatus: 'Copiar enlace al toot',
|
||||
// Account profile
|
||||
profileForAccount: 'Perfil para {account}',
|
||||
statisticsAndMoreOptions: 'Estadísticas y más opciones',
|
||||
statuses: 'Toots',
|
||||
follows: 'Siguiendo',
|
||||
followers: 'Seguidores',
|
||||
moreOptions: 'Más opciones',
|
||||
followersLabel: 'Te han seguido {count}',
|
||||
followingLabel: 'Has seguido a {count}',
|
||||
followLabel: `Seguimiento {requested, select,
|
||||
true {(solicitud de seguimiento)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Dejar de seguir {requested, select,
|
||||
true {(solicitud de seguimiento)}
|
||||
other {}
|
||||
}`,
|
||||
notify: 'Suscribirse a {account}',
|
||||
denotify: 'Cancelar suscripción a {account}',
|
||||
subscribedAccount: 'Te has suscrito a la cuenta',
|
||||
unsubscribedAccount: 'Has cancelado tu suscripción a la cuenta',
|
||||
unblock: 'Desbloquear',
|
||||
nameAndFollowing: 'Nombre y seguimientos',
|
||||
clickToSeeAvatar: 'Haz clic para ver el avatar',
|
||||
opensInNewWindow: '{label} (Se abre en nueva ventana)',
|
||||
blocked: 'Bloqueado',
|
||||
domainHidden: 'Dominio oculto',
|
||||
muted: 'Silenciado',
|
||||
followsYou: 'Te está siguiendo',
|
||||
avatarForAccount: 'Avatar para {account}',
|
||||
fields: 'Campos',
|
||||
accountHasMoved: '{account} se ha trasladado:',
|
||||
profilePageForAccount: 'Página de perfil para {account}',
|
||||
// About page
|
||||
about: 'Acerca de',
|
||||
aboutApp: 'Acerca de Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore es
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">software libre y de código abierto</a>
|
||||
creado por
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
y distribuido bajo la
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Política de privacidad</h2>
|
||||
|
||||
<p>
|
||||
Pinafore no almacena ninguna información personal en sus servidores,
|
||||
incluyendo, pero no limitándose a nombres, direcciones de correo electrónico,
|
||||
direcciones IP, posts y fotos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore es un sitio estático. Todos los datos son almacenados en tu navegador y compartidos con las instancias del fediverso
|
||||
a las que te conectas.
|
||||
</p>
|
||||
|
||||
<h2>Créditos</h2>
|
||||
|
||||
<p>
|
||||
Iconos proporcionados por <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Logo gracias a "sailboat" por Gregor Cresnar, de
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Opciones de configuración',
|
||||
general: 'General',
|
||||
generalSettings: 'Opciones generales',
|
||||
showSensitive: 'Mostrar multimedia sensible por defecto',
|
||||
showPlain: 'Mostrar un color gris liso para multimedia sensible',
|
||||
allSensitive: 'Tratar todo multimedia como sensible',
|
||||
largeMedia: 'Mostrar imágenes y vídeos grandes incrustados',
|
||||
autoplayGifs: 'Reproducir automáticamente GIFs animados',
|
||||
hideCards: 'Ocultar paneles de previsualización de enlaces',
|
||||
underlineLinks: 'Subrayar enlaces en toots y perfiles',
|
||||
accessibility: 'Accesibilidad',
|
||||
reduceMotion: 'Reducir movimiento en animaciones de la interfaz',
|
||||
disableTappable: 'Deshabilitar área para tocar en todo el toot',
|
||||
removeEmoji: 'Eliminar emoji de nombres de usuario',
|
||||
shortAria: 'Usar etiquetas ARIA cortas para artículos',
|
||||
theme: 'Diseño visual',
|
||||
themeForInstance: 'Diseño visual para {instance}',
|
||||
disableCustomScrollbars: 'Deshabilitar barras deslizantes personalizadas',
|
||||
bottomNav: 'Situar la barra de navegación al final de la pantalla',
|
||||
centerNav: 'Centrar la barra de navegación',
|
||||
preferences: 'Preferencias',
|
||||
hotkeySettings: 'Opciones para atajos de teclado',
|
||||
disableHotkeys: 'Deshabilitar todos los atajos de teclado',
|
||||
leftRightArrows: 'Las flechas izquierda/derecha cambian el foco en vez de columnas/multimedia',
|
||||
guide: 'Guía',
|
||||
reload: 'Recargar',
|
||||
// Wellness settings
|
||||
wellness: 'Bienestar',
|
||||
wellnessSettings: 'Opciones para el bienestar',
|
||||
wellnessDescription: `Las opciones para el bienestar están diseñadas para reducir los aspectos que inducen adicción o ansiedad en las redes sociales.
|
||||
Elige cualquier opción que vaya bien para ti.`,
|
||||
enableAll: 'Habilitar todos',
|
||||
metrics: 'Métricas',
|
||||
hideFollowerCount: 'Ocultar recuento de seguidores (hasta 10)',
|
||||
hideReblogCount: 'Ocultar recuento de reenvíos',
|
||||
hideFavoriteCount: 'Ocultar recuento de favoritos',
|
||||
hideUnread: 'Ocultar recuento de notificaciones sin leer (es decir, el punto rojo)',
|
||||
// The quality that makes something seem important or interesting because it seems to be happening now
|
||||
immediacy: 'Inmediatez',
|
||||
showAbsoluteTimestamps: 'Mostrar marcas de tiempo absolutas (p.ej., "3 de marzo") en vez de marcas de tiempo relativas (p. ej., "hace 5 minutos")',
|
||||
ui: 'Interfaz',
|
||||
grayscaleMode: 'Modo escala de grises',
|
||||
wellnessFooter: `Estas opciones están parcialmente basadas en pautas del
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'Puedes filtrar o deshabilitar notificaciones en',
|
||||
filterNotificationsText: 'opciones para instancia',
|
||||
filterNotificationsPost: '',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: 'Deshabilitar',
|
||||
disableInfiniteScrollText: 'desplazamiento infinito',
|
||||
disableInfiniteScrollDescription: `Cuando el desplazamiento infinito esté deshabilitado, los nuevos toots no se mostrarán automáticamente al final o al principio de la cronología. En vez de esto, habrá botones que te permitirán
|
||||
cargar más contenido a demanda.`,
|
||||
disableInfiniteScrollPost: '',
|
||||
// Instance settings
|
||||
loggedInAs: 'Iniciaste sesión como',
|
||||
homeTimelineFilters: 'Filtros para la cronología Inicio',
|
||||
notificationFilters: 'Filtros para notificaciones',
|
||||
pushNotifications: 'Notificaciones Push',
|
||||
// Add instance page
|
||||
storageError: `Parece que Pinafore no puede almacenar datos localmente. ¿Está tu navegador en modo privado
|
||||
o bloqueando las cookies? Pinafore almacena todos los datos localmente, y requiere LocalStorage e
|
||||
IndexedDB para funcionar correctamente.`,
|
||||
javaScriptError: 'Debes habilitar JavaScript para iniciar sesión.',
|
||||
enterInstanceName: 'Introducir nombre de instancia',
|
||||
instanceColon: 'Instancia:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: '¿No tienes una',
|
||||
getAnInstanceText: 'instancia',
|
||||
getAnInstanceDescription: 'Una instancia es tu servidor de inicio de Mastodon, por ejemplo, mastodon.social o cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: '¡Unirse a Mastodon!',
|
||||
instancesYouveLoggedInTo: 'Instancias en las que has iniciado sesión:',
|
||||
addAnotherInstance: 'Añadir otra instancia',
|
||||
youreNotLoggedIn: 'No has iniciado sesión en ninguna instancia.',
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(instancia actual)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Inicia sesión en una instancia',
|
||||
logInToAnInstancePost: 'para empezar a usar Pinafore.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Mostrar siempre',
|
||||
showRingText: 'anillo del foco',
|
||||
showRingDescription: 'El anillo del foco es el contorno que muestra el elemento que actualmente tiene el foco. Por defecto solo se muestra cuando se usa el teclado (no el ratón o un dispositivo táctil), pero puedes elegir mostrarlo siempre.',
|
||||
showRingPost: '',
|
||||
instances: 'Instancias',
|
||||
addInstance: 'Añadir instancia',
|
||||
homeTimelineFilterSettings: 'Opciones para filtros de la cronología Inicio',
|
||||
showReblogs: 'Mostrar reenvíos',
|
||||
showReplies: 'Mostrar respuestas',
|
||||
switchOrLogOut: 'Seleccionar o cerrar sesión en esta instancia',
|
||||
switchTo: 'Seleccionar esta instancia',
|
||||
switchToInstance: 'Seleccionar instancia',
|
||||
switchToNameOfInstance: 'Seleccionar {instance}',
|
||||
logOut: 'Cerrar sesión',
|
||||
logOutOfInstanceConfirm: '¿Cerrar sesión en {instance}?',
|
||||
notificationFilterSettings: 'Opciones para filtros de notificaciones',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: 'Tu navegador no admite notificaciones Push.',
|
||||
deniedPush: 'Has denegado el permiso para mostrar notificaciones.',
|
||||
pushNotificationsNote: 'Observa que solo puedes recibir notificaciones Push para una instancia al mismo tiempo.',
|
||||
pushSettings: 'Opciones para notificaciones Push',
|
||||
newFollowers: 'Nuevos seguidores',
|
||||
reblogs: 'Reenvíos',
|
||||
pollResults: 'Resultados de encuesta',
|
||||
subscriptions: 'Suscripción a toots',
|
||||
needToReauthenticate: 'Tienes que volver a autenticarte para habilitar las notificaciones Push. ¿Cerrr sesión en {instance}?',
|
||||
failedToUpdatePush: 'Se ha producido un fallo al actualizar las opciones para notificaciones Push: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Elegir un diseño visual',
|
||||
darkBackground: 'Fondo oscuro',
|
||||
lightBackground: 'Fondo claro',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(por defecto)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Imagen animada: {description}',
|
||||
showImage: `Mostrar {animated, select,
|
||||
true {animated}
|
||||
other {}
|
||||
} imagen: {description}`,
|
||||
playVideoOrAudio: `Reproducir {audio, select,
|
||||
true {audio}
|
||||
other {vídeo}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} te siguió, {account}',
|
||||
accountSignedUp: '{name} inició sesión, {account}',
|
||||
accountRequestedFollow: '{name} solicitó seguirte, {account}',
|
||||
accountReported: '{name} creó una denuncia, {account}',
|
||||
reblogCountsHidden: 'Recuento de reenvíos oculto',
|
||||
favoriteCountsHidden: 'Recuento de favoritos oculto',
|
||||
rebloggedTimes: `Reenviado {count, plural,
|
||||
one {1 vez}
|
||||
other {{count} veces}
|
||||
}`,
|
||||
favoritedTimes: `Marcado como favorito {count, plural,
|
||||
one {1 vez}
|
||||
other {{count} veces}
|
||||
}`,
|
||||
pinnedStatus: 'Toot fijado',
|
||||
rebloggedYou: 'reenvió tu toot',
|
||||
favoritedYou: 'marcó como favorito tu toot',
|
||||
followedYou: 'te siguió',
|
||||
edited: 'editó su toot',
|
||||
requestedFollow: 'solicitó seguirte',
|
||||
reported: 'creó una denuncia',
|
||||
signedUp: 'sesión iniciada',
|
||||
posted: 'publicado',
|
||||
pollYouCreatedEnded: 'Una encuesta que creaste ha finalizado',
|
||||
pollYouVotedEnded: 'Una encuesta en la que votaste ha finalizado',
|
||||
reblogged: 'reenviado',
|
||||
favorited: 'marcado como favorito',
|
||||
unreblogged: 'no reenviado',
|
||||
unfavorited: 'no marcado como favorito',
|
||||
showSensitiveMedia: 'Mostrar multimedia sensible',
|
||||
hideSensitiveMedia: 'Ocultar multimedia sensible',
|
||||
clickToShowSensitive: 'Contenido sensible. Haz clic para mostrar.',
|
||||
longPost: 'Publicación larga',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} reenvió tu toot',
|
||||
accountFavoritedYou: '{account} marcó como favorito tu toot',
|
||||
accountEdited: '{account} editó su toot',
|
||||
rebloggedByAccount: 'reenviado por {account}',
|
||||
contentWarningContent: 'Advertencia de contenido: {spoiler}',
|
||||
hasMedia: 'tiene multimedia',
|
||||
hasPoll: 'tiene encuesta',
|
||||
shortStatusLabel: '{privacy} toot de {account}',
|
||||
// Privacy types
|
||||
public: 'Público',
|
||||
unlisted: 'No listado',
|
||||
followersOnly: 'Solo seguidores',
|
||||
direct: 'Directo',
|
||||
// Themes
|
||||
themeRoyal: 'Royal',
|
||||
themeScarlet: 'Escarlata',
|
||||
themeSeafoam: 'Espuma de mar',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Roble',
|
||||
themeMajesty: 'Majesty',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Escala de grises',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalto',
|
||||
themeSorcery: 'Sorcery',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Tono negro',
|
||||
themeDarkGrayscale: 'Escala de gris oscuro',
|
||||
// Polls
|
||||
voteOnPoll: 'Votar en encuesta',
|
||||
pollChoices: 'Opciones de la encuesta',
|
||||
vote: 'Votar',
|
||||
pollDetails: 'Detalles de la encuesta',
|
||||
refresh: 'Actualizar',
|
||||
expires: 'Finaliza',
|
||||
expired: 'Finalizada',
|
||||
voteCount: `{count, plural,
|
||||
one {1 voto}
|
||||
other {{count} votos}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - haz clic para mostrar el hilo',
|
||||
showMore: 'Mostrar más',
|
||||
showLess: 'Mostrar menos',
|
||||
closeReply: 'Cerrar respuesta',
|
||||
cannotReblogFollowersOnly: 'No se puede reenviar porque es solo para seguidores',
|
||||
cannotReblogDirectMessage: 'No se puede reenviar porque es un mensaje directo',
|
||||
reblog: 'Reenviar',
|
||||
reply: 'Responder',
|
||||
replyToThread: 'Responder al hilo',
|
||||
favorite: 'Favorito',
|
||||
unfavorite: 'No favorito',
|
||||
// timeline
|
||||
loadingMore: 'Cargando más…',
|
||||
loadMore: 'Cargar más',
|
||||
showCountMore: 'Mostrar {count} más',
|
||||
nothingToShow: 'Nada para mostrar.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Página de hilo de toots',
|
||||
status: 'Toot',
|
||||
// toast messages
|
||||
blockedAccount: 'Cuenta bloqueada',
|
||||
unblockedAccount: 'Cuenta desbloqueada',
|
||||
unableToBlock: 'No se puede bloquear la cuenta: {error}',
|
||||
unableToUnblock: 'No se puede desbloquear la cuenta: {error}',
|
||||
bookmarkedStatus: 'Toot con marcador',
|
||||
unbookmarkedStatus: 'Toot sin marcador',
|
||||
unableToBookmark: 'No se puede poner marcador: {error}',
|
||||
unableToUnbookmark: 'No se puede quitar marcador: {error}',
|
||||
cannotPostOffline: 'No puedes publicar mientras estás sin conexión',
|
||||
unableToPost: 'No se puede publicar el toot: {error}',
|
||||
statusDeleted: 'Toot borrado',
|
||||
unableToDelete: 'No se puede borrar el toot: {error}',
|
||||
cannotFavoriteOffline: 'No puedes marcar como favorito mientras estás sin conexión',
|
||||
cannotUnfavoriteOffline: 'No puedes quitar marca de favorito mientras estás sin conexión',
|
||||
unableToFavorite: 'No se puede marcar como favorito: {error}',
|
||||
unableToUnfavorite: 'No se puede quitar marca de favorito: {error}',
|
||||
followedAccount: 'Cuenta seguida',
|
||||
unfollowedAccount: 'Cuenta no seguida',
|
||||
unableToFollow: 'No se puede seguir a la cuenta: {error}',
|
||||
unableToUnfollow: 'No se puede dejar de seguir a la cuenta: {error}',
|
||||
accessTokenRevoked: 'El token de acceso fue anulado, se cerró sesión en {instance}',
|
||||
loggedOutOfInstance: 'Se cerró sesión en {instance}',
|
||||
failedToUploadMedia: 'Falló la subida del multimedia: {error}',
|
||||
mutedAccount: 'Cuenta silenciada',
|
||||
unmutedAccount: 'Cuenta no silenciada',
|
||||
unableToMute: 'No se puede silenciar la cuenta: {error}',
|
||||
unableToUnmute: 'No se puede dejar de silenciar la cuenta: {error}',
|
||||
mutedConversation: 'Conversación silenciada',
|
||||
unmutedConversation: 'Conversación no silenciada',
|
||||
unableToMuteConversation: 'No se puede silenciar la conversación: {error}',
|
||||
unableToUnmuteConversation: 'No se puede dejar de silenciar la conversación: {error}',
|
||||
unpinnedStatus: 'Toot no fijado',
|
||||
unableToPinStatus: 'No se puede fijar el toot: {error}',
|
||||
unableToUnpinStatus: 'No se puede dejar de fijar el toot: {error}',
|
||||
unableToRefreshPoll: 'No se puede actualizar la encuesta: {error}',
|
||||
unableToVoteInPoll: 'No se puede votar en la encuesta: {error}',
|
||||
cannotReblogOffline: 'No puedes reenviar mientras estás sin conexión.',
|
||||
cannotUnreblogOffline: 'No puedes deshacer reenvíos mientras estás sin conexión.',
|
||||
failedToReblog: 'Fallo al reenviar: {error}',
|
||||
failedToUnreblog: 'Fallo al deshacer reenvío: {error}',
|
||||
submittedReport: 'Denuncia enviada',
|
||||
failedToReport: 'Fallo al enviar denuncia: {error}',
|
||||
approvedFollowRequest: 'Solicitud de seguimiento aceptada',
|
||||
rejectedFollowRequest: 'Solicitud de seguimiento rechazada',
|
||||
unableToApproveFollowRequest: 'No se puede aceptar la solicitud de seguimiento: {error}',
|
||||
unableToRejectFollowRequest: 'No se puede rechazar la solicitud de seguimiento: {error}',
|
||||
searchError: 'Error durante la búsqueda: {error}',
|
||||
hidDomain: 'Dominio oculto',
|
||||
unhidDomain: 'Dominio no oculto',
|
||||
unableToHideDomain: 'No se puede ocultar el dominio: {error}',
|
||||
unableToUnhideDomain: 'No se puede dejar de ocultar el dominio: {error}',
|
||||
showingReblogs: 'Mostrando reenvíos',
|
||||
hidingReblogs: 'Ocultando reenvíos',
|
||||
unableToShowReblogs: 'No se puede mostrar los reenvíos: {error}',
|
||||
unableToHideReblogs: 'No se puede ocultar los reenvíos: {error}',
|
||||
unableToShare: 'No se puede compartir: {error}',
|
||||
unableToSubscribe: 'Imposible suscribirse: {error}',
|
||||
unableToUnsubscribe: 'Imposible dejar de suscribirse: {error}',
|
||||
showingOfflineContent: 'La petición a internet falló. Mostrando contenido sin conexión.',
|
||||
youAreOffline: 'Parece que estás sin conexión. Puedes leer contenido incluso sin conexión.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'Actualización de la aplicación disponible.',
|
||||
// Word/phrase filters
|
||||
wordFilters: 'Filtros de palabras',
|
||||
noFilters: 'No tienes ningún filtro de palabras.',
|
||||
wordOrPhrase: 'Palabra o frase',
|
||||
contexts: 'Contextos',
|
||||
addFilter: 'Añadir filtro',
|
||||
editFilter: 'Editar filtro',
|
||||
filterHome: 'Inicio y listas',
|
||||
filterNotifications: 'Notificaciones',
|
||||
filterPublic: 'Cronologías públicas',
|
||||
filterThread: 'Conversaciones',
|
||||
filterAccount: 'Perfiles',
|
||||
filterUnknown: 'Desconocido',
|
||||
expireAfter: 'Expira al cabo de',
|
||||
whereToFilter: 'Dónde filtrar',
|
||||
irreversible: 'Irreversible',
|
||||
wholeWord: 'Palabra completa',
|
||||
save: 'Guardar',
|
||||
updatedFilter: 'Filtro actualizado',
|
||||
createdFilter: 'Filtro creado',
|
||||
failedToModifyFilter: 'Fallo al modificar el filtro: {error}',
|
||||
deletedFilter: 'Filtro borrado',
|
||||
required: 'Requerido',
|
||||
// Dialogs
|
||||
profileOptions: 'Opciones de perfil',
|
||||
copyLink: 'Copiar enlace',
|
||||
emoji: 'Emoji',
|
||||
editMedia: 'Editar multimedia',
|
||||
shortcutHelp: 'Ayuda sobre atajos de teclado',
|
||||
statusOptions: 'Opciones de estado',
|
||||
confirm: 'Confirmar',
|
||||
closeDialog: 'Cerrar diálogo',
|
||||
postPrivacy: 'Privacidad del post',
|
||||
homeOnInstance: 'Inicio en {instance}',
|
||||
statusesTimelineOnInstance: 'Estados: {timeline} cronología en {instance}',
|
||||
statusesHashtag: 'Estados: #{hashtag} hashtag',
|
||||
statusesThread: 'Estados: hilo',
|
||||
statusesAccountTimeline: 'Estado: cronología de cuenta',
|
||||
statusesList: 'Estado: lista',
|
||||
notificationsOnInstance: 'Notificaciones en {instance}'
|
||||
}
|
|
@ -0,0 +1,627 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'Un client alternatif pour Mastodon, concentré sur la vitesse et la simplicité',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore est un client web pour
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
dessiné pour la vitesse et la simplicité.
|
||||
</p>
|
||||
<p>
|
||||
Lire
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">l'article introductoire (anglais)</a>,
|
||||
ou se connecter à une instance:
|
||||
</p>`,
|
||||
logIn: 'Se connecter',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore est
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">logiciel open-source</a>
|
||||
créé par
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
et distribué sous la
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License AGPL</a>.
|
||||
Lire la <a href="/settings/about#privacy-policy" rel="prefetch">politique de confidentialité</a>.
|
||||
</p>
|
||||
`,
|
||||
// Generic UI
|
||||
loading: 'Chargement en cours',
|
||||
okay: 'OK',
|
||||
cancel: 'Annuler',
|
||||
alert: 'Alerte',
|
||||
close: 'Fermer',
|
||||
error: 'Erreur: {error}',
|
||||
errorShort: 'Erreur:',
|
||||
// Relative timestamps
|
||||
justNow: 'il y a un moment',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(page actuelle)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(1 notification)}
|
||||
other {({count} notifications)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(1 demande de suivre)}
|
||||
other {({count} demandes de suivre)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Utilisateurs bloqués',
|
||||
bookmarks: 'Signets',
|
||||
directMessages: 'Messages directs',
|
||||
favorites: 'Favoris',
|
||||
federated: 'Fédéré',
|
||||
home: 'Accueil',
|
||||
local: 'Local',
|
||||
notifications: 'Notifications',
|
||||
mutedUsers: 'Utilisateurs mis en sourdine',
|
||||
pinnedStatuses: 'Pouets épinglés',
|
||||
followRequests: 'Demandes de suivre',
|
||||
followRequestsLabel: `Demandes de suivre {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'Liste',
|
||||
search: 'Recherche',
|
||||
pageHeader: 'Titre de page',
|
||||
goBack: 'Rentrer',
|
||||
back: 'Rentrer',
|
||||
profile: 'Profil',
|
||||
federatedTimeline: 'Historique fédéré',
|
||||
localTimeline: 'Historique local',
|
||||
// community page
|
||||
community: 'Communauté',
|
||||
pinnableTimelines: 'Historiques épinglables',
|
||||
timelines: 'Historiques',
|
||||
lists: 'Listes',
|
||||
instanceSettings: "Paramètres d'instance",
|
||||
notificationMentions: 'Notifications de mention',
|
||||
profileWithMedia: 'Profil avec medias',
|
||||
profileWithReplies: 'Profil avec réponses',
|
||||
hashtag: 'Mot-dièse',
|
||||
// not logged in
|
||||
profileNotLoggedIn: "Un historique d'utilisateur s'apparêtra ici quand on est conncté.",
|
||||
bookmarksNotLoggedIn: "Vos signets s'apparêtront ici quand on est conncté.",
|
||||
directMessagesNotLoggedIn: "Vos messages directes s'apparêtront ici quand on est conncté.",
|
||||
favoritesNotLoggedIn: "Vos favoris s'apparêtront ici quand on est conncté.",
|
||||
federatedTimelineNotLoggedIn: "L'historique fédéré s'apparêtra ici quand on est conncté.",
|
||||
localTimelineNotLoggedIn: "L'historique local s'apparêtra ici quand on est conncté.",
|
||||
searchNotLoggedIn: "On peut rechercher dès qu'on est conncté.",
|
||||
communityNotLoggedIn: "Les paramètres de commnautés s'apparêtront ici quand on est conncté.",
|
||||
listNotLoggedIn: "Une liste s'apparêtra ici dès qu'on est conncté.",
|
||||
notificationsNotLoggedIn: "Vos notifications s'apparêtront ici quand on est conncté.",
|
||||
notificationMentionsNotLoggedIn: "Vos notifications de mention s'apparêtront ici quand on est conncté.",
|
||||
statusNotLoggedIn: "Un historique de pouet s'apparêtra ici quand on est conncté.",
|
||||
tagNotLoggedIn: "Un historique de mot-dièse s'apparêtra ici quand on est conncté.",
|
||||
// Notification subpages
|
||||
filters: 'Filtres',
|
||||
all: 'Tous',
|
||||
mentions: 'Mentions',
|
||||
// Follow requests
|
||||
approve: 'Accepter',
|
||||
reject: 'Rejeter',
|
||||
// Hotkeys
|
||||
hotkeys: 'Raccourcis clavier',
|
||||
global: 'Global',
|
||||
timeline: 'Historique',
|
||||
media: 'Medias',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> pour changer de focus à l'élément suivant</li>
|
||||
<li><kbd>←</kbd> pour changer de focus à l'élément précédent</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {ou <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
pour changer de pages
|
||||
</li>
|
||||
<li><kbd>7</kbd> or <kbd>c</kbd> pour écrire un nouveau pouet</li>
|
||||
<li><kbd>s</kbd> or <kbd>/</kbd> pour rechercher</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> pour renter à l'acceuil</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> pour voir les notifications</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> pour voir l'historique local</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> pour voir l'historique fédéré</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> pour voir les paramètres de communauté</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> pour voir les messages directs</li>
|
||||
<li><kbd>h</kbd> ou <kbd>?</kbd> pour voir les raccourcis clavier</li>
|
||||
<li><kbd>Retour arrière</kbd> pour rentrer à la page précédente, ou fermer une boite de dialogue</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> ou <kbd>↓</kbd> pour activer le pouet suivant</li>
|
||||
<li><kbd>k</kbd> ou <kbd>↑</kbd> pour activer le pouet précedent</li>
|
||||
<li><kbd>.</kbd> pour afficher les nouveaus messages et renter en haut</li>
|
||||
<li><kbd>o</kbd> pour ouvrir</li>
|
||||
<li><kbd>f</kbd> pour ajouter aux favoris</li>
|
||||
<li><kbd>b</kbd> pour partager</li>
|
||||
<li><kbd>r</kbd> pour répondre</li>
|
||||
<li><kbd>i</kbd> pour voir une image, vidéo, ou audio</li>
|
||||
<li><kbd>y</kbd> pour afficher ou cacher une image sensible</li>
|
||||
<li><kbd>m</kbd> pour mentionner l'auteur</li>
|
||||
<li><kbd>p</kbd> pour voir le profile de l'auteur</li>
|
||||
<li><kbd>l</kbd> pour ouvrir un lien de carte dans un nouvel onglet</li>
|
||||
<li><kbd>x</kbd> pour afficher ou cacher le texte caché derrière une avertissement</li>
|
||||
<li><kbd>z</kbd> pour afficher ou cacher toutes les avertissements</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> pour voir la prochaine ou dernière image</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Actuel)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(Page épinglée)}
|
||||
other {(Page non-épinglée)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Epingler {label}',
|
||||
// Status composition
|
||||
composeStatus: 'Ecrire un pouet',
|
||||
postStatus: 'Pouet!',
|
||||
contentWarning: 'Avertissement',
|
||||
dropToUpload: 'Déposer',
|
||||
invalidFileType: "Impossible d'uploader ce type de fichier",
|
||||
composeLabel: "Qu'avez vous en tête?",
|
||||
autocompleteDescription: 'Quand les résultats sont dispibles, appuyez la fleche vers le haut ou vers le bas pour selectionner.',
|
||||
mediaUploads: 'Medias uploadés',
|
||||
edit: 'Rediger',
|
||||
delete: 'Supprimer',
|
||||
description: 'Déscription',
|
||||
descriptionLabel: 'Décrire pour les aveugles (image, video) ou les sourds (audio, video)',
|
||||
markAsSensitive: 'Désigner comme sensible',
|
||||
// Polls
|
||||
createPoll: 'Créer une enquête',
|
||||
removePollChoice: 'Supprimer la choix {index}',
|
||||
pollChoiceLabel: 'Choix {index}',
|
||||
multipleChoice: 'Choix multiple',
|
||||
pollDuration: "Duration de l'enquête",
|
||||
fiveMinutes: '5 minutes',
|
||||
thirtyMinutes: '30 minutes',
|
||||
oneHour: '1 heure',
|
||||
sixHours: '6 heures',
|
||||
oneDay: '1 jour',
|
||||
threeDays: '3 jours',
|
||||
sevenDays: '7 jours',
|
||||
addEmoji: 'Insérer un emoji',
|
||||
addMedia: 'Ajouter un media (images, vidéos, audios)',
|
||||
addPoll: 'Ajouter une enquête',
|
||||
removePoll: "Enlever l'enquête",
|
||||
postPrivacyLabel: 'Changer de confidentialité (actuellement {label})',
|
||||
addContentWarning: 'Ajouter une avertissement',
|
||||
removeContentWarning: "Enlever l'avertissement",
|
||||
altLabel: 'Décrire pour les aveugles ou les sourds',
|
||||
extractText: "Extraire le texte de l'image",
|
||||
extractingText: 'Extraction de texte en cours…',
|
||||
extractingTextCompletion: 'Extraction de texte en cours ({percent}% finit)…',
|
||||
unableToExtractText: "Impossible d'extraire le texte.",
|
||||
// Account options
|
||||
followAccount: 'Suivre {account}',
|
||||
unfollowAccount: 'Ne plus suivre {account}',
|
||||
blockAccount: 'Bloquer {account}',
|
||||
unblockAccount: 'Ne plus bloquer {account}',
|
||||
muteAccount: 'Mettre {account} en sourdine',
|
||||
unmuteAccount: 'Ne plus mettre {account} en sourdine',
|
||||
showReblogsFromAccount: 'Afficher les partages de {account}',
|
||||
hideReblogsFromAccount: 'Ne plus afficher les partages de {account}',
|
||||
showDomain: 'Ne plus cacher {domain}',
|
||||
hideDomain: 'Cacher {domain}',
|
||||
reportAccount: 'Signaler {account}',
|
||||
mentionAccount: 'Mentionner {account}',
|
||||
copyLinkToAccount: 'Copier un lien vers ce compte',
|
||||
copiedToClipboard: 'Copié vers le presse-papiers',
|
||||
// Media dialog
|
||||
navigateMedia: 'Changer de medias',
|
||||
showPreviousMedia: 'Afficher le media précédent',
|
||||
showNextMedia: 'Afficher le media suivant',
|
||||
enterPinchZoom: 'Pincer pour zoomer',
|
||||
exitPinchZoom: 'Ne plus pincer pour zoomer',
|
||||
showMedia: `Afficher le {index, select,
|
||||
1 {premier}
|
||||
2 {deuxième}
|
||||
3 {troisième}
|
||||
other {quatrième}
|
||||
} média {current, select,
|
||||
true {(actuel)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Aperçu (point de mire)',
|
||||
enterFocalPoint: 'Saisir le point de mire (X, Y) pour ce média',
|
||||
muteNotifications: 'Mettre aussi bien les notifications en sourdine',
|
||||
muteAccountConfirm: 'Mettre {account} en sourdine?',
|
||||
mute: 'Mettre en sourdine',
|
||||
unmute: 'Ne plus mettre en sourdine',
|
||||
zoomOut: 'Dé-zoomer',
|
||||
zoomIn: 'Zoomer',
|
||||
// Reporting
|
||||
reportingLabel: 'Vous signalez {account} aux modérateurs/modératrices de {instance}.',
|
||||
additionalComments: 'Commentaires additionels',
|
||||
forwardDescription: 'Faire parvenir aux modérateurs/modératrices de {instance} aussi?',
|
||||
forwardLabel: 'Fair pervenir à {instance}',
|
||||
unableToLoadStatuses: 'Impossible de charger les pouets récents: {error}',
|
||||
report: 'Signaler',
|
||||
noContent: '(Pas de contenu)',
|
||||
noStatuses: 'Aucun pouet à signaler',
|
||||
// Status options
|
||||
unpinFromProfile: 'Ne plus épingler sur son profil',
|
||||
pinToProfile: 'Epingler sur son profil',
|
||||
muteConversation: 'Mettre en sourdine la conversation',
|
||||
unmuteConversation: 'Ne plus mettre en sourdine la conversation',
|
||||
bookmarkStatus: 'Ajouter aux signets',
|
||||
unbookmarkStatus: 'Enlever des signets',
|
||||
deleteAndRedraft: 'Supprimer et rediger',
|
||||
reportStatus: 'Signaler ce pouet',
|
||||
shareStatus: 'Partager ce pouet externellement',
|
||||
copyLinkToStatus: 'Copier un lien vers ce pouet',
|
||||
// Account profile
|
||||
profileForAccount: 'Profil pour {account}',
|
||||
statisticsAndMoreOptions: "Statistiques et plus d'options",
|
||||
statuses: 'Pouets',
|
||||
follows: 'Suis',
|
||||
followers: 'Suivants',
|
||||
moreOptions: "Plus d'options",
|
||||
followersLabel: 'Suivi(e) par {count}',
|
||||
followingLabel: 'Suis {count}',
|
||||
followLabel: `Suivre {requested, select,
|
||||
true {(suivre demandé)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Ne plus suivre {requested, select,
|
||||
true {(suivre demandé)}
|
||||
other {}
|
||||
}`,
|
||||
unblock: 'Ne plus bloquer',
|
||||
nameAndFollowing: 'Nom et suivants',
|
||||
clickToSeeAvatar: "Cliquer pour voir l'image de profile",
|
||||
opensInNewWindow: '{label} (ouvrir dans un nouvel onglet)',
|
||||
blocked: 'Bloquer',
|
||||
domainHidden: 'Domaine bloqué',
|
||||
muted: 'Mis en sourdine',
|
||||
followsYou: 'Suivant',
|
||||
avatarForAccount: 'Image de profil pour {account}',
|
||||
fields: 'Champs',
|
||||
accountHasMoved: '{account} a déménagé',
|
||||
profilePageForAccount: 'Page de profil pour {account}',
|
||||
// About page
|
||||
about: 'Infos',
|
||||
aboutApp: 'Infos sur Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore est un logiciel
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">gratuit et open-source</a>
|
||||
créé par
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
et distribué sous le
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License GNU Affero General Public (AGPL)</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Politique de confidentialité</h2>
|
||||
|
||||
<p>
|
||||
Pinafore ne garde pas d'informations personelles dans ses serveurs,
|
||||
y compris les noms, addresses courriel, addresses IP, messages, et photos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore est un site statique. Tous données sont gardées en locale dans le navigateur, et sont partagée qu'avec
|
||||
les instances auxquelles vous vous connectez.
|
||||
</p>
|
||||
|
||||
<h2>Crédits</h2>
|
||||
|
||||
<p>
|
||||
Icônes par <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Logo grâce à Gregor Cresnar du
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/">Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Paramètres',
|
||||
general: 'Général',
|
||||
generalSettings: 'Paramètres générales',
|
||||
showSensitive: 'Afficher les medias sensible par défaut',
|
||||
showPlain: 'Afficher un simple gris pour les medias sensibles',
|
||||
allSensitive: 'Considérer tous medias comme sensible',
|
||||
largeMedia: 'Afficher de plus grands images et vidéos',
|
||||
autoplayGifs: 'Repasser automatiquement les GIFs animés',
|
||||
hideCards: 'Cacher les liens «cartes»',
|
||||
underlineLinks: 'Souligner les liens dans les pouets et profils',
|
||||
accessibility: 'Accessibilité',
|
||||
reduceMotion: 'Reduire la motions dans les animations',
|
||||
disableTappable: "Désactiver l'espace touchable sur un pouet entier",
|
||||
removeEmoji: "Enlever les emojis des noms d'utilisateur",
|
||||
shortAria: 'Utiliser des etiquettes courtes ARIA',
|
||||
theme: 'Thème',
|
||||
themeForInstance: 'Theème pour {instance}',
|
||||
disableCustomScrollbars: 'Désactiver les scrollbars customisés',
|
||||
preferences: 'Préférences',
|
||||
hotkeySettings: 'Paramètres de raccourcis clavier',
|
||||
disableHotkeys: 'Désactiver les raccourcis clavier',
|
||||
leftRightArrows: 'Les flèches gauche/droit change de focus plutôt que les pages',
|
||||
guide: 'Guide',
|
||||
reload: 'Recharger',
|
||||
// Wellness settings
|
||||
wellness: 'Bien-être',
|
||||
wellnessSettings: 'Paramètres de bien-être',
|
||||
wellnessDescription: `Les paramètres de bien-être sont dessinées pour rédruire les effets accrochants ou d'anxiété des réseaux sociaux.
|
||||
Veuillez choisir les options qui marchent pour vous.`,
|
||||
enableAll: 'Activer tous',
|
||||
metrics: 'Métrics',
|
||||
hideFollowerCount: 'Cacher le nombre de suivants (10 maximum)',
|
||||
hideReblogCount: 'Cacher le nombre de partages',
|
||||
hideFavoriteCount: 'Cacher le nombre de favoris',
|
||||
hideUnread: "Cacher le nombre de notifications (c'est-à-dire le point rouge)",
|
||||
ui: 'Interface Utilisateur',
|
||||
grayscaleMode: 'Mode echelle de gris',
|
||||
wellnessFooter: `Ces paramètres sont basé sur les recommendations du
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'Vous pouvez filtrer ou désactiver les notifications dans les',
|
||||
filterNotificationsText: "paramètres d'instance",
|
||||
filterNotificationsPost: '',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: 'Désactiver le',
|
||||
disableInfiniteScrollText: 'défilage infini',
|
||||
disableInfiniteScrollDescription: `Quand le défilage infini est désactivé, les pouets nouveau ne
|
||||
s'apparêtront pas automatique au haut ou au bas de l'historique. Plutôt, il y aura des boutons pour
|
||||
charger sur demande.`,
|
||||
disableInfiniteScrollPost: '',
|
||||
// Instance settings
|
||||
loggedInAs: 'Connecté en tant que',
|
||||
homeTimelineFilters: "Filtres d'historique de l'acceuil",
|
||||
notificationFilters: 'Filtres de notifications',
|
||||
pushNotifications: 'Filtres de notifications push',
|
||||
// Add instance page
|
||||
storageError: `Il semble que Pinafore ne peut pas stocker les données en locale. Est-ce que votre navigateur
|
||||
est en mode privé, ou est-ce qu'il bloque les cookies? Pinafore garde tous ses données en locale et
|
||||
ne peut pas fonctionner sans LocalStorage ou IndexedDB.`,
|
||||
javaScriptError: 'Le JavaScript devrait être activé pour continuer.',
|
||||
enterInstanceName: "Saisir le nom d'instance",
|
||||
instanceColon: 'Instance:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: "N'avez-vous pas d'",
|
||||
getAnInstanceText: 'instance',
|
||||
getAnInstanceDescription: 'Une instance est votre serveur Mastodon, par exemple mastodon.social ou cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: 'Joignez-vous à Mastodon!',
|
||||
instancesYouveLoggedInTo: 'Instances conntectées:',
|
||||
addAnotherInstance: 'Ajouter une nouvelle instance',
|
||||
youreNotLoggedIn: 'Vous êtes connecté(e) à aucune instance.',
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(instance actuelle)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Se connecter à une instance',
|
||||
logInToAnInstancePost: 'pour utiliser Pinafore.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Afficher toujours',
|
||||
showRingText: "l'anneau de focus",
|
||||
showRingDescription: `L'anneau de focus est le contour qui indique l'élément en focus actuel. Par défaut, ce n'est
|
||||
affiché que quand on utilise le clavier (et ne pas la souris ou l'écran touche), mais vous pouvez choisr de
|
||||
l'afficher toujours.`,
|
||||
showRingPost: '',
|
||||
instances: 'Les instances',
|
||||
addInstance: 'Ajouter une instance',
|
||||
homeTimelineFilterSettings: "Paramètres de filtre d'historique",
|
||||
showReblogs: 'Afficher les partages',
|
||||
showReplies: 'Afficher les réponses',
|
||||
switchOrLogOut: 'Changer ou se déconnecter de cette instance',
|
||||
switchTo: "Changer d'instance à celle-ci",
|
||||
switchToInstance: "Changer d'instance",
|
||||
switchToNameOfInstance: "Faire {instance} l'instance actuelle",
|
||||
logOut: 'Se déconnecter',
|
||||
logOutOfInstanceConfirm: 'Déconnectez-vous de {instance}?',
|
||||
notificationFilterSettings: 'Paramètres de filtre de notifications',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: 'Votre navigateur ne soutient pas les notifications push.',
|
||||
deniedPush: 'Vous avez désactivé les notifications push.',
|
||||
pushNotificationsNote: 'Veuillez noter que les notifications push ne peuvent être activées que pour une instance à la fois.',
|
||||
pushSettings: 'Paramètres de notifications push',
|
||||
newFollowers: 'Suivants nouveaux',
|
||||
reblogs: 'Partages',
|
||||
pollResults: "Résultats d'enquête",
|
||||
needToReauthenticate: 'Vous devez ré-authentiquer pour activer les notifications push. Déconnectez-vous de {instance}?',
|
||||
failedToUpdatePush: 'Impossible de mettre à jour les paramètres de notifications push: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Choisir une thème',
|
||||
darkBackground: 'Sombre',
|
||||
lightBackground: 'Clair',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(défaut)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Image animée: {description}',
|
||||
showImage: `Afficher l'image {animated, select,
|
||||
true {animée}
|
||||
other {}
|
||||
}: {description}`,
|
||||
playVideoOrAudio: `Repasser {audio, select,
|
||||
true {l'audio}
|
||||
other {la vidéo}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} vous a suivi(e), {account}',
|
||||
reblogCountsHidden: 'Nombre de partages caché',
|
||||
favoriteCountsHidden: 'nombre de mises en favori caché',
|
||||
rebloggedTimes: `Partagé {count, plural,
|
||||
one {une fois}
|
||||
other {{count} fois}
|
||||
}`,
|
||||
favoritedTimes: `Mis en favori {count, plural,
|
||||
one {une fois}
|
||||
other {{count} fois}
|
||||
}`,
|
||||
pinnedStatus: 'Pouet épinglé',
|
||||
rebloggedYou: 'a partagé votre pouet',
|
||||
favoritedYou: 'a mis en favori votre pouet',
|
||||
followedYou: 'followed you',
|
||||
pollYouCreatedEnded: 'Une enquête vous avez créée a terminée',
|
||||
pollYouVotedEnded: 'Une enquête dans laquelle vous avez voté a terminée',
|
||||
reblogged: 'partagé',
|
||||
showSensitiveMedia: 'Afficher la média sensible',
|
||||
hideSensitiveMedia: 'Cacher la média sensible',
|
||||
clickToShowSensitive: 'Image sensible. Cliquer pour afficher.',
|
||||
longPost: 'Pouet long',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} a partagé votre pouet',
|
||||
accountFavoritedYou: '{account} a mis votre pouet en favori',
|
||||
rebloggedByAccount: 'Partagé par {account}',
|
||||
contentWarningContent: 'Avertissement: {spoiler}',
|
||||
hasMedia: 'média',
|
||||
hasPoll: 'enquête',
|
||||
shortStatusLabel: 'Pouet {privacy} par {account}',
|
||||
// Privacy types
|
||||
public: 'Publique',
|
||||
unlisted: 'Non listé',
|
||||
followersOnly: 'Abonnés/abonnées uniquement',
|
||||
direct: 'Direct',
|
||||
// Themes
|
||||
themeRoyal: 'Royale',
|
||||
themeScarlet: 'Ecarlate',
|
||||
themeSeafoam: 'Ecume',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Chêne',
|
||||
themeMajesty: 'Majesté',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Echelle gris',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalt',
|
||||
themeSorcery: 'Sorcellerie',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Noir complet',
|
||||
themeDarkGrayscale: 'Echelle gris sombre',
|
||||
// Polls
|
||||
voteOnPoll: 'Voter dans cette enquête',
|
||||
pollChoices: 'Choix',
|
||||
vote: 'Voter',
|
||||
pollDetails: 'Détails',
|
||||
refresh: 'Recharger',
|
||||
expires: 'Se termine',
|
||||
expired: 'Terminée',
|
||||
voteCount: `{count, plural,
|
||||
one {1 vote}
|
||||
other {{count} votes}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - cliquer pour afficher le discussion',
|
||||
showMore: 'Afficher plus',
|
||||
showLess: 'Afficher moins',
|
||||
closeReply: 'Fermer la réponse',
|
||||
cannotReblogFollowersOnly: "Impossible de partager car ce pouet n'est que pour les abonné(e)s",
|
||||
cannotReblogDirectMessage: 'Impossible de partager car ce pouet est direct',
|
||||
reblog: 'Partager',
|
||||
reply: 'Répondre',
|
||||
replyToThread: 'Répondre au discussion',
|
||||
favorite: 'Mettre en favori',
|
||||
unfavorite: 'Ne plus mettre en favori',
|
||||
// timeline
|
||||
loadingMore: 'Chargement en cours…',
|
||||
loadMore: 'Charger plus',
|
||||
showCountMore: 'Afficher {count} de plus',
|
||||
nothingToShow: 'Rien à afficher.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Page de discussion',
|
||||
status: 'Pouet',
|
||||
// toast messages
|
||||
blockedAccount: 'Compte bloqué',
|
||||
unblockedAccount: 'Compte ne plus bloqué',
|
||||
unableToBlock: 'Impossible de bloquer ce compte: {error}',
|
||||
unableToUnblock: 'Impossible de ne plus bloquer ce compte: {error}',
|
||||
bookmarkedStatus: 'Ajouté aux signets',
|
||||
unbookmarkedStatus: 'Enlever des signets',
|
||||
unableToBookmark: "Impossible d'ajouter aux signets: {error}",
|
||||
unableToUnbookmark: "Impossible d'enlever des signets: {error}",
|
||||
cannotPostOffline: 'Vous ne pouvez pas poueter car vous êtes hors connexion',
|
||||
unableToPost: 'Impossible de poueter: {error}',
|
||||
statusDeleted: 'Pouet supprimé',
|
||||
unableToDelete: 'Impossible de supprimer: {error}',
|
||||
cannotFavoriteOffline: 'Vous ne pouvez pas mettre en favori car vous êtes hors connexion',
|
||||
cannotUnfavoriteOffline: 'Vous ne pouvez pas enlever des favoris car vous êtes hors connexion',
|
||||
unableToFavorite: 'Impossible de mettre en favori: {error}',
|
||||
unableToUnfavorite: "Impossible d'enlever des favoris: {error}",
|
||||
followedAccount: 'Compte suivi',
|
||||
unfollowedAccount: 'Compte ne plus suivi',
|
||||
unableToFollow: 'Impossible de suivre: {error}',
|
||||
unableToUnfollow: 'Impossible de ne plus suivre: {error}',
|
||||
accessTokenRevoked: 'Authentication revoquée, déconnecté de {instance}',
|
||||
loggedOutOfInstance: 'Déconnecté de {instance}',
|
||||
failedToUploadMedia: "Impossible d'uploader: {error}",
|
||||
mutedAccount: 'Compte mis en sourdine',
|
||||
unmutedAccount: 'Compte ne plus mis en sourdine',
|
||||
unableToMute: 'Impossible de mettre en sourdine: {error}',
|
||||
unableToUnmute: 'Impossible de plus mettre en sourdine: {error}',
|
||||
mutedConversation: 'Conversation mis en sourdine',
|
||||
unmutedConversation: 'Conversation ne plus mis en sourdine',
|
||||
unableToMuteConversation: 'Impossible de mettre en sourdine: {error}',
|
||||
unableToUnmuteConversation: 'Impossible de ne plus mettre en sourdine: {error}',
|
||||
unpinnedStatus: 'Pouet ne plus épinglé',
|
||||
unableToPinStatus: "Impossible d'épingler: {error}",
|
||||
unableToUnpinStatus: 'Impossible de ne plus épingler: {error}',
|
||||
unableToRefreshPoll: 'Impossible de recharger: {error}',
|
||||
unableToVoteInPoll: 'Impossible de voter: {error}',
|
||||
cannotReblogOffline: 'Vous ne pouvez pas partager car vous êtes hors de connexion.',
|
||||
cannotUnreblogOffline: 'Vous ne pouvez pas ne plus partager car vous êtes hors de connexion.',
|
||||
failedToReblog: 'Impossible de partager: {error}',
|
||||
failedToUnreblog: 'Impossible de ne plus partager: {error}',
|
||||
submittedReport: 'Report signalé',
|
||||
failedToReport: 'Impossible de signaler: {error}',
|
||||
approvedFollowRequest: 'Demande de suivre approuvée',
|
||||
rejectedFollowRequest: 'Demande de suivre rejetée',
|
||||
unableToApproveFollowRequest: "Impossible d'appouver: {error}",
|
||||
unableToRejectFollowRequest: 'Impossible de rejeter: {error}',
|
||||
searchError: 'Erreur de recherche: {error}',
|
||||
hidDomain: 'Domaine cachée',
|
||||
unhidDomain: 'Domaine ne plus cachée',
|
||||
unableToHideDomain: 'Impossible de cacher la domaine: {error}',
|
||||
unableToUnhideDomain: 'Imipossible de ne plus cacher la domaine: {error}',
|
||||
showingReblogs: 'Partages affichés',
|
||||
hidingReblogs: 'Partages ne plus affichés',
|
||||
unableToShowReblogs: "Impossible d'afficher les partages: {error}",
|
||||
unableToHideReblogs: 'Impossible de ne plus afficher les partages: {error}',
|
||||
unableToShare: 'Impossible de partager externellement: {error}',
|
||||
showingOfflineContent: "Requête d'internet impossible. Contenu hors de connexion affiché.",
|
||||
youAreOffline: 'Il semble que vous êtes hors de connextion. Vous pouvez toujours lire les pouets dans cet état.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'Mise à jour disponible.'
|
||||
}
|
|
@ -0,0 +1,690 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'Альтернативный веб-клиент для Mastodon, ориентированный на скорость и простоту.',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore — веб-клиент для
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
разработан для скорости и простоты.
|
||||
</p>
|
||||
<p>
|
||||
Прочитайте
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">вводную запись в блоге</a>,
|
||||
или начните работу, войдя в инстанс:
|
||||
</p>`,
|
||||
logIn: 'Войти',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore — это
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">программное обеспечение с открытым исходным кодом</a>
|
||||
созданное
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
|
||||
и распространяемое под лицензией
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>.
|
||||
Здесь <a href="/settings/about#privacy-policy" rel="prefetch">политика конфиденциальности</a>.
|
||||
</p>
|
||||
`,
|
||||
// Manifest
|
||||
longAppName: 'Pinafore для Mastodon',
|
||||
newStatus: 'Новая запись',
|
||||
// Generic UI
|
||||
loading: 'Загрузка',
|
||||
okay: 'OK',
|
||||
cancel: 'Отмена',
|
||||
alert: 'Оповещение',
|
||||
close: 'Закрыть',
|
||||
error: 'Ошибка: {error}',
|
||||
errorShort: 'Ошибка:',
|
||||
// Relative timestamps
|
||||
justNow: 'только что',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(current page)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(1 notification)}
|
||||
other {({count} notifications)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(1 follow request)}
|
||||
other {({count} follow requests)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Заблокированные пользователи',
|
||||
bookmarks: 'Закладки',
|
||||
directMessages: 'Личные сообщения',
|
||||
favorites: 'Избранное',
|
||||
federated: 'Федеративное',
|
||||
home: 'Главная',
|
||||
local: 'Локальная',
|
||||
notifications: 'Уведомления',
|
||||
mutedUsers: 'Игнорируемые пользователи',
|
||||
pinnedStatuses: 'Закрепленные записи',
|
||||
followRequests: 'Запросы на подписку',
|
||||
followRequestsLabel: `Запросы на подписку {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'Список',
|
||||
search: 'Поиск',
|
||||
pageHeader: 'Заголовок страницы',
|
||||
goBack: 'Вернуться назад',
|
||||
back: 'Назад',
|
||||
profile: 'Профиль',
|
||||
federatedTimeline: 'Глобальная лента',
|
||||
localTimeline: 'Локальная лента',
|
||||
// community page
|
||||
community: 'Сообщество',
|
||||
pinnableTimelines: 'Закрепляемые ленты',
|
||||
timelines: 'Ленты',
|
||||
lists: 'Списки',
|
||||
instanceSettings: 'Настройки инстанса',
|
||||
notificationMentions: 'Уведомление упоминаний',
|
||||
profileWithMedia: 'Профиль с медиа',
|
||||
profileWithReplies: 'Профиль с ответами',
|
||||
hashtag: 'Хэштег',
|
||||
// not logged in
|
||||
profileNotLoggedIn: 'При входе в систему здесь появится лента пользователя.',
|
||||
bookmarksNotLoggedIn: 'Ваши закладки появятся здесь после входа в систему.',
|
||||
directMessagesNotLoggedIn: 'Ваши личные сообщения будут отображаться здесь после входа в систему.',
|
||||
favoritesNotLoggedIn: 'Ваше избранное появится здесь после входа в систему.',
|
||||
federatedTimelineNotLoggedIn: 'Ваша глобальная лента появится здесь после входа в систему.',
|
||||
localTimelineNotLoggedIn: 'Ваша локальная лента появится здесь после входа в систему.',
|
||||
searchNotLoggedIn: 'Вы можете выполнять поиск после входа в инстанс.',
|
||||
communityNotLoggedIn: 'Параметры сообщества появится здесь при входе в систему.',
|
||||
listNotLoggedIn: 'Список появится здесь после входа в систему.',
|
||||
notificationsNotLoggedIn: 'Ваши уведомления будут отображаться здесь после входа в систему.',
|
||||
notificationMentionsNotLoggedIn: 'Ваши уведомления с упоминаниями будут отображаться здесь после входа в систему.',
|
||||
statusNotLoggedIn: 'При входе в систему здесь появится тред сообщений.',
|
||||
tagNotLoggedIn: 'При входе в систему здесь появится лента с хэштегом.',
|
||||
// Notification subpages
|
||||
filters: 'Фильтры',
|
||||
all: 'Все',
|
||||
mentions: 'Упоминания',
|
||||
// Follow requests
|
||||
approve: 'Одобрить',
|
||||
reject: 'Отклонить',
|
||||
// Hotkeys
|
||||
hotkeys: 'Горячие клавиши',
|
||||
global: 'Глобальная',
|
||||
timeline: 'Лента',
|
||||
media: 'Медиа',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> перейти к следующему элементу</li>
|
||||
<li><kbd>←</kbd> перейти к предыдущему элементу</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {или <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
переключение столбцов
|
||||
</li>
|
||||
<li><kbd>7</kbd> или <kbd>c</kbd> создать запись</li>
|
||||
<li><kbd>s</kbd> или <kbd>/</kbd> искать</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> главная</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> уведомления</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> локальная лента</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> глобальная лента</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> сообщество</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> личные сообщения</li>
|
||||
<li><kbd>h</kbd> или <kbd>?</kbd> диалог справки</li>
|
||||
<li><kbd>Backspace</kbd> закрыть диалог, чтобы вернуться назад</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> или <kbd>↓</kbd> следующая запись</li>
|
||||
<li><kbd>k</kbd> или <kbd>↑</kbd> предыдущая запись</li>
|
||||
<li><kbd>.</kbd> показать больше и прокрутить вверх</li>
|
||||
<li><kbd>o</kbd> открыть</li>
|
||||
<li><kbd>f</kbd> в избранное</li>
|
||||
<li><kbd>b</kbd> продвинуть</li>
|
||||
<li><kbd>r</kbd> ответить</li>
|
||||
<li><kbd>i</kbd> открыть изображения, видео или аудио</li>
|
||||
<li><kbd>y</kbd> показать или скрыть деликатное медиа</li>
|
||||
<li><kbd>m</kbd> упомянуть автора</li>
|
||||
<li><kbd>p</kbd> открыть профиль автора</li>
|
||||
<li><kbd>l</kbd> открыть ссылку карточки в новой вкладке</li>
|
||||
<li><kbd>x</kbd> показать или скрыть текст за предупреждением о содержимом</li>
|
||||
<li><kbd>z</kbd> показать или скрыть все предупреждения о содержимом в треде</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> перейти к следующему или предыдущему</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Current)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
·
|
||||
{name}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(Pinned page)}
|
||||
other {(Unpinned page)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Закрепить {label}',
|
||||
// Status composition
|
||||
composeStatus: 'Создать запись',
|
||||
postStatus: 'Опубликовать!',
|
||||
contentWarning: 'Предупреждение о содержимом',
|
||||
dropToUpload: 'Перетащите для загрузки',
|
||||
invalidFileType: 'Неверный тип файла',
|
||||
composeLabel: "О чем Вы думаете?",
|
||||
autocompleteDescription: 'Когда результаты автозаполнения доступны, нажмите стрелки вверх или вниз и нажмите Enter, чтобы выбрать.',
|
||||
mediaUploads: 'Загрузка медиа',
|
||||
edit: 'Редактировать',
|
||||
delete: 'Удалить',
|
||||
description: 'Описание',
|
||||
descriptionLabel: 'Добавьте описание для слабовидящих (изображение, видео) или слабослышащих (аудио, видео)',
|
||||
markAsSensitive: 'Отметить медиа как деликатное',
|
||||
// Polls
|
||||
createPoll: 'Создать опрос',
|
||||
removePollChoice: 'Удалить вариант {index}',
|
||||
pollChoiceLabel: 'Вариант {index}',
|
||||
multipleChoice: 'Несколько вариантов',
|
||||
pollDuration: 'Продолжительность опроса',
|
||||
fiveMinutes: '5 минут',
|
||||
thirtyMinutes: '30 минут',
|
||||
oneHour: '1 час',
|
||||
sixHours: '6 часов',
|
||||
twelveHours: '12 часов',
|
||||
oneDay: '1 день',
|
||||
threeDays: '3 дня',
|
||||
sevenDays: '7 дней',
|
||||
never: 'Никогда',
|
||||
addEmoji: 'Вставить эмодзи',
|
||||
addMedia: 'Добавить медиа (изображения, видео, аудио)',
|
||||
addPoll: 'Добавить опрос',
|
||||
removePoll: 'Удалить опрос',
|
||||
postPrivacyLabel: 'Настройка конфиденциальности (на данный момент {label})',
|
||||
addContentWarning: 'Добавить предупреждение о содержимом',
|
||||
removeContentWarning: 'Удалить предупреждение о содержимом',
|
||||
altLabel: 'Описание для слабовидящих',
|
||||
extractText: 'Извлечь текст из изображения',
|
||||
extractingText: 'Извлечение текста…',
|
||||
extractingTextCompletion: 'Извлечение текста ({percent}% завершено)…',
|
||||
unableToExtractText: 'Не удалось извлечь текст.',
|
||||
// Account options
|
||||
followAccount: 'Подписаться на {account}',
|
||||
unfollowAccount: 'Отписаться от {account}',
|
||||
blockAccount: 'Заблокировать {account}',
|
||||
unblockAccount: 'Разблокировать {account}',
|
||||
muteAccount: 'Игнорировать {account}',
|
||||
unmuteAccount: 'Не игнорировать {account}',
|
||||
showReblogsFromAccount: 'Показывать продвижения от {account}',
|
||||
hideReblogsFromAccount: 'Скрыть продвижения от {account}',
|
||||
showDomain: 'Показать {domain}',
|
||||
hideDomain: 'Скрыть домен {domain}',
|
||||
reportAccount: 'Пожаловаться на {account}',
|
||||
mentionAccount: 'Упомянуть {account}',
|
||||
copyLinkToAccount: 'Копировать ссылку на аккаунт',
|
||||
copiedToClipboard: 'Скопировано в буфер обмена',
|
||||
// Media dialog
|
||||
navigateMedia: 'Навигация по элементам мультимедиа',
|
||||
showPreviousMedia: 'Показать предыдущие медиа',
|
||||
showNextMedia: 'Показать следующее медиа',
|
||||
enterPinchZoom: 'Режим масштабирования щипком',
|
||||
exitPinchZoom: 'Выйти из режима щипкового масштабирования.',
|
||||
showMedia: `Показать {index, select,
|
||||
1 {first}
|
||||
2 {second}
|
||||
3 {third}
|
||||
other {fourth}
|
||||
} медиа {current, select,
|
||||
true {(current)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Предварительный просмотр (фокус)',
|
||||
enterFocalPoint: 'Введите точку фокусировки (X, Y) для этого медиа',
|
||||
muteNotifications: 'Отключить уведомления',
|
||||
muteAccountConfirm: 'Игнорировать {account}?',
|
||||
mute: 'Игнорировать',
|
||||
unmute: 'Не игнорировать',
|
||||
zoomOut: 'Уменьшить',
|
||||
zoomIn: 'Увеличить',
|
||||
// Reporting
|
||||
reportingLabel: 'Вы отправляете жалобу на {account} модератору {instance}.',
|
||||
additionalComments: 'Дополнительные комментарии',
|
||||
forwardDescription: 'Переслать также модераторам {instance}?',
|
||||
forwardLabel: 'Переслать {instance}',
|
||||
unableToLoadStatuses: 'Не удалось загрузить последние записи: {error}',
|
||||
report: 'Жалоба',
|
||||
noContent: '(Без содержания)',
|
||||
noStatuses: 'Нет записей для жалобы',
|
||||
// Status options
|
||||
unpinFromProfile: 'Открепить от профиля',
|
||||
pinToProfile: 'Закрепить в профиле',
|
||||
muteConversation: 'Игнорировать обсуждение',
|
||||
unmuteConversation: 'Не игнорировать обсуждение',
|
||||
bookmarkStatus: 'Добавить в закладки',
|
||||
unbookmarkStatus: 'Удалить закладку',
|
||||
deleteAndRedraft: 'Удалить и исправить',
|
||||
reportStatus: 'Пожаловаться на запись',
|
||||
shareStatus: 'Поделиться записью',
|
||||
copyLinkToStatus: 'Копировать ссылку на запись',
|
||||
// Account profile
|
||||
profileForAccount: 'Профиль для {account}',
|
||||
statisticsAndMoreOptions: 'Статистика и другие параметры',
|
||||
statuses: 'Записи',
|
||||
follows: 'Подписки',
|
||||
followers: 'Подписчики',
|
||||
moreOptions: 'Больше опций',
|
||||
followersLabel: 'Подписчиков {count}',
|
||||
followingLabel: 'Пописок {count}',
|
||||
followLabel: `Подписаться {requested, select,
|
||||
true {(follow requested)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Отписаться {requested, select,
|
||||
true {(follow requested)}
|
||||
other {}
|
||||
}`,
|
||||
notify: 'Подписаться на {account}',
|
||||
denotify: 'Отписаться от {account}',
|
||||
subscribedAccount: 'Подписан на аккаунт',
|
||||
unsubscribedAccount: 'Отписаться от аккаунта',
|
||||
unblock: 'Разблокировать',
|
||||
nameAndFollowing: 'Имя и подписка',
|
||||
clickToSeeAvatar: 'Нажмите, чтобы увидеть аватар',
|
||||
opensInNewWindow: '{label} (открывается в новом окне)',
|
||||
blocked: 'Заблокирован',
|
||||
domainHidden: 'Домен скрыт',
|
||||
muted: 'Игнорирован',
|
||||
followsYou: 'Подписан на вас',
|
||||
avatarForAccount: 'Аватар для {account}',
|
||||
fields: 'Поля',
|
||||
accountHasMoved: '{account} переехал:',
|
||||
profilePageForAccount: 'Страница профиля для {account}',
|
||||
// About page
|
||||
about: 'О нас',
|
||||
aboutApp: 'О Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore — это
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">бесплатное программное обеспечение с открытым исходным кодом</a>
|
||||
создано
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
|
||||
и распространяется под
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Политика конфиденциальности</h2>
|
||||
|
||||
<p>
|
||||
Pinafore не хранит никакой личной информации на своих серверах,
|
||||
включая, помимо прочего, имена, адреса электронной почты,
|
||||
IP-адреса, сообщения и фотографии.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore — это статический сайт. Все данные хранятся локально в вашем браузере и передаются через Федиверс
|
||||
инстансы, к которым вы подключаетесь.
|
||||
</p>
|
||||
|
||||
<h2>Кредиты</h2>
|
||||
|
||||
<p>
|
||||
Иконки предоставлены <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Благодарим за логотип «парусника» Грегора Креснара из
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/"> Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Настройки',
|
||||
general: 'Общие',
|
||||
generalSettings: 'Общие настройки',
|
||||
showSensitive: 'Показывать деликатные медиа по умолчанию',
|
||||
showPlain: 'Показать простой серый цвет для деликатного медиа',
|
||||
allSensitive: 'Относиться ко всем медиа как к деликатным',
|
||||
largeMedia: 'Показывать большие изображения и видео',
|
||||
autoplayGifs: 'Автовоспроизведение анимированных GIF-файлов',
|
||||
hideCards: 'Скрыть предварительный просмотр ссылок',
|
||||
underlineLinks: 'Подчеркивание ссылок в записях и профилях',
|
||||
accessibility: 'Специальные возможности',
|
||||
reduceMotion: 'Уменьшить анимацию интерфейса',
|
||||
disableTappable: 'Отключить нажимаемую область на записи.',
|
||||
removeEmoji: 'Удалить эмодзи из имен пользователей',
|
||||
shortAria: 'Использовать метки ARIA для коротких статей',
|
||||
theme: 'Тема',
|
||||
themeForInstance: 'Тема для {instance}',
|
||||
disableCustomScrollbars: 'Отключить пользовательские полосы прокрутки',
|
||||
bottomNav: 'Поместите панель навигации в нижнюю часть экрана',
|
||||
centerNav: 'Центрировать панель навигации',
|
||||
preferences: 'Предпочтения',
|
||||
hotkeySettings: 'Настройки горячих клавиш',
|
||||
disableHotkeys: 'Отключить все горячие клавиши',
|
||||
leftRightArrows: 'Клавиши со стрелками влево/вправо изменяют фокус, а не столбцы/медиа',
|
||||
guide: 'Руководство',
|
||||
reload: 'Перезагрузить',
|
||||
// Wellness settings
|
||||
wellness: 'Здоровье',
|
||||
wellnessSettings: 'Настройки здоровья',
|
||||
wellnessDescription: `Настройки здоровья предназначены для уменьшения вызывающих привыкание или тревогу аспектов социальных сетей.
|
||||
Выберите любые варианты, которые вам подходят.`,
|
||||
enableAll: 'Включить все',
|
||||
metrics: 'Метрики',
|
||||
hideFollowerCount: 'Скрыть количество подписчиков (до 10)',
|
||||
hideReblogCount: 'Скрыть количество продижений',
|
||||
hideFavoriteCount: 'Скрыть количество избранных',
|
||||
hideUnread: 'Скрыть количество непрочитанных уведомлений (например, красную точку)',
|
||||
// The quality that makes something seem important or interesting because it seems to be happening now
|
||||
immediacy: 'Оперативность',
|
||||
showAbsoluteTimestamps: 'Показывать абсолютные метки времени (например, «3-е марта») вместо относительных меток времени (например, «5 минут назад»)',
|
||||
ui: 'Интерфейс',
|
||||
grayscaleMode: 'Режим оттенков серого',
|
||||
wellnessFooter: `Эти настройки частично основаны на рекомендациях
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Центра гуманитарных технологий</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'Вы можете фильтровать или отключать уведомления в',
|
||||
filterNotificationsText: 'настройках инстанса',
|
||||
filterNotificationsPost: '',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: 'Отключить',
|
||||
disableInfiniteScrollText: 'бесконечную прокрутку',
|
||||
disableInfiniteScrollDescription: `Когда бесконечная прокрутка отключена, новые записи не будут автоматически появляться в
|
||||
внизу или вверху ленты. Вместо этого кнопки позволят вам
|
||||
загружать больше контента по запросу.`,
|
||||
disableInfiniteScrollPost: '',
|
||||
// Instance settings
|
||||
loggedInAs: 'Вы вошли как',
|
||||
homeTimelineFilters: 'Фильтры главной ленты',
|
||||
notificationFilters: 'Фильтры уведомлений',
|
||||
pushNotifications: 'Всплывающее уведомление',
|
||||
// Add instance page
|
||||
storageError: `Похоже, Pinafore не может хранить данные локально. Ваш браузер находится в приватном режиме
|
||||
или блокирует файлов cookie? Pinafore хранит все данные локально, и для этого требуется LocalStorage и
|
||||
IndexedDB для корректной работы.`,
|
||||
javaScriptError: 'Вы должны включить JavaScript, чтобы войти в систему.',
|
||||
enterInstanceName: 'Введите имя инстанса',
|
||||
instanceColon: 'Инстанс:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: "У вас нет",
|
||||
getAnInstanceText: 'инстанса',
|
||||
getAnInstanceDescription: 'Инстанс — это ваш домашний сервер Mastodon, например, mastodon.social или cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: 'Присоединяйтесь к Mastodon!',
|
||||
instancesYouveLoggedInTo: "Инстансы, в которые вы вошли:",
|
||||
addAnotherInstance: 'Добавить другой инстанс',
|
||||
youreNotLoggedIn: "Вы не вошли ни в один инстанс.",
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(current instance)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Войти в инстанс',
|
||||
logInToAnInstancePost: 'чтобы начать использовать Pinafore.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Всегда показывать',
|
||||
showRingText: 'кольцо фокусировки',
|
||||
showRingDescription: `TКольцо фокусировки — это контур, показывающий элемент, на котором в данный момент установлен фокус. По умолчанию отображается
|
||||
только при использовании клавиатуры (не мыши или сенсорного экрана), но вы можете выбрать, чтобы он отображался всегда.`,
|
||||
showRingPost: '',
|
||||
instances: 'Инстансы',
|
||||
addInstance: 'Добавить инстанс',
|
||||
homeTimelineFilterSettings: 'Настройки фильтров главной ленты',
|
||||
showReblogs: 'Показать продвижения',
|
||||
showReplies: 'Показывать ответы',
|
||||
switchOrLogOut: 'Переключитесь или выйдите из этого инстанса',
|
||||
switchTo: 'Переключиться на этот инстанс',
|
||||
switchToInstance: 'Переключиться на инстанс',
|
||||
switchToNameOfInstance: 'Переключиться на {instance}',
|
||||
logOut: 'Выйти',
|
||||
logOutOfInstanceConfirm: 'Выйти из {instance}?',
|
||||
notificationFilterSettings: 'Настройки фильтра уведомлений',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: "Ваш браузер не поддерживает push-уведомления.",
|
||||
deniedPush: 'Вы запретили показывать уведомления.',
|
||||
pushNotificationsNote: 'Обратите внимание, что вы можете получать push-уведомления только для одного инстанса за раз.',
|
||||
pushSettings: 'Настройки push-уведомлений',
|
||||
newFollowers: 'Новые подписчики',
|
||||
reblogs: 'Продвижения',
|
||||
pollResults: 'Результаты опроса',
|
||||
subscriptions: 'Подписка на записи',
|
||||
needToReauthenticate: 'Вам необходимо пройти повторную аутентификацию, чтобы включить push-уведомления. Выйти из {instance}?',
|
||||
failedToUpdatePush: 'Не удалось обновить настройки push-уведомлений: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Выберите тему',
|
||||
darkBackground: 'Темный фон',
|
||||
lightBackground: 'Светлый фон',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(default)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Анимированное изображение: {description}',
|
||||
showImage: `Показывать {animated, select,
|
||||
true {animated}
|
||||
other {}
|
||||
} image: {description}`,
|
||||
playVideoOrAudio: `Воспроизводить {audio, select,
|
||||
true {audio}
|
||||
other {video}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} подписался на вас, {account}',
|
||||
accountSignedUp: '{name} зарегистрировался, {account}',
|
||||
reblogCountsHidden: 'Количество продвижений скрыто',
|
||||
favoriteCountsHidden: 'Количество избранного скрыто',
|
||||
rebloggedTimes: `Продвинуто {count, plural,
|
||||
one {1 time}
|
||||
other {{count} times}
|
||||
}`,
|
||||
favoritedTimes: `Добавлено в избранное {count, plural,
|
||||
one {1 time}
|
||||
other {{count} times}
|
||||
}`,
|
||||
pinnedStatus: 'Закрепленная запись',
|
||||
rebloggedYou: 'продвинул вашу запись',
|
||||
favoritedYou: 'добавил(-а) в избранное вашу запись',
|
||||
followedYou: 'подписался на вас',
|
||||
signedUp: 'зарегистрировался',
|
||||
posted: 'опубликовал',
|
||||
pollYouCreatedEnded: 'Созданный вами опрос завершен',
|
||||
pollYouVotedEnded: 'Опрос, в котором вы голосовали, завершен',
|
||||
reblogged: 'продвинул(-а)',
|
||||
favorited: 'добавил(-а) в избранное',
|
||||
unreblogged: 'отменил(-а) продвижение',
|
||||
unfavorited: 'удалил(-а) из избранного',
|
||||
showSensitiveMedia: 'Показать деликатное медиа',
|
||||
hideSensitiveMedia: 'Скрыть деликатное медиа',
|
||||
clickToShowSensitive: 'Деликатное содержимое. Нажмите, чтобы показать.',
|
||||
longPost: 'Длинная запись',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} продвинул(-а) вашу запись',
|
||||
accountFavoritedYou: '{account} добавил(-а) в избранное вашу запись',
|
||||
rebloggedByAccount: 'Продвинул(-а) {account}',
|
||||
contentWarningContent: 'Предупреждение о содержимом: {spoiler}',
|
||||
hasMedia: 'имеет медия',
|
||||
hasPoll: 'имеет опрос',
|
||||
shortStatusLabel: '{privacy} запись от {account}',
|
||||
// Privacy types
|
||||
public: 'Публичный',
|
||||
unlisted: 'Открытый',
|
||||
followersOnly: 'Только для подписчиков',
|
||||
direct: 'Личное сообщение',
|
||||
// Themes
|
||||
themeRoyal: 'Royal',
|
||||
themeScarlet: 'Scarlet',
|
||||
themeSeafoam: 'Seafoam',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Oaken',
|
||||
themeMajesty: 'Majesty',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Grayscale',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalt',
|
||||
themeSorcery: 'Sorcery',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Pitch Black',
|
||||
themeDarkGrayscale: 'Dark Grayscale',
|
||||
// Polls
|
||||
voteOnPoll: 'Голосовать в опросе',
|
||||
pollChoices: 'Варианты опроса',
|
||||
vote: 'Голосовать',
|
||||
pollDetails: 'Детали опроса',
|
||||
refresh: 'Обновить',
|
||||
expires: 'Завершается',
|
||||
expired: 'Завершено',
|
||||
voteCount: `{count, plural,
|
||||
one {1 vote}
|
||||
other {{count} голосов}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - нажмите, чтобы показать тред',
|
||||
showMore: 'Показать больше',
|
||||
showLess: 'Показать меньше',
|
||||
closeReply: 'Закрыть ответ',
|
||||
cannotReblogFollowersOnly: 'Невозможно продвинуть, потому что это только для подписчиков',
|
||||
cannotReblogDirectMessage: 'Невозможно продвинуть, потому что это личное сообщение',
|
||||
reblog: 'Продвинуть',
|
||||
reply: 'Ответить',
|
||||
replyToThread: 'Ответить в треде',
|
||||
favorite: 'Добавить в избранное',
|
||||
unfavorite: 'Удалить из избранного',
|
||||
// timeline
|
||||
loadingMore: 'Загружается ещё…',
|
||||
loadMore: 'Загрузить ещё',
|
||||
showCountMore: 'Показать ещё {count}',
|
||||
nothingToShow: 'Нечего показывать.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Страница треда записи',
|
||||
status: 'Запись',
|
||||
// toast messages
|
||||
blockedAccount: 'Аккаунт заблокирован',
|
||||
unblockedAccount: 'Аккаунт разблокирован',
|
||||
unableToBlock: 'Не удалось заблокировать аккаунт: {error}',
|
||||
unableToUnblock: 'Не удалось разблокировать аккаунт: {error}',
|
||||
bookmarkedStatus: 'Запись добавлена в закладки',
|
||||
unbookmarkedStatus: 'Запись удалена из закладок',
|
||||
unableToBookmark: 'Не удалось добавить в закладки: {error}',
|
||||
unableToUnbookmark: 'Не удалось удалить из закладок: {error}',
|
||||
cannotPostOffline: 'Вы не можете публиковать записи в офлайн-режиме',
|
||||
unableToPost: 'Не удалось опубликовать запись: {error}',
|
||||
statusDeleted: 'Запись удалена',
|
||||
unableToDelete: 'Не удалось удалить запись: {error}',
|
||||
cannotFavoriteOffline: 'Вы не можете добавлять в избранное в офлайн-режиме режиме',
|
||||
cannotUnfavoriteOffline: 'Вы не можете удалять из избранного в офлайн-режиме режиме',
|
||||
unableToFavorite: 'Не удалось добавить в избранное: {error}',
|
||||
unableToUnfavorite: 'Не удалось удалить из избранного: {error}',
|
||||
followedAccount: 'Подписан(-на) на аккаунт',
|
||||
unfollowedAccount: 'Отписан(-на) от аккаунта',
|
||||
unableToFollow: 'Не удалось подписаться на аккаунт: {error}',
|
||||
unableToUnfollow: 'Не удалось отписаться от аккаунта: {error}',
|
||||
accessTokenRevoked: 'Токен доступа был отозван, выполнен выход из {instance}',
|
||||
loggedOutOfInstance: 'Выполнен выход из {instance}',
|
||||
failedToUploadMedia: 'Не удалось загрузить мультимедиа: {error}',
|
||||
mutedAccount: 'Аккаунт игнорируется',
|
||||
unmutedAccount: 'Аккаунт не игнорируется',
|
||||
unableToMute: 'Не удалось добавить аккаунт в игнорируемые: {error}',
|
||||
unableToUnmute: 'Не удалось удалить аккаунт из игнорируемых: {error}',
|
||||
mutedConversation: 'Обсуждение добавлено в игнорируемые',
|
||||
unmutedConversation: 'Обсуждение удалено из игнорируемых',
|
||||
unableToMuteConversation: 'Не удалось добавить обсуждение в игнорируемые: {error}',
|
||||
unableToUnmuteConversation: 'Не удалось удалить обсуждение из игнорируемых: {error}',
|
||||
unpinnedStatus: 'Запись откреплена',
|
||||
unableToPinStatus: 'Не удалось закрепить запись: {error}',
|
||||
unableToUnpinStatus: 'Не удалось открепить запись: {error}',
|
||||
unableToRefreshPoll: 'Не удалось обновить опрос: {error}',
|
||||
unableToVoteInPoll: 'Не удалось проголосовать в опросе: {error}',
|
||||
cannotReblogOffline: 'Вы не можете продвигать в оффлайн-режиме.',
|
||||
cannotUnreblogOffline: 'Вы не можете отменить продвижение в оффлайн-режиме.',
|
||||
failedToReblog: 'Не удалось продвинуть: {error}',
|
||||
failedToUnreblog: 'Не удалось отменить продвижение: {error}',
|
||||
submittedReport: 'Жалоба отправлена',
|
||||
failedToReport: 'Не удалось отправить жалобу: {error}',
|
||||
approvedFollowRequest: 'Запрос на подписку одобрен',
|
||||
rejectedFollowRequest: 'Запрос на подписку отклонен',
|
||||
unableToApproveFollowRequest: 'Не удалось одобрить запрос на подписку: {error}',
|
||||
unableToRejectFollowRequest: 'Не удалось отклонить запрос на подписку: {error}',
|
||||
searchError: 'Ошибка во время поиска: {error}',
|
||||
hidDomain: 'Домен скрыт',
|
||||
unhidDomain: 'Домен удален из скрытых',
|
||||
unableToHideDomain: 'Не удалось скрыть домен: {error}',
|
||||
unableToUnhideDomain: 'Не удалось удалить домен из скрытых: {error}',
|
||||
showingReblogs: 'Показывать продвижения',
|
||||
hidingReblogs: 'Скрывать продвижения',
|
||||
unableToShowReblogs: 'Не удалось показать продвижения: {error}',
|
||||
unableToHideReblogs: 'Не удалось скрыть продвижения: {error}',
|
||||
unableToShare: 'Не удалось поделиться: {error}',
|
||||
unableToSubscribe: 'Не удалось подписаться: {error}',
|
||||
unableToUnsubscribe: 'Не удалось отписаться: {error}',
|
||||
showingOfflineContent: 'Интернет-запрос не выполнен. Отображается офлайн-содержимое.',
|
||||
youAreOffline: 'Похоже, вы не в сети. Вы по-прежнему можете читать записи в офлайн-режиме.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'Доступно обновление приложения.',
|
||||
// Word/phrase filters
|
||||
wordFilters: 'Фильтры слов',
|
||||
noFilters: 'У вас нет фильтров слов.',
|
||||
wordOrPhrase: 'Слово или фраза',
|
||||
contexts: 'Контексты',
|
||||
addFilter: 'Добавить фильтр',
|
||||
editFilter: 'Редактировать фильтр',
|
||||
filterHome: 'Главная и списки',
|
||||
filterNotifications: 'Уведомления',
|
||||
filterPublic: 'Публичные ленты',
|
||||
filterThread: 'Обсуждения',
|
||||
filterAccount: 'Профили',
|
||||
filterUnknown: 'Неизвестный',
|
||||
expireAfter: 'Истекает через',
|
||||
whereToFilter: 'Где фильтровать',
|
||||
irreversible: 'Необратимый',
|
||||
wholeWord: 'Целое слово',
|
||||
save: 'Сохранить',
|
||||
updatedFilter: 'Фильтр обновлён',
|
||||
createdFilter: 'Фильтр создан',
|
||||
failedToModifyFilter: 'Не удалось изменить фильтр: {error}',
|
||||
deletedFilter: 'Фильтр удалён',
|
||||
required: 'Требуется',
|
||||
// Dialogs
|
||||
profileOptions: 'Параметры профиля',
|
||||
copyLink: 'Копировать ссылку',
|
||||
emoji: 'Эмодзи',
|
||||
editMedia: 'Редактировать медиа',
|
||||
shortcutHelp: 'Быстрая помощь',
|
||||
statusOptions: 'Параметры статуса',
|
||||
confirm: 'Подтвердить',
|
||||
closeDialog: 'Закрыть диалог',
|
||||
postPrivacy: 'Конфиденциальность записи',
|
||||
homeOnInstance: 'Главная на {instance}',
|
||||
statusesTimelineOnInstance: 'Записи: {timeline} лента на {instance}',
|
||||
statusesHashtag: 'Записи: #{hashtag} хэштег',
|
||||
statusesThread: 'Записи: треды',
|
||||
statusesAccountTimeline: 'Записи: лента аккаунта',
|
||||
statusesList: 'Записи: список',
|
||||
notificationsOnInstance: 'Уведомления на {instance}'
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { getAccountAccessibleName } from './getAccountAccessibleName'
|
||||
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
|
||||
import { getAccountAccessibleName } from './getAccountAccessibleName.js'
|
||||
import { POST_PRIVACY_OPTIONS } from '../_static/statuses.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
function getNotificationText (notification, omitEmojiInDisplayNames) {
|
||||
if (!notification) {
|
||||
|
@ -7,9 +8,11 @@ function getNotificationText (notification, omitEmojiInDisplayNames) {
|
|||
}
|
||||
const notificationAccountDisplayName = getAccountAccessibleName(notification.account, omitEmojiInDisplayNames)
|
||||
if (notification.type === 'reblog') {
|
||||
return `${notificationAccountDisplayName} boosted your status`
|
||||
return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'favourite') {
|
||||
return `${notificationAccountDisplayName} favorited your status`
|
||||
return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'update') {
|
||||
return formatIntl('intl.accountEdited', { account: notificationAccountDisplayName })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +29,7 @@ function getReblogText (reblog, account, omitEmojiInDisplayNames) {
|
|||
return
|
||||
}
|
||||
const accountDisplayName = getAccountAccessibleName(account, omitEmojiInDisplayNames)
|
||||
return `Boosted by ${accountDisplayName}`
|
||||
return formatIntl('intl.rebloggedByAccount', { account: accountDisplayName })
|
||||
}
|
||||
|
||||
function cleanupText (text) {
|
||||
|
@ -34,26 +37,34 @@ function cleanupText (text) {
|
|||
}
|
||||
|
||||
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
|
||||
timeagoFormattedDate, spoilerText, showContent,
|
||||
shortInlineFormattedDate, spoilerText, showContent,
|
||||
reblog, notification, visibility, omitEmojiInDisplayNames,
|
||||
disableLongAriaLabels) {
|
||||
disableLongAriaLabels, showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll) {
|
||||
const originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
||||
const contentTextToShow = (showContent || !spoilerText)
|
||||
? cleanupText(plainTextContent)
|
||||
: `Content warning: ${cleanupText(spoilerText)}`
|
||||
: formatIntl('intl.contentWarningContent', { spoiler: cleanupText(spoilerText) })
|
||||
const mediaTextToShow = showMedia && 'intl.hasMedia'
|
||||
const mediaDescText = (showMedia && (!sensitive || sensitiveShown))
|
||||
? mediaAttachments.map(media => media.description)
|
||||
: []
|
||||
const pollTextToShow = showPoll && 'intl.hasPoll'
|
||||
const 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}`
|
||||
return formatIntl('intl.shortStatusLabel', { privacy: privacyText, account: originalAccountDisplayName })
|
||||
}
|
||||
|
||||
const values = [
|
||||
getNotificationText(notification, omitEmojiInDisplayNames),
|
||||
originalAccountDisplayName,
|
||||
contentTextToShow,
|
||||
timeagoFormattedDate,
|
||||
mediaTextToShow,
|
||||
...mediaDescText,
|
||||
pollTextToShow,
|
||||
shortInlineFormattedDate,
|
||||
`@${originalAccount.acct}`,
|
||||
privacyText,
|
||||
getReblogText(reblog, account, omitEmojiInDisplayNames)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { removeEmoji } from '../_utils/removeEmoji'
|
||||
import { removeEmoji } from '../_utils/removeEmoji.js'
|
||||
|
||||
export function getAccountAccessibleName (account, omitEmojiInDisplayNames) {
|
||||
const emojis = account.emojis
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getAccount } from '../_api/user'
|
||||
import { getRelationship } from '../_api/relationships'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store'
|
||||
import { getAccount } from '../_api/user.js'
|
||||
import { getRelationship } from '../_api/relationships.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
async function _updateAccount (accountId, instanceName, accessToken) {
|
||||
const localPromise = database.getAccount(instanceName, accountId)
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
|
||||
import { getInstanceInfo } from '../_api/instance'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||
import { store } from '../_store/store'
|
||||
import { updateVerifyCredentialsForInstance } from './instances'
|
||||
import { updateCustomEmojiForInstance } from './emoji'
|
||||
import { database } from '../_database/database'
|
||||
import { DOMAIN_BLOCKS } from '../_static/blocks'
|
||||
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth.js'
|
||||
import { getInstanceInfo } from '../_api/instance.js'
|
||||
import { goto } from '../../../__sapper__/client.js'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { updateVerifyCredentialsForInstance } from './instances.js'
|
||||
import { updateCustomEmojiForInstance } from './emoji.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { DOMAIN_BLOCKS } from '../_static/blocks.js'
|
||||
|
||||
const GENERIC_ERROR = `
|
||||
Is this a valid Mastodon instance? Is a browser extension
|
||||
blocking the request? Are you in private browsing mode?
|
||||
If you believe this is a problem with your instance, please send
|
||||
<a href="https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md"
|
||||
target="_blank" rel="noopener">this link</a> to the administrator of your instance.`
|
||||
|
||||
function createKnownError (message) {
|
||||
const err = new Error(message)
|
||||
|
@ -30,8 +37,16 @@ async function redirectToOauth () {
|
|||
}
|
||||
const redirectUri = getRedirectUri()
|
||||
const registrationPromise = registerApplication(instanceNameInSearch, redirectUri)
|
||||
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
||||
try {
|
||||
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
||||
} catch (err) {
|
||||
// We get a 401 in limited federation mode, so we can just skip setting the instance info in that case.
|
||||
// It will be fetched automatically later.
|
||||
if (err.status !== 401) {
|
||||
throw err // this is a good way to test for typos in the instance name or some other problem
|
||||
}
|
||||
}
|
||||
const instanceData = await registrationPromise
|
||||
store.set({
|
||||
currentRegisteredInstanceName: instanceNameInSearch,
|
||||
|
@ -59,10 +74,7 @@ export async function logInToInstance () {
|
|||
} catch (err) {
|
||||
console.error(err)
|
||||
const error = `${err.message || err.name}. ` +
|
||||
(err.knownError ? '' : (navigator.onLine
|
||||
? `Is this a valid Mastodon instance? Is a browser extension
|
||||
blocking the request? Are you in private browsing mode?`
|
||||
: 'Are you offline?'))
|
||||
(err.knownError ? '' : (navigator.onLine ? GENERIC_ERROR : 'Are you offline?'))
|
||||
const { instanceNameInSearch } = store.get()
|
||||
store.set({
|
||||
logInToInstanceError: error,
|
||||
|
@ -93,10 +105,10 @@ async function registerNewInstance (code) {
|
|||
instanceNameInSearch: '',
|
||||
currentRegisteredInstanceName: null,
|
||||
currentRegisteredInstance: null,
|
||||
loggedInInstances: loggedInInstances,
|
||||
loggedInInstances,
|
||||
currentInstance: currentRegisteredInstanceName,
|
||||
loggedInInstancesInOrder: loggedInInstancesInOrder,
|
||||
instanceThemes: instanceThemes
|
||||
loggedInInstancesInOrder,
|
||||
instanceThemes
|
||||
})
|
||||
store.save()
|
||||
const { enableGrayscale } = store.get()
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { mark, stop } from '../_utils/marks'
|
||||
import { store } from '../_store/store'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { concat } from '../_utils/arrays'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { uniqBy, isEqual } from '../_thirdparty/lodash/objects.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { concat } from '../_utils/arrays.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { timelineItemToSummary } from '../_utils/timelineItemToSummary.js'
|
||||
|
||||
function getExistingItemIdsSet (instanceName, timelineName) {
|
||||
const timelineItemSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries') || []
|
||||
|
@ -31,9 +30,9 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
|
|||
console.log('itemSummariesToAdd', JSON.parse(JSON.stringify(itemSummariesToAdd)))
|
||||
console.log('updates.map(timelineItemToSummary)', JSON.parse(JSON.stringify(updates.map(timelineItemToSummary))))
|
||||
console.log('concat(itemSummariesToAdd, updates.map(timelineItemToSummary))',
|
||||
JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(timelineItemToSummary)))))
|
||||
JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName))))))
|
||||
const newItemSummariesToAdd = uniqBy(
|
||||
concat(itemSummariesToAdd, updates.map(timelineItemToSummary)),
|
||||
concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName))),
|
||||
_ => _.id
|
||||
)
|
||||
if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
|
||||
|
@ -78,7 +77,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
|
|||
continue
|
||||
}
|
||||
const newItemSummariesToAdd = uniqBy(
|
||||
concat(itemSummariesToAdd, validUpdates.map(timelineItemToSummary)),
|
||||
concat(itemSummariesToAdd, validUpdates.map(item => timelineItemToSummary(item, instanceName))),
|
||||
_ => _.id
|
||||
)
|
||||
if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
|
||||
|
@ -119,6 +118,6 @@ export function addStatusesOrNotifications (instanceName, timelineName, newStatu
|
|||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
||||
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
|
||||
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates })
|
||||
lazilyProcessFreshUpdates(instanceName, timelineName)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
const emojiMapper = emoji => `:${emoji.shortcode}:`
|
||||
const emojiMapper = emoji => emoji.unicode ? emoji.unicode : `:${emoji.shortcodes[0]}:`
|
||||
const hashtagMapper = hashtag => `#${hashtag.name}`
|
||||
const accountMapper = account => `@${account.acct}`
|
||||
|
||||
|
@ -61,7 +61,7 @@ export function selectAutosuggestItem (item) {
|
|||
const endIndex = composeSelectionStart
|
||||
if (item.acct) {
|
||||
/* no await */ insertUsername(currentComposeRealm, item, startIndex, endIndex)
|
||||
} else if (item.shortcode) {
|
||||
} else if (item.shortcodes) {
|
||||
/* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
|
||||
} else { // hashtag
|
||||
/* no await */ insertHashtag(currentComposeRealm, item, startIndex, endIndex)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store'
|
||||
import { search } from '../_api/search'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||
import { concat } from '../_utils/arrays'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { RequestThrottler } from '../_utils/RequestThrottler'
|
||||
import { database } from '../_database/database.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { search } from '../_api/search.js'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
|
||||
import { concat } from '../_utils/arrays.js'
|
||||
import { uniqBy } from '../_thirdparty/lodash/objects.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { RequestThrottler } from '../_utils/RequestThrottler.js'
|
||||
|
||||
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||
|
||||
|
|
|
@ -1,24 +1,45 @@
|
|||
import { store } from '../_store/store'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { store } from '../_store/store.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import * as emojiDatabase from '../_utils/emojiDatabase.js'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
|
||||
import { testEmojiSupported } from '../_utils/testEmojiSupported.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
|
||||
function searchEmoji (searchText) {
|
||||
searchText = searchText.toLowerCase().substring(1)
|
||||
const { currentCustomEmoji } = store.get()
|
||||
const results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText))
|
||||
.sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1)
|
||||
.slice(0, SEARCH_RESULTS_LIMIT)
|
||||
async function searchEmoji (searchText) {
|
||||
let emojis = await emojiDatabase.findBySearchQuery(searchText)
|
||||
|
||||
const results = []
|
||||
|
||||
if (searchText.startsWith(':') && searchText.endsWith(':')) {
|
||||
// exact shortcode search
|
||||
const shortcode = searchText.substring(1, searchText.length - 1).toLowerCase()
|
||||
emojis = emojis.filter(_ => _.shortcodes.includes(shortcode))
|
||||
}
|
||||
|
||||
mark('testEmojiSupported')
|
||||
for (const emoji of emojis) {
|
||||
if (results.length === SEARCH_RESULTS_LIMIT) {
|
||||
break
|
||||
}
|
||||
if (emoji.url || testEmojiSupported(emoji.unicode)) { // emoji.url is a custom emoji
|
||||
results.push(emoji)
|
||||
}
|
||||
}
|
||||
stop('testEmojiSupported')
|
||||
return results
|
||||
}
|
||||
|
||||
export function doEmojiSearch (searchText) {
|
||||
let canceled = false
|
||||
|
||||
scheduleIdleTask(() => {
|
||||
scheduleIdleTask(async () => {
|
||||
if (canceled) {
|
||||
return
|
||||
}
|
||||
const results = await searchEmoji(searchText)
|
||||
if (canceled) {
|
||||
return
|
||||
}
|
||||
const results = searchEmoji(searchText)
|
||||
store.setForCurrentAutosuggest({
|
||||
autosuggestType: 'emoji',
|
||||
autosuggestSelected: 0,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { search } from '../_api/search'
|
||||
import { store } from '../_store/store'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||
import { RequestThrottler } from '../_utils/RequestThrottler'
|
||||
import { sum } from '../_utils/lodash-lite'
|
||||
import { search } from '../_api/search.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
|
||||
import { RequestThrottler } from '../_utils/RequestThrottler.js'
|
||||
import { sum } from '../_utils/lodash-lite.js'
|
||||
|
||||
const HASHTAG_SEARCH_LIMIT = 10
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockAccount, unblockAccount } from '../_api/block'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { store } from '../_store/store.js'
|
||||
import { blockAccount, unblockAccount } from '../_api/block.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateLocalRelationship } from './accounts.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -16,14 +17,17 @@ export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
|||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say('Blocked account')
|
||||
/* no await */ toast.say('intl.blockedAccount')
|
||||
} else {
|
||||
toast.say('Unblocked account')
|
||||
/* no await */ toast.say('intl.unblockedAccount')
|
||||
}
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
|
||||
/* no await */ toast.say(block
|
||||
? formatIntl('intl.unableToBlock', { block: !!block, error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnblock', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
if (bookmarked) {
|
||||
await bookmarkStatus(currentInstance, accessToken, statusId)
|
||||
} else {
|
||||
await unbookmarkStatus(currentInstance, accessToken, statusId)
|
||||
}
|
||||
if (bookmarked) {
|
||||
/* no await */ toast.say('intl.bookmarkedStatus')
|
||||
} else {
|
||||
/* no await */ toast.say('intl.unbookmarkedStatus')
|
||||
}
|
||||
store.setStatusBookmarked(currentInstance, statusId, bookmarked)
|
||||
await database.setStatusBookmarked(currentInstance, statusId, bookmarked)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/* no await */toast.say(
|
||||
bookmarked
|
||||
? formatIntl('intl.unableToBookmark', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnbookmark', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||
import { database } from '../_database/database'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { putMediaMetadata } from '../_api/media'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { postStatus as postStatusToServer } from '../_api/statuses.js'
|
||||
import { addStatusOrNotification } from './addStatusOrNotification.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { putMediaMetadata } from '../_api/media.js'
|
||||
import { uniqBy } from '../_thirdparty/lodash/objects.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function insertHandleForReply (statusId) {
|
||||
const { currentInstance } = store.get()
|
||||
|
@ -29,7 +31,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
const { currentInstance, accessToken, online } = store.get()
|
||||
|
||||
if (!online) {
|
||||
toast.say('You cannot post while offline')
|
||||
/* no await */ toast.say('intl.cannotPostOffline')
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -58,9 +60,10 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
addStatusOrNotification(currentInstance, 'home', status)
|
||||
store.clearComposeData(realm)
|
||||
emit('postedStatus', realm, inReplyToUuid)
|
||||
scheduleIdleTask(() => (mediaIds || []).forEach(mediaId => database.deleteCachedMediaFile(mediaId))) // clean up media cache
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Unable to post status: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.unableToPost', { error: (e.message || '') }))
|
||||
} finally {
|
||||
store.set({ postingStatus: false })
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
export function enablePoll (realm) {
|
||||
store.setComposeData(realm, {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
export function toggleContentWarningShown (realm) {
|
||||
const shown = store.getComposeData(realm, 'contentWarningShown')
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { importShowCopyDialog } from '../_components/dialog/asyncDialogs/importShowCopyDialog.js'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
|
||||
export async function copyText (text) {
|
||||
if (navigator.clipboard) { // not supported in all browsers
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
toast.say('Copied to clipboard')
|
||||
/* no await */ toast.say('intl.copiedToClipboard')
|
||||
return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { database } from '../_database/database'
|
||||
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { get } from '../_utils/lodash-lite'
|
||||
import { database } from '../_database/database.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
|
||||
|
||||
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
|
||||
return {
|
||||
|
@ -19,52 +18,10 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
function tryInitBlurhash () {
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeAllBlurhashes (statusOrNotification) {
|
||||
const status = statusOrNotification.status || statusOrNotification.notification.status
|
||||
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
|
||||
.concat(get(status, ['reblog', 'media_attachments'], []))
|
||||
.filter(_ => _.blurhash)
|
||||
if (mediaWithBlurhashes.length) {
|
||||
mark(`decodeBlurhash-${status.id}`)
|
||||
await Promise.all(mediaWithBlurhashes.map(async media => {
|
||||
try {
|
||||
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
|
||||
} catch (err) {
|
||||
console.warn('Could not decode blurhash, ignoring', err)
|
||||
}
|
||||
}))
|
||||
stop(`decodeBlurhash-${status.id}`)
|
||||
}
|
||||
return statusOrNotification
|
||||
}
|
||||
|
||||
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||
let taskCount = 0
|
||||
let pending = []
|
||||
let promiseChain = Promise.resolve()
|
||||
|
||||
tryInitBlurhash() // start the blurhash worker a bit early to save time
|
||||
|
||||
// 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 = []
|
||||
}
|
||||
})
|
||||
}
|
||||
prepareToRehydrate() // start blurhash early to save time
|
||||
|
||||
async function fetchFromIndexedDB (itemId) {
|
||||
mark(`fetchFromIndexedDB-${itemId}`)
|
||||
|
@ -78,13 +35,20 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
|
|||
}
|
||||
}
|
||||
|
||||
return (itemId) => {
|
||||
taskCount++
|
||||
async function getStatusOrNotification (itemId) {
|
||||
const statusOrNotification = await fetchFromIndexedDB(itemId)
|
||||
await rehydrateStatusOrNotification(statusOrNotification)
|
||||
return statusOrNotification
|
||||
}
|
||||
|
||||
return fetchFromIndexedDB(itemId)
|
||||
.then(decodeAllBlurhashes)
|
||||
.then(statusOrNotification => {
|
||||
return awaitAllTasksComplete().then(() => statusOrNotification)
|
||||
})
|
||||
// The results from IndexedDB or the worker thread can return in random order,
|
||||
// so we ensure consistent ordering based on the order this function is called in.
|
||||
return itemId => {
|
||||
const getStatusOrNotificationPromise = getStatusOrNotification(itemId) // start the promise ASAP
|
||||
return new Promise((resolve, reject) => {
|
||||
promiseChain = promiseChain
|
||||
.then(() => getStatusOrNotificationPromise)
|
||||
.then(resolve, reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { store } from '../_store/store'
|
||||
import { deleteStatus } from '../_api/delete'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
|
||||
import { store } from '../_store/store.js'
|
||||
import { deleteStatus } from '../_api/delete.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function doDeleteStatus (statusId) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
const deletedStatus = await deleteStatus(currentInstance, accessToken, statusId)
|
||||
deleteStatusLocally(currentInstance, statusId)
|
||||
toast.say('Status deleted.')
|
||||
/* no await */ toast.say('intl.statusDeleted')
|
||||
return deletedStatus
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Unable to delete status: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.unableToDelete', { error: (e.message || '') }))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
|
||||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
|
||||
import { doDeleteStatus } from './delete'
|
||||
import { store } from '../_store/store'
|
||||
import { doDeleteStatus } from './delete.js'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
export async function deleteAndRedraft (status) {
|
||||
const deleteStatusPromise = doDeleteStatus(status.id)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||
import { store } from '../_store/store'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { isEqual } from '../_thirdparty/lodash/objects.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
|
||||
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||
const keys = ['timelineItemSummaries', 'timelineItemSummariesToAdd']
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// "Secret" API to quickly log in with an access token and instance name.
|
||||
// Used in the integration tests. Can't see a problem with exposing this publicly
|
||||
// since you would have to know the access token anyway.
|
||||
|
||||
import { store } from '../_store/store.js'
|
||||
import { goto } from '../../../__sapper__/client.js'
|
||||
|
||||
export function doQuickLoginIfNecessary () {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const accessToken = params.get('accessToken')
|
||||
const instanceName = params.get('instanceName')
|
||||
if (!accessToken || !instanceName) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
loggedInInstances,
|
||||
loggedInInstancesInOrder
|
||||
} = store.get()
|
||||
|
||||
loggedInInstances[instanceName] = {
|
||||
access_token: accessToken
|
||||
}
|
||||
|
||||
if (!loggedInInstancesInOrder.includes(instanceName)) {
|
||||
loggedInInstancesInOrder.push(instanceName)
|
||||
}
|
||||
|
||||
store.set({
|
||||
currentInstance: instanceName,
|
||||
loggedInInstances,
|
||||
loggedInInstancesInOrder
|
||||
})
|
||||
store.save()
|
||||
goto('/') // re-navigate without the URL params
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
import {
|
||||
cacheFirstUpdateAfter,
|
||||
cacheFirstUpdateOnlyIfNotInCache
|
||||
} from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { getCustomEmoji } from '../_api/emoji'
|
||||
import { store } from '../_store/store'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
} from '../_utils/sync.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { getCustomEmoji } from '../_api/emoji.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { isEqual } from '../_thirdparty/lodash/objects.js'
|
||||
|
||||
async function syncEmojiForInstance (instanceName, syncMethod) {
|
||||
await syncMethod(
|
||||
() => getCustomEmoji(instanceName),
|
||||
() => {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
return getCustomEmoji(instanceName, accessToken)
|
||||
},
|
||||
() => database.getCustomEmoji(instanceName),
|
||||
emoji => database.setCustomEmoji(instanceName, emoji),
|
||||
emoji => {
|
||||
|
@ -31,7 +35,7 @@ export async function setupCustomEmojiForInstance (instanceName) {
|
|||
}
|
||||
|
||||
export function insertEmoji (realm, emoji) {
|
||||
const emojiText = emoji.custom ? emoji.colons : emoji.native
|
||||
const emojiText = emoji.unicode || `:${emoji.name}:`
|
||||
const { composeSelectionStart } = store.get()
|
||||
const idx = composeSelectionStart || 0
|
||||
const oldText = store.getComposeData(realm, 'text') || ''
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setFavorited (statusId, favorited) {
|
||||
const { online } = store.get()
|
||||
if (!online) {
|
||||
toast.say(`You cannot ${favorited ? 'favorite' : 'unfavorite'} while offline.`)
|
||||
/* no await */ toast.say(favorited ? 'intl.cannotFavoriteOffline' : 'intl.cannotUnfavoriteOffline')
|
||||
return
|
||||
}
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -19,7 +20,10 @@ export async function setFavorited (statusId, favorited) {
|
|||
await database.setStatusFavorited(currentInstance, statusId, favorited)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
|
||||
/* no await */ toast.say(favorited
|
||||
? formatIntl('intl.unableToFavorite', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnfavorite', { error: (e.message || '') })
|
||||
)
|
||||
store.setStatusFavorited(currentInstance, statusId, !favorited) // undo optimistic update
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters.js'
|
||||
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { isEqual } from '../_thirdparty/lodash/objects.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
|
||||
async function syncFilters (instanceName, syncMethod) {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
|
||||
await syncMethod(
|
||||
() => getFilters(instanceName, accessToken),
|
||||
() => database.getFilters(instanceName),
|
||||
filters => database.setFilters(instanceName, filters),
|
||||
filters => {
|
||||
const { instanceFilters } = store.get()
|
||||
if (!isEqual(instanceFilters[instanceName], filters)) { // avoid re-render if nothing changed
|
||||
instanceFilters[instanceName] = filters
|
||||
store.set({ instanceFilters })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateFiltersForInstance (instanceName) {
|
||||
await syncFilters(instanceName, cacheFirstUpdateAfter)
|
||||
}
|
||||
|
||||
export async function setupFiltersForInstance (instanceName) {
|
||||
await syncFilters(instanceName, cacheFirstUpdateOnlyIfNotInCache)
|
||||
}
|
||||
|
||||
export async function createOrUpdateFilter (instanceName, filter) {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
try {
|
||||
if (filter.id) {
|
||||
await updateFilter(instanceName, accessToken, filter)
|
||||
/* no await */ toast.say('intl.updatedFilter')
|
||||
} else {
|
||||
await createFilter(instanceName, accessToken, filter)
|
||||
/* no await */ toast.say('intl.createdFilter')
|
||||
}
|
||||
emit('wordFiltersChanged', instanceName)
|
||||
} catch (err) {
|
||||
/* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || ''))
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFilter (instanceName, id) {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
try {
|
||||
await doDeleteFilter(instanceName, accessToken, id)
|
||||
/* no await */ toast.say('intl.deletedFilter')
|
||||
emit('wordFiltersChanged', instanceName)
|
||||
} catch (err) {
|
||||
/* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || ''))
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { store } from '../_store/store.js'
|
||||
import { followAccount, unfollowAccount } from '../_api/follow.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateLocalRelationship } from './accounts.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -14,14 +15,13 @@ export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
|||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (follow) {
|
||||
toast.say('Followed account')
|
||||
} else {
|
||||
toast.say('Unfollowed account')
|
||||
}
|
||||
/* no await */ toast.say(follow ? 'intl.followedAccount' : 'intl.unfollowedAccount')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
|
||||
/* no await */ toast.say(follow
|
||||
? formatIntl('intl.unableToFollow', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnfollow', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { getFollowRequests } from '../_api/followRequests'
|
||||
import { get } from '../_utils/lodash-lite'
|
||||
import { store } from '../_store/store.js'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { getFollowRequests } from '../_api/followRequests.js'
|
||||
import { get } from '../_utils/lodash-lite.js'
|
||||
|
||||
export async function updateFollowRequestCountIfLockedAccount (instanceName) {
|
||||
const { verifyCredentials, loggedInInstances } = store.get()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { getTimeline } from '../_api/timelines'
|
||||
import { store } from '../_store/store.js'
|
||||
import { getTimeline } from '../_api/timelines.js'
|
||||
|
||||
export async function getRecentStatusesForAccount (accountId) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
return getTimeline(currentInstance, accessToken, `account/${accountId}`, null, null, 20)
|
||||
return (await getTimeline(currentInstance, accessToken, `account/${accountId}`, null, null, 20)).items
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { goto } from '../../../__sapper__/client.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
|
||||
// Go to the search page, and also focus the search input. For accessibility
|
||||
// and usability reasons, this only happens on pressing these particular hotkeys.
|
||||
export async function goToSearch () {
|
||||
if (store.get().currentPage === 'search') {
|
||||
emit('focusSearchInput')
|
||||
} else {
|
||||
store.set({ focusSearchInput: true })
|
||||
goto('/search')
|
||||
}
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
import { getVerifyCredentials } from '../_api/user'
|
||||
import { store } from '../_store/store'
|
||||
import { switchToTheme } from '../_utils/themeEngine'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { getInstanceInfo } from '../_api/instance'
|
||||
import { database } from '../_database/database'
|
||||
import { getVerifyCredentials } from '../_api/user.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { switchToTheme } from '../_utils/themeEngine.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { goto } from '../../../__sapper__/client.js'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
|
||||
import { getInstanceInfo } from '../_api/instance.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { importVirtualListStore } from '../_utils/asyncModules/importVirtualListStore.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export function changeTheme (instanceName, newTheme) {
|
||||
const { instanceThemes } = store.get()
|
||||
instanceThemes[instanceName] = newTheme
|
||||
store.set({ instanceThemes: instanceThemes })
|
||||
store.set({ instanceThemes })
|
||||
store.save()
|
||||
const { currentInstance } = store.get()
|
||||
if (instanceName === currentInstance) {
|
||||
|
@ -32,7 +33,8 @@ export function switchToInstance (instanceName) {
|
|||
switchToTheme(instanceThemes[instanceName], enableGrayscale)
|
||||
}
|
||||
|
||||
export async function logOutOfInstance (instanceName, message = `Logged out of ${instanceName}`) {
|
||||
export async function logOutOfInstance (instanceName, message) {
|
||||
message = message || formatIntl('intl.loggedOutOfInstance', { instance: instanceName })
|
||||
const {
|
||||
composeData,
|
||||
currentInstance,
|
||||
|
@ -88,7 +90,7 @@ export async function logOutOfInstance (instanceName, message = `Logged out of $
|
|||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||
const { verifyCredentials } = store.get()
|
||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||
store.set({ verifyCredentials: verifyCredentials })
|
||||
store.set({ verifyCredentials })
|
||||
}
|
||||
|
||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||
|
@ -109,13 +111,17 @@ export async function updateVerifyCredentialsForCurrentInstance () {
|
|||
|
||||
export async function updateInstanceInfo (instanceName) {
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getInstanceInfo(instanceName),
|
||||
() => {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName] && loggedInInstances[instanceName].access_token
|
||||
return getInstanceInfo(instanceName, accessToken)
|
||||
},
|
||||
() => database.getInstanceInfo(instanceName),
|
||||
info => database.setInstanceInfo(instanceName, info),
|
||||
info => {
|
||||
const { instanceInfos } = store.get()
|
||||
instanceInfos[instanceName] = info
|
||||
store.set({ instanceInfos: instanceInfos })
|
||||
store.set({ instanceInfos })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -123,7 +129,7 @@ export async function updateInstanceInfo (instanceName) {
|
|||
export function logOutOnUnauthorized (instanceName) {
|
||||
return async error => {
|
||||
if (error.message.startsWith('401:')) {
|
||||
await logOutOfInstance(instanceName, `The access token was revoked, logged out of ${instanceName}`)
|
||||
await logOutOfInstance(instanceName, formatIntl('intl.accessTokenRevoked', { instance: instanceName }))
|
||||
}
|
||||
|
||||
throw error
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { getLists } from '../_api/lists'
|
||||
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store.js'
|
||||
import { getLists } from '../_api/lists.js'
|
||||
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync.js'
|
||||
import { database } from '../_database/database.js'
|
||||
|
||||
async function syncLists (instanceName, syncMethod) {
|
||||
const { loggedInInstances } = store.get()
|
||||
|
@ -14,7 +14,7 @@ async function syncLists (instanceName, syncMethod) {
|
|||
lists => {
|
||||
const { instanceLists } = store.get()
|
||||
instanceLists[instanceName] = lists
|
||||
store.set({ instanceLists: instanceLists })
|
||||
store.set({ instanceLists })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { uploadMedia } from '../_api/media'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { mediaUploadFileCache } from '../_utils/mediaUploadFileCache'
|
||||
import { store } from '../_store/store.js'
|
||||
import { uploadMedia } from '../_api/media.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
import { database } from '../_database/database.js'
|
||||
|
||||
export async function doMediaUpload (realm, file) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -13,7 +14,7 @@ export async function doMediaUpload (realm, file) {
|
|||
if (composeMedia.length === 4) {
|
||||
throw new Error('Only 4 media max are allowed')
|
||||
}
|
||||
mediaUploadFileCache.set(response.url, file)
|
||||
await database.setCachedMediaFile(response.id, file)
|
||||
composeMedia.push({
|
||||
data: response,
|
||||
file: { name: file.name },
|
||||
|
@ -25,7 +26,7 @@ export async function doMediaUpload (realm, file) {
|
|||
scheduleIdleTask(() => store.save())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Failed to upload media: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.failedToUploadMedia', { error: (e.message || '') }))
|
||||
} finally {
|
||||
store.set({ uploadingMedia: false })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
|
||||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
export async function composeNewStatusMentioning (account) {
|
||||
store.setComposeData('dialog', { text: `@${account.acct} ` })
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { store } from '../_store/store.js'
|
||||
import { muteAccount, unmuteAccount } from '../_api/mute.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateLocalRelationship } from './accounts.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -15,15 +16,14 @@ export async function setAccountMuted (accountId, mute, notifications, toastOnSu
|
|||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted account')
|
||||
} else {
|
||||
toast.say('Unmuted account')
|
||||
}
|
||||
/* no await */ toast.say(mute ? 'intl.mutedAccount' : 'intl.unmutedAccount')
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || ''))
|
||||
/* no await */ toast.say(mute
|
||||
? formatIntl('intl.unableToMute', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnmute', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store.js'
|
||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -13,14 +14,13 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
|||
}
|
||||
await database.setStatusMuted(currentInstance, statusId, mute)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted conversation')
|
||||
} else {
|
||||
toast.say('Unmuted conversation')
|
||||
}
|
||||
/* no await */ toast.say(mute ? 'intl.mutedConversation' : 'intl.unmutedConversation')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${mute ? 'mute' : 'unmute'} conversation: ` + (e.message || ''))
|
||||
/* no await */ toast.say(mute
|
||||
? formatIntl('intl.unableToMuteConversation', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnmuteConversation', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { pinStatus, unpinStatus } from '../_api/pin'
|
||||
import { database } from '../_database/database'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { pinStatus, unpinStatus } from '../_api/pin.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -13,17 +14,16 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
|
|||
await unpinStatus(currentInstance, accessToken, statusId)
|
||||
}
|
||||
if (toastOnSuccess) {
|
||||
if (pinned) {
|
||||
toast.say('Pinned status')
|
||||
} else {
|
||||
toast.say('Unpinned status')
|
||||
}
|
||||
/* no await */ toast.say(pinned ? 'intl.pinnedStatus' : 'intl.unpinnedStatus')
|
||||
}
|
||||
store.setStatusPinned(currentInstance, statusId, pinned)
|
||||
await database.setStatusPinned(currentInstance, statusId, pinned)
|
||||
emit('updatePinnedStatuses')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${pinned ? 'pin' : 'unpin'} status: ` + (e.message || ''))
|
||||
/* no await */ toast.say(pinned
|
||||
? formatIntl('intl.unableToPinStatus', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnpinStatus', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
import { store } from '../_store/store'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store.js'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import {
|
||||
getPinnedStatuses
|
||||
} from '../_api/pinnedStatuses'
|
||||
} from '../_api/pinnedStatuses.js'
|
||||
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
|
||||
|
||||
// Pinned statuses aren't a "normal" timeline, so their blurhashes/plaintext need to be calculated specially
|
||||
async function rehydratePinnedStatuses (statuses) {
|
||||
await Promise.all(statuses.map(status => rehydrateStatusOrNotification({ status })))
|
||||
return statuses
|
||||
}
|
||||
|
||||
export async function updatePinnedStatusesForAccount (accountId) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||
async () => {
|
||||
return rehydratePinnedStatuses(await getPinnedStatuses(currentInstance, accessToken, accountId))
|
||||
},
|
||||
async () => {
|
||||
prepareToRehydrate() // start blurhash early to save time
|
||||
const pinnedStatuses = await database.getPinnedStatuses(currentInstance, accountId)
|
||||
if (!pinnedStatuses || !pinnedStatuses.every(Boolean)) {
|
||||
throw new Error('missing pinned statuses in idb')
|
||||
}
|
||||
return pinnedStatuses
|
||||
return rehydratePinnedStatuses(pinnedStatuses)
|
||||
},
|
||||
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
||||
statuses => {
|
||||
const { pinnedStatuses } = store.get()
|
||||
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
||||
pinnedStatuses[currentInstance][accountId] = statuses
|
||||
store.set({ pinnedStatuses: pinnedStatuses })
|
||||
store.set({ pinnedStatuses })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function getPoll (pollId) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -9,7 +10,7 @@ export async function getPoll (pollId) {
|
|||
return poll
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Unable to refresh poll: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.unableToRefreshPoll', { error: (e.message || '') }))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +21,6 @@ export async function voteOnPoll (pollId, choices) {
|
|||
return poll
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Unable to vote in poll: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.unableToVoteInPoll', { error: (e.message || '') }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
export function setPostPrivacy (realm, postPrivacyKey) {
|
||||
store.setComposeData(realm, { postPrivacy: postPrivacyKey })
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription'
|
||||
import { store } from '../_store/store'
|
||||
import { urlBase64ToUint8Array } from '../_utils/base64'
|
||||
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { urlBase64ToUint8Array } from '../_utils/base64.js'
|
||||
|
||||
const dummyApplicationServerKey = 'BImgAz4cF_yvNFp8uoBJCaGpCX4d0atNIFMHfBvAAXCyrnn9IMAFQ10DW_ZvBCzGeR4fZI5FnEi2JVcRE-L88jY='
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { reblogStatus, unreblogStatus } from '../_api/reblog.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setReblogged (statusId, reblogged) {
|
||||
const online = store.get()
|
||||
if (!online) {
|
||||
toast.say(`You cannot ${reblogged ? 'boost' : 'unboost'} while offline.`)
|
||||
/* no await */ toast.say(reblogged ? 'intl.cannotReblogOffline' : 'intl.cannotUnreblogOffline')
|
||||
return
|
||||
}
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -19,7 +20,10 @@ export async function setReblogged (statusId, reblogged) {
|
|||
await database.setStatusReblogged(currentInstance, statusId, reblogged)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
||||
/* no await */ toast.say(reblogged
|
||||
? formatIntl('intl.failedToReblog', { error: (e.message || '') })
|
||||
: formatIntl('intl.failedToUnreblog', { error: (e.message || '') })
|
||||
)
|
||||
store.setStatusReblogged(currentInstance, statusId, !reblogged) // undo optimistic update
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { get } from '../_utils/lodash-lite.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
|
||||
|
||||
function getActualStatus (statusOrNotification) {
|
||||
return get(statusOrNotification, ['status']) ||
|
||||
get(statusOrNotification, ['notification', 'status'])
|
||||
}
|
||||
|
||||
export function prepareToRehydrate () {
|
||||
// start the blurhash worker a bit early to save time
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeAllBlurhashes (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
|
||||
.concat(get(status, ['reblog', 'media_attachments'], []))
|
||||
.filter(_ => _.blurhash)
|
||||
if (mediaWithBlurhashes.length) {
|
||||
mark(`decodeBlurhash-${status.id}`)
|
||||
await Promise.all(mediaWithBlurhashes.map(async media => {
|
||||
try {
|
||||
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
|
||||
} catch (err) {
|
||||
console.warn('Could not decode blurhash, ignoring', err)
|
||||
}
|
||||
}))
|
||||
stop(`decodeBlurhash-${status.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function calculatePlainTextContent (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const originalStatus = status.reblog ? status.reblog : status
|
||||
const content = originalStatus.content || ''
|
||||
const mentions = originalStatus.mentions || []
|
||||
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
|
||||
// as well do it in advance, while blurhash is being decoded on the worker thread.
|
||||
await new Promise(resolve => {
|
||||
scheduleIdleTask(() => {
|
||||
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Do stuff that we need to do when the status or notification is fetched from the database,
|
||||
// like calculating the blurhash or calculating the plain text content
|
||||
export async function rehydrateStatusOrNotification (statusOrNotification) {
|
||||
await Promise.all([
|
||||
decodeAllBlurhashes(statusOrNotification),
|
||||
calculatePlainTextContent(statusOrNotification)
|
||||
])
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { report } from '../_api/report'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { report } from '../_api/report.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function reportStatuses (account, statusIds, comment, forward) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
await report(currentInstance, accessToken, account.id, statusIds, comment, forward)
|
||||
toast.say('Submitted report')
|
||||
/* no await */ toast.say('intl.submittedReport')
|
||||
} catch (e) {
|
||||
toast.say('Failed to report: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.failedToReport', { error: (e.message || '') }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { store } from '../_store/store.js'
|
||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||
const {
|
||||
|
@ -15,15 +16,14 @@ export async function setFollowRequestApprovedOrRejected (accountId, approved, t
|
|||
await rejectFollowRequest(currentInstance, accessToken, accountId)
|
||||
}
|
||||
if (toastOnSuccess) {
|
||||
if (approved) {
|
||||
toast.say('Approved follow request')
|
||||
} else {
|
||||
toast.say('Rejected follow request')
|
||||
}
|
||||
/* no await */ toast.say(approved ? 'intl.approvedFollowRequest' : 'intl.rejectedFollowRequest')
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || ''))
|
||||
/* no await */ toast.say(approved
|
||||
? formatIntl('intl.unableToApproveFollowRequest', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToRejectFollowRequest', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { search } from '../_api/search'
|
||||
import { store } from '../_store/store.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { search } from '../_api/search.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function doSearch () {
|
||||
const { currentInstance, accessToken, queryInSearch } = store.get()
|
||||
|
@ -15,7 +16,7 @@ export async function doSearch () {
|
|||
})
|
||||
}
|
||||
} catch (e) {
|
||||
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.searchError', { error: (e.message || '') }))
|
||||
console.error(e)
|
||||
} finally {
|
||||
store.set({ searchLoading: false })
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { notifyAccount, denotifyAccount } from '../_api/notify.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateLocalRelationship } from './accounts.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setAccountNotified (accountId, notify, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (notify) {
|
||||
relationship = await notifyAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
relationship = await denotifyAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
/* no await */ toast.say(notify ? 'intl.subscribedAccount' : 'intl.unsubscribedAccount')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/* no await */ toast.say(notify
|
||||
? formatIntl('intl.unableToSubscribe', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnsubscribe', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockDomain, unblockDomain } from '../_api/blockDomain'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateRelationship } from './accounts'
|
||||
import { store } from '../_store/store.js'
|
||||
import { blockDomain, unblockDomain } from '../_api/blockDomain.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateRelationship } from './accounts.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -13,14 +14,13 @@ export async function setDomainBlocked (accountId, domain, block, toastOnSuccess
|
|||
}
|
||||
await updateRelationship(accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say(`Hiding ${domain}`)
|
||||
} else {
|
||||
toast.say(`Unhiding ${domain}`)
|
||||
}
|
||||
/* no await */ toast.say(block ? 'intl.hidDomain' : 'intl.unhidDomain')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${block ? 'hide' : 'unhide'} domain: ` + (e.message || ''))
|
||||
/* no await */ toast.say(block
|
||||
? formatIntl('intl.unableToHideDomain', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToUnhideDomain', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { store } from '../_store/store.js'
|
||||
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { updateLocalRelationship } from './accounts.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
@ -9,14 +10,13 @@ export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
|
|||
const 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')
|
||||
}
|
||||
/* no await */ toast.say(showReblogs ? 'intl.showingReblogs' : 'intl.hidingReblogs')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${showReblogs ? 'show' : 'hide'} boosts: ` + (e.message || ''))
|
||||
/* no await */ toast.say(showReblogs
|
||||
? formatIntl('intl.unableToShowReblogs', { error: (e.message || '') })
|
||||
: formatIntl('intl.unableToHideReblogs', { error: (e.message || '') })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { toast } from '../_components/toast/toast'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
export async function shareStatus (status) {
|
||||
try {
|
||||
|
@ -9,6 +10,6 @@ export async function shareStatus (status) {
|
|||
url: status.url
|
||||
})
|
||||
} catch (e) {
|
||||
toast.say('Unable to share: ' + (e.message || ''))
|
||||
/* no await */ toast.say(formatIntl('intl.unableToShare', { error: (e.message || '') }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { doMediaUpload } from './media.js'
|
||||
|
||||
// show a compose dialog, typically invoked by the Web Share API or a PWA shortcut
|
||||
export async function showComposeDialog () {
|
||||
const { isUserLoggedIn } = store.get()
|
||||
if (!isUserLoggedIn) {
|
||||
return
|
||||
}
|
||||
const importShowComposeDialogPromise = importShowComposeDialog() // start promise early
|
||||
|
||||
const data = await database.getWebShareData()
|
||||
|
||||
if (data) {
|
||||
await database.deleteWebShareData() // only need this data once; it came from Web Share (service worker)
|
||||
}
|
||||
|
||||
console.log('share data', data)
|
||||
const { title, text, url, file } = (data || {})
|
||||
|
||||
// url is currently ignored on Android, but one can dream
|
||||
// https://web.dev/web-share-target/#verifying-shared-content
|
||||
const composeText = [title, text, url].filter(Boolean).join('\n\n')
|
||||
|
||||
store.clearComposeData('dialog')
|
||||
store.setComposeData('dialog', { text: composeText })
|
||||
store.save()
|
||||
|
||||
const showComposeDialog = await importShowComposeDialogPromise
|
||||
showComposeDialog()
|
||||
if (file) { // start the upload once the dialog is in view so it shows the loading spinner and everything
|
||||
/* no await */ doMediaUpload('dialog', file)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
import { showMoreItemsForCurrentTimeline } from './timeline'
|
||||
import { scrollToTop } from '../_utils/scrollToTop'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
const RETRIES = 5
|
||||
const TIMEOUT = 50
|
||||
import { showMoreItemsForCurrentTimeline } from './timeline.js'
|
||||
import { scrollToTop } from '../_utils/scrollToTop.js'
|
||||
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid.js'
|
||||
import { store } from '../_store/store.js'
|
||||
import { tryToFocusElement } from '../_utils/tryToFocusElement.js'
|
||||
|
||||
export function showMoreAndScrollToTop () {
|
||||
// Similar to Twitter, pressing "." will click the "show more" button and select
|
||||
|
@ -24,25 +21,9 @@ export function showMoreAndScrollToTop () {
|
|||
const notificationId = currentTimelineType === 'notifications' && firstItemSummary.id
|
||||
const statusId = currentTimelineType !== 'notifications' && firstItemSummary.id
|
||||
scrollToTop(/* smooth */ false)
|
||||
// try 5 times to wait for the element to be rendered and then focus it
|
||||
let count = 0
|
||||
const tryToFocusElement = () => {
|
||||
const uuid = createStatusOrNotificationUuid(
|
||||
currentInstance, currentTimelineType,
|
||||
currentTimelineValue, notificationId, statusId
|
||||
)
|
||||
const element = document.getElementById(uuid)
|
||||
if (element) {
|
||||
try {
|
||||
element.focus({ preventScroll: true })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} else {
|
||||
if (++count <= RETRIES) {
|
||||
setTimeout(() => scheduleIdleTask(tryToFocusElement), TIMEOUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleIdleTask(tryToFocusElement)
|
||||
const id = createStatusOrNotificationUuid(
|
||||
currentInstance, currentTimelineType,
|
||||
currentTimelineValue, notificationId, statusId
|
||||
)
|
||||
tryToFocusElement(id)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue