Porównaj commity

...

370 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
Nolan Lawson 6b1533c947 2.3.2 2022-11-21 20:04:36 -08:00
Nolan Lawson 347dab4e29
fix: fix pencil button location with bottom nav (#2222) 2022-11-21 18:53:25 -08:00
Nolan Lawson fdec7b2b3d
docs: add details on use of Svelte v2 / Sapper 2022-11-21 07:42:30 -08:00
Сергей Ворон 7f86a94414
feat: create ru-RU.JS (#2218)
Russian language
2022-11-21 07:19:36 -08:00
Nolan Lawson 302845866a 2.3.1 2022-11-20 20:37:33 -08:00
Nolan Lawson f875e65c49
chore: bump z-index again (#2217) 2022-11-20 20:30:20 -08:00
Nolan Lawson 85bc6ba372
fix: fix position of toast/snackbar for bottom nav (#2213) 2022-11-20 11:50:38 -08:00
Nolan Lawson 00b6d31f0c 2.3.0 2022-11-20 10:00:38 -08:00
Nolan Lawson 035ab9cbff
fix: reduce flash-of-unstyled on grayscale (#2206) 2022-11-19 11:25:42 -08:00
Nolan Lawson ad73918fa8
feat(ui): bottom nav (#2205)
Co-authored-by: Benny Powers <web@bennypowers.com>
2022-11-19 10:13:57 -08:00
Nolan Lawson d57ab7238f
chore: update yarn.lock, check lockfile in CI (#2204) 2022-11-19 09:51:54 -08:00
Nolan Lawson fb5478cd06
fix: do not show "signed up" notifications in mentions tab (#2203)
Partially addresses #2199
2022-11-18 09:32:54 -08:00
Nolan Lawson 52880a4689
chore: update emoji-regex, replace copyright character (#2202) 2022-11-18 09:32:46 -08:00
Nolan Lawson 2131ababf3
fix: fix inserting emoji with skintone (#2201) 2022-11-18 09:32:39 -08:00
Nolan Lawson a318746961
chore: update dev dependencies (#2200) 2022-11-18 09:32:31 -08:00
Nolan Lawson 601c3e40c9
fix: recalculate toot height after voting on poll (#2198) 2022-11-17 19:57:26 -08:00
Nolan Lawson ff6e1dc6fc
chore: update non-breaking deps (#2193) 2022-11-17 18:26:41 -08:00
Nolan Lawson 4273666ce5
chore: update emoji-picker-element (#2190) 2022-11-17 18:26:29 -08:00
Nolan Lawson 6f4eb98397
chore: use node 14 in CI, update mocha, fix gitignore (#2191) 2022-11-17 17:09:37 -08:00
Nolan Lawson 3c59069490
chore: attempt to fix flaky test (#2197) 2022-11-17 17:09:24 -08:00
dependabot[bot] 55189e840b
chore(deps): bump loader-utils from 1.4.1 to 1.4.2 (#2196)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  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-11-17 17:09:18 -08:00
Nolan Lawson e2d5b5928d
chore: update formatjs deps (#2194) 2022-11-17 07:44:50 -08:00
Nolan Lawson 6fde4e0b90
chore: update webpack/webpack-bundle-analyzer (#2192) 2022-11-17 07:44:40 -08:00
Nolan Lawson 6ebd6a6a01
fix: fix max number of status characters (#2188)
Fixes #2187
2022-11-17 06:17:49 -08:00
Gavin Mogan 36ead0406d
docs: fix link to social media (#2185) 2022-11-16 07:00:28 -08:00
Nolan Lawson 1de26d4b06 2.2.3 2022-11-13 17:01:41 -08:00
Nolan Lawson f10e9dbcf3
fix(a11y): fix number of headings (#2183)
Fixes #2162
2022-11-13 07:01:12 -08:00
Nolan Lawson 1c6387a0a4
fix: show proper text for signup notifications (#2182) 2022-11-13 07:00:37 -08:00
Nolan Lawson 0fd7154ed4
perf: optimize screenshot PNGs with pngquant (#2181) 2022-11-13 06:59:49 -08:00
Nolan Lawson 7ea387bc4c
docs: clarify self-hosting 2022-11-12 12:28:19 -08:00
Nolan Lawson 68d756ca34 2.2.2 2022-11-12 11:01:25 -08:00
Nolan Lawson 6edb6df17d
chore: update deps (#2177) 2022-11-12 10:17:52 -08:00
Nolan Lawson ed38cad661
test: update mastodon to 3.5.3 (#2175) 2022-11-12 09:47:11 -08:00
Nolan Lawson 19e466aa90
fix(a11y): add aria-live for keyboard shortcuts (#2176)
Fixes #2163
2022-11-12 09:47:02 -08:00
Nolan Lawson f301fc59f6
chore: attempt to fix flaky test (#2178) 2022-11-12 09:46:45 -08:00
Nolan Lawson 8fb4c40275
fix: update link rel=me (#2160)
* fix: update link rel=me

* chore: bump
2022-11-09 06:58:04 -08:00
dependabot[bot] 87cb80bf18
chore(deps): bump loader-utils from 1.4.0 to 1.4.1 (#2170)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  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-11-09 06:57:49 -08:00
dependabot[bot] 46682296d6
chore(deps): bump moment from 2.29.2 to 2.29.4 (#2156)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  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-11-08 05:46:38 -08:00
dependabot[bot] 99d4cb4d8c
chore(deps): bump shell-quote from 1.7.2 to 1.7.3 (#2152)
* chore(deps): bump shell-quote from 1.7.2 to 1.7.3

Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/substack/node-shell-quote/releases)
- [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md)
- [Commits](https://github.com/substack/node-shell-quote/compare/v1.7.2...1.7.3)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-type: indirect
...

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

* chore: test

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-11-08 05:46:29 -08:00
dependabot[bot] 9eb8fc1f1d
chore(deps): bump jpeg-js from 0.4.3 to 0.4.4 (#2151)
* chore(deps): bump jpeg-js from 0.4.3 to 0.4.4

Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

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

* chore: test

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-11-08 05:46:20 -08:00
dependabot[bot] b152359428
chore(deps): bump terser from 5.7.0 to 5.14.2 (#2155)
Bumps [terser](https://github.com/terser/terser) from 5.7.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-22 17:49:18 -07:00
Nolan Lawson a211763d98 2.2.1 2022-05-22 14:07:01 -07:00
Nolan Lawson 69bb849508
fix: fix login to limited federation instance (#2147)
Fixes #2146
2022-05-15 10:10:04 -07:00
Nolan Lawson 5fd8d0ac23
fix: invalid date strings (#2145)
* fix: invalid date strings

Fixes #2113

* fix: polls without expiry date

Fixes #2112
2022-05-14 11:27:32 -07:00
Nolan Lawson 78687479df
fix: hide embeds in spoilered statuses (#2143)
Fixes #2142
2022-05-07 10:04:57 -07:00
tobi b312b3b485
fix: use empty array for ws protocols instead of null (#2140) 2022-05-06 08:03:49 -07:00
Nolan Lawson b2d900f078 2.2.0 2022-05-01 13:17:06 -07:00
Nolan Lawson 9282d7099d docs: update readme [skip ci] 2022-05-01 13:16:10 -07:00
Nolan Lawson 135c51a856
fix: fix votes count on Misskey polls (#2138) 2022-05-01 10:19:57 -07:00
Nolan Lawson 1a7bbe19a2
fix: add rel=me (#2137) 2022-05-01 08:55:02 -07:00
Nolan Lawson c67be9acc2 fix: fix bell notifications, add tests 2022-05-01 08:54:37 -07:00
Alexander Yakovlev 2e9afd711f feat: support account "bell" notifications
Fixes #1961
2022-05-01 08:54:37 -07:00
Nolan Lawson 54a11778da
chore: make local mastodon server work in docker (#2136) 2022-04-30 18:34:58 -07:00
Nolan Lawson 2a53bd3f80
chore: update mastodon 3.5.1 backup files (#2135) 2022-04-30 17:14:31 -07:00
Nolan Lawson 6794514916
chore: update to mastodon v3.5.1 (#2133)
* chore: update to mastodon v3.5.1

* chore: empty commit
2022-04-30 14:38:37 -07:00
Nolan Lawson 8685e4f603
test: fix flaky test (#2134) 2022-04-30 14:20:22 -07:00
Nolan Lawson 58d81a25ad
chore: remove deprecate git.io comments (#2131) 2022-04-30 12:48:05 -07:00
Nolan Lawson 7d13f27d6c
chore: update testcafe (#2132) 2022-04-30 12:47:56 -07:00
Nolan Lawson 30ad0becb5
fix: make the center nav optional (#2128) 2022-04-25 18:36:29 -07:00
Rylan Cates ce03460b86
feat: center navbar for large screen sizes (#2126)
* feat: center navbar for widths >991px

* fix: update src/routes/_components/Nav.html

fixes #403

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-04-23 08:08:55 -07:00
Matthew Connelly 05a3b2d31f
fix(Dockerfile): Use explicit node version (#2125)
The `node:alpine` Docker image appears to have recently been updated to point to Node 17, breaking new builds of Pinafore. This commit explicitly specifies Node 16 until the underlying reason for the build failure on 17+ can be fixed.
2022-04-19 06:38:06 -07:00
Nolan Lawson e04b1da754 2.1.0 2022-04-15 08:01:00 -07:00
Nolan Lawson 3e2fd130e0
fix: make ios status bar default color again (#2123) 2022-04-10 11:25:29 -07:00
dependabot[bot] 49723fa91e
chore(deps): bump moment from 2.29.1 to 2.29.2 (#2119)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-10 10:36:11 -07:00
Nolan Lawson a9119fa53f
fix: use /api/v2/media (#2121)
* fix: use /api/v2/media

Fixes #2078

* fix: fix comment
2022-04-10 10:35:24 -07:00
hellojaccc 10ed291950
feat: fix ios white status bar + add iOS splash screen (#2108)
* Fix iOS statusbar #2

add theme-color mea tag

* change default to black

* Update template.html

* return to 'default'

* Update template.html

* Add splash screen

* Update template.html

* Update template.html

* fix: filter splash files in service worker

* perf: zopfli-optimize splash pngs

* fix: wrong cache

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
2022-04-10 10:34:56 -07:00
Nolan Lawson 00c6aa1843 2.0.6 2022-04-02 10:36:01 -07:00
dependabot[bot] 6e42e9f2b0
chore(deps): bump minimist from 1.2.5 to 1.2.6 (#2118)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-28 07:32:55 -07:00
Nolan Lawson f2d752bfc2
fix: add country flag emoji on windows (#2117)
* fix: add country flag emoji on windows

* fix: missing file

* fix: cache font file on-demand

* fix: attempt to fix

* fix: working

* fix: ordering

* fix: ordering

* fix: ordering

* fix: fixup

* fix: fixup

* fix: add comment

* fix: fix vercel

* fix: fix vercel.json

* fix: vercel

* refactor: refactor
2022-03-27 20:59:02 -07:00
Nolan Lawson fd6bb63450
chore: update emoji-picker-element (#2116) 2022-03-21 08:23:17 -07:00
Nolan Lawson e15f4523ba
chore: update dependencies (#2115) 2022-03-21 08:23:08 -07:00
Nolan Lawson 5ecf8b8ab9
chore: update deps (#2109) 2022-02-18 12:56:05 -08:00
Nolan Lawson 8b8246c59f 2.0.5 2022-02-17 07:45:33 -08:00
Nolan Lawson cac792a830
fix(ios): change status-bar-style to default (#2107)
Fixes #2104
2022-02-06 11:53:29 -08:00
dependabot[bot] dc8b7c93f3
chore(deps): bump node-fetch from 2.6.1 to 2.6.7 (#2106)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-06 11:37:59 -08:00
Nolan Lawson c57e3a2e7e 2.0.4 2022-01-23 09:47:08 -08:00
Nolan Lawson 331f6e8803
fix: fix multiple-choice poll results (#2101)
Fixes #2100
2022-01-02 16:00:41 -08:00
Nolan Lawson 67f4a1ab2f 2.0.3 2021-12-31 18:49:36 -08:00
Nolan Lawson f3c5e7de5f
fix: ignore falsy last_status (#2099)
Fixes #2097
2021-12-27 20:57:16 -08:00
Nolan Lawson 58ff6beb26 2.0.2 2021-11-26 15:11:08 -08:00
Nolan Lawson 0df4b724ca
fix: fix for when notification is undefined (#2093) 2021-11-13 10:57:36 -08:00
dependabot[bot] fdf4110dad
chore(deps): bump nth-check from 2.0.0 to 2.0.1 (#2091)
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-13 10:47:24 -08:00
Nolan Lawson 54b3042042 2.0.1 2021-09-14 07:51:43 -07:00
Nolan Lawson 21678ec78e
fix: tweak accent colors (#2089)
* fix: tweak accent colors

* fix: fixup
2021-08-20 18:08:26 -07:00
Nolan Lawson 368775e220
fix: add accent-color/color-scheme (#2088) 2021-08-18 07:11:14 -07:00
Nolan Lawson 9d5157f15c
fix: increase poll answer max length to 50 (#2086)
Fixes #2077
2021-08-06 15:20:48 -07:00
Nolan Lawson a1e105ccef 2.0.0 2021-08-06 12:00:18 -07:00
Nolan Lawson 344a23fddd
chore: update emoji-picker-element (#2084) 2021-08-06 12:00:11 -07:00
Nolan Lawson 32b1be96a9
chore: use node 12 everywhere (#2081) 2021-07-29 07:25:40 -07:00
Nolan Lawson da4d32c1e7
chore: use volta (#2080) 2021-07-28 21:55:17 -07:00
Nolan Lawson 821b785e6b
fix: update usage of safari-14-idb-fix (#2072)
* chore: update deps

* fix: fix dep path

* fix: fix import

* fix: fix pkg
2021-07-16 07:42:32 -07:00
Nolan Lawson d30f7f4b1a
fix: enable focus-visible in Firefox 90 (#2075) 2021-07-16 07:14:15 -07:00
Nolan Lawson d84f4604ad
chore(package): update deps (#2074)
* chore(package): update deps

* chore: update
2021-07-16 07:14:06 -07:00
Nolan Lawson 374b8b251e
perf: avoid style recalc for spinner in Chrome (#2071) 2021-07-05 10:23:48 -07:00
Nolan Lawson 16e66346d7
fix!: remove esm package, use native Node ES modules (#2064)
BREAKING CHANGE: Node v12.20+, v14.14+, or v16.0+ is required

* fix!: remove esm package, use native Node ES modules

* fix: fix some CJS imports
2021-07-04 20:19:04 -07:00
Nolan Lawson c5de673990
test: improve flaky tests (#2067) 2021-07-04 17:42:43 -07:00
Nolan Lawson b3ab427ac0
fix!: remove performance-now module, use perf_hooks (#2065) 2021-07-04 16:39:48 -07:00
Nolan Lawson f012369d72
chore: do not run Webpack BundleAnalyzerPlugin in Circle CI (#2063) 2021-07-04 16:39:31 -07:00
Nolan Lawson 992c5efd7e
chore: update testcafe (#2062) 2021-07-04 16:39:09 -07:00
Nolan Lawson b31a72f850
fix : update deps, remove unused deps, code cleanup (#2061) 2021-07-04 16:38:58 -07:00
Nolan Lawson 7bc9c3f263
test: fix flaky test (#2060) 2021-07-03 18:07:54 -07:00
Nolan Lawson f13e5be3a0
chore: update emoji picker (#2057) 2021-07-03 13:50:28 -07:00
Nolan Lawson 658a9736e1
chore: update deps (#2058)
* chore: update deps

* chore: downgrade p-any
2021-07-03 13:15:54 -07:00
Nolan Lawson 0c455c35c9
chore: run Circle CI tests in parallel (#2059)
* chore: run Circle CI tests in parallel

* fix: fix schema

* fix: fix schema

* chore: persist and load workspace

* chore: optimize ci steps

* chore: fix

* chore: fix

* chore: fix cache

* chore: fix cache

* chore: fix cache

* chore: fix cache
2021-07-03 13:15:44 -07:00
Nolan Lawson b241ea18ac
chore: update emoji picker (#2056) 2021-07-01 19:35:14 -07:00
Nolan Lawson cbdbb05926 1.24.5 2021-06-25 07:34:12 -07:00
Nolan Lawson c692a1850b
fix: work around missing indexedDB.databases (#2054) 2021-06-20 09:48:44 -07:00
Nolan Lawson e0827be8c8
fix: fix safari 14 idb issue (#2053) 2021-06-19 09:29:32 -07:00
Nolan Lawson a166dccb59
chore: update deps (#2052)
* chore: update deps

* fix: fix cheerio

* fix: update vercel.json
2021-06-06 13:16:47 -07:00
Nolan Lawson 7255221c5c
test: test mastodon v3.4.x (#2051) 2021-06-06 13:16:25 -07:00
Nolan Lawson e2813ae428
chore: update deps (#2049) 2021-06-06 10:25:21 -07:00
dependabot[bot] aa9878d1a9
chore(deps): bump browserslist from 4.16.1 to 4.16.6 (#2050)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.1 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.1...4.16.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-29 12:52:08 -07:00
Nolan Lawson 284d812367
chore: update svgo (#2048) 2021-05-15 13:28:42 -07:00
Nolan Lawson 0861b22c85
chore: update @rollup/plugin-replace (#2047) 2021-05-15 13:28:31 -07:00
Nolan Lawson c4fbf34a27
chore: update deps (#2044)
* chore: update deps

* chore: update deps
2021-05-15 09:49:01 -07:00
Nolan Lawson 8205b6a2a6
chore: update deps (#2043) 2021-05-15 09:00:18 -07:00
Nolan Lawson 9937b6f3cc
chore: update deps (#2042) 2021-05-15 09:00:07 -07:00
Nolan Lawson 75de31f7c7 1.24.4 2021-05-15 08:17:41 -07:00
Nolan Lawson c4e8d772dd
fix: fully disable focus-visible for firefox for now (#2041) 2021-05-14 17:54:22 -07:00
Nolan Lawson 69e3582157
chore: update lodash (#2040) 2021-05-12 07:19:17 -07:00
Nolan Lawson 3971f9a636
fix: switch to native :focus-visible for firefox 88+ (#2039) 2021-05-11 21:40:40 -07:00
dependabot[bot] f9ac31465d
chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#2037)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 07:11:53 -07:00
Nolan Lawson f7ea5d98ad 1.24.3 2021-05-01 10:58:19 -07:00
Nolan Lawson 566cf6cd78
fix: remove Pinafore from FLOC (#2035) 2021-04-17 13:34:46 -07:00
Nolan Lawson 85a5874876
fix: internationalize manifest.json (#2034)
* fix: internationalize manifest.json

fixes #2020

* test: fix test
2021-04-11 19:40:24 -07:00
Nolan Lawson 66fc202b5c
fix: internationalize dialogs (#2033)
* fix: internationalize dialogs

Fixes #1988

* test: fix test

* test: fix test

* Revert "test: fix test"

This reverts commit 559e3d80eb.
2021-04-11 19:40:18 -07:00
Nolan Lawson ad9609738b
fix: fix a11y for audio/video controls in dialog (#2031) 2021-04-11 09:58:32 -07:00
Nolan Lawson 3a91ad75b8
test: fix lint (#2032) 2021-04-11 09:00:10 -07:00
Dylan Staley 11fca7b792
fix: add support for building on Windows (#2029)
Fixes #1919

* update path to sapper cli

* use os-aware module rule tests
2021-04-11 07:43:53 -07:00
Nolan Lawson 7a28bd2d88
fix: use is-emoji-supported library (#2030)
* fix: use is-emoji-supported library

* fix: add code comment
2021-04-11 07:42:30 -07:00
Nolan Lawson bb7ebb04bc 1.24.2 2021-04-03 09:03:47 -07:00
Nolan Lawson c815292b0b
fix: fix aria-labels on relative timestamps (#2028) 2021-04-02 17:01:08 -07:00
Nolan Lawson d0c9be0c09 1.24.1 2021-04-02 15:38:19 -07:00
Nolan Lawson 69ef9f2798
fix: initialize all Intl formatters lazily (#2026)
fixes #2024
2021-04-02 11:02:01 -07:00
Nolan Lawson 3c307a47fc
chore: update tested mastodon to fix mimemagic (#2027) 2021-04-02 10:13:25 -07:00
Nolan Lawson b48db404ad 1.24.0 2021-03-27 10:33:45 -07:00
Nolan Lawson 081df2bf82
fix: use day-only time format for wellness setting (#2021)
* fix: use day-only time format for wellness setting

* fix: tweak intl strings

[skip ci]

* test: add test
2021-03-21 18:03:53 -07:00
Calvin Walton 1aa06bc041
feat: add a Wellness option to show absolute timestamps instead of relative (#2014)
Relative timestamps can cause you to feel that things are especially
interesting because they are happening "right now"; the effect is
lessened if you see absolute timestamps instead.

This fixes #2011
2021-03-21 15:06:45 -07:00
Nolan Lawson d044e12aee
feat: add PWA shortcuts for compose/notifications (#2019)
* feat: add PWA shortcuts for compose/notifications

Fixes #2012

* fix: fix icon path
2021-03-21 13:49:59 -07:00
Nolan Lawson 65733ce68a
chore: fix vercel/git/docker ignore files (#2018) 2021-03-21 10:11:41 -07:00
Nolan Lawson 751ed299f6
fix: remove explicit webpack chunk names (#2016)
* fix: remove explicit webpack chunk names

* fix: fix vercel json
2021-03-21 09:06:08 -07:00
Nolan Lawson 237ac836c0 1.23.0 2021-03-19 18:42:38 -07:00
Nolan Lawson 75458a3410
feat: use web badge API to show notifications/follow requests (#2005)
* feat: use web badge API to show notifications/follow requests

Fixes #1900

* fix: change detection logic

* fix: add UA check

* fix: tweak
2021-03-19 08:00:59 -07:00
Nolan Lawson 66cfc342f0
fix: revert "test: reduce concurrency again (#2007)" (#2008)
This reverts commit eba2b1cd74.
2021-03-19 07:22:47 -07:00
Nolan Lawson eba2b1cd74
test: reduce concurrency again (#2007) 2021-03-18 21:50:02 -07:00
Nolan Lawson 746c341fda
chore: reduce test concurrency (#2006) 2021-03-18 20:50:24 -07:00
Nolan Lawson 3bf744d2c5
fix: add pwa=true query param when sharing files (#2004) 2021-03-18 07:00:48 -07:00
Nolan Lawson fd321720f2
fix: disable :focus-visible on Firefox (#2003)
* fix: disable :focus-visible on Firefox

* fix: use unambiguous closure
2021-03-18 07:00:42 -07:00
Nolan Lawson 40cb793e81
fix: fix word filter style on small screens (#2002) 2021-03-15 22:25:40 -07:00
Nolan Lawson 98815714ba
fix: fix name of webpack chunk for intl polyfill (#2001)
* fix: fix name of webpack chunk for intl polyfill

* fix: fix typo
2021-03-15 19:46:58 -07:00
Nolan Lawson a7fb2e68dd
perf: avoid importing the DB for non-logged-in users (#1998) 2021-03-15 17:25:20 -07:00
Nolan Lawson c3fb1e2038
fix: media cache should be behind async db API (#1999) 2021-03-15 17:25:13 -07:00
Nolan Lawson c4e73106cf
fix: fix tesseract in dev mode (#2000) 2021-03-15 17:25:07 -07:00
Nolan Lawson 02019e9251
perf: use scheduling.isInputPending() (#1996) 2021-03-14 18:05:57 -07:00
Nolan Lawson 193db0aa15
perf: remove quick-login.html (#1994) 2021-03-14 13:39:32 -07:00
Nolan Lawson cf0f1d884a
feat: add screenshots/categories to web app manifest (#1993)
* feat: add screenshots/categories to web app manifest

fixes #1971

* fix: whoops forgot the screenshots
2021-03-14 13:39:11 -07:00
Nolan Lawson 5e7440aaee
feat: accept files in web share target (#1992)
fixes #1009
2021-03-14 10:20:23 -07:00
Nolan Lawson 5e61a8582b
perf: slightly more efficient word filter format (#1991) 2021-03-14 09:24:00 -07:00
Nolan Lawson 4adc8ff748
feat: implement word/phrase filters (#1990)
* feat: implement word filters

* fix: more progress on word filters

* fix: more progress

* fix: more work

* fix: more work

* fix: more progress

* fix: tweaks

* fix: basic crud stuff

* fix: more work

* test: add tests

* test: more test

* fix: handle filter expiry correctly

* fix: implement more efficient word filter logic

* fix: better required labels

* test: fix test
2021-03-13 17:31:17 -08:00
Nolan Lawson 3271344c76
fix: add cross-origin-opener-policy (#1989) 2021-03-09 08:08:19 -08:00
Nolan Lawson 9a5ce33efb
test: add CI test that vercel.json does not change (#1986)
* test: add CI test that vercel.json does not change

* fix: fixup

* test: debug

* test: do not use tmp file
2021-03-07 10:34:13 -08:00
Nolan Lawson a6c9d41f46
fix: fix permissions policy format (#1987) 2021-03-07 08:55:03 -08:00
Nolan Lawson 987e5827b0
fix: fix CSP checksums (#1985) 2021-03-07 08:21:20 -08:00
Nolan Lawson 88ccfdad6e
fix: add permissions policy (#1984)
does not really do anything right now, but fun to experiment with
2021-03-06 15:21:26 -08:00
Nolan Lawson f22b1bf328
perf: reduce tesseract bundle size by directly importing createWorker (#1979) 2021-03-06 09:07:06 -08:00
Nolan Lawson a2dcbcdcda
fix: use class instead of object for easier debugging (#1980) 2021-03-06 09:07:00 -08:00
Nolan Lawson 1f2ce30fd4
fix: fix DEBUG mode for inline script (#1981) 2021-03-06 09:06:53 -08:00
Nolan Lawson 7d96876aca
chore: break up circle CI tasks (#1983) 2021-03-06 09:06:42 -08:00
Nolan Lawson 650751d343
fix: fix dangling } in string (#1976) 2021-02-28 16:36:47 -08:00
Nolan Lawson 8f63cc479c
chore: do not throw intl errors in dev mode (#1977) 2021-02-28 16:36:38 -08:00
Timo Tijhof 5573f7cf32
chore: clean up input.type property access (#1975)
Follows-up 4218c4ce64.

When accessing the IDL property, values tend to be reflected in a normalised
type and form. In the case of HTMLInputElement.type, this means the
returned value is always one of the supported and canonical lowercase
values regardless of what value the corresponding attribute holds, or
even if the attribute doesn't exist (the default will still be "text").
2021-02-28 14:46:23 -08:00
Marco Zehe b9496c9bca
fix: adjust German help text for expand/collapse all CWs (#1974) 2021-02-28 14:45:49 -08:00
Nolan Lawson 8c09ede2d4
feat: implement shortcut for opening/closing all CWs (#1973)
Fixes #1914
2021-02-27 18:31:53 -08:00
Nolan Lawson a21a889f5f
chore: update sapper to actually fix webpack 5 (#1972) 2021-02-27 18:31:09 -08:00
Nolan Lawson 67a338be17
fix: tweak style of audio player (#1968) 2021-02-22 20:37:18 -08:00
Nolan Lawson ef3f107d82
fix: tweak emoji picker style on mobile (#1969)
* fix: tweak emoji picker style on mobile

* fix: remove unnecessary global styles
2021-02-22 20:37:08 -08:00
Nolan Lawson b0c694b1bd
test: fix flakey test (#1970) 2021-02-22 20:36:59 -08:00
Nolan Lawson b2583277eb
chore: update to webpack v5 (#1967)
The bundle size has decreased slightly, so I really can't complain.
2021-02-20 15:30:58 -08:00
Nolan Lawson 2c34527411
chore: update lodash-webpack-plugin (#1966) 2021-02-20 13:40:50 -08:00
Nolan Lawson e507618451
chore: update deps (#1965) 2021-02-20 13:40:45 -08:00
Nolan Lawson 0b3ccdb6a2
chore: update webpack (#1964) 2021-02-20 13:40:41 -08:00
Nolan Lawson 533360e32f
chore: update standard and eslint-plugin-html (#1963) 2021-02-20 13:40:33 -08:00
Nolan Lawson e3d3249a20
chore: update webpack-bundle-analyzer (#1962) 2021-02-20 12:39:25 -08:00
Nolan Lawson ba3b76f769
fix: fix error message on media upload (#1959) 2021-02-15 19:23:01 -08:00
Nolan Lawson c209fb23d5 1.22.0 2021-02-15 16:59:44 -08:00
Nolan Lawson c1e9cab238
chore: update mocha (#1953) 2021-02-15 16:47:42 -08:00
Nolan Lawson 68178ce40e
chore: update emoji-regex (#1952) 2021-02-15 16:47:31 -08:00
Nolan Lawson 9cf8f8b516
perf: cache polyfills on-demand (#1954)
* perf: cache polyfills on-demand

* fix: actually apply the name
2021-02-15 16:47:18 -08:00
Nolan Lawson 2ecd8a39a9
chore: update yarn.lock (#1955) 2021-02-15 16:45:56 -08:00
Nolan Lawson ddd565c708
test: fix flakey notification test (#1958) 2021-02-15 16:45:51 -08:00
Nolan Lawson b451093ece
test: fix flakey test (#1957) 2021-02-15 16:45:46 -08:00
Nolan Lawson 5b04db8442
test: fix flakey test (#1956) 2021-02-15 16:45:41 -08:00
Nolan Lawson 1e974824e1
chore: update cross-env (#1951) 2021-02-15 15:07:35 -08:00
Nolan Lawson cad8201692
chore: update sass (#1949) 2021-02-15 15:07:30 -08:00
Nolan Lawson 63790021a9
chore: update chokidar (#1948) 2021-02-15 15:07:26 -08:00
Nolan Lawson 760b7f6cd4
fix: fix Intl.RelativeTimeFormat on iOS 13 (#1947)
Fixes #1938
2021-02-15 15:07:19 -08:00
Nolan Lawson c3d25b88cf
feat: allow file-drop to accept multiple files (#1945) 2021-02-15 15:07:13 -08:00
Nolan Lawson 6fdbedd594
chore: update husky and lint-staged (#1943) 2021-02-15 15:07:03 -08:00
Nolan Lawson 1afd14b7ac
chore: update testcafe (#1942) 2021-02-15 12:54:13 -08:00
Nolan Lawson 09b5474e22
test: always migrate mastodon server when launching (#1941)
This fixes some bugs in dev mode where the Mastodon DB might say it needs a Rails migration stil.
2021-02-15 12:54:08 -08:00
Nolan Lawson 2585b55479
fix: fix custom emoji in secure mode (#1940)
Fixes #1915
2021-02-15 12:54:03 -08:00
Nolan Lawson 456dac73b5
fix: partially fix video/audio in dialogs (#1939) 2021-02-15 12:53:58 -08:00
Nolan Lawson a3d0c87e27
fix: fix cursor set incorrectly on WebKit browsers (#1937)
fixes #1921
2021-02-14 18:44:14 -08:00
Nolan Lawson c909b0d9d9
chore: disable vercel comment bot (#1936) 2021-02-14 14:02:02 -08:00
Nolan Lawson c833680ecc
chore: update vercel config and docs (#1933) 2021-02-14 14:01:52 -08:00
Nolan Lawson 4218c4ce64
fix: fix up/down arrows in poll options (#1934)
fixes #1928
2021-02-14 14:01:46 -08:00
Nolan Lawson 6d5bb0e39e
fix: increase sapper timeout from 5s to 5min (#1935)
fixes #1924
2021-02-14 14:01:40 -08:00
Nolan Lawson eba90b3122 1.21.0 2021-02-14 11:46:33 -08:00
Nolan Lawson 46a4774a61
chore: move format-message-parse to dependencies (#1931)
fixes #1925, see also #1927
2021-02-14 11:32:04 -08:00
Marco Zehe 96d84134b4
feat: add German locale (#1930)
* Add German locale

* Appeas the linter.

* Add Emoji Picker strings as well

* Correct double und typo in footer
2021-02-14 09:59:12 -08:00
Nolan Lawson fc96d7137d
docs: add better docs on internationalization (#1932)
[skip ci]
2021-02-14 09:58:55 -08:00
Nolan Lawson a3e970fe7a
test: use mastodon 3.3.0 for testing (#1917)
* test: use mastodon 3.3.0 for testing

* test: fix test

* test: fix test

* test: fix test

* test: fix test

* test: revert test change

* test: use ruby 2.6.6
2021-01-24 18:26:40 -08:00
Nolan Lawson eff9d6c52e
chore: update testcafe (#1918) 2021-01-24 09:36:30 -08:00
Nolan Lawson 67feaef844
chore: update emoji-picker-element (#1916) 2021-01-23 15:58:07 -08:00
Nolan Lawson 0afaef350a
perf: remove lookup-closest-locale (#1911) 2020-12-20 15:33:41 -08:00
Nolan Lawson a028a7e880
feat: intl support for emoji picker (#1910)
* feat: intl support for emoji picker

Fixes #1908

* fix: update emoji-picker-element

* fix: fix typo
2020-12-18 20:02:36 -08:00
dependabot[bot] 2de875795b
chore(deps): bump ini from 1.3.5 to 1.3.7 (#1907)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-13 11:33:12 -08:00
Nolan Lawson 6433a9c644
test: fix timeago unit test (#1906) 2020-11-29 18:50:13 -08:00
Nolan Lawson 0022286b46
fix: first stab at i18n, extract English strings, add French (#1904)
* first attempt

* progress

* working

* working

* test timeago

* rm

* get timeago working

* reduce size

* fix whitespace

* more intl stuff

* more effort

* more work

* more progress

* more work

* more intl

* set lang=LOCALE

* flatten

* more work

* add ltr/rtl

* more work

* add comments

* yet more work

* still more work

* more work

* fix tests

* more test and string fixes

* fix test

* fix test

* fix test

* fix some more strings, add test

* fix snackbar

* fix }

* fix typo

* fix english

* measure perf

* start on french

* more work on french

* more french

* more french

* finish french

* fix some missing translations

* update readme

* fix test
2020-11-29 14:13:27 -08:00
Nolan Lawson 583285a09c
chore: update testcafe to 1.9.4 (#1905) 2020-11-27 19:13:55 -08:00
Nolan Lawson 5bf5e1d36e
test: add test for relative timeago date formatting (#1903) 2020-11-25 16:43:12 -08:00
Nolan Lawson 69aad56421
fix: fix tainted canvas error with OCR (#1902)
* fix: fix tainted canvas error with OCR

fixes #1901

* fix: minor tweaks
2020-11-24 15:37:10 -08:00
Nolan Lawson d3ce112f60
chore: fix bundler caching in circleci (#1899) 2020-11-23 18:07:38 -08:00
Nolan Lawson a124ba9dc8
chore: cache bundler correctly in CircleCI (#1898) 2020-11-23 16:29:03 -08:00
Nolan Lawson f2e51bbbfe 1.20.0 2020-11-23 14:47:45 -08:00
Nolan Lawson de2c58be6a
chore: commit vercel.json, update husky (#1896) 2020-11-23 14:43:39 -08:00
Nolan Lawson ac08a53014
chore: switch from travis to circleci (#1895)
* chore: update to circleci

* chore: fixup

* chore: fixup

* chore: remove travis.yml

* chore: wait for redis

* chore: fix postgres

* chore: try to fix db

* chore: debug

* chore: debug

* chore: try another node

* chore: try 2.5

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: fix node

* chore: cleanup

* chore: cleanup

* chore: remove travis stuff

* chore: fix emoji

* chore: fix cache

* chore: update readme
2020-11-23 12:45:01 -08:00
Nolan Lawson 006f0deee8
fix: fix tappable area between toolbar buttons (#1893)
fixes #1884

If "disable entire toot area" is on, then the cursor becomes default between the buttons, and clicking does nothing. If it is off (default), then the cursor is always pointer and clicking between the buttons clicks the whole toot.
2020-11-14 17:22:12 -08:00
Nolan Lawson 01ba161520
chore: update vercel token (#1892) 2020-11-14 15:04:28 -08:00
Nolan Lawson 9cb16ea91c
fix: move "bookmark" button past "report" (#1891)
fixes #1879
2020-11-14 14:13:45 -08:00
Nolan Lawson 870fa0e93c
feat: add "i" as shortcut to open media (#1890)
fixes #1883
2020-11-14 14:13:38 -08:00
Nolan Lawson 742a76b4dd
chore: update tesseract (#1889) 2020-11-14 14:13:31 -08:00
Nolan Lawson 57b75ade1b
chore: update emoji-picker-element to v1.3.0 (#1888) 2020-11-14 14:13:22 -08:00
Timo Tijhof 9acb3faac8
fix: change dark grayscale to use have a brighter action-button color (#1881)
The pressed state, such as used for the "Unfollow" button, was effectively
identical to the background, thus making it invisible.

Brighten it up to fix this. The relative differences are kept the same as
before and match the default theme,, e.g. the brightness ranges from
(darkest) Unfollow > Unfollow-hover > Follow > Follow-hover (brightest)

Fixes https://github.com/nolanlawson/pinafore/issues/1880.
2020-11-14 11:35:55 -08:00
Nolan Lawson f09e1bd975 1.19.2 2020-09-20 19:49:16 -07:00
Nolan Lawson ef3cecae74
chore: update deps of deps (#1876)
* chore: update deps of deps

* chore: pin testcafe for now
2020-09-20 19:37:45 -07:00
Nolan Lawson 04b56f5dc5
fix: remove license files from service worker (#1875) 2020-09-20 14:31:15 -07:00
Nolan Lawson 620dfafd09
chore: update emoji-picker-element (#1871) 2020-09-13 16:23:01 -07:00
Nolan Lawson 37711ee17e
fix: dynamically import focus-visible polyfill for emoji picker (#1870) 2020-09-13 15:34:01 -07:00
Nolan Lawson 7803bdf797
fix: remove LEGACY mode (#1867) 2020-09-13 13:37:54 -07:00
Nolan Lawson 295bd18e05
chore: update deps (#1863) 2020-09-07 14:42:50 -07:00
Nolan Lawson e3b3382c01
chore: update worker-loader (#1864)
* chore: update worker-loader

* fix: fix worker-loader
2020-09-07 14:42:44 -07:00
Nolan Lawson 35818250c0
chore: remove unused css-loader dependency (#1862) 2020-09-06 20:02:03 -07:00
Nolan Lawson f6ba607493
chore: update deps (#1865) 2020-09-06 20:01:55 -07:00
Nolan Lawson f1907a8315
chore: update dev deps (#1866) 2020-09-06 20:01:48 -07:00
Nolan Lawson c683a4b85d
fix: ensure bookmarks page has generated HTML file (#1861)
* fix: ensure bookmarks page has generated HTML file

* fix: only render if not logged in
2020-09-02 18:01:46 -07:00
Nolan Lawson fd7c22345c 1.19.1 2020-09-01 22:45:01 -07:00
Nolan Lawson 7445a16e3c
fix: tweak maskable PWA icons (#1860) 2020-09-01 22:14:45 -07:00
Emilia Michanek 60a146eb40
fix: pinning the bookmarks page (#1859)
Pinning the bookmarks page would accidentally pin the local timeline page due a forgotten `else if`.

Fixes #1858
2020-09-01 21:57:36 -07:00
Nolan Lawson 978afb8753 1.19.0 2020-08-31 17:29:01 -07:00
Nolan Lawson 6adad8e4a9
feat: add maskable PWA icons (#1857)
fixes #1856
2020-08-31 17:07:15 -07:00
Nolan Lawson 07f23c5990
feat: pressing / or s focuses search input (#1855) 2020-08-31 16:06:31 -07:00
Nolan Lawson 430ab4db4c
fix: empty timelines no longer show infinite loading spinner (#1854)
Instead, they now show "Nothing to show." I only fixed this for VirtualList because List should never be non-empty (threads).

Fixes #1763
2020-08-30 18:08:55 -07:00
Nolan Lawson 55b9c8d3b8
fix: use absolute positioning over transform (#1850) 2020-08-29 19:19:24 -07:00
Nolan Lawson 40e9b44adc
fix(VirtualList): fix some TODOs (#1851)
* fix(VirtualList): fix some TODOs

* fix: fix memory leak

* fix: remove dead code
2020-08-29 19:19:16 -07:00
Nolan Lawson 4d1a72bb98
chore: add husky to automatically fix lint issues (#1852) 2020-08-29 19:18:59 -07:00
Nolan Lawson 1466371909
test: count store listeners in memory leak test (#1853) 2020-08-29 19:18:53 -07:00
Nolan Lawson 2f41494a9a
fix: tweak language around bookmarking (#1848)
For consistency with the rest of the UI, say "toot" instead of "status" and specify "toot."
2020-08-27 08:49:36 -07:00
Nolan Lawson 08c021bc56
fix: log an error when images cannot be decoded (#1849) 2020-08-27 08:49:22 -07:00
Nolan Lawson 5a9e5ae8bc fix: tidy up bookmarks, add tests 2020-08-25 23:47:20 -07:00
charlag 2113ab3d46 feat: Implement bookmarks, close #1726 2020-08-25 23:47:20 -07:00
Nolan Lawson 4e8a60ddef
chore: fix vercel deployments (#1846)
* chore: fix vercel deployments

* fix: encode properly
2020-08-25 22:41:39 -07:00
Nolan Lawson c081bf67dc
chore: update testcafe (#1821) 2020-08-25 22:16:25 -07:00
Nolan Lawson 36cf9fd56d
test: add test for accessible radio buttons in /community (#1845) 2020-08-25 22:16:14 -07:00
Nolan Lawson 07deb122f3
chore: update emoji-picker-element, use declarative format (#1840) 2020-08-25 16:46:02 -07:00
Nolan Lawson 2812e4521e
chore: update from now to vercel (#1844) 2020-08-25 16:45:53 -07:00
Nolan Lawson 7de0023d17
fix: add "/" hotkey to help info (#1843) 2020-08-25 16:45:41 -07:00
shine c86d2b5088
feat: add `/` as a navigation shortcut for search (#1838)
`/` is a well-known vi/vim key-binding for search. It is supported by
Firefox for a 'quick find' feature in addition to the main find feature
available with the Ctrl+F key combination. DuckDuckGo also supports the
key to focus the search bar as well.

Signed-off-by: shine <4771718+shinenelson@users.noreply.github.com>
2020-08-25 16:45:32 -07:00
Nolan Lawson cc8c605828
chore: update deps (#1834) 2020-07-20 07:02:05 -07:00
Nolan Lawson d40cd429e0 1.18.1 2020-07-07 07:43:37 -07:00
Nolan Lawson bd09718cf6
fix: improve a11y of autosuggest labels (#1830) 2020-07-06 22:27:04 -07:00
Alex 780db2be22
fix: Updated Dockerfile. Referenced in #1826 (#1828) 2020-07-06 19:52:13 -07:00
Nolan Lawson f1606706c4 1.18.0 2020-07-05 19:07:38 -07:00
Nolan Lawson b8fef16a92
fix: fix mobile size of picker when searching (#1822) 2020-07-05 12:38:05 -07:00
Nolan Lawson 518691b8b2
chore: update all dev deps except testcafe (#1820) 2020-07-05 12:37:53 -07:00
Nolan Lawson 60dc4c4e7b
chore: update tesseract deps (#1819) 2020-07-05 12:37:33 -07:00
Nolan Lawson f6f9b7d294
chore: update rollup deps (#1815) 2020-07-05 12:07:17 -07:00
Nolan Lawson c5c6b6a14b
chore: update deps (#1818) 2020-07-05 10:26:32 -07:00
Nolan Lawson 30819c6c47
chore: update babel deps (#1816) 2020-07-05 10:26:25 -07:00
Nolan Lawson 8c8934a8c6
chore: update webpack deps (#1814) 2020-07-05 10:26:16 -07:00
Nolan Lawson ad066e39b1
chore: remove package-lock.json (#1813) 2020-07-05 10:26:09 -07:00
Nolan Lawson 55ded5c234
fix: fix stacking context in Safari/WebKit (#1812)
fixes #1806
2020-07-04 23:17:55 -07:00
Nolan Lawson f17096a8ac
fix: emoji picker height on mobile (#1811) 2020-07-04 19:34:21 -07:00
Nolan Lawson 44c1b6feb5 fix: fix ajax code, add test, switch parser library 2020-07-04 19:34:01 -07:00
charlag 5e7c8003db fix: Fix favorites, fix #850
This commit fixes invalid assumption that all timelines are sorted by status id.
Some, like favorites or bookmarks are sorted by private server id. To correctly
paginate we must use the Link header.

To work around the issue, offline for favorites was effectively disabled.
Statuses are still inserted into the database but we can't reproduce correct
timeline order.
2020-07-04 19:34:01 -07:00
Nolan Lawson 8bbe372fda
chore: upgrade emoji-picker-element (#1809) 2020-07-01 12:49:44 -07:00
Nolan Lawson eb436de9c3
chore: update emoji-picker-element to v1.0.1 (#1807) 2020-06-30 13:42:03 -07:00
Nolan Lawson 1371175bce
feat: use emoji-picker-element, add emoji autocompletions/tooltips (#1804)
* feat: use emoji-picker-element, add emoji autocompletions/tooltips

* fix: fix lint bug

* test: fix emoji in chrome on linux in travis

* test: try bionic in travis

* chore: try to fix travis

* chore: try to fix travis

* fix: filter unsupported emoji

* chore: try to fix travis

* chore: try to fix travis

* chore: try to fix travis

* chore: try to fix travis

* Revert "chore: try to fix travis"

This reverts commit 3cd2d94469.

* fix: fix emoji autosuggest

* test: fix test
2020-06-28 23:12:14 -07:00
Nolan Lawson 85ce93177b
fix: add apple-mobile-web-app-capable (#1803)
Fixes #1802
2020-06-25 19:09:27 -07:00
Nolan Lawson 949fb5f8a4
docs: improve docs in Admin guide (#1794)
[skip ci]
2020-05-30 12:55:20 -07:00
Nolan Lawson ec8e872f8d
fix: better error message for invalid instances (#1793) 2020-05-30 11:05:13 -07:00
Nolan Lawson 3476b9dc7e
chore: use sass instead of node-sass (#1792)
* chore: use sass instead of node-sass

* perf: renderSync is slightly faster
2020-05-28 21:06:24 -07:00
Nolan Lawson d26470592c 1.17.0 2020-05-23 09:56:59 -07:00
Nolan Lawson ceff1f1f8f
fix: tweak indicator design again (#1789) 2020-05-23 09:28:23 -07:00
Nolan Lawson 1fc14107c8
fix: tweak nav indicator so it's a bit more prominent (#1788) 2020-05-20 21:10:56 -07:00
Nolan Lawson 4fc41affe4
chore: update node-sass (#1787) 2020-05-20 19:39:48 -07:00
Nolan Lawson bedb636182
fix: css cleanup of nav-related variables (#1786)
* fix: css cleanup of nav-related variables

* changed my mind on this margin
2020-05-20 07:07:47 -07:00
Nolan Lawson f080148aad
perf: lazy-lazy-load the :focus-visible polyfill (#1785) 2020-05-19 07:52:28 -07:00
Nolan Lawson a790004be7
fix: Revert "perf: always load focus-visible polyfill (#1780)" (#1784)
This reverts commit c98b198e60.
2020-05-18 22:19:33 -07:00
Nolan Lawson c98b198e60
perf: always load focus-visible polyfill (#1780) 2020-05-18 21:11:13 -07:00
Nolan Lawson ea1315858d
perf: use OffscreenCanvas in Chrome 82+ (#1779) 2020-05-18 20:00:02 -07:00
Nolan Lawson beade4aec3
fix: use attr rather than class for focus-visible polyfill (#1778)
fixes #1777
2020-05-16 14:25:12 -07:00
Nolan Lawson cc62000b21
feat: use :focus-visible, add setting to enable/disable it (#1775)
* feat: use :focus-visible, add setting to enable it

* add the ids back

* css cleanup
2020-05-16 13:36:08 -07:00
Nolan Lawson 836b0e341f
perf: lazy-load the thread context (#1774)
* perf: lazy-load the thread context

fixes #898

* more tests

* test: more tests

* simplify implementation
2020-05-16 13:35:57 -07:00
Nolan Lawson 9e09ba6ca1
test: add another threading test (#1773) 2020-05-15 07:47:42 -07:00
Nolan Lawson 3f7aa7fb00
test: add more tests for thread order (#1772) 2020-05-14 22:39:57 -07:00
Nolan Lawson c610a259d5
fix: ListItem should have proper fade animations (#1771) 2020-05-14 21:22:33 -07:00
Nolan Lawson f5eb5fc50b
test: add tests for thread order (#1770) 2020-05-14 21:22:22 -07:00
Nolan Lawson 1df3b506e9 1.16.0 2020-05-11 19:03:29 -07:00
Nolan Lawson dacd9dcc5b
fix: fix polls with content warnings (#1768)
* fix: fix polls with content warnings

fixes #1766

* fixup
2020-05-10 19:41:55 -07:00
Alex a4820a2792
feat: Docker compose (#1767)
* Added docker-compose.yml file to replace Dockerfile.

* Added instruction in the README.md on starting the docker-compose build.
2020-05-07 19:45:52 -07:00
688 zmienionych plików z 23904 dodań i 11456 usunięć

Wyświetl plik

@ -12,9 +12,9 @@ tests
/src/template.html
/static/*.css
/static/icons.svg
/static/robots.txt
/static/inline-script.js.map
/static/emoji-mart-all.json
/static/emoji-*.json
/static/manifest.json
/src/inline-script/checksum.js
yarn-error.log
/now.json
/vercel.json

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

10
.gitignore vendored
Wyświetl plik

@ -6,11 +6,13 @@
/src/template.html
/static/*.css
/static/icons.svg
/static/robots.txt
/static/inline-script.js.map
/static/emoji-mart-all.json
/static/emoji-*.json
/static/manifest.json
/static/TwemojiCountryFlags.woff2
/src/inline-script/checksum.js
yarn-error.log
/now.json
.now
.now
.vercel
/webpack/*.cjs

1
.husky/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1 @@
_

11
.husky/pre-commit 100755
Wyświetl plik

@ -0,0 +1,11 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
set -e
set -x
PATH="$PATH:./node_modules/.bin"
lint-staged
run-s build-vercel-json
git add vercel.json

Wyświetl plik

@ -1,74 +0,0 @@
language: node_js
node_js:
- "10"
dist: xenial
sudo: false
services:
- redis-server
- postgresql
addons:
chrome: stable
postgresql: "10"
apt:
packages:
- autoconf
- bison
- build-essential
- ffmpeg
- file
- g++
- gcc
- imagemagick
- libffi-dev
- libgdbm-dev
- libgdbm-dev
- libicu-dev
- libidn11-dev
- libncurses5-dev
- libpq-dev
- libprotobuf-dev
- libreadline6-dev
- libssl-dev
- libxml2-dev
- libxslt1-dev
- libyaml-dev
- pkg-config
- postgresql-10
- postgresql-client-10
- postgresql-contrib-10
- protobuf-compiler
- redis-server
- redis-tools
- zlib1g-dev
before_install:
- psql -d template1 -U postgres -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;"
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
- export PATH="$HOME/.yarn/bin:$PATH"
- ./bin/setup-mastodon-in-travis.sh
before_script:
- yarn run lint
after_script:
- rm -f /home/travis/.rvm/gems/ruby-*/bin/posix-spawn-benchmark # file seems generated every time, breaks travis cache
script: travis_retry yarn run $COMMAND
env:
global:
- ESM_DISABLE_CACHE=1 # this cache causes travis to rebuild its cache every single time
- TERSER_DISABLE_CACHE=1 # ditto
- secure: "iR11lD+OAyTJdNoK67esDOrd34HKQboJo0DrVL4xqwoAwQGmNX7posBtcj0TOwVAdHfd5S80xlhWlMebrtHWR9oMtdcBXusWnehZVRB0WE89n8enkJCbAxn5uMpcEpKpDHTcfR/Gbxf2sw15dTy0PrW/ldiZXWf7wybJEBGbrEP7QI8oy3VHzmKpSyjRpN/hnSlgxskVnfIMKPp43D+705Ka7aMJNTWZ5dAdKdIjQWX6j6jlqx6Vl+qIq7td3DTZA9A5ft8HxaWC27F1bbd52PdRY2h8Ii3Ps+n8Q8uZK1KJPp9t3pPk+PmYINu2715ArukRk73kahnFadBQLhytn95FLiyKOLj+ajBNo+o3KIQDs3qRj8gkXpkJpuLAPgwABOEVWuLh9y+oa33IDYVzyESRVtXqbbwgziYVjNQCozP1Nt9+Wyh4YHfdOjEEMdlVlkwlyzPfaLAqVBusEphnaF/vx7itdVvxOMQYNcSRoBrAgciN4ng0GZHi5P85DMXnNV41r+d3JK5JEYZD/gpSja5cPUlpPlkXsKiElG3fEoO2D/Uc8rR9Cu84XiJiJQjP91QuWagfdhgqM4YOndt2YukiGzRzDMjTx1BzSW4S11RQGWzZrv06zmDLXTlnUAMEm1/Exo4L6VfgRvyFmgM0LAT+IceVEHbOKC/Hnd8Y3mo="
- secure: "E9t4zTDdPX9I4XgeC5zJEy+mIM2s0MFPpNfJ/mc5q/JX+gQkHSNkE/32NKgfSce85v33kWWxiPK4qorgX4B+v/MkK6kVQ2HSC7p4XttlQucvSICh/hYSM21WnH7g7DRNj3mDWHWEAwQuquaxVLfGWL60M/svnKG9MOoewos9iDj9AvANm6J09DjRmLuqmDV+VL+cV6ZL/SvUZ5Ervkgs/s3nXHEMse9rKMH/6/KncTGGPRolqyx86XSU6/XtRKX2+bEdiOaIxUYYvjcHJZTKxpSelPpEHjoUfWCM2CG3WyjtYkLF/1Romh8Ft4pnz+iiTzN3eWaT2ralOFvW30oB3cKbDcFb6LDGfXYw7v+XIORc79Ehcb2XlweEymf7fPhbx+7bkCfudRCMLw6OUWXoh66BBjOh2gcQYQ2+3U8KV7YKl/ZQHYb722wE6rN0YvJ6zGriWomDuV1smdyu4teo4lY2oVUUUGflyz2HWxnjVbqWizw4k69TNIcEEQ8j8YgdXMUxNMUOJoCu0c3Lnd8J1BeU/7LX87c54/oCMjEivnsENGIC/EUgAoXDi3L0y7HzHgaDs112p5zjspJsSSON/T4E2uyyb2RpjBY4Ghl43a/RDAlv1gUFtvbanphg3PEGMfG7B2gxk9Z/v5J9pUP/NtsspmT2MvTHZXtH/44XPEU="
matrix:
include:
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
- env: COMMAND=test-unit
- env: COMMAND=deploy-all-travis
allow_failures:
- env: COMMAND=deploy-all-travis
branches:
only:
- master
cache:
yarn: true
bundler: true
directories:
- /home/travis/.rvm/

Wyświetl plik

@ -7,6 +7,7 @@
/static/*.css
/static/icons.svg
/static/inline-script.js.map
/static/emoji-mart-all.json
/static/emoji-*.json
/static/manifest.json
/src/inline-script/checksum.js
yarn-error.log

Wyświetl plik

@ -1,10 +1,16 @@
# Breaking changes
This document contains a list of _breaking changes_ for Pinafore. For a full changelog, see [the GitHub release page](https://github.com/nolanlawson/pinafore/releases).
This document contains a list of _breaking changes_ for Pinafore. For a full changelog, see [GitHub releases](https://github.com/nolanlawson/pinafore/releases).
## 2.0.0
For self-hosters, the new minimum Node.js versions are v12.20+, v14.14+, or v16.0+ [due to native ES Modules](https://github.com/nolanlawson/pinafore/pull/2064).
Please check your Node version using `node --version` and update as necessary.
## 1.0.0
**Breaking change:** This version [switches Pinafore from npm to yarn](https://github.com/nolanlawson/pinafore/pull/927). Those who self-host Pinafore will need to make the following changes:
This version [switches Pinafore from npm to yarn](https://github.com/nolanlawson/pinafore/pull/927). Those who self-host Pinafore will need to make the following changes:
1. [Install yarn](https://yarnpkg.com/en/docs/install) if you haven't already.
2. Instead of `npm install`, run `yarn --pure-lockfile`.

5
CHANGELOG.md 100644
Wyświetl plik

@ -0,0 +1,5 @@
# Changelog
For full release notes, see [GitHub releases](https://github.com/nolanlawson/pinafore/releases).
For breaking changes, see [BREAKING_CHANGES.md](https://github.com/nolanlawson/pinafore/blob/master/BREAKING_CHANGES.md).

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 `run-mastodon.js` to whatever you want
3. If the Ruby version changed, install it and update `setup-mastodon-in.travis.sh`
2. Update the `GIT_TAG` in `mastodon-config.js` to whatever you want
3. If the Ruby version changed (check Mastodon's `.ruby-version`), install it and update `RUBY_VERSION` in `mastodon-config.js` as well as the Ruby version in `.github/workflows`.
4. Run `yarn run-mastodon`
5. Run `yarn backup-mastodon-data` to overwrite the data in `fixtures/`
6. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
@ -138,12 +138,6 @@ updating the `fixtures/` should make that a no-op.
There are also some unit tests that run in Node using Mocha. You can find them in `tests/unit` and
run them using `yarn run test-unit`.
## Legacy build
Pinafore also offers a "legacy" build designed for older browsers. To build this version, use:
LEGACY=1 yarn build
## Debug build
To disable minification in a production build (for debugging purposes), you can run:
@ -159,52 +153,19 @@ The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicl
This is also available locally after `yarn run build` at `.sapper/client/report.html`.
## Codebase overview
## Deploying
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
are some quirks, which are described below. This list of quirks is non-exhaustive.
This section only applies to `dev.pinafore.social` and `pinafore.social`, not if you're hosting your own version of
Pinafore.
### Prebuild process
The site uses [Vercel](https://vercel.com). The `master` branch publishes to `dev.pinafore.social` and the `production`
branch deploys to `pinafore.social`.
The `template.html` is itself templated. The "template template" has some inline scripts, CSS, and SVGs
injected into it during the build process. SCSS is used for global CSS and themed CSS, but inside of the
components themselves, it's just vanilla CSS because I couldn't figure out how to get Svelte to run a SCSS
preprocessor.
## Architecture
### Lots of small files
See [Architecture.md](https://github.com/nolanlawson/pinafore/blob/master/docs/Architecture.md).
Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and
code-splitting, as well as avoiding circular dependencies.
## Internationalization
### Preact is loaded dynamically
See [Internationalization.md](https://github.com/nolanlawson/pinafore/blob/master/docs/Internationalization.md).
This is a Svelte project, but `emoji-mart` is used for the emoji picker, and it's written in React. So we
lazy-load the React-compatible Preact library when we load `emoji-mart`.
### Some third-party code is bundled
For various reasons, `a11y-dialog`, `autosize`, and `timeago` are forked and bundled into the source code.
This was either because something needed to be tweaked or fixed, or I was trimming unused code and didn't
see much value in contributing it back, because it was too Pinafore-specific.
### Every Sapper page is "duplicated"
To get a nice animation on the nav bar when you switch columns, every page is lazy-loaded as `LazyPage.html`.
This "lazy page" is merely delayed a few frames to let the animation run. Therefore there is a duplication
between `src/routes` and `src/routes/_pages`. The "lazy page" is in the former, and the actual page is in the
latter. One imports the other.
### There are multiple stores
Originally I conceived of separating out the virtual list into a separate npm package, so I gave it its
own Svelte store (`virtualListStore.js`). This never happened, but it still has its own store. This is useful
anyway, because each store has its state maintained in an LRU cache that allows us to keep the scroll position
in the virtual list e.g. when the user hits the back button.
Also, the main `store.js` store is explicitly
loaded by every component that uses it. So there's no `store` inheritance; every component just declares
whatever store it uses. The main `store.js` is the primary one.
### There is a global event bus
It's in `eventBus.js`. This is useful for some stuff that is hard to do with standard Svelte or DOM events.

Wyświetl plik

@ -1,23 +1,16 @@
# Using Alpine to keep the images smaller
FROM alpine:latest
# Change to using the official NodeJS Alpine container
FROM node:16-alpine
# Pushing all files into image
WORKDIR /app
ADD . /app
COPY . /app
# Install updates and NodeJS+Dependencies
RUN apk add --update --no-cache --virtual build-dependencies git python build-base clang \
# Install updates and NodeJS+Dependencies
&& apk add --update --no-cache nodejs npm \
# Install yarn
&& npm i yarn -g \
# Install Pinafore
&& yarn --production --pure-lockfile \
&& yarn build \
&& yarn cache clean \
&& rm -rf ./src \
# Cleanup
&& apk del build-dependencies
RUN yarn --production --pure-lockfile && \
yarn build && \
yarn cache clean && \
rm -rf ./src ./docs ./tests ./bin
# Expose port 4002
EXPOSE 4002

Wyświetl plik

@ -1,4 +1,6 @@
# Pinafore [![Build Status](https://travis-ci.com/nolanlawson/pinafore.svg?branch=master)](https://travis-ci.com/nolanlawson/pinafore)
# Pinafore [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/)
_**Note:** Pinafore is unmaintained. Read [this](https://nolanlawson.com/2023/01/09/retiring-pinafore/). Original documentation follows._
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
@ -6,7 +8,7 @@ Pinafore is available at [pinafore.social](https://pinafore.social). Beta releas
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) if Pinafore cannot connect to your instance.
For updates and support, follow [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
For updates and support, follow [@pinafore@fosstodon.org](https://fosstodon.org/@pinafore).
## Browser support
@ -31,12 +33,12 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
- Progressive Web App features
- Multi-instance support
- Support latest versions of Chrome, Edge, Firefox, and Safari
- Support non-Mastodon instances (e.g. Pleroma) as well as possible
- Internationalization
### Secondary / possible future goals
- Support for Pleroma or other non-Mastodon backends
- Serve as an alternative frontend tied to a particular instance
- Support for non-English languages (i18n)
- Offline search
### Non-goals
@ -52,7 +54,7 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
## Building
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
Pinafore requires [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com).
To build Pinafore for production, first install dependencies:
@ -75,6 +77,14 @@ To build a Docker image for production:
Now Pinafore is running at `localhost:4002`.
### docker-compose
Alternatively, use docker-compose to build and serve the image for production:
docker-compose up --build -d
The image will build and start, then detach from the terminal running at `localhost:4002`.
### Updating
To keep your version of Pinafore up to date, you can use `git` to check out the latest tag:
@ -83,19 +93,20 @@ To keep your version of Pinafore up to date, you can use `git` to check out the
### Exporting
Pinafore is a static site. When you run `yarn build`, static files will be
Pinafore is a static site. When you run `yarn build`, static files will be
written to `__sapper__/export`.
In theory you could host these static files yourself (e.g. using nginx or Apache), but
it's not recommended, because:
It is _not_ recommended to directly expose these files when self-hosting. Instead, you should use `node server.js` (e.g. with an
nginx or Apache proxy in front). This adds several things you don't get from the raw static files:
- You'd have to set the [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers yourself,
which are an important security feature.
- Some routes are dynamic and need to be routed to the correct static file.
- [CSP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (important for security)
- Certain dynamic routes (less important because of Service Worker managing routing, but certain things could break if Service Workers are disabled in the user's browser)
Having an [nginx config generator](https://github.com/nolanlawson/pinafore/issues/1878) is currently an open issue.
## Developing and testing
See [CONTRIBUTING.md](https://github.com/nolanlawson/pinafore/blob/master/CONTRIBUTING.md) for
See [CONTRIBUTING.md](https://github.com/nolanlawson/pinafore/blob/master/CONTRIBUTING.md) for
how to run Pinafore in dev mode and run tests.
## Changelog

Wyświetl plik

@ -1,29 +1,83 @@
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import CleanCSS from 'clean-css'
import { LOCALE } from '../src/routes/_static/intl.js'
import { getIntl, trimWhitespace } from './getIntl.js'
const writeFile = promisify(fs.writeFile)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const copyFile = promisify(fs.copyFile)
async function compileThirdPartyCss () {
let css = await readFile(path.resolve(__dirname, '../node_modules/emoji-mart/css/emoji-mart.css'), 'utf8')
css = '/* compiled from emoji-mart.css */' + new CleanCSS().minify(css).styles
await writeFile(path.resolve(__dirname, '../static/emoji-mart.css'), css, 'utf8')
// Try 'en-US' first, then 'en' if that doesn't exist
const PREFERRED_LOCALES = [LOCALE, LOCALE.split('-')[0]]
// emojibase seems like the most "neutral" shortcodes, but cldr is available in every language
const PREFERRED_SHORTCODES = ['emojibase', 'cldr']
async function getEmojiI18nFile (locale, shortcode) {
const filename = path.resolve(__dirname,
'../node_modules/emoji-picker-element-data',
locale,
shortcode,
'data.json')
try {
return JSON.parse(await readFile(filename, 'utf8'))
} catch (err) { /* ignore */ }
}
async function compileThirdPartyJson () {
await copyFile(
path.resolve(__dirname, '../node_modules/emoji-mart/data/all.json'),
path.resolve(__dirname, '../static/emoji-mart-all.json')
async function getFirstExistingEmojiI18nFile () {
for (const locale of PREFERRED_LOCALES) {
for (const shortcode of PREFERRED_SHORTCODES) {
const json = await getEmojiI18nFile(locale, shortcode)
if (json) {
return json
}
}
}
}
async function buildEmojiI18nFile () {
const json = await getFirstExistingEmojiI18nFile()
if (!json) {
throw new Error(`Couldn't find i18n data for locale ${LOCALE}. Is it supported in emoji-picker-element-data?`)
}
await writeFile(
path.resolve(__dirname, `../static/emoji-${LOCALE}.json`),
JSON.stringify(json),
'utf8'
)
}
async function buildManifestJson () {
const template = await readFile(path.resolve(__dirname, '../src/build/manifest.json'), 'utf8')
// replace {@intl.foo}
const output = template
.replace(/{intl\.([^}]+)}/g, (match, p1) => trimWhitespace(getIntl(p1)))
await writeFile(
path.resolve(__dirname, '../static/manifest.json'),
JSON.stringify(JSON.parse(output)), // minify json
'utf8'
)
}
async function buildFlagEmojiFile () {
await copyFile(path.resolve(
__dirname,
'../node_modules/country-flag-emoji-polyfill/dist/TwemojiCountryFlags.woff2'
), path.resolve(
__dirname, '../static/TwemojiCountryFlags.woff2'
))
}
async function main () {
await Promise.all([
compileThirdPartyCss(),
compileThirdPartyJson()
buildEmojiI18nFile(),
buildManifestJson(),
buildFlagEmojiFile()
])
}

Wyświetl plik

@ -5,14 +5,12 @@ import path from 'path'
import { rollup } from 'rollup'
import { terser } from 'rollup-plugin-terser'
import replace from '@rollup/plugin-replace'
import fromPairs from 'lodash-es/fromPairs'
import babel from 'rollup-plugin-babel'
import { themes } from '../src/routes/_static/themes'
import terserOptions from './terserOptions'
import { themes } from '../src/routes/_static/themes.js'
import terserOptions from './terserOptions.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
const themeColors = Object.fromEntries(themes.map(_ => ([_.name, _.color])))
export async function buildInlineScript () {
const inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
@ -21,15 +19,15 @@ export async function buildInlineScript () {
input: inlineScriptPath,
plugins: [
replace({
'process.browser': true,
'process.env.LEGACY': JSON.stringify(process.env.LEGACY),
'process.env.THEME_COLORS': JSON.stringify(themeColors)
values: {
'process.browser': true,
'process.env.THEME_COLORS': JSON.stringify(themeColors)
},
preventAssignment: true
}),
process.env.LEGACY && babel({
runtimeHelpers: true,
presets: ['@babel/preset-env']
}),
!process.env.DEBUG && terser(terserOptions)
// TODO: can't disable terser at all, it causes the CSP checksum to stop working
// because the HTML gets minified as some point so the checksums don't match.
terser({ ...terserOptions, mangle: !process.env.DEBUG })
]
})
const { output } = await bundle.generate({
@ -40,10 +38,10 @@ export async function buildInlineScript () {
const { code, map } = output[0]
const fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
const checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
const checksum = crypto.createHash('sha256').update(fullCode, 'utf8').digest('base64')
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
`export default ${JSON.stringify(checksum)}`, 'utf8')
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
map.toString(), 'utf8')

Wyświetl plik

@ -1,13 +1,14 @@
import sass from 'node-sass'
import sass from 'sass'
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import cssDedoupe from 'css-dedoupe'
import { TextDecoder } from 'text-encoding'
import textEncodingPackage from 'text-encoding'
const { TextDecoder } = textEncodingPackage
const writeFile = promisify(fs.writeFile)
const readdir = promisify(fs.readdir)
const render = promisify(sass.render.bind(sass))
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const globalScss = path.join(__dirname, '../src/scss/global.scss')
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
@ -16,7 +17,7 @@ const themesScssDir = path.join(__dirname, '../src/scss/themes')
const assetsDir = path.join(__dirname, '../static')
async function renderCss (file) {
return (await render({ file, outputStyle: 'compressed' })).css
return sass.renderSync({ file, outputStyle: 'compressed' }).css
}
async function compileGlobalSass () {

Wyświetl plik

@ -1,23 +1,26 @@
import svgs from './svgs'
import svgs from './svgs.js'
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import SVGO from 'svgo'
import $ from 'cheerio'
import { optimize } from 'svgo'
import cheerioPackage from 'cheerio'
const svgo = new SVGO()
const { default: $ } = cheerioPackage
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
async function readSvg (svg) {
const filepath = path.join(__dirname, '../', svg.src)
const content = await readFile(filepath, 'utf8')
const optimized = (await svgo.optimize(content))
const optimized = (await optimize(content, { multipass: true }))
const $optimized = $(optimized.data)
const $path = $optimized.find('path, circle').removeAttr('fill')
const viewBox = $optimized.attr('viewBox') || `0 0 ${$optimized.attr('width')} ${$optimized.attr('height')}`
const $symbol = $('<symbol></symbol>')
.attr('id', svg.id)
.attr('viewBox', $optimized.attr('viewBox'))
.attr('viewBox', viewBox)
.append($path)
return $.xml($symbol)
}

Wyświetl plik

@ -2,14 +2,20 @@ import chokidar from 'chokidar'
import fs from 'fs'
import path from 'path'
import { promisify } from 'util'
import { buildSass } from './build-sass'
import { buildInlineScript } from './build-inline-script'
import { buildSvg } from './build-svg'
import now from 'performance-now'
import debounce from 'lodash-es/debounce'
import { buildSass } from './build-sass.js'
import { buildInlineScript } from './build-inline-script.js'
import { buildSvg } from './build-svg.js'
import { performance } from 'perf_hooks'
import { debounce } from '../src/routes/_thirdparty/lodash/timers.js'
import applyIntl from '../webpack/svelte-intl-loader.js'
import { LOCALE } from '../src/routes/_static/intl.js'
import rtlDetectPackage from 'rtl-detect'
const { getLangDir } = rtlDetectPackage
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)
const LOCALE_DIRECTION = getLangDir(LOCALE)
const DEBOUNCE = 500
const builders = [
@ -77,8 +83,8 @@ function doWatch () {
}
async function buildAll () {
const start = now()
const html = (await Promise.all(partials.map(async partial => {
const start = performance.now()
let html = (await Promise.all(partials.map(async partial => {
if (typeof partial === 'string') {
return partial
}
@ -88,8 +94,11 @@ async function buildAll () {
return partial.result
}))).join('')
html = applyIntl(html)
.replace('{process.env.LOCALE}', LOCALE)
.replace('{process.env.LOCALE_DIRECTION}', LOCALE_DIRECTION)
await writeFile(path.resolve(__dirname, '../src/template.html'), html, 'utf8')
const end = now()
const end = performance.now()
console.log(`Built template.html in ${(end - start).toFixed(2)}ms`)
}

Wyświetl plik

@ -1,15 +1,16 @@
// create the now.json file
// create the vercel.json file
// Unfortunately this has to be re-run periodically, as AFAICT there is no way to
// give Zeit a script and tell them to run that, instead of using a static now.json file.
// give Zeit a script and tell them to run that, instead of using a static vercel.json file.
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import { routes } from '../__sapper__/service-worker'
import cloneDeep from 'lodash-es/cloneDeep'
import inlineScriptChecksum from '../src/inline-script/checksum'
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums'
import { routes } from '../__sapper__/service-worker.js'
import { cloneDeep } from '../src/routes/_utils/lodash-lite.js'
import inlineScriptChecksum from '../src/inline-script/checksum.js'
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)
const JSON_TEMPLATE = {
@ -17,15 +18,11 @@ const JSON_TEMPLATE = {
env: {
NODE_ENV: 'production'
},
builds: [
{
src: 'package.json',
use: '@now/static-build',
config: {
distDir: '__sapper__/export'
}
}
],
github: {
silent: true
},
buildCommand: 'yarn build',
outputDirectory: '__sapper__/export',
routes: [
{
src: '^/service-worker\\.js$',
@ -41,13 +38,19 @@ const JSON_TEMPLATE = {
dest: 'client/$1'
},
{
src: '^/client/.*\\.(js|css|map|LICENSE)$',
src: '^/client/.*\\.(js|css|map|LICENSE|txt)$',
headers: {
'cache-control': 'public,max-age=31536000,immutable'
}
},
{
src: '^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp)$',
src: '^/.*\\.(png|jpe?g)$',
headers: {
'cache-control': 'public,max-age=31536000,immutable'
}
},
{
src: '^/.*\\.(css|json|svg|map|txt|gz|webapp|woff|woff2)$',
headers: {
'cache-control': 'public,max-age=3600'
}
@ -60,6 +63,8 @@ const SCRIPT_CHECKSUMS = [inlineScriptChecksum]
.map(_ => `'sha256-${_}'`)
.join(' ')
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=(),interest-cohort=()'
const HTML_HEADERS = {
'cache-control': 'public,max-age=3600',
'content-security-policy': [
@ -74,15 +79,17 @@ const HTML_HEADERS = {
"frame-ancestors 'none'",
"object-src 'none'",
"manifest-src 'self'",
"form-action 'none'",
"form-action 'self'", // we need form-action for the Web Share Target API
"base-uri 'self'"
].join(';'),
'referrer-policy': 'no-referrer',
'strict-transport-security': 'max-age=15552000; includeSubDomains',
'permissions-policy': PERMISSIONS_POLICY,
'x-content-type-options': 'nosniff',
'x-download-options': 'noopen',
'x-frame-options': 'DENY',
'x-xss-protection': '1; mode=block'
'x-xss-protection': '1; mode=block',
'cross-origin-opener-policy': 'same-origin'
}
async function main () {
@ -119,7 +126,7 @@ async function main () {
headers: cloneDeep(HTML_HEADERS)
})
await writeFile(path.resolve(__dirname, '../now.json'), JSON.stringify(json, null, ' '), 'utf8')
await writeFile(path.resolve(__dirname, '../vercel.json'), JSON.stringify(json, null, ' '), 'utf8')
}
main().catch(err => {

Wyświetl plik

@ -0,0 +1,32 @@
import { promisify } from 'util'
import childProcessPromise from 'child-process-promise'
import path from 'path'
import fs from 'fs'
import { envFile, GIT_TAG, GIT_URL, RUBY_VERSION } from './mastodon-config.js'
import esMain from 'es-main'
const exec = childProcessPromise.exec
const stat = promisify(fs.stat)
const writeFile = promisify(fs.writeFile)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const dir = __dirname
const mastodonDir = path.join(dir, '../mastodon')
export default async function cloneMastodon () {
try {
await stat(mastodonDir)
} catch (e) {
console.log('Cloning mastodon...')
await exec(`git clone --single-branch --branch ${GIT_TAG} ${GIT_URL} "${mastodonDir}"`)
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
await writeFile(path.join(dir, '../mastodon/.ruby-version'), RUBY_VERSION, 'utf8')
}
}
if (esMain(import.meta)) {
cloneMastodon().catch(err => {
console.error(err)
process.exit(1)
})
}

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

@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -e
set -x
if [ "$TRAVIS_BRANCH" = master -a "$TRAVIS_PULL_REQUEST" = false ]; then
yarn run deploy-dev
fi

Wyświetl plik

@ -1,41 +0,0 @@
#!/usr/bin/env bash
set -e
set -x
PATH="$PATH:./node_modules/.bin"
# need to build to update now.json
yarn run build
# set up robots.txt
if [[ "$DEPLOY_TYPE" == "dev" ]]; then
printf 'User-agent: *\nDisallow: /' > static/robots.txt
else
rm -f static/robots.txt
fi
# if in travis, use the $NOW_TOKEN
NOW_COMMAND="now --scope nolanlawson"
if [[ ! -z "$NOW_TOKEN" ]]; then
NOW_COMMAND="$NOW_COMMAND --token $NOW_TOKEN"
fi
# launch
URL=$($NOW_COMMAND --confirm -e SAPPER_TIMESTAMP=$(date +%s%3N))
# fixes issues with now being unavailable immediately
sleep 60
# choose the right alias
NOW_ALIAS="dev.pinafore.social"
if [[ "$DEPLOY_TYPE" == "prod" ]]; then
NOW_ALIAS="pinafore.social"
fi
# alias
$NOW_COMMAND alias "$URL" "$NOW_ALIAS"
# cleanup
$NOW_COMMAND rm pinafore --safe --yes

39
bin/getIntl.js 100644
Wyświetl plik

@ -0,0 +1,39 @@
import { get } from '../src/routes/_utils/lodash-lite.js'
import { DEFAULT_LOCALE, LOCALE } from '../src/routes/_static/intl.js'
import enUS from '../src/intl/en-US.js'
import fr from '../src/intl/fr.js'
import de from '../src/intl/de.js'
import es from '../src/intl/es.js'
// TODO: make it so we don't have to explicitly list these out
const locales = {
'en-US': enUS,
fr,
de,
es
}
const intl = locales[LOCALE]
const defaultIntl = locales[DEFAULT_LOCALE]
export function warningOrError (message) { // avoid crashing the whole server on `yarn dev`
if (process.env.NODE_ENV === 'production') {
throw new Error(message)
}
console.warn(message)
return '(Placeholder intl string)'
}
export function getIntl (path) {
path = path.split('.')
const res = get(intl, path, get(defaultIntl, path))
if (typeof res !== 'string') {
return warningOrError('Unknown intl string: ' + JSON.stringify(path))
}
return res
}
export function trimWhitespace (str) {
return str.trim().replace(/\s+/g, ' ')
}

Wyświetl plik

@ -0,0 +1,79 @@
import { promisify } from 'util'
import childProcessPromise from 'child-process-promise'
import path from 'path'
import fs from 'fs'
import { DB_NAME, DB_PASS, DB_USER, mastodonDir, env } from './mastodon-config.js'
import mkdirp from 'mkdirp'
import esMain from 'es-main'
const exec = childProcessPromise.exec
const stat = promisify(fs.stat)
const writeFile = promisify(fs.writeFile)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const dir = __dirname
async function setupMastodonDatabase () {
console.log('Setting up mastodon database...')
try {
await exec(`psql -d template1 -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;"`)
} catch (e) { /* ignore */ }
try {
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
} catch (e) { /* ignore */ }
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
const dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
const tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
const systemDir = path.join(mastodonDir, 'public/system')
await mkdirp(systemDir)
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
}
async function installMastodonDependencies () {
const cwd = mastodonDir
const installCommands = [
'gem install bundler -v 2.3.26 --no-document',
'gem install foreman -v 0.87.2 --no-document',
'bundle config set --local frozen \'true\'',
'bundle install',
'yarn --pure-lockfile'
]
const installedFile = path.join(mastodonDir, 'installed.txt')
try {
await stat(installedFile)
console.log('Already installed Mastodon dependencies')
} catch (e) {
console.log('Installing Mastodon dependencies...')
for (const cmd of installCommands) {
console.log(cmd)
await exec(cmd, { cwd, env })
}
await writeFile(installedFile, '', 'utf8')
}
await exec('bundle exec rails db:migrate', { cwd, env })
}
export default async function installMastodon () {
console.log('Installing Mastodon...')
await setupMastodonDatabase()
await installMastodonDependencies()
}
if (esMain(import.meta)) {
installMastodon().catch(err => {
console.error(err)
process.exit(1)
})
}

Wyświetl plik

@ -0,0 +1,38 @@
import path from 'path'
export const DB_NAME = 'pinafore_development'
export const DB_USER = 'pinafore'
export const DB_PASS = 'pinafore'
export const DB_PORT = process.env.PGPORT || 5432
export const DB_HOST = '127.0.0.1'
export const envFile = `
PAPERCLIP_SECRET=foo
SECRET_KEY_BASE=bar
OTP_SECRET=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT}
DB_USER=${DB_USER}
DB_NAME=${DB_NAME}
DB_PASS=${DB_PASS}
BIND=0.0.0.0
`
export const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
export const GIT_TAG = 'v4.0.2'
export const RUBY_VERSION = '3.0.4'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
export const mastodonDir = path.join(__dirname, '../mastodon')
export const env = Object.assign({}, process.env, {
RAILS_ENV: 'development',
NODE_ENV: 'development',
BUNDLE_PATH: path.join(mastodonDir, 'vendor/bundle'),
DB_NAME,
DB_USER,
DB_PASS,
DB_HOST,
DB_PORT
})

Wyświetl plik

@ -1,4 +1,4 @@
import times from 'lodash-es/times'
import { times } from '../src/routes/_utils/lodash-lite.js'
function unrollThread (user, prefix, privacy, thread) {
const res = []
@ -9,11 +9,11 @@ function unrollThread (user, prefix, privacy, thread) {
}
for (const key of Object.keys(node)) {
res.push({
user: user,
user,
post: {
internalId: prefix + key,
text: key,
privacy: privacy,
privacy,
inReplyTo: parentKey && (prefix + parentKey)
}
})

Wyświetl plik

@ -1,13 +1,13 @@
import { actions } from './mastodon-data'
import { users } from '../tests/users'
import { postStatus } from '../src/routes/_api/statuses'
import { followAccount } from '../src/routes/_api/follow'
import { favoriteStatus } from '../src/routes/_api/favorite'
import { reblogStatus } from '../src/routes/_api/reblog'
import { actions } from './mastodon-data.js'
import { users } from '../tests/users.js'
import { postStatus } from '../src/routes/_api/statuses.js'
import { followAccount } from '../src/routes/_api/follow.js'
import { favoriteStatus } from '../src/routes/_api/favorite.js'
import { reblogStatus } from '../src/routes/_api/reblog.js'
import fetch from 'node-fetch'
import FileApi from 'file-api'
import { pinStatus } from '../src/routes/_api/pin'
import { submitMedia } from '../tests/submitMedia'
import { pinStatus } from '../src/routes/_api/pin.js'
import { submitMedia } from '../tests/submitMedia.js'
global.File = FileApi.File
global.FormData = FileApi.FormData
@ -17,10 +17,10 @@ export async function restoreMastodonData () {
console.log('Restoring mastodon data...')
const internalIdsToIds = {}
for (const action of actions) {
if (!action.post) {
// If the action is a boost, favorite, etc., then it needs to
if (!action.post || /@/.test(action.post.text)) {
// If the action is a boost, favorite, mention, etc., then it needs to
// be delayed, otherwise it may appear in an unpredictable order and break the tests.
await new Promise(resolve => setTimeout(resolve, 1000))
await new Promise(resolve => setTimeout(resolve, 1500))
}
console.log(JSON.stringify(action))
const accessToken = users[action.user].accessToken

Wyświetl plik

@ -1,116 +1,21 @@
import { restoreMastodonData } from './restore-mastodon-data'
import { promisify } from 'util'
import { restoreMastodonData } from './restore-mastodon-data.js'
import childProcessPromise from 'child-process-promise'
import path from 'path'
import fs from 'fs'
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start'
import mkdirp from 'mkdirp'
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start.js'
import cloneMastodon from './clone-mastodon.js'
import installMastodon from './install-mastodon.js'
import { mastodonDir, env } from './mastodon-config.js'
const exec = childProcessPromise.exec
const spawn = childProcessPromise.spawn
const stat = promisify(fs.stat)
const writeFile = promisify(fs.writeFile)
const dir = __dirname
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
const GIT_TAG_OR_COMMIT = 'v3.1.3'
const GIT_BRANCH = 'master'
const DB_NAME = 'pinafore_development'
const DB_USER = 'pinafore'
const DB_PASS = 'pinafore'
const DB_PORT = process.env.PGPORT || 5432
const DB_HOST = '127.0.0.1'
const envFile = `
PAPERCLIP_SECRET=foo
SECRET_KEY_BASE=bar
OTP_SECRET=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT}
DB_USER=${DB_USER}
DB_NAME=${DB_NAME}
DB_PASS=${DB_PASS}
`
const mastodonDir = path.join(dir, '../mastodon')
let childProc
async function cloneMastodon () {
try {
await stat(mastodonDir)
} catch (e) {
console.log('Cloning mastodon...')
await exec(`git clone --single-branch --branch ${GIT_BRANCH} ${GIT_URL} "${mastodonDir}"`)
await exec('git fetch origin --tags', { cwd: mastodonDir }) // may already be cloned, e.g. in CI
await exec(`git checkout ${GIT_TAG_OR_COMMIT}`, { cwd: mastodonDir })
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
}
}
async function setupMastodonDatabase () {
console.log('Setting up mastodon database...')
try {
await exec(`psql -d template1 -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;"`)
} catch (e) { /* ignore */ }
try {
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
} catch (e) { /* ignore */ }
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
const dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
cwd: mastodonDir,
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
})
const tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
const systemDir = path.join(mastodonDir, 'public/system')
await mkdirp(systemDir)
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
}
async function runMastodon () {
console.log('Running mastodon...')
const env = Object.assign({}, process.env, {
RAILS_ENV: 'development',
NODE_ENV: 'development',
DB_NAME,
DB_USER,
DB_PASS,
DB_HOST,
DB_PORT
})
const cwd = mastodonDir
const cmds = [
'gem install bundler foreman',
'bundle install',
'bundle exec rails db:migrate',
'yarn --pure-lockfile'
]
const installedFile = path.join(mastodonDir, 'installed.txt')
try {
await stat(installedFile)
console.log('Already installed Mastodon')
} catch (e) {
console.log('Installing Mastodon...')
for (const cmd of cmds) {
console.log(cmd)
await exec(cmd, { cwd, env })
}
await writeFile(installedFile, '', 'utf8')
}
const promise = spawn('foreman', ['start'], { cwd, env })
// don't bother writing to mastodon.log in Travis; we can't read the file anyway
const logFile = process.env.TRAVIS === 'true' ? '/dev/null' : 'mastodon.log'
// don't bother writing to mastodon.log in CI; we can't read the file anyway
const logFile = process.env.CI ? '/dev/null' : 'mastodon.log'
const log = fs.createWriteStream(logFile, { flags: 'a' })
childProc = promise.childProcess
childProc.stdout.pipe(log)
@ -125,7 +30,7 @@ async function runMastodon () {
async function main () {
await cloneMastodon()
await setupMastodonDatabase()
await installMastodon()
await runMastodon()
await waitForMastodonApiToStart()
await restoreMastodonData()

Wyświetl plik

@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -e
if [[ "$COMMAND" = deploy-all-travis || "$COMMAND" = test-unit ]]; then
exit 0 # no need to setup mastodon in this case
fi
# install ruby
source "$HOME/.rvm/scripts/rvm"
rvm install 2.6.6
rvm use 2.6.6
# check versions
ruby --version
node --version
yarn --version
postgres --version
redis-server --version
ffmpeg -version

Wyświetl plik

@ -1,6 +1,8 @@
module.exports = [
export default [
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
{ id: 'fa-arrow-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/arrow-left.svg' },
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
{ id: 'fa-bell-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell-o.svg' },
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg', inline: true },
@ -22,6 +24,7 @@ module.exports = [
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg', inline: true },
{ id: 'fa-comment', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comment.svg' },
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg', inline: true },
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
@ -55,5 +58,6 @@ module.exports = [
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' },
{ id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' },
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' }
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' },
{ id: 'fa-bookmark', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg' }
]

Wyświetl plik

@ -1,4 +1,4 @@
module.exports = {
export default {
ecma: 8,
mangle: true,
compress: {

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

@ -1,5 +1,6 @@
import fetch from 'node-fetch'
import { actions } from './mastodon-data'
import { actions } from './mastodon-data.js'
import esMain from 'es-main'
const numStatuses = actions
.map(_ => _.post || _.boost)
@ -26,7 +27,7 @@ async function waitForMastodonData () {
console.log('Mastodon data populated')
}
if (require.main === module) {
if (esMain(import.meta)) {
waitForMastodonData().catch(err => {
console.error(err)
process.exit(1)

Wyświetl plik

@ -1,4 +1,5 @@
import fetch from 'node-fetch'
import esMain from 'es-main'
export async function waitForMastodonUiToStart () {
while (true) {
@ -30,7 +31,7 @@ export async function waitForMastodonApiToStart () {
console.log('Mastodon API started up')
}
if (require.main === module) {
if (esMain(import.meta)) {
Promise.resolve()
.then(waitForMastodonApiToStart)
.then(waitForMastodonUiToStart).catch(err => {

11
docker-compose.yml 100644
Wyświetl plik

@ -0,0 +1,11 @@
---
version: "3"
services:
pinafore:
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
image: pinafore:latest
ports:
- 4002:4002

Wyświetl plik

@ -5,10 +5,11 @@ This guide is for instance admins who may be having trouble with Pinafore compat
By default, [Mastodon allows cross-origin access to the `/api` endpoint](https://github.com/tootsuite/mastodon/blob/50529cbceb84e611bca497624a7a4c38113e5135/config/initializers/cors.rb#L15-L20). Thus Pinafore should "just work" for most Mastodon servers.
If the nginx/Apache settings have been changed, though, then Pinafore might not be able to connect to an instance. To check if the instance is supported, run this command:
If the nginx/Apache settings have been changed, though, then Pinafore might not be able to connect to an instance. To check if the instance is supported, run this command (replacing `myinstance.com` with your instance URL):
```bash
curl -sLv example.com/api/v1/instance -o /dev/null
curl -sLv -H 'Origin: https://pinafore.social' -o /dev/null \
myinstance.com/api/v1/instance
```
If you see this in the output:
@ -19,7 +20,7 @@ Access-Control-Allow-Origin: *
Then Pinafore should work as expected!
Otherwise, if the instance admin would like to whitelist only certain websites (including Pinafore) to work with CORS, then they will need to make sure that the server echoes:
Otherwise, if the instance admin would like to whitelist only certain websites (including Pinafore) to work with CORS, then they will need to make sure that the server echoes:
```
Access-Control-Allow-Origin: https://pinafore.social

Wyświetl plik

@ -0,0 +1,62 @@
# Architecture
This document describes some things about the codebase that are worth knowing if you're trying to contribute.
Basically think of it as a "lay of the land" as well as "weird unusual stuff that may surprise you."
## Overview
Pinafore uses [SvelteJS](https://svelte.technology) v2 and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
are some quirks, which are described below. This list of quirks is non-exhaustive.
## Why Svelte v2 / Sapper ?
There is [no upgrade path from Svelte v2 to v3](https://github.com/sveltejs/svelte/issues/2462). Doing so would require manually migrating every component over. And in the end, it would probably not change the UX (user experience) of Pinafore – only the DX (developer experience).
Similarly, [Sapper would need to be migrated to SvelteKit](https://kit.svelte.dev/docs/migrating). Since Pinafore generates static files, there is probably not much benefit in moving from Sapper to SvelteKit.
For this reason, Pinafore has been stuck on Svelte v2 and Sapper for a long time. Migrating it is not something I've considered. The [v2 Svelte docs](https://v2.svelte.dev/) are still online, and share many similarities with Svelte v3.
## Prebuild process
The `template.html` is itself templated. The "template template" has some inline scripts, CSS, and SVGs
injected into it during the build process. SCSS is used for global CSS and themed CSS, but inside of the
components themselves, it's just vanilla CSS because I couldn't figure out how to get Svelte to run a SCSS
preprocessor.
## Lots of small files
Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and
code-splitting, as well as avoiding circular dependencies.
## emoji-picker-element is loaded as a third-party bundle
`emoji-picker-element` uses Svelte 3, whereas we use Svelte 2. So it's just imported
as a bundled custom element, not as a Svelte component.
## Some third-party code is bundled
For various reasons, `a11y-dialog`, `autosize`, and `timeago` are forked and bundled into the source code.
This was either because something needed to be tweaked or fixed, or I was trimming unused code and didn't
see much value in contributing it back, because it was too Pinafore-specific.
## Every Sapper page is "duplicated"
To get a nice animation on the nav bar when you switch columns, every page is lazy-loaded as `LazyPage.html`.
This "lazy page" is merely delayed a few frames to let the animation run. Therefore there is a duplication
between `src/routes` and `src/routes/_pages`. The "lazy page" is in the former, and the actual page is in the
latter. One imports the other.
## There are multiple stores
Originally I conceived of separating out the virtual list into a separate npm package, so I gave it its
own Svelte store (`virtualListStore.js`). This never happened, but it still has its own store. This is useful
anyway, because each store has its state maintained in an LRU cache that allows us to keep the scroll position
in the virtual list e.g. when the user hits the back button.
Also, the main `store.js` store is explicitly
loaded by every component that uses it. So there's no `store` inheritance; every component just declares
whatever store it uses. The main `store.js` is the primary one.
## There is a global event bus
It's in `eventBus.js`. This is useful for some stuff that is hard to do with standard Svelte or DOM events.

Wyświetl plik

@ -0,0 +1,19 @@
# Internationalization
To contribute or change translations for Pinafore, look in the [src/intl](https://github.com/nolanlawson/pinafore/tree/master/src/intl) directory. Create a new file or edit an existing file based on its [two-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and optionally, a region. For instance, `en-US.js` is American English, and `fr.js` is French.
The default is `en-US.js`, and any strings not defined in a language file will fall back to the strings from that file.
There is also an `intl/emoji-picker` directory, which contains translations for [emoji-picker-element](https://github.com/nolanlawson/emoji-picker-element)
(which already comes with English built-in).
Note that internationalization is currently experimental. Client-side locale switching is not supported – when you build
the instance of Pinafore, it is either one language or another. To build in a particular language, use (for example):
LOCALE=fr yarn build
or
LOCALE=fr yarn dev
To host a localized version of Pinafore using Vercel, you can see this example: [buildCommand in vercel.json for Spanish](https://github.com/nvdaes/vercelPinafore/blob/45c70fb2088fe5f2380a729dab83e6f3ab4e6291/vercel.json#L9).

Wyświetl plik

@ -1,129 +1,138 @@
{
"name": "pinafore",
"description": "Alternative web client for Mastodon",
"version": "1.15.9",
"version": "2.6.0",
"type": "module",
"engines": {
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0 || ^20.0.0"
},
"scripts": {
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
"dev": "run-s build-template-html build-assets serve-dev",
"dev": "run-s before-build serve-dev",
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
"before-build": "run-s build-template-html build-assets",
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper dev",
"before-build": "run-p build-template-html build-assets build-webpack-config",
"build": "cross-env NODE_ENV=production run-s build-steps",
"build-steps": "run-s before-build sapper-export build-now-json",
"sapper-build": "sapper build",
"build-steps": "run-s before-build sapper-export build-vercel-json",
"sapper-build": "cross-env WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper build",
"start": "node server.js",
"build-and-start": "run-s build start",
"build-template-html": "node -r esm ./bin/build-template-html.js",
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
"build-assets": "node -r esm ./bin/build-assets.js",
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
"build-template-html": "node ./bin/build-template-html.js",
"build-template-html-watch": "node ./bin/build-template-html.js --watch",
"build-assets": "node ./bin/build-assets.js",
"build-webpack-config": "rollup -c ./webpack/rollup.config.js",
"clone-mastodon": "node ./bin/clone-mastodon.js",
"install-mastodon": "node ./bin/install-mastodon.js",
"run-mastodon": "node ./bin/run-mastodon.js",
"test": "cross-env BROWSER=chrome:headless run-s test-browser",
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
"test-browser-suite0": "run-p --race run-mastodon build-and-start test-mastodon-suite0",
"test-mastodon-suite0": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite0",
"test-browser-suite1": "run-p --race run-mastodon build-and-start test-mastodon-suite1",
"test-mastodon-suite1": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite1",
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
"testcafe-suite0": "cross-env-shell testcafe -c 4 $BROWSER tests/spec/0*",
"testcafe-suite0": "cross-env-shell testcafe -c 2 $BROWSER tests/spec/0*",
"testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*",
"test-unit": "mocha -r esm -r bin/browser-shim.js tests/unit/",
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
"deploy-all-travis": "./bin/deploy-all-travis.sh",
"test-unit": "NODE_ENV=test mocha -r bin/browser-shim.js tests/unit/",
"test-in-ci-suite0": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite0",
"test-in-ci-suite1": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite1",
"test-vercel-json": "run-s test-vercel-json-copy build test-vercel-json-test",
"test-vercel-json-copy": "./bin/copy-vercel-json.sh",
"test-vercel-json-test": "./bin/test-vercel-json-unchanged.sh",
"wait-for-mastodon-to-start": "node bin/wait-for-mastodon-to-start.js",
"wait-for-mastodon-data": "node bin/wait-for-mastodon-data.js",
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
"sapper-export": "cross-env PORT=22939 sapper export",
"sapper-export": "cross-env PORT=22939 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper export",
"print-export-info": "node ./bin/print-export-info.js",
"export-steps": "run-s before-build sapper-export print-export-info",
"export": "cross-env NODE_ENV=production run-s export-steps",
"now-build": "run-s export",
"build-now-json": "node -r esm ./bin/build-now-json.js"
"build-vercel-json": "node bin/build-vercel-json.js"
},
"dependencies": {
"@babel/core": "^7.8.6",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.6",
"@babel/runtime": "^7.8.4",
"@rollup/plugin-replace": "^2.3.0",
"@webcomponents/custom-elements": "^1.4.0",
"arrow-key-navigation": "^1.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"blurhash": "^1.1.3",
"cheerio": "^1.0.0-rc.3",
"@formatjs/intl-listformat": "^7.1.3",
"@formatjs/intl-locale": "^3.0.7",
"@formatjs/intl-pluralrules": "^5.1.4",
"@formatjs/intl-relativetimeformat": "^11.1.4",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-replace": "^2.4.2",
"@stdlib/utils-noop": "^0.0.13",
"arrow-key-navigation": "^1.2.0",
"blurhash": "^1.1.5",
"cheerio": "1.0.0-rc.10",
"child-process-promise": "^2.2.1",
"chokidar": "^3.3.1",
"circular-dependency-plugin": "^5.2.0",
"clean-css": "^4.2.3",
"chokidar": "^3.5.3",
"circular-dependency-plugin": "^5.2.2",
"compression": "^1.7.4",
"cross-env": "^7.0.0",
"country-flag-emoji-polyfill": "^0.1.4",
"cross-env": "^7.0.3",
"css-dedoupe": "^0.1.1",
"css-loader": "^3.4.2",
"emoji-mart": "nolanlawson/emoji-mart#8bb6fb6",
"emoji-regex": "^9.0.0",
"encoding": "^0.1.12",
"emoji-picker-element": "^1.13.1",
"emoji-picker-element-data": "^1.3.0",
"emoji-regex": "^10.2.1",
"encoding": "^0.1.13",
"es-main": "^1.2.0",
"escape-html": "^1.0.3",
"esm": "^3.2.25",
"events-light": "^1.0.5",
"express": "^4.17.1",
"express": "^4.18.2",
"file-api": "^0.10.4",
"file-drop-element": "0.2.0",
"file-loader": "^6.0.0",
"form-data": "^3.0.0",
"glob": "^7.1.6",
"indexeddb-getall-shim": "^1.3.6",
"intersection-observer": "^0.8.0",
"intl": "^1.2.5",
"file-drop-element": "^1.0.1",
"file-loader": "^6.2.0",
"focus-visible": "^5.2.0",
"form-data": "^4.0.0",
"format-message-interpret": "^6.2.4",
"format-message-parse": "^6.2.4",
"glob": "^7.2.0",
"is-emoji-supported": "^0.0.5",
"li": "^1.3.0",
"localstorage-memory": "^1.0.3",
"lodash-es": "^4.17.15",
"lodash-webpack-plugin": "^0.11.5",
"mkdirp": "^1.0.3",
"node-fetch": "^2.6.0",
"node-sass": "^4.13.1",
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.7",
"npm-run-all": "^4.1.5",
"p-any": "^3.0.0",
"p-any": "^4.0.0",
"page-lifecycle": "^0.1.2",
"performance-now": "^2.1.0",
"pinch-zoom-element": "^1.1.1",
"preact": "^10.3.3",
"promise-worker": "^2.0.1",
"prop-types": "^15.7.2",
"prop-types": "^15.8.1",
"requestidlecallback": "^0.3.0",
"rollup": "^2.0.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-terser": "^5.2.0",
"sapper": "nolanlawson/sapper#for-pinafore-21",
"rollup": "^2.67.3",
"rollup-plugin-terser": "^7.0.2",
"rtl-detect": "^1.0.4",
"safari-14-idb-fix": "^1.0.4",
"sapper": "nolanlawson/sapper#for-pinafore-26",
"sass": "^1.56.1",
"stringz": "^2.1.0",
"svelte": "^2.16.1",
"svelte-extras": "^2.0.2",
"svelte-loader": "^2.13.6",
"svelte-transitions": "^1.2.0",
"svgo": "^1.3.2",
"terser-webpack-plugin": "^3.0.0",
"tesseract.js": "^2.0.2",
"tesseract.js-core": "^2.0.0",
"svgo": "^2.8.0",
"terser-webpack-plugin": "^5.3.6",
"tesseract.js": "^2.1.5",
"tesseract.js-core": "^2.2.0",
"text-encoding": "^0.7.0",
"tiny-queue": "^0.2.1",
"webpack": "^4.42.0",
"webpack-bundle-analyzer": "^3.6.1",
"worker-loader": "^2.0.0"
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"worker-loader": "^3.0.8"
},
"devDependencies": {
"assert": "^2.0.0",
"eslint-plugin-html": "^6.0.0",
"fake-indexeddb": "^3.0.0",
"mocha": "^7.1.0",
"now": "^18.0.0",
"standard": "^14.3.1",
"testcafe": "^1.8.3"
},
"engines": {
"node": ">= 8"
"eslint-plugin-html": "^7.1.0",
"fake-indexeddb": "^4.0.0",
"globby": "^13.1.2",
"husky": "^8.0.2",
"lint-staged": "^13.0.3",
"mocha": "^10.1.0",
"standard": "^17.0.0",
"testcafe": "^1.20.1"
},
"standard": {
"ignore": [
"webpack/*.cjs"
],
"globals": [
"AbortController",
"Blob",
@ -133,6 +142,7 @@
"Element",
"Event",
"FormData",
"HTMLElement",
"IDBKeyRange",
"IDBObjectStore",
"Image",
@ -184,5 +194,13 @@
"bugs": {
"url": "https://github.com/nolanlawson/pinafore/issues"
},
"homepage": "https://github.com/nolanlawson/pinafore#readme"
"homepage": "https://github.com/nolanlawson/pinafore#readme",
"lint-staged": {
"*.js": "standard --fix",
"*.html": "standard --fix --plugin html 'src/routes/**/*.html'"
},
"volta": {
"node": "14.21.1",
"yarn": "1.22.19"
}
}

Wyświetl plik

@ -1,15 +1,22 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import express from 'express'
import compression from 'compression'
const path = require('path')
const express = require('express')
const compression = require('compression')
const { routes: nowRoutes } = require('./now.json')
const __dirname = path.dirname(new URL(import.meta.url).pathname)
// JSON files not supported in ESM yet
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-import-json
const vercelJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'vercel.json'), 'utf8'))
const { routes: rawRoutes } = vercelJson
const { PORT = 4002 } = process.env
const app = express()
const exportDir = path.resolve(__dirname, '__sapper__/export')
const routes = nowRoutes.map(({ src, headers, dest }) => ({
const routes = rawRoutes.map(({ src, headers, dest }) => ({
regex: new RegExp(src),
headers,
dest

Wyświetl plik

@ -0,0 +1,154 @@
{
"background_color": "#ffffff",
"theme_color": "#4169e1",
"name": "{intl.longAppName}",
"short_name": "{intl.appName}",
"description": "{intl.appDescription}",
"categories": [
"social"
],
"display": "standalone",
"start_url": "/?pwa=true",
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "file",
"accept": [
"audio/*",
"image/*",
"video/*"
]
}
]
}
},
"icons": [
{
"src": "icon-48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icon-44.png",
"sizes": "44x44",
"type": "image/png"
},
{
"src": "icon-50.png",
"sizes": "50x50",
"type": "image/png"
},
{
"src": "icon-150.png",
"sizes": "150x150",
"type": "image/png"
},
{
"src": "icon-48-maskable.png",
"sizes": "48x48",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icon-72-maskable.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icon-96-maskable.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icon-144-maskable.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icon-192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icon-512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "{intl.newStatus}",
"url": "/?pwa=true&compose=true",
"icons": [
{
"src": "/icon-shortcut-fa-pencil.png",
"sizes": "192x192"
}
]
},
{
"name": "{intl.notifications}",
"url": "/notifications?pwa=true",
"icons": [
{
"src": "/icon-shortcut-fa-bell.png",
"sizes": "192x192"
}
]
}
],
"screenshots": [
{
"src": "screenshot-540-720-1.png",
"type": "image/png",
"sizes": "540x720"
},
{
"src": "screenshot-540-720-2.png",
"type": "image/jpeg",
"sizes": "540x720"
},
{
"src": "screenshot-540-720-3.png",
"type": "image/jpeg",
"sizes": "540x720"
}
]
}

Wyświetl plik

@ -1,33 +1,93 @@
<!doctype html>
<html lang="en">
<html lang="{process.env.LOCALE}" dir="{process.env.LOCALE_DIRECTION}">
<head>
<meta charset='utf-8' >
<meta name="viewport" content="width=device-width, viewport-fit=cover">
<meta name="viewport" content="width=device-width">
<meta id='theThemeColor' name='theme-color' content='#4169e1' >
<meta name="description" content="An alternative web client for Mastodon, focused on speed and simplicity." >
<meta name="description" content="{intl.appDescription}" >
%sapper.base%
<link id='theManifest' rel='manifest' href='/manifest.json' >
<link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
<link rel="apple-touch-icon" href="/apple-icon.png" >
<!-- both of these *-web-app-capable are required, for Chrome on Android and Safari on iOS
https://developers.google.com/web/fundamentals/native-hardware/fullscreen/ -->
<meta name="mobile-web-app-capable" content="yes" >
<meta name="apple-mobile-web-app-title" content="Pinafore" >
<meta name="apple-mobile-web-app-status-bar-style" content="white" >
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="{intl.appName}">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<!-- splashscreen for iOS -->
<link href="/iphone5_splash.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/iphone6_splash.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/iphoneplus_splash.png" media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="/iphonex_splash.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="/iphonexr_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/iphonexsmax_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="/ipad_splash.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/ipadpro1_splash.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/ipadpro3_splash.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="/ipadpro2_splash.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link rel="me" href="https://fosstodon.org/@pinafore">
<!-- inline CSS -->
<style id="theBottomNavStyle" media="only x">
:root {
--nav-top: calc(100dvh - var(--nav-total-height));
--nav-bottom: initial;
--main-content-pad-top: 0px;
--main-content-pad-bottom: var(--main-content-pad-vertical);
--toast-gap-bottom: var(--nav-total-height);
--fab-gap-top: 0px;
}
@supports not (height: 1dvh) {
/* In browsers that don't support dvh, use the large-small-dynamic-viewport-units-polyfill */
:root {
--nav-top: calc((100 * var(--1dvh)) - var(--nav-total-height));
}
}
</style>
<style id="theGrayscaleStyle" media="only x">
/* Firefox doesn't seem to like applying filter: grayscale() to
* the entire body, so we apply individually.
*/
img, svg, video,
input[type="checkbox"], input[type="radio"],
.inline-emoji, .theme-preview, .emoji-mart-emoji, .emoji-mart-skin {
.inline-emoji, .theme-preview, .account-profile {
filter: grayscale(100%);
}
</style>
<style id="theFocusVisiblePolyfillStyle" media="only x">
/* polyfill */
/* Note we have to use [data-focus-visible-added] rather than .focus-visible because
* Svelte overrides classes */
.js-focus-visible :focus:not([data-focus-visible-added]) {
outline: none !important; /* important to win the specificity war */
}
.js-focus-visible :focus:not([data-focus-visible-added]).focus-after::after {
display: none;
}
</style>
<style id="theFocusVisibleStyle" media="only x">
/* standard version */
:focus:not(:focus-visible) {
outline: none !important; /* important to win the specificity war */
}
:focus:not(:focus-visible).focus-after::after {
display: none;
}
</style>
<style id="theCenterNavStyle" media="only x">
@media (min-width: 992px) {
.main-nav-ul {
justify-content: center;
}
}
</style>
<noscript>
<style>
.hidden-from-ssr {
@ -61,6 +121,9 @@
<!-- LoadingMask.html gets rendered here -->
<div id="loading-mask" aria-hidden="true"></div>
<!-- announceAriaLivePolite.js gets rendered here -->
<div id="theAriaLive" class="sr-only" aria-live="polite"></div>
<!-- inline SVG -->
<!-- Sapper creates a <script> tag containing `templates/client.js`

Wyświetl plik

@ -1,18 +1,22 @@
import './routes/_thirdparty/regenerator-runtime/runtime.js'
import * as sapper from '../__sapper__/client.js'
import { loadPolyfills } from './routes/_utils/loadPolyfills'
import './routes/_utils/serviceWorkerClient'
import './routes/_utils/historyEvents'
import './routes/_utils/loadingMask'
import './routes/_utils/forceOnline'
import './routes/_utils/serviceWorkerClient.js'
import './routes/_utils/historyEvents.js'
import './routes/_utils/loadingMask.js'
import './routes/_utils/forceOnline.js'
import { mark, stop } from './routes/_utils/marks.js'
import { loadPolyfills } from './routes/_utils/polyfills/loadPolyfills.js'
import { loadNonCriticalPolyfills } from './routes/_utils/polyfills/loadNonCriticalPolyfills.js'
import idbReady from 'safari-14-idb-fix/dist/esm'
loadPolyfills().then(() => {
console.log('init()')
Promise.all([idbReady(), loadPolyfills()]).then(() => {
mark('sapperStart')
sapper.start({ target: document.querySelector('#sapper') })
stop('sapperStart')
/* no await */ loadNonCriticalPolyfills()
})
console.log('process.env.NODE_ENV', process.env.NODE_ENV)
if (module.hot) {
module.hot.accept()
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
}

Wyświetl plik

@ -3,12 +3,12 @@
// To allow CSP to work correctly, we also calculate a sha256 hash during
// the build process and write it to checksum.js.
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
import { basename } from '../routes/_api/utils'
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
import { storeLite } from '../routes/_store/storeLite'
import { isIOSPre12Point2 } from '../routes/_utils/userAgent/isIOSPre12Point2'
import { isMac } from '../routes/_utils/userAgent/isMac'
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine.js'
import { basename } from '../routes/_api/utils.js'
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut.js'
import { storeLite } from '../routes/_store/storeLite.js'
import { isIOSPre12Point2 } from '../routes/_utils/userAgent/isIOSPre12Point2.js'
import { isMac } from '../routes/_utils/userAgent/isMac.js'
window.__themeColors = process.env.THEME_COLORS
@ -16,9 +16,11 @@ const {
currentInstance,
instanceThemes,
disableCustomScrollbars,
bottomNav,
enableGrayscale,
pushSubscription,
loggedInInstancesInOrder
loggedInInstancesInOrder,
centerNav
} = storeLite.get()
const theme = (instanceThemes && instanceThemes[currentInstance]) || DEFAULT_THEME
@ -32,12 +34,13 @@ if (currentInstance) {
document.head.appendChild(link)
}
if (theme !== INLINE_THEME) {
if (theme !== INLINE_THEME || enableGrayscale) {
// switch theme ASAP to minimize flash of default theme
switchToTheme(theme, enableGrayscale)
}
if (enableGrayscale) {
// set the grayscale style on every img, svg, etc.
document.getElementById('theGrayscaleStyle')
.setAttribute('media', 'all') // enables the style
}
@ -52,6 +55,16 @@ if (disableCustomScrollbars) {
.setAttribute('media', 'only x') // disables the style
}
if (bottomNav) {
document.getElementById('theBottomNavStyle')
.setAttribute('media', 'all') // enables the style
}
if (centerNav) {
document.getElementById('theCenterNavStyle')
.setAttribute('media', 'all') // enables the style
}
// hack to make the scrollbars rounded only on macOS
if (isMac()) {
document.documentElement.style.setProperty('--scrollbar-border-radius', '50px')

620
src/intl/de.js 100644
Wyświetl plik

@ -0,0 +1,620 @@
export default {
// Home page, basic <title> and <description>
appName: 'Pinafore',
appDescription: 'Ein alternativer Web Client für Mastodon, der auf Geschwindigkeit und einfache Bedienung ausgelegt ist.',
homeDescription: `
<p>
Pinafore ist ein Web Client für
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
der für Geschwindigkeit und einfache Bedienung konzipiert wurde.
</p>
<p>
Lies den
<a rel="noopener" target="_blank"
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">einführenden Blogbeitrag</a> (auf englisch)
oder lege los, indem Du Dich bei einer Instanz anmeldest:
</p>`,
logIn: 'Anmelden',
footer: `
<p>
Pinafore ist
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">quelloffene Software</a>,
erstellt von <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
und verteilt unter der
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL Lizenz</a>.
Hier ist die <a href="/settings/about#privacy-policy" rel="prefetch">Datenschutzerklärung</a>.
</p>
`,
// Generic UI
loading: 'Wird geladen',
okay: 'OK',
cancel: 'Abbrechen',
alert: 'Hinweis',
close: 'Schließen',
error: 'Fehler: {error}',
errorShort: 'Fehler:',
// Relative timestamps
justNow: 'gerade eben',
// Navigation, page titles
navItemLabel: `
{label} {selected, select,
true {(aktuelle Seite)}
other {}
} {name, select,
notifications {{count, plural,
=0 {}
one {(eine Benachrichtigung)}
other {({count} Benachrichtigungen)}
}}
community {{count, plural,
=0 {}
one {(eine Followeranfrage)}
other {({count} Followeranfragen)}
}}
other {}
}
`,
blockedUsers: 'Blockierte Benutzer',
bookmarks: 'Lesezeichen',
directMessages: 'Direktnachrichten',
favorites: 'Favoriten',
federated: 'Föderiert',
home: 'Startseite',
local: 'Lokal',
notifications: 'Benachrichtigungen',
mutedUsers: 'Stummgeschaltete Benutzer',
pinnedStatuses: 'Angeheftete Tröts',
followRequests: 'Followeranfragen',
followRequestsLabel: `Followeranfragen {hasFollowRequests, select,
true {({count})}
other {}
}`,
list: 'Liste',
search: 'Suchen',
pageHeader: 'Seitenkopf',
goBack: 'Zurückgehen',
back: 'Zurück',
profile: 'Profil',
federatedTimeline: 'Föderierte Zeitleiste',
localTimeline: 'Lokale Zeitleiste',
// community page
community: 'Community',
pinnableTimelines: 'Zeitleisten, die angeheftet werden können',
timelines: 'Zeitleisten',
lists: 'Listen',
instanceSettings: 'Einstellungen der Instanz',
notificationMentions: 'Benachrichtigungen über Erwähnungen',
profileWithMedia: 'Profil mit Medien',
profileWithReplies: 'Profil mit Antworten',
hashtag: 'Hashtag',
// not logged in
profileNotLoggedIn: 'Hier erscheint Deine Benutzerzeitleiste, wenn Du Dich anmeldest.',
bookmarksNotLoggedIn: 'Hier erscheinen Deine Lesezeichen, wenn Du Dich anmeldest.',
directMessagesNotLoggedIn: 'Hier erscheinen Deine Direktnachrichten, wenn Du Dich anmeldest.',
favoritesNotLoggedIn: 'Hier erscheinen Deine Favoriten, wenn Du Dich anmeldest.',
federatedTimelineNotLoggedIn: 'Hier erscheint Deine föderierte Zeitleiste, wenn Du Dich anmeldest.',
localTimelineNotLoggedIn: 'Hier erscheint Deine lokale Zeitleiste, wenn Du Dich anmeldest.',
searchNotLoggedIn: 'Du kannst eine Suche ausführen, sobald Du bei einer Instanz angemeldet bist.',
communityNotLoggedIn: 'Hier erscheinen Community-Optionen, wenn Du Dich anmeldest.',
listNotLoggedIn: 'Hier erscheint eine Liste, wenn Du Dich anmeldest.',
notificationsNotLoggedIn: 'Hier erscheinen Deine Benachrichtigungen, wenn Du Dich anmeldest.',
notificationMentionsNotLoggedIn: 'Hier erscheinen Deine Benachrichtigungen zu Erwähnungen, wenn Du Dich anmeldest.',
statusNotLoggedIn: 'Hier erscheint der Faden zu einem Tröt, wenn Du Dich anmeldest.',
tagNotLoggedIn: 'Hier erscheinen Tröts zu einem hashtag, wenn Du Dich anmeldest.',
// Notification subpages
filters: 'Filter',
all: 'Alle',
mentions: 'Erwähnungen',
// Follow requests
approve: 'Genehmigen',
reject: 'Ablehnen',
// Hotkeys
hotkeys: 'Tastenkürzel',
global: 'Global',
timeline: 'Zeitleiste',
media: 'Medien',
globalHotkeys: `
{leftRightChangesFocus, select,
true {
<li><kbd></kbd> gehe zum nächsten fokussierbaren Element</li>
<li><kbd></kbd> gehe zum vorherigen fokussierbaren Element</li>
}
other {}
}
<li>
<kbd>1</kbd> - <kbd>6</kbd>
{leftRightChangesFocus, select,
true {}
other {oder <kbd></kbd>/<kbd></kbd>}
}
um die Spalten umzuschalten
</li>
<li><kbd>7</kbd> oder <kbd>c</kbd> zum Erstellen eines neuen Tröts</li>
<li><kbd>s</kbd> oder <kbd>/</kbd> zum Suchen</li>
<li><kbd>g</kbd> + <kbd>h</kbd> zur Startseite gehen</li>
<li><kbd>g</kbd> + <kbd>n</kbd> zu den Benachrichtigungen gehen</li>
<li><kbd>g</kbd> + <kbd>l</kbd> zur lokalen zeitleiste gehen</li>
<li><kbd>g</kbd> + <kbd>t</kbd> zur föderierten Zeitleiste gehen</li>
<li><kbd>g</kbd> + <kbd>c</kbd> zur Community-Seite gehen</li>
<li><kbd>g</kbd> + <kbd>d</kbd> zur Seite mit Direktnachrichten gehen</li>
<li><kbd>h</kbd> oder <kbd>?</kbd> zum Umschalten des Hilfe-Dialogs</li>
<li><kbd>Rückschritttaste</kbd> zurückgehen, Dialogfelder schließen</li>
`,
timelineHotkeys: `
<li><kbd>j</kbd> oder <kbd></kbd> nächsten Tröt ansteuern</li>
<li><kbd>k</kbd> oder <kbd></kbd> den vorherigen Tröt ansteuern</li>
<li><kbd>.</kbd> neue Tröts anzeigen und nach oben scrollen</li>
<li><kbd>o</kbd> Tröt öffnen</li>
<li><kbd>f</kbd> Tröt favorisieren</li>
<li><kbd>b</kbd> Tröt boosten</li>
<li><kbd>r</kbd> auf Tröt antworten</li>
<li><kbd>i</kbd> Bilder, Videos oder Audio öffnen</li>
<li><kbd>y</kbd> sensible Medieninhalte zeigen oder verbergen</li>
<li><kbd>m</kbd> den Verfasser erwähnen</li>
<li><kbd>p</kbd> das Profil des Verfassers öffnen</li>
<li><kbd>l</kbd> den Link des aktuellen Tröts in einem neuen Tab öffnen</li>
<li><kbd>x</kbd> den Text hinter der Inhaltswarnung anzeigen oder verbergen</li>
<li><kbd>z</kbd> den Text hinter der Inhaltswarnung für alle in dieser Unterhaltung anzeigen oder verbergen</li>
`,
mediaHotkeys: `
<li><kbd></kbd> / <kbd></kbd> zum nächsten oder vorherigen Inhalt gehen</li>
`,
// Community page, tabs
tabLabel: `{label} {current, select,
true {(Aktuell)}
other {}
}`,
pageTitle: `
{hasNotifications, select,
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
`,
pinLabel: `{label} {pinnable, select,
true {
{pinned, select,
true {(Angeheftete Seite)}
other {(Nicht angeheftete Seite)}
}
}
other {}
}`,
pinPage: 'Hefte {label} an',
// Status composition
composeStatus: 'Tröt erstellen',
postStatus: 'Tröt!',
contentWarning: 'Inhaltswarnung',
dropToUpload: 'Fallenlassen zum Hochladen',
invalidFileType: 'Ungültiger Dateityp',
composeLabel: "Was gibt's Neues?",
autocompleteDescription: 'Autovervollständigungsergebnisse verfügbar. Drücke die Pfeiltasten auf und ab und dann Eingabe zum Auswählen.',
mediaUploads: 'Medien-Uploads',
edit: 'Bearbeiten',
delete: 'Löschen',
description: 'Beschreibung',
descriptionLabel: 'Beschreibe Bilder oder Videos für Menschen mit Seheinschränkungen, oder Audio- oder Videoinhalte für Menschen mit Höreinschränkungen',
markAsSensitive: 'Medien als sensibel kennzeichnen',
// Polls
createPoll: 'Umfrage erstellen',
removePollChoice: 'Auswahl {index} entfernen',
pollChoiceLabel: 'Auswahl {index}',
multipleChoice: 'Mehrfachauswahl',
pollDuration: 'Dauer der Umfrage',
fiveMinutes: '5 Minuten',
thirtyMinutes: '30 Minuten',
oneHour: '1 Stunde',
sixHours: '6 Stunden',
oneDay: '1 Tag',
threeDays: '3 Tage',
sevenDays: '7 Tage',
addEmoji: 'Emoji einfügen',
addMedia: 'Medien einfügen (Bilder, video, audio)',
addPoll: 'Umfrage hinzufügen',
removePoll: 'Umfrage entfernen',
postPrivacyLabel: 'Privatsphäre anpassen (aktuell {label})',
addContentWarning: 'Inhaltswarnung hinzufügen',
removeContentWarning: 'Inhaltswarnung entfernen',
altLabel: 'Beschreibe für Menschen mit Seheinschränkungen',
extractText: 'Text aus Bild ermitteln',
extractingText: 'Erkenne Text',
extractingTextCompletion: 'Erkenne Text ({percent}% abgeschlossen)…',
unableToExtractText: 'Es konnte kein Text erkannt werden.',
// Account options
followAccount: 'Folge {account}',
unfollowAccount: 'Entfolge {account}',
blockAccount: 'Blockiere {account}',
unblockAccount: 'Blockieren von {account} aufheben',
muteAccount: 'Schalte {account} stumm',
unmuteAccount: 'Hebe Stummschaltung von {account} auf',
showReblogsFromAccount: 'Boosts von {account} anzeigen',
hideReblogsFromAccount: 'Boosts von {account} verbergen',
showDomain: 'Verbergen von {domain} aufheben',
hideDomain: 'Verbirg {domain}',
reportAccount: 'Melde {account}',
mentionAccount: 'Erwähne {account}',
copyLinkToAccount: 'Kopiere Link zu account',
copiedToClipboard: 'In Zwischenablage kopiert',
// Media dialog
navigateMedia: 'Durch Medieninhalte navigieren',
showPreviousMedia: 'Vorherigen Medieninhalt zeigen',
showNextMedia: 'Nächsten Medieninhalt zeigen',
enterPinchZoom: 'Modus Ziehen zum Zoomen',
exitPinchZoom: 'Modus Ziehen zum Zoomen beenden',
showMedia: `Zeige {index, select,
1 {ersten}
2 {zweiten}
3 {dritten}
other {vierten}
} Medieninhalt {current, select,
true {(aktuell)}
other {}
}`,
previewFocalPoint: 'Vorschau (Hauptausschnitt)',
enterFocalPoint: 'Koordinaten des Hauptausschnitts des Medieninhalts eingeben (X, Y)',
muteNotifications: 'Auch Benachrichtigungen stummschalten',
muteAccountConfirm: '{account} stummschalten?',
mute: 'Stummschalten',
unmute: 'Stummschaltung aufheben',
zoomOut: 'Herauszoomen',
zoomIn: 'Hineinzoomen',
// Reporting
reportingLabel: 'Du machst eine Meldung von {account} an die Moderatoren von {instance}.',
additionalComments: 'Zusätzliche Kommentare',
forwardDescription: 'Auch an die Moderatoren von {instance} weiterleiten?',
forwardLabel: 'An {instance} weiterleiten',
unableToLoadStatuses: 'Kann neueste Tröts nicht laden: {error}',
report: 'Melden',
noContent: '(Keine Inhalte)',
noStatuses: 'Keine Tröts zum Melden vorhanden',
// Status options
unpinFromProfile: 'Vom Profil abheften',
pinToProfile: 'An Profil anheften',
muteConversation: 'Unterhaltung stummschalten',
unmuteConversation: 'Stummschaltung der Unterhaltung aufheben',
bookmarkStatus: 'Tröt als Lesezeichen speichern',
unbookmarkStatus: 'Tröt aus Lesezeichen entfernen',
deleteAndRedraft: 'Löschen und neu eingeben',
reportStatus: 'Tröt melden',
shareStatus: 'Tröt teilen',
copyLinkToStatus: 'Link zum Tröt kopieren',
// Account profile
profileForAccount: 'Profil für {account}',
statisticsAndMoreOptions: 'Statistiken und weitere Optionen',
statuses: 'Tröts',
follows: 'Folgt',
followers: 'Folgende',
moreOptions: 'Weitere Optionen',
followersLabel: 'Gefolgt von {count}',
followingLabel: 'Folgt {count}',
followLabel: `Folgen {requested, select,
true {(Followeranfrage gestellt)}
other {}
}`,
unfollowLabel: `Entfolgen {requested, select,
true {(Followeranfrage gestellt)}
other {}
}`,
unblock: 'Blockade aufheben',
nameAndFollowing: 'Name und folgt',
clickToSeeAvatar: 'Klicke zum Anzeigen des Avatars',
opensInNewWindow: '{label} (öffnet in neuem Fenster)',
blocked: 'Blockiert',
domainHidden: 'Domain verborgen',
muted: 'Stummgeschaltet',
followsYou: 'Folgt Dir',
avatarForAccount: 'Avatar für {account}',
fields: 'Felder',
accountHasMoved: '{account} ist umgezogen:',
profilePageForAccount: 'Profilseite für {account}',
// About page
about: 'Über',
aboutApp: 'Über Pinafore',
aboutAppDescription: `
<p>
Pinafore ist
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore">freie und quelloffene Software</a>
erstellt von
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
und verteilt unter der
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
</p>
<h2 id="privacy-policy">Datenschutzerklärung</h2>
<p>
Pinafore speichert keine persönlichen Informationen auf seinen Servern,
einschließlich, aber nicht beschränkt auf, Namen, E-Mail-Adressen,
IP-Adressen, Beiträgen, und Fotos.
</p>
<p>
Pinafore ist eine statische Seite. Alle Daten werden lokal in Ihrem Browser gespeichert und mit den Instanzen des Fediversums geteilt, zu denen Sie sich verbinden.
</p>
<h2>Mitwirkende</h2>
<p>
Icons provided by <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
</p>
<p>
Logo thanks to "sailboat" by Gregor Cresnar from
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
</p>`,
// Settings
settings: 'Einstellungen',
general: 'Allgemein',
generalSettings: 'Allgemeine Einstellungen',
showSensitive: 'Sensible Inhalte standardmäßig anzeigen',
showPlain: 'Eine graue Fläche für sensible Inhalte anzeigen',
allSensitive: 'Alle Medien als sensibel behandeln',
largeMedia: 'Große eingebettete Bilder und Videos anzeigen',
autoplayGifs: 'Animierte Gifs automatisch abspielen',
hideCards: 'Linkvorschauen verbergen',
underlineLinks: 'Links in Tröts und Profilen unterstreichen',
accessibility: 'Barrierefreiheit',
reduceMotion: 'Bewegung in Animationen der Oberfläche reduzieren',
disableTappable: 'Berührungsempfindlichkeit auf ganzem Tröt deaktivieren',
removeEmoji: 'Emoji aus Anzeigenamen der Benutzer entfernen',
shortAria: 'Verkürzte aria-label für Artikel verwenden',
theme: 'Design',
themeForInstance: 'Design für {instance}',
disableCustomScrollbars: 'Angepasste Rollbalken deaktivieren',
preferences: 'Vorlieben',
hotkeySettings: 'Einstellungen für Tastenkürzel',
disableHotkeys: 'Alle Tastenkürzel deaktivieren',
leftRightArrows: 'Linke und rechte Pfeiltasten schalten den Fokus um anstatt der Spalten oder Medien',
guide: 'Anleitung',
reload: 'Neu laden',
// Wellness settings
wellness: 'Wohlbefinden',
wellnessSettings: 'Einstellungen für ein gutes Wohlbefinden',
wellnessDescription: `Die Einstellungen fürs Wohlbefinden dienen dazu, die süchtig machenden oder Angst induzierenden Aspekte von Social Media zu reduzieren.
Nimm hier Einstellungen vor, die Dir gut tun.`,
enableAll: 'Alle einschalten',
metrics: 'Messungen',
hideFollowerCount: 'Verbirg Anzahl Folgender (ab 10 gedeckelt)',
hideReblogCount: 'Verbirg Anzahl der Boosts',
hideFavoriteCount: 'Verbirg Anzahl Favorisierungen',
hideUnread: 'Verbirg Anzeige ungelesener Benachrichtigungen (z.B. den roten Punkt)',
ui: 'Benutzeroberfläche',
grayscaleMode: 'Graustufenmodus',
wellnessFooter: `Diese Einstellungen basieren zum Teil auf Richtlinien des
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
// This is a link: "You can filter or disable notifications in the _instance settings_"
filterNotificationsPre: 'Du kannst die Einstellungen für Benachrichtigungen in den',
filterNotificationsText: 'Instanzeinstellungen',
filterNotificationsPost: 'anpassen.',
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
// to see a description. It's hard to properly internationalize, so we just break up the strings.
disableInfiniteScrollPre: '',
disableInfiniteScrollText: 'Unendliches Scrollen',
disableInfiniteScrollDescription: 'Wenn unendliches Scrollen deaktiviert ist, erscheinen neue Tröts nicht automatisch am unteren oder oberen Ende der zeitleiste. Stattdessen kannst Du weitere Inhalte durch dafür vorgesehene Schaltflächen nachladen.',
disableInfiniteScrollPost: 'deaktivieren',
// Instance settings
loggedInAs: 'Eingeloggt als',
homeTimelineFilters: 'Filter für Startzeitleiste',
notificationFilters: 'Filter für Benachrichtigungen',
pushNotifications: 'Push-Benachrichtigungen',
// Add instance page
storageError: 'Es sieht so aus als ob Pinafore lokal keine Daten speichern kann. Ist Dein Browser im privaten Modus oder blockiert Cookies? Pinafore speichert alle Daten lokal und braucht zum ordnungsgemäßen Betrieb LocalStorage und IndexedDB.',
javaScriptError: 'Du musst zum Einloggen javaScript einschalten.',
enterInstanceName: 'Namen der Instanz eingeben',
instanceColon: 'Instanz:',
// Custom tooltip, concatenated together
getAnInstancePre: 'Hast Du noch keine',
getAnInstanceText: 'instanz',
getAnInstanceDescription: 'Eine Instanz ist Deine Heimat auf Mastodon, wie z.B. mastodon.social oder cybre.space.',
getAnInstancePost: '?',
joinMastodon: 'Tritt Mastodon bei!',
instancesYouveLoggedInTo: 'Instanzen, in denen Du angemeldet bist:',
addAnotherInstance: 'Eine weitere Instanz hinzufügen',
youreNotLoggedIn: 'Du bist bei keiner Instanz angemeldet.',
currentInstanceLabel: `{instance} {current, select,
true {(jetzige Instanz)}
other {}
}`,
// Link text
logInToAnInstancePre: '',
logInToAnInstanceText: 'Melde Dich bei einer Instanz an',
logInToAnInstancePost: 'um Pinafore zu verwenden.',
// Another custom tooltip
showRingPre: 'Immer einen',
showRingText: 'Fokusring',
showRingDescription: 'Der Fokusring ist der Rahmen, der um das fokussierte Element angezeigt wird, wenn Du mit der Tastatur navigierst. Standardmäßig wird er nicht angezeigt, wenn Du die maus oder einen Touchscreen verwendest. Hier kannst du einstellen, dass er immer angezeigt wird.',
showRingPost: 'anzeigen',
instances: 'Instanzen',
addInstance: 'Instanz hinzufügen',
homeTimelineFilterSettings: 'Einstellungen für die Filterung der Startzeitleiste',
showReblogs: 'Boosts zeigen',
showReplies: 'Antworten zeigen',
switchOrLogOut: 'Zu dieser Instanz wechseln oder sich von ihr abmelden',
switchTo: 'Zu dieser Instanz wechseln',
switchToInstance: 'Zu Instanz wechseln',
switchToNameOfInstance: 'Wechsle zu {instance}',
logOut: 'Abmelden',
logOutOfInstanceConfirm: 'Von {instance} abmelden?',
notificationFilterSettings: 'Einstellungen für die Filterung von Benachrichtigungen',
// Push notifications
browserDoesNotSupportPush: 'Dein Browser unterstützt keine Push-Benachrichtigungen.',
deniedPush: 'Du hast es abgelehnt, Push-Benachrichtigungen anzuzeigen.',
pushNotificationsNote: 'Beachte, dass Du nur für jeweils eine Instanz Push-Benachrichtigungen anzeigen lassen kannst.',
pushSettings: 'Einstellungen für Push-Benachrichtigungen',
newFollowers: 'Neue Folgende',
reblogs: 'Boosts',
pollResults: 'Umfrageergebnisse',
needToReauthenticate: 'Du musst Dich neu anmelden, um die Push-Benachrichtigung einschalten zu können. Jetzt von {instance} abmelden?',
failedToUpdatePush: 'Fehler beim Aktualisieren der Einstellungen für Push-Benachrichtigungen: {error}',
// Themes
chooseTheme: 'Wähle ein Design',
darkBackground: 'Dunkler Hintergrund',
lightBackground: 'Heller Hintergrund',
themeLabel: `{label} {default, select,
true {(standard)}
other {}
}`,
animatedImage: 'Animiertes Gif: {description}',
showImage: `Zeige {animated, select,
true {animiert}
other {}
} image: {description}`,
playVideoOrAudio: `Wiedergabe von {audio, select,
true {audio}
other {video}
}: {description}`,
accountFollowedYou: '{name} folgt Dir jetzt, {account}',
reblogCountsHidden: 'Anzahl Boosts verborgen',
favoriteCountsHidden: 'Anzahl Favorisierungen verborgen',
rebloggedTimes: `Geboostet {count, plural,
one {einmal}
other {{count} mal}
}`,
favoritedTimes: `Favorisiert {count, plural,
one {einmal}
other {{count} mal}
}`,
pinnedStatus: 'Angehefteter Tröt',
rebloggedYou: 'hat Deinen Tröt geboostet',
favoritedYou: 'hat Deinen Tröt favorisiert',
followedYou: 'folgt Dir jetzt',
pollYouCreatedEnded: 'Eine von Dir erstellte Umfrage ist beendet',
pollYouVotedEnded: 'Eine Umfrage, an der Du teilgenommen hast, ist beendet',
reblogged: 'geboostet',
showSensitiveMedia: 'Sensible Inhalte zeigen',
hideSensitiveMedia: 'Sensible Inhalte verbergen',
clickToShowSensitive: 'Sensible Inhalte. Klicke zum Anzeigen.',
longPost: 'Langer Beitrag',
// Accessible status labels
accountRebloggedYou: '{account} hat Deinen Tröt geboostet',
accountFavoritedYou: '{account} hat Deinen Tröt favorisiert',
rebloggedByAccount: 'geboostet von {account}',
contentWarningContent: 'Inhaltswarnung: {spoiler}',
hasMedia: 'hat Medien',
hasPoll: 'hat Umfrage',
shortStatusLabel: '{privacy} Tröt von {account}',
// Privacy types
public: 'Öffentlich',
unlisted: 'Nicht gelistet',
followersOnly: 'Nur Folgende',
direct: 'Direkt',
// Themes
themeRoyal: 'Royal',
themeScarlet: 'Scarlet',
themeSeafoam: 'Seafoam',
themeHotpants: 'Hotpants',
themeOaken: 'Oaken',
themeMajesty: 'Majesty',
themeGecko: 'Gecko',
themeGrayscale: 'Grayscale',
themeOzark: 'Ozark',
themeCobalt: 'Cobalt',
themeSorcery: 'Sorcery',
themePunk: 'Punk',
themeRiot: 'Riot',
themeHacker: 'Hacker',
themeMastodon: 'Mastodon',
themePitchBlack: 'Pitch Black',
themeDarkGrayscale: 'Dark Grayscale',
// Polls
voteOnPoll: 'In dieser Umfrage abstimmen',
pollChoices: 'Auswahlmöglichkeiten',
vote: 'Abstimmen',
pollDetails: 'Einzelheiten zur Umfrage',
refresh: 'Aktualisieren',
expires: 'Endet',
expired: 'Beendet',
voteCount: `{count, plural,
one {eine Stimme}
other {{count} Stimmen}
}`,
// Status interactions
clickToShowThread: '{time} - Klicke, um Unterhaltung anzuzeigen',
showMore: 'Zeige mehr',
showLess: 'Zeige weniger',
closeReply: 'Antwort schließen',
cannotReblogFollowersOnly: 'Kann nicht geboostet werden, da nur Folgende',
cannotReblogDirectMessage: 'Kann nicht geboostet werden, da dies eine Direktnachricht ist',
reblog: 'Boost',
reply: 'Antworten',
replyToThread: 'Auf Unterhaltung antworten',
favorite: 'Favorisieren',
unfavorite: 'Favorisieren aufheben',
// timeline
loadingMore: 'Lade weitere',
loadMore: 'Lade weitere',
showCountMore: 'Zeige {count} weitere',
nothingToShow: 'Nichts zum anzeigen.',
// status thread page
statusThreadPage: 'Seite für Tröt-Unterhaltung',
status: 'Tröt',
// toast messages
blockedAccount: 'Account blockiert',
unblockedAccount: 'Blockade des Accounts aufgehoben',
unableToBlock: 'Konnte Account nicht blockieren: {error}',
unableToUnblock: 'Konnte Blockade des Accounts nicht aufheben: {error}',
bookmarkedStatus: 'Tröt als Lesezeichen gespeichert',
unbookmarkedStatus: 'Tröt aus Lesezeichen entfernt',
unableToBookmark: 'Konnte kein lesezeichen setzen: {error}',
unableToUnbookmark: 'Konnte Lesezeichen nicht entfernen: {error}',
cannotPostOffline: 'Du kannst nicht senden, wenn Du offline bist',
unableToPost: 'Konnte Tröt nicht posten: {error}',
statusDeleted: 'Tröt gelöscht',
unableToDelete: 'Konnte Tröt nicht löschen: {error}',
cannotFavoriteOffline: 'Du kannst nicht favorisieren, wenn Du offline bist',
cannotUnfavoriteOffline: 'Du kannst Favorisierung nicht zurücknehmen, wenn Du offline bist',
unableToFavorite: 'Konnte nicht favorisieren: {error}',
unableToUnfavorite: 'Konnte Favorisierung nicht aufheben: {error}',
followedAccount: 'folge jetzt diesem Account',
unfollowedAccount: 'Folge diesem Account nicht mehr',
unableToFollow: 'Konnte dem Account nicht folgen: {error}',
unableToUnfollow: 'Konnte den Account nicht entfolgen: {error}',
accessTokenRevoked: 'Das Zugriffstoken wurde zurückgezogen, von {instance} abgemeldet',
loggedOutOfInstance: 'Von {instance} abgemeldet',
failedToUploadMedia: 'Konnte Medien nicht hochladen: {error}',
mutedAccount: 'Account stummgeschaltet',
unmutedAccount: 'Stummschaltung von Account aufgehoben',
unableToMute: 'Konnte Account nicht stummschalten: {error}',
unableToUnmute: 'Konnte Stummschaltung von Account nicht aufheben: {error}',
mutedConversation: 'Unterhaltung stummgeschaltet',
unmutedConversation: 'Stummschaltung der Unterhaltung aufgehoben',
unableToMuteConversation: 'Konnte Unterhaltung nicht stummschalten: {error}',
unableToUnmuteConversation: 'Konnte Stummschaltung der Unterhaltung nicht aufheben: {error}',
unpinnedStatus: 'Tröt abgeheftet',
unableToPinStatus: 'Konnte Tröt nicht anheften: {error}',
unableToUnpinStatus: 'Konnte Tröt nicht abheften: {error}',
unableToRefreshPoll: 'Konnte Umfrage nicht aktualisieren: {error}',
unableToVoteInPoll: 'Konte in der Umfrage nicht abstimmen: {error}',
cannotReblogOffline: 'Du kannst nicht boosten, wenn Du offline bist.',
cannotUnreblogOffline: 'Du kannst einen Boost nicht aufheben, wenn Du offline bist.',
failedToReblog: 'Boosten fehlgeschlagen: {error}',
failedToUnreblog: 'Aufheben des Boosts fehlgeschlagen: {error}',
submittedReport: 'Meldung übermittelt',
failedToReport: 'Übermittlung der Meldung fehlgeschlagen: {error}',
approvedFollowRequest: 'Folgeanfrage genehmigt',
rejectedFollowRequest: 'Folgeanfrage abgelehnt',
unableToApproveFollowRequest: 'Konnte Folgeanfrage nicht genehmigen: {error}',
unableToRejectFollowRequest: 'Konnte Folgeanfrage nicht ablehnen: {error}',
searchError: 'Fehler bei der Suche: {error}',
hidDomain: 'Domain verborgen',
unhidDomain: 'Domain nicht mehr verborgen',
unableToHideDomain: 'Konnte Domain nicht verbergen: {error}',
unableToUnhideDomain: 'Konnte Verbergen der Domain nicht aufheben: {error}',
showingReblogs: 'Zeige Boosts an',
hidingReblogs: 'Verberge Boosts',
unableToShowReblogs: 'Kann Boosts nicht anzeigen: {error}',
unableToHideReblogs: 'Kann Boosts nicht verbergen: {error}',
unableToShare: 'Teilen fehlgeschlagen: {error}',
showingOfflineContent: 'Anforderung übers Internet fehlgeschlagen. Zeige Offline-Inhalte an.',
youAreOffline: 'Du scheinst keine Verbindung zum Internet zu haben. Du kanst weiterhin Tröts lesen, solange Du offline bist.',
// Snackbar UI
updateAvailable: 'Update der App verfügbar'
}

699
src/intl/en-US.js 100644
Wyświetl plik

@ -0,0 +1,699 @@
export default {
// Home page, basic <title> and <description>
appName: 'Pinafore',
appDescription: 'An alternative web client for Mastodon, focused on speed and simplicity.',
homeDescription: `
<p>
Pinafore is a web client for
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
designed for speed and simplicity.
</p>
<p>
Read the
<a rel="noopener" target="_blank"
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">introductory blog post</a>,
or get started by logging in to an instance:
</p>`,
logIn: 'Log in',
footer: `
<p>
Pinafore is
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">open-source software</a>
created by
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
and distributed under the
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>.
Here is the <a href="/settings/about#privacy-policy" rel="prefetch">privacy policy</a>.
</p>
`,
// Manifest
longAppName: 'Pinafore for Mastodon',
newStatus: 'New toot',
// Generic UI
loading: 'Loading',
okay: 'OK',
cancel: 'Cancel',
alert: 'Alert',
close: 'Close',
error: 'Error: {error}',
errorShort: 'Error:',
// Relative timestamps
justNow: 'just now',
// Navigation, page titles
navItemLabel: `
{label} {selected, select,
true {(current page)}
other {}
} {name, select,
notifications {{count, plural,
=0 {}
one {(1 notification)}
other {({count} notifications)}
}}
community {{count, plural,
=0 {}
one {(1 follow request)}
other {({count} follow requests)}
}}
other {}
}
`,
blockedUsers: 'Blocked users',
bookmarks: 'Bookmarks',
directMessages: 'Direct messages',
favorites: 'Favorites',
federated: 'Federated',
home: 'Home',
local: 'Local',
notifications: 'Notifications',
mutedUsers: 'Muted users',
pinnedStatuses: 'Pinned toots',
followRequests: 'Follow requests',
followRequestsLabel: `Follow requests {hasFollowRequests, select,
true {({count})}
other {}
}`,
list: 'List',
search: 'Search',
pageHeader: 'Page header',
goBack: 'Go back',
back: 'Back',
profile: 'Profile',
federatedTimeline: 'Federated timeline',
localTimeline: 'Local timeline',
// community page
community: 'Community',
pinnableTimelines: 'Pinnable timelines',
timelines: 'Timelines',
lists: 'Lists',
instanceSettings: 'Instance settings',
notificationMentions: 'Notification mentions',
profileWithMedia: 'Profile with media',
profileWithReplies: 'Profile with replies',
hashtag: 'Hashtag',
// not logged in
profileNotLoggedIn: 'A user timeline will appear here when logged in.',
bookmarksNotLoggedIn: 'Your bookmarks will appear here when logged in.',
directMessagesNotLoggedIn: 'Your direct messages will appear here when logged in.',
favoritesNotLoggedIn: 'Your favorites will appear here when logged in.',
federatedTimelineNotLoggedIn: 'Your federated timeline will appear here when logged in.',
localTimelineNotLoggedIn: 'Your local timeline will appear here when logged in.',
searchNotLoggedIn: 'You can search once logged in to an instance.',
communityNotLoggedIn: 'Community options appear here when logged in.',
listNotLoggedIn: 'A list will appear here when logged in.',
notificationsNotLoggedIn: 'Your notifications will appear here when logged in.',
notificationMentionsNotLoggedIn: 'Your notification mentions will appear here when logged in.',
statusNotLoggedIn: 'A toot thread will appear here when logged in.',
tagNotLoggedIn: 'A hashtag timeline will appear here when logged in.',
// Notification subpages
filters: 'Filters',
all: 'All',
mentions: 'Mentions',
// Follow requests
approve: 'Approve',
reject: 'Reject',
// Hotkeys
hotkeys: 'Hotkeys',
global: 'Global',
timeline: 'Timeline',
media: 'Media',
globalHotkeys: `
{leftRightChangesFocus, select,
true {
<li><kbd></kbd> to go to the next focusable element</li>
<li><kbd></kbd> to go to the previous focusable element</li>
}
other {}
}
<li>
<kbd>1</kbd> - <kbd>6</kbd>
{leftRightChangesFocus, select,
true {}
other {or <kbd></kbd>/<kbd></kbd>}
}
to switch columns
</li>
<li><kbd>7</kbd> or <kbd>c</kbd> to compose a new toot</li>
<li><kbd>s</kbd> or <kbd>/</kbd> to search</li>
<li><kbd>g</kbd> + <kbd>h</kbd> to go home</li>
<li><kbd>g</kbd> + <kbd>n</kbd> to go to notifications</li>
<li><kbd>g</kbd> + <kbd>l</kbd> to go to the local timeline</li>
<li><kbd>g</kbd> + <kbd>t</kbd> to go to the federated timeline</li>
<li><kbd>g</kbd> + <kbd>c</kbd> to go to the community page</li>
<li><kbd>g</kbd> + <kbd>d</kbd> to go to the direct messages page</li>
<li><kbd>h</kbd> or <kbd>?</kbd> to toggle the help dialog</li>
<li><kbd>Backspace</kbd> to go back, close dialogs</li>
`,
timelineHotkeys: `
<li><kbd>j</kbd> or <kbd></kbd> to activate the next toot</li>
<li><kbd>k</kbd> or <kbd></kbd> to activate the previous toot</li>
<li><kbd>.</kbd> to show more and scroll to top</li>
<li><kbd>o</kbd> to open</li>
<li><kbd>f</kbd> to favorite</li>
<li><kbd>b</kbd> to boost</li>
<li><kbd>r</kbd> to reply</li>
<li><kbd>Escape</kbd> to close reply</li>
<li><kbd>a</kbd> to bookmark</li>
<li><kbd>i</kbd> to open images, video, or audio</li>
<li><kbd>y</kbd> to show or hide sensitive media</li>
<li><kbd>m</kbd> to mention the author</li>
<li><kbd>p</kbd> to open the author's profile</li>
<li><kbd>l</kbd> to open the card's link in a new tab</li>
<li><kbd>x</kbd> to show or hide text behind content warning</li>
<li><kbd>z</kbd> to show or hide all content warnings in a thread</li>
`,
mediaHotkeys: `
<li><kbd></kbd> / <kbd></kbd> to go to next or previous</li>
`,
// Community page, tabs
tabLabel: `{label} {current, select,
true {(Current)}
other {}
}`,
pageTitle: `
{hasNotifications, select,
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
`,
pinLabel: `{label} {pinnable, select,
true {
{pinned, select,
true {(Pinned page)}
other {(Unpinned page)}
}
}
other {}
}`,
pinPage: 'Pin {label}',
// Status composition
composeStatus: 'Compose toot',
postStatus: 'Toot!',
contentWarning: 'Content warning',
dropToUpload: 'Drop to upload',
invalidFileType: 'Invalid file type',
composeLabel: "What's on your mind?",
autocompleteDescription: 'When autocomplete results are available, press up or down arrows and enter to select.',
mediaUploads: 'Media uploads',
edit: 'Edit',
delete: 'Delete',
description: 'Description',
descriptionLabel: 'Describe for visually impaired (image, video) or auditorily impaired (audio, video) people',
markAsSensitive: 'Mark media as sensitive',
// Polls
createPoll: 'Create poll',
removePollChoice: 'Remove choice {index}',
pollChoiceLabel: 'Choice {index}',
multipleChoice: 'Multiple choice',
pollDuration: 'Poll duration',
fiveMinutes: '5 minutes',
thirtyMinutes: '30 minutes',
oneHour: '1 hour',
sixHours: '6 hours',
twelveHours: '12 hours',
oneDay: '1 day',
threeDays: '3 days',
sevenDays: '7 days',
never: 'Never',
addEmoji: 'Insert emoji',
addMedia: 'Add media (images, video, audio)',
addPoll: 'Add poll',
removePoll: 'Remove poll',
postPrivacyLabel: 'Adjust privacy (currently {label})',
addContentWarning: 'Add content warning',
removeContentWarning: 'Remove content warning',
altLabel: 'Describe for visually impaired people',
extractText: 'Extract text from image',
extractingText: 'Extracting text…',
extractingTextCompletion: 'Extracting text ({percent}% complete)…',
unableToExtractText: 'Unable to extract text.',
// Account options
followAccount: 'Follow {account}',
unfollowAccount: 'Unfollow {account}',
blockAccount: 'Block {account}',
unblockAccount: 'Unblock {account}',
muteAccount: 'Mute {account}',
unmuteAccount: 'Unmute {account}',
showReblogsFromAccount: 'Show boosts from {account}',
hideReblogsFromAccount: 'Hide boosts from {account}',
showDomain: 'Unhide {domain}',
hideDomain: 'Hide {domain}',
reportAccount: 'Report {account}',
mentionAccount: 'Mention {account}',
copyLinkToAccount: 'Copy link to account',
copiedToClipboard: 'Copied to clipboard',
// Media dialog
navigateMedia: 'Navigate media items',
showPreviousMedia: 'Show previous media',
showNextMedia: 'Show next media',
enterPinchZoom: 'Pinch-zoom mode',
exitPinchZoom: 'Exit pinch-zoom mode',
showMedia: `Show {index, select,
1 {first}
2 {second}
3 {third}
other {fourth}
} media {current, select,
true {(current)}
other {}
}`,
previewFocalPoint: 'Preview (focal point)',
enterFocalPoint: 'Enter the focal point (X, Y) for this media',
muteNotifications: 'Mute notifications as well',
muteAccountConfirm: 'Mute {account}?',
mute: 'Mute',
unmute: 'Unmute',
zoomOut: 'Zoom out',
zoomIn: 'Zoom in',
// Reporting
reportingLabel: 'You are reporting {account} to the moderators of {instance}.',
additionalComments: 'Additional comments',
forwardDescription: 'Forward to the moderators of {instance} as well?',
forwardLabel: 'Forward to {instance}',
unableToLoadStatuses: 'Unable to load recent toots: {error}',
report: 'Report',
noContent: '(No content)',
noStatuses: 'No toots to report',
// Status options
unpinFromProfile: 'Unpin from profile',
pinToProfile: 'Pin to profile',
muteConversation: 'Mute conversation',
unmuteConversation: 'Unmute conversation',
bookmarkStatus: 'Bookmark toot',
unbookmarkStatus: 'Unbookmark toot',
deleteAndRedraft: 'Delete and redraft',
reportStatus: 'Report toot',
shareStatus: 'Share toot',
copyLinkToStatus: 'Copy link to toot',
// Account profile
profileForAccount: 'Profile for {account}',
statisticsAndMoreOptions: 'Stats and more options',
statuses: 'Toots',
follows: 'Follows',
followers: 'Followers',
moreOptions: 'More options',
followersLabel: 'Followed by {count}',
followingLabel: 'Follows {count}',
followLabel: `Follow {requested, select,
true {(follow requested)}
other {}
}`,
unfollowLabel: `Unfollow {requested, select,
true {(follow requested)}
other {}
}`,
notify: 'Subscribe to {account}',
denotify: 'Unsubscribe from {account}',
subscribedAccount: 'Subscribed to account',
unsubscribedAccount: 'Unsubscribed from account',
unblock: 'Unblock',
nameAndFollowing: 'Name and following',
clickToSeeAvatar: 'Click to see avatar',
opensInNewWindow: '{label} (opens in new window)',
blocked: 'Blocked',
domainHidden: 'Domain hidden',
muted: 'Muted',
followsYou: 'Follows you',
avatarForAccount: 'Avatar for {account}',
fields: 'Fields',
accountHasMoved: '{account} has moved:',
profilePageForAccount: 'Profile page for {account}',
// About page
about: 'About',
aboutApp: 'About Pinafore',
aboutAppDescription: `
<p>
Pinafore is
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore">free and open-source software</a>
created by
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
and distributed under the
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
</p>
<h2 id="privacy-policy">Privacy Policy</h2>
<p>
Pinafore does not store any personal information on its servers,
including but not limited to names, email addresses,
IP addresses, posts, and photos.
</p>
<p>
Pinafore is a static site. All data is stored locally in your browser and shared with the fediverse
instance(s) you connect to.
</p>
<h2>Credits</h2>
<p>
Icons provided by <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
</p>
<p>
Logo thanks to "sailboat" by Gregor Cresnar from
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
</p>`,
// Settings
settings: 'Settings',
general: 'General',
generalSettings: 'General settings',
showSensitive: 'Show sensitive media by default',
showAllSpoilers: 'Expand content warnings by default',
showPlain: 'Show a plain gray color for sensitive media',
allSensitive: 'Treat all media as sensitive',
largeMedia: 'Show large inline images and videos',
autoplayGifs: 'Autoplay animated GIFs',
hideCards: 'Hide link preview cards',
underlineLinks: 'Underline links in toots and profiles',
accessibility: 'Accessibility',
reduceMotion: 'Reduce motion in UI animations',
disableTappable: 'Disable tappable area on entire toot',
removeEmoji: 'Remove emoji from user display names',
shortAria: 'Use short article ARIA labels',
theme: 'Theme',
themeForInstance: 'Theme for {instance}',
disableCustomScrollbars: 'Disable custom scrollbars',
bottomNav: 'Place the navigation bar at the bottom of the screen',
centerNav: 'Center the navigation bar',
preferences: 'Preferences',
hotkeySettings: 'Hotkey settings',
disableHotkeys: 'Disable all hotkeys',
leftRightArrows: 'Left/right arrow keys change focus rather than columns/media',
guide: 'Guide',
reload: 'Reload',
// Wellness settings
wellness: 'Wellness',
wellnessSettings: 'Wellness settings',
wellnessDescription: `Wellness settings are designed to reduce the addictive or anxiety-inducing aspects of social media.
Choose any options that work well for you.`,
enableAll: 'Enable all',
metrics: 'Metrics',
hideFollowerCount: 'Hide follower counts (capped at 10)',
hideReblogCount: 'Hide boost counts',
hideFavoriteCount: 'Hide favorite counts',
hideUnread: 'Hide unread notifications count (i.e. the red dot)',
// The quality that makes something seem important or interesting because it seems to be happening now
immediacy: 'Immediacy',
showAbsoluteTimestamps: 'Show absolute timestamps (e.g. "March 3rd") instead of relative timestamps (e.g. "5 minutes ago")',
ui: 'UI',
grayscaleMode: 'Grayscale mode',
wellnessFooter: `These settings are partly based on guidelines from the
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
// This is a link: "You can filter or disable notifications in the _instance settings_"
filterNotificationsPre: 'You can filter or disable notifications in the',
filterNotificationsText: 'instance settings',
filterNotificationsPost: '',
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
// to see a description. It's hard to properly internationalize, so we just break up the strings.
disableInfiniteScrollPre: 'Disable',
disableInfiniteScrollText: 'infinite scroll',
disableInfiniteScrollDescription: `When infinite scroll is disabled, new toots will not automatically appear at
the bottom or top of the timeline. Instead, buttons will allow you to
load more content on demand.`,
disableInfiniteScrollPost: '',
// Instance settings
loggedInAs: 'Logged in as',
homeTimelineFilters: 'Home timeline filters',
notificationFilters: 'Notification filters',
pushNotifications: 'Push notifications',
// Add instance page
storageError: `It seems Pinafore cannot store data locally. Is your browser in private mode
or blocking cookies? Pinafore stores all data locally, and requires LocalStorage and
IndexedDB to work correctly.`,
javaScriptError: 'You must enable JavaScript to log in.',
enterInstanceName: 'Enter instance name',
instanceColon: 'Instance:',
// Custom tooltip, concatenated together
getAnInstancePre: "Don't have an",
getAnInstanceText: 'instance',
getAnInstanceDescription: 'An instance is your Mastodon home server, such as mastodon.social or cybre.space.',
getAnInstancePost: '?',
joinMastodon: 'Join Mastodon!',
instancesYouveLoggedInTo: "Instances you've logged in to:",
addAnotherInstance: 'Add another instance',
youreNotLoggedIn: "You're not logged in to any instances.",
currentInstanceLabel: `{instance} {current, select,
true {(current instance)}
other {}
}`,
// Link text
logInToAnInstancePre: '',
logInToAnInstanceText: 'Log in to an instance',
logInToAnInstancePost: 'to start using Pinafore.',
// Another custom tooltip
showRingPre: 'Always show',
showRingText: 'focus ring',
showRingDescription: `The focus ring is the outline showing the currently focused element. By default, it's only
shown when using the keyboard (not mouse or touch), but you may choose to always show it.`,
showRingPost: '',
instances: 'Instances',
addInstance: 'Add instance',
homeTimelineFilterSettings: 'Home timeline filter settings',
showReblogs: 'Show boosts',
showReplies: 'Show replies',
switchOrLogOut: 'Switch to or log out of this instance',
switchTo: 'Switch to this instance',
switchToInstance: 'Switch to instance',
switchToNameOfInstance: 'Switch to {instance}',
logOut: 'Log out',
logOutOfInstanceConfirm: 'Log out of {instance}?',
notificationFilterSettings: 'Notification filter settings',
// Push notifications
browserDoesNotSupportPush: "Your browser doesn't support push notifications.",
deniedPush: 'You have denied permission to show notifications.',
pushNotificationsNote: 'Note that you can only have push notifications for one instance at a time.',
pushSettings: 'Push notification settings',
newFollowers: 'New followers',
reblogs: 'Boosts',
pollResults: 'Poll results',
subscriptions: 'Subscribed toots',
needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?',
failedToUpdatePush: 'Failed to update push notification settings: {error}',
// Themes
chooseTheme: 'Choose a theme',
darkBackground: 'Dark background',
lightBackground: 'Light background',
themeLabel: `{label} {default, select,
true {(default)}
other {}
}`,
animatedImage: 'Animated image: {description}',
showImage: `Show {animated, select,
true {animated}
other {}
} image: {description}`,
playVideoOrAudio: `Play {audio, select,
true {audio}
other {video}
}: {description}`,
accountFollowedYou: '{name} followed you, {account}',
accountSignedUp: '{name} signed up, {account}',
accountRequestedFollow: '{name} requested to follow you, {account}',
accountReported: '{name} filed a report, {account}',
reblogCountsHidden: 'Boost counts hidden',
favoriteCountsHidden: 'Favorite counts hidden',
rebloggedTimes: `Boosted {count, plural,
one {1 time}
other {{count} times}
}`,
favoritedTimes: `Favorited {count, plural,
one {1 time}
other {{count} times}
}`,
pinnedStatus: 'Pinned toot',
rebloggedYou: 'boosted your toot',
favoritedYou: 'favorited your toot',
followedYou: 'followed you',
edited: 'edited their toot',
requestedFollow: 'requested to follow you',
reported: 'filed a report',
signedUp: 'signed up',
posted: 'posted',
pollYouCreatedEnded: 'A poll you created has ended',
pollYouVotedEnded: 'A poll you voted on has ended',
reblogged: 'boosted',
favorited: 'favorited',
unreblogged: 'unboosted',
unfavorited: 'unfavorited',
showSensitiveMedia: 'Show sensitive media',
hideSensitiveMedia: 'Hide sensitive media',
clickToShowSensitive: 'Sensitive content. Click to show.',
longPost: 'Long post',
// Accessible status labels
accountRebloggedYou: '{account} boosted your toot',
accountFavoritedYou: '{account} favorited your toot',
accountEdited: '{account} edited their toot',
rebloggedByAccount: 'Boosted by {account}',
contentWarningContent: 'Content warning: {spoiler}',
hasMedia: 'has media',
hasPoll: 'has poll',
shortStatusLabel: '{privacy} toot by {account}',
// Privacy types
public: 'Public',
unlisted: 'Unlisted',
followersOnly: 'Followers-only',
direct: 'Direct',
// Themes
themeRoyal: 'Royal',
themeScarlet: 'Scarlet',
themeSeafoam: 'Seafoam',
themeHotpants: 'Hotpants',
themeOaken: 'Oaken',
themeMajesty: 'Majesty',
themeGecko: 'Gecko',
themeGrayscale: 'Grayscale',
themeOzark: 'Ozark',
themeCobalt: 'Cobalt',
themeSorcery: 'Sorcery',
themePunk: 'Punk',
themeRiot: 'Riot',
themeHacker: 'Hacker',
themeMastodon: 'Mastodon',
themePitchBlack: 'Pitch Black',
themeDarkGrayscale: 'Dark Grayscale',
// Polls
voteOnPoll: 'Vote on poll',
pollChoices: 'Poll choices',
vote: 'Vote',
pollDetails: 'Poll details',
refresh: 'Refresh',
expires: 'Ends',
expired: 'Ended',
voteCount: `{count, plural,
one {1 vote}
other {{count} votes}
}`,
// Status interactions
clickToShowThread: '{time} - click to show thread',
showMore: 'Show more',
showLess: 'Show less',
closeReply: 'Close reply',
cannotReblogFollowersOnly: 'Cannot be boosted because this is followers-only',
cannotReblogDirectMessage: 'Cannot be boosted because this is a direct message',
reblog: 'Boost',
reply: 'Reply',
replyToThread: 'Reply to thread',
favorite: 'Favorite',
unfavorite: 'Unfavorite',
// timeline
loadingMore: 'Loading more…',
loadMore: 'Load more',
showCountMore: 'Show {count} more',
nothingToShow: 'Nothing to show.',
// status thread page
statusThreadPage: 'Toot thread page',
status: 'Toot',
// toast messages
blockedAccount: 'Blocked account',
unblockedAccount: 'Unblocked account',
unableToBlock: 'Unable to block account: {error}',
unableToUnblock: 'Unable to unblock account: {error}',
bookmarkedStatus: 'Bookmarked toot',
unbookmarkedStatus: 'Unbookmarked toot',
unableToBookmark: 'Unable to bookmark: {error}',
unableToUnbookmark: 'Unable to unbookmark: {error}',
cannotPostOffline: 'You cannot post while offline',
unableToPost: 'Unable to post toot: {error}',
statusDeleted: 'Toot deleted',
unableToDelete: 'Unable to delete toot: {error}',
cannotFavoriteOffline: 'You cannot favorite while offline',
cannotUnfavoriteOffline: 'You cannot unfavorite while offline',
unableToFavorite: 'Unable to favorite: {error}',
unableToUnfavorite: 'Unable to unfavorite: {error}',
followedAccount: 'Followed account',
unfollowedAccount: 'Unfollowed account',
unableToFollow: 'Unable to follow account: {error}',
unableToUnfollow: 'Unable to unfollow account: {error}',
accessTokenRevoked: 'The access token was revoked, logged out of {instance}',
loggedOutOfInstance: 'Logged out of {instance}',
failedToUploadMedia: 'Failed to upload media: {error}',
mutedAccount: 'Muted account',
unmutedAccount: 'Unmuted account',
unableToMute: 'Unable to mute account: {error}',
unableToUnmute: 'Unable to unmute account: {error}',
mutedConversation: 'Muted conversation',
unmutedConversation: 'Unmuted conversation',
unableToMuteConversation: 'Unable to mute conversation: {error}',
unableToUnmuteConversation: 'Unable to unmute conversation: {error}',
unpinnedStatus: 'Unpinned toot',
unableToPinStatus: 'Unable to pin toot: {error}',
unableToUnpinStatus: 'Unable to unpin toot: {error}',
unableToRefreshPoll: 'Unable to refresh poll: {error}',
unableToVoteInPoll: 'Unable to vote in poll: {error}',
cannotReblogOffline: 'You cannot boost while offline.',
cannotUnreblogOffline: 'You cannot unboost while offline.',
failedToReblog: 'Failed to boost: {error}',
failedToUnreblog: 'Failed to unboost: {error}',
submittedReport: 'Submitted report',
failedToReport: 'Failed to report: {error}',
approvedFollowRequest: 'Approved follow request',
rejectedFollowRequest: 'Rejected follow request',
unableToApproveFollowRequest: 'Unable to approve follow request: {error}',
unableToRejectFollowRequest: 'Unable to reject follow request: {error}',
searchError: 'Error during search: {error}',
hidDomain: 'Hid domain',
unhidDomain: 'Unhid domain',
unableToHideDomain: 'Unable to hide domain: {error}',
unableToUnhideDomain: 'Unable to unhide domain: {error}',
showingReblogs: 'Showing boosts',
hidingReblogs: 'Hiding boosts',
unableToShowReblogs: 'Unable to show boosts: {error}',
unableToHideReblogs: 'Unable to hide boosts: {error}',
unableToShare: 'Unable to share: {error}',
unableToSubscribe: 'Unable to subscribe: {error}',
unableToUnsubscribe: 'Unable to unsubscribe: {error}',
showingOfflineContent: 'Internet request failed. Showing offline content.',
youAreOffline: 'You seem to be offline. You can still read toots while offline.',
// Snackbar UI
updateAvailable: 'App update available.',
// Word/phrase filters
wordFilters: 'Word filters',
noFilters: 'You don\'t have any word filters.',
wordOrPhrase: 'Word or phrase',
contexts: 'Contexts',
addFilter: 'Add filter',
editFilter: 'Edit filter',
filterHome: 'Home and lists',
filterNotifications: 'Notifications',
filterPublic: 'Public timelines',
filterThread: 'Conversations',
filterAccount: 'Profiles',
filterUnknown: 'Unknown',
expireAfter: 'Expire after',
whereToFilter: 'Where to filter',
irreversible: 'Irreversible',
wholeWord: 'Whole word',
save: 'Save',
updatedFilter: 'Updated filter',
createdFilter: 'Created filter',
failedToModifyFilter: 'Failed to modify filter: {error}',
deletedFilter: 'Deleted filter',
required: 'Required',
// Dialogs
profileOptions: 'Profile options',
copyLink: 'Copy link',
emoji: 'Emoji',
editMedia: 'Edit media',
shortcutHelp: 'Shortcut help',
statusOptions: 'Status options',
confirm: 'Confirm',
closeDialog: 'Close dialog',
postPrivacy: 'Post privacy',
homeOnInstance: 'Home on {instance}',
statusesTimelineOnInstance: 'Statuses: {timeline} timeline on {instance}',
statusesHashtag: 'Statuses: #{hashtag} hashtag',
statusesThread: 'Statuses: thread',
statusesAccountTimeline: 'Statuses: account timeline',
statusesList: 'Statuses: list',
notificationsOnInstance: 'Notifications on {instance}'
}

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

627
src/intl/fr.js 100644
Wyświetl plik

@ -0,0 +1,627 @@
export default {
// Home page, basic <title> and <description>
appName: 'Pinafore',
appDescription: 'Un client alternatif pour Mastodon, concentré sur la vitesse et la simplicité',
homeDescription: `
<p>
Pinafore est un client web pour
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
dessiné pour la vitesse et la simplicité.
</p>
<p>
Lire
<a rel="noopener" target="_blank"
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">l'article introductoire (anglais)</a>,
ou se connecter à une instance:
</p>`,
logIn: 'Se connecter',
footer: `
<p>
Pinafore est
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">logiciel open-source</a>
créé par
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
et distribué sous la
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License AGPL</a>.
Lire la <a href="/settings/about#privacy-policy" rel="prefetch">politique de confidentialité</a>.
</p>
`,
// Generic UI
loading: 'Chargement en cours',
okay: 'OK',
cancel: 'Annuler',
alert: 'Alerte',
close: 'Fermer',
error: 'Erreur: {error}',
errorShort: 'Erreur:',
// Relative timestamps
justNow: 'il y a un moment',
// Navigation, page titles
navItemLabel: `
{label} {selected, select,
true {(page actuelle)}
other {}
} {name, select,
notifications {{count, plural,
=0 {}
one {(1 notification)}
other {({count} notifications)}
}}
community {{count, plural,
=0 {}
one {(1 demande de suivre)}
other {({count} demandes de suivre)}
}}
other {}
}
`,
blockedUsers: 'Utilisateurs bloqués',
bookmarks: 'Signets',
directMessages: 'Messages directs',
favorites: 'Favoris',
federated: 'Fédéré',
home: 'Accueil',
local: 'Local',
notifications: 'Notifications',
mutedUsers: 'Utilisateurs mis en sourdine',
pinnedStatuses: 'Pouets épinglés',
followRequests: 'Demandes de suivre',
followRequestsLabel: `Demandes de suivre {hasFollowRequests, select,
true {({count})}
other {}
}`,
list: 'Liste',
search: 'Recherche',
pageHeader: 'Titre de page',
goBack: 'Rentrer',
back: 'Rentrer',
profile: 'Profil',
federatedTimeline: 'Historique fédéré',
localTimeline: 'Historique local',
// community page
community: 'Communauté',
pinnableTimelines: 'Historiques épinglables',
timelines: 'Historiques',
lists: 'Listes',
instanceSettings: "Paramètres d'instance",
notificationMentions: 'Notifications de mention',
profileWithMedia: 'Profil avec medias',
profileWithReplies: 'Profil avec réponses',
hashtag: 'Mot-dièse',
// not logged in
profileNotLoggedIn: "Un historique d'utilisateur s'apparêtra ici quand on est conncté.",
bookmarksNotLoggedIn: "Vos signets s'apparêtront ici quand on est conncté.",
directMessagesNotLoggedIn: "Vos messages directes s'apparêtront ici quand on est conncté.",
favoritesNotLoggedIn: "Vos favoris s'apparêtront ici quand on est conncté.",
federatedTimelineNotLoggedIn: "L'historique fédéré s'apparêtra ici quand on est conncté.",
localTimelineNotLoggedIn: "L'historique local s'apparêtra ici quand on est conncté.",
searchNotLoggedIn: "On peut rechercher dès qu'on est conncté.",
communityNotLoggedIn: "Les paramètres de commnautés s'apparêtront ici quand on est conncté.",
listNotLoggedIn: "Une liste s'apparêtra ici dès qu'on est conncté.",
notificationsNotLoggedIn: "Vos notifications s'apparêtront ici quand on est conncté.",
notificationMentionsNotLoggedIn: "Vos notifications de mention s'apparêtront ici quand on est conncté.",
statusNotLoggedIn: "Un historique de pouet s'apparêtra ici quand on est conncté.",
tagNotLoggedIn: "Un historique de mot-dièse s'apparêtra ici quand on est conncté.",
// Notification subpages
filters: 'Filtres',
all: 'Tous',
mentions: 'Mentions',
// Follow requests
approve: 'Accepter',
reject: 'Rejeter',
// Hotkeys
hotkeys: 'Raccourcis clavier',
global: 'Global',
timeline: 'Historique',
media: 'Medias',
globalHotkeys: `
{leftRightChangesFocus, select,
true {
<li><kbd></kbd> pour changer de focus à l'élément suivant</li>
<li><kbd></kbd> pour changer de focus à l'élément précédent</li>
}
other {}
}
<li>
<kbd>1</kbd> - <kbd>6</kbd>
{leftRightChangesFocus, select,
true {}
other {ou <kbd></kbd>/<kbd></kbd>}
}
pour changer de pages
</li>
<li><kbd>7</kbd> or <kbd>c</kbd> pour écrire un nouveau pouet</li>
<li><kbd>s</kbd> or <kbd>/</kbd> pour rechercher</li>
<li><kbd>g</kbd> + <kbd>h</kbd> pour renter à l'acceuil</li>
<li><kbd>g</kbd> + <kbd>n</kbd> pour voir les notifications</li>
<li><kbd>g</kbd> + <kbd>l</kbd> pour voir l'historique local</li>
<li><kbd>g</kbd> + <kbd>t</kbd> pour voir l'historique fédéré</li>
<li><kbd>g</kbd> + <kbd>c</kbd> pour voir les paramètres de communauté</li>
<li><kbd>g</kbd> + <kbd>d</kbd> pour voir les messages directs</li>
<li><kbd>h</kbd> ou <kbd>?</kbd> pour voir les raccourcis clavier</li>
<li><kbd>Retour arrière</kbd> pour rentrer à la page précédente, ou fermer une boite de dialogue</li>
`,
timelineHotkeys: `
<li><kbd>j</kbd> ou <kbd></kbd> pour activer le pouet suivant</li>
<li><kbd>k</kbd> ou <kbd></kbd> pour activer le pouet précedent</li>
<li><kbd>.</kbd> pour afficher les nouveaus messages et renter en haut</li>
<li><kbd>o</kbd> pour ouvrir</li>
<li><kbd>f</kbd> pour ajouter aux favoris</li>
<li><kbd>b</kbd> pour partager</li>
<li><kbd>r</kbd> pour répondre</li>
<li><kbd>i</kbd> pour voir une image, vidéo, ou audio</li>
<li><kbd>y</kbd> pour afficher ou cacher une image sensible</li>
<li><kbd>m</kbd> pour mentionner l'auteur</li>
<li><kbd>p</kbd> pour voir le profile de l'auteur</li>
<li><kbd>l</kbd> pour ouvrir un lien de carte dans un nouvel onglet</li>
<li><kbd>x</kbd> pour afficher ou cacher le texte caché derrière une avertissement</li>
<li><kbd>z</kbd> pour afficher ou cacher toutes les avertissements</li>
`,
mediaHotkeys: `
<li><kbd></kbd> / <kbd></kbd> pour voir la prochaine ou dernière image</li>
`,
// Community page, tabs
tabLabel: `{label} {current, select,
true {(Actuel)}
other {}
}`,
pageTitle: `
{hasNotifications, select,
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
`,
pinLabel: `{label} {pinnable, select,
true {
{pinned, select,
true {(Page épinglée)}
other {(Page non-épinglée)}
}
}
other {}
}`,
pinPage: 'Epingler {label}',
// Status composition
composeStatus: 'Ecrire un pouet',
postStatus: 'Pouet!',
contentWarning: 'Avertissement',
dropToUpload: 'Déposer',
invalidFileType: "Impossible d'uploader ce type de fichier",
composeLabel: "Qu'avez vous en tête?",
autocompleteDescription: 'Quand les résultats sont dispibles, appuyez la fleche vers le haut ou vers le bas pour selectionner.',
mediaUploads: 'Medias uploadés',
edit: 'Rediger',
delete: 'Supprimer',
description: 'Déscription',
descriptionLabel: 'Décrire pour les aveugles (image, video) ou les sourds (audio, video)',
markAsSensitive: 'Désigner comme sensible',
// Polls
createPoll: 'Créer une enquête',
removePollChoice: 'Supprimer la choix {index}',
pollChoiceLabel: 'Choix {index}',
multipleChoice: 'Choix multiple',
pollDuration: "Duration de l'enquête",
fiveMinutes: '5 minutes',
thirtyMinutes: '30 minutes',
oneHour: '1 heure',
sixHours: '6 heures',
oneDay: '1 jour',
threeDays: '3 jours',
sevenDays: '7 jours',
addEmoji: 'Insérer un emoji',
addMedia: 'Ajouter un media (images, vidéos, audios)',
addPoll: 'Ajouter une enquête',
removePoll: "Enlever l'enquête",
postPrivacyLabel: 'Changer de confidentialité (actuellement {label})',
addContentWarning: 'Ajouter une avertissement',
removeContentWarning: "Enlever l'avertissement",
altLabel: 'Décrire pour les aveugles ou les sourds',
extractText: "Extraire le texte de l'image",
extractingText: 'Extraction de texte en cours…',
extractingTextCompletion: 'Extraction de texte en cours ({percent}% finit)…',
unableToExtractText: "Impossible d'extraire le texte.",
// Account options
followAccount: 'Suivre {account}',
unfollowAccount: 'Ne plus suivre {account}',
blockAccount: 'Bloquer {account}',
unblockAccount: 'Ne plus bloquer {account}',
muteAccount: 'Mettre {account} en sourdine',
unmuteAccount: 'Ne plus mettre {account} en sourdine',
showReblogsFromAccount: 'Afficher les partages de {account}',
hideReblogsFromAccount: 'Ne plus afficher les partages de {account}',
showDomain: 'Ne plus cacher {domain}',
hideDomain: 'Cacher {domain}',
reportAccount: 'Signaler {account}',
mentionAccount: 'Mentionner {account}',
copyLinkToAccount: 'Copier un lien vers ce compte',
copiedToClipboard: 'Copié vers le presse-papiers',
// Media dialog
navigateMedia: 'Changer de medias',
showPreviousMedia: 'Afficher le media précédent',
showNextMedia: 'Afficher le media suivant',
enterPinchZoom: 'Pincer pour zoomer',
exitPinchZoom: 'Ne plus pincer pour zoomer',
showMedia: `Afficher le {index, select,
1 {premier}
2 {deuxième}
3 {troisième}
other {quatrième}
} média {current, select,
true {(actuel)}
other {}
}`,
previewFocalPoint: 'Aperçu (point de mire)',
enterFocalPoint: 'Saisir le point de mire (X, Y) pour ce média',
muteNotifications: 'Mettre aussi bien les notifications en sourdine',
muteAccountConfirm: 'Mettre {account} en sourdine?',
mute: 'Mettre en sourdine',
unmute: 'Ne plus mettre en sourdine',
zoomOut: 'Dé-zoomer',
zoomIn: 'Zoomer',
// Reporting
reportingLabel: 'Vous signalez {account} aux modérateurs/modératrices de {instance}.',
additionalComments: 'Commentaires additionels',
forwardDescription: 'Faire parvenir aux modérateurs/modératrices de {instance} aussi?',
forwardLabel: 'Fair pervenir à {instance}',
unableToLoadStatuses: 'Impossible de charger les pouets récents: {error}',
report: 'Signaler',
noContent: '(Pas de contenu)',
noStatuses: 'Aucun pouet à signaler',
// Status options
unpinFromProfile: 'Ne plus épingler sur son profil',
pinToProfile: 'Epingler sur son profil',
muteConversation: 'Mettre en sourdine la conversation',
unmuteConversation: 'Ne plus mettre en sourdine la conversation',
bookmarkStatus: 'Ajouter aux signets',
unbookmarkStatus: 'Enlever des signets',
deleteAndRedraft: 'Supprimer et rediger',
reportStatus: 'Signaler ce pouet',
shareStatus: 'Partager ce pouet externellement',
copyLinkToStatus: 'Copier un lien vers ce pouet',
// Account profile
profileForAccount: 'Profil pour {account}',
statisticsAndMoreOptions: "Statistiques et plus d'options",
statuses: 'Pouets',
follows: 'Suis',
followers: 'Suivants',
moreOptions: "Plus d'options",
followersLabel: 'Suivi(e) par {count}',
followingLabel: 'Suis {count}',
followLabel: `Suivre {requested, select,
true {(suivre demandé)}
other {}
}`,
unfollowLabel: `Ne plus suivre {requested, select,
true {(suivre demandé)}
other {}
}`,
unblock: 'Ne plus bloquer',
nameAndFollowing: 'Nom et suivants',
clickToSeeAvatar: "Cliquer pour voir l'image de profile",
opensInNewWindow: '{label} (ouvrir dans un nouvel onglet)',
blocked: 'Bloquer',
domainHidden: 'Domaine bloqué',
muted: 'Mis en sourdine',
followsYou: 'Suivant',
avatarForAccount: 'Image de profil pour {account}',
fields: 'Champs',
accountHasMoved: '{account} a déménagé',
profilePageForAccount: 'Page de profil pour {account}',
// About page
about: 'Infos',
aboutApp: 'Infos sur Pinafore',
aboutAppDescription: `
<p>
Pinafore est un logiciel
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore">gratuit et open-source</a>
créé par
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
et distribué sous le
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License GNU Affero General Public (AGPL)</a>.
</p>
<h2 id="privacy-policy">Politique de confidentialité</h2>
<p>
Pinafore ne garde pas d'informations personelles dans ses serveurs,
y compris les noms, addresses courriel, addresses IP, messages, et photos.
</p>
<p>
Pinafore est un site statique. Tous données sont gardées en locale dans le navigateur, et sont partagée qu'avec
les instances auxquelles vous vous connectez.
</p>
<h2>Crédits</h2>
<p>
Icônes par <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
</p>
<p>
Logo grâce à Gregor Cresnar du
<a rel="noopener" target="_blank" href="https://thenounproject.com/">Noun Project</a>.
</p>`,
// Settings
settings: 'Paramètres',
general: 'Général',
generalSettings: 'Paramètres générales',
showSensitive: 'Afficher les medias sensible par défaut',
showPlain: 'Afficher un simple gris pour les medias sensibles',
allSensitive: 'Considérer tous medias comme sensible',
largeMedia: 'Afficher de plus grands images et vidéos',
autoplayGifs: 'Repasser automatiquement les GIFs animés',
hideCards: 'Cacher les liens «cartes»',
underlineLinks: 'Souligner les liens dans les pouets et profils',
accessibility: 'Accessibilité',
reduceMotion: 'Reduire la motions dans les animations',
disableTappable: "Désactiver l'espace touchable sur un pouet entier",
removeEmoji: "Enlever les emojis des noms d'utilisateur",
shortAria: 'Utiliser des etiquettes courtes ARIA',
theme: 'Thème',
themeForInstance: 'Theème pour {instance}',
disableCustomScrollbars: 'Désactiver les scrollbars customisés',
preferences: 'Préférences',
hotkeySettings: 'Paramètres de raccourcis clavier',
disableHotkeys: 'Désactiver les raccourcis clavier',
leftRightArrows: 'Les flèches gauche/droit change de focus plutôt que les pages',
guide: 'Guide',
reload: 'Recharger',
// Wellness settings
wellness: 'Bien-être',
wellnessSettings: 'Paramètres de bien-être',
wellnessDescription: `Les paramètres de bien-être sont dessinées pour rédruire les effets accrochants ou d'anxiété des réseaux sociaux.
Veuillez choisir les options qui marchent pour vous.`,
enableAll: 'Activer tous',
metrics: 'Métrics',
hideFollowerCount: 'Cacher le nombre de suivants (10 maximum)',
hideReblogCount: 'Cacher le nombre de partages',
hideFavoriteCount: 'Cacher le nombre de favoris',
hideUnread: "Cacher le nombre de notifications (c'est-à-dire le point rouge)",
ui: 'Interface Utilisateur',
grayscaleMode: 'Mode echelle de gris',
wellnessFooter: `Ces paramètres sont basé sur les recommendations du
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
// This is a link: "You can filter or disable notifications in the _instance settings_"
filterNotificationsPre: 'Vous pouvez filtrer ou désactiver les notifications dans les',
filterNotificationsText: "paramètres d'instance",
filterNotificationsPost: '',
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
// to see a description. It's hard to properly internationalize, so we just break up the strings.
disableInfiniteScrollPre: 'Désactiver le',
disableInfiniteScrollText: 'défilage infini',
disableInfiniteScrollDescription: `Quand le défilage infini est désactivé, les pouets nouveau ne
s'apparêtront pas automatique au haut ou au bas de l'historique. Plutôt, il y aura des boutons pour
charger sur demande.`,
disableInfiniteScrollPost: '',
// Instance settings
loggedInAs: 'Connecté en tant que',
homeTimelineFilters: "Filtres d'historique de l'acceuil",
notificationFilters: 'Filtres de notifications',
pushNotifications: 'Filtres de notifications push',
// Add instance page
storageError: `Il semble que Pinafore ne peut pas stocker les données en locale. Est-ce que votre navigateur
est en mode privé, ou est-ce qu'il bloque les cookies? Pinafore garde tous ses données en locale et
ne peut pas fonctionner sans LocalStorage ou IndexedDB.`,
javaScriptError: 'Le JavaScript devrait être activé pour continuer.',
enterInstanceName: "Saisir le nom d'instance",
instanceColon: 'Instance:',
// Custom tooltip, concatenated together
getAnInstancePre: "N'avez-vous pas d'",
getAnInstanceText: 'instance',
getAnInstanceDescription: 'Une instance est votre serveur Mastodon, par exemple mastodon.social ou cybre.space.',
getAnInstancePost: '?',
joinMastodon: 'Joignez-vous à Mastodon!',
instancesYouveLoggedInTo: 'Instances conntectées:',
addAnotherInstance: 'Ajouter une nouvelle instance',
youreNotLoggedIn: 'Vous êtes connecté(e) à aucune instance.',
currentInstanceLabel: `{instance} {current, select,
true {(instance actuelle)}
other {}
}`,
// Link text
logInToAnInstancePre: '',
logInToAnInstanceText: 'Se connecter à une instance',
logInToAnInstancePost: 'pour utiliser Pinafore.',
// Another custom tooltip
showRingPre: 'Afficher toujours',
showRingText: "l'anneau de focus",
showRingDescription: `L'anneau de focus est le contour qui indique l'élément en focus actuel. Par défaut, ce n'est
affiché que quand on utilise le clavier (et ne pas la souris ou l'écran touche), mais vous pouvez choisr de
l'afficher toujours.`,
showRingPost: '',
instances: 'Les instances',
addInstance: 'Ajouter une instance',
homeTimelineFilterSettings: "Paramètres de filtre d'historique",
showReblogs: 'Afficher les partages',
showReplies: 'Afficher les réponses',
switchOrLogOut: 'Changer ou se déconnecter de cette instance',
switchTo: "Changer d'instance à celle-ci",
switchToInstance: "Changer d'instance",
switchToNameOfInstance: "Faire {instance} l'instance actuelle",
logOut: 'Se déconnecter',
logOutOfInstanceConfirm: 'Déconnectez-vous de {instance}?',
notificationFilterSettings: 'Paramètres de filtre de notifications',
// Push notifications
browserDoesNotSupportPush: 'Votre navigateur ne soutient pas les notifications push.',
deniedPush: 'Vous avez désactivé les notifications push.',
pushNotificationsNote: 'Veuillez noter que les notifications push ne peuvent être activées que pour une instance à la fois.',
pushSettings: 'Paramètres de notifications push',
newFollowers: 'Suivants nouveaux',
reblogs: 'Partages',
pollResults: "Résultats d'enquête",
needToReauthenticate: 'Vous devez ré-authentiquer pour activer les notifications push. Déconnectez-vous de {instance}?',
failedToUpdatePush: 'Impossible de mettre à jour les paramètres de notifications push: {error}',
// Themes
chooseTheme: 'Choisir une thème',
darkBackground: 'Sombre',
lightBackground: 'Clair',
themeLabel: `{label} {default, select,
true {(défaut)}
other {}
}`,
animatedImage: 'Image animée: {description}',
showImage: `Afficher l'image {animated, select,
true {animée}
other {}
}: {description}`,
playVideoOrAudio: `Repasser {audio, select,
true {l'audio}
other {la vidéo}
}: {description}`,
accountFollowedYou: '{name} vous a suivi(e), {account}',
reblogCountsHidden: 'Nombre de partages caché',
favoriteCountsHidden: 'nombre de mises en favori caché',
rebloggedTimes: `Partagé {count, plural,
one {une fois}
other {{count} fois}
}`,
favoritedTimes: `Mis en favori {count, plural,
one {une fois}
other {{count} fois}
}`,
pinnedStatus: 'Pouet épinglé',
rebloggedYou: 'a partagé votre pouet',
favoritedYou: 'a mis en favori votre pouet',
followedYou: 'followed you',
pollYouCreatedEnded: 'Une enquête vous avez créée a terminée',
pollYouVotedEnded: 'Une enquête dans laquelle vous avez voté a terminée',
reblogged: 'partagé',
showSensitiveMedia: 'Afficher la média sensible',
hideSensitiveMedia: 'Cacher la média sensible',
clickToShowSensitive: 'Image sensible. Cliquer pour afficher.',
longPost: 'Pouet long',
// Accessible status labels
accountRebloggedYou: '{account} a partagé votre pouet',
accountFavoritedYou: '{account} a mis votre pouet en favori',
rebloggedByAccount: 'Partagé par {account}',
contentWarningContent: 'Avertissement: {spoiler}',
hasMedia: 'média',
hasPoll: 'enquête',
shortStatusLabel: 'Pouet {privacy} par {account}',
// Privacy types
public: 'Publique',
unlisted: 'Non listé',
followersOnly: 'Abonnés/abonnées uniquement',
direct: 'Direct',
// Themes
themeRoyal: 'Royale',
themeScarlet: 'Ecarlate',
themeSeafoam: 'Ecume',
themeHotpants: 'Hotpants',
themeOaken: 'Chêne',
themeMajesty: 'Majesté',
themeGecko: 'Gecko',
themeGrayscale: 'Echelle gris',
themeOzark: 'Ozark',
themeCobalt: 'Cobalt',
themeSorcery: 'Sorcellerie',
themePunk: 'Punk',
themeRiot: 'Riot',
themeHacker: 'Hacker',
themeMastodon: 'Mastodon',
themePitchBlack: 'Noir complet',
themeDarkGrayscale: 'Echelle gris sombre',
// Polls
voteOnPoll: 'Voter dans cette enquête',
pollChoices: 'Choix',
vote: 'Voter',
pollDetails: 'Détails',
refresh: 'Recharger',
expires: 'Se termine',
expired: 'Terminée',
voteCount: `{count, plural,
one {1 vote}
other {{count} votes}
}`,
// Status interactions
clickToShowThread: '{time} - cliquer pour afficher le discussion',
showMore: 'Afficher plus',
showLess: 'Afficher moins',
closeReply: 'Fermer la réponse',
cannotReblogFollowersOnly: "Impossible de partager car ce pouet n'est que pour les abonné(e)s",
cannotReblogDirectMessage: 'Impossible de partager car ce pouet est direct',
reblog: 'Partager',
reply: 'Répondre',
replyToThread: 'Répondre au discussion',
favorite: 'Mettre en favori',
unfavorite: 'Ne plus mettre en favori',
// timeline
loadingMore: 'Chargement en cours…',
loadMore: 'Charger plus',
showCountMore: 'Afficher {count} de plus',
nothingToShow: 'Rien à afficher.',
// status thread page
statusThreadPage: 'Page de discussion',
status: 'Pouet',
// toast messages
blockedAccount: 'Compte bloqué',
unblockedAccount: 'Compte ne plus bloqué',
unableToBlock: 'Impossible de bloquer ce compte: {error}',
unableToUnblock: 'Impossible de ne plus bloquer ce compte: {error}',
bookmarkedStatus: 'Ajouté aux signets',
unbookmarkedStatus: 'Enlever des signets',
unableToBookmark: "Impossible d'ajouter aux signets: {error}",
unableToUnbookmark: "Impossible d'enlever des signets: {error}",
cannotPostOffline: 'Vous ne pouvez pas poueter car vous êtes hors connexion',
unableToPost: 'Impossible de poueter: {error}',
statusDeleted: 'Pouet supprimé',
unableToDelete: 'Impossible de supprimer: {error}',
cannotFavoriteOffline: 'Vous ne pouvez pas mettre en favori car vous êtes hors connexion',
cannotUnfavoriteOffline: 'Vous ne pouvez pas enlever des favoris car vous êtes hors connexion',
unableToFavorite: 'Impossible de mettre en favori: {error}',
unableToUnfavorite: "Impossible d'enlever des favoris: {error}",
followedAccount: 'Compte suivi',
unfollowedAccount: 'Compte ne plus suivi',
unableToFollow: 'Impossible de suivre: {error}',
unableToUnfollow: 'Impossible de ne plus suivre: {error}',
accessTokenRevoked: 'Authentication revoquée, déconnecté de {instance}',
loggedOutOfInstance: 'Déconnecté de {instance}',
failedToUploadMedia: "Impossible d'uploader: {error}",
mutedAccount: 'Compte mis en sourdine',
unmutedAccount: 'Compte ne plus mis en sourdine',
unableToMute: 'Impossible de mettre en sourdine: {error}',
unableToUnmute: 'Impossible de plus mettre en sourdine: {error}',
mutedConversation: 'Conversation mis en sourdine',
unmutedConversation: 'Conversation ne plus mis en sourdine',
unableToMuteConversation: 'Impossible de mettre en sourdine: {error}',
unableToUnmuteConversation: 'Impossible de ne plus mettre en sourdine: {error}',
unpinnedStatus: 'Pouet ne plus épinglé',
unableToPinStatus: "Impossible d'épingler: {error}",
unableToUnpinStatus: 'Impossible de ne plus épingler: {error}',
unableToRefreshPoll: 'Impossible de recharger: {error}',
unableToVoteInPoll: 'Impossible de voter: {error}',
cannotReblogOffline: 'Vous ne pouvez pas partager car vous êtes hors de connexion.',
cannotUnreblogOffline: 'Vous ne pouvez pas ne plus partager car vous êtes hors de connexion.',
failedToReblog: 'Impossible de partager: {error}',
failedToUnreblog: 'Impossible de ne plus partager: {error}',
submittedReport: 'Report signalé',
failedToReport: 'Impossible de signaler: {error}',
approvedFollowRequest: 'Demande de suivre approuvée',
rejectedFollowRequest: 'Demande de suivre rejetée',
unableToApproveFollowRequest: "Impossible d'appouver: {error}",
unableToRejectFollowRequest: 'Impossible de rejeter: {error}',
searchError: 'Erreur de recherche: {error}',
hidDomain: 'Domaine cachée',
unhidDomain: 'Domaine ne plus cachée',
unableToHideDomain: 'Impossible de cacher la domaine: {error}',
unableToUnhideDomain: 'Imipossible de ne plus cacher la domaine: {error}',
showingReblogs: 'Partages affichés',
hidingReblogs: 'Partages ne plus affichés',
unableToShowReblogs: "Impossible d'afficher les partages: {error}",
unableToHideReblogs: 'Impossible de ne plus afficher les partages: {error}',
unableToShare: 'Impossible de partager externellement: {error}',
showingOfflineContent: "Requête d'internet impossible. Contenu hors de connexion affiché.",
youAreOffline: 'Il semble que vous êtes hors de connextion. Vous pouvez toujours lire les pouets dans cet état.',
// Snackbar UI
updateAvailable: 'Mise à jour disponible.'
}

690
src/intl/ru-RU.JS 100644
Wyświetl plik

@ -0,0 +1,690 @@
export default {
// Home page, basic <title> and <description>
appName: 'Pinafore',
appDescription: 'Альтернативный веб-клиент для Mastodon, ориентированный на скорость и простоту.',
homeDescription: `
<p>
Pinafore — веб-клиент для
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
разработан для скорости и простоты.
</p>
<p>
Прочитайте
<a rel="noopener" target="_blank"
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">вводную запись в блоге</a>,
или начните работу, войдя в инстанс:
</p>`,
logIn: 'Войти',
footer: `
<p>
Pinafore — это
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">программное обеспечение с открытым исходным кодом</a>
созданное
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
и распространяемое под лицензией
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>.
Здесь <a href="/settings/about#privacy-policy" rel="prefetch">политика конфиденциальности</a>.
</p>
`,
// Manifest
longAppName: 'Pinafore для Mastodon',
newStatus: 'Новая запись',
// Generic UI
loading: 'Загрузка',
okay: 'OK',
cancel: 'Отмена',
alert: 'Оповещение',
close: 'Закрыть',
error: 'Ошибка: {error}',
errorShort: 'Ошибка:',
// Relative timestamps
justNow: 'только что',
// Navigation, page titles
navItemLabel: `
{label} {selected, select,
true {(current page)}
other {}
} {name, select,
notifications {{count, plural,
=0 {}
one {(1 notification)}
other {({count} notifications)}
}}
community {{count, plural,
=0 {}
one {(1 follow request)}
other {({count} follow requests)}
}}
other {}
}
`,
blockedUsers: 'Заблокированные пользователи',
bookmarks: 'Закладки',
directMessages: 'Личные сообщения',
favorites: 'Избранное',
federated: 'Федеративное',
home: 'Главная',
local: 'Локальная',
notifications: 'Уведомления',
mutedUsers: 'Игнорируемые пользователи',
pinnedStatuses: 'Закрепленные записи',
followRequests: 'Запросы на подписку',
followRequestsLabel: `Запросы на подписку {hasFollowRequests, select,
true {({count})}
other {}
}`,
list: 'Список',
search: 'Поиск',
pageHeader: 'Заголовок страницы',
goBack: 'Вернуться назад',
back: 'Назад',
profile: 'Профиль',
federatedTimeline: 'Глобальная лента',
localTimeline: 'Локальная лента',
// community page
community: 'Сообщество',
pinnableTimelines: 'Закрепляемые ленты',
timelines: 'Ленты',
lists: 'Списки',
instanceSettings: 'Настройки инстанса',
notificationMentions: 'Уведомление упоминаний',
profileWithMedia: 'Профиль с медиа',
profileWithReplies: 'Профиль с ответами',
hashtag: 'Хэштег',
// not logged in
profileNotLoggedIn: 'При входе в систему здесь появится лента пользователя.',
bookmarksNotLoggedIn: 'Ваши закладки появятся здесь после входа в систему.',
directMessagesNotLoggedIn: 'Ваши личные сообщения будут отображаться здесь после входа в систему.',
favoritesNotLoggedIn: 'Ваше избранное появится здесь после входа в систему.',
federatedTimelineNotLoggedIn: 'Ваша глобальная лента появится здесь после входа в систему.',
localTimelineNotLoggedIn: 'Ваша локальная лента появится здесь после входа в систему.',
searchNotLoggedIn: 'Вы можете выполнять поиск после входа в инстанс.',
communityNotLoggedIn: 'Параметры сообщества появится здесь при входе в систему.',
listNotLoggedIn: 'Список появится здесь после входа в систему.',
notificationsNotLoggedIn: 'Ваши уведомления будут отображаться здесь после входа в систему.',
notificationMentionsNotLoggedIn: 'Ваши уведомления с упоминаниями будут отображаться здесь после входа в систему.',
statusNotLoggedIn: 'При входе в систему здесь появится тред сообщений.',
tagNotLoggedIn: 'При входе в систему здесь появится лента с хэштегом.',
// Notification subpages
filters: 'Фильтры',
all: 'Все',
mentions: 'Упоминания',
// Follow requests
approve: 'Одобрить',
reject: 'Отклонить',
// Hotkeys
hotkeys: 'Горячие клавиши',
global: 'Глобальная',
timeline: 'Лента',
media: 'Медиа',
globalHotkeys: `
{leftRightChangesFocus, select,
true {
<li><kbd>→</kbd> перейти к следующему элементу</li>
<li><kbd>←</kbd> перейти к предыдущему элементу</li>
}
other {}
}
<li>
<kbd>1</kbd> - <kbd>6</kbd>
{leftRightChangesFocus, select,
true {}
other {или <kbd>←</kbd>/<kbd>→</kbd>}
}
переключение столбцов
</li>
<li><kbd>7</kbd> или <kbd>c</kbd> создать запись</li>
<li><kbd>s</kbd> или <kbd>/</kbd> искать</li>
<li><kbd>g</kbd> + <kbd>h</kbd> главная</li>
<li><kbd>g</kbd> + <kbd>n</kbd> уведомления</li>
<li><kbd>g</kbd> + <kbd>l</kbd> локальная лента</li>
<li><kbd>g</kbd> + <kbd>t</kbd> глобальная лента</li>
<li><kbd>g</kbd> + <kbd>c</kbd> сообщество</li>
<li><kbd>g</kbd> + <kbd>d</kbd> личные сообщения</li>
<li><kbd>h</kbd> или <kbd>?</kbd> диалог справки</li>
<li><kbd>Backspace</kbd> закрыть диалог, чтобы вернуться назад</li>
`,
timelineHotkeys: `
<li><kbd>j</kbd> или <kbd>↓</kbd> следующая запись</li>
<li><kbd>k</kbd> или <kbd>↑</kbd> предыдущая запись</li>
<li><kbd>.</kbd> показать больше и прокрутить вверх</li>
<li><kbd>o</kbd> открыть</li>
<li><kbd>f</kbd> в избранное</li>
<li><kbd>b</kbd> продвинуть</li>
<li><kbd>r</kbd> ответить</li>
<li><kbd>i</kbd> открыть изображения, видео или аудио</li>
<li><kbd>y</kbd> показать или скрыть деликатное медиа</li>
<li><kbd>m</kbd> упомянуть автора</li>
<li><kbd>p</kbd> открыть профиль автора</li>
<li><kbd>l</kbd> открыть ссылку карточки в новой вкладке</li>
<li><kbd>x</kbd> показать или скрыть текст за предупреждением о содержимом</li>
<li><kbd>z</kbd> показать или скрыть все предупреждения о содержимом в треде</li>
`,
mediaHotkeys: `
<li><kbd>←</kbd> / <kbd>→</kbd> перейти к следующему или предыдущему</li>
`,
// Community page, tabs
tabLabel: `{label} {current, select,
true {(Current)}
other {}
}`,
pageTitle: `
{hasNotifications, select,
true {({count})}
other {}
}
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
·
{name}
`,
pinLabel: `{label} {pinnable, select,
true {
{pinned, select,
true {(Pinned page)}
other {(Unpinned page)}
}
}
other {}
}`,
pinPage: 'Закрепить {label}',
// Status composition
composeStatus: 'Создать запись',
postStatus: 'Опубликовать!',
contentWarning: 'Предупреждение о содержимом',
dropToUpload: 'Перетащите для загрузки',
invalidFileType: 'Неверный тип файла',
composeLabel: "О чем Вы думаете?",
autocompleteDescription: 'Когда результаты автозаполнения доступны, нажмите стрелки вверх или вниз и нажмите Enter, чтобы выбрать.',
mediaUploads: 'Загрузка медиа',
edit: 'Редактировать',
delete: 'Удалить',
description: 'Описание',
descriptionLabel: 'Добавьте описание для слабовидящих (изображение, видео) или слабослышащих (аудио, видео)',
markAsSensitive: 'Отметить медиа как деликатное',
// Polls
createPoll: 'Создать опрос',
removePollChoice: 'Удалить вариант {index}',
pollChoiceLabel: 'Вариант {index}',
multipleChoice: 'Несколько вариантов',
pollDuration: 'Продолжительность опроса',
fiveMinutes: '5 минут',
thirtyMinutes: '30 минут',
oneHour: '1 час',
sixHours: '6 часов',
twelveHours: '12 часов',
oneDay: '1 день',
threeDays: '3 дня',
sevenDays: '7 дней',
never: 'Никогда',
addEmoji: 'Вставить эмодзи',
addMedia: 'Добавить медиа (изображения, видео, аудио)',
addPoll: 'Добавить опрос',
removePoll: 'Удалить опрос',
postPrivacyLabel: 'Настройка конфиденциальности (на данный момент {label})',
addContentWarning: 'Добавить предупреждение о содержимом',
removeContentWarning: 'Удалить предупреждение о содержимом',
altLabel: 'Описание для слабовидящих',
extractText: 'Извлечь текст из изображения',
extractingText: 'Извлечение текста…',
extractingTextCompletion: 'Извлечение текста ({percent}% завершено)…',
unableToExtractText: 'Не удалось извлечь текст.',
// Account options
followAccount: 'Подписаться на {account}',
unfollowAccount: 'Отписаться от {account}',
blockAccount: 'Заблокировать {account}',
unblockAccount: 'Разблокировать {account}',
muteAccount: 'Игнорировать {account}',
unmuteAccount: 'Не игнорировать {account}',
showReblogsFromAccount: 'Показывать продвижения от {account}',
hideReblogsFromAccount: 'Скрыть продвижения от {account}',
showDomain: 'Показать {domain}',
hideDomain: 'Скрыть домен {domain}',
reportAccount: 'Пожаловаться на {account}',
mentionAccount: 'Упомянуть {account}',
copyLinkToAccount: 'Копировать ссылку на аккаунт',
copiedToClipboard: 'Скопировано в буфер обмена',
// Media dialog
navigateMedia: 'Навигация по элементам мультимедиа',
showPreviousMedia: 'Показать предыдущие медиа',
showNextMedia: 'Показать следующее медиа',
enterPinchZoom: 'Режим масштабирования щипком',
exitPinchZoom: 'Выйти из режима щипкового масштабирования.',
showMedia: `Показать {index, select,
1 {first}
2 {second}
3 {third}
other {fourth}
} медиа {current, select,
true {(current)}
other {}
}`,
previewFocalPoint: 'Предварительный просмотр (фокус)',
enterFocalPoint: 'Введите точку фокусировки (X, Y) для этого медиа',
muteNotifications: 'Отключить уведомления',
muteAccountConfirm: 'Игнорировать {account}?',
mute: 'Игнорировать',
unmute: 'Не игнорировать',
zoomOut: 'Уменьшить',
zoomIn: 'Увеличить',
// Reporting
reportingLabel: 'Вы отправляете жалобу на {account} модератору {instance}.',
additionalComments: 'Дополнительные комментарии',
forwardDescription: 'Переслать также модераторам {instance}?',
forwardLabel: 'Переслать {instance}',
unableToLoadStatuses: 'Не удалось загрузить последние записи: {error}',
report: 'Жалоба',
noContent: '(Без содержания)',
noStatuses: 'Нет записей для жалобы',
// Status options
unpinFromProfile: 'Открепить от профиля',
pinToProfile: 'Закрепить в профиле',
muteConversation: 'Игнорировать обсуждение',
unmuteConversation: 'Не игнорировать обсуждение',
bookmarkStatus: 'Добавить в закладки',
unbookmarkStatus: 'Удалить закладку',
deleteAndRedraft: 'Удалить и исправить',
reportStatus: 'Пожаловаться на запись',
shareStatus: 'Поделиться записью',
copyLinkToStatus: 'Копировать ссылку на запись',
// Account profile
profileForAccount: 'Профиль для {account}',
statisticsAndMoreOptions: 'Статистика и другие параметры',
statuses: 'Записи',
follows: 'Подписки',
followers: 'Подписчики',
moreOptions: 'Больше опций',
followersLabel: 'Подписчиков {count}',
followingLabel: 'Пописок {count}',
followLabel: `Подписаться {requested, select,
true {(follow requested)}
other {}
}`,
unfollowLabel: `Отписаться {requested, select,
true {(follow requested)}
other {}
}`,
notify: 'Подписаться на {account}',
denotify: 'Отписаться от {account}',
subscribedAccount: 'Подписан на аккаунт',
unsubscribedAccount: 'Отписаться от аккаунта',
unblock: 'Разблокировать',
nameAndFollowing: 'Имя и подписка',
clickToSeeAvatar: 'Нажмите, чтобы увидеть аватар',
opensInNewWindow: '{label} (открывается в новом окне)',
blocked: 'Заблокирован',
domainHidden: 'Домен скрыт',
muted: 'Игнорирован',
followsYou: 'Подписан на вас',
avatarForAccount: 'Аватар для {account}',
fields: 'Поля',
accountHasMoved: '{account} переехал:',
profilePageForAccount: 'Страница профиля для {account}',
// About page
about: 'О нас',
aboutApp: 'О Pinafore',
aboutAppDescription: `
<p>
Pinafore — это
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore">бесплатное программное обеспечение с открытым исходным кодом</a>
создано
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
и распространяется под
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
</p>
<h2 id="privacy-policy">Политика конфиденциальности</h2>
<p>
Pinafore не хранит никакой личной информации на своих серверах,
включая, помимо прочего, имена, адреса электронной почты,
IP-адреса, сообщения и фотографии.
</p>
<p>
Pinafore — это статический сайт. Все данные хранятся локально в вашем браузере и передаются через Федиверс
инстансы, к которым вы подключаетесь.
</p>
<h2>Кредиты</h2>
<p>
Иконки предоставлены <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
</p>
<p>
Благодарим за логотип «парусника» Грегора Креснара из
<a rel="noopener" target="_blank" href="https://thenounproject.com/"> Noun Project</a>.
</p>`,
// Settings
settings: 'Настройки',
general: 'Общие',
generalSettings: 'Общие настройки',
showSensitive: 'Показывать деликатные медиа по умолчанию',
showPlain: 'Показать простой серый цвет для деликатного медиа',
allSensitive: 'Относиться ко всем медиа как к деликатным',
largeMedia: 'Показывать большие изображения и видео',
autoplayGifs: 'Автовоспроизведение анимированных GIF-файлов',
hideCards: 'Скрыть предварительный просмотр ссылок',
underlineLinks: 'Подчеркивание ссылок в записях и профилях',
accessibility: 'Специальные возможности',
reduceMotion: 'Уменьшить анимацию интерфейса',
disableTappable: 'Отключить нажимаемую область на записи.',
removeEmoji: 'Удалить эмодзи из имен пользователей',
shortAria: 'Использовать метки ARIA для коротких статей',
theme: 'Тема',
themeForInstance: 'Тема для {instance}',
disableCustomScrollbars: 'Отключить пользовательские полосы прокрутки',
bottomNav: 'Поместите панель навигации в нижнюю часть экрана',
centerNav: 'Центрировать панель навигации',
preferences: 'Предпочтения',
hotkeySettings: 'Настройки горячих клавиш',
disableHotkeys: 'Отключить все горячие клавиши',
leftRightArrows: 'Клавиши со стрелками влево/вправо изменяют фокус, а не столбцы/медиа',
guide: 'Руководство',
reload: 'Перезагрузить',
// Wellness settings
wellness: 'Здоровье',
wellnessSettings: 'Настройки здоровья',
wellnessDescription: `Настройки здоровья предназначены для уменьшения вызывающих привыкание или тревогу аспектов социальных сетей.
Выберите любые варианты, которые вам подходят.`,
enableAll: 'Включить все',
metrics: 'Метрики',
hideFollowerCount: 'Скрыть количество подписчиков (до 10)',
hideReblogCount: 'Скрыть количество продижений',
hideFavoriteCount: 'Скрыть количество избранных',
hideUnread: 'Скрыть количество непрочитанных уведомлений (например, красную точку)',
// The quality that makes something seem important or interesting because it seems to be happening now
immediacy: 'Оперативность',
showAbsoluteTimestamps: 'Показывать абсолютные метки времени (например, «3-е марта») вместо относительных меток времени (например, «5 минут назад»)',
ui: 'Интерфейс',
grayscaleMode: 'Режим оттенков серого',
wellnessFooter: `Эти настройки частично основаны на рекомендациях
<a rel="noopener" target="_blank" href="https://humanetech.com">Центра гуманитарных технологий</a>.`,
// This is a link: "You can filter or disable notifications in the _instance settings_"
filterNotificationsPre: 'Вы можете фильтровать или отключать уведомления в',
filterNotificationsText: 'настройках инстанса',
filterNotificationsPost: '',
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
// to see a description. It's hard to properly internationalize, so we just break up the strings.
disableInfiniteScrollPre: 'Отключить',
disableInfiniteScrollText: 'бесконечную прокрутку',
disableInfiniteScrollDescription: `Когда бесконечная прокрутка отключена, новые записи не будут автоматически появляться в
внизу или вверху ленты. Вместо этого кнопки позволят вам
загружать больше контента по запросу.`,
disableInfiniteScrollPost: '',
// Instance settings
loggedInAs: 'Вы вошли как',
homeTimelineFilters: 'Фильтры главной ленты',
notificationFilters: 'Фильтры уведомлений',
pushNotifications: 'Всплывающее уведомление',
// Add instance page
storageError: `Похоже, Pinafore не может хранить данные локально. Ваш браузер находится в приватном режиме
или блокирует файлов cookie? Pinafore хранит все данные локально, и для этого требуется LocalStorage и
IndexedDB для корректной работы.`,
javaScriptError: 'Вы должны включить JavaScript, чтобы войти в систему.',
enterInstanceName: 'Введите имя инстанса',
instanceColon: 'Инстанс:',
// Custom tooltip, concatenated together
getAnInstancePre: "У вас нет",
getAnInstanceText: 'инстанса',
getAnInstanceDescription: 'Инстанс — это ваш домашний сервер Mastodon, например, mastodon.social или cybre.space.',
getAnInstancePost: '?',
joinMastodon: 'Присоединяйтесь к Mastodon!',
instancesYouveLoggedInTo: "Инстансы, в которые вы вошли:",
addAnotherInstance: 'Добавить другой инстанс',
youreNotLoggedIn: "Вы не вошли ни в один инстанс.",
currentInstanceLabel: `{instance} {current, select,
true {(current instance)}
other {}
}`,
// Link text
logInToAnInstancePre: '',
logInToAnInstanceText: 'Войти в инстанс',
logInToAnInstancePost: 'чтобы начать использовать Pinafore.',
// Another custom tooltip
showRingPre: 'Всегда показывать',
showRingText: 'кольцо фокусировки',
showRingDescription: `TКольцо фокусировки — это контур, показывающий элемент, на котором в данный момент установлен фокус. По умолчанию отображается
только при использовании клавиатуры (не мыши или сенсорного экрана), но вы можете выбрать, чтобы он отображался всегда.`,
showRingPost: '',
instances: 'Инстансы',
addInstance: 'Добавить инстанс',
homeTimelineFilterSettings: 'Настройки фильтров главной ленты',
showReblogs: 'Показать продвижения',
showReplies: 'Показывать ответы',
switchOrLogOut: 'Переключитесь или выйдите из этого инстанса',
switchTo: 'Переключиться на этот инстанс',
switchToInstance: 'Переключиться на инстанс',
switchToNameOfInstance: 'Переключиться на {instance}',
logOut: 'Выйти',
logOutOfInstanceConfirm: 'Выйти из {instance}?',
notificationFilterSettings: 'Настройки фильтра уведомлений',
// Push notifications
browserDoesNotSupportPush: "Ваш браузер не поддерживает push-уведомления.",
deniedPush: 'Вы запретили показывать уведомления.',
pushNotificationsNote: 'Обратите внимание, что вы можете получать push-уведомления только для одного инстанса за раз.',
pushSettings: 'Настройки push-уведомлений',
newFollowers: 'Новые подписчики',
reblogs: 'Продвижения',
pollResults: 'Результаты опроса',
subscriptions: 'Подписка на записи',
needToReauthenticate: 'Вам необходимо пройти повторную аутентификацию, чтобы включить push-уведомления. Выйти из {instance}?',
failedToUpdatePush: 'Не удалось обновить настройки push-уведомлений: {error}',
// Themes
chooseTheme: 'Выберите тему',
darkBackground: 'Темный фон',
lightBackground: 'Светлый фон',
themeLabel: `{label} {default, select,
true {(default)}
other {}
}`,
animatedImage: 'Анимированное изображение: {description}',
showImage: `Показывать {animated, select,
true {animated}
other {}
} image: {description}`,
playVideoOrAudio: `Воспроизводить {audio, select,
true {audio}
other {video}
}: {description}`,
accountFollowedYou: '{name} подписался на вас, {account}',
accountSignedUp: '{name} зарегистрировался, {account}',
reblogCountsHidden: 'Количество продвижений скрыто',
favoriteCountsHidden: 'Количество избранного скрыто',
rebloggedTimes: `Продвинуто {count, plural,
one {1 time}
other {{count} times}
}`,
favoritedTimes: `Добавлено в избранное {count, plural,
one {1 time}
other {{count} times}
}`,
pinnedStatus: 'Закрепленная запись',
rebloggedYou: 'продвинул вашу запись',
favoritedYou: 'добавил(-а) в избранное вашу запись',
followedYou: 'подписался на вас',
signedUp: 'зарегистрировался',
posted: 'опубликовал',
pollYouCreatedEnded: 'Созданный вами опрос завершен',
pollYouVotedEnded: 'Опрос, в котором вы голосовали, завершен',
reblogged: 'продвинул(-а)',
favorited: 'добавил(-а) в избранное',
unreblogged: 'отменил(-а) продвижение',
unfavorited: 'удалил(-а) из избранного',
showSensitiveMedia: 'Показать деликатное медиа',
hideSensitiveMedia: 'Скрыть деликатное медиа',
clickToShowSensitive: 'Деликатное содержимое. Нажмите, чтобы показать.',
longPost: 'Длинная запись',
// Accessible status labels
accountRebloggedYou: '{account} продвинул(-а) вашу запись',
accountFavoritedYou: '{account} добавил(-а) в избранное вашу запись',
rebloggedByAccount: 'Продвинул(-а) {account}',
contentWarningContent: 'Предупреждение о содержимом: {spoiler}',
hasMedia: 'имеет медия',
hasPoll: 'имеет опрос',
shortStatusLabel: '{privacy} запись от {account}',
// Privacy types
public: 'Публичный',
unlisted: 'Открытый',
followersOnly: 'Только для подписчиков',
direct: 'Личное сообщение',
// Themes
themeRoyal: 'Royal',
themeScarlet: 'Scarlet',
themeSeafoam: 'Seafoam',
themeHotpants: 'Hotpants',
themeOaken: 'Oaken',
themeMajesty: 'Majesty',
themeGecko: 'Gecko',
themeGrayscale: 'Grayscale',
themeOzark: 'Ozark',
themeCobalt: 'Cobalt',
themeSorcery: 'Sorcery',
themePunk: 'Punk',
themeRiot: 'Riot',
themeHacker: 'Hacker',
themeMastodon: 'Mastodon',
themePitchBlack: 'Pitch Black',
themeDarkGrayscale: 'Dark Grayscale',
// Polls
voteOnPoll: 'Голосовать в опросе',
pollChoices: 'Варианты опроса',
vote: 'Голосовать',
pollDetails: 'Детали опроса',
refresh: 'Обновить',
expires: 'Завершается',
expired: 'Завершено',
voteCount: `{count, plural,
one {1 vote}
other {{count} голосов}
}`,
// Status interactions
clickToShowThread: '{time} - нажмите, чтобы показать тред',
showMore: 'Показать больше',
showLess: 'Показать меньше',
closeReply: 'Закрыть ответ',
cannotReblogFollowersOnly: 'Невозможно продвинуть, потому что это только для подписчиков',
cannotReblogDirectMessage: 'Невозможно продвинуть, потому что это личное сообщение',
reblog: 'Продвинуть',
reply: 'Ответить',
replyToThread: 'Ответить в треде',
favorite: 'Добавить в избранное',
unfavorite: 'Удалить из избранного',
// timeline
loadingMore: 'Загружается ещё…',
loadMore: 'Загрузить ещё',
showCountMore: 'Показать ещё {count}',
nothingToShow: 'Нечего показывать.',
// status thread page
statusThreadPage: 'Страница треда записи',
status: 'Запись',
// toast messages
blockedAccount: 'Аккаунт заблокирован',
unblockedAccount: 'Аккаунт разблокирован',
unableToBlock: 'Не удалось заблокировать аккаунт: {error}',
unableToUnblock: 'Не удалось разблокировать аккаунт: {error}',
bookmarkedStatus: 'Запись добавлена в закладки',
unbookmarkedStatus: 'Запись удалена из закладок',
unableToBookmark: 'Не удалось добавить в закладки: {error}',
unableToUnbookmark: 'Не удалось удалить из закладок: {error}',
cannotPostOffline: 'Вы не можете публиковать записи в офлайн-режиме',
unableToPost: 'Не удалось опубликовать запись: {error}',
statusDeleted: 'Запись удалена',
unableToDelete: 'Не удалось удалить запись: {error}',
cannotFavoriteOffline: 'Вы не можете добавлять в избранное в офлайн-режиме режиме',
cannotUnfavoriteOffline: 'Вы не можете удалять из избранного в офлайн-режиме режиме',
unableToFavorite: 'Не удалось добавить в избранное: {error}',
unableToUnfavorite: 'Не удалось удалить из избранного: {error}',
followedAccount: 'Подписан(-на) на аккаунт',
unfollowedAccount: 'Отписан(-на) от аккаунта',
unableToFollow: 'Не удалось подписаться на аккаунт: {error}',
unableToUnfollow: 'Не удалось отписаться от аккаунта: {error}',
accessTokenRevoked: 'Токен доступа был отозван, выполнен выход из {instance}',
loggedOutOfInstance: 'Выполнен выход из {instance}',
failedToUploadMedia: 'Не удалось загрузить мультимедиа: {error}',
mutedAccount: 'Аккаунт игнорируется',
unmutedAccount: 'Аккаунт не игнорируется',
unableToMute: 'Не удалось добавить аккаунт в игнорируемые: {error}',
unableToUnmute: 'Не удалось удалить аккаунт из игнорируемых: {error}',
mutedConversation: 'Обсуждение добавлено в игнорируемые',
unmutedConversation: 'Обсуждение удалено из игнорируемых',
unableToMuteConversation: 'Не удалось добавить обсуждение в игнорируемые: {error}',
unableToUnmuteConversation: 'Не удалось удалить обсуждение из игнорируемых: {error}',
unpinnedStatus: 'Запись откреплена',
unableToPinStatus: 'Не удалось закрепить запись: {error}',
unableToUnpinStatus: 'Не удалось открепить запись: {error}',
unableToRefreshPoll: 'Не удалось обновить опрос: {error}',
unableToVoteInPoll: 'Не удалось проголосовать в опросе: {error}',
cannotReblogOffline: 'Вы не можете продвигать в оффлайн-режиме.',
cannotUnreblogOffline: 'Вы не можете отменить продвижение в оффлайн-режиме.',
failedToReblog: 'Не удалось продвинуть: {error}',
failedToUnreblog: 'Не удалось отменить продвижение: {error}',
submittedReport: 'Жалоба отправлена',
failedToReport: 'Не удалось отправить жалобу: {error}',
approvedFollowRequest: 'Запрос на подписку одобрен',
rejectedFollowRequest: 'Запрос на подписку отклонен',
unableToApproveFollowRequest: 'Не удалось одобрить запрос на подписку: {error}',
unableToRejectFollowRequest: 'Не удалось отклонить запрос на подписку: {error}',
searchError: 'Ошибка во время поиска: {error}',
hidDomain: 'Домен скрыт',
unhidDomain: 'Домен удален из скрытых',
unableToHideDomain: 'Не удалось скрыть домен: {error}',
unableToUnhideDomain: 'Не удалось удалить домен из скрытых: {error}',
showingReblogs: 'Показывать продвижения',
hidingReblogs: 'Скрывать продвижения',
unableToShowReblogs: 'Не удалось показать продвижения: {error}',
unableToHideReblogs: 'Не удалось скрыть продвижения: {error}',
unableToShare: 'Не удалось поделиться: {error}',
unableToSubscribe: 'Не удалось подписаться: {error}',
unableToUnsubscribe: 'Не удалось отписаться: {error}',
showingOfflineContent: 'Интернет-запрос не выполнен. Отображается офлайн-содержимое.',
youAreOffline: 'Похоже, вы не в сети. Вы по-прежнему можете читать записи в офлайн-режиме.',
// Snackbar UI
updateAvailable: 'Доступно обновление приложения.',
// Word/phrase filters
wordFilters: 'Фильтры слов',
noFilters: 'У вас нет фильтров слов.',
wordOrPhrase: 'Слово или фраза',
contexts: 'Контексты',
addFilter: 'Добавить фильтр',
editFilter: 'Редактировать фильтр',
filterHome: 'Главная и списки',
filterNotifications: 'Уведомления',
filterPublic: 'Публичные ленты',
filterThread: 'Обсуждения',
filterAccount: 'Профили',
filterUnknown: 'Неизвестный',
expireAfter: 'Истекает через',
whereToFilter: 'Где фильтровать',
irreversible: 'Необратимый',
wholeWord: 'Целое слово',
save: 'Сохранить',
updatedFilter: 'Фильтр обновлён',
createdFilter: 'Фильтр создан',
failedToModifyFilter: 'Не удалось изменить фильтр: {error}',
deletedFilter: 'Фильтр удалён',
required: 'Требуется',
// Dialogs
profileOptions: 'Параметры профиля',
copyLink: 'Копировать ссылку',
emoji: 'Эмодзи',
editMedia: 'Редактировать медиа',
shortcutHelp: 'Быстрая помощь',
statusOptions: 'Параметры статуса',
confirm: 'Подтвердить',
closeDialog: 'Закрыть диалог',
postPrivacy: 'Конфиденциальность записи',
homeOnInstance: 'Главная на {instance}',
statusesTimelineOnInstance: 'Записи: {timeline} лента на {instance}',
statusesHashtag: 'Записи: #{hashtag} хэштег',
statusesThread: 'Записи: треды',
statusesAccountTimeline: 'Записи: лента аккаунта',
statusesList: 'Записи: список',
notificationsOnInstance: 'Уведомления на {instance}'
}

Wyświetl plik

@ -1,5 +1,6 @@
import { getAccountAccessibleName } from './getAccountAccessibleName'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
import { getAccountAccessibleName } from './getAccountAccessibleName.js'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses.js'
import { formatIntl } from '../_utils/formatIntl.js'
function getNotificationText (notification, omitEmojiInDisplayNames) {
if (!notification) {
@ -7,9 +8,11 @@ function getNotificationText (notification, omitEmojiInDisplayNames) {
}
const notificationAccountDisplayName = getAccountAccessibleName(notification.account, omitEmojiInDisplayNames)
if (notification.type === 'reblog') {
return `${notificationAccountDisplayName} boosted your status`
return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName })
} else if (notification.type === 'favourite') {
return `${notificationAccountDisplayName} favorited your status`
return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName })
} else if (notification.type === 'update') {
return formatIntl('intl.accountEdited', { account: notificationAccountDisplayName })
}
}
@ -26,7 +29,7 @@ function getReblogText (reblog, account, omitEmojiInDisplayNames) {
return
}
const accountDisplayName = getAccountAccessibleName(account, omitEmojiInDisplayNames)
return `Boosted by ${accountDisplayName}`
return formatIntl('intl.rebloggedByAccount', { account: accountDisplayName })
}
function cleanupText (text) {
@ -34,21 +37,24 @@ function cleanupText (text) {
}
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
timeagoFormattedDate, spoilerText, showContent,
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)
: `Content warning: ${cleanupText(spoilerText)}`
const mediaTextToShow = showMedia && 'has media'
const pollTextToShow = showPoll && 'has poll'
: formatIntl('intl.contentWarningContent', { spoiler: cleanupText(spoilerText) })
const mediaTextToShow = showMedia && 'intl.hasMedia'
const mediaDescText = (showMedia && (!sensitive || sensitiveShown))
? mediaAttachments.map(media => media.description)
: []
const pollTextToShow = showPoll && 'intl.hasPoll'
const privacyText = getPrivacyText(visibility)
if (disableLongAriaLabels) {
// Long text can crash NVDA; allow users to shorten it like we had it before.
// https://github.com/nolanlawson/pinafore/issues/694
return `${privacyText} status by ${originalAccountDisplayName}`
return formatIntl('intl.shortStatusLabel', { privacy: privacyText, account: originalAccountDisplayName })
}
const values = [
@ -56,8 +62,9 @@ export function getAccessibleLabelForStatus (originalAccount, account, plainText
originalAccountDisplayName,
contentTextToShow,
mediaTextToShow,
...mediaDescText,
pollTextToShow,
timeagoFormattedDate,
shortInlineFormattedDate,
`@${originalAccount.acct}`,
privacyText,
getReblogText(reblog, account, omitEmojiInDisplayNames)

Wyświetl plik

@ -1,4 +1,4 @@
import { removeEmoji } from '../_utils/removeEmoji'
import { removeEmoji } from '../_utils/removeEmoji.js'
export function getAccountAccessibleName (account, omitEmojiInDisplayNames) {
const emojis = account.emojis

Wyświetl plik

@ -1,7 +1,7 @@
import { getAccount } from '../_api/user'
import { getRelationship } from '../_api/relationships'
import { database } from '../_database/database'
import { store } from '../_store/store'
import { getAccount } from '../_api/user.js'
import { getRelationship } from '../_api/relationships.js'
import { database } from '../_database/database.js'
import { store } from '../_store/store.js'
async function _updateAccount (accountId, instanceName, accessToken) {
const localPromise = database.getAccount(instanceName, accountId)

Wyświetl plik

@ -1,12 +1,19 @@
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
import { getInstanceInfo } from '../_api/instance'
import { goto } from '../../../__sapper__/client'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
import { store } from '../_store/store'
import { updateVerifyCredentialsForInstance } from './instances'
import { updateCustomEmojiForInstance } from './emoji'
import { database } from '../_database/database'
import { DOMAIN_BLOCKS } from '../_static/blocks'
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth.js'
import { getInstanceInfo } from '../_api/instance.js'
import { goto } from '../../../__sapper__/client.js'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine.js'
import { store } from '../_store/store.js'
import { updateVerifyCredentialsForInstance } from './instances.js'
import { updateCustomEmojiForInstance } from './emoji.js'
import { database } from '../_database/database.js'
import { DOMAIN_BLOCKS } from '../_static/blocks.js'
const GENERIC_ERROR = `
Is this a valid Mastodon instance? Is a browser extension
blocking the request? Are you in private browsing mode?
If you believe this is a problem with your instance, please send
<a href="https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md"
target="_blank" rel="noopener">this link</a> to the administrator of your instance.`
function createKnownError (message) {
const err = new Error(message)
@ -30,8 +37,16 @@ async function redirectToOauth () {
}
const redirectUri = getRedirectUri()
const registrationPromise = registerApplication(instanceNameInSearch, redirectUri)
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
try {
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
} catch (err) {
// We get a 401 in limited federation mode, so we can just skip setting the instance info in that case.
// It will be fetched automatically later.
if (err.status !== 401) {
throw err // this is a good way to test for typos in the instance name or some other problem
}
}
const instanceData = await registrationPromise
store.set({
currentRegisteredInstanceName: instanceNameInSearch,
@ -59,10 +74,7 @@ export async function logInToInstance () {
} catch (err) {
console.error(err)
const error = `${err.message || err.name}. ` +
(err.knownError ? '' : (navigator.onLine
? `Is this a valid Mastodon instance? Is a browser extension
blocking the request? Are you in private browsing mode?`
: 'Are you offline?'))
(err.knownError ? '' : (navigator.onLine ? GENERIC_ERROR : 'Are you offline?'))
const { instanceNameInSearch } = store.get()
store.set({
logInToInstanceError: error,
@ -93,10 +105,10 @@ async function registerNewInstance (code) {
instanceNameInSearch: '',
currentRegisteredInstanceName: null,
currentRegisteredInstance: null,
loggedInInstances: loggedInInstances,
loggedInInstances,
currentInstance: currentRegisteredInstanceName,
loggedInInstancesInOrder: loggedInInstancesInOrder,
instanceThemes: instanceThemes
loggedInInstancesInOrder,
instanceThemes
})
store.save()
const { enableGrayscale } = store.get()

Wyświetl plik

@ -1,11 +1,10 @@
import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store'
import uniqBy from 'lodash-es/uniqBy'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { concat } from '../_utils/arrays'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'
import { mark, stop } from '../_utils/marks.js'
import { store } from '../_store/store.js'
import { uniqBy, isEqual } from '../_thirdparty/lodash/objects.js'
import { database } from '../_database/database.js'
import { concat } from '../_utils/arrays.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary.js'
function getExistingItemIdsSet (instanceName, timelineName) {
const timelineItemSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries') || []
@ -31,9 +30,9 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
console.log('itemSummariesToAdd', JSON.parse(JSON.stringify(itemSummariesToAdd)))
console.log('updates.map(timelineItemToSummary)', JSON.parse(JSON.stringify(updates.map(timelineItemToSummary))))
console.log('concat(itemSummariesToAdd, updates.map(timelineItemToSummary))',
JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(timelineItemToSummary)))))
JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName))))))
const newItemSummariesToAdd = uniqBy(
concat(itemSummariesToAdd, updates.map(timelineItemToSummary)),
concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName))),
_ => _.id
)
if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
@ -78,7 +77,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
continue
}
const newItemSummariesToAdd = uniqBy(
concat(itemSummariesToAdd, validUpdates.map(timelineItemToSummary)),
concat(itemSummariesToAdd, validUpdates.map(item => timelineItemToSummary(item, instanceName))),
_ => _.id
)
if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
@ -119,6 +118,6 @@ export function addStatusesOrNotifications (instanceName, timelineName, newStatu
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
freshUpdates = uniqBy(freshUpdates, _ => _.id)
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
store.setForTimeline(instanceName, timelineName, { freshUpdates })
lazilyProcessFreshUpdates(instanceName, timelineName)
}

Wyświetl plik

@ -1,6 +1,6 @@
import { store } from '../_store/store'
import { store } from '../_store/store.js'
const emojiMapper = emoji => `:${emoji.shortcode}:`
const emojiMapper = emoji => emoji.unicode ? emoji.unicode : `:${emoji.shortcodes[0]}:`
const hashtagMapper = hashtag => `#${hashtag.name}`
const accountMapper = account => `@${account.acct}`
@ -61,7 +61,7 @@ export function selectAutosuggestItem (item) {
const endIndex = composeSelectionStart
if (item.acct) {
/* no await */ insertUsername(currentComposeRealm, item, startIndex, endIndex)
} else if (item.shortcode) {
} else if (item.shortcodes) {
/* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
} else { // hashtag
/* no await */ insertHashtag(currentComposeRealm, item, startIndex, endIndex)

Wyświetl plik

@ -1,11 +1,11 @@
import { database } from '../_database/database'
import { store } from '../_store/store'
import { search } from '../_api/search'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { concat } from '../_utils/arrays'
import uniqBy from 'lodash-es/uniqBy'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { RequestThrottler } from '../_utils/RequestThrottler'
import { database } from '../_database/database.js'
import { store } from '../_store/store.js'
import { search } from '../_api/search.js'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
import { concat } from '../_utils/arrays.js'
import { uniqBy } from '../_thirdparty/lodash/objects.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { RequestThrottler } from '../_utils/RequestThrottler.js'
const DATABASE_SEARCH_RESULTS_LIMIT = 30

Wyświetl plik

@ -1,24 +1,45 @@
import { store } from '../_store/store'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { store } from '../_store/store.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import * as emojiDatabase from '../_utils/emojiDatabase.js'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
import { testEmojiSupported } from '../_utils/testEmojiSupported.js'
import { mark, stop } from '../_utils/marks.js'
function searchEmoji (searchText) {
searchText = searchText.toLowerCase().substring(1)
const { currentCustomEmoji } = store.get()
const results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText))
.sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1)
.slice(0, SEARCH_RESULTS_LIMIT)
async function searchEmoji (searchText) {
let emojis = await emojiDatabase.findBySearchQuery(searchText)
const results = []
if (searchText.startsWith(':') && searchText.endsWith(':')) {
// exact shortcode search
const shortcode = searchText.substring(1, searchText.length - 1).toLowerCase()
emojis = emojis.filter(_ => _.shortcodes.includes(shortcode))
}
mark('testEmojiSupported')
for (const emoji of emojis) {
if (results.length === SEARCH_RESULTS_LIMIT) {
break
}
if (emoji.url || testEmojiSupported(emoji.unicode)) { // emoji.url is a custom emoji
results.push(emoji)
}
}
stop('testEmojiSupported')
return results
}
export function doEmojiSearch (searchText) {
let canceled = false
scheduleIdleTask(() => {
scheduleIdleTask(async () => {
if (canceled) {
return
}
const results = await searchEmoji(searchText)
if (canceled) {
return
}
const results = searchEmoji(searchText)
store.setForCurrentAutosuggest({
autosuggestType: 'emoji',
autosuggestSelected: 0,

Wyświetl plik

@ -1,9 +1,9 @@
import { search } from '../_api/search'
import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { RequestThrottler } from '../_utils/RequestThrottler'
import { sum } from '../_utils/lodash-lite'
import { search } from '../_api/search.js'
import { store } from '../_store/store.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
import { RequestThrottler } from '../_utils/RequestThrottler.js'
import { sum } from '../_utils/lodash-lite.js'
const HASHTAG_SEARCH_LIMIT = 10

Wyświetl plik

@ -1,8 +1,9 @@
import { store } from '../_store/store'
import { blockAccount, unblockAccount } from '../_api/block'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'
import { store } from '../_store/store.js'
import { blockAccount, unblockAccount } from '../_api/block.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -16,14 +17,17 @@ export async function setAccountBlocked (accountId, block, toastOnSuccess) {
await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) {
if (block) {
toast.say('Blocked account')
/* no await */ toast.say('intl.blockedAccount')
} else {
toast.say('Unblocked account')
/* no await */ toast.say('intl.unblockedAccount')
}
}
emit('refreshAccountsList')
} catch (e) {
console.error(e)
toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
/* no await */ toast.say(block
? formatIntl('intl.unableToBlock', { block: !!block, error: (e.message || '') })
: formatIntl('intl.unableToUnblock', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -0,0 +1,30 @@
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) {
const { currentInstance, accessToken } = store.get()
try {
if (bookmarked) {
await bookmarkStatus(currentInstance, accessToken, statusId)
} else {
await unbookmarkStatus(currentInstance, accessToken, statusId)
}
if (bookmarked) {
/* no await */ toast.say('intl.bookmarkedStatus')
} else {
/* no await */ toast.say('intl.unbookmarkedStatus')
}
store.setStatusBookmarked(currentInstance, statusId, bookmarked)
await database.setStatusBookmarked(currentInstance, statusId, bookmarked)
} catch (e) {
console.error(e)
/* no await */toast.say(
bookmarked
? formatIntl('intl.unableToBookmark', { error: (e.message || '') })
: formatIntl('intl.unableToUnbookmark', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,11 +1,13 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { postStatus as postStatusToServer } from '../_api/statuses'
import { addStatusOrNotification } from './addStatusOrNotification'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'
import { putMediaMetadata } from '../_api/media'
import uniqBy from 'lodash-es/uniqBy'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { postStatus as postStatusToServer } from '../_api/statuses.js'
import { addStatusOrNotification } from './addStatusOrNotification.js'
import { database } from '../_database/database.js'
import { emit } from '../_utils/eventBus.js'
import { putMediaMetadata } from '../_api/media.js'
import { uniqBy } from '../_thirdparty/lodash/objects.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function insertHandleForReply (statusId) {
const { currentInstance } = store.get()
@ -29,7 +31,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
const { currentInstance, accessToken, online } = store.get()
if (!online) {
toast.say('You cannot post while offline')
/* no await */ toast.say('intl.cannotPostOffline')
return
}
@ -58,9 +60,10 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
addStatusOrNotification(currentInstance, 'home', status)
store.clearComposeData(realm)
emit('postedStatus', realm, inReplyToUuid)
scheduleIdleTask(() => (mediaIds || []).forEach(mediaId => database.deleteCachedMediaFile(mediaId))) // clean up media cache
} catch (e) {
console.error(e)
toast.say('Unable to post status: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.unableToPost', { error: (e.message || '') }))
} finally {
store.set({ postingStatus: false })
}

Wyświetl plik

@ -1,4 +1,4 @@
import { store } from '../_store/store'
import { store } from '../_store/store.js'
export function enablePoll (realm) {
store.setComposeData(realm, {

Wyświetl plik

@ -1,4 +1,4 @@
import { store } from '../_store/store'
import { store } from '../_store/store.js'
export function toggleContentWarningShown (realm) {
const shown = store.getComposeData(realm, 'contentWarningShown')

Wyświetl plik

@ -1,11 +1,11 @@
import { importShowCopyDialog } from '../_components/dialog/asyncDialogs/importShowCopyDialog.js'
import { toast } from '../_components/toast/toast'
import { toast } from '../_components/toast/toast.js'
export async function copyText (text) {
if (navigator.clipboard) { // not supported in all browsers
try {
await navigator.clipboard.writeText(text)
toast.say('Copied to clipboard')
/* no await */ toast.say('intl.copiedToClipboard')
return
} catch (e) {
console.error(e)

Wyświetl plik

@ -1,9 +1,6 @@
import { database } from '../_database/database'
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash'
import { mark, stop } from '../_utils/marks'
import { get } from '../_utils/lodash-lite'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { database } from '../_database/database.js'
import { mark, stop } from '../_utils/marks.js'
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
return {
@ -21,62 +18,10 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
}
}
function tryInitBlurhash () {
try {
initBlurhash()
} catch (err) {
console.error('could not start blurhash worker', err)
}
}
function getActualStatus (statusOrNotification) {
return get(statusOrNotification, ['status']) ||
get(statusOrNotification, ['notification', 'status'])
}
async function decodeAllBlurhashes (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
.concat(get(status, ['reblog', 'media_attachments'], []))
.filter(_ => _.blurhash)
if (mediaWithBlurhashes.length) {
mark(`decodeBlurhash-${status.id}`)
await Promise.all(mediaWithBlurhashes.map(async media => {
try {
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
} catch (err) {
console.warn('Could not decode blurhash, ignoring', err)
}
}))
stop(`decodeBlurhash-${status.id}`)
}
}
async function calculatePlainTextContent (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const originalStatus = status.reblog ? status.reblog : status
const content = originalStatus.content || ''
const mentions = originalStatus.mentions || []
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
// as well do it in advance, while blurhash is being decoded on the worker thread.
await new Promise(resolve => {
scheduleIdleTask(() => {
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
resolve()
})
})
}
export function createMakeProps (instanceName, timelineType, timelineValue) {
let promiseChain = Promise.resolve()
tryInitBlurhash() // start the blurhash worker a bit early to save time
prepareToRehydrate() // start blurhash early to save time
async function fetchFromIndexedDB (itemId) {
mark(`fetchFromIndexedDB-${itemId}`)
@ -92,10 +37,7 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
async function getStatusOrNotification (itemId) {
const statusOrNotification = await fetchFromIndexedDB(itemId)
await Promise.all([
decodeAllBlurhashes(statusOrNotification),
calculatePlainTextContent(statusOrNotification)
])
await rehydrateStatusOrNotification(statusOrNotification)
return statusOrNotification
}

Wyświetl plik

@ -1,18 +1,19 @@
import { store } from '../_store/store'
import { deleteStatus } from '../_api/delete'
import { toast } from '../_components/toast/toast'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
import { store } from '../_store/store.js'
import { deleteStatus } from '../_api/delete.js'
import { toast } from '../_components/toast/toast.js'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function doDeleteStatus (statusId) {
const { currentInstance, accessToken } = store.get()
try {
const deletedStatus = await deleteStatus(currentInstance, accessToken, statusId)
deleteStatusLocally(currentInstance, statusId)
toast.say('Status deleted.')
/* no await */ toast.say('intl.statusDeleted')
return deletedStatus
} catch (e) {
console.error(e)
toast.say('Unable to delete status: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.unableToDelete', { error: (e.message || '') }))
throw e
}
}

Wyświetl plik

@ -1,7 +1,7 @@
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
import { doDeleteStatus } from './delete'
import { store } from '../_store/store'
import { doDeleteStatus } from './delete.js'
import { store } from '../_store/store.js'
export async function deleteAndRedraft (status) {
const deleteStatusPromise = doDeleteStatus(status.id)

Wyświetl plik

@ -1,8 +1,8 @@
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
import { store } from '../_store/store'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses.js'
import { store } from '../_store/store.js'
import { isEqual } from '../_thirdparty/lodash/objects.js'
import { database } from '../_database/database.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
const keys = ['timelineItemSummaries', 'timelineItemSummariesToAdd']

Wyświetl plik

@ -0,0 +1,35 @@
// "Secret" API to quickly log in with an access token and instance name.
// Used in the integration tests. Can't see a problem with exposing this publicly
// since you would have to know the access token anyway.
import { store } from '../_store/store.js'
import { goto } from '../../../__sapper__/client.js'
export function doQuickLoginIfNecessary () {
const params = new URLSearchParams(location.search)
const accessToken = params.get('accessToken')
const instanceName = params.get('instanceName')
if (!accessToken || !instanceName) {
return
}
const {
loggedInInstances,
loggedInInstancesInOrder
} = store.get()
loggedInInstances[instanceName] = {
access_token: accessToken
}
if (!loggedInInstancesInOrder.includes(instanceName)) {
loggedInInstancesInOrder.push(instanceName)
}
store.set({
currentInstance: instanceName,
loggedInInstances,
loggedInInstancesInOrder
})
store.save()
goto('/') // re-navigate without the URL params
}

Wyświetl plik

@ -1,15 +1,19 @@
import {
cacheFirstUpdateAfter,
cacheFirstUpdateOnlyIfNotInCache
} from '../_utils/sync'
import { database } from '../_database/database'
import { getCustomEmoji } from '../_api/emoji'
import { store } from '../_store/store'
import isEqual from 'lodash-es/isEqual'
} from '../_utils/sync.js'
import { database } from '../_database/database.js'
import { getCustomEmoji } from '../_api/emoji.js'
import { store } from '../_store/store.js'
import { isEqual } from '../_thirdparty/lodash/objects.js'
async function syncEmojiForInstance (instanceName, syncMethod) {
await syncMethod(
() => getCustomEmoji(instanceName),
() => {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName].access_token
return getCustomEmoji(instanceName, accessToken)
},
() => database.getCustomEmoji(instanceName),
emoji => database.setCustomEmoji(instanceName, emoji),
emoji => {
@ -31,7 +35,7 @@ export async function setupCustomEmojiForInstance (instanceName) {
}
export function insertEmoji (realm, emoji) {
const emojiText = emoji.custom ? emoji.colons : emoji.native
const emojiText = emoji.unicode || `:${emoji.name}:`
const { composeSelectionStart } = store.get()
const idx = composeSelectionStart || 0
const oldText = store.getComposeData(realm, 'text') || ''

Wyświetl plik

@ -1,12 +1,13 @@
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite.js'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setFavorited (statusId, favorited) {
const { online } = store.get()
if (!online) {
toast.say(`You cannot ${favorited ? 'favorite' : 'unfavorite'} while offline.`)
/* no await */ toast.say(favorited ? 'intl.cannotFavoriteOffline' : 'intl.cannotUnfavoriteOffline')
return
}
const { currentInstance, accessToken } = store.get()
@ -19,7 +20,10 @@ export async function setFavorited (statusId, favorited) {
await database.setStatusFavorited(currentInstance, statusId, favorited)
} catch (e) {
console.error(e)
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
/* no await */ toast.say(favorited
? formatIntl('intl.unableToFavorite', { error: (e.message || '') })
: formatIntl('intl.unableToUnfavorite', { error: (e.message || '') })
)
store.setStatusFavorited(currentInstance, statusId, !favorited) // undo optimistic update
}
}

Wyświetl plik

@ -0,0 +1,63 @@
import { store } from '../_store/store.js'
import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters.js'
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync.js'
import { database } from '../_database/database.js'
import { isEqual } from '../_thirdparty/lodash/objects.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
import { emit } from '../_utils/eventBus.js'
async function syncFilters (instanceName, syncMethod) {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName].access_token
await syncMethod(
() => getFilters(instanceName, accessToken),
() => database.getFilters(instanceName),
filters => database.setFilters(instanceName, filters),
filters => {
const { instanceFilters } = store.get()
if (!isEqual(instanceFilters[instanceName], filters)) { // avoid re-render if nothing changed
instanceFilters[instanceName] = filters
store.set({ instanceFilters })
}
}
)
}
export async function updateFiltersForInstance (instanceName) {
await syncFilters(instanceName, cacheFirstUpdateAfter)
}
export async function setupFiltersForInstance (instanceName) {
await syncFilters(instanceName, cacheFirstUpdateOnlyIfNotInCache)
}
export async function createOrUpdateFilter (instanceName, filter) {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName].access_token
try {
if (filter.id) {
await updateFilter(instanceName, accessToken, filter)
/* no await */ toast.say('intl.updatedFilter')
} else {
await createFilter(instanceName, accessToken, filter)
/* no await */ toast.say('intl.createdFilter')
}
emit('wordFiltersChanged', instanceName)
} catch (err) {
/* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || ''))
}
}
export async function deleteFilter (instanceName, id) {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName].access_token
try {
await doDeleteFilter(instanceName, accessToken, id)
/* no await */ toast.say('intl.deletedFilter')
emit('wordFiltersChanged', instanceName)
} catch (err) {
/* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || ''))
}
}

Wyświetl plik

@ -1,7 +1,8 @@
import { store } from '../_store/store'
import { followAccount, unfollowAccount } from '../_api/follow'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { store } from '../_store/store.js'
import { followAccount, unfollowAccount } from '../_api/follow.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -14,14 +15,13 @@ export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
}
await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) {
if (follow) {
toast.say('Followed account')
} else {
toast.say('Unfollowed account')
}
/* no await */ toast.say(follow ? 'intl.followedAccount' : 'intl.unfollowedAccount')
}
} catch (e) {
console.error(e)
toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
/* no await */ toast.say(follow
? formatIntl('intl.unableToFollow', { error: (e.message || '') })
: formatIntl('intl.unableToUnfollow', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { database } from '../_database/database'
import { getFollowRequests } from '../_api/followRequests'
import { get } from '../_utils/lodash-lite'
import { store } from '../_store/store.js'
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
import { database } from '../_database/database.js'
import { getFollowRequests } from '../_api/followRequests.js'
import { get } from '../_utils/lodash-lite.js'
export async function updateFollowRequestCountIfLockedAccount (instanceName) {
const { verifyCredentials, loggedInInstances } = store.get()

Wyświetl plik

@ -1,7 +1,7 @@
import { store } from '../_store/store'
import { getTimeline } from '../_api/timelines'
import { store } from '../_store/store.js'
import { getTimeline } from '../_api/timelines.js'
export async function getRecentStatusesForAccount (accountId) {
const { currentInstance, accessToken } = store.get()
return getTimeline(currentInstance, accessToken, `account/${accountId}`, null, null, 20)
return (await getTimeline(currentInstance, accessToken, `account/${accountId}`, null, null, 20)).items
}

Wyświetl plik

@ -0,0 +1,14 @@
import { store } from '../_store/store.js'
import { goto } from '../../../__sapper__/client.js'
import { emit } from '../_utils/eventBus.js'
// Go to the search page, and also focus the search input. For accessibility
// and usability reasons, this only happens on pressing these particular hotkeys.
export async function goToSearch () {
if (store.get().currentPage === 'search') {
emit('focusSearchInput')
} else {
store.set({ focusSearchInput: true })
goto('/search')
}
}

Wyświetl plik

@ -1,17 +1,18 @@
import { getVerifyCredentials } from '../_api/user'
import { store } from '../_store/store'
import { switchToTheme } from '../_utils/themeEngine'
import { toast } from '../_components/toast/toast'
import { goto } from '../../../__sapper__/client'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { getInstanceInfo } from '../_api/instance'
import { database } from '../_database/database'
import { getVerifyCredentials } from '../_api/user.js'
import { store } from '../_store/store.js'
import { switchToTheme } from '../_utils/themeEngine.js'
import { toast } from '../_components/toast/toast.js'
import { goto } from '../../../__sapper__/client.js'
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
import { getInstanceInfo } from '../_api/instance.js'
import { database } from '../_database/database.js'
import { importVirtualListStore } from '../_utils/asyncModules/importVirtualListStore.js'
import { formatIntl } from '../_utils/formatIntl.js'
export function changeTheme (instanceName, newTheme) {
const { instanceThemes } = store.get()
instanceThemes[instanceName] = newTheme
store.set({ instanceThemes: instanceThemes })
store.set({ instanceThemes })
store.save()
const { currentInstance } = store.get()
if (instanceName === currentInstance) {
@ -32,7 +33,8 @@ export function switchToInstance (instanceName) {
switchToTheme(instanceThemes[instanceName], enableGrayscale)
}
export async function logOutOfInstance (instanceName, message = `Logged out of ${instanceName}`) {
export async function logOutOfInstance (instanceName, message) {
message = message || formatIntl('intl.loggedOutOfInstance', { instance: instanceName })
const {
composeData,
currentInstance,
@ -88,7 +90,7 @@ export async function logOutOfInstance (instanceName, message = `Logged out of $
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
const { verifyCredentials } = store.get()
verifyCredentials[instanceName] = thisVerifyCredentials
store.set({ verifyCredentials: verifyCredentials })
store.set({ verifyCredentials })
}
export async function updateVerifyCredentialsForInstance (instanceName) {
@ -109,13 +111,17 @@ export async function updateVerifyCredentialsForCurrentInstance () {
export async function updateInstanceInfo (instanceName) {
await cacheFirstUpdateAfter(
() => getInstanceInfo(instanceName),
() => {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName] && loggedInInstances[instanceName].access_token
return getInstanceInfo(instanceName, accessToken)
},
() => database.getInstanceInfo(instanceName),
info => database.setInstanceInfo(instanceName, info),
info => {
const { instanceInfos } = store.get()
instanceInfos[instanceName] = info
store.set({ instanceInfos: instanceInfos })
store.set({ instanceInfos })
}
)
}
@ -123,7 +129,7 @@ export async function updateInstanceInfo (instanceName) {
export function logOutOnUnauthorized (instanceName) {
return async error => {
if (error.message.startsWith('401:')) {
await logOutOfInstance(instanceName, `The access token was revoked, logged out of ${instanceName}`)
await logOutOfInstance(instanceName, formatIntl('intl.accessTokenRevoked', { instance: instanceName }))
}
throw error

Wyświetl plik

@ -1,7 +1,7 @@
import { store } from '../_store/store'
import { getLists } from '../_api/lists'
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync'
import { database } from '../_database/database'
import { store } from '../_store/store.js'
import { getLists } from '../_api/lists.js'
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync.js'
import { database } from '../_database/database.js'
async function syncLists (instanceName, syncMethod) {
const { loggedInInstances } = store.get()
@ -14,7 +14,7 @@ async function syncLists (instanceName, syncMethod) {
lists => {
const { instanceLists } = store.get()
instanceLists[instanceName] = lists
store.set({ instanceLists: instanceLists })
store.set({ instanceLists })
}
)
}

Wyświetl plik

@ -1,8 +1,9 @@
import { store } from '../_store/store'
import { uploadMedia } from '../_api/media'
import { toast } from '../_components/toast/toast'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { mediaUploadFileCache } from '../_utils/mediaUploadFileCache'
import { store } from '../_store/store.js'
import { uploadMedia } from '../_api/media.js'
import { toast } from '../_components/toast/toast.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { formatIntl } from '../_utils/formatIntl.js'
import { database } from '../_database/database.js'
export async function doMediaUpload (realm, file) {
const { currentInstance, accessToken } = store.get()
@ -13,7 +14,7 @@ export async function doMediaUpload (realm, file) {
if (composeMedia.length === 4) {
throw new Error('Only 4 media max are allowed')
}
mediaUploadFileCache.set(response.url, file)
await database.setCachedMediaFile(response.id, file)
composeMedia.push({
data: response,
file: { name: file.name },
@ -25,7 +26,7 @@ export async function doMediaUpload (realm, file) {
scheduleIdleTask(() => store.save())
} catch (e) {
console.error(e)
toast.say('Failed to upload media: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.failedToUploadMedia', { error: (e.message || '') }))
} finally {
store.set({ uploadingMedia: false })
}

Wyświetl plik

@ -1,5 +1,5 @@
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
import { store } from '../_store/store'
import { store } from '../_store/store.js'
export async function composeNewStatusMentioning (account) {
store.setComposeData('dialog', { text: `@${account.acct} ` })

Wyświetl plik

@ -1,8 +1,9 @@
import { store } from '../_store/store'
import { muteAccount, unmuteAccount } from '../_api/mute'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'
import { store } from '../_store/store.js'
import { muteAccount, unmuteAccount } from '../_api/mute.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -15,15 +16,14 @@ export async function setAccountMuted (accountId, mute, notifications, toastOnSu
}
await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) {
if (mute) {
toast.say('Muted account')
} else {
toast.say('Unmuted account')
}
/* no await */ toast.say(mute ? 'intl.mutedAccount' : 'intl.unmutedAccount')
}
emit('refreshAccountsList')
} catch (e) {
console.error(e)
toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || ''))
/* no await */ toast.say(mute
? formatIntl('intl.unableToMute', { error: (e.message || '') })
: formatIntl('intl.unableToUnmute', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,7 +1,8 @@
import { store } from '../_store/store'
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'
import { store } from '../_store/store.js'
import { muteConversation, unmuteConversation } from '../_api/muteConversation.js'
import { toast } from '../_components/toast/toast.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -13,14 +14,13 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
}
await database.setStatusMuted(currentInstance, statusId, mute)
if (toastOnSuccess) {
if (mute) {
toast.say('Muted conversation')
} else {
toast.say('Unmuted conversation')
}
/* no await */ toast.say(mute ? 'intl.mutedConversation' : 'intl.unmutedConversation')
}
} catch (e) {
console.error(e)
toast.say(`Unable to ${mute ? 'mute' : 'unmute'} conversation: ` + (e.message || ''))
/* no await */ toast.say(mute
? formatIntl('intl.unableToMuteConversation', { error: (e.message || '') })
: formatIntl('intl.unableToUnmuteConversation', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,8 +1,9 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { pinStatus, unpinStatus } from '../_api/pin'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { pinStatus, unpinStatus } from '../_api/pin.js'
import { database } from '../_database/database.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -13,17 +14,16 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
await unpinStatus(currentInstance, accessToken, statusId)
}
if (toastOnSuccess) {
if (pinned) {
toast.say('Pinned status')
} else {
toast.say('Unpinned status')
}
/* no await */ toast.say(pinned ? 'intl.pinnedStatus' : 'intl.unpinnedStatus')
}
store.setStatusPinned(currentInstance, statusId, pinned)
await database.setStatusPinned(currentInstance, statusId, pinned)
emit('updatePinnedStatuses')
} catch (e) {
console.error(e)
toast.say(`Unable to ${pinned ? 'pin' : 'unpin'} status: ` + (e.message || ''))
/* no await */ toast.say(pinned
? formatIntl('intl.unableToPinStatus', { error: (e.message || '') })
: formatIntl('intl.unableToUnpinStatus', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,28 +1,38 @@
import { store } from '../_store/store'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { database } from '../_database/database'
import { store } from '../_store/store.js'
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
import { database } from '../_database/database.js'
import {
getPinnedStatuses
} from '../_api/pinnedStatuses'
} from '../_api/pinnedStatuses.js'
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
// Pinned statuses aren't a "normal" timeline, so their blurhashes/plaintext need to be calculated specially
async function rehydratePinnedStatuses (statuses) {
await Promise.all(statuses.map(status => rehydrateStatusOrNotification({ status })))
return statuses
}
export async function updatePinnedStatusesForAccount (accountId) {
const { currentInstance, accessToken } = store.get()
await cacheFirstUpdateAfter(
() => getPinnedStatuses(currentInstance, accessToken, accountId),
async () => {
return rehydratePinnedStatuses(await getPinnedStatuses(currentInstance, accessToken, accountId))
},
async () => {
prepareToRehydrate() // start blurhash early to save time
const pinnedStatuses = await database.getPinnedStatuses(currentInstance, accountId)
if (!pinnedStatuses || !pinnedStatuses.every(Boolean)) {
throw new Error('missing pinned statuses in idb')
}
return pinnedStatuses
return rehydratePinnedStatuses(pinnedStatuses)
},
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
statuses => {
const { pinnedStatuses } = store.get()
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
pinnedStatuses[currentInstance][accountId] = statuses
store.set({ pinnedStatuses: pinnedStatuses })
store.set({ pinnedStatuses })
}
)
}

Wyświetl plik

@ -1,6 +1,7 @@
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls.js'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function getPoll (pollId) {
const { currentInstance, accessToken } = store.get()
@ -9,7 +10,7 @@ export async function getPoll (pollId) {
return poll
} catch (e) {
console.error(e)
toast.say('Unable to refresh poll: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.unableToRefreshPoll', { error: (e.message || '') }))
}
}
@ -20,6 +21,6 @@ export async function voteOnPoll (pollId, choices) {
return poll
} catch (e) {
console.error(e)
toast.say('Unable to vote in poll: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.unableToVoteInPoll', { error: (e.message || '') }))
}
}

Wyświetl plik

@ -1,5 +1,5 @@
import { store } from '../_store/store'
import { store } from '../_store/store.js'
export function setPostPrivacy (realm, postPrivacyKey) {
store.setComposeData(realm, { postPrivacy: postPrivacyKey })

Wyświetl plik

@ -1,6 +1,6 @@
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription'
import { store } from '../_store/store'
import { urlBase64ToUint8Array } from '../_utils/base64'
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription.js'
import { store } from '../_store/store.js'
import { urlBase64ToUint8Array } from '../_utils/base64.js'
const dummyApplicationServerKey = 'BImgAz4cF_yvNFp8uoBJCaGpCX4d0atNIFMHfBvAAXCyrnn9IMAFQ10DW_ZvBCzGeR4fZI5FnEi2JVcRE-L88jY='

Wyświetl plik

@ -1,12 +1,13 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { reblogStatus, unreblogStatus } from '../_api/reblog'
import { database } from '../_database/database'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { reblogStatus, unreblogStatus } from '../_api/reblog.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setReblogged (statusId, reblogged) {
const online = store.get()
if (!online) {
toast.say(`You cannot ${reblogged ? 'boost' : 'unboost'} while offline.`)
/* no await */ toast.say(reblogged ? 'intl.cannotReblogOffline' : 'intl.cannotUnreblogOffline')
return
}
const { currentInstance, accessToken } = store.get()
@ -19,7 +20,10 @@ export async function setReblogged (statusId, reblogged) {
await database.setStatusReblogged(currentInstance, statusId, reblogged)
} catch (e) {
console.error(e)
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
/* no await */ toast.say(reblogged
? formatIntl('intl.failedToReblog', { error: (e.message || '') })
: formatIntl('intl.failedToUnreblog', { error: (e.message || '') })
)
store.setStatusReblogged(currentInstance, statusId, !reblogged) // undo optimistic update
}
}

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

@ -1,13 +1,14 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { report } from '../_api/report'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { report } from '../_api/report.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function reportStatuses (account, statusIds, comment, forward) {
const { currentInstance, accessToken } = store.get()
try {
await report(currentInstance, accessToken, account.id, statusIds, comment, forward)
toast.say('Submitted report')
/* no await */ toast.say('intl.submittedReport')
} catch (e) {
toast.say('Failed to report: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.failedToReport', { error: (e.message || '') }))
}
}

Wyświetl plik

@ -1,7 +1,8 @@
import { store } from '../_store/store'
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
import { emit } from '../_utils/eventBus'
import { toast } from '../_components/toast/toast'
import { store } from '../_store/store.js'
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests.js'
import { emit } from '../_utils/eventBus.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
const {
@ -15,15 +16,14 @@ export async function setFollowRequestApprovedOrRejected (accountId, approved, t
await rejectFollowRequest(currentInstance, accessToken, accountId)
}
if (toastOnSuccess) {
if (approved) {
toast.say('Approved follow request')
} else {
toast.say('Rejected follow request')
}
/* no await */ toast.say(approved ? 'intl.approvedFollowRequest' : 'intl.rejectedFollowRequest')
}
emit('refreshAccountsList')
} catch (e) {
console.error(e)
toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || ''))
/* no await */ toast.say(approved
? formatIntl('intl.unableToApproveFollowRequest', { error: (e.message || '') })
: formatIntl('intl.unableToRejectFollowRequest', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,6 +1,7 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { search } from '../_api/search'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { search } from '../_api/search.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function doSearch () {
const { currentInstance, accessToken, queryInSearch } = store.get()
@ -15,7 +16,7 @@ export async function doSearch () {
})
}
} catch (e) {
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.searchError', { error: (e.message || '') }))
console.error(e)
} finally {
store.set({ searchLoading: false })

Wyświetl plik

@ -0,0 +1,27 @@
import { store } from '../_store/store.js'
import { notifyAccount, denotifyAccount } from '../_api/notify.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountNotified (accountId, notify, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
try {
let relationship
if (notify) {
relationship = await notifyAccount(currentInstance, accessToken, accountId)
} else {
relationship = await denotifyAccount(currentInstance, accessToken, accountId)
}
await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) {
/* no await */ toast.say(notify ? 'intl.subscribedAccount' : 'intl.unsubscribedAccount')
}
} catch (e) {
console.error(e)
/* no await */ toast.say(notify
? formatIntl('intl.unableToSubscribe', { error: (e.message || '') })
: formatIntl('intl.unableToUnsubscribe', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,7 +1,8 @@
import { store } from '../_store/store'
import { blockDomain, unblockDomain } from '../_api/blockDomain'
import { toast } from '../_components/toast/toast'
import { updateRelationship } from './accounts'
import { store } from '../_store/store.js'
import { blockDomain, unblockDomain } from '../_api/blockDomain.js'
import { toast } from '../_components/toast/toast.js'
import { updateRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -13,14 +14,13 @@ export async function setDomainBlocked (accountId, domain, block, toastOnSuccess
}
await updateRelationship(accountId)
if (toastOnSuccess) {
if (block) {
toast.say(`Hiding ${domain}`)
} else {
toast.say(`Unhiding ${domain}`)
}
/* no await */ toast.say(block ? 'intl.hidDomain' : 'intl.unhidDomain')
}
} catch (e) {
console.error(e)
toast.say(`Unable to ${block ? 'hide' : 'unhide'} domain: ` + (e.message || ''))
/* no await */ toast.say(block
? formatIntl('intl.unableToHideDomain', { error: (e.message || '') })
: formatIntl('intl.unableToUnhideDomain', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,7 +1,8 @@
import { store } from '../_store/store'
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { store } from '../_store/store.js'
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()
@ -9,14 +10,13 @@ export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
const relationship = await setShowReblogsApi(currentInstance, accessToken, accountId, showReblogs)
await updateLocalRelationship(currentInstance, accountId, relationship)
if (toastOnSuccess) {
if (showReblogs) {
toast.say('Showing boosts')
} else {
toast.say('Hiding boosts')
}
/* no await */ toast.say(showReblogs ? 'intl.showingReblogs' : 'intl.hidingReblogs')
}
} catch (e) {
console.error(e)
toast.say(`Unable to ${showReblogs ? 'show' : 'hide'} boosts: ` + (e.message || ''))
/* no await */ toast.say(showReblogs
? formatIntl('intl.unableToShowReblogs', { error: (e.message || '') })
: formatIntl('intl.unableToHideReblogs', { error: (e.message || '') })
)
}
}

Wyświetl plik

@ -1,5 +1,6 @@
import { toast } from '../_components/toast/toast'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
import { toast } from '../_components/toast/toast.js'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function shareStatus (status) {
try {
@ -9,6 +10,6 @@ export async function shareStatus (status) {
url: status.url
})
} catch (e) {
toast.say('Unable to share: ' + (e.message || ''))
/* no await */ toast.say(formatIntl('intl.unableToShare', { error: (e.message || '') }))
}
}

Wyświetl plik

@ -0,0 +1,36 @@
import { store } from '../_store/store.js'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
import { database } from '../_database/database.js'
import { doMediaUpload } from './media.js'
// show a compose dialog, typically invoked by the Web Share API or a PWA shortcut
export async function showComposeDialog () {
const { isUserLoggedIn } = store.get()
if (!isUserLoggedIn) {
return
}
const importShowComposeDialogPromise = importShowComposeDialog() // start promise early
const data = await database.getWebShareData()
if (data) {
await database.deleteWebShareData() // only need this data once; it came from Web Share (service worker)
}
console.log('share data', data)
const { title, text, url, file } = (data || {})
// url is currently ignored on Android, but one can dream
// https://web.dev/web-share-target/#verifying-shared-content
const composeText = [title, text, url].filter(Boolean).join('\n\n')
store.clearComposeData('dialog')
store.setComposeData('dialog', { text: composeText })
store.save()
const showComposeDialog = await importShowComposeDialogPromise
showComposeDialog()
if (file) { // start the upload once the dialog is in view so it shows the loading spinner and everything
/* no await */ doMediaUpload('dialog', file)
}
}

Wyświetl plik

@ -1,8 +1,8 @@
import { showMoreItemsForCurrentTimeline } from './timeline'
import { scrollToTop } from '../_utils/scrollToTop'
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid'
import { store } from '../_store/store'
import { tryToFocusElement } from '../_utils/tryToFocusElement'
import { showMoreItemsForCurrentTimeline } from './timeline.js'
import { scrollToTop } from '../_utils/scrollToTop.js'
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid.js'
import { store } from '../_store/store.js'
import { tryToFocusElement } from '../_utils/tryToFocusElement.js'
export function showMoreAndScrollToTop () {
// Similar to Twitter, pressing "." will click the "show more" button and select

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