Porównaj commity

...

194 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
565 zmienionych plików z 15613 dodań i 7195 usunięć

Wyświetl plik

@ -1,99 +0,0 @@
version: 2.1
workflows:
version: 2
build:
jobs:
- build_and_test
jobs:
build_and_test:
working_directory: ~/pinafore
docker:
# see https://discuss.circleci.com/t/build-failed-the-engine-node-is-incompatible-with-this-module-expected-version-12-x-got-14-15-0/37921/7
# we want Node v12, not v14
- image: circleci/ruby@sha256:b018ec2a8f0bbf06880735d2801402bad316c465edb60663be83ac8f1086b805
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: pinafore
POSTGRES_PASSWORD: pinafore
POSTGRES_DB: pinafore_development
BROWSER: chrome:headless
- image: circleci/redis:5-alpine
steps:
- checkout
- run:
name: Install system dependencies
command: |
sudo apt-get update
sudo apt-get install -y ffmpeg fonts-noto-color-emoji libicu-dev libidn11-dev libprotobuf-dev postgresql-contrib protobuf-compiler
- run:
name: Check node version
command: node -v
- restore_cache:
name: Restore yarn cache
key: yarn-v1-{{ checksum "yarn.lock" }}
- run:
name: Yarn install
command: yarn install --immutable
- run:
name: Clone mastodon
command: yarn clone-mastodon
- restore_cache:
name: Restore bundler cache
key: bundler-v2-{{ checksum "mastodon/Gemfile.lock" }}
- run:
name: Lint
command: yarn lint
- run:
name: Unit tests
command: yarn test-unit
- run:
name: Install mastodon
command: yarn install-mastodon
- run:
name: Wait for postgres to be ready
command: |
for i in `seq 1 10`;
do
nc -z localhost 5432 && echo Success && exit 0
echo -n .
sleep 1
done
echo Failed waiting for postgres && exit 1
- run:
name: Wait for redis to be ready
command: |
for i in `seq 1 10`;
do
nc -z localhost 6379 && echo Success && exit 0
echo -n .
sleep 1
done
echo Failed waiting for redis && exit 1
- run:
name: Copy vercel.json
command: cp vercel.json vercel-old.json
- run:
name: Build
command: yarn build
- run:
name: Check vercel.json unchanged
command: |
if ! diff -q vercel-old.json vercel.json &>/dev/null; then
diff vercel-old.json vercel.json
echo "vercel.json changed, run yarn build and make sure everything looks okay"
exit 1
fi
- run:
name: Integration tests
command: yarn test-in-ci
- save_cache:
name: Save yarn cache
key: yarn-v1-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- save_cache:
name: Save bundler cache
key: bundler-v2-{{ checksum "mastodon/Gemfile.lock" }}
paths:
- mastodon/vendor/bundle

Wyświetl plik

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

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

3
.gitignore vendored
Wyświetl plik

@ -8,8 +8,11 @@
/static/icons.svg
/static/inline-script.js.map
/static/emoji-*.json
/static/manifest.json
/static/TwemojiCountryFlags.woff2
/src/inline-script/checksum.js
yarn-error.log
.now
.vercel
/webpack/*.cjs

Wyświetl plik

@ -8,5 +8,6 @@
/static/icons.svg
/static/inline-script.js.map
/static/emoji-*.json
/static/manifest.json
/src/inline-script/checksum.js
yarn-error.log

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`

Wyświetl plik

@ -1,6 +1,6 @@
# Using Alpine to keep the images smaller
# Change to using the official NodeJS Alpine container
FROM node:alpine
FROM node:16-alpine
# Pushing all files into image
WORKDIR /app

Wyświetl plik

@ -1,4 +1,6 @@
# Pinafore [![Build status](https://circleci.com/gh/nolanlawson/pinafore.svg?style=svg)](https://app.circleci.com/pipelines/gh/nolanlawson/pinafore)
# Pinafore [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/)
_**Note:** Pinafore is unmaintained. Read [this](https://nolanlawson.com/2023/01/09/retiring-pinafore/). Original documentation follows._
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
@ -6,7 +8,7 @@ Pinafore is available at [pinafore.social](https://pinafore.social). Beta releas
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) if Pinafore cannot connect to your instance.
For updates and support, follow [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
For updates and support, follow [@pinafore@fosstodon.org](https://fosstodon.org/@pinafore).
## Browser support
@ -52,7 +54,7 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
## Building
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
Pinafore requires [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com).
To build Pinafore for production, first install dependencies:
@ -94,12 +96,13 @@ To keep your version of Pinafore up to date, you can use `git` to check out the
Pinafore is a static site. When you run `yarn build`, static files will be
written to `__sapper__/export`.
In theory you could host these static files yourself (e.g. using nginx or Apache), but
it's not recommended, because:
It is _not_ recommended to directly expose these files when self-hosting. Instead, you should use `node server.js` (e.g. with an
nginx or Apache proxy in front). This adds several things you don't get from the raw static files:
- You'd have to set the [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers yourself,
which are an important security feature.
- Some routes are dynamic and need to be routed to the correct static file.
- [CSP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (important for security)
- Certain dynamic routes (less important because of Service Worker managing routing, but certain things could break if Service Workers are disabled in the user's browser)
Having an [nginx config generator](https://github.com/nolanlawson/pinafore/issues/1878) is currently an open issue.
## Developing and testing

Wyświetl plik

@ -1,10 +1,13 @@
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import { LOCALE } from '../src/routes/_static/intl'
import { LOCALE } from '../src/routes/_static/intl.js'
import { getIntl, trimWhitespace } from './getIntl.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const copyFile = promisify(fs.copyFile)
// Try 'en-US' first, then 'en' if that doesn't exist
const PREFERRED_LOCALES = [LOCALE, LOCALE.split('-')[0]]
@ -34,7 +37,7 @@ async function getFirstExistingEmojiI18nFile () {
}
}
async function main () {
async function buildEmojiI18nFile () {
const json = await getFirstExistingEmojiI18nFile()
if (!json) {
@ -48,6 +51,36 @@ async function main () {
)
}
async function buildManifestJson () {
const template = await readFile(path.resolve(__dirname, '../src/build/manifest.json'), 'utf8')
// replace {@intl.foo}
const output = template
.replace(/{intl\.([^}]+)}/g, (match, p1) => trimWhitespace(getIntl(p1)))
await writeFile(
path.resolve(__dirname, '../static/manifest.json'),
JSON.stringify(JSON.parse(output)), // minify json
'utf8'
)
}
async function buildFlagEmojiFile () {
await copyFile(path.resolve(
__dirname,
'../node_modules/country-flag-emoji-polyfill/dist/TwemojiCountryFlags.woff2'
), path.resolve(
__dirname, '../static/TwemojiCountryFlags.woff2'
))
}
async function main () {
await Promise.all([
buildEmojiI18nFile(),
buildManifestJson(),
buildFlagEmojiFile()
])
}
main().catch(err => {
console.error(err)
process.exit(1)

Wyświetl plik

@ -5,13 +5,12 @@ import path from 'path'
import { rollup } from 'rollup'
import { terser } from 'rollup-plugin-terser'
import replace from '@rollup/plugin-replace'
import fromPairs from 'lodash-es/fromPairs'
import { themes } from '../src/routes/_static/themes'
import terserOptions from './terserOptions'
import { themes } from '../src/routes/_static/themes.js'
import terserOptions from './terserOptions.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
const themeColors = Object.fromEntries(themes.map(_ => ([_.name, _.color])))
export async function buildInlineScript () {
const inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
@ -20,8 +19,11 @@ export async function buildInlineScript () {
input: inlineScriptPath,
plugins: [
replace({
'process.browser': true,
'process.env.THEME_COLORS': JSON.stringify(themeColors)
values: {
'process.browser': true,
'process.env.THEME_COLORS': JSON.stringify(themeColors)
},
preventAssignment: true
}),
// TODO: can't disable terser at all, it causes the CSP checksum to stop working
// because the HTML gets minified as some point so the checksums don't match.
@ -39,7 +41,7 @@ export async function buildInlineScript () {
const checksum = crypto.createHash('sha256').update(fullCode, 'utf8').digest('base64')
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
`export default ${JSON.stringify(checksum)}`, 'utf8')
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
map.toString(), 'utf8')

Wyświetl plik

@ -3,10 +3,12 @@ import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import cssDedoupe from 'css-dedoupe'
import { TextDecoder } from 'text-encoding'
import textEncodingPackage from 'text-encoding'
const { TextDecoder } = textEncodingPackage
const writeFile = promisify(fs.writeFile)
const readdir = promisify(fs.readdir)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const globalScss = path.join(__dirname, '../src/scss/global.scss')
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -5,11 +5,12 @@
import path from 'path'
import fs from 'fs'
import { promisify } from 'util'
import { routes } from '../__sapper__/service-worker'
import cloneDeep from 'lodash-es/cloneDeep'
import inlineScriptChecksum from '../src/inline-script/checksum'
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums'
import { routes } from '../__sapper__/service-worker.js'
import { cloneDeep } from '../src/routes/_utils/lodash-lite.js'
import inlineScriptChecksum from '../src/inline-script/checksum.js'
import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChecksums.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)
const JSON_TEMPLATE = {
@ -20,15 +21,8 @@ const JSON_TEMPLATE = {
github: {
silent: true
},
builds: [
{
src: 'package.json',
use: '@now/static-build',
config: {
distDir: '__sapper__/export'
}
}
],
buildCommand: 'yarn build',
outputDirectory: '__sapper__/export',
routes: [
{
src: '^/service-worker\\.js$',
@ -50,7 +44,13 @@ const JSON_TEMPLATE = {
}
},
{
src: '^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp)$',
src: '^/.*\\.(png|jpe?g)$',
headers: {
'cache-control': 'public,max-age=31536000,immutable'
}
},
{
src: '^/.*\\.(css|json|svg|map|txt|gz|webapp|woff|woff2)$',
headers: {
'cache-control': 'public,max-age=3600'
}
@ -63,7 +63,7 @@ const SCRIPT_CHECKSUMS = [inlineScriptChecksum]
.map(_ => `'sha256-${_}'`)
.join(' ')
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=()'
const PERMISSIONS_POLICY = 'sync-xhr=(),document-domain=(),interest-cohort=()'
const HTML_HEADERS = {
'cache-control': 'public,max-age=3600',

Wyświetl plik

@ -2,16 +2,15 @@ import { promisify } from 'util'
import childProcessPromise from 'child-process-promise'
import path from 'path'
import fs from 'fs'
import { envFile, RUBY_VERSION } from './mastodon-config'
import { envFile, GIT_TAG, GIT_URL, RUBY_VERSION } from './mastodon-config.js'
import esMain from 'es-main'
const exec = childProcessPromise.exec
const stat = promisify(fs.stat)
const writeFile = promisify(fs.writeFile)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const dir = __dirname
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
const GIT_TAG = 'v3.3.0'
const mastodonDir = path.join(dir, '../mastodon')
export default async function cloneMastodon () {
@ -25,7 +24,7 @@ export default async function cloneMastodon () {
}
}
if (require.main === module) {
if (esMain(import.meta)) {
cloneMastodon().catch(err => {
console.error(err)
process.exit(1)

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

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

@ -2,12 +2,14 @@ import { promisify } from 'util'
import childProcessPromise from 'child-process-promise'
import path from 'path'
import fs from 'fs'
import { DB_NAME, DB_PASS, DB_USER, mastodonDir, env } from './mastodon-config'
import { DB_NAME, DB_PASS, DB_USER, mastodonDir, env } from './mastodon-config.js'
import mkdirp from 'mkdirp'
import esMain from 'es-main'
const exec = childProcessPromise.exec
const stat = promisify(fs.stat)
const writeFile = promisify(fs.writeFile)
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const dir = __dirname
async function setupMastodonDatabase () {
@ -41,8 +43,8 @@ async function setupMastodonDatabase () {
async function installMastodonDependencies () {
const cwd = mastodonDir
const installCommands = [
'gem update --system',
'gem install bundler foreman',
'gem install bundler -v 2.3.26 --no-document',
'gem install foreman -v 0.87.2 --no-document',
'bundle config set --local frozen \'true\'',
'bundle install',
'yarn --pure-lockfile'
@ -69,7 +71,7 @@ export default async function installMastodon () {
await installMastodonDependencies()
}
if (require.main === module) {
if (esMain(import.meta)) {
installMastodon().catch(err => {
console.error(err)
process.exit(1)

Wyświetl plik

@ -15,12 +15,15 @@ DB_PORT=${DB_PORT}
DB_USER=${DB_USER}
DB_NAME=${DB_NAME}
DB_PASS=${DB_PASS}
BIND=0.0.0.0
`
// Need a Ruby version that CircleCI bundles with Node v12, not Node v14 which doesn't
// work for streaming
export const RUBY_VERSION = '2.6.6'
export const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
export const GIT_TAG = 'v4.0.2'
export const RUBY_VERSION = '3.0.4'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
export const mastodonDir = path.join(__dirname, '../mastodon')
export const env = Object.assign({}, process.env, {

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,10 +1,10 @@
import { restoreMastodonData } from './restore-mastodon-data'
import { restoreMastodonData } from './restore-mastodon-data.js'
import childProcessPromise from 'child-process-promise'
import fs from 'fs'
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start'
import cloneMastodon from './clone-mastodon'
import installMastodon from './install-mastodon'
import { mastodonDir, env } from './mastodon-config'
import { waitForMastodonUiToStart, waitForMastodonApiToStart } from './wait-for-mastodon-to-start.js'
import cloneMastodon from './clone-mastodon.js'
import installMastodon from './install-mastodon.js'
import { mastodonDir, env } from './mastodon-config.js'
const spawn = childProcessPromise.spawn
@ -15,7 +15,7 @@ async function runMastodon () {
const cwd = mastodonDir
const promise = spawn('foreman', ['start'], { cwd, env })
// don't bother writing to mastodon.log in CI; we can't read the file anyway
const logFile = process.env.CIRCLECI ? '/dev/null' : 'mastodon.log'
const logFile = process.env.CI ? '/dev/null' : 'mastodon.log'
const log = fs.createWriteStream(logFile, { flags: 'a' })
childProc = promise.childProcess
childProc.stdout.pipe(log)

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

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 => {

Wyświetl plik

@ -5,9 +5,17 @@ Basically think of it as a "lay of the land" as well as "weird unusual stuff tha
## Overview
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
Pinafore uses [SvelteJS](https://svelte.technology) v2 and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
are some quirks, which are described below. This list of quirks is non-exhaustive.
## Why Svelte v2 / Sapper ?
There is [no upgrade path from Svelte v2 to v3](https://github.com/sveltejs/svelte/issues/2462). Doing so would require manually migrating every component over. And in the end, it would probably not change the UX (user experience) of Pinafore – only the DX (developer experience).
Similarly, [Sapper would need to be migrated to SvelteKit](https://kit.svelte.dev/docs/migrating). Since Pinafore generates static files, there is probably not much benefit in moving from Sapper to SvelteKit.
For this reason, Pinafore has been stuck on Svelte v2 and Sapper for a long time. Migrating it is not something I've considered. The [v2 Svelte docs](https://v2.svelte.dev/) are still online, and share many similarities with Svelte v3.
## Prebuild process
The `template.html` is itself templated. The "template template" has some inline scripts, CSS, and SVGs

Wyświetl plik

@ -16,5 +16,4 @@ or
LOCALE=fr yarn dev
There is also an experimental `LOCALE_DIRECTION` environment variable for the direction (LTR versus RTL) which is
exposed to the source code while building.
To host a localized version of Pinafore using Vercel, you can see this example: [buildCommand in vercel.json for Spanish](https://github.com/nvdaes/vercelPinafore/blob/45c70fb2088fe5f2380a729dab83e6f3ab4e6291/vercel.json#L9).

Wyświetl plik

@ -1,125 +1,138 @@
{
"name": "pinafore",
"description": "Alternative web client for Mastodon",
"version": "1.24.0",
"version": "2.6.0",
"type": "module",
"engines": {
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0 || ^20.0.0"
},
"scripts": {
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
"dev": "run-s build-template-html build-assets serve-dev",
"dev": "run-s before-build serve-dev",
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 node -r esm ./node_modules/.bin/sapper dev",
"before-build": "run-s build-template-html build-assets",
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper dev",
"before-build": "run-p build-template-html build-assets build-webpack-config",
"build": "cross-env NODE_ENV=production run-s build-steps",
"build-steps": "run-s before-build sapper-export build-vercel-json",
"sapper-build": "node -r esm ./node_modules/.bin/sapper build",
"sapper-build": "cross-env WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper build",
"start": "node server.js",
"build-and-start": "run-s build start",
"build-template-html": "node -r esm ./bin/build-template-html.js",
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
"build-assets": "node -r esm ./bin/build-assets.js",
"clone-mastodon": "node -r esm ./bin/clone-mastodon.js",
"install-mastodon": "node -r esm ./bin/install-mastodon.js",
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
"build-template-html": "node ./bin/build-template-html.js",
"build-template-html-watch": "node ./bin/build-template-html.js --watch",
"build-assets": "node ./bin/build-assets.js",
"build-webpack-config": "rollup -c ./webpack/rollup.config.js",
"clone-mastodon": "node ./bin/clone-mastodon.js",
"install-mastodon": "node ./bin/install-mastodon.js",
"run-mastodon": "node ./bin/run-mastodon.js",
"test": "cross-env BROWSER=chrome:headless run-s test-browser",
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
"test-mastodon-suite0": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite0",
"test-mastodon-suite1": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite1",
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
"testcafe-suite0": "cross-env-shell testcafe -c 2 $BROWSER tests/spec/0*",
"testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*",
"test-unit": "NODE_ENV=test mocha -r esm -r bin/browser-shim.js tests/unit/",
"test-in-ci": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon",
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
"test-unit": "NODE_ENV=test mocha -r bin/browser-shim.js tests/unit/",
"test-in-ci-suite0": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite0",
"test-in-ci-suite1": "cross-env BROWSER=chrome:headless run-p --race run-mastodon start test-mastodon-suite1",
"test-vercel-json": "run-s test-vercel-json-copy build test-vercel-json-test",
"test-vercel-json-copy": "./bin/copy-vercel-json.sh",
"test-vercel-json-test": "./bin/test-vercel-json-unchanged.sh",
"wait-for-mastodon-to-start": "node bin/wait-for-mastodon-to-start.js",
"wait-for-mastodon-data": "node bin/wait-for-mastodon-data.js",
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
"sapper-export": "cross-env PORT=22939 node -r esm ./node_modules/.bin/sapper export",
"sapper-export": "cross-env PORT=22939 WEBPACK_CONFIG_FILE=webpack/webpack.config.cjs SERVER_FILE_EXT=cjs node ./node_modules/sapper/sapper export",
"print-export-info": "node ./bin/print-export-info.js",
"export-steps": "run-s before-build sapper-export print-export-info",
"export": "cross-env NODE_ENV=production run-s export-steps",
"now-build": "run-s export",
"build-vercel-json": "node -r esm bin/build-vercel-json.js"
"build-vercel-json": "node bin/build-vercel-json.js"
},
"dependencies": {
"@formatjs/intl-listformat": "^5.0.10",
"@formatjs/intl-locale": "^2.4.14",
"@formatjs/intl-pluralrules": "^4.0.6",
"@formatjs/intl-relativetimeformat": "^8.0.4",
"@rollup/plugin-replace": "^2.3.3",
"@formatjs/intl-listformat": "^7.1.3",
"@formatjs/intl-locale": "^3.0.7",
"@formatjs/intl-pluralrules": "^5.1.4",
"@formatjs/intl-relativetimeformat": "^11.1.4",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-replace": "^2.4.2",
"@stdlib/utils-noop": "^0.0.13",
"arrow-key-navigation": "^1.2.0",
"blurhash": "^1.1.3",
"cheerio": "^1.0.0-rc.3",
"blurhash": "^1.1.5",
"cheerio": "1.0.0-rc.10",
"child-process-promise": "^2.2.1",
"chokidar": "^3.5.1",
"chokidar": "^3.5.3",
"circular-dependency-plugin": "^5.2.2",
"compression": "^1.7.4",
"country-flag-emoji-polyfill": "^0.1.4",
"cross-env": "^7.0.3",
"css-dedoupe": "^0.1.1",
"emoji-picker-element": "^1.4.0",
"emoji-picker-element-data": "^1.1.0",
"emoji-regex": "^9.2.1",
"emoji-picker-element": "^1.13.1",
"emoji-picker-element-data": "^1.3.0",
"emoji-regex": "^10.2.1",
"encoding": "^0.1.13",
"es-main": "^1.2.0",
"escape-html": "^1.0.3",
"esm": "^3.2.25",
"events-light": "^1.0.5",
"express": "^4.17.1",
"express": "^4.18.2",
"file-api": "^0.10.4",
"file-drop-element": "^1.0.1",
"file-loader": "^6.2.0",
"focus-visible": "^5.2.0",
"form-data": "^3.0.0",
"format-message-interpret": "^6.2.3",
"format-message-parse": "^6.2.3",
"glob": "^7.1.6",
"form-data": "^4.0.0",
"format-message-interpret": "^6.2.4",
"format-message-parse": "^6.2.4",
"glob": "^7.2.0",
"is-emoji-supported": "^0.0.5",
"li": "^1.3.0",
"localstorage-memory": "^1.0.3",
"lodash-es": "4.17.15",
"lodash-webpack-plugin": "^0.11.6",
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.1",
"node-fetch": "^2.6.7",
"npm-run-all": "^4.1.5",
"p-any": "^3.0.0",
"p-any": "^4.0.0",
"page-lifecycle": "^0.1.2",
"performance-now": "^2.1.0",
"pinch-zoom-element": "^1.1.1",
"promise-worker": "^2.0.1",
"prop-types": "^15.7.2",
"prop-types": "^15.8.1",
"requestidlecallback": "^0.3.0",
"rollup": "^2.26.10",
"rollup-plugin-babel": "^4.4.0",
"rollup": "^2.67.3",
"rollup-plugin-terser": "^7.0.2",
"rtl-detect": "^1.0.2",
"sapper": "nolanlawson/sapper#for-pinafore-25",
"sass": "^1.32.8",
"rtl-detect": "^1.0.4",
"safari-14-idb-fix": "^1.0.4",
"sapper": "nolanlawson/sapper#for-pinafore-26",
"sass": "^1.56.1",
"stringz": "^2.1.0",
"svelte": "^2.16.1",
"svelte-extras": "^2.0.2",
"svelte-loader": "^2.13.6",
"svelte-transitions": "^1.2.0",
"svgo": "^1.3.2",
"terser-webpack-plugin": "^5.1.1",
"tesseract.js": "^2.1.4",
"svgo": "^2.8.0",
"terser-webpack-plugin": "^5.3.6",
"tesseract.js": "^2.1.5",
"tesseract.js-core": "^2.2.0",
"text-encoding": "^0.7.0",
"tiny-queue": "^0.2.1",
"webpack": "^5.23.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"worker-loader": "^3.0.8"
},
"devDependencies": {
"assert": "^2.0.0",
"eslint-plugin-html": "^6.1.1",
"fake-indexeddb": "^3.1.2",
"globby": "^11.0.1",
"husky": "^5.0.9",
"lint-staged": "^10.5.4",
"mocha": "^8.3.0",
"standard": "^16.0.3",
"testcafe": "^1.11.0",
"vercel": "^20.1.0"
},
"engines": {
"node": ">= 8"
"eslint-plugin-html": "^7.1.0",
"fake-indexeddb": "^4.0.0",
"globby": "^13.1.2",
"husky": "^8.0.2",
"lint-staged": "^13.0.3",
"mocha": "^10.1.0",
"standard": "^17.0.0",
"testcafe": "^1.20.1"
},
"standard": {
"ignore": [
"webpack/*.cjs"
],
"globals": [
"AbortController",
"Blob",
@ -185,5 +198,9 @@
"lint-staged": {
"*.js": "standard --fix",
"*.html": "standard --fix --plugin html 'src/routes/**/*.html'"
},
"volta": {
"node": "14.21.1",
"yarn": "1.22.19"
}
}

Wyświetl plik

@ -1,9 +1,16 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import express from 'express'
import compression from 'compression'
const path = require('path')
const express = require('express')
const compression = require('compression')
const { routes: rawRoutes } = require('./vercel.json')
const __dirname = path.dirname(new URL(import.meta.url).pathname)
// JSON files not supported in ESM yet
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-import-json
const vercelJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'vercel.json'), 'utf8'))
const { routes: rawRoutes } = vercelJson
const { PORT = 4002 } = process.env
const app = express()

Wyświetl plik

@ -1,9 +1,9 @@
{
"background_color": "#ffffff",
"theme_color": "#4169e1",
"name": "Pinafore for Mastodon",
"short_name": "Pinafore",
"description": "Alternative web client for Mastodon, focused on speed and simplicity.",
"name": "{intl.longAppName}",
"short_name": "{intl.appName}",
"description": "{intl.appDescription}",
"categories": [
"social"
],
@ -114,9 +114,7 @@
],
"shortcuts": [
{
"name": "Write a toot",
"short_name": "New toot",
"description": "Start composing a new toot",
"name": "{intl.newStatus}",
"url": "/?pwa=true&compose=true",
"icons": [
{
@ -126,9 +124,7 @@
]
},
{
"name": "View notifications",
"short_name": "Notifications",
"description": "View your new notifications",
"name": "{intl.notifications}",
"url": "/notifications?pwa=true",
"icons": [
{

Wyświetl plik

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

Wyświetl plik

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

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

Wyświetl plik

@ -170,12 +170,12 @@ export default {
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
·
{name}
`,
pinLabel: `{label} {pinnable, select,
true {
@ -188,8 +188,6 @@ export default {
}`,
pinPage: 'Hefte {label} an',
// Status composition
overLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} über der Beschränkung',
underLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} übrig',
composeStatus: 'Tröt erstellen',
postStatus: 'Tröt!',
contentWarning: 'Inhaltswarnung',

Wyświetl plik

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategorien',
emojiUnsupportedMessage: 'Dein Browser unterstützt keine farbigen Emojis.',
favoritesLabel: 'Favoriten',
loadingMessage: 'Wird geladen…',
networkErrorMessage: 'Konnte Emoji nicht laden. Versuche, die Seite neu zu laden.',
regionLabel: 'Emoji auswählen',
searchDescription: 'Wenn Suchergebnisse verfügbar sind, wähle sie mit Pfeil rauf und runter, dann Eingabetaste, aus.',
searchLabel: 'Suchen',
searchResultsLabel: 'Suchergebnisse',
skinToneDescription: 'Wenn angezeigt, nutze Pfeiltasten rauf und runter zum Auswählen, Eingabe zum Akzeptieren.',
skinToneLabel: 'Wähle einen Hautton (aktuell {skinTone})',
skinTonesLabel: 'Hauttöne',
skinTones: [
'Standard',
'Hell',
'Mittel-hell',
'Mittel',
'Mittel-dunkel',
'Dunkel'
],
categories: {
custom: 'Benutzerdefiniert',
'smileys-emotion': 'Smileys und Emoticons',
'people-body': 'Menschen und Körper',
'animals-nature': 'Tiere und Natur',
'food-drink': 'Essen und Trinken',
'travel-places': 'Reisen und Orte',
activities: 'Aktivitäten',
objects: 'Objekte',
symbols: 'Symbole',
flags: 'Flaggen'
}
}

Wyświetl plik

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Catégories',
emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.',
favoritesLabel: 'Favoris',
loadingMessage: 'Chargement en cours…',
networkErrorMessage: 'Impossible de charger les emojis. Veuillez essayer de recharger.',
regionLabel: 'Choisir un emoji',
searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.',
searchLabel: 'Rechercher',
searchResultsLabel: 'Résultats',
skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.',
skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})',
skinTonesLabel: 'Couleurs de peau',
skinTones: [
'Défaut',
'Clair',
'Moyennement clair',
'Moyen',
'Moyennement sombre',
'Sombre'
],
categories: {
custom: 'Customisé',
'smileys-emotion': 'Les smileyes et les émoticônes',
'people-body': 'Les gens et le corps',
'animals-nature': 'Les animaux et la nature',
'food-drink': 'La nourriture et les boissons',
'travel-places': 'Les voyages et les endroits',
activities: 'Les activités',
objects: 'Les objets',
symbols: 'Les symbols',
flags: 'Les drapeaux'
}
}

Wyświetl plik

@ -27,6 +27,9 @@ export default {
Here is the <a href="/settings/about#privacy-policy" rel="prefetch">privacy policy</a>.
</p>
`,
// Manifest
longAppName: 'Pinafore for Mastodon',
newStatus: 'New toot',
// Generic UI
loading: 'Loading',
okay: 'OK',
@ -150,6 +153,8 @@ export default {
<li><kbd>f</kbd> to favorite</li>
<li><kbd>b</kbd> to boost</li>
<li><kbd>r</kbd> to reply</li>
<li><kbd>Escape</kbd> to close reply</li>
<li><kbd>a</kbd> to bookmark</li>
<li><kbd>i</kbd> to open images, video, or audio</li>
<li><kbd>y</kbd> to show or hide sensitive media</li>
<li><kbd>m</kbd> to mention the author</li>
@ -171,12 +176,12 @@ export default {
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
·
{name}
`,
pinLabel: `{label} {pinnable, select,
true {
@ -189,8 +194,6 @@ export default {
}`,
pinPage: 'Pin {label}',
// Status composition
overLimit: '{count} {count, plural, =1 {character} other {characters}} over limit',
underLimit: '{count} {count, plural, =1 {character} other {characters}} remaining',
composeStatus: 'Compose toot',
postStatus: 'Toot!',
contentWarning: 'Content warning',
@ -202,7 +205,7 @@ export default {
edit: 'Edit',
delete: 'Delete',
description: 'Description',
descriptionLabel: 'Describe for the visually impaired (image, video) or auditorily impaired (audio, video)',
descriptionLabel: 'Describe for visually impaired (image, video) or auditorily impaired (audio, video) people',
markAsSensitive: 'Mark media as sensitive',
// Polls
createPoll: 'Create poll',
@ -226,7 +229,7 @@ export default {
postPrivacyLabel: 'Adjust privacy (currently {label})',
addContentWarning: 'Add content warning',
removeContentWarning: 'Remove content warning',
altLabel: 'Describe for the visually impaired',
altLabel: 'Describe for visually impaired people',
extractText: 'Extract text from image',
extractingText: 'Extracting text…',
extractingTextCompletion: 'Extracting text ({percent}% complete)…',
@ -306,6 +309,10 @@ export default {
true {(follow requested)}
other {}
}`,
notify: 'Subscribe to {account}',
denotify: 'Unsubscribe from {account}',
subscribedAccount: 'Subscribed to account',
unsubscribedAccount: 'Unsubscribed from account',
unblock: 'Unblock',
nameAndFollowing: 'Name and following',
clickToSeeAvatar: 'Click to see avatar',
@ -361,6 +368,7 @@ export default {
general: 'General',
generalSettings: 'General settings',
showSensitive: 'Show sensitive media by default',
showAllSpoilers: 'Expand content warnings by default',
showPlain: 'Show a plain gray color for sensitive media',
allSensitive: 'Treat all media as sensitive',
largeMedia: 'Show large inline images and videos',
@ -375,6 +383,8 @@ export default {
theme: 'Theme',
themeForInstance: 'Theme for {instance}',
disableCustomScrollbars: 'Disable custom scrollbars',
bottomNav: 'Place the navigation bar at the bottom of the screen',
centerNav: 'Center the navigation bar',
preferences: 'Preferences',
hotkeySettings: 'Hotkey settings',
disableHotkeys: 'Disable all hotkeys',
@ -466,6 +476,7 @@ export default {
newFollowers: 'New followers',
reblogs: 'Boosts',
pollResults: 'Poll results',
subscriptions: 'Subscribed toots',
needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?',
failedToUpdatePush: 'Failed to update push notification settings: {error}',
// Themes
@ -486,6 +497,9 @@ export default {
other {video}
}: {description}`,
accountFollowedYou: '{name} followed you, {account}',
accountSignedUp: '{name} signed up, {account}',
accountRequestedFollow: '{name} requested to follow you, {account}',
accountReported: '{name} filed a report, {account}',
reblogCountsHidden: 'Boost counts hidden',
favoriteCountsHidden: 'Favorite counts hidden',
rebloggedTimes: `Boosted {count, plural,
@ -500,9 +514,17 @@ export default {
rebloggedYou: 'boosted your toot',
favoritedYou: 'favorited your toot',
followedYou: 'followed you',
edited: 'edited their toot',
requestedFollow: 'requested to follow you',
reported: 'filed a report',
signedUp: 'signed up',
posted: 'posted',
pollYouCreatedEnded: 'A poll you created has ended',
pollYouVotedEnded: 'A poll you voted on has ended',
reblogged: 'boosted',
favorited: 'favorited',
unreblogged: 'unboosted',
unfavorited: 'unfavorited',
showSensitiveMedia: 'Show sensitive media',
hideSensitiveMedia: 'Hide sensitive media',
clickToShowSensitive: 'Sensitive content. Click to show.',
@ -510,6 +532,7 @@ export default {
// Accessible status labels
accountRebloggedYou: '{account} boosted your toot',
accountFavoritedYou: '{account} favorited your toot',
accountEdited: '{account} edited their toot',
rebloggedByAccount: 'Boosted by {account}',
contentWarningContent: 'Content warning: {spoiler}',
hasMedia: 'has media',
@ -627,6 +650,8 @@ export default {
unableToShowReblogs: 'Unable to show boosts: {error}',
unableToHideReblogs: 'Unable to hide boosts: {error}',
unableToShare: 'Unable to share: {error}',
unableToSubscribe: 'Unable to subscribe: {error}',
unableToUnsubscribe: 'Unable to unsubscribe: {error}',
showingOfflineContent: 'Internet request failed. Showing offline content.',
youAreOffline: 'You seem to be offline. You can still read toots while offline.',
// Snackbar UI
@ -653,5 +678,22 @@ export default {
createdFilter: 'Created filter',
failedToModifyFilter: 'Failed to modify filter: {error}',
deletedFilter: 'Deleted filter',
required: 'Required'
required: 'Required',
// Dialogs
profileOptions: 'Profile options',
copyLink: 'Copy link',
emoji: 'Emoji',
editMedia: 'Edit media',
shortcutHelp: 'Shortcut help',
statusOptions: 'Status options',
confirm: 'Confirm',
closeDialog: 'Close dialog',
postPrivacy: 'Post privacy',
homeOnInstance: 'Home on {instance}',
statusesTimelineOnInstance: 'Statuses: {timeline} timeline on {instance}',
statusesHashtag: 'Statuses: #{hashtag} hashtag',
statusesThread: 'Statuses: thread',
statusesAccountTimeline: 'Statuses: account timeline',
statusesList: 'Statuses: list',
notificationsOnInstance: 'Notifications on {instance}'
}

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

@ -0,0 +1,696 @@
export default {
// Home page, basic <title> and <description>
appName: 'Pinafore',
appDescription: 'Un cliente web alternativo para Mastodon, centrado en la velocidad y la sencillez.',
homeDescription: `
<p>
Pinafore es un cliente web para
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
diseñado para ser rápido y sencillo.
</p>
<p>
Lee el
<a rel="noopener" target="_blank"
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">artículo introductorio en el blog</a>,
o comienza iniciando sesión en una instancia:
</p>`,
logIn: 'Iniciar sesión',
footer: `
<p>
Pinafore es
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">software de código abierto</a>
creado por
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
y distribuido bajo la
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">Licencia AGPL</a>.
Aquí está la <a href="/settings/about#privacy-policy" rel="prefetch">política de privacidad</a>.
</p>
`,
// Manifest
longAppName: 'Pinafore para Mastodon',
newStatus: 'Nuevo toot',
// Generic UI
loading: 'Cargando',
okay: 'OK',
cancel: 'Cancelar',
alert: 'Alerta',
close: 'Cerrar',
error: 'Error: {error}',
errorShort: 'Error:',
// Relative timestamps
justNow: 'ahora mismo',
// Navigation, page titles
navItemLabel: `
{label} {selected, select,
true {(página actual)}
other {}
} {name, select,
notifications {{count, plural,
=0 {}
one {(1 notificación)}
other {({count} notificaciones)}
}}
community {{count, plural,
=0 {}
one {(1 solicitud de seguimiento)}
other {({count} solicitudes de seguimiento)}
}}
other {}
}
`,
blockedUsers: 'Usuarios bloqueados',
bookmarks: 'Marcadores',
directMessages: 'Mensajes directos',
favorites: 'Favoritos',
federated: 'Federada',
home: 'Inicio',
local: 'Local',
notifications: 'Notificaciones',
mutedUsers: 'Usuarios silenciados',
pinnedStatuses: 'Toots fijados',
followRequests: 'Solicitudes de seguimiento',
followRequestsLabel: `Solicitudes de seguimiento {hasFollowRequests, select,
true {({count})}
other {}
}`,
list: 'Lista',
search: 'Buscar',
pageHeader: 'Encabezado de página',
goBack: 'Retroceder',
back: 'Atrás',
profile: 'Perfil',
federatedTimeline: 'Cronología federada',
localTimeline: 'Cronología local',
// community page
community: 'Comunidad',
pinnableTimelines: 'Cronologías que puedes fijar',
timelines: 'Cronologías',
lists: 'Listas',
instanceSettings: 'Opciones para instancia',
notificationMentions: 'Notificación de menciones',
profileWithMedia: 'Perfil con multimedia',
profileWithReplies: 'Perfil con respuestas',
hashtag: 'Hashtag',
// not logged in
profileNotLoggedIn: 'Aquí se mostrará una cronología de usuario cuando hayas iniciado sesión.',
bookmarksNotLoggedIn: 'Tus marcadores se mostrarán aquí cuando hayas iniciado sesión.',
directMessagesNotLoggedIn: 'Tus mensajes directos se mostrarán aquí cuando hayas iniciado sesión.',
favoritesNotLoggedIn: 'Tus favoritos se mostrarán aquí cuando hayas iniciado sesión.',
federatedTimelineNotLoggedIn: 'Tu cronología federada se mostrará aquí cuando hayas iniciado sesión.',
localTimelineNotLoggedIn: 'Tu cronología localse mostrará aquí cuando hayas iniciado sesión.',
searchNotLoggedIn: 'Puedes buscar una vez que inicias sesión en una instancia.',
communityNotLoggedIn: 'Las opciones para comunidad se mostrarán aquí cuando hayas iniciado sesión.',
listNotLoggedIn: 'Aquí se mostrará una lista cuando hayas iniciado sesión.',
notificationsNotLoggedIn: 'Tus notificaciones se mostrarán aquí cuando hayas iniciado sesión.',
notificationMentionsNotLoggedIn: 'Las notificaciones de tus menciones se mostrarán aquí cuando hayas iniciado sesión.',
statusNotLoggedIn: 'Aquí se mostrará un hilo de toots cuando hayas iniciado sesión.',
tagNotLoggedIn: 'Aquí se mostrará una cronología de hashtags cuando hayas iniciado sesión.',
// Notification subpages
filters: 'Filtros',
all: 'Todo',
mentions: 'Menciones',
// Follow requests
approve: 'Aceptar',
reject: 'Rechazar',
// Hotkeys
hotkeys: 'Atajos de teclado',
global: 'Globales',
timeline: 'Cronología',
media: 'Multimedia',
globalHotkeys: `
{leftRightChangesFocus, select,
true {
<li><kbd></kbd> para ir al elemento enfocable siguiente</li>
<li><kbd></kbd> para ir al elemento enfocable anterior</li>
}
other {}
}
<li>
<kbd>1</kbd> - <kbd>6</kbd>
{leftRightChangesFocus, select,
true {}
other {o <kbd></kbd>/<kbd></kbd>}
}
para cambiar de columna
</li>
<li><kbd>7</kbd> o <kbd>c</kbd> para redactar un nuevo toot</li>
<li><kbd>s</kbd> o <kbd>/</kbd> para buscar</li>
<li><kbd>g</kbd> + <kbd>h</kbd> para ir a inicio</li>
<li><kbd>g</kbd> + <kbd>n</kbd> para ir a notificaciones</li>
<li><kbd>g</kbd> + <kbd>l</kbd> to para ir a la cronología local</li>
<li><kbd>g</kbd> + <kbd>t</kbd> para ir a la cronología federada</li>
<li><kbd>g</kbd> + <kbd>c</kbd> para ir a la página comunidad</li>
<li><kbd>g</kbd> + <kbd>d</kbd> para ir a la página de mensajes directos</li>
<li><kbd>h</kbd> o <kbd>?</kbd> para abrir o cerrar el diálogo de ayuda</li>
<li><kbd>Backspace</kbd> para retroceder, cerrar diálogos</li>
`,
timelineHotkeys: `
<li><kbd>j</kbd> o <kbd></kbd> para activar el toot siguiente</li>
<li><kbd>k</kbd> o <kbd></kbd> para activar el toot anterior</li>
<li><kbd>.</kbd> para mostrar más y desplazarse al principio</li>
<li><kbd>o</kbd> para abrir</li>
<li><kbd>f</kbd> para marcar como favorito</li>
<li><kbd>b</kbd> para reenviar</li>
<li><kbd>r</kbd> para responder</li>
<li><kbd>Escape</kbd> para cerrar respuesta</li>
<li><kbd>a</kbd> para marcador</li>
<li><kbd>i</kbd> para abrir imágenes, vídeo o audio</li>
<li><kbd>y</kbd> para mostrar u ocultar multimedia sensible</li>
<li><kbd>m</kbd> para mencionar al autor</li>
<li><kbd>p</kbd> para abrir el perfil del autor</li>
<li><kbd>l</kbd> para abrir el enlace de la publicación en una nueva pestaña</li>
<li><kbd>x</kbd> para mostrar u ocultar el texto tras una advertencia de contenido</li>
<li><kbd>z</kbd> para mostrar u ocultar todas las advertencias de contenido en un hilo</li>
`,
mediaHotkeys: `
<li><kbd></kbd> / <kbd></kbd> para ir a siguiente o anterior</li>
`,
// Community page, tabs
tabLabel: `{label} {current, select,
true {(Actual)}
other {}
}`,
pageTitle: `
{hasNotifications, select,
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
`,
pinLabel: `{label} {pinnable, select,
true {
{pinned, select,
true {(página fijada)}
other {(Página no fijada)}
}
}
other {}
}`,
pinPage: 'Fijar {label}',
// Status composition
composeStatus: 'Redactar toot',
postStatus: 'Toot!',
contentWarning: 'Advertencia de contenido',
dropToUpload: 'Soltar para subir',
invalidFileType: 'Tipo de fichero no válido',
composeLabel: '¿En qué estás pensando?',
autocompleteDescription: 'Cuando haya disponibles resultados de autocompletado, pulsa las flechas arriba o abajo y enter para seleccionar.',
mediaUploads: 'Subidas multimedia',
edit: 'Editar',
delete: 'Borrar',
description: 'Descripción',
descriptionLabel: 'Describir para las personas con discapacidad visual (imagen, vídeo) o con discapacidad auditiva (audio, vídeo)',
markAsSensitive: 'Marcar multimedia como sensible',
// Polls
createPoll: 'Crear encuesta',
removePollChoice: 'Eliminar opción {index}',
pollChoiceLabel: 'Opción {index}',
multipleChoice: 'Selección múltiple',
pollDuration: 'Duración de la encuesta',
fiveMinutes: '5 minutos',
thirtyMinutes: '30 minutos',
oneHour: '1 hora',
sixHours: '6 horas',
twelveHours: '12 horas',
oneDay: '1 día',
threeDays: '3 días',
sevenDays: '7 días',
never: 'Nunca',
addEmoji: 'Insertar emoji',
addMedia: 'Añadir multimedia (imágenes, vídeo, audio)',
addPoll: 'Añadir encuesta',
removePoll: 'Eliminar encuesta',
postPrivacyLabel: 'Ajustar privacidad (actualmente {label})',
addContentWarning: 'Añadir advertencia de contenido',
removeContentWarning: 'Eliminar advertencia de contenido',
altLabel: 'Describir para las personas con discapacidad visual',
extractText: 'Extraer texto de imagen',
extractingText: 'Extrayendo texto…',
extractingTextCompletion: 'Extrayendo texto ({percent}% completado)…',
unableToExtractText: 'No se puede extraer texto.',
// Account options
followAccount: 'Seguir a {account}',
unfollowAccount: 'Dejar de seguir a {account}',
blockAccount: 'Bloquear a {account}',
unblockAccount: 'Desbloquear a {account}',
muteAccount: 'Silenciar a {account}',
unmuteAccount: 'Dejar de silenciar a Unmute {account}',
showReblogsFromAccount: 'Mostrar toots reenviados por {account}',
hideReblogsFromAccount: 'Ocultar toots reenviados por {account}',
showDomain: 'Dejar de ocultar {domain}',
hideDomain: 'Ocultar {domain}',
reportAccount: 'Denunciar a {account}',
mentionAccount: 'Mencionar a {account}',
copyLinkToAccount: 'Copiar enlace a cuenta',
copiedToClipboard: 'Copiado al portapapeles',
// Media dialog
navigateMedia: 'Navegar por elementos multimedia',
showPreviousMedia: 'Mostrar multimedia anterior',
showNextMedia: 'Mostrar multimedia siguiente',
enterPinchZoom: 'Modo pinch-zoom',
exitPinchZoom: 'Salir del modo pinch-zoom',
showMedia: `Mostrar {index, select,
1 {primer}
2 {segundo}
3 {tercero}
other {cuarto}
} multimedia {current, select,
true {(actual)}
other {}
}`,
previewFocalPoint: 'Previsualizar (punto focal)',
enterFocalPoint: 'Introducir el punto focal (X, Y) para este multimedia',
muteNotifications: 'Silenciar también las notificaciones',
muteAccountConfirm: '¿Silenciar a {account}?',
mute: 'Silenciar',
unmute: 'Dejar de silenciar',
zoomOut: 'Alejar',
zoomIn: 'Acercar',
// Reporting
reportingLabel: 'Estás denunciando a {account} a los moderadores de {instance}.',
additionalComments: 'Comentarios adicionales',
forwardDescription: '?Reenviar también a los moderadores de {instance}?',
forwardLabel: 'Reenviar a {instance}',
unableToLoadStatuses: 'No se pueden cargar los toots recientes: {error}',
report: 'Denunciar',
noContent: '(Sin contenido)',
noStatuses: 'No hay toots para denunciar',
// Status options
unpinFromProfile: 'Dejar de fijar en el perfil',
pinToProfile: 'Fijar en el perfil',
muteConversation: 'Silenciar conversación',
unmuteConversation: 'Dejar de silenciar conversación',
bookmarkStatus: 'Poner marcador al toot',
unbookmarkStatus: 'Quitar marcador al toot',
deleteAndRedraft: 'Borrar y volver a redactar',
reportStatus: 'Denunciar toot',
shareStatus: 'Compartir toot',
copyLinkToStatus: 'Copiar enlace al toot',
// Account profile
profileForAccount: 'Perfil para {account}',
statisticsAndMoreOptions: 'Estadísticas y más opciones',
statuses: 'Toots',
follows: 'Siguiendo',
followers: 'Seguidores',
moreOptions: 'Más opciones',
followersLabel: 'Te han seguido {count}',
followingLabel: 'Has seguido a {count}',
followLabel: `Seguimiento {requested, select,
true {(solicitud de seguimiento)}
other {}
}`,
unfollowLabel: `Dejar de seguir {requested, select,
true {(solicitud de seguimiento)}
other {}
}`,
notify: 'Suscribirse a {account}',
denotify: 'Cancelar suscripción a {account}',
subscribedAccount: 'Te has suscrito a la cuenta',
unsubscribedAccount: 'Has cancelado tu suscripción a la cuenta',
unblock: 'Desbloquear',
nameAndFollowing: 'Nombre y seguimientos',
clickToSeeAvatar: 'Haz clic para ver el avatar',
opensInNewWindow: '{label} (Se abre en nueva ventana)',
blocked: 'Bloqueado',
domainHidden: 'Dominio oculto',
muted: 'Silenciado',
followsYou: 'Te está siguiendo',
avatarForAccount: 'Avatar para {account}',
fields: 'Campos',
accountHasMoved: '{account} se ha trasladado:',
profilePageForAccount: 'Página de perfil para {account}',
// About page
about: 'Acerca de',
aboutApp: 'Acerca de Pinafore',
aboutAppDescription: `
<p>
Pinafore es
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore">software libre y de código abierto</a>
creado por
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
y distribuido bajo la
<a rel="noopener" target="_blank"
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
</p>
<h2 id="privacy-policy">Política de privacidad</h2>
<p>
Pinafore no almacena ninguna información personal en sus servidores,
incluyendo, pero no limitándose a nombres, direcciones de correo electrónico,
direcciones IP, posts y fotos.
</p>
<p>
Pinafore es un sitio estático. Todos los datos son almacenados en tu navegador y compartidos con las instancias del fediverso
a las que te conectas.
</p>
<h2>Créditos</h2>
<p>
Iconos proporcionados por <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
</p>
<p>
Logo gracias a "sailboat" por Gregor Cresnar, de
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
</p>`,
// Settings
settings: 'Opciones de configuración',
general: 'General',
generalSettings: 'Opciones generales',
showSensitive: 'Mostrar multimedia sensible por defecto',
showPlain: 'Mostrar un color gris liso para multimedia sensible',
allSensitive: 'Tratar todo multimedia como sensible',
largeMedia: 'Mostrar imágenes y vídeos grandes incrustados',
autoplayGifs: 'Reproducir automáticamente GIFs animados',
hideCards: 'Ocultar paneles de previsualización de enlaces',
underlineLinks: 'Subrayar enlaces en toots y perfiles',
accessibility: 'Accesibilidad',
reduceMotion: 'Reducir movimiento en animaciones de la interfaz',
disableTappable: 'Deshabilitar área para tocar en todo el toot',
removeEmoji: 'Eliminar emoji de nombres de usuario',
shortAria: 'Usar etiquetas ARIA cortas para artículos',
theme: 'Diseño visual',
themeForInstance: 'Diseño visual para {instance}',
disableCustomScrollbars: 'Deshabilitar barras deslizantes personalizadas',
bottomNav: 'Situar la barra de navegación al final de la pantalla',
centerNav: 'Centrar la barra de navegación',
preferences: 'Preferencias',
hotkeySettings: 'Opciones para atajos de teclado',
disableHotkeys: 'Deshabilitar todos los atajos de teclado',
leftRightArrows: 'Las flechas izquierda/derecha cambian el foco en vez de columnas/multimedia',
guide: 'Guía',
reload: 'Recargar',
// Wellness settings
wellness: 'Bienestar',
wellnessSettings: 'Opciones para el bienestar',
wellnessDescription: `Las opciones para el bienestar están diseñadas para reducir los aspectos que inducen adicción o ansiedad en las redes sociales.
Elige cualquier opción que vaya bien para ti.`,
enableAll: 'Habilitar todos',
metrics: 'Métricas',
hideFollowerCount: 'Ocultar recuento de seguidores (hasta 10)',
hideReblogCount: 'Ocultar recuento de reenvíos',
hideFavoriteCount: 'Ocultar recuento de favoritos',
hideUnread: 'Ocultar recuento de notificaciones sin leer (es decir, el punto rojo)',
// The quality that makes something seem important or interesting because it seems to be happening now
immediacy: 'Inmediatez',
showAbsoluteTimestamps: 'Mostrar marcas de tiempo absolutas (p.ej., "3 de marzo") en vez de marcas de tiempo relativas (p. ej., "hace 5 minutos")',
ui: 'Interfaz',
grayscaleMode: 'Modo escala de grises',
wellnessFooter: `Estas opciones están parcialmente basadas en pautas del
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
// This is a link: "You can filter or disable notifications in the _instance settings_"
filterNotificationsPre: 'Puedes filtrar o deshabilitar notificaciones en',
filterNotificationsText: 'opciones para instancia',
filterNotificationsPost: '',
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
// to see a description. It's hard to properly internationalize, so we just break up the strings.
disableInfiniteScrollPre: 'Deshabilitar',
disableInfiniteScrollText: 'desplazamiento infinito',
disableInfiniteScrollDescription: `Cuando el desplazamiento infinito esté deshabilitado, los nuevos toots no se mostrarán automáticamente al final o al principio de la cronología. En vez de esto, habrá botones que te permitirán
cargar más contenido a demanda.`,
disableInfiniteScrollPost: '',
// Instance settings
loggedInAs: 'Iniciaste sesión como',
homeTimelineFilters: 'Filtros para la cronología Inicio',
notificationFilters: 'Filtros para notificaciones',
pushNotifications: 'Notificaciones Push',
// Add instance page
storageError: `Parece que Pinafore no puede almacenar datos localmente. ¿Está tu navegador en modo privado
o bloqueando las cookies? Pinafore almacena todos los datos localmente, y requiere LocalStorage e
IndexedDB para funcionar correctamente.`,
javaScriptError: 'Debes habilitar JavaScript para iniciar sesión.',
enterInstanceName: 'Introducir nombre de instancia',
instanceColon: 'Instancia:',
// Custom tooltip, concatenated together
getAnInstancePre: '¿No tienes una',
getAnInstanceText: 'instancia',
getAnInstanceDescription: 'Una instancia es tu servidor de inicio de Mastodon, por ejemplo, mastodon.social o cybre.space.',
getAnInstancePost: '?',
joinMastodon: '¡Unirse a Mastodon!',
instancesYouveLoggedInTo: 'Instancias en las que has iniciado sesión:',
addAnotherInstance: 'Añadir otra instancia',
youreNotLoggedIn: 'No has iniciado sesión en ninguna instancia.',
currentInstanceLabel: `{instance} {current, select,
true {(instancia actual)}
other {}
}`,
// Link text
logInToAnInstancePre: '',
logInToAnInstanceText: 'Inicia sesión en una instancia',
logInToAnInstancePost: 'para empezar a usar Pinafore.',
// Another custom tooltip
showRingPre: 'Mostrar siempre',
showRingText: 'anillo del foco',
showRingDescription: 'El anillo del foco es el contorno que muestra el elemento que actualmente tiene el foco. Por defecto solo se muestra cuando se usa el teclado (no el ratón o un dispositivo táctil), pero puedes elegir mostrarlo siempre.',
showRingPost: '',
instances: 'Instancias',
addInstance: 'Añadir instancia',
homeTimelineFilterSettings: 'Opciones para filtros de la cronología Inicio',
showReblogs: 'Mostrar reenvíos',
showReplies: 'Mostrar respuestas',
switchOrLogOut: 'Seleccionar o cerrar sesión en esta instancia',
switchTo: 'Seleccionar esta instancia',
switchToInstance: 'Seleccionar instancia',
switchToNameOfInstance: 'Seleccionar {instance}',
logOut: 'Cerrar sesión',
logOutOfInstanceConfirm: '¿Cerrar sesión en {instance}?',
notificationFilterSettings: 'Opciones para filtros de notificaciones',
// Push notifications
browserDoesNotSupportPush: 'Tu navegador no admite notificaciones Push.',
deniedPush: 'Has denegado el permiso para mostrar notificaciones.',
pushNotificationsNote: 'Observa que solo puedes recibir notificaciones Push para una instancia al mismo tiempo.',
pushSettings: 'Opciones para notificaciones Push',
newFollowers: 'Nuevos seguidores',
reblogs: 'Reenvíos',
pollResults: 'Resultados de encuesta',
subscriptions: 'Suscripción a toots',
needToReauthenticate: 'Tienes que volver a autenticarte para habilitar las notificaciones Push. ¿Cerrr sesión en {instance}?',
failedToUpdatePush: 'Se ha producido un fallo al actualizar las opciones para notificaciones Push: {error}',
// Themes
chooseTheme: 'Elegir un diseño visual',
darkBackground: 'Fondo oscuro',
lightBackground: 'Fondo claro',
themeLabel: `{label} {default, select,
true {(por defecto)}
other {}
}`,
animatedImage: 'Imagen animada: {description}',
showImage: `Mostrar {animated, select,
true {animated}
other {}
} imagen: {description}`,
playVideoOrAudio: `Reproducir {audio, select,
true {audio}
other {vídeo}
}: {description}`,
accountFollowedYou: '{name} te siguió, {account}',
accountSignedUp: '{name} inició sesión, {account}',
accountRequestedFollow: '{name} solicitó seguirte, {account}',
accountReported: '{name} creó una denuncia, {account}',
reblogCountsHidden: 'Recuento de reenvíos oculto',
favoriteCountsHidden: 'Recuento de favoritos oculto',
rebloggedTimes: `Reenviado {count, plural,
one {1 vez}
other {{count} veces}
}`,
favoritedTimes: `Marcado como favorito {count, plural,
one {1 vez}
other {{count} veces}
}`,
pinnedStatus: 'Toot fijado',
rebloggedYou: 'reenvió tu toot',
favoritedYou: 'marcó como favorito tu toot',
followedYou: 'te siguió',
edited: 'editó su toot',
requestedFollow: 'solicitó seguirte',
reported: 'creó una denuncia',
signedUp: 'sesión iniciada',
posted: 'publicado',
pollYouCreatedEnded: 'Una encuesta que creaste ha finalizado',
pollYouVotedEnded: 'Una encuesta en la que votaste ha finalizado',
reblogged: 'reenviado',
favorited: 'marcado como favorito',
unreblogged: 'no reenviado',
unfavorited: 'no marcado como favorito',
showSensitiveMedia: 'Mostrar multimedia sensible',
hideSensitiveMedia: 'Ocultar multimedia sensible',
clickToShowSensitive: 'Contenido sensible. Haz clic para mostrar.',
longPost: 'Publicación larga',
// Accessible status labels
accountRebloggedYou: '{account} reenvió tu toot',
accountFavoritedYou: '{account} marcó como favorito tu toot',
accountEdited: '{account} editó su toot',
rebloggedByAccount: 'reenviado por {account}',
contentWarningContent: 'Advertencia de contenido: {spoiler}',
hasMedia: 'tiene multimedia',
hasPoll: 'tiene encuesta',
shortStatusLabel: '{privacy} toot de {account}',
// Privacy types
public: 'Público',
unlisted: 'No listado',
followersOnly: 'Solo seguidores',
direct: 'Directo',
// Themes
themeRoyal: 'Royal',
themeScarlet: 'Escarlata',
themeSeafoam: 'Espuma de mar',
themeHotpants: 'Hotpants',
themeOaken: 'Roble',
themeMajesty: 'Majesty',
themeGecko: 'Gecko',
themeGrayscale: 'Escala de grises',
themeOzark: 'Ozark',
themeCobalt: 'Cobalto',
themeSorcery: 'Sorcery',
themePunk: 'Punk',
themeRiot: 'Riot',
themeHacker: 'Hacker',
themeMastodon: 'Mastodon',
themePitchBlack: 'Tono negro',
themeDarkGrayscale: 'Escala de gris oscuro',
// Polls
voteOnPoll: 'Votar en encuesta',
pollChoices: 'Opciones de la encuesta',
vote: 'Votar',
pollDetails: 'Detalles de la encuesta',
refresh: 'Actualizar',
expires: 'Finaliza',
expired: 'Finalizada',
voteCount: `{count, plural,
one {1 voto}
other {{count} votos}
}`,
// Status interactions
clickToShowThread: '{time} - haz clic para mostrar el hilo',
showMore: 'Mostrar más',
showLess: 'Mostrar menos',
closeReply: 'Cerrar respuesta',
cannotReblogFollowersOnly: 'No se puede reenviar porque es solo para seguidores',
cannotReblogDirectMessage: 'No se puede reenviar porque es un mensaje directo',
reblog: 'Reenviar',
reply: 'Responder',
replyToThread: 'Responder al hilo',
favorite: 'Favorito',
unfavorite: 'No favorito',
// timeline
loadingMore: 'Cargando más…',
loadMore: 'Cargar más',
showCountMore: 'Mostrar {count} más',
nothingToShow: 'Nada para mostrar.',
// status thread page
statusThreadPage: 'Página de hilo de toots',
status: 'Toot',
// toast messages
blockedAccount: 'Cuenta bloqueada',
unblockedAccount: 'Cuenta desbloqueada',
unableToBlock: 'No se puede bloquear la cuenta: {error}',
unableToUnblock: 'No se puede desbloquear la cuenta: {error}',
bookmarkedStatus: 'Toot con marcador',
unbookmarkedStatus: 'Toot sin marcador',
unableToBookmark: 'No se puede poner marcador: {error}',
unableToUnbookmark: 'No se puede quitar marcador: {error}',
cannotPostOffline: 'No puedes publicar mientras estás sin conexión',
unableToPost: 'No se puede publicar el toot: {error}',
statusDeleted: 'Toot borrado',
unableToDelete: 'No se puede borrar el toot: {error}',
cannotFavoriteOffline: 'No puedes marcar como favorito mientras estás sin conexión',
cannotUnfavoriteOffline: 'No puedes quitar marca de favorito mientras estás sin conexión',
unableToFavorite: 'No se puede marcar como favorito: {error}',
unableToUnfavorite: 'No se puede quitar marca de favorito: {error}',
followedAccount: 'Cuenta seguida',
unfollowedAccount: 'Cuenta no seguida',
unableToFollow: 'No se puede seguir a la cuenta: {error}',
unableToUnfollow: 'No se puede dejar de seguir a la cuenta: {error}',
accessTokenRevoked: 'El token de acceso fue anulado, se cerró sesión en {instance}',
loggedOutOfInstance: 'Se cerró sesión en {instance}',
failedToUploadMedia: 'Falló la subida del multimedia: {error}',
mutedAccount: 'Cuenta silenciada',
unmutedAccount: 'Cuenta no silenciada',
unableToMute: 'No se puede silenciar la cuenta: {error}',
unableToUnmute: 'No se puede dejar de silenciar la cuenta: {error}',
mutedConversation: 'Conversación silenciada',
unmutedConversation: 'Conversación no silenciada',
unableToMuteConversation: 'No se puede silenciar la conversación: {error}',
unableToUnmuteConversation: 'No se puede dejar de silenciar la conversación: {error}',
unpinnedStatus: 'Toot no fijado',
unableToPinStatus: 'No se puede fijar el toot: {error}',
unableToUnpinStatus: 'No se puede dejar de fijar el toot: {error}',
unableToRefreshPoll: 'No se puede actualizar la encuesta: {error}',
unableToVoteInPoll: 'No se puede votar en la encuesta: {error}',
cannotReblogOffline: 'No puedes reenviar mientras estás sin conexión.',
cannotUnreblogOffline: 'No puedes deshacer reenvíos mientras estás sin conexión.',
failedToReblog: 'Fallo al reenviar: {error}',
failedToUnreblog: 'Fallo al deshacer reenvío: {error}',
submittedReport: 'Denuncia enviada',
failedToReport: 'Fallo al enviar denuncia: {error}',
approvedFollowRequest: 'Solicitud de seguimiento aceptada',
rejectedFollowRequest: 'Solicitud de seguimiento rechazada',
unableToApproveFollowRequest: 'No se puede aceptar la solicitud de seguimiento: {error}',
unableToRejectFollowRequest: 'No se puede rechazar la solicitud de seguimiento: {error}',
searchError: 'Error durante la búsqueda: {error}',
hidDomain: 'Dominio oculto',
unhidDomain: 'Dominio no oculto',
unableToHideDomain: 'No se puede ocultar el dominio: {error}',
unableToUnhideDomain: 'No se puede dejar de ocultar el dominio: {error}',
showingReblogs: 'Mostrando reenvíos',
hidingReblogs: 'Ocultando reenvíos',
unableToShowReblogs: 'No se puede mostrar los reenvíos: {error}',
unableToHideReblogs: 'No se puede ocultar los reenvíos: {error}',
unableToShare: 'No se puede compartir: {error}',
unableToSubscribe: 'Imposible suscribirse: {error}',
unableToUnsubscribe: 'Imposible dejar de suscribirse: {error}',
showingOfflineContent: 'La petición a internet falló. Mostrando contenido sin conexión.',
youAreOffline: 'Parece que estás sin conexión. Puedes leer contenido incluso sin conexión.',
// Snackbar UI
updateAvailable: 'Actualización de la aplicación disponible.',
// Word/phrase filters
wordFilters: 'Filtros de palabras',
noFilters: 'No tienes ningún filtro de palabras.',
wordOrPhrase: 'Palabra o frase',
contexts: 'Contextos',
addFilter: 'Añadir filtro',
editFilter: 'Editar filtro',
filterHome: 'Inicio y listas',
filterNotifications: 'Notificaciones',
filterPublic: 'Cronologías públicas',
filterThread: 'Conversaciones',
filterAccount: 'Perfiles',
filterUnknown: 'Desconocido',
expireAfter: 'Expira al cabo de',
whereToFilter: 'Dónde filtrar',
irreversible: 'Irreversible',
wholeWord: 'Palabra completa',
save: 'Guardar',
updatedFilter: 'Filtro actualizado',
createdFilter: 'Filtro creado',
failedToModifyFilter: 'Fallo al modificar el filtro: {error}',
deletedFilter: 'Filtro borrado',
required: 'Requerido',
// Dialogs
profileOptions: 'Opciones de perfil',
copyLink: 'Copiar enlace',
emoji: 'Emoji',
editMedia: 'Editar multimedia',
shortcutHelp: 'Ayuda sobre atajos de teclado',
statusOptions: 'Opciones de estado',
confirm: 'Confirmar',
closeDialog: 'Cerrar diálogo',
postPrivacy: 'Privacidad del post',
homeOnInstance: 'Inicio en {instance}',
statusesTimelineOnInstance: 'Estados: {timeline} cronología en {instance}',
statusesHashtag: 'Estados: #{hashtag} hashtag',
statusesThread: 'Estados: hilo',
statusesAccountTimeline: 'Estado: cronología de cuenta',
statusesList: 'Estado: lista',
notificationsOnInstance: 'Notificaciones en {instance}'
}

Wyświetl plik

@ -171,12 +171,12 @@ export default {
true {({count})}
other {}
}
{name}
·
{showInstanceName, select,
true {{instanceName}}
other {Pinafore}
}
·
{name}
`,
pinLabel: `{label} {pinnable, select,
true {
@ -189,8 +189,6 @@ export default {
}`,
pinPage: 'Epingler {label}',
// Status composition
overLimit: '{count} {count, plural, =1 {caractère} other {caractères}} en dessus de la limite',
underLimit: '{count} {count, plural, =1 {caractère} other {caractères}} qui reste',
composeStatus: 'Ecrire un pouet',
postStatus: 'Pouet!',
contentWarning: 'Avertissement',

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,6 +1,6 @@
import { getAccountAccessibleName } from './getAccountAccessibleName'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
import { formatIntl } from '../_utils/formatIntl'
import { getAccountAccessibleName } from './getAccountAccessibleName.js'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses.js'
import { formatIntl } from '../_utils/formatIntl.js'
function getNotificationText (notification, omitEmojiInDisplayNames) {
if (!notification) {
@ -11,6 +11,8 @@ function getNotificationText (notification, omitEmojiInDisplayNames) {
return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName })
} else if (notification.type === 'favourite') {
return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName })
} else if (notification.type === 'update') {
return formatIntl('intl.accountEdited', { account: notificationAccountDisplayName })
}
}
@ -37,12 +39,15 @@ function cleanupText (text) {
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
shortInlineFormattedDate, spoilerText, showContent,
reblog, notification, visibility, omitEmojiInDisplayNames,
disableLongAriaLabels, showMedia, showPoll) {
disableLongAriaLabels, showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll) {
const originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
const contentTextToShow = (showContent || !spoilerText)
? cleanupText(plainTextContent)
: formatIntl('intl.contentWarningContent', { spoiler: cleanupText(spoilerText) })
const mediaTextToShow = showMedia && 'intl.hasMedia'
const mediaDescText = (showMedia && (!sensitive || sensitiveShown))
? mediaAttachments.map(media => media.description)
: []
const pollTextToShow = showPoll && 'intl.hasPoll'
const privacyText = getPrivacyText(visibility)
@ -57,6 +62,7 @@ export function getAccessibleLabelForStatus (originalAccount, account, plainText
originalAccountDisplayName,
contentTextToShow,
mediaTextToShow,
...mediaDescText,
pollTextToShow,
shortInlineFormattedDate,
`@${originalAccount.acct}`,

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,12 @@
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
import { getInstanceInfo } from '../_api/instance'
import { goto } from '../../../__sapper__/client'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
import { store } from '../_store/store'
import { updateVerifyCredentialsForInstance } from './instances'
import { updateCustomEmojiForInstance } from './emoji'
import { database } from '../_database/database'
import { DOMAIN_BLOCKS } from '../_static/blocks'
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth.js'
import { getInstanceInfo } from '../_api/instance.js'
import { goto } from '../../../__sapper__/client.js'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine.js'
import { store } from '../_store/store.js'
import { updateVerifyCredentialsForInstance } from './instances.js'
import { updateCustomEmojiForInstance } from './emoji.js'
import { database } from '../_database/database.js'
import { DOMAIN_BLOCKS } from '../_static/blocks.js'
const GENERIC_ERROR = `
Is this a valid Mastodon instance? Is a browser extension
@ -37,8 +37,16 @@ async function redirectToOauth () {
}
const redirectUri = getRedirectUri()
const registrationPromise = registerApplication(instanceNameInSearch, redirectUri)
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
try {
const instanceInfo = await getInstanceInfo(instanceNameInSearch)
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
} catch (err) {
// We get a 401 in limited federation mode, so we can just skip setting the instance info in that case.
// It will be fetched automatically later.
if (err.status !== 401) {
throw err // this is a good way to test for typos in the instance name or some other problem
}
}
const instanceData = await registrationPromise
store.set({
currentRegisteredInstanceName: instanceNameInSearch,
@ -97,10 +105,10 @@ async function registerNewInstance (code) {
instanceNameInSearch: '',
currentRegisteredInstanceName: null,
currentRegisteredInstance: null,
loggedInInstances: loggedInInstances,
loggedInInstances,
currentInstance: currentRegisteredInstanceName,
loggedInInstancesInOrder: loggedInInstancesInOrder,
instanceThemes: instanceThemes
loggedInInstancesInOrder,
instanceThemes
})
store.save()
const { enableGrayscale } = store.get()

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') || []
@ -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,4 +1,4 @@
import { store } from '../_store/store'
import { store } from '../_store/store.js'
const emojiMapper = emoji => emoji.unicode ? emoji.unicode : `:${emoji.shortcodes[0]}:`
const hashtagMapper = hashtag => `#${hashtag.name}`

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,9 +1,9 @@
import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import * as emojiDatabase from '../_utils/emojiDatabase'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { testEmojiSupported } from '../_utils/testEmojiSupported'
import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import * as emojiDatabase from '../_utils/emojiDatabase.js'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest.js'
import { testEmojiSupported } from '../_utils/testEmojiSupported.js'
import { mark, stop } from '../_utils/marks.js'
async function searchEmoji (searchText) {
let emojis = await emojiDatabase.findBySearchQuery(searchText)

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,9 +1,9 @@
import { store } from '../_store/store'
import { blockAccount, unblockAccount } from '../_api/block'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { blockAccount, unblockAccount } from '../_api/block.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark'
import { database } from '../_database/database'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,13 +1,13 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { postStatus as postStatusToServer } from '../_api/statuses'
import { addStatusOrNotification } from './addStatusOrNotification'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'
import { putMediaMetadata } from '../_api/media'
import uniqBy from 'lodash-es/uniqBy'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { postStatus as postStatusToServer } from '../_api/statuses.js'
import { addStatusOrNotification } from './addStatusOrNotification.js'
import { database } from '../_database/database.js'
import { emit } from '../_utils/eventBus.js'
import { putMediaMetadata } from '../_api/media.js'
import { uniqBy } from '../_thirdparty/lodash/objects.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function insertHandleForReply (statusId) {
const { currentInstance } = store.get()

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,5 +1,5 @@
import { importShowCopyDialog } from '../_components/dialog/asyncDialogs/importShowCopyDialog.js'
import { toast } from '../_components/toast/toast'
import { toast } from '../_components/toast/toast.js'
export async function copyText (text) {
if (navigator.clipboard) { // not supported in all browsers

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,8 +1,8 @@
import { store } from '../_store/store'
import { deleteStatus } from '../_api/delete'
import { toast } from '../_components/toast/toast'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { deleteStatus } from '../_api/delete.js'
import { toast } from '../_components/toast/toast.js'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function doDeleteStatus (statusId) {
const { currentInstance, accessToken } = store.get()

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

@ -2,8 +2,8 @@
// Used in the integration tests. Can't see a problem with exposing this publicly
// since you would have to know the access token anyway.
import { store } from '../_store/store'
import { goto } from '../../../__sapper__/client'
import { store } from '../_store/store.js'
import { goto } from '../../../__sapper__/client.js'
export function doQuickLoginIfNecessary () {
const params = new URLSearchParams(location.search)

Wyświetl plik

@ -1,11 +1,11 @@
import {
cacheFirstUpdateAfter,
cacheFirstUpdateOnlyIfNotInCache
} from '../_utils/sync'
import { database } from '../_database/database'
import { getCustomEmoji } from '../_api/emoji'
import { store } from '../_store/store'
import isEqual from 'lodash-es/isEqual'
} from '../_utils/sync.js'
import { database } from '../_database/database.js'
import { getCustomEmoji } from '../_api/emoji.js'
import { store } from '../_store/store.js'
import { isEqual } from '../_thirdparty/lodash/objects.js'
async function syncEmojiForInstance (instanceName, syncMethod) {
await syncMethod(

Wyświetl plik

@ -1,8 +1,8 @@
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'
import { formatIntl } from '../_utils/formatIntl'
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite.js'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setFavorited (statusId, favorited) {
const { online } = store.get()

Wyświetl plik

@ -1,11 +1,11 @@
import { store } from '../_store/store'
import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters'
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync'
import { database } from '../_database/database'
import { isEqual } from 'lodash-es'
import { toast } from '../_components/toast/toast'
import { formatIntl } from '../_utils/formatIntl'
import { emit } from '../_utils/eventBus'
import { store } from '../_store/store.js'
import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters.js'
import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync.js'
import { database } from '../_database/database.js'
import { isEqual } from '../_thirdparty/lodash/objects.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
import { emit } from '../_utils/eventBus.js'
async function syncFilters (instanceName, syncMethod) {
const { loggedInInstances } = store.get()

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { followAccount, unfollowAccount } from '../_api/follow'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { followAccount, unfollowAccount } from '../_api/follow.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

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,5 +1,5 @@
import { store } from '../_store/store'
import { getTimeline } from '../_api/timelines'
import { store } from '../_store/store.js'
import { getTimeline } from '../_api/timelines.js'
export async function getRecentStatusesForAccount (accountId) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,6 +1,6 @@
import { store } from '../_store/store'
import { goto } from '../../../__sapper__/client'
import { emit } from '../_utils/eventBus'
import { store } from '../_store/store.js'
import { goto } from '../../../__sapper__/client.js'
import { emit } from '../_utils/eventBus.js'
// Go to the search page, and also focus the search input. For accessibility
// and usability reasons, this only happens on pressing these particular hotkeys.

Wyświetl plik

@ -1,18 +1,18 @@
import { getVerifyCredentials } from '../_api/user'
import { store } from '../_store/store'
import { switchToTheme } from '../_utils/themeEngine'
import { toast } from '../_components/toast/toast'
import { goto } from '../../../__sapper__/client'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { getInstanceInfo } from '../_api/instance'
import { database } from '../_database/database'
import { getVerifyCredentials } from '../_api/user.js'
import { store } from '../_store/store.js'
import { switchToTheme } from '../_utils/themeEngine.js'
import { toast } from '../_components/toast/toast.js'
import { goto } from '../../../__sapper__/client.js'
import { cacheFirstUpdateAfter } from '../_utils/sync.js'
import { getInstanceInfo } from '../_api/instance.js'
import { database } from '../_database/database.js'
import { importVirtualListStore } from '../_utils/asyncModules/importVirtualListStore.js'
import { formatIntl } from '../_utils/formatIntl'
import { formatIntl } from '../_utils/formatIntl.js'
export function changeTheme (instanceName, newTheme) {
const { instanceThemes } = store.get()
instanceThemes[instanceName] = newTheme
store.set({ instanceThemes: instanceThemes })
store.set({ instanceThemes })
store.save()
const { currentInstance } = store.get()
if (instanceName === currentInstance) {
@ -90,7 +90,7 @@ export async function logOutOfInstance (instanceName, message) {
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
const { verifyCredentials } = store.get()
verifyCredentials[instanceName] = thisVerifyCredentials
store.set({ verifyCredentials: verifyCredentials })
store.set({ verifyCredentials })
}
export async function updateVerifyCredentialsForInstance (instanceName) {
@ -111,13 +111,17 @@ export async function updateVerifyCredentialsForCurrentInstance () {
export async function updateInstanceInfo (instanceName) {
await cacheFirstUpdateAfter(
() => getInstanceInfo(instanceName),
() => {
const { loggedInInstances } = store.get()
const accessToken = loggedInInstances[instanceName] && loggedInInstances[instanceName].access_token
return getInstanceInfo(instanceName, accessToken)
},
() => database.getInstanceInfo(instanceName),
info => database.setInstanceInfo(instanceName, info),
info => {
const { instanceInfos } = store.get()
instanceInfos[instanceName] = info
store.set({ instanceInfos: instanceInfos })
store.set({ instanceInfos })
}
)
}

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,9 +1,9 @@
import { store } from '../_store/store'
import { uploadMedia } from '../_api/media'
import { toast } from '../_components/toast/toast'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { formatIntl } from '../_utils/formatIntl'
import { database } from '../_database/database'
import { store } from '../_store/store.js'
import { uploadMedia } from '../_api/media.js'
import { toast } from '../_components/toast/toast.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { formatIntl } from '../_utils/formatIntl.js'
import { database } from '../_database/database.js'
export async function doMediaUpload (realm, file) {
const { currentInstance, accessToken } = store.get()

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,9 +1,9 @@
import { store } from '../_store/store'
import { muteAccount, unmuteAccount } from '../_api/mute'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { muteAccount, unmuteAccount } from '../_api/mute.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { muteConversation, unmuteConversation } from '../_api/muteConversation.js'
import { toast } from '../_components/toast/toast.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,9 +1,9 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { pinStatus, unpinStatus } from '../_api/pin'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { pinStatus, unpinStatus } from '../_api/pin.js'
import { database } from '../_database/database.js'
import { emit } from '../_utils/eventBus.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

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,7 +1,7 @@
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { formatIntl } from '../_utils/formatIntl'
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls.js'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function getPoll (pollId) {
const { currentInstance, accessToken } = store.get()

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,8 +1,8 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { reblogStatus, unreblogStatus } from '../_api/reblog'
import { database } from '../_database/database'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { reblogStatus, unreblogStatus } from '../_api/reblog.js'
import { database } from '../_database/database.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setReblogged (statusId, reblogged) {
const online = store.get()

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,7 +1,7 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { report } from '../_api/report'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { report } from '../_api/report.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function reportStatuses (account, statusIds, comment, forward) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
import { emit } from '../_utils/eventBus'
import { toast } from '../_components/toast/toast'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests.js'
import { emit } from '../_utils/eventBus.js'
import { toast } from '../_components/toast/toast.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
const {

Wyświetl plik

@ -1,7 +1,7 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { search } from '../_api/search'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { toast } from '../_components/toast/toast.js'
import { search } from '../_api/search.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function doSearch () {
const { currentInstance, accessToken, queryInSearch } = store.get()

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,8 +1,8 @@
import { store } from '../_store/store'
import { blockDomain, unblockDomain } from '../_api/blockDomain'
import { toast } from '../_components/toast/toast'
import { updateRelationship } from './accounts'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { blockDomain, unblockDomain } from '../_api/blockDomain.js'
import { toast } from '../_components/toast/toast.js'
import { updateRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,8 +1,8 @@
import { store } from '../_store/store'
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { formatIntl } from '../_utils/formatIntl'
import { store } from '../_store/store.js'
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs.js'
import { toast } from '../_components/toast/toast.js'
import { updateLocalRelationship } from './accounts.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
const { currentInstance, accessToken } = store.get()

Wyświetl plik

@ -1,6 +1,6 @@
import { toast } from '../_components/toast/toast'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
import { formatIntl } from '../_utils/formatIntl'
import { toast } from '../_components/toast/toast.js'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
import { formatIntl } from '../_utils/formatIntl.js'
export async function shareStatus (status) {
try {

Wyświetl plik

@ -1,7 +1,7 @@
import { store } from '../_store/store'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog'
import { database } from '../_database/database'
import { doMediaUpload } from './media'
import { store } from '../_store/store.js'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/importShowComposeDialog.js'
import { database } from '../_database/database.js'
import { doMediaUpload } from './media.js'
// show a compose dialog, typically invoked by the Web Share API or a PWA shortcut
export async function showComposeDialog () {

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

Wyświetl plik

@ -1,4 +1,4 @@
import { database } from '../_database/database'
import { database } from '../_database/database.js'
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
const status = await database.getStatus(instanceName, statusId)

Wyświetl plik

@ -1,5 +1,5 @@
import { addStatusesOrNotifications } from '../addStatusOrNotification'
import { getTimeline } from '../../_api/timelines'
import { addStatusesOrNotifications } from '../addStatusOrNotification.js'
import { getTimeline } from '../../_api/timelines.js'
const TIMELINE_GAP_BATCH_SIZE = 20 // Mastodon timeline API maximum limit
const MAX_NUM_REQUESTS = 15 // to avoid getting caught in an infinite loop somehow

Wyświetl plik

@ -1,9 +1,10 @@
import { mark, stop } from '../../_utils/marks'
import { deleteStatus } from '../deleteStatuses'
import { addStatusOrNotification } from '../addStatusOrNotification'
import { emit } from '../../_utils/eventBus'
import { mark, stop } from '../../_utils/marks.js'
import { deleteStatus } from '../deleteStatuses.js'
import { addStatusOrNotification } from '../addStatusOrNotification.js'
import { emit } from '../../_utils/eventBus.js'
import { updateStatus } from '../updateStatus.js'
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed']
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed', 'status.update']
export function processMessage (instanceName, timelineName, message) {
let { event, payload } = (message || {})
@ -12,7 +13,7 @@ export function processMessage (instanceName, timelineName, message) {
return
}
mark('processMessage')
if (['update', 'notification', 'conversation'].includes(event)) {
if (['update', 'notification', 'conversation', 'status.update'].includes(event)) {
payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason
}
@ -35,11 +36,17 @@ export function processMessage (instanceName, timelineName, message) {
// reproduce what is done for statuses for the conversation.
//
// It will add new DMs as new conversations instead of updating existing threads
addStatusOrNotification(instanceName, timelineName, payload.last_status)
if (payload.last_status) {
// If the last_status doesn't exist, just ignore it. There's not much we can do in that case
addStatusOrNotification(instanceName, timelineName, payload.last_status)
}
break
case 'filters_changed':
emit('wordFiltersChanged', instanceName)
break
case 'status.update':
updateStatus(instanceName, payload)
break
}
stop('processMessage')
}

Wyświetl plik

@ -1,7 +1,7 @@
import { TimelineStream } from '../../_api/stream/TimelineStream'
import { processMessage } from './processMessage'
import { fillStreamingGap } from './fillStreamingGap'
import { store } from '../../_store/store'
import { TimelineStream } from '../../_api/stream/TimelineStream.js'
import { processMessage } from './processMessage.js'
import { fillStreamingGap } from './fillStreamingGap.js'
import { store } from '../../_store/store.js'
export function createStream (api, instanceName, accessToken, timelineName, firstStatusId, firstNotificationId) {
console.log(`streaming ${instanceName} ${timelineName}: createStream`, 'firstStatusId', firstStatusId,

Wyświetl plik

@ -1,19 +1,18 @@
import { store } from '../_store/store'
import { getTimeline } from '../_api/timelines'
import { toast } from '../_components/toast/toast'
import { mark, stop } from '../_utils/marks'
import { concat, mergeArrays } from '../_utils/arrays'
import { compareTimelineItemSummaries } from '../_utils/statusIdSorting'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { getStatus, getStatusContext } from '../_api/statuses'
import { emit } from '../_utils/eventBus'
import { TIMELINE_BATCH_SIZE } from '../_static/timelines'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'
import uniqBy from 'lodash-es/uniqBy'
import { addStatusesOrNotifications } from './addStatusOrNotification'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread'
import { store } from '../_store/store.js'
import { getTimeline } from '../_api/timelines.js'
import { toast } from '../_components/toast/toast.js'
import { mark, stop } from '../_utils/marks.js'
import { concat, mergeArrays } from '../_utils/arrays.js'
import { compareTimelineItemSummaries } from '../_utils/statusIdSorting.js'
import { uniqBy, isEqual } from '../_thirdparty/lodash/objects.js'
import { database } from '../_database/database.js'
import { getStatus, getStatusContext } from '../_api/statuses.js'
import { emit } from '../_utils/eventBus.js'
import { TIMELINE_BATCH_SIZE } from '../_static/timelines.js'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary.js'
import { addStatusesOrNotifications } from './addStatusOrNotification.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread.js'
import li from 'li'
const byId = _ => _.id

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