kopia lustrzana https://github.com/nolanlawson/pinafore
Porównaj commity
194 Commity
Autor | SHA1 | Data |
---|---|---|
![]() |
8f61ea75ce | |
![]() |
5889b404cb | |
![]() |
794d9ca74e | |
![]() |
72a07ac40d | |
![]() |
ed9a9f6539 | |
![]() |
452b34b3b4 | |
![]() |
fd4bb4d864 | |
![]() |
c426b7fe31 | |
![]() |
c2851ce104 | |
![]() |
2578d0964d | |
![]() |
ff53fcab10 | |
![]() |
750235cd8f | |
![]() |
b5cad87aaf | |
![]() |
a85ff62d48 | |
![]() |
e06f63684e | |
![]() |
f81778d37f | |
![]() |
746298a1f7 | |
![]() |
02f1dad098 | |
![]() |
3edfed971f | |
![]() |
d71430f86d | |
![]() |
6124c948de | |
![]() |
774aa7a21c | |
![]() |
276c6e7bea | |
![]() |
f61054a3d5 | |
![]() |
b1dc43a9c9 | |
![]() |
040462f5b5 | |
![]() |
f5f3395a53 | |
![]() |
3fb152ac7c | |
![]() |
97e3b04f1f | |
![]() |
3c32b48e29 | |
![]() |
4a6907bbdc | |
![]() |
d31c800806 | |
![]() |
380d2a0d45 | |
![]() |
7fdbd72f13 | |
![]() |
62b30f6d99 | |
![]() |
6d6eb59f41 | |
![]() |
30b00667f2 | |
![]() |
da28e98cfb | |
![]() |
7417e89f78 | |
![]() |
815438172e | |
![]() |
8fc9d5c728 | |
![]() |
a775bd9193 | |
![]() |
edb7e7b442 | |
![]() |
3c857d74b8 | |
![]() |
5eb7183048 | |
![]() |
a3f41917c7 | |
![]() |
098da30f2a | |
![]() |
abc39ef982 | |
![]() |
b543399e0a | |
![]() |
fda00fc87c | |
![]() |
0e4523a37d | |
![]() |
4fb8f37db7 | |
![]() |
fac42a91a0 | |
![]() |
b50b9dc40b | |
![]() |
bc664e5ca1 | |
![]() |
fa41fe7649 | |
![]() |
53803db5be | |
![]() |
8792d912bc | |
![]() |
a447b9535e | |
![]() |
6b1533c947 | |
![]() |
347dab4e29 | |
![]() |
fdec7b2b3d | |
![]() |
7f86a94414 | |
![]() |
302845866a | |
![]() |
f875e65c49 | |
![]() |
85bc6ba372 | |
![]() |
00b6d31f0c | |
![]() |
035ab9cbff | |
![]() |
ad73918fa8 | |
![]() |
d57ab7238f | |
![]() |
fb5478cd06 | |
![]() |
52880a4689 | |
![]() |
2131ababf3 | |
![]() |
a318746961 | |
![]() |
601c3e40c9 | |
![]() |
ff6e1dc6fc | |
![]() |
4273666ce5 | |
![]() |
6f4eb98397 | |
![]() |
3c59069490 | |
![]() |
55189e840b | |
![]() |
e2d5b5928d | |
![]() |
6fde4e0b90 | |
![]() |
6ebd6a6a01 | |
![]() |
36ead0406d | |
![]() |
1de26d4b06 | |
![]() |
f10e9dbcf3 | |
![]() |
1c6387a0a4 | |
![]() |
0fd7154ed4 | |
![]() |
7ea387bc4c | |
![]() |
68d756ca34 | |
![]() |
6edb6df17d | |
![]() |
ed38cad661 | |
![]() |
19e466aa90 | |
![]() |
f301fc59f6 | |
![]() |
8fb4c40275 | |
![]() |
87cb80bf18 | |
![]() |
46682296d6 | |
![]() |
99d4cb4d8c | |
![]() |
9eb8fc1f1d | |
![]() |
b152359428 | |
![]() |
a211763d98 | |
![]() |
69bb849508 | |
![]() |
5fd8d0ac23 | |
![]() |
78687479df | |
![]() |
b312b3b485 | |
![]() |
b2d900f078 | |
![]() |
9282d7099d | |
![]() |
135c51a856 | |
![]() |
1a7bbe19a2 | |
![]() |
c67be9acc2 | |
![]() |
2e9afd711f | |
![]() |
54a11778da | |
![]() |
2a53bd3f80 | |
![]() |
6794514916 | |
![]() |
8685e4f603 | |
![]() |
58d81a25ad | |
![]() |
7d13f27d6c | |
![]() |
30ad0becb5 | |
![]() |
ce03460b86 | |
![]() |
05a3b2d31f | |
![]() |
e04b1da754 | |
![]() |
3e2fd130e0 | |
![]() |
49723fa91e | |
![]() |
a9119fa53f | |
![]() |
10ed291950 | |
![]() |
00c6aa1843 | |
![]() |
6e42e9f2b0 | |
![]() |
f2d752bfc2 | |
![]() |
fd6bb63450 | |
![]() |
e15f4523ba | |
![]() |
5ecf8b8ab9 | |
![]() |
8b8246c59f | |
![]() |
cac792a830 | |
![]() |
dc8b7c93f3 | |
![]() |
c57e3a2e7e | |
![]() |
331f6e8803 | |
![]() |
67f4a1ab2f | |
![]() |
f3c5e7de5f | |
![]() |
58ff6beb26 | |
![]() |
0df4b724ca | |
![]() |
fdf4110dad | |
![]() |
54b3042042 | |
![]() |
21678ec78e | |
![]() |
368775e220 | |
![]() |
9d5157f15c | |
![]() |
a1e105ccef | |
![]() |
344a23fddd | |
![]() |
32b1be96a9 | |
![]() |
da4d32c1e7 | |
![]() |
821b785e6b | |
![]() |
d30f7f4b1a | |
![]() |
d84f4604ad | |
![]() |
374b8b251e | |
![]() |
16e66346d7 | |
![]() |
c5de673990 | |
![]() |
b3ab427ac0 | |
![]() |
f012369d72 | |
![]() |
992c5efd7e | |
![]() |
b31a72f850 | |
![]() |
7bc9c3f263 | |
![]() |
f13e5be3a0 | |
![]() |
658a9736e1 | |
![]() |
0c455c35c9 | |
![]() |
b241ea18ac | |
![]() |
cbdbb05926 | |
![]() |
c692a1850b | |
![]() |
e0827be8c8 | |
![]() |
a166dccb59 | |
![]() |
7255221c5c | |
![]() |
e2813ae428 | |
![]() |
aa9878d1a9 | |
![]() |
284d812367 | |
![]() |
0861b22c85 | |
![]() |
c4fbf34a27 | |
![]() |
8205b6a2a6 | |
![]() |
9937b6f3cc | |
![]() |
75de31f7c7 | |
![]() |
c4e8d772dd | |
![]() |
69e3582157 | |
![]() |
3971f9a636 | |
![]() |
f9ac31465d | |
![]() |
f7ea5d98ad | |
![]() |
566cf6cd78 | |
![]() |
85a5874876 | |
![]() |
66fc202b5c | |
![]() |
ad9609738b | |
![]() |
3a91ad75b8 | |
![]() |
11fca7b792 | |
![]() |
7a28bd2d88 | |
![]() |
bb7ebb04bc | |
![]() |
c815292b0b | |
![]() |
d0c9be0c09 | |
![]() |
69ef9f2798 | |
![]() |
3c307a47fc |
|
@ -1,99 +0,0 @@
|
|||
version: 2.1
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build_and_test
|
||||
jobs:
|
||||
build_and_test:
|
||||
working_directory: ~/pinafore
|
||||
docker:
|
||||
# see https://discuss.circleci.com/t/build-failed-the-engine-node-is-incompatible-with-this-module-expected-version-12-x-got-14-15-0/37921/7
|
||||
# we want Node v12, not v14
|
||||
- image: circleci/ruby@sha256:b018ec2a8f0bbf06880735d2801402bad316c465edb60663be83ac8f1086b805
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: pinafore
|
||||
POSTGRES_PASSWORD: pinafore
|
||||
POSTGRES_DB: pinafore_development
|
||||
BROWSER: chrome:headless
|
||||
- image: circleci/redis:5-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install system dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg fonts-noto-color-emoji libicu-dev libidn11-dev libprotobuf-dev postgresql-contrib protobuf-compiler
|
||||
- run:
|
||||
name: Check node version
|
||||
command: node -v
|
||||
- restore_cache:
|
||||
name: Restore yarn cache
|
||||
key: yarn-v1-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Yarn install
|
||||
command: yarn install --immutable
|
||||
- run:
|
||||
name: Clone mastodon
|
||||
command: yarn clone-mastodon
|
||||
- restore_cache:
|
||||
name: Restore bundler cache
|
||||
key: bundler-v2-{{ checksum "mastodon/Gemfile.lock" }}
|
||||
- run:
|
||||
name: Lint
|
||||
command: yarn lint
|
||||
- run:
|
||||
name: Unit tests
|
||||
command: yarn test-unit
|
||||
- run:
|
||||
name: Install mastodon
|
||||
command: yarn install-mastodon
|
||||
- run:
|
||||
name: Wait for postgres to be ready
|
||||
command: |
|
||||
for i in `seq 1 10`;
|
||||
do
|
||||
nc -z localhost 5432 && echo Success && exit 0
|
||||
echo -n .
|
||||
sleep 1
|
||||
done
|
||||
echo Failed waiting for postgres && exit 1
|
||||
- run:
|
||||
name: Wait for redis to be ready
|
||||
command: |
|
||||
for i in `seq 1 10`;
|
||||
do
|
||||
nc -z localhost 6379 && echo Success && exit 0
|
||||
echo -n .
|
||||
sleep 1
|
||||
done
|
||||
echo Failed waiting for redis && exit 1
|
||||
- run:
|
||||
name: Copy vercel.json
|
||||
command: cp vercel.json vercel-old.json
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn build
|
||||
- run:
|
||||
name: Check vercel.json unchanged
|
||||
command: |
|
||||
if ! diff -q vercel-old.json vercel.json &>/dev/null; then
|
||||
diff vercel-old.json vercel.json
|
||||
echo "vercel.json changed, run yarn build and make sure everything looks okay"
|
||||
exit 1
|
||||
fi
|
||||
- run:
|
||||
name: Integration tests
|
||||
command: yarn test-in-ci
|
||||
- save_cache:
|
||||
name: Save yarn cache
|
||||
key: yarn-v1-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- save_cache:
|
||||
name: Save bundler cache
|
||||
key: bundler-v2-{{ checksum "mastodon/Gemfile.lock" }}
|
||||
paths:
|
||||
- mastodon/vendor/bundle
|
|
@ -14,6 +14,7 @@ tests
|
|||
/static/icons.svg
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-*.json
|
||||
/static/manifest.json
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
/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
|
|
@ -8,8 +8,11 @@
|
|||
/static/icons.svg
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-*.json
|
||||
/static/manifest.json
|
||||
/static/TwemojiCountryFlags.woff2
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
|
||||
.now
|
||||
.vercel
|
||||
/webpack/*.cjs
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
/static/icons.svg
|
||||
/static/inline-script.js.map
|
||||
/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).
|
|
@ -38,7 +38,7 @@ 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/)
|
||||
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`.
|
||||
|
||||
|
@ -120,8 +120,8 @@ 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. If the Ruby version changed, install it and update `setup-mastodon-in.travis.sh`
|
||||
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`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Using Alpine to keep the images smaller
|
||||
# Change to using the official NodeJS Alpine container
|
||||
FROM node:alpine
|
||||
FROM node:16-alpine
|
||||
|
||||
# Pushing all files into image
|
||||
WORKDIR /app
|
||||
|
|
19
README.md
19
README.md
|
@ -1,4 +1,6 @@
|
|||
# Pinafore [![Build status](https://circleci.com/gh/nolanlawson/pinafore.svg?style=svg)](https://app.circleci.com/pipelines/gh/nolanlawson/pinafore)
|
||||
# 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
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -94,12 +96,13 @@ To keep your version of Pinafore up to date, you can use `git` to check out the
|
|||
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
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import { LOCALE } from '../src/routes/_static/intl'
|
||||
import { LOCALE } from '../src/routes/_static/intl.js'
|
||||
import { getIntl, trimWhitespace } from './getIntl.js'
|
||||
|
||||
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)
|
||||
|
||||
// Try 'en-US' first, then 'en' if that doesn't exist
|
||||
const PREFERRED_LOCALES = [LOCALE, LOCALE.split('-')[0]]
|
||||
|
@ -34,7 +37,7 @@ async function getFirstExistingEmojiI18nFile () {
|
|||
}
|
||||
}
|
||||
|
||||
async function main () {
|
||||
async function buildEmojiI18nFile () {
|
||||
const json = await getFirstExistingEmojiI18nFile()
|
||||
|
||||
if (!json) {
|
||||
|
@ -48,6 +51,36 @@ async function main () {
|
|||
)
|
||||
}
|
||||
|
||||
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([
|
||||
buildEmojiI18nFile(),
|
||||
buildManifestJson(),
|
||||
buildFlagEmojiFile()
|
||||
])
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
|
|
|
@ -5,13 +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 { 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')
|
||||
|
@ -20,8 +19,11 @@ export async function buildInlineScript () {
|
|||
input: inlineScriptPath,
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
||||
values: {
|
||||
'process.browser': true,
|
||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
||||
},
|
||||
preventAssignment: true
|
||||
}),
|
||||
// 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.
|
||||
|
@ -39,7 +41,7 @@ export async function buildInlineScript () {
|
|||
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')
|
||||
|
||||
|
|
|
@ -3,10 +3,12 @@ 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 __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')
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
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')}`
|
||||
|
|
|
@ -2,15 +2,18 @@ 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 applyIntl from '../webpack/svelte-intl-loader'
|
||||
import { LOCALE } from '../src/routes/_static/intl'
|
||||
import { getLangDir } from 'rtl-detect'
|
||||
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
|
||||
|
@ -80,7 +83,7 @@ function doWatch () {
|
|||
}
|
||||
|
||||
async function buildAll () {
|
||||
const start = now()
|
||||
const start = performance.now()
|
||||
let html = (await Promise.all(partials.map(async partial => {
|
||||
if (typeof partial === 'string') {
|
||||
return partial
|
||||
|
@ -95,7 +98,7 @@ async function buildAll () {
|
|||
.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`)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
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 = {
|
||||
|
@ -20,15 +21,8 @@ const JSON_TEMPLATE = {
|
|||
github: {
|
||||
silent: true
|
||||
},
|
||||
builds: [
|
||||
{
|
||||
src: 'package.json',
|
||||
use: '@now/static-build',
|
||||
config: {
|
||||
distDir: '__sapper__/export'
|
||||
}
|
||||
}
|
||||
],
|
||||
buildCommand: 'yarn build',
|
||||
outputDirectory: '__sapper__/export',
|
||||
routes: [
|
||||
{
|
||||
src: '^/service-worker\\.js$',
|
||||
|
@ -50,7 +44,13 @@ const JSON_TEMPLATE = {
|
|||
}
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ const SCRIPT_CHECKSUMS = [inlineScriptChecksum]
|
|||
.map(_ => `'sha256-${_}'`)
|
||||
.join(' ')
|
||||
|
||||
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=()'
|
||||
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=(),interest-cohort=()'
|
||||
|
||||
const HTML_HEADERS = {
|
||||
'cache-control': 'public,max-age=3600',
|
||||
|
|
|
@ -2,16 +2,15 @@ import { promisify } from 'util'
|
|||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { envFile, RUBY_VERSION } from './mastodon-config'
|
||||
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 GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
||||
const GIT_TAG = 'v3.3.0'
|
||||
|
||||
const mastodonDir = path.join(dir, '../mastodon')
|
||||
|
||||
export default async function cloneMastodon () {
|
||||
|
@ -25,7 +24,7 @@ export default async function cloneMastodon () {
|
|||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
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
|
|
@ -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, ' ')
|
||||
}
|
|
@ -2,12 +2,14 @@ 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'
|
||||
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 () {
|
||||
|
@ -41,8 +43,8 @@ async function setupMastodonDatabase () {
|
|||
async function installMastodonDependencies () {
|
||||
const cwd = mastodonDir
|
||||
const installCommands = [
|
||||
'gem update --system',
|
||||
'gem install bundler foreman',
|
||||
'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'
|
||||
|
@ -69,7 +71,7 @@ export default async function installMastodon () {
|
|||
await installMastodonDependencies()
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (esMain(import.meta)) {
|
||||
installMastodon().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
|
|
|
@ -15,12 +15,15 @@ DB_PORT=${DB_PORT}
|
|||
DB_USER=${DB_USER}
|
||||
DB_NAME=${DB_NAME}
|
||||
DB_PASS=${DB_PASS}
|
||||
BIND=0.0.0.0
|
||||
`
|
||||
|
||||
// Need a Ruby version that CircleCI bundles with Node v12, not Node v14 which doesn't
|
||||
// work for streaming
|
||||
export const RUBY_VERSION = '2.6.6'
|
||||
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, {
|
||||
|
|
|
@ -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,10 +1,10 @@
|
|||
import { restoreMastodonData } from './restore-mastodon-data'
|
||||
import { restoreMastodonData } from './restore-mastodon-data.js'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import fs from 'fs'
|
||||
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start'
|
||||
import cloneMastodon from './clone-mastodon'
|
||||
import installMastodon from './install-mastodon'
|
||||
import { mastodonDir, env } from './mastodon-config'
|
||||
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 spawn = childProcessPromise.spawn
|
||||
|
||||
|
@ -15,7 +15,7 @@ async function runMastodon () {
|
|||
const cwd = mastodonDir
|
||||
const promise = spawn('foreman', ['start'], { cwd, env })
|
||||
// don't bother writing to mastodon.log in CI; we can't read the file anyway
|
||||
const logFile = process.env.CIRCLECI ? '/dev/null' : 'mastodon.log'
|
||||
const logFile = process.env.CI ? '/dev/null' : 'mastodon.log'
|
||||
const log = fs.createWriteStream(logFile, { flags: 'a' })
|
||||
childProc = promise.childProcess
|
||||
childProc.stdout.pipe(log)
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -5,9 +5,17 @@ Basically think of it as a "lay of the land" as well as "weird unusual stuff tha
|
|||
|
||||
## Overview
|
||||
|
||||
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
||||
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
|
||||
|
|
|
@ -16,5 +16,4 @@ or
|
|||
|
||||
LOCALE=fr yarn dev
|
||||
|
||||
There is also an experimental `LOCALE_DIRECTION` environment variable for the direction (LTR versus RTL) which is
|
||||
exposed to the source code while building.
|
||||
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).
|
139
package.json
139
package.json
|
@ -1,125 +1,138 @@
|
|||
{
|
||||
"name": "pinafore",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "1.24.0",
|
||||
"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 node -r esm ./node_modules/.bin/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-vercel-json",
|
||||
"sapper-build": "node -r esm ./node_modules/.bin/sapper build",
|
||||
"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",
|
||||
"clone-mastodon": "node -r esm ./bin/clone-mastodon.js",
|
||||
"install-mastodon": "node -r esm ./bin/install-mastodon.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-mastodon-suite0": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite0",
|
||||
"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 2 $BROWSER tests/spec/0*",
|
||||
"testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*",
|
||||
"test-unit": "NODE_ENV=test mocha -r esm -r bin/browser-shim.js tests/unit/",
|
||||
"test-in-ci": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon",
|
||||
"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",
|
||||
"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 node -r esm ./node_modules/.bin/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-vercel-json": "node -r esm bin/build-vercel-json.js"
|
||||
"build-vercel-json": "node bin/build-vercel-json.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-listformat": "^5.0.10",
|
||||
"@formatjs/intl-locale": "^2.4.14",
|
||||
"@formatjs/intl-pluralrules": "^4.0.6",
|
||||
"@formatjs/intl-relativetimeformat": "^8.0.4",
|
||||
"@rollup/plugin-replace": "^2.3.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.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"blurhash": "^1.1.5",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^3.5.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"compression": "^1.7.4",
|
||||
"country-flag-emoji-polyfill": "^0.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-dedoupe": "^0.1.1",
|
||||
"emoji-picker-element": "^1.4.0",
|
||||
"emoji-picker-element-data": "^1.1.0",
|
||||
"emoji-regex": "^9.2.1",
|
||||
"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": "^1.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"focus-visible": "^5.2.0",
|
||||
"form-data": "^3.0.0",
|
||||
"format-message-interpret": "^6.2.3",
|
||||
"format-message-parse": "^6.2.3",
|
||||
"glob": "^7.1.6",
|
||||
"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.6",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"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",
|
||||
"promise-worker": "^2.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"rollup": "^2.26.10",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup": "^2.67.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rtl-detect": "^1.0.2",
|
||||
"sapper": "nolanlawson/sapper#for-pinafore-25",
|
||||
"sass": "^1.32.8",
|
||||
"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": "^5.1.1",
|
||||
"tesseract.js": "^2.1.4",
|
||||
"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": "^5.23.0",
|
||||
"webpack-bundle-analyzer": "^4.4.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.1.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"globby": "^11.0.1",
|
||||
"husky": "^5.0.9",
|
||||
"lint-staged": "^10.5.4",
|
||||
"mocha": "^8.3.0",
|
||||
"standard": "^16.0.3",
|
||||
"testcafe": "^1.11.0",
|
||||
"vercel": "^20.1.0"
|
||||
},
|
||||
"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",
|
||||
|
@ -185,5 +198,9 @@
|
|||
"lint-staged": {
|
||||
"*.js": "standard --fix",
|
||||
"*.html": "standard --fix --plugin html 'src/routes/**/*.html'"
|
||||
},
|
||||
"volta": {
|
||||
"node": "14.21.1",
|
||||
"yarn": "1.22.19"
|
||||
}
|
||||
}
|
||||
|
|
15
server.js
15
server.js
|
@ -1,9 +1,16 @@
|
|||
#!/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: rawRoutes } = require('./vercel.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()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#4169e1",
|
||||
"name": "Pinafore for Mastodon",
|
||||
"short_name": "Pinafore",
|
||||
"description": "Alternative web client for Mastodon, focused on speed and simplicity.",
|
||||
"name": "{intl.longAppName}",
|
||||
"short_name": "{intl.appName}",
|
||||
"description": "{intl.appDescription}",
|
||||
"categories": [
|
||||
"social"
|
||||
],
|
||||
|
@ -114,9 +114,7 @@
|
|||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Write a toot",
|
||||
"short_name": "New toot",
|
||||
"description": "Start composing a new toot",
|
||||
"name": "{intl.newStatus}",
|
||||
"url": "/?pwa=true&compose=true",
|
||||
"icons": [
|
||||
{
|
||||
|
@ -126,9 +124,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "View notifications",
|
||||
"short_name": "Notifications",
|
||||
"description": "View your new notifications",
|
||||
"name": "{intl.notifications}",
|
||||
"url": "/notifications?pwa=true",
|
||||
"icons": [
|
||||
{
|
|
@ -2,7 +2,7 @@
|
|||
<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="{intl.appDescription}" >
|
||||
|
||||
|
@ -15,24 +15,52 @@
|
|||
https://developers.google.com/web/fundamentals/native-hardware/fullscreen/ -->
|
||||
<meta name="mobile-web-app-capable" content="yes" >
|
||||
<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="white" >
|
||||
<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 {
|
||||
.inline-emoji, .theme-preview, .account-profile {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style id="theFocusVisibleStyle" media="all">
|
||||
/* :focus-visible styles */
|
||||
<style id="theFocusVisiblePolyfillStyle" media="only x">
|
||||
/* polyfill */
|
||||
/* Note we have to use [data-focus-visible-added] rather than .focus-visible because
|
||||
* Svelte overrides classes */
|
||||
|
@ -42,7 +70,8 @@
|
|||
.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 */
|
||||
|
@ -51,6 +80,13 @@
|
|||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style id="theCenterNavStyle" media="only x">
|
||||
@media (min-width: 992px) {
|
||||
.main-nav-ul {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
|
@ -85,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,15 +1,14 @@
|
|||
import * as sapper from '../__sapper__/client.js'
|
||||
import './routes/_utils/serviceWorkerClient'
|
||||
import './routes/_utils/historyEvents'
|
||||
import './routes/_utils/loadingMask'
|
||||
import './routes/_utils/forceOnline'
|
||||
import { mark, stop } from './routes/_utils/marks'
|
||||
import { loadPolyfills } from './routes/_utils/polyfills/loadPolyfills'
|
||||
import { loadNonCriticalPolyfills } from './routes/_utils/polyfills/loadNonCriticalPolyfills'
|
||||
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'
|
||||
|
||||
mark('loadPolyfills')
|
||||
loadPolyfills().then(() => {
|
||||
stop('loadPolyfills')
|
||||
Promise.all([idbReady(), loadPolyfills()]).then(() => {
|
||||
mark('sapperStart')
|
||||
sapper.start({ target: document.querySelector('#sapper') })
|
||||
stop('sapperStart')
|
||||
|
@ -18,6 +17,6 @@ loadPolyfills().then(() => {
|
|||
|
||||
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')
|
||||
|
|
|
@ -170,12 +170,12 @@ export default {
|
|||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
·
|
||||
{name}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
|
@ -188,8 +188,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Hefte {label} an',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} über der Beschränkung',
|
||||
underLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} übrig',
|
||||
composeStatus: 'Tröt erstellen',
|
||||
postStatus: 'Tröt!',
|
||||
contentWarning: 'Inhaltswarnung',
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
export default {
|
||||
categoriesLabel: 'Kategorien',
|
||||
emojiUnsupportedMessage: 'Dein Browser unterstützt keine farbigen Emojis.',
|
||||
favoritesLabel: 'Favoriten',
|
||||
loadingMessage: 'Wird geladen…',
|
||||
networkErrorMessage: 'Konnte Emoji nicht laden. Versuche, die Seite neu zu laden.',
|
||||
regionLabel: 'Emoji auswählen',
|
||||
searchDescription: 'Wenn Suchergebnisse verfügbar sind, wähle sie mit Pfeil rauf und runter, dann Eingabetaste, aus.',
|
||||
searchLabel: 'Suchen',
|
||||
searchResultsLabel: 'Suchergebnisse',
|
||||
skinToneDescription: 'Wenn angezeigt, nutze Pfeiltasten rauf und runter zum Auswählen, Eingabe zum Akzeptieren.',
|
||||
skinToneLabel: 'Wähle einen Hautton (aktuell {skinTone})',
|
||||
skinTonesLabel: 'Hauttöne',
|
||||
skinTones: [
|
||||
'Standard',
|
||||
'Hell',
|
||||
'Mittel-hell',
|
||||
'Mittel',
|
||||
'Mittel-dunkel',
|
||||
'Dunkel'
|
||||
],
|
||||
categories: {
|
||||
custom: 'Benutzerdefiniert',
|
||||
'smileys-emotion': 'Smileys und Emoticons',
|
||||
'people-body': 'Menschen und Körper',
|
||||
'animals-nature': 'Tiere und Natur',
|
||||
'food-drink': 'Essen und Trinken',
|
||||
'travel-places': 'Reisen und Orte',
|
||||
activities: 'Aktivitäten',
|
||||
objects: 'Objekte',
|
||||
symbols: 'Symbole',
|
||||
flags: 'Flaggen'
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
export default {
|
||||
categoriesLabel: 'Catégories',
|
||||
emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.',
|
||||
favoritesLabel: 'Favoris',
|
||||
loadingMessage: 'Chargement en cours…',
|
||||
networkErrorMessage: 'Impossible de charger les emojis. Veuillez essayer de recharger.',
|
||||
regionLabel: 'Choisir un emoji',
|
||||
searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.',
|
||||
searchLabel: 'Rechercher',
|
||||
searchResultsLabel: 'Résultats',
|
||||
skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.',
|
||||
skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})',
|
||||
skinTonesLabel: 'Couleurs de peau',
|
||||
skinTones: [
|
||||
'Défaut',
|
||||
'Clair',
|
||||
'Moyennement clair',
|
||||
'Moyen',
|
||||
'Moyennement sombre',
|
||||
'Sombre'
|
||||
],
|
||||
categories: {
|
||||
custom: 'Customisé',
|
||||
'smileys-emotion': 'Les smileyes et les émoticônes',
|
||||
'people-body': 'Les gens et le corps',
|
||||
'animals-nature': 'Les animaux et la nature',
|
||||
'food-drink': 'La nourriture et les boissons',
|
||||
'travel-places': 'Les voyages et les endroits',
|
||||
activities: 'Les activités',
|
||||
objects: 'Les objets',
|
||||
symbols: 'Les symbols',
|
||||
flags: 'Les drapeaux'
|
||||
}
|
||||
}
|
|
@ -27,6 +27,9 @@ export default {
|
|||
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',
|
||||
|
@ -150,6 +153,8 @@ export default {
|
|||
<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>
|
||||
|
@ -171,12 +176,12 @@ export default {
|
|||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
·
|
||||
{name}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
|
@ -189,8 +194,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Pin {label}',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {character} other {characters}} over limit',
|
||||
underLimit: '{count} {count, plural, =1 {character} other {characters}} remaining',
|
||||
composeStatus: 'Compose toot',
|
||||
postStatus: 'Toot!',
|
||||
contentWarning: 'Content warning',
|
||||
|
@ -202,7 +205,7 @@ export default {
|
|||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
description: 'Description',
|
||||
descriptionLabel: 'Describe for the visually impaired (image, video) or auditorily impaired (audio, video)',
|
||||
descriptionLabel: 'Describe for visually impaired (image, video) or auditorily impaired (audio, video) people',
|
||||
markAsSensitive: 'Mark media as sensitive',
|
||||
// Polls
|
||||
createPoll: 'Create poll',
|
||||
|
@ -226,7 +229,7 @@ export default {
|
|||
postPrivacyLabel: 'Adjust privacy (currently {label})',
|
||||
addContentWarning: 'Add content warning',
|
||||
removeContentWarning: 'Remove content warning',
|
||||
altLabel: 'Describe for the visually impaired',
|
||||
altLabel: 'Describe for visually impaired people',
|
||||
extractText: 'Extract text from image',
|
||||
extractingText: 'Extracting text…',
|
||||
extractingTextCompletion: 'Extracting text ({percent}% complete)…',
|
||||
|
@ -306,6 +309,10 @@ export default {
|
|||
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',
|
||||
|
@ -361,6 +368,7 @@ export default {
|
|||
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',
|
||||
|
@ -375,6 +383,8 @@ export default {
|
|||
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',
|
||||
|
@ -466,6 +476,7 @@ export default {
|
|||
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
|
||||
|
@ -486,6 +497,9 @@ export default {
|
|||
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,
|
||||
|
@ -500,9 +514,17 @@ export default {
|
|||
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.',
|
||||
|
@ -510,6 +532,7 @@ export default {
|
|||
// 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',
|
||||
|
@ -627,6 +650,8 @@ export default {
|
|||
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
|
||||
|
@ -653,5 +678,22 @@ export default {
|
|||
createdFilter: 'Created filter',
|
||||
failedToModifyFilter: 'Failed to modify filter: {error}',
|
||||
deletedFilter: 'Deleted filter',
|
||||
required: 'Required'
|
||||
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}'
|
||||
}
|
|
@ -171,12 +171,12 @@ export default {
|
|||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
·
|
||||
{name}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
|
@ -189,8 +189,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Epingler {label}',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {caractère} other {caractères}} en dessus de la limite',
|
||||
underLimit: '{count} {count, plural, =1 {caractère} other {caractères}} qui reste',
|
||||
composeStatus: 'Ecrire un pouet',
|
||||
postStatus: 'Pouet!',
|
||||
contentWarning: 'Avertissement',
|
||||
|
|
|
@ -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,6 +1,6 @@
|
|||
import { getAccountAccessibleName } from './getAccountAccessibleName'
|
||||
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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) {
|
||||
|
@ -11,6 +11,8 @@ function getNotificationText (notification, omitEmojiInDisplayNames) {
|
|||
return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'favourite') {
|
||||
return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'update') {
|
||||
return formatIntl('intl.accountEdited', { account: notificationAccountDisplayName })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,12 +39,15 @@ function cleanupText (text) {
|
|||
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
|
||||
shortInlineFormattedDate, spoilerText, showContent,
|
||||
reblog, notification, visibility, omitEmojiInDisplayNames,
|
||||
disableLongAriaLabels, showMedia, showPoll) {
|
||||
disableLongAriaLabels, showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll) {
|
||||
const originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
||||
const contentTextToShow = (showContent || !spoilerText)
|
||||
? cleanupText(plainTextContent)
|
||||
: 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)
|
||||
|
||||
|
@ -57,6 +62,7 @@ export function getAccessibleLabelForStatus (originalAccount, account, plainText
|
|||
originalAccountDisplayName,
|
||||
contentTextToShow,
|
||||
mediaTextToShow,
|
||||
...mediaDescText,
|
||||
pollTextToShow,
|
||||
shortInlineFormattedDate,
|
||||
`@${originalAccount.acct}`,
|
||||
|
|
|
@ -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,12 @@
|
|||
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
|
||||
|
@ -37,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,
|
||||
|
@ -97,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') || []
|
||||
|
@ -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,4 +1,4 @@
|
|||
import { store } from '../_store/store'
|
||||
import { store } from '../_store/store.js'
|
||||
|
||||
const emojiMapper = emoji => emoji.unicode ? emoji.unicode : `:${emoji.shortcodes[0]}:`
|
||||
const hashtagMapper = hashtag => `#${hashtag.name}`
|
||||
|
|
|
@ -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,9 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import * as emojiDatabase from '../_utils/emojiDatabase'
|
||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||
import { testEmojiSupported } from '../_utils/testEmojiSupported'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
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'
|
||||
|
||||
async function searchEmoji (searchText) {
|
||||
let emojis = await emojiDatabase.findBySearchQuery(searchText)
|
||||
|
|
|
@ -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,9 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark'
|
||||
import { database } from '../_database/database'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,13 +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 { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,5 +1,5 @@
|
|||
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
|
||||
|
|
|
@ -1,9 +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 { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
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 {
|
||||
|
@ -21,62 +18,10 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
function tryInitBlurhash () {
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
function getActualStatus (statusOrNotification) {
|
||||
return get(statusOrNotification, ['status']) ||
|
||||
get(statusOrNotification, ['notification', 'status'])
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||
let promiseChain = Promise.resolve()
|
||||
|
||||
tryInitBlurhash() // start the blurhash worker a bit early to save time
|
||||
prepareToRehydrate() // start blurhash early to save time
|
||||
|
||||
async function fetchFromIndexedDB (itemId) {
|
||||
mark(`fetchFromIndexedDB-${itemId}`)
|
||||
|
@ -92,10 +37,7 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
|
|||
|
||||
async function getStatusOrNotification (itemId) {
|
||||
const statusOrNotification = await fetchFromIndexedDB(itemId)
|
||||
await Promise.all([
|
||||
decodeAllBlurhashes(statusOrNotification),
|
||||
calculatePlainTextContent(statusOrNotification)
|
||||
])
|
||||
await rehydrateStatusOrNotification(statusOrNotification)
|
||||
return statusOrNotification
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { deleteStatus } from '../_api/delete'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// 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'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { store } from '../_store/store.js'
|
||||
import { goto } from '../../../__sapper__/client.js'
|
||||
|
||||
export function doQuickLoginIfNecessary () {
|
||||
const params = new URLSearchParams(location.search)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { store } from '../_store/store'
|
||||
import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters'
|
||||
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
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()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,5 +1,5 @@
|
|||
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()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { store } from '../_store/store'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
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.
|
||||
|
|
|
@ -1,18 +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'
|
||||
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) {
|
||||
|
@ -90,7 +90,7 @@ export async function logOutOfInstance (instanceName, message) {
|
|||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||
const { verifyCredentials } = store.get()
|
||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||
store.set({ verifyCredentials: verifyCredentials })
|
||||
store.set({ verifyCredentials })
|
||||
}
|
||||
|
||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||
|
@ -111,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 })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,9 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { uploadMedia } from '../_api/media'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
import { database } from '../_database/database'
|
||||
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()
|
||||
|
|
|
@ -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,9 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,8 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,9 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,7 +1,7 @@
|
|||
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
||||
import { database } from '../_database/database'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { report } from '../_api/report'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,8 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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 {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { search } from '../_api/search'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -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,8 +1,8 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockDomain, unblockDomain } from '../_api/blockDomain'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateRelationship } from './accounts'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,8 +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 { formatIntl } from '../_utils/formatIntl'
|
||||
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()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { toast } from '../_components/toast/toast'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
|
||||
import { formatIntl } from '../_utils/formatIntl'
|
||||
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 {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog'
|
||||
import { database } from '../_database/database'
|
||||
import { doMediaUpload } from './media'
|
||||
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 () {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { showMoreItemsForCurrentTimeline } from './timeline'
|
||||
import { scrollToTop } from '../_utils/scrollToTop'
|
||||
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid'
|
||||
import { store } from '../_store/store'
|
||||
import { tryToFocusElement } from '../_utils/tryToFocusElement'
|
||||
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { database } from '../_database/database'
|
||||
import { database } from '../_database/database.js'
|
||||
|
||||
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
|
||||
const status = await database.getStatus(instanceName, statusId)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { addStatusesOrNotifications } from '../addStatusOrNotification'
|
||||
import { getTimeline } from '../../_api/timelines'
|
||||
import { addStatusesOrNotifications } from '../addStatusOrNotification.js'
|
||||
import { getTimeline } from '../../_api/timelines.js'
|
||||
|
||||
const TIMELINE_GAP_BATCH_SIZE = 20 // Mastodon timeline API maximum limit
|
||||
const MAX_NUM_REQUESTS = 15 // to avoid getting caught in an infinite loop somehow
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { mark, stop } from '../../_utils/marks'
|
||||
import { deleteStatus } from '../deleteStatuses'
|
||||
import { addStatusOrNotification } from '../addStatusOrNotification'
|
||||
import { emit } from '../../_utils/eventBus'
|
||||
import { mark, stop } from '../../_utils/marks.js'
|
||||
import { deleteStatus } from '../deleteStatuses.js'
|
||||
import { addStatusOrNotification } from '../addStatusOrNotification.js'
|
||||
import { emit } from '../../_utils/eventBus.js'
|
||||
import { updateStatus } from '../updateStatus.js'
|
||||
|
||||
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed']
|
||||
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed', 'status.update']
|
||||
|
||||
export function processMessage (instanceName, timelineName, message) {
|
||||
let { event, payload } = (message || {})
|
||||
|
@ -12,7 +13,7 @@ export function processMessage (instanceName, timelineName, message) {
|
|||
return
|
||||
}
|
||||
mark('processMessage')
|
||||
if (['update', 'notification', 'conversation'].includes(event)) {
|
||||
if (['update', 'notification', 'conversation', 'status.update'].includes(event)) {
|
||||
payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason
|
||||
}
|
||||
|
||||
|
@ -35,11 +36,17 @@ export function processMessage (instanceName, timelineName, message) {
|
|||
// reproduce what is done for statuses for the conversation.
|
||||
//
|
||||
// It will add new DMs as new conversations instead of updating existing threads
|
||||
addStatusOrNotification(instanceName, timelineName, payload.last_status)
|
||||
if (payload.last_status) {
|
||||
// If the last_status doesn't exist, just ignore it. There's not much we can do in that case
|
||||
addStatusOrNotification(instanceName, timelineName, payload.last_status)
|
||||
}
|
||||
break
|
||||
case 'filters_changed':
|
||||
emit('wordFiltersChanged', instanceName)
|
||||
break
|
||||
case 'status.update':
|
||||
updateStatus(instanceName, payload)
|
||||
break
|
||||
}
|
||||
stop('processMessage')
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TimelineStream } from '../../_api/stream/TimelineStream'
|
||||
import { processMessage } from './processMessage'
|
||||
import { fillStreamingGap } from './fillStreamingGap'
|
||||
import { store } from '../../_store/store'
|
||||
import { TimelineStream } from '../../_api/stream/TimelineStream.js'
|
||||
import { processMessage } from './processMessage.js'
|
||||
import { fillStreamingGap } from './fillStreamingGap.js'
|
||||
import { store } from '../../_store/store.js'
|
||||
|
||||
export function createStream (api, instanceName, accessToken, timelineName, firstStatusId, firstNotificationId) {
|
||||
console.log(`streaming ${instanceName} ${timelineName}: createStream`, 'firstStatusId', firstStatusId,
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import { store } from '../_store/store'
|
||||
import { getTimeline } from '../_api/timelines'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { concat, mergeArrays } from '../_utils/arrays'
|
||||
import { compareTimelineItemSummaries } from '../_utils/statusIdSorting'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { getStatus, getStatusContext } from '../_api/statuses'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { TIMELINE_BATCH_SIZE } from '../_static/timelines'
|
||||
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import { addStatusesOrNotifications } from './addStatusOrNotification'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread'
|
||||
import { store } from '../_store/store.js'
|
||||
import { getTimeline } from '../_api/timelines.js'
|
||||
import { toast } from '../_components/toast/toast.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { concat, mergeArrays } from '../_utils/arrays.js'
|
||||
import { compareTimelineItemSummaries } from '../_utils/statusIdSorting.js'
|
||||
import { uniqBy, isEqual } from '../_thirdparty/lodash/objects.js'
|
||||
import { database } from '../_database/database.js'
|
||||
import { getStatus, getStatusContext } from '../_api/statuses.js'
|
||||
import { emit } from '../_utils/eventBus.js'
|
||||
import { TIMELINE_BATCH_SIZE } from '../_static/timelines.js'
|
||||
import { timelineItemToSummary } from '../_utils/timelineItemToSummary.js'
|
||||
import { addStatusesOrNotifications } from './addStatusOrNotification.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread.js'
|
||||
import li from 'li'
|
||||
|
||||
const byId = _ => _.id
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue