Porównaj commity

...

59 Commity

Autor SHA1 Wiadomość Data
Nolan Lawson 8f61ea75ce chore: cache png icons forever to lower vercel costs 2024-05-04 13:11:33 -07:00
Nolan Lawson 5889b404cb Revert "chore: remove small icons to reduce vercel costs"
This reverts commit 794d9ca74e.
2024-05-04 09:41:47 -07:00
Nolan Lawson 794d9ca74e chore: remove small icons to reduce vercel costs 2024-04-26 06:43:50 -07:00
Nolan Lawson 72a07ac40d
docs: mark as unmaintained (#2355) 2023-01-09 08:13:18 -08:00
Nolan Lawson ed9a9f6539 2.6.0 2023-01-09 07:56:22 -08:00
Arnaldo Gabriel 452b34b3b4
fix: grayscale mode support for header images (#2354) 2023-01-09 07:55:41 -08:00
Thomas Preece fd4bb4d864
feat: add option to always expand posts marked with content warnings (#2342)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2023-01-08 22:54:39 -08:00
vitalyster c426b7fe31
fix: OAuth2: use correct `Content-Type` as specified in RFC (#2343)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2023-01-08 22:31:00 -08:00
Noelia Ruiz Martínez c2851ce104
docs: explain how to use the buildCommand for internationalization (#2344) 2023-01-08 20:02:17 -08:00
Nolan Lawson 2578d0964d
chore: pin bundler/foreman versions (#2353) 2023-01-08 20:01:31 -08:00
Noelia Ruiz Martínez ff53fcab10
Replace builds with buildCommand in vercel.json (#2329)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-31 08:56:03 -08:00
Nolan Lawson 750235cd8f 2.5.1 2022-12-26 11:35:49 -08:00
Nolan Lawson b5cad87aaf
fix: lighten button colors on some themes (#2331) 2022-12-26 11:29:12 -08:00
Nick Colley a85ff62d48
fix: pitchback svgs not being visible (#2328) 2022-12-26 11:27:53 -08:00
Nick Colley e06f63684e
fix: improve dark theme icons (#2327) 2022-12-26 11:26:58 -08:00
Nick Colley f81778d37f
fix: improve icon readability in light theme (#2323)
Boost contrast of the default colour theme, to be closer to
the other theme's saturation then boost the unpressed state for action
buttons.

This brings the icons to 3:1 contrast while keeping colour in themes.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-18 12:07:53 -08:00
Nick Colley 746298a1f7
fix: pitchblack theme unpressed icons readability (#2324)
increase contrast so they're more readable.
2022-12-18 11:20:58 -08:00
Nolan Lawson 02f1dad098
fix: handle status edit events (#2325) 2022-12-18 11:20:17 -08:00
Nick Colley 3edfed971f
fix: notification page contrast (#2302)
Use lowest possible contrast gray that meets WCAG requirements for
very deemphasised text.

Makes the notification page more readable without compromising access.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-17 18:12:13 +00:00
Noelia Ruiz Martínez d71430f86d
feat: translation into Spanish (#2281)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
Co-authored-by: Noelia Ruiz Martínez <n4m1977@gmail.com>
2022-12-17 09:47:51 -08:00
Noelia Ruiz Martínez 6124c948de
fix: communicate expanded state of tooltips to screenreaders (#2322)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-17 09:47:02 -08:00
Nolan Lawson 774aa7a21c 2.5.0 2022-12-11 14:49:35 -08:00
Nolan Lawson 276c6e7bea
fix: show text for report notifications (#2318)
Fixes #2315
2022-12-11 13:09:12 -08:00
Nolan Lawson f61054a3d5
test: add test for #2263 (#2317) 2022-12-11 12:46:59 -08:00
Nolan Lawson b1dc43a9c9
fix: show proper notification text for follow request (#2314)
Fixes #1800
2022-12-11 12:01:01 -08:00
Nolan Lawson 040462f5b5
fix: fix pinned status aria-label/blurhash (#2313)
Fixes #2294
2022-12-11 11:00:45 -08:00
Thomas Broyer f5f3395a53
fix: fix rich push notifications for single-instance situations (#2296)
Partially addresses #1663.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-10 15:48:29 -08:00
Nick Colley 3fb152ac7c
fix: back button icon rendering inconsistently (#2306)
Depending on the operating system and therefore the system font
the back icon being a unicode arrow means it'll render inconsistently,
sometimes I've seen it looking really odd.

Instead make use of the font awesome arrow so that'll it render consistently
no matter ths system font.
2022-12-10 23:30:43 +00:00
Daniel Soohan Park 97e3b04f1f
fix: redesigned boost icon to fix alignment (#916) (#2297) 2022-12-10 14:50:46 -08:00
Scott Feeney 3c32b48e29
fix: improve toot edited notification (#2303) 2022-12-10 10:56:12 -08:00
Noelia Ruiz Martínez 4a6907bbdc
fix: report remaining chars to screenreaders (#2300)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-10 10:40:37 -08:00
Thomas Broyer d31c800806
fix: add badge and tag to simple push notifications (#2299) 2022-12-09 08:22:36 -08:00
Nolan Lawson 380d2a0d45
fix: fix poll "ends at" time (#2292)
Fixes #2286
2022-12-03 18:53:20 -08:00
Nolan Lawson 7fdbd72f13
fix: fix nav animations (#2291)
Fixes #2290
2022-12-03 16:48:22 -08:00
dependabot[bot] 62b30f6d99
chore(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#2289)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-03 16:35:55 -08:00
Nolan Lawson 6d6eb59f41
test: run tests on Mastodon v4 (#2256) 2022-12-02 15:09:58 -08:00
James Teh 30b00667f2
feat: Add "a" keyboard shortcut to bookmark a toot. (#2268)
Fixes #2237.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-02 14:01:02 -08:00
Nick Colley da28e98cfb
fix: contrast for active navigation (#2274)
Increase the background contrast for the selected page.
Increase the prominance of the indictor bar so we dont need to rely on
the background to have a strong contrast difference.
2022-12-02 13:58:29 -08:00
Nick Colley 7417e89f78
fix: improve wording of disabled identity (#2275)
Generally phrasing that talks about "the disabled" or "the visually impaired"
feels othering, whereas it is more common these days to have identity focused
framing.
2022-12-02 12:58:57 -08:00
James Teh 815438172e
feat: Make it possible to close inline reply with the escape key. (#2273)
Fixes #915.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-02 12:54:54 -08:00
James Teh 8fc9d5c728
feat: Allow image descriptions to be read automatically by screen readers without needing to expand media. (#2269)
Fixes #2257.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-12-02 12:54:03 -08:00
Scott Feeney a775bd9193
docs: update mastodon dev guide link (#2272) 2022-12-02 11:14:32 -08:00
Nolan Lawson edb7e7b442 2.4.0 2022-11-27 17:38:00 -08:00
Maxime Le Conte des Floris 3c857d74b8
fix: improve button a11y for theme Sorcery (#2266)
Similar to https://github.com/tootcafe/mastodon/pull/183
2022-11-27 17:04:49 -08:00
Ringtail Software 5eb7183048
feat: make click on reposter's small avatar image go to reposter's account page (#2260)
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-11-27 13:22:13 -08:00
Nolan Lawson a3f41917c7
fix: change page titles (#2211)
Co-authored-by: Gabriel de Perthuis <g2p.code@gmail.com>
2022-11-27 07:49:18 -08:00
Nolan Lawson 098da30f2a
test: fix flaky test (#2259) 2022-11-25 12:24:59 -08:00
Nolan Lawson abc39ef982
chore: remove emoji picker i18n files (#2258) 2022-11-25 12:06:09 -08:00
Nick Colley b543399e0a
fix: hide invisible content consistently (#2254)
Some other parts of the interface for example URLs in profiles
use the invisible class, move this to the top level global file
so it'll be applied everywhere.
2022-11-25 12:03:26 -08:00
Nolan Lawson fda00fc87c
chore: switch from circle ci to github actions (#2253) 2022-11-25 11:04:37 -08:00
Nick Colley 0e4523a37d
fix: bring check closer to icon (#2246)
Just a small aesthetic change to make them feel more connected.
2022-11-25 07:49:44 -08:00
Nolan Lawson 4fb8f37db7
fix: fix mismatched dark/light color-scheme (#2244) 2022-11-24 21:34:13 -08:00
Nolan Lawson fac42a91a0
fix: use dvh for bottom nav (#2241) 2022-11-24 15:24:29 -08:00
Nolan Lawson b50b9dc40b
fix: announce "loading more" with aria-live=polite after delay (#2240) 2022-11-24 11:02:17 -08:00
Nick Colley bc664e5ca1
fix: don't rely on colour for boost/favourite state (#2234)
By changing the shape it means no matter what the colour difference between
pressed and non-pressed it'll be possible to know the state.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-11-24 09:20:35 -08:00
Marco Zehe fa41fe7649
fix: remove alert role from the loading indicator (#2238)
...so it no longer interferes with screen readers. Fixes "Loading more" alert creates extraneous verbosity for screen reader users

Fixes #2226
2022-11-24 09:16:18 -08:00
Nolan Lawson 53803db5be
test: add test for shortcut when focus is inside status (#2232) 2022-11-23 08:49:40 -08:00
James Teh 8792d912bc
fix: focus the textarea in the Compose dialog (#2227)
ComposeBox already specified autoFocus for the ComposeInput.
However, this didn't work because the dialog was disabled when the ComposeInput code tried to focus the textarea.
To fix this, tweak A11yDialog to look for the autofocus attribute when determining what to focus.
This is consistent with the behavior for native HTML dialogs.
Then, have ComposeInput set this attribute if it's in a dialog.
Fixes #1839.
2022-11-23 08:21:42 -08:00
James Teh a447b9535e
fix: make shortcuts operate relative to the parent toot (#2229)
Previously, if focus was on an element inside a toot instead of the toot itself (e.g. moving to a toot and pressing tab), keyboard commands acted as if no toot was active.
In particular, this meant that the arrow keys scrolled to the first visible toot.
Fixes #2228.
2022-11-23 08:19:42 -08:00
107 zmienionych plików z 2547 dodań i 657 usunięć

Wyświetl plik

@ -1,212 +0,0 @@
version: 2.1
orbs:
browser-tools: circleci/browser-tools@1.1.3
workflows:
version: 2
build_and_test:
jobs:
- build_and_unit_test
- integration_test_readonly:
requires:
- build_and_unit_test
- integration_test_readwrite:
requires:
- build_and_unit_test
executors:
node:
working_directory: ~/pinafore
docker:
- image: cimg/ruby:3.0.3-browsers
node_and_ruby:
working_directory: ~/pinafore
docker:
- image: cimg/ruby:3.0.3-browsers
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: pinafore
POSTGRES_PASSWORD: pinafore
POSTGRES_DB: pinafore_development
BROWSER: chrome:headless
- image: circleci/redis:5-alpine
commands:
install_mastodon_system_dependencies:
description: Install system dependencies that Mastodon requires
steps:
- run:
name: Install system dependencies
command: |
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
install_browsers:
description: Install browsers and tools
steps:
- browser-tools/install-chrome:
chrome-version: 91.0.4472.114
- browser-tools/install-chromedriver
- run:
name: "Check browser version"
command: |
google-chrome --version
install_node:
description: Install Node.js
steps:
- run:
name: "Install Node.js"
# via https://circleci.com/docs/2.0/circleci-images/#notes-on-pinning-images
command: |
curl -sSL "https://nodejs.org/dist/v14.21.1/node-v14.21.1-linux-x64.tar.xz" \
| sudo tar --strip-components=2 -xJ -C /usr/local/bin/ node-v14.21.1-linux-x64/bin/node
- run:
name: Check current version of node
command: node -v
save_workspace:
description: Persist workspace
steps:
- persist_to_workspace:
root: .
paths:
- .
load_workspace:
description: Load workspace
steps:
- attach_workspace:
at: ~/pinafore
restore_yarn_cache:
description: Restore yarn cache
steps:
- restore_cache:
name: Restore yarn cache
key: yarn-v4-{{ checksum "yarn.lock" }}
save_yarn_cache:
description: Save yarn cache
steps:
- save_cache:
name: Save yarn cache
key: yarn-v4-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
restore_yarn_cache_mastodon:
description: Restore yarn cache for Mastodon
steps:
- restore_cache:
name: Restore yarn cache for Mastodon
key: yarn-v4-{{ checksum "mastodon/yarn.lock" }}
save_yarn_cache_mastodon:
description: Save yarn cache for Mastodon
steps:
- save_cache:
name: Save yarn cache for Mastodon
key: yarn-v4-{{ checksum "mastodon/yarn.lock" }}
paths:
- ~/.cache/yarn
restore_bundler_cache:
description: Restore bundler cache
steps:
- restore_cache:
name: Restore bundler cache
key: bundler-v4-{{ checksum "mastodon/Gemfile.lock" }}
save_bundler_cache:
description: Save bundler cache
steps:
- save_cache:
name: Save bundler cache
key: bundler-v4-{{ checksum "mastodon/Gemfile.lock" }}
paths:
- mastodon/vendor/bundle
install_mastodon:
description: Install Mastodon and set up Postgres/Redis
steps:
- run:
name: Clone mastodon
command: yarn clone-mastodon
- restore_yarn_cache_mastodon
- restore_bundler_cache
- run:
name: Install mastodon
command: yarn install-mastodon
- save_yarn_cache_mastodon
- save_bundler_cache
- 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
jobs:
build_and_unit_test:
executor: node
steps:
- checkout
- install_node
- restore_yarn_cache
- run:
name: Yarn install
command: yarn install --frozen-lockfile
- save_yarn_cache
- run:
name: Lint
command: yarn lint
- 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: Unit tests
command: yarn test-unit
- save_workspace
integration_test_readonly:
executor: node_and_ruby
steps:
- install_mastodon_system_dependencies
- install_browsers
- install_node
- load_workspace
- install_mastodon
- run:
name: Read-only integration tests
command: yarn test-in-ci-suite0
integration_test_readwrite:
executor: node_and_ruby
steps:
- install_mastodon_system_dependencies
- install_browsers
- install_node
- load_workspace
- install_mastodon
- run:
name: Read-write integration tests
command: yarn test-in-ci-suite1

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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 `clone-mastodon.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 `.circleci/config.yml`.
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`

Wyświetl plik

@ -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.

Wyświetl plik

@ -21,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$',
@ -51,7 +44,13 @@ const JSON_TEMPLATE = {
}
},
{
src: '^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp|woff|woff2)$',
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'
}

Wyświetl plik

@ -2,7 +2,7 @@ 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.js'
import { envFile, GIT_TAG, GIT_URL, RUBY_VERSION } from './mastodon-config.js'
import esMain from 'es-main'
const exec = childProcessPromise.exec
@ -11,9 +11,6 @@ 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.5.3'
const mastodonDir = path.join(dir, '../mastodon')
export default async function cloneMastodon () {

Wyświetl plik

@ -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

Wyświetl plik

@ -4,12 +4,14 @@ 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
de,
es
}
const intl = locales[LOCALE]

Wyświetl plik

@ -43,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'

Wyświetl plik

@ -18,7 +18,10 @@ DB_PASS=${DB_PASS}
BIND=0.0.0.0
`
export const RUBY_VERSION = '3.0.3'
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')

Wyświetl plik

@ -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)

Wyświetl plik

@ -1,5 +1,6 @@
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 },

Wyświetl plik

@ -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

Wyświetl plik

@ -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).

Wyświetl plik

@ -1,10 +1,10 @@
{
"name": "pinafore",
"description": "Alternative web client for Mastodon",
"version": "2.3.2",
"version": "2.6.0",
"type": "module",
"engines": {
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0"
"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'",
@ -31,11 +31,14 @@
"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 $BROWSER tests/spec/0*",
"testcafe-suite0": "cross-env-shell testcafe -c 2 $BROWSER tests/spec/0*",
"testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*",
"test-unit": "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",
@ -52,6 +55,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",

Wyświetl plik

@ -34,13 +34,19 @@
<style id="theBottomNavStyle" media="only x">
:root {
--nav-top: calc(100vh - var(--nav-total-height));
--nav-bottom: 0px;
--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">
@ -49,7 +55,7 @@
*/
img, svg, video,
input[type="checkbox"], input[type="radio"],
.inline-emoji, .theme-preview {
.inline-emoji, .theme-preview, .account-profile {
filter: grayscale(100%);
}
</style>

Wyświetl plik

@ -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',

Wyświetl plik

@ -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'
}
}

Wyświetl plik

@ -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'
}
}

Wyświetl plik

@ -153,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>
@ -174,12 +176,12 @@ export default {
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
·
{name}
`,
pinLabel: `{label} {pinnable, select,
true {
@ -192,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',
@ -205,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',
@ -229,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)…',
@ -368,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',
@ -497,6 +498,8 @@ export default {
}: {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,
@ -511,6 +514,9 @@ 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',
@ -526,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',

696
src/intl/es.js 100644
Wyświetl plik

@ -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}'
}

Wyświetl plik

@ -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',

Wyświetl plik

@ -17,7 +17,7 @@ export default {
logIn: 'Войти',
footer: `
<p>
Pinafore — это
Pinafore — это
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">программное обеспечение с открытым исходным кодом</a>
созданное
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
@ -192,8 +192,6 @@ export default {
}`,
pinPage: 'Закрепить {label}',
// Status composition
overLimit: '{count} {count, plural, =1 {символ} other {символов}} сверх лимита',
underLimit: '{count} {count, plural, =1 {символ} other {символов}} осталось',
composeStatus: 'Создать запись',
postStatus: 'Опубликовать!',
contentWarning: 'Предупреждение о содержимом',

Wyświetl plik

@ -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}`,

Wyświetl plik

@ -1,9 +1,6 @@
import { database } from '../_database/database.js'
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
import { mark, stop } from '../_utils/marks.js'
import { get } from '../_utils/lodash-lite.js'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.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
}

Wyświetl plik

@ -4,18 +4,28 @@ import { database } from '../_database/database.js'
import {
getPinnedStatuses
} 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 => {

Wyświetl plik

@ -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)
])
}

Wyświetl plik

@ -2,8 +2,9 @@ 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
}
@ -43,6 +44,9 @@ export function processMessage (instanceName, timelineName, message) {
case 'filters_changed':
emit('wordFiltersChanged', instanceName)
break
case 'status.update':
updateStatus(instanceName, payload)
break
}
stop('processMessage')
}

Wyświetl plik

@ -0,0 +1,13 @@
import { database } from '../_database/database.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
async function doUpdateStatus (instanceName, newStatus) {
console.log('updating status', newStatus)
await database.updateStatus(instanceName, newStatus)
}
export function updateStatus (instanceName, newStatus) {
scheduleIdleTask(() => {
/* no await */ doUpdateStatus(instanceName, newStatus)
})
}

Wyświetl plik

@ -27,11 +27,13 @@ export function generateAuthLink (instanceName, clientId, redirectUri) {
export function getAccessTokenFromAuthCode (instanceName, clientId, clientSecret, code, redirectUri) {
const url = `${basename(instanceName)}/oauth/token`
return post(url, {
// Using URLSearchParams here guarantees a content type of application/x-www-form-urlencoded
// See https://fetch.spec.whatwg.org/#bodyinit-unions
return post(url, new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
code
}, null, { timeout: WRITE_TIMEOUT })
}), null, { timeout: WRITE_TIMEOUT })
}

Wyświetl plik

@ -1,18 +1,20 @@
import { auth, basename } from './utils.js'
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax.js'
import { DEFAULT_TIMEOUT, get, post, put, WRITE_TIMEOUT } from '../_utils/ajax.js'
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
// post is create, put is edit
async function postOrPutStatus (url, accessToken, method, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses`
const body = {
status: text,
in_reply_to_id: inReplyToId,
media_ids: mediaIds,
sensitive,
spoiler_text: spoilerText,
visibility,
poll
poll,
...(method === 'post' && {
// you can't change these properties when editing
in_reply_to_id: inReplyToId,
visibility
})
}
for (const key of Object.keys(body)) {
@ -23,7 +25,23 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
}
}
return post(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT })
const func = method === 'post' ? post : put
return func(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT })
}
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses`
return postOrPutStatus(url, accessToken, 'post', text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll)
}
export async function putStatus (instanceName, accessToken, id, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses/${id}`
return postOrPutStatus(url, accessToken, 'put', text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll)
}
export async function getStatusContext (instanceName, accessToken, statusId) {

Wyświetl plik

@ -66,7 +66,7 @@ export async function getTimeline (instanceName, accessToken, timeline, maxId, s
}
if (timeline === 'notifications/mentions') {
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up']
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up', 'update', 'follow_request', 'admin.report']
}
url += '?' + paramsString(params)

Wyświetl plik

@ -8,7 +8,10 @@
<button type="button"
class="dynamic-page-go-back"
aria-label="{intl.goBack}"
on:click|preventDefault="onGoBack()">{intl.back}</button>
on:click|preventDefault="onGoBack()">
<SvgIcon className="dynamic-page-go-back-icon" href="#fa-arrow-left" />
{intl.back}
</button>
</div>
<Shortcut key="Backspace" on:pressed="onGoBack()"/>
<style>
@ -34,19 +37,25 @@
text-overflow: ellipsis;
}
.dynamic-page-go-back {
font-size: 1.3em;
display: inline-flex;
align-items: center;
justify-self: flex-end;
font-size: 1.2857142857142858em;
color: var(--anchor-text);
border: 0;
padding: 0;
background: none;
justify-self: flex-end;
}
.dynamic-page-go-back:hover {
text-decoration: underline;
}
.dynamic-page-go-back::before {
content: '←';
margin-right: 5px;
:global(.dynamic-page-go-back-icon) {
position: relative;
bottom: 0.06em;
margin-right: 0.2em;
height: 0.66666666em;
width: 0.66666666em;
fill: currentColor;
}
@media (max-width: 767px) {
.dynamic-page-banner {

Wyświetl plik

@ -12,9 +12,13 @@
ref:node
>
<SvgIcon className="icon-button-svg {svgClassName || ''}" ref:svg {href} />
{#if checked}
<SvgIcon className="icon-button-svg icon-button-check" ref:check href="#fa-check" />
{/if}
</button>
<style>
.icon-button {
position: relative;
padding: 6px 10px;
background: none;
border: none;
@ -31,6 +35,14 @@
pointer-events: none; /* hack for Edge */
}
:global(.icon-button-check) {
position: absolute;
top: 1px;
right: 2px;
height: 12px;
width: 12px;
}
:global(.icon-button.big-icon .icon-button-svg) {
width: 32px;
height: 32px;
@ -128,7 +140,8 @@
className: undefined,
sameColorWhenPressed: false,
ariaHidden: false,
clickListener: true
clickListener: true,
checked: false
}),
store: () => store,
computed: {
@ -144,8 +157,11 @@
ariaLabel: ({ pressable, pressed, label, pressedLabel }) => ((pressable && pressed) ? pressedLabel : label)
},
methods: {
animate (animation) {
animate (animation, checkmarkAnimation) {
this.refs.svg.animate(animation)
if (checkmarkAnimation && this.get().checked) {
this.refs.check.animate(checkmarkAnimation)
}
},
onClick (e) {
this.fire('click', e)

Wyświetl plik

@ -1,5 +1,6 @@
<span class="length-indicator {overLimit ? 'over-char-limit' : ''}"
aria-label={lengthLabel}
aria-live={lengthVerbosity}
aria-atomic='true'
{style}
>{lengthToDisplayDeferred}</span>
<style>
@ -17,10 +18,11 @@
import { store } from '../_store/store.js'
import { observe } from 'svelte-extras'
import { throttleTimer } from '../_utils/throttleTimer.js'
import { formatIntl } from '../_utils/formatIntl.js'
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
// How many chars within the limit to start warning at
const WARN_THRESHOLD = 10
export default {
oncreate () {
const { lengthToDisplay } = this.get()
@ -42,11 +44,12 @@
store: () => store,
computed: {
lengthToDisplay: ({ length, max }) => max - length,
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
if (overLimit) {
return formatIntl('intl.overLimit', { count: -lengthToDisplayDeferred })
lengthVerbosity: ({ lengthToDisplayDeferred }) => {
// When approaching the limit, notify screen reader users
if (lengthToDisplayDeferred > WARN_THRESHOLD) {
return 'off'
} else {
return formatIntl('intl.underLimit', { count: lengthToDisplayDeferred })
return 'polite'
}
}
},

Wyświetl plik

@ -12,7 +12,7 @@
/>
<span class="nav-link-label">{label}</span>
</div>
<div class="nav-indicator-wrapper">
<div class="nav-indicator-wrapper {animationClasses}">
<div class="nav-indicator" ref:indicator></div>
</div>
</a>
@ -45,35 +45,36 @@
.nav-indicator-wrapper {
width: 100%;
height: var(--nav-indicator-height);
background: var(--nav-a-border);
display: flex;
}
.nav-indicator {
flex: 1;
background: var(--nav-a-border);
transform-origin: left;
}
.nav-indicator.animate {
.nav-indicator {
background: var(--nav-indicator-bg);
}
.nav-indicator-wrapper.animating > .nav-indicator {
transition: transform 333ms ease-in-out;
will-change: transform;
}
.main-nav-link:hover .nav-indicator {
background: var(--nav-a-border-hover);
}
.main-nav-link.selected .nav-indicator-wrapper {
background: var(--nav-a-border-hover);
background: var(--nav-indicator-bg-hover);
}
.main-nav-link.selected .nav-indicator {
background: var(--nav-indicator-bg);
background: var(--nav-indicator-bg-active);
}
.main-nav-link.selected:hover .nav-indicator {
background: var(--nav-indicator-bg-hover);
/* Desktop/mouse only https://medium.com/@mezoistvan/finally-a-css-only-solution-to-hover-on-touchscreens-c498af39c31c */
@media(hover: hover) and (pointer: fine) {
.main-nav-link:hover .nav-indicator-wrapper.pre-animating {
background: var(--nav-indicator-bg-hover);
}
}
.main-nav-link:hover {
@ -129,6 +130,7 @@
import { scrollToTop } from '../_utils/scrollToTop.js'
import { normalizePageName } from '../_utils/normalizePageName.js'
import { formatIntl } from '../_utils/formatIntl.js'
import { classname } from '../_utils/classname.js'
export default {
oncreate () {
@ -148,6 +150,10 @@
})
},
store: () => store,
data: () => ({
preAnimating: false,
animating: false
}),
computed: {
selected: ({ page, name }) => name === normalizePageName(page),
ariaLabel: ({ selected, name, label, $numberOfNotifications, $numberOfFollowRequests }) => {
@ -166,6 +172,10 @@
),
badgeNumber: ({ name, $numberOfNotifications, $numberOfFollowRequests }) => (
(name === 'notifications' && $numberOfNotifications) || (name === 'community' && $numberOfFollowRequests) || 0
),
animationClasses: ({ animating, preAnimating }) => classname(
animating && 'animating',
preAnimating && 'pre-animating'
)
},
methods: {
@ -187,7 +197,7 @@
emit('animateNavPart2', { fromRect, toPage })
},
animatePart2 ({ fromRect }) {
const indicator = this.refs.indicator
const { indicator } = this.refs
mark('animateNavPart2 gBCR')
const toRect = indicator.getBoundingClientRect()
stop('animateNavPart2 gBCR')
@ -196,11 +206,12 @@
indicator.style.transform = `translateX(${translateX}px) scaleX(${scaleX})`
const onTransitionEnd = () => {
indicator.removeEventListener('transitionend', onTransitionEnd)
indicator.classList.remove('animate')
this.set({ animating: false, preAnimating: false })
}
indicator.addEventListener('transitionend', onTransitionEnd)
this.set({ preAnimating: true }) // avoids a flicker before the doubleRAF
doubleRAF(() => {
indicator.classList.add('animate')
this.set({ animating: true })
indicator.style.transform = ''
})
}

Wyświetl plik

@ -3,6 +3,8 @@
-->
<span class="tooltip-button"
aria-describedby="tooltip-{id}"
aria-expanded={shown}
aria-controls="tooltip-{id}"
role="button"
tabindex="0"
on:mouseover="set({shown: true, mouseover: true})"

Wyświetl plik

@ -117,7 +117,15 @@
firstTime = false
const { autoFocus } = this.get()
if (autoFocus) {
requestAnimationFrame(() => textarea.focus({ preventScroll: true }))
const { realm } = this.get()
if (realm === 'dialog') {
// If we're in a dialog, the dialog will be hidden at this
// point. Also, the dialog has its own initial focus behavior.
// Tell the dialog to focus the textarea.
textarea.setAttribute('autofocus', true)
} else {
requestAnimationFrame(() => textarea.focus({ preventScroll: true }))
}
}
}
})

Wyświetl plik

@ -119,7 +119,13 @@
if (!activeElement) {
return null
}
const activeItem = activeElement.getAttribute('id')
// The user might be focused on an element inside a toot. We want to
// move relative to that toot.
const activeArticle = activeElement.closest('article')
if (!activeArticle) {
return null
}
const activeItem = activeArticle.getAttribute('id')
if (!activeItem) {
return null
}

Wyświetl plik

@ -76,6 +76,10 @@
}
if (notificationType === 'admin.sign_up') {
return formatIntl('intl.accountSignedUp', params)
} else if (notificationType === 'follow_request') {
return formatIntl('intl.accountRequestedFollow', params)
} else if (notificationType === 'admin.report') {
return formatIntl('intl.accountReported', params)
} else { // 'follow'
return formatIntl('intl.accountFollowedYou', params)
}

Wyświetl plik

@ -39,7 +39,9 @@
{#if isStatusInOwnThread}
<StatusDetails {...params} {...timestampParams} />
{/if}
<StatusToolbar {...params} {replyShown} on:recalculateHeight />
{#if !isStatusInNotification}
<StatusToolbar {...params} {replyShown} on:recalculateHeight on:focusArticle="focusArticle()" />
{/if}
{#if replyShown}
<StatusComposeBox {...params} on:recalculateHeight />
{/if}
@ -133,6 +135,7 @@
import { composeNewStatusMentioning } from '../../_actions/mention.js'
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid.js'
import { addEmojiTooltips } from '../../_utils/addEmojiTooltips.js'
import { tryToFocusElement } from '../../_utils/tryToFocusElement.js'
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
@ -213,6 +216,10 @@
async mentionAuthor () {
const { accountForShortcut } = this.get()
await composeNewStatusMentioning(accountForShortcut)
},
focusArticle () {
const { elementId } = this.get()
tryToFocusElement(elementId, /* scroll */ true)
}
},
computed: {
@ -253,7 +260,7 @@
notification && notification.status &&
notification.type !== 'mention' && notification.status.id === originalStatusId
),
spoilerShown: ({ $spoilersShown, uuid }) => !!$spoilersShown[uuid],
spoilerShown: ({ $spoilersShown, uuid, $showAllSpoilers }) => (typeof $spoilersShown[uuid] === 'undefined' ? !!$showAllSpoilers : !!$spoilersShown[uuid]),
replyShown: ({ $repliesShown, uuid }) => !!$repliesShown[uuid],
showCard: ({ originalStatus, isStatusInNotification, showMedia, $hideCards }) => (
!$hideCards &&
@ -270,6 +277,13 @@
originalStatus.media_attachments &&
originalStatus.media_attachments.length
),
mediaAttachments: ({ originalStatus }) => (
originalStatus.media_attachments
),
sensitiveShown: ({ $sensitivesShown, uuid }) => !!$sensitivesShown[uuid],
sensitive: ({ originalStatus, $markMediaAsSensitive, $neverMarkMediaAsSensitive }) => (
!$neverMarkMediaAsSensitive && ($markMediaAsSensitive || originalStatus.sensitive)
),
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
@ -288,16 +302,16 @@
ariaLabel: ({
originalAccount, account, plainTextContent, shortInlineFormattedDate, spoilerText,
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels,
showMedia, showPoll
showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll
}) => (
getAccessibleLabelForStatus(originalAccount, account, plainTextContent,
shortInlineFormattedDate, spoilerText, showContent,
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels,
showMedia, showPoll
showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll
)
),
showHeader: ({ notification, status, timelineType }) => (
(notification && ['reblog', 'favourite', 'poll', 'status'].includes(notification.type)) ||
(notification && ['reblog', 'favourite', 'poll', 'status', 'update'].includes(notification.type)) ||
status.reblog ||
timelineType === 'pinned'
),

Wyświetl plik

@ -67,16 +67,6 @@
color: var(--very-deemphasized-link-color);
}
:global(.status-content .invisible) {
/* copied from Mastodon */
font-size: 0;
line-height: 0;
display: inline-block;
width: 0;
height: 0;
position: absolute;
}
:global(.underline-links .status-content a) {
text-decoration: underline;
}

Wyświetl plik

@ -1,6 +1,13 @@
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notificationType === 'follow' ? 'header-is-follow' : ''}">
<div class="status-header-avatar {timelineType === 'pinned' || notificationType === 'poll' ? 'hidden' : ''}">
<Avatar {account} size="extra-small"/>
<a id={avatarElementId}
href="/accounts/{accountId}"
rel="prefetch"
aria-hidden="true"
tabindex="-1"
>
<Avatar {account} size="extra-small"/>
</a>
</div>
<SvgIcon className="status-header-svg" href={icon} />
@ -10,7 +17,7 @@
{intl.pinnedStatus}
</span>
{:elseif notificationType !== 'poll'}
<a id={elementId}
<a id={authorElementId}
href="/accounts/{accountId}"
rel="prefetch"
class="status-header-author"
@ -114,7 +121,8 @@
},
store: () => store,
computed: {
elementId: ({ uuid }) => `status-header-${uuid}`,
authorElementId: ({ uuid }) => `status-header-author-${uuid}`,
avatarElementId: ({ uuid }) => `status-header-avatar-${uuid}`,
notificationType: ({ notification }) => notification && notification.type,
icon: ({ notificationType, status, timelineType }) => {
if (timelineType === 'pinned') {
@ -129,6 +137,12 @@
return '#fa-comment'
} else if (notificationType === 'admin.sign_up') {
return '#fa-user-plus'
} else if (notificationType === 'update') {
return '#fa-pencil'
} else if (notificationType === 'follow_request') {
return '#fa-hourglass'
} else if (notificationType === 'admin.report') {
return '#fa-flag'
}
return '#fa-star'
},
@ -151,6 +165,12 @@
}
} else if (status && status.reblog) {
return 'intl.reblogged'
} else if (notificationType === 'update') {
return 'intl.edited'
} else if (notificationType === 'follow_request') {
return 'intl.requestedFollow'
} else if (notificationType === 'admin.report') {
return 'intl.reported'
} else {
return ''
}

Wyświetl plik

@ -122,7 +122,7 @@
}
.status-in-notification svg {
opacity: 0.5;
stroke: var(--very-deemphasized-text-color);
}
.status-in-own-thread .option-text {
@ -307,7 +307,10 @@
expired: ({ poll }) => poll.expired,
expiresAt: ({ poll }) => poll.expires_at,
// Misskey can have polls that never end. These give expiresAt as null
expiresAtTS: ({ expiresAt }) => typeof expiresAt === 'number' ? new Date(expiresAt).getTime() : null,
// Also, Mastodon v4+ uses ISO strings, whereas Mastodon pre-v4 used numbers
expiresAtTS: ({ expiresAt }) => (
(typeof expiresAt === 'number' || typeof expiresAt === 'string') ? new Date(expiresAt).getTime() : null
),
expiresAtTimeagoFormatted: ({ expiresAtTS, expired, $now }) => (
expired ? formatTimeagoDate(expiresAtTS, $now) : formatTimeagoFutureDate(expiresAtTS, $now)
),

Wyświetl plik

@ -76,8 +76,9 @@
methods: {
toggleSpoilers (shown) {
const { uuid } = this.get()
const { spoilersShown } = this.store.get()
spoilersShown[uuid] = typeof shown === 'undefined' ? !spoilersShown[uuid] : !!shown
const { spoilersShown, showAllSpoilers } = this.store.get()
const currentValue = typeof spoilersShown[uuid] === 'undefined' ? !!showAllSpoilers : spoilersShown[uuid]
spoilersShown[uuid] = typeof shown === 'undefined' ? !currentValue : !!shown
this.store.set({ spoilersShown })
requestAnimationFrame(() => {
mark('clickSpoilerButton')

Wyświetl plik

@ -14,6 +14,7 @@
pressedLabel="Unboost"
pressable={!reblogDisabled}
pressed={reblogged}
checked={reblogged}
disabled={reblogDisabled}
href={reblogIcon}
clickListener={false}
@ -25,6 +26,7 @@
pressedLabel="{intl.unfavorite}"
pressable={true}
pressed={favorited}
checked={favorited}
href="#fa-star"
clickListener={false}
elementId={favoriteKey}
@ -40,7 +42,9 @@
{#if enableShortcuts}
<Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite(true)"/>
<Shortcut scope={shortcutScope} key="r" on:pressed="reply()"/>
<Shortcut scope={shortcutScope} key="escape" on:pressed="dismiss()"/>
<Shortcut scope={shortcutScope} key="b" on:pressed="reblog(true)"/>
<Shortcut scope={shortcutScope} key="a" on:pressed="bookmark()"/>
{/if}
<style>
.status-toolbar {
@ -75,9 +79,10 @@
import { setReblogged } from '../../_actions/reblog.js'
import { importShowStatusOptionsDialog } from '../dialog/asyncDialogs/importShowStatusOptionsDialog.js'
import { updateProfileAndRelationship } from '../../_actions/accounts.js'
import { FAVORITE_ANIMATION, REBLOG_ANIMATION } from '../../_static/animations.js'
import { CHECKMARK_ANIMATION, FAVORITE_ANIMATION, REBLOG_ANIMATION } from '../../_static/animations.js'
import { on } from '../../_utils/eventBus.js'
import { announceAriaLivePolite } from '../../_utils/announceAriaLivePolite.js'
import { setStatusBookmarkedOrUnbookmarked } from '../../_actions/bookmark.js'
export default {
oncreate () {
@ -118,7 +123,7 @@
const newFavoritedValue = !favorited
/* no await */ setFavorited(originalStatusId, newFavoritedValue)
if (newFavoritedValue) {
this.refs.favoriteIcon.animate(FAVORITE_ANIMATION)
this.refs.favoriteIcon.animate(FAVORITE_ANIMATION, CHECKMARK_ANIMATION)
}
if (announce) {
announceAriaLivePolite(newFavoritedValue ? 'intl.favorited' : 'intl.unfavorited')
@ -129,7 +134,7 @@
const newRebloggedValue = !reblogged
/* no await */ setReblogged(originalStatusId, newRebloggedValue)
if (newRebloggedValue) {
this.refs.reblogIcon.animate(REBLOG_ANIMATION)
this.refs.reblogIcon.animate(REBLOG_ANIMATION, CHECKMARK_ANIMATION)
}
if (announce) {
announceAriaLivePolite(newRebloggedValue ? 'intl.reblogged' : 'intl.unreblogged')
@ -144,6 +149,13 @@
this.fire('recalculateHeight')
})
},
dismiss () {
const { replyShown } = this.get()
if (replyShown) {
this.reply()
this.fire('focusArticle')
}
},
async onOptionsClick () {
const { originalStatus, originalAccountId } = this.get()
const updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
@ -164,6 +176,10 @@
// return status to the reply button after posting a reply
this.refs.node.querySelector('.status-toolbar-reply-button').focus({ preventScroll: true })
} catch (e) { /* ignore */ }
},
bookmark () {
const { originalStatus, originalStatusId } = this.get()
/* no await */ setStatusBookmarkedOrUnbookmarked(originalStatusId, !originalStatus.bookmarked)
}
},
data: () => ({

Wyświetl plik

@ -1,7 +1,6 @@
<div class="loading-footer {shown ? '' : 'hidden'}">
<div class="loading-wrapper {showLoading ? 'shown' : ''}"
aria-hidden={!showLoading}
role="alert"
>
<!-- Sapper's mousemove event listener schedules style recalculations for the loading spinner in
Chrome because it's always animating, even when hidden. So disable animations when not visible
@ -66,11 +65,30 @@
}
</style>
<script>
import { observe } from 'svelte-extras'
import LoadingSpinner from '../LoadingSpinner.html'
import { store } from '../../_store/store.js'
import { fetchMoreItemsAtBottomOfTimeline } from '../../_actions/timeline.js'
import { announceAriaLivePolite } from '../../_utils/announceAriaLivePolite.js'
const SCREEN_READER_ANNOUNCE_DELAY = 1000 // 1 second
export default {
oncreate () {
// If the new statuses are delayed a significant amount of time, announce to screen readers that we're loading
let delayedAriaAnnouncementHandle
this.observe('showLoading', showLoading => {
if (showLoading) {
delayedAriaAnnouncementHandle = setTimeout(() => {
delayedAriaAnnouncementHandle = undefined
announceAriaLivePolite('intl.loadingMore')
}, SCREEN_READER_ANNOUNCE_DELAY)
} else if (delayedAriaAnnouncementHandle) {
clearTimeout(delayedAriaAnnouncementHandle)
}
})
},
store: () => store,
computed: {
shown: ({ $timelineInitialized, $runningUpdate, $disableInfiniteScroll }) => (
@ -80,6 +98,7 @@
showLoadButton: ({ $runningUpdate, $disableInfiniteScroll }) => !$runningUpdate && $disableInfiniteScroll
},
methods: {
observe,
onClickLoadMore (e) {
e.preventDefault()
e.stopPropagation()

Wyświetl plik

@ -3,12 +3,13 @@ import { getInCache, hasInCache, statusesCache } from '../cache.js'
import { STATUSES_STORE } from '../constants.js'
import { cacheStatus } from './cacheStatus.js'
import { putStatus } from './insertion.js'
import { cloneForStorage } from '../helpers.js'
//
// update statuses
//
async function updateStatus (instanceName, statusId, updateFunc) {
async function doUpdateStatus (instanceName, statusId, updateFunc) {
const db = await getDatabase(instanceName)
if (hasInCache(statusesCache, instanceName, statusId)) {
const status = getInCache(statusesCache, instanceName, statusId)
@ -25,7 +26,7 @@ async function updateStatus (instanceName, statusId, updateFunc) {
}
export async function setStatusFavorited (instanceName, statusId, favorited) {
return updateStatus(instanceName, statusId, status => {
return doUpdateStatus(instanceName, statusId, status => {
const delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
status.favourited = favorited
status.favourites_count = (status.favourites_count || 0) + delta
@ -33,7 +34,7 @@ export async function setStatusFavorited (instanceName, statusId, favorited) {
}
export async function setStatusReblogged (instanceName, statusId, reblogged) {
return updateStatus(instanceName, statusId, status => {
return doUpdateStatus(instanceName, statusId, status => {
const delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0)
status.reblogged = reblogged
status.reblogs_count = (status.reblogs_count || 0) + delta
@ -41,19 +42,36 @@ export async function setStatusReblogged (instanceName, statusId, reblogged) {
}
export async function setStatusPinned (instanceName, statusId, pinned) {
return updateStatus(instanceName, statusId, status => {
return doUpdateStatus(instanceName, statusId, status => {
status.pinned = pinned
})
}
export async function setStatusMuted (instanceName, statusId, muted) {
return updateStatus(instanceName, statusId, status => {
return doUpdateStatus(instanceName, statusId, status => {
status.muted = muted
})
}
export async function setStatusBookmarked (instanceName, statusId, bookmarked) {
return updateStatus(instanceName, statusId, status => {
return doUpdateStatus(instanceName, statusId, status => {
status.bookmarked = bookmarked
})
}
// For the full list, see https://docs.joinmastodon.org/methods/statuses/#edit
const PROPS_THAT_CAN_BE_EDITED = ['content', 'spoiler_text', 'sensitive', 'language', 'media_ids', 'poll']
export async function updateStatus (instanceName, newStatus) {
const clonedNewStatus = cloneForStorage(newStatus)
return doUpdateStatus(instanceName, newStatus.id, status => {
// We can't use a simple Object.assign() to merge because a prop might have been deleted
for (const prop of PROPS_THAT_CAN_BE_EDITED) {
if (!(prop in clonedNewStatus)) {
delete status[prop]
} else {
status[prop] = clonedNewStatus[prop]
}
}
})
}

Wyświetl plik

@ -8,6 +8,11 @@
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
{intl.showSensitive}
</label>
<label class="setting-group">
<input type="checkbox" id="choice-show-all-spoilers"
bind:checked="$showAllSpoilers" on:change="onChange(event)">
{intl.showAllSpoilers}
</label>
<label class="setting-group">
<input type="checkbox" id="choice-use-blurhash"
bind:checked="$ignoreBlurhash" on:change="onChange(event)">

Wyświetl plik

@ -1,39 +1,37 @@
export const FAVORITE_ANIMATION = [
{
properties: [
{ transform: 'scale(1)' },
{ transform: 'scale(2)' },
{ transform: 'scale(1)' }
],
options: {
duration: 333,
easing: 'ease-in-out'
}
},
{
properties: [
{ fill: 'var(--action-button-fill-color)' },
{ fill: 'var(--action-button-fill-color-pressed)' }
],
options: {
duration: 333,
easing: 'linear'
}
const growBigThenSmall = {
properties: [
{ transform: 'scale(1)' },
{ transform: 'scale(2)' },
{ transform: 'scale(1)' }
],
options: {
duration: 333,
easing: 'ease-in-out'
}
}
const fadeColorToPressedState = {
properties: [
{ fill: 'var(--action-button-fill-color)' },
{ fill: 'var(--action-button-fill-color-pressed)' }
],
options: {
duration: 333,
easing: 'linear'
}
}
export const FAVORITE_ANIMATION = [
growBigThenSmall,
fadeColorToPressedState
]
export const REBLOG_ANIMATION = FAVORITE_ANIMATION
export const FOLLOW_BUTTON_ANIMATION = [
{
properties: [
{ transform: 'scale(1)' },
{ transform: 'scale(2)' },
{ transform: 'scale(1)' }
],
options: {
duration: 333,
easing: 'ease-in-out'
}
}
growBigThenSmall
]
export const CHECKMARK_ANIMATION = [
fadeColorToPressedState
]

Wyświetl plik

@ -35,6 +35,7 @@ const persistedState = {
loggedInInstances: {},
loggedInInstancesInOrder: [],
markMediaAsSensitive: false,
showAllSpoilers: false,
neverMarkMediaAsSensitive: false,
ignoreBlurhash: false,
omitEmojiInDisplayNames: undefined,

Wyświetl plik

@ -108,7 +108,7 @@ A11yDialog.prototype.show = function (event) {
// it later, then set the focus to the first focusable child of the dialog
// element
focusedBeforeDialog = document.activeElement
setFocusToFirstItem(this.node)
setInitialFocus(this.node)
// Bind a focus event listener to the body element to make sure the focus
// stays trapped inside the dialog while open, and start listening for some
@ -281,7 +281,7 @@ A11yDialog.prototype._maintainFocus = function (event) {
// If the dialog is shown and the focus is not within the dialog element,
// move it back to its first focusable child
if (this.shown && !this.node.contains(event.target)) {
setFocusToFirstItem(this.node)
setInitialFocus(this.node)
}
}
@ -333,9 +333,17 @@ function collect (target) {
*
* @param {Element} node
*/
function setFocusToFirstItem (node) {
function setInitialFocus (node) {
const focusableChildren = getFocusableChildren(node)
// If there's an element with an autofocus attribute, focus that.
for (const child of focusableChildren) {
if (child.getAttribute('autofocus')) {
child.focus()
return
}
}
// Otherwise, focus the first focusable element.
if (focusableChildren.length) {
focusableChildren[0].focus()
}

Wyświetl plik

@ -0,0 +1,7 @@
Copyright <YEAR> <COPYRIGHT HOLDER>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Wyświetl plik

@ -0,0 +1,24 @@
// via https://github.com/joppuyo/large-small-dynamic-viewport-units-polyfill/blob/93782ffff5d76f46b71591b859aac44f3cd591b2/src/index.js
// with some stuff removed that we don't need
import { throttleTimer } from '../../_utils/throttleTimer.js'
// Don't execute this resize listener more than the browser can paint
const rafAlignedResize = process.browser && throttleTimer(requestAnimationFrame)
function setVh () {
const dvh = window.innerHeight * 0.01
document.documentElement.style.setProperty('--1dvh', (dvh + 'px'))
}
if (process.browser) {
// We run the calculation as soon as possible (eg. the script is in document head)
setVh()
// We run the calculation again when DOM has loaded
document.addEventListener('DOMContentLoaded', setVh)
// We run the calculation when window is resized
window.addEventListener('resize', () => {
rafAlignedResize(setVh)
})
}

Wyświetl plik

@ -51,7 +51,7 @@ async function _fetch (url, fetchOptions, options) {
async function _putOrPostOrPatch (method, url, body, headers, options) {
const fetchOptions = makeFetchOptions(method, headers, options)
if (body) {
if (body instanceof FormData) {
if (body instanceof FormData || body instanceof URLSearchParams) {
fetchOptions.body = body
} else {
fetchOptions.body = JSON.stringify(body)

Wyświetl plik

@ -37,3 +37,7 @@ export const importIntlListFormat = async () => { // has to be imported serially
/* webpackChunkName: '$polyfill$-internationalization' */ '@formatjs/intl-listformat/locale-data/en.js'
)
}
export const importDynamicViewportUnitsPolyfill = () => import(
/* webpackChunkName: '$polyfill$-dynamic-viewport-units' */ '../../_thirdparty/large-small-dynamic-viewport-units-polyfill/dynamic-viewport-utils-polyfill.js'
)

Wyświetl plik

@ -1,4 +1,5 @@
import {
importDynamicViewportUnitsPolyfill,
importIntlListFormat,
importIntlLocale, importIntlPluralRules, importIntlRelativeTimeFormat,
importRequestIdleCallback
@ -25,7 +26,8 @@ export async function loadPolyfills () {
mark('loadPolyfills')
await Promise.all([
typeof requestIdleCallback !== 'function' && importRequestIdleCallback(),
loadIntlPolyfillsIfNecessary()
loadIntlPolyfillsIfNecessary(),
!CSS.supports('height: 1dvh') && importDynamicViewportUnitsPolyfill()
])
stop('loadPolyfills')
}

Wyświetl plik

@ -172,15 +172,22 @@ function unmapKeys (keyMap, keys, component) {
function acceptShortcutEvent (event) {
const { target } = event
return !(
if (
event.altKey ||
event.metaKey ||
event.ctrlKey ||
(event.shiftKey && event.key !== '?') || // '?' is a special case - it is allowed
(target && (
target.isContentEditable ||
(event.shiftKey && event.key !== '?') // '?' is a special case - it is allowed
) {
return false
}
if (event.key === 'Escape') {
// Allow escape everywhere.
return true
}
// Don't allow other keys in text boxes.
return !(target && (
target.isContentEditable ||
['TEXTAREA', 'SELECT'].includes(target.tagName) ||
(target.tagName === 'INPUT' && !['radio', 'checkbox'].includes(target.getAttribute('type')))
))
)
))
}

Wyświetl plik

@ -4,7 +4,7 @@ import { scheduleIdleTask } from './scheduleIdleTask.js'
const RETRIES = 5
const TIMEOUT = 50
export async function tryToFocusElement (id) {
export async function tryToFocusElement (id, scroll) {
for (let i = 0; i < RETRIES; i++) {
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, TIMEOUT))
@ -13,7 +13,7 @@ export async function tryToFocusElement (id) {
const element = document.getElementById(id)
if (element) {
try {
element.focus({ preventScroll: true })
element.focus({ preventScroll: !scroll })
console.log('focused element', id)
return
} catch (e) {

Wyświetl plik

@ -1,4 +1,5 @@
<Title name="{intl.followers}" />
<!-- TODO: this should probably be formatted as intl rather than concatenated -->
<Title name="{profileName}{intl.followers}" />
<LazyPage {pageComponent} {params} />
@ -15,6 +16,11 @@
},
data: () => ({
pageComponent
})
}),
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct + ' · ')) || ''
}
}
}
</script>

Wyświetl plik

@ -1,4 +1,5 @@
<Title name="{intl.follows}" />
<!-- TODO: this should probably be formatted as intl rather than concatenated -->
<Title name="{profileName}{intl.follows}" />
<LazyPage {pageComponent} {params} />
@ -15,6 +16,11 @@
},
data: () => ({
pageComponent
})
}),
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct + ' · ')) || ''
}
}
}
</script>

Wyświetl plik

@ -1,4 +1,5 @@
<Title name="{intl.profile}" />
<!-- TODO: this should probably be formatted as intl rather than concatenated -->
<Title name="{profileName}{intl.profile}" />
<LazyPage {pageComponent} {params} />
@ -15,6 +16,11 @@
},
data: () => ({
pageComponent
})
}),
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct + ' · ')) || ''
}
}
}
</script>

Wyświetl plik

@ -1,4 +1,5 @@
<Title name="{intl.profileWithMedia}" />
<!-- TODO: this should probably be formatted as intl rather than concatenated -->
<Title name="{profileName}{intl.profileWithMedia}" />
<LazyPage {pageComponent} {params} />
@ -15,6 +16,11 @@
},
data: () => ({
pageComponent
})
}),
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct + ' · ')) || ''
}
}
}
</script>

Wyświetl plik

@ -1,4 +1,5 @@
<Title name="{intl.profileWithReplies}" />
<!-- TODO: this should probably be formatted as intl rather than concatenated -->
<Title name="{profileName}{intl.profileWithReplies}" />
<LazyPage {pageComponent} {params} />
@ -15,6 +16,11 @@
},
data: () => ({
pageComponent
})
}),
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct + ' · ')) || ''
}
}
}
</script>

Wyświetl plik

@ -207,3 +207,13 @@ textarea {
.inline-emoji {
font-family: CountryFlagEmojiPolyfill, PinaforeEmoji, sans-serif;
}
.invisible {
/* copied from Mastodon */
font-size: 0;
line-height: 0;
display: inline-block;
width: 0;
height: 0;
position: absolute;
}

Wyświetl plik

@ -26,30 +26,28 @@
--form-border: #{darken($border-color, 10%)};
--nav-bg: #{$main-theme-color};
--nav-active-bg: #{lighten($main-theme-color, 9%)};
--nav-active-bg: #{lighten($main-theme-color, 3%)};
--nav-border: #{darken($main-theme-color, 10%)};
--nav-a-border: #{$main-theme-color};
--nav-a-selected-border: #{$secondary-text-color};
--nav-a-selected-bg: #{lighten($main-theme-color, 10%)};
--nav-a-selected-active-bg: #{lighten($main-theme-color, 17%)};
--nav-a-selected-bg: #{lighten($main-theme-color, 3%)};
--nav-a-selected-active-bg: var(--nav-a-selected-bg-hover);
--nav-svg-fill: #{$secondary-text-color};
--nav-text-color: #{$secondary-text-color};
--nav-indicator-bg: #{rgba($secondary-text-color, 0.8)};
--nav-indicator-bg-hover: #{rgba($secondary-text-color, 0.85)};
--nav-indicator-bg: #{$main-theme-color};
--nav-indicator-bg-active: #{mix($secondary-text-color, $main-theme-color, 90%)};
--nav-indicator-bg-hover: #{mix($secondary-text-color, $main-theme-color, 60%)};
--nav-a-selected-border-hover: #{$secondary-text-color};
--nav-a-selected-bg-hover: #{lighten($main-theme-color, 15%)};
--nav-a-bg-hover: #{lighten($main-theme-color, 5%)};
--nav-a-border-hover: #{$main-theme-color};
--nav-a-selected-bg-hover: #{lighten($main-theme-color, 4.5%)};
--nav-a-bg-hover: #{lighten($main-theme-color, 1.5%)};
--nav-svg-fill-hover: #{$secondary-text-color};
--nav-text-color-hover: #{$secondary-text-color};
--action-button-fill-color: #{lighten($main-theme-color, 18%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 22%)};
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
--action-button-fill-color-pressed: #{darken($main-theme-color, 7%)};
--action-button-fill-color-pressed-hover: #{darken($main-theme-color, 2%)};
--action-button-fill-color-pressed-active: #{darken($main-theme-color, 15%)};
--action-button-fill-color: #{lighten($main-theme-color, 11.5%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 6%)};
--action-button-fill-color-active: #{$main-theme-color};
--action-button-fill-color-pressed: #{darken(saturate($main-theme-color, 5%), 6%)};
--action-button-fill-color-pressed-hover: #{darken(saturate($main-theme-color, 5%), 12%)};
--action-button-fill-color-pressed-active: #{darken(saturate($main-theme-color, 5%), 15%)};
--action-button-deemphasized-fill-color: #{$deemphasized-color};
--action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
@ -83,8 +81,8 @@
--deemphasized-text-color: #{$deemphasized-color};
--focus-outline: #{$focus-outline};
--very-deemphasized-link-color: #{rgba($anchor-color, 0.6)};
--very-deemphasized-text-color: #{rgba(#666, 0.6)};
--very-deemphasized-text-color: #757575;
--very-deemphasized-link-color: var(--very-deemphasized-text-color);
--status-direct-background: #{darken($body-bg-color, 5%)};
--main-theme-color: #{$main-theme-color};
@ -135,5 +133,5 @@
--focus-bg: #{rgba(0, 0, 0, 0.1)};
accent-color: #{$main-theme-color};
color-scheme: light dark;
color-scheme: light;
}

Wyświetl plik

@ -1,5 +1,5 @@
:root {
$deemphasized-color: lighten($main-bg-color, 45%);
$deemphasized-color: lighten($main-bg-color, 54%);
--action-button-deemphasized-fill-color: #{$deemphasized-color};
--action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
@ -12,8 +12,8 @@
--deemphasized-text-color: #{$deemphasized-color};
--very-deemphasized-link-color: #{rgba($anchor-color, 0.8)};
--very-deemphasized-text-color: #{lighten($main-bg-color, 32%)};
--very-deemphasized-text-color: #{lighten($main-bg-color, 44%)};
--very-deemphasized-link-color: var(--very-deemphasized-text-color);
--status-direct-background: #{darken($body-bg-color, 5%)};
--main-theme-color: #{$main-theme-color};
@ -53,5 +53,5 @@
--focus-bg: #{rgba(255, 255, 255, 0.1)};
color-scheme: dark light;
color-scheme: dark;
}

Wyświetl plik

@ -32,7 +32,6 @@ $compose-background: lighten($main-theme-color, 32%);
--nav-text-color: #{$main-text-color};
--nav-svg-fill-hover: #{$main-text-color};
--nav-text-color-hover: #{$main-text-color};
--nav-a-selected-border: #{$anchor-color};
--nav-a-selected-border-hover: #{$anchor-color};
accent-color: #{lighten($main-theme-color, 15%)};

Wyświetl plik

@ -24,4 +24,6 @@ $compose-background: lighten($main-theme-color, 52%);
--action-button-fill-color-pressed: #{darken($anchor-color, 7%)};
--action-button-fill-color-pressed-hover: #{darken($anchor-color, 2%)};
--action-button-fill-color-pressed-active: #{darken($anchor-color, 15%)};
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
}

Wyświetl plik

@ -12,3 +12,10 @@ $compose-background: lighten($main-theme-color, 17%);
@import "_base.scss";
@import "_light_scrollbars.scss";
:root {
// make the action buttons a bit lighter
--action-button-fill-color: #{lighten($main-theme-color, 17%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 10%)};
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
}

Wyświetl plik

@ -15,3 +15,7 @@ $compose-background: lighten($main-theme-color, 52%);
@import "_dark.scss";
@import "_dark_navbar.scss";
@import "_dark_scrollbars.scss";
:root {
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
}

Wyświetl plik

@ -11,4 +11,11 @@ $focus-outline: lighten($main-theme-color, 30%);
$compose-background: lighten($main-theme-color, 32%);
@import "_base.scss";
@import "_light_scrollbars.scss";
@import "_light_scrollbars.scss";
:root {
// make the action buttons a bit lighter
--action-button-fill-color: #{lighten($main-theme-color, 17%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 10%)};
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
}

Wyświetl plik

@ -23,6 +23,11 @@ $compose-background: darken($main-theme-color, 12%);
--button-primary-bg-hover: #56a7e1;
--button-primary-border: transparent;
--action-button-fill-color: #{lighten($main-theme-color, 30%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 36%)};
--action-button-fill-color-active: #{lighten($main-theme-color, 42%)};
--action-button-fill-color-pressed: #2b90d9;
--action-button-fill-color-pressed-hover: #2b90d9;
--action-button-fill-color-pressed-hover: #{darken(#2b90d9, 6%)};
--action-button-fill-color-pressed-active: #{darken(#2b90d9, 12%)};
}

Wyświetl plik

@ -12,4 +12,10 @@ $compose-background: darken($main-theme-color, 12%);
@import "_base.scss";
@import "_dark.scss";
@import "_dark_scrollbars.scss";
@import "_dark_scrollbars.scss";
:root {
--action-button-fill-color-pressed: #{lighten(saturate($main-theme-color, 25%), 8%)};
--action-button-fill-color-pressed-hover: #{lighten(saturate($main-theme-color, 25%), 12%)};
--action-button-fill-color-pressed-active: #{lighten(saturate($main-theme-color, 25%), 15%)};
}

Wyświetl plik

@ -33,10 +33,12 @@ $compose-background: darken($main-theme-color, 12%);
--form-bg: #{$body-bg-color};
--form-border: #{darken($border-color, 10%)};
--action-button-fill-color: #{lighten($main-theme-color, 20%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 30%)};
--action-button-fill-color-active: #{darken($main-theme-color, 40%)};
--action-button-fill-color: #{lighten($main-theme-color, 50%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 60%)};
--action-button-fill-color-active: #{darken($main-theme-color, 70%)};
--action-button-fill-color-pressed: #{lighten($main-theme-color, 85%)};
--action-button-fill-color-pressed-hover: #{lighten($main-theme-color, 100%)};
--action-button-fill-color-pressed-active: #{lighten($main-theme-color, 80%)};
--svg-fill: #{lighten($main-theme-color, 50%)};
}

Wyświetl plik

@ -15,3 +15,7 @@ $compose-background: lighten($main-theme-color, 52%);
@import "_dark.scss";
@import "_dark_navbar.scss";
@import "_dark_scrollbars.scss";
:root {
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
}

Wyświetl plik

@ -18,4 +18,5 @@ $compose-background: lighten($main-theme-color, 52%);
:root {
accent-color: #{darken($main-theme-color, 5%)};
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
}

Wyświetl plik

@ -23,4 +23,7 @@ $scrollbar-face-active: #{lighten($main-theme-color, 1%)};
:root {
accent-color: #{darken($main-theme-color, 12%)};
}
--button-primary-bg: #{$main-theme-color};
--button-primary-text: #141414;
}

Wyświetl plik

@ -21,9 +21,9 @@
//
--nav-font-size: 1rem;
--nav-indicator-height: 2px;
--nav-indicator-height: 3px;
--nav-border-bottom: 0px;
--nav-icon-pad-v: 15px;
--nav-icon-pad-v: 14px;
--nav-icon-pad-h: 20px;
--nav-icon-size: 20px;
@ -46,10 +46,9 @@
--main-border-size: 1px;
@media (max-width: 991px) {
--nav-icon-pad-v: 20px;
--nav-icon-pad-v: 18px;
--nav-icon-pad-h: 10px;
--nav-icon-size: 25px;
--nav-indicator-height: 3px;
--nav-border-bottom: 0px;
}

Wyświetl plik

@ -5,6 +5,8 @@ import {
} from '../__sapper__/service-worker.js'
import { get, post } from './routes/_utils/ajax.js'
import { setWebShareData, closeKeyValIDBConnection } from './routes/_database/webShare.js'
import { getKnownInstances } from './routes/_database/knownInstances.js'
import { basename } from './routes/_api/utils.js'
const timestamp = process.env.SAPPER_TIMESTAMP
const ASSETS = `assets_${timestamp}`
@ -169,8 +171,18 @@ self.addEventListener('fetch', event => {
self.addEventListener('push', event => {
event.waitUntil((async () => {
const data = event.data.json()
const { origin } = event.target
// If there is only once instance, then we know for sure that the push notification came from it
const knownInstances = await getKnownInstances()
if (knownInstances.length !== 1) {
// TODO: Mastodon currently does not tell us which instance the push notification came from.
// So we have to guess and currently just choose the first one. We _could_ locally store the instance that
// currently has push notifications enabled, but this would only work for one instance at a time.
// See: https://github.com/mastodon/mastodon/issues/22183
await showSimpleNotification(data)
return
}
const origin = basename(knownInstances[0])
try {
const notification = await get(`${origin}/api/v1/notifications/${data.notification_id}`, {
Authorization: `Bearer ${data.access_token}`
@ -185,8 +197,10 @@ self.addEventListener('push', event => {
async function showSimpleNotification (data) {
await self.registration.showNotification(data.title, {
badge: '/icon-push-badge.png',
icon: data.icon,
body: data.body,
tag: data.notification_id,
data: {
url: `${self.origin}/notifications`
}
@ -201,6 +215,8 @@ async function showRichNotification (data, notification) {
switch (notification.type) {
case 'follow':
case 'follow_request':
case 'admin.report':
case 'admin.sign_up': {
await self.registration.showNotification(data.title, {
badge,

Wyświetl plik

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 1792"><path fill="#fff" d="M1344 1504q0 13-9 23t-23 9H352q-8 0-13-2t-9-7-6-8-3-11-1-12V896H128q-26 0-45-19t-19-45q0-24 15-41l320-384q19-22 49-22t49 22l320 384q15 17 15 41 0 26-19 45t-45 19H576v384h576q16 0 25 11l160 192q7 10 7 21zm640-416q0 24-15 41l-320 384q-20 23-49 23t-49-23l-320-384q-15-17-15-41 0-26 19-45t45-19h192V640H896q-16 0-25-12L711 436q-7-9-7-20 0-13 10-22t22-10h960q8 0 14 2t9 7 5 8 3 12 1 11v600h192q26 0 45 19t19 45z"/></svg>
<svg viewBox="0 0 1792 1792" width="1792" height="1792" xmlns:svg="http://www.w3.org/2000/svg"><path fill="#fff" d="m 384.00001,344.0625 a 71.966714,71.966714 0 0 0 -56.18749,27 l -256.000008,320 a 71.966714,71.966714 0 0 0 56.187498,116.875 h 104.0625 V 1376 a 71.966714,71.966714 0 0 0 71.9375,71.9375 H 1024 a 71.966714,71.966714 0 0 0 56.1875,-116.875 l -128,-160 a 71.966714,71.966714 0 0 0 -56.18749,-27 h -360.0625 v -336.125 h 104.0625 a 71.966714,71.966714 0 0 0 56.18748,-116.875 l -256,-320 a 71.966714,71.966714 0 0 0 -56.18748,-27 z m 384,0 a 71.966714,71.966714 0 0 0 -56.18749,116.875 l 128,160 a 71.966714,71.966714 0 0 0 56.18749,27 h 360.06249 v 336.125 H 1152 a 71.966714,71.966714 0 0 0 -56.1875,116.875 l 256,320 a 71.966714,71.966714 0 0 0 112.375,0 l 256,-320 A 71.966714,71.966714 0 0 0 1664,984.0625 H 1559.9375 V 416 A 71.966714,71.966714 0 0 0 1488,344.0625 Z" /></svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 500 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 897 B

Plik diff jest za duży Load Diff

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -2,7 +2,7 @@ import { favoriteStatus } from '../src/routes/_api/favorite.js'
import fetch from 'node-fetch'
import FileApi from 'file-api'
import { users } from './users.js'
import { postStatus } from '../src/routes/_api/statuses.js'
import { postStatus, putStatus } from '../src/routes/_api/statuses.js'
import { deleteStatus } from '../src/routes/_api/delete.js'
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests.js'
import { followAccount, unfollowAccount } from '../src/routes/_api/follow.js'
@ -33,6 +33,11 @@ export async function postAs (username, text) {
null, null, false, null, 'public')
}
export async function putAs (username, text, statusId) {
return putStatus(instanceName, users[username].accessToken, statusId, text,
null, null, false, null, 'public')
}
export async function postWithSpoilerAndPrivacyAs (username, text, spoiler, privacy) {
return postStatus(instanceName, users[username].accessToken, text,
null, null, true, spoiler, privacy)

Wyświetl plik

@ -35,3 +35,10 @@ test('shows direct vs followers-only vs regular in notifications', async t => {
.eql('Cannot be boosted because this is a direct message')
.expect($(`${getNthStatusSelector(5)} .status-toolbar button:nth-child(2)`).hasAttribute('disabled')).ok()
})
test('hides status toolbar on notification page', async t => {
await loginAsFoobar(t)
await t
.navigateTo('/notifications')
.expect($(`${getNthStatusSelector(1)} .status-toolbar`).exists).notOk()
})

Wyświetl plik

@ -41,8 +41,8 @@ test('shows account profile 3', async t => {
.expect(accountProfileName.innerText).contains('foobar')
.expect(accountProfileUsername.innerText).contains('@foobar')
// can't follow or be followed by your own account
.expect(accountProfileFollowedBy.innerText).eql('')
.expect($('.account-profile .account-profile-follow').innerText).eql('')
.expect(accountProfileFollowedBy.innerText).match(/\s*/)
.expect($('.account-profile .account-profile-follow').innerText).match(/\s*/)
})
test('shows account profile statuses', async t => {

Wyświetl plik

@ -14,7 +14,7 @@ import {
getActiveElementTagName,
getActiveElementClassList,
getNthStatusSensitiveMediaButton,
getActiveElementAriaLabel, settingsNavButton, getActiveElementHref, communityNavButton
getActiveElementAriaLabel, settingsNavButton, getActiveElementHref, communityNavButton, getActiveElementId
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
@ -59,7 +59,7 @@ test('timeline link preserves focus', async t => {
await loginAsFoobar(t)
await t
.expect(getNthStatus(1).exists).ok({ timeout: 20000 })
.click($(`${getNthStatusSelector(1)} .status-header a`))
.click($(`${getNthStatusSelector(1)} .status-header-author`))
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/')
@ -73,12 +73,28 @@ test('timeline link preserves focus', async t => {
.expect(getActiveElementInsideNthStatus()).eql('1')
})
test('timeline link preserves focus - reblogger avatar', async t => {
await loginAsFoobar(t)
await t
.expect(getNthStatus(1).exists).ok({ timeout: 20000 })
const avatar = `${getNthStatusSelector(1)} .status-header-avatar a`
const id = await $(avatar).getAttribute('id')
await t
.click($(avatar))
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatus(1).exists).ok()
.expect(getActiveElementId()).eql(id)
})
test('notification timeline preserves focus', async t => {
await loginAsFoobar(t)
await t
.navigateTo('/notifications')
await scrollToStatus(t, 6)
await t.click($(`${getNthStatusSelector(6)} .status-header a`))
await t.click($(`${getNthStatusSelector(6)} .status-header-author`))
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).contains('/notifications')

Wyświetl plik

@ -2,7 +2,7 @@ import { loginAsFoobar } from '../roles'
import {
generalSettingsButton,
getNthShowOrHideButton,
getNthStatus, getNthStatusRelativeDateTime, homeNavButton,
getNthStatus, getNthStatusAndSensitiveButton, getNthStatusRelativeDateTime, homeNavButton,
notificationsNavButton,
scrollToStatus,
settingsNavButton
@ -39,6 +39,7 @@ test('aria-labels for CWed statuses', async t => {
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
)
// toggle the CW button
.click(getNthShowOrHideButton(1 + kittenIdx))
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
/foobar, here's a kitten with a CW, .* ago, @foobar, Public/i
@ -47,6 +48,26 @@ test('aria-labels for CWed statuses', async t => {
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
)
// toggle the "show sensitive media" button
.click(getNthStatusAndSensitiveButton(1 + kittenIdx, 1))
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
/foobar, Content warning: kitten CW, has media, kitten, .* ago, @foobar, Public/i
)
.click(getNthStatusAndSensitiveButton(1 + kittenIdx, 1))
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
)
})
test('aria-labels for two media attachments', async t => {
await loginAsFoobar(t)
const twoKittensIdx = homeTimeline.findIndex(_ => _.content === 'here\'s 2 kitten photos')
await scrollToStatus(t, 1 + twoKittensIdx)
await t
.hover(getNthStatus(1 + twoKittensIdx))
.expect(getNthStatus(1 + twoKittensIdx).getAttribute('aria-label')).match(
/foobar, here's 2 kitten photos, has media, kitten, kitten, .* ago, @foobar, Public/i
)
})
test('aria-labels for notifications', async t => {

Wyświetl plik

@ -7,9 +7,17 @@ import {
getNthStatusMediaImg,
getNthStatusSensitiveMediaButton,
getNthStatusSpoiler,
getUrl, modalDialog,
getUrl,
modalDialog,
scrollToStatus,
isNthStatusActive, getActiveElementRectTop, scrollToTop, isActiveStatusPinned, getFirstModalMedia
isNthStatusActive,
getActiveElementRectTop,
scrollToTop,
isActiveStatusPinned,
getFirstModalMedia,
getNthStatusAccountLink,
getNthStatusAccountLinkSelector,
focus, getNthComposeReplyInput, getActiveElementId, getActiveElementClassList
} from '../utils'
import { homeTimeline } from '../fixtures'
import { loginAsFoobar } from '../roles'
@ -216,3 +224,29 @@ test('Shortcut j/k change the active status on pinned statuses', async t => {
.expect(isNthStatusActive(1)()).ok()
.expect(isActiveStatusPinned()).eql(true)
})
test('Shortcut down makes next status active when focused inside of a status', async t => {
await loginAsFoobar(t)
await t
.expect(getNthStatusAccountLink(1).exists).ok()
await focus(getNthStatusAccountLinkSelector(1))()
await t
.pressKey('down')
.expect(isNthStatusActive(2)()).ok()
})
test('Press r to reply, press Esc to close reply', async t => {
await loginAsFoobar(t)
await t
.expect(getNthStatus(1).exists).ok()
await activateStatus(t, 0)
const id = await getActiveElementId()
await t
.expect(getNthComposeReplyInput(1).exists).notOk()
.pressKey('r')
.expect(getNthComposeReplyInput(1).exists).ok()
.expect(getActiveElementClassList()).contains('compose-box-input')
.pressKey('esc')
.expect(getNthComposeReplyInput(1).exists).notOk()
.expect(getActiveElementId()).eql(id)
})

Wyświetl plik

@ -1,10 +1,10 @@
import {
closeDialogButton,
composeModalInput,
getNthFavoritedLabel,
getNthStatus,
getUrl, modalDialog, notificationsNavButton,
isNthStatusActive, goBack
isNthStatusActive, goBack,
getNthFavoritedLabel
} from '../utils'
import { loginAsFoobar } from '../roles'
@ -12,16 +12,22 @@ fixture`026-shortcuts-notification.js`
.page`http://localhost:4002`
test('Shortcut f toggles favorite status in notification', async t => {
const idx = 0
const idx = 6 // "hello foobar"
await loginAsFoobar(t)
await t
.expect(getUrl()).eql('http://localhost:4002/')
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
.expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
.expect(getNthStatus(1).exists).ok({ timeout: 30000 })
for (let i = 0; i < idx + 1; i++) {
await t.pressKey('j')
.expect(getNthStatus(1 + i).exists).ok()
.expect(isNthStatusActive(1 + i)()).ok()
}
await t
.expect(getNthFavoritedLabel(1 + idx)).eql('Favorite')
.pressKey('j '.repeat(idx + 1))
.expect(isNthStatusActive(1 + idx)()).ok()
.pressKey('f')
.expect(getNthFavoritedLabel(1 + idx)).eql('Unfavorite')
.pressKey('f')

Wyświetl plik

@ -1,7 +1,7 @@
import {
getActiveElementTagName,
getUrl,
searchButton, searchInput, searchNavButton
searchButton, searchInput, searchNavButton, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
@ -40,6 +40,8 @@ test('Pressing / without logging in just goes to the search page', async t => {
await t
.expect(getUrl()).eql('http://localhost:4002/')
.expect($('.main-content h1').innerText).eql('Pinafore')
await sleep(500) // wait for keyboard shortcuts to be active
await t
.pressKey('/')
.expect(getUrl()).contains('/search')
.expect(getActiveElementTagName()).notMatch(/input/i)

Wyświetl plik

@ -0,0 +1,37 @@
import {
getUrl,
scrollToStatus,
getNthStatusSpoiler,
settingsNavButton,
generalSettingsButton,
homeNavButton,
getNthStatus,
getNthShowOrHideButton
} from '../utils'
import { loginAsFoobar } from '../roles'
import { homeTimeline } from '../fixtures.js'
import { Selector as $ } from 'testcafe'
fixture`043-content-warnings.js`
.page`http://localhost:4002`
test('Can set content warnings to auto-expand', async t => {
await loginAsFoobar(t)
await t
.expect(getUrl()).eql('http://localhost:4002/')
.click(settingsNavButton)
.click(generalSettingsButton)
.click($('#choice-show-all-spoilers'))
.click(homeNavButton)
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatus(1).exists).ok()
const idx = homeTimeline.findIndex(_ => _.spoiler === 'kitten CW')
await scrollToStatus(t, idx + 1)
await t
.expect(getNthStatusSpoiler(1 + idx).innerText).contains('kitten CW')
.expect(getNthStatus(1 + idx).innerText).contains('here\'s a kitten with a CW')
.click(getNthShowOrHideButton(1 + idx))
.expect(getNthStatus(1 + idx).innerText).notContains('here\'s a kitten with a CW')
.click(getNthShowOrHideButton(1 + idx))
.expect(getNthStatus(1 + idx).innerText).contains('here\'s a kitten with a CW')
})

Wyświetl plik

@ -12,7 +12,7 @@ test('shows unread notification', async t => {
await loginAsFoobar(t)
await t
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
.expect(getTitleText()).eql('localhost:3000 · Home')
.expect(getTitleText()).eql('Home · localhost:3000')
.expect(getNthStatusContent(1).innerText).contains('somebody please favorite this to validate me', {
timeout: 20000
})
@ -21,17 +21,17 @@ test('shows unread notification', async t => {
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)', {
timeout: 20000
})
.expect(getTitleText()).eql('(1) localhost:3000 · Home')
.expect(getTitleText()).eql('(1) Home · localhost:3000')
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)')
.expect(getTitleText()).eql('localhost:3000 · Notifications')
.expect(getTitleText()).eql('Notifications · localhost:3000')
.expect(getNthStatus(1).innerText).contains('somebody please favorite this to validate me')
.expect(getNthStatus(1).innerText).match(/admin\s+favorited your toot/)
await t
.click(homeNavButton)
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
.expect(getTitleText()).eql('localhost:3000 · Home')
.expect(getTitleText()).eql('Home · localhost:3000')
})
test('shows unread notifications, more than one', async t => {
@ -39,7 +39,7 @@ test('shows unread notifications, more than one', async t => {
await loginAsFoobar(t)
await t
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
.expect(getTitleText()).eql('localhost:3000 · Home')
.expect(getTitleText()).eql('Home · localhost:3000')
.expect(getNthStatusContent(1).innerText).contains('I need lots of favorites on this one', {
timeout: 20000
})
@ -49,14 +49,14 @@ test('shows unread notifications, more than one', async t => {
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (2 notifications)', {
timeout: 20000
})
.expect(getTitleText()).eql('(2) localhost:3000 · Home')
.expect(getTitleText()).eql('(2) Home · localhost:3000')
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)')
.expect(getTitleText()).eql('localhost:3000 · Notifications')
.expect(getTitleText()).eql('Notifications · localhost:3000')
.expect(getNthStatus(1).innerText).contains('I need lots of favorites on this one')
await t
.click(homeNavButton)
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
.expect(getTitleText()).eql('localhost:3000 · Home')
.expect(getTitleText()).eql('Home · localhost:3000')
})

Wyświetl plik

@ -37,10 +37,10 @@ test('External links, hashtags, and mentions have correct attributes', async t =
.expect(nthAnchor(3).getAttribute('href')).eql('/tags/tag')
.expect(nthAnchor(3).hasAttribute('rel')).notOk()
.expect(nthAnchor(3).hasAttribute('target')).notOk()
.expect(nthAnchor(4).getAttribute('href')).eql('/tags/anotherTag')
.expect(nthAnchor(4).getAttribute('href')).eql('/tags/anothertag')
.expect(nthAnchor(4).hasAttribute('rel')).notOk()
.expect(nthAnchor(4).hasAttribute('target')).notOk()
.expect(nthAnchor(5).getAttribute('href')).eql('/tags/yetAnotherTag')
.expect(nthAnchor(5).getAttribute('href')).eql('/tags/yetanothertag')
.expect(nthAnchor(5).hasAttribute('rel')).notOk()
.expect(nthAnchor(5).hasAttribute('target')).notOk()
.expect(nthAnchor(6).getAttribute('href')).eql('http://example.com')

Wyświetl plik

@ -2,8 +2,16 @@ import { loginAsLockedAccount } from '../roles'
import { followAs, unfollowAs } from '../serverActions'
import {
avatarInComposeBox,
communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
homeNavButton, sleep
communityNavButton,
followersButton,
getNthSearchResult,
getNthStatus,
getSearchResultByHref,
getUrl,
goBack,
homeNavButton,
notificationsNavButton,
sleep
} from '../utils'
import { users } from '../users'
import { Selector as $ } from 'testcafe'
@ -93,6 +101,9 @@ test('Shows unresolved follow requests', async t => {
await t
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (2 follow requests)')
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
.expect(getNthStatus(1).innerText).contains('requested to follow you')
.click(communityNavButton)
.expect(requestsButton.innerText).contains('Follow requests (2)')
.click(requestsButton)

Wyświetl plik

@ -4,10 +4,10 @@ import {
getNthPinnedStatusFavoriteButton,
getNthStatus, getNthStatusContent,
getNthStatusOptionsButton, getUrl, homeNavButton, postStatusButton, scrollToTop, scrollToBottom,
settingsNavButton, sleep
settingsNavButton, sleep, getNthStatusAccountLink
} from '../utils'
import { users } from '../users'
import { postAs } from '../serverActions'
import { postAs, postStatusWithMediaAs } from '../serverActions'
fixture`117-pin-unpin.js`
.page`http://localhost:4002`
@ -84,3 +84,22 @@ test('Saved pinned/unpinned state of status', async t => {
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile', { timeout })
})
test('pinned posts and aria-labels', async t => {
const timeout = 20000
await postStatusWithMediaAs('foobar', 'here is a sensitive kitty', 'kitten2.jpg', 'kitten', true)
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty', { timeout })
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
.click(getNthDialogOptionsOption(2))
.click(getNthStatusAccountLink(1))
.expect(getNthPinnedStatus(1).getAttribute('aria-label')).match(
/foobar, here is a sensitive kitty, has media, (.+ ago|just now), @foobar, Public/i
)
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty')
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile')
await sleep(2000)
})

Wyświetl plik

@ -11,7 +11,7 @@ test('aria-labels for statuses with no content text', async t => {
await t
.hover(getNthStatus(1))
.expect(getNthStatus(1).getAttribute('aria-label')).match(
/foobar, has media, (.+ ago|just now), @foobar, Public/i
/foobar, has media, kitteh, (.+ ago|just now), @foobar, Public/i
)
})

Wyświetl plik

@ -7,7 +7,7 @@ import {
sleep,
getNthStatusPollRefreshButton,
getNthStatusPollVoteCount,
getNthStatusRelativeDate, getUrl, goBack, getNthStatusSpoiler, getNthShowOrHideButton
getNthStatusRelativeDate, getUrl, goBack, getNthStatusSpoiler, getNthShowOrHideButton, getNthStatusPollExpiry
} from '../utils'
import { loginAsFoobar } from '../roles'
import { createPollAs, voteOnPollAs } from '../serverActions'
@ -22,6 +22,7 @@ test('Can vote on polls', async t => {
await t
.expect(getNthStatusContent(1).innerText).contains('vote on my cool poll')
.expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
.expect(getNthStatusPollExpiry(1).innerText).match(/Ends in .*/)
await sleep(1000)
await t
.click(getNthStatusPollOption(1, 2))
@ -32,6 +33,7 @@ test('Can vote on polls', async t => {
.expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('100% no')
.expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
.expect(getNthStatusPollExpiry(1).innerText).match(/Ends in .*/)
})
test('Can vote on multiple-choice polls', async t => {

Some files were not shown because too many files have changed in this diff Show More