Compare commits

...

123 Commits

Author SHA1 Message Date
Gô Shoemake
3a0b47982d load public.js on vanilla#admin 2018-01-12 18:52:53 -08:00
Gô Shoemake
9b9b7fa005 Load packs/public.js for glitch#admin 2018-01-12 18:51:52 -08:00
David Yip
0210e59759 Merge remote-tracking branch 'remotes/origin/fix-column-headers-accessibility' 2018-01-12 20:10:58 -06:00
beatrix
4773481a90 Merge pull request #322 from m4sk1n/glitch-soc-pl
i18n: Update Polish translation
2018-01-12 18:16:31 -05:00
Marcin Mikołajczak
ee7217bc94 i18n: Update Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-12 23:08:51 +01:00
David Yip
e2ce628724 Merge pull request #321 from KnzkDev/fix/dropdown-text
Fix dropdown item text
2018-01-12 09:09:10 -06:00
ncls7615
cf5789146b Fix dropdown text 2018-01-12 23:18:25 +09:00
Jenkins
9fa79bc317 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-12 02:17:15 +00:00
Jeong Arm
704053d221 Handle sessions that can't be translated (#6245)
* Handle sessions that can't be translated

This commit fixes #6165.

* Fix typo
2018-01-12 02:44:28 +01:00
David Yip
8c08c852bc Merge pull request #320 from glitch-soc/dockerfile-stack-space
dockerfile: Give more stack space to /sbin/tini
2018-01-11 04:25:26 -06:00
David Yip
f13ebd02c9 dockerfile: Give more stack space to /sbin/tini.
/sbin/tini ends up running the Sass compiler, which seems to need a
larger thread stack size than the musl default.  This patch incorporates
a fix from
https://raw.githubusercontent.com/jubel-han/dockerfiles/master/common/stack-fix.c,
as described in https://github.com/sass/node-sass/issues/2031.
2018-01-11 04:09:16 -06:00
David Yip
26f054253c Merge pull request #319 from glitch-soc/317-attempt-to-switch-flavors-raises-actioncontroller-parametermissing
Fix #317: Attempting to switch flavors raises ActionController::ParameterMissing
2018-01-10 12:49:43 -06:00
David Yip
395e64e858 Thank you, Officer Murphy 2018-01-10 12:10:43 -06:00
David Yip
514db316f7 The flavour parameter is unused, so omit it (#317) 2018-01-10 12:09:42 -06:00
David Yip
6fcb870d96 Allow for user object to be empty. Fixes #317.
If a flavour has only one skin, the skin selector will be omitted.  This
omits the user[setting_skin] field, and because that's the only
user[...] field on the page, the entire user object will not be present
in the request handler's params object.

This commit accounts for that scenario by avoiding params.require(:user)
and instead picking out what we need from the params hash.
2018-01-10 12:09:42 -06:00
David Yip
3ce1385b25 Add examples based on errors seen in #317 2018-01-10 12:09:41 -06:00
David Yip
095a00ef3d Merge pull request #316 from glitch-soc/fix-async
Fix glitch async chunks
2018-01-09 18:30:45 -06:00
kibigo!
35be02f21d Renamed glitch async chunks 2018-01-09 16:26:14 -08:00
David Yip
622c8fdb75 Merge pull request #314 from chriswmartin/merge-vanilla-updates-2
Merge vanilla updates into glitch - round 2
2018-01-09 17:01:23 -06:00
David Yip
991371af5f Merge remote-tracking branch 'origin/master' into merge-upstream
Conflicts:
	db/schema.rb
2018-01-09 14:16:45 -06:00
Eugen Rochko
35b84985a8 Skip ActivityPub Announces of non-public objects (#6230)
* Skip ActivityPub Announces of non-public objects

* Skip OStatus reblogs of non-public statuses
2018-01-09 19:35:10 +01:00
Eugen Rochko
d41f0b66cc Fix #6204: Use content warning for page title when present (#6231) 2018-01-09 19:34:58 +01:00
cwm
aef4b1af66 Refactor /api/web APIs to use the centralized axios instance (tootsuite pr #6223) 2018-01-09 10:51:14 -06:00
cwm
f43c2f12f3 Fix overflowing audit logs (tootsuite pr #6184) 2018-01-09 10:40:14 -06:00
cwm
9bdbe66316 Set background to the navigation of Getting Started column (tootsuite pr #6163) 2018-01-09 10:37:27 -06:00
cwm
b535f24fe5 Use const instead of let for constant (tootsuite pr #6106) 2018-01-09 10:35:36 -06:00
Eugen Rochko
921b781909 Increase rate limit on protected paths (#6229)
Previously each protected path had a separate rate limit. Now they're all in the same bucket, so people are more likely to hit one with register->login. Increasing to 25 per 5 minutes should be fine.
2018-01-09 17:07:54 +01:00
cwm
0d4dcb5fb2 change key to path in timeline settings containers 2018-01-09 09:58:40 -06:00
cwm
6d1c325167 Rename key to path in actions and reducers for settings (tootsuite pr #6105) 2018-01-09 09:48:14 -06:00
cwm
7f4374d97d Fix newlines-to-spaces functionality (tootsuite pr #6158) 2018-01-09 08:52:14 -06:00
cwm
8a0e4bb9a4 Hide moved account's follow button in search result (tootsuite pr #5913) 2018-01-09 08:50:55 -06:00
cwm
5963630c63 Prevent duplicate load of favourites (tootsuite pr #5931) 2018-01-09 08:46:35 -06:00
takayamaki
6f5c0afe93 add index on statuses for /api/v1/accounts/:account_id/statuses (#6202) 2018-01-09 15:00:19 +01:00
Evgeny Petrov
eec6095e02 Russian language update (#6227)
* Russian language update

New strings translated, except "terms" key.
Should this be translated? Can it be changed by end user?

* Removed double quotes in "terms" -> "title" key
2018-01-09 14:59:19 +01:00
Jenkins
e780d5951b Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-09 12:17:16 +00:00
Renato "Lond" Cerqueira
9f04b0d4b1 Weblate translations (#6228)
* Translated using Weblate (Catalan)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ca/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pt_BR/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (523 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt_BR/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (525 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/

* Translated using Weblate (Arabic)

Currently translated at 80.3% (45 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Arabic)

Currently translated at 83.9% (47 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Persian)

Currently translated at 87.6% (460 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ja/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ja/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pt/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pt/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pt/

* Translated using Weblate (Catalan)

Currently translated at 99.2% (521 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ca/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (75 of 75 strings)

Translation: Mastodon/Doorkeeper
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/pt/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (75 of 75 strings)

Translation: Mastodon/Doorkeeper
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/pt/

* Translated using Weblate (Portuguese)

Currently translated at 47.2% (248 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 48.0% (252 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Persian)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/fa/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (525 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/

* Translated using Weblate (Japanese)

Currently translated at 99.0% (520 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ja/

* Translated using Weblate (Persian)

Currently translated at 90.4% (475 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/

* Translated using Weblate (Polish)

Currently translated at 99.8% (524 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pl/

* Translated using Weblate (Persian)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/fa/

* Translated using Weblate (Persian)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/fa/

* Translated using Weblate (Polish)

Currently translated at 99.8% (524 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pl/

* Translated using Weblate (Persian)

Currently translated at 99.8% (524 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/

* Translated using Weblate (Portuguese)

Currently translated at 48.3% (254 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pt/

* Translated using Weblate (Portuguese)

Currently translated at 56.5% (297 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 59.4% (312 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (75 of 75 strings)

Translation: Mastodon/Doorkeeper
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/nl/

* Translated using Weblate (Arabic)

Currently translated at 91.0% (51 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Portuguese)

Currently translated at 59.6% (313 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ar/

* Translated using Weblate (Japanese)

Currently translated at 99.6% (523 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ja/

* Translated using Weblate (Portuguese)

Currently translated at 67.6% (355 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Arabic)

Currently translated at 98.2% (55 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pt/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ar/

* Translated using Weblate (Galician)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/gl/

* Translated using Weblate (Arabic)

Currently translated at 51.1% (22 of 43 strings)

Translation: Mastodon/Devise
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ar/

* Translated using Weblate (Galician)

Currently translated at 100.0% (75 of 75 strings)

Translation: Mastodon/Doorkeeper
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/gl/

* Added translation using Weblate (Galician)

* Added translation using Weblate (Galician)

* Translated using Weblate (Galician)

Currently translated at 50.0% (1 of 2 strings)

Translation: Mastodon/Activerecord
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/activerecord/gl/

* Translated using Weblate (Galician)

Currently translated at 100.0% (43 of 43 strings)

Translation: Mastodon/Devise
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/gl/

* Added translation using Weblate (Galician)

* Translated using Weblate (Galician)

Currently translated at 24.0% (126 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/

* Added translation using Weblate (Portuguese)

* Translated using Weblate (Arabic)

Currently translated at 55.2% (290 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ar/

* Translated using Weblate (Galician)

Currently translated at 42.6% (224 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/

* Translated using Weblate (Portuguese)

Currently translated at 80.9% (425 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ar/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pt/

* Translated using Weblate (Arabic)

Currently translated at 62.7% (27 of 43 strings)

Translation: Mastodon/Devise
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ar/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (75 of 75 strings)

Translation: Mastodon/Doorkeeper
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/pt/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pt/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (56 of 56 strings)

Translation: Mastodon/Preferences
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (2 of 2 strings)

Translation: Mastodon/Activerecord
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/activerecord/pt/

* Translated using Weblate (Portuguese)

Currently translated at 81.3% (427 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Galician)

Currently translated at 100.0% (2 of 2 strings)

Translation: Mastodon/Activerecord
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/activerecord/gl/

* Translated using Weblate (Galician)

Currently translated at 99.8% (524 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/

* Translated using Weblate (Portuguese)

Currently translated at 93.7% (492 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 95.4% (501 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (525 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/

* Translated using Weblate (Galician)

Currently translated at 99.8% (524 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/

* Translated using Weblate (Galician)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/gl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (525 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/

* Translated using Weblate (Portuguese)

Currently translated at 96.0% (504 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

Translation: Mastodon/React
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/

* Translated using Weblate (Arabic)

Currently translated at 69.7% (30 of 43 strings)

Translation: Mastodon/Devise
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ar/

* Translated using Weblate (Portuguese)

Currently translated at 97.9% (514 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (523 of 525 strings)

Translation: Mastodon/Backend
Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt_BR/

* Normalize translations
Ran i18n-tasks normalize && yarn manage:translations

* Add back simple_forms removed by weblate
Weblate for some reason removed this files, add back to their previous
versions

* Remove linebreak on doorkeeper.pt.yml
PR review
2018-01-09 20:30:28 +09:00
David Yip
ce7f4aef16 Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-08 23:20:39 -06:00
David Yip
ec0bdd6c1a Merge pull request #312 from glitch-soc/tag-sensitive-imgs
Show SENSITIVE tag on sensitive images (#267)
2018-01-08 23:19:14 -06:00
David Yip
df04da098a Merge pull request #311 from glitch-soc/dont-show-solo-skins
Don't show skins selector when there's only one (#256)
2018-01-08 23:18:30 -06:00
Jenkins
7c719c567c Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-09 05:17:13 +00:00
David Yip
488381ae2f Merge pull request #310 from glitch-soc/fix-thread-inheritance
Threads now inherit privacy directly (#309)
2018-01-08 23:15:58 -06:00
kibigo!
60433d03f5 Add missing comma 2018-01-08 18:38:28 -08:00
kibigo!
5d2ef7a616 Show SENSITIVE tag on sensitive images (#267) 2018-01-08 18:25:29 -08:00
kibigo!
44792de49a Don't show skins selector when there's only one (#256) 2018-01-08 16:45:32 -08:00
kibigo!
824a790e63 Threads now inherit privacy directly (#309) 2018-01-08 16:35:01 -08:00
puckipedia
628358aeea Add the author of a status to cc if reblogged (#6226)
This makes slightly more sense, and ensures that the author of a post is always referenced in the audience (which some servers might rely on). And the announce is POSTed to the author's inbox anyways.
2018-01-09 00:47:43 +01:00
nightpool
c235711ffe Refactor /api/web APIs to use the centralized axios instance (#6223)
Also adds the ability to decouple the centralized axios logic from the
state dispatcher
2018-01-08 20:01:33 +01:00
Eugen Rochko
ff6ca8bdc6 Bump version to 2.1.3 2018-01-08 19:15:31 +01:00
beatrix
90e568413b Merge pull request #308 from KnzkDev/fix/list-editor
Fix list editor design
2018-01-08 13:08:11 -05:00
ncls7615
ef0b7d1e76 fix list editor scss 2018-01-09 02:50:24 +09:00
David Yip
65986b6f0b Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-08 09:48:42 -06:00
David Yip
2dc4fbbd1a When pulling out max_toot_chars, handle nulls
flavours/glitch/util/initial_state is used in places where we want to
exhibit different behavior based on user preferences.  This means that
it's used in places where no preference is defined, i.e. on an
unauthenticated access.  All values exported from that module must
therefore expect that case; previously, the max chars value didn't.

Addresses #306.
2018-01-08 09:45:59 -06:00
Jenkins
f839ac694c Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-08 10:17:15 +00:00
Eugen Rochko
dbda87c31f Revert #5772 (#6221) 2018-01-08 10:57:52 +01:00
Jenkins
722b3f567f Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-08 04:17:11 +00:00
Eugen Rochko
e4a241abef Fix bad URL schemes being accepted (#6219)
* Fix actors accepting invalid URI schemes or different host between URI and URL

* Fix statuses accepting invalid URI scheme or different host to actor

* Adjust tests to new requirements

* Improve readability of mismatching_origin?/invalid_origin? methods
2018-01-08 05:00:23 +01:00
Eugen Rochko
93555182c3 Do not display elephant friend in single-column layout (#6222) 2018-01-08 03:50:53 +01:00
puckipedia
0eff42d688 Move Article from supported to converted types (#6218) 2018-01-08 00:21:14 +01:00
David Yip
f7c4d4464b Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-07 13:30:52 -06:00
David Yip
70c99a9f34 Use error pack when rendering error pages. Fixes #305. 2018-01-07 13:30:17 -06:00
Jenkins
c2e1bfd9ae Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-07 15:17:13 +00:00
Yamagishi Kazutoshi
1d92b90be9 Fix force_ssl conditional (#6201) 2018-01-07 15:19:23 +01:00
Yamagishi Kazutoshi
da809f9eec Fix unintended cache (#6214) 2018-01-07 15:12:59 +01:00
SerCom_KC
c4d36d024c Update Simplified Chinese translations (#6215)
* i18n: (zh-CN) Add translations of #6125

* i18n: (zh-CN) Add translations of #6132

* i18n: (zh-CN) Add translations of #6099

* i18n: (zh-CN) Add translations of #6071

* i18n: (zh-CN) Improve translations
2018-01-07 17:32:50 +09:00
David Yip
5083311d64 Merge remote-tracking branch 'ykzts/fix-unintended-cache' into gs-master 2018-01-07 00:32:24 -06:00
Yamagishi Kazutoshi
2af307bce4 Fix unintended cache 2018-01-07 14:59:12 +09:00
Jenkins
bcbdd4f88d Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-07 02:17:10 +00:00
Jeong Arm
9e97fbf0af Translate Korean (#6212) 2018-01-07 11:13:42 +09:00
kibigo!
b5874c1428 Fixes to search dropdown 2018-01-06 15:34:01 -08:00
beatrix
61ef8d643e fix typo in vanilla names.yml 2018-01-06 16:49:53 -05:00
Ondřej Hruška
9f29fd31ba fixed ctrl enter 2018-01-06 19:58:04 +01:00
Ondřej Hruška
53caab0c0b Fix the always-threaded bug 2018-01-06 19:55:53 +01:00
beatrix-bitrot
b75a1ce326 tighten csp 2018-01-06 18:49:03 +00:00
beatrix
d442cfa65c Merge pull request #303 from KnzkDev/ja-for-thread-mode
Update ja.js for #296
2018-01-06 12:06:17 -05:00
ncls7615
f5a4201ad8 Update ja.js 2018-01-07 01:51:49 +09:00
beatrix
a251c42192 Merge pull request #296 from glitch-soc/thread-mode
Threaded mode~
2018-01-06 11:28:36 -05:00
beatrix
2ec9a75a1d Merge pull request #302 from KnzkDev/fix/search-popout
Fix search popout
2018-01-06 11:25:59 -05:00
beatrix
fa92e88fb2 appease eslint 2018-01-06 10:30:49 -05:00
ncls7615
da98c33161 Fix search popout 2018-01-06 21:50:11 +09:00
David Yip
2eed4ace11 Read max_toot_chars from root object. Fixes #297.
max_toot_chars is present in the root of the initial state object.
(Previously, we were trying to read it from the meta child object.)
2018-01-06 03:01:11 -06:00
kibigo!
c71d848855 my global .gitignore excluded this file ;_; 2018-01-05 21:40:02 -08:00
kibigo!
e4bc013d6f Threaded mode~ 2018-01-05 21:16:43 -08:00
kibigo!
6932b464e6 Fixed improper dropdown func binding for #293 + toot button spacing 2018-01-05 21:02:53 -08:00
kibigo!
ad10a80a99 Styling and autosuggest fixes for #293 2018-01-05 20:43:16 -08:00
kibigo!
8bf9d9362a Fixes composer mounting issue with #293 2018-01-05 18:30:06 -08:00
David Yip
03aeab857f Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-05 17:31:56 -06:00
beatrix
f441770e50 Merge pull request #290 from chriswmartin/web-push-updates
Web push updates
2018-01-05 18:29:57 -05:00
beatrix
b4e667f86b Merge pull request #295 from chriswmartin/getting-started-key-fix
unique ColumnLink keys in getting_started
2018-01-05 18:29:40 -05:00
beatrix
faf20eeaa4 Merge pull request #293 from glitch-soc/compose-refactor
Compose refactor
2018-01-05 18:29:08 -05:00
Jenkins
f6adb409fd Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-05 22:17:12 +00:00
ThibG
10f6793fd0 Fix PuSH workers (#6200) 2018-01-05 23:04:35 +01:00
ThibG
a594139115 When fetching an ActivityPub-enabled status, do not re-request it as text/html (#6196) 2018-01-05 22:42:50 +01:00
TheKinrar
95bd85d9e8 Represent numbers by strings in instance activity API (#6198)
Fixes #6197.
2018-01-05 22:38:33 +01:00
kibigo!
ac686d5a5d Fixed overflow issue 2018-01-05 13:34:21 -08:00
kibigo!
ec620ae486 Styling fixes 2018-01-05 12:41:15 -08:00
Naoki Kosaka
8d51ce4290 Fix enforce HTTPS in production. (#6180) 2018-01-05 20:04:22 +01:00
beatrix
f41b33eb01 Merge pull request #243 from m4sk1n/glitch-pl
i18n: 🇵🇱
2018-01-05 12:36:53 -05:00
cwm
9fc08e4861 add key to lists div 2018-01-05 09:00:48 -06:00
cwm
6236577734 change how list ColumnLink keys are determined 2018-01-05 08:12:34 -06:00
Quenty31
06636c6eca l10n Occitan language: mailer update (#6193)
* Create email_changed.oc.html.erb

* Create email_changed.oc.text.erb

* Update email_changed.oc.html.erb

* Update email_changed.oc.html.erb

* Create reconfirmation_instructions.oc.html.erb

* Create reconfirmation_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update reconfirmation_instructions.oc.html.erb

* Update reconfirmation_instructions.oc.text.erb

* Update reconfirmation_instructions.oc.html.erb
2018-01-05 18:59:43 +09:00
Eugen Rochko
e9822a4e4e Bump version to 2.1.2 2018-01-05 04:52:06 +01:00
Yamagishi Kazutoshi
9a61b0ef22 Fix RFC 5646 Regular Expression (#6190) 2018-01-05 04:43:50 +01:00
Jenkins
c69a23ae46 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-04 23:17:11 +00:00
Branko Kokanovic
d872902997 Small translation fixes for Serbian (and sr@Latn too) (#6188) 2018-01-05 00:16:06 +01:00
Patrick Figel
5ec25ff3e1 Fix email confirmation link not updating email (#6187)
A change introduced in #6125 prevents
`Devise::Models::Confirmable#confirm` from being called for existing
users, which in turn leads to `email` not being set to
`unconfirmed_email`, breaking email updates. This also adds a test
that would've caught this issue.
2018-01-05 00:15:35 +01:00
Lynx Kotoura
49e296e1b0 Fix overflowing audit logs (#6184) 2018-01-04 19:38:46 +01:00
unarist
7347d4f8bb Use disable_ddl_transaction! to prevent warnings on migration (#6183)
Migration is wrapped by transaction, so manual `commit_db_transaction` without transaction restarting causes "there is no transaction in progress" warnings. We should use `disable_ddl_transaction!` instead, if we can omit transaction completely.
2018-01-04 19:38:29 +01:00
Nolan Darilek
3b016342c6 Fix accessibility of column headers
As a screen reader user new to Mastodon, I encountered the following issues with the column headers as designed:
 * Jumping between them was difficult. FOr instance, passing my home timeline to reach notification settings was difficult to impossible, especially considering infinite scrolling.
 * There doesn't appear to be any means for triggering the control via the keyboard. the `titleClick` handler only responds to mouse clicks.
 * I didn't even realize there was a Settings toggle until I made this change.

Thanks for using ARIA in your designs. It's a huge help. But adding a `button` role doesn't add keyboard handling and other button behavior. Also, because the role was on the heading container, it obscured the controls within the container itself. This fix resolve that. It also exposes the headings as headings rather than buttons, enabling skipping columns by using screen readers' heading navigation commands.

Since I myself am blind, if this fix requires additional visual styling, I'd like help applying that so it can be merged. I'd consider it an essential accessibility fix for my and other blind users' existence on the platform. Thanks!
2018-01-04 10:25:26 -06:00
Eugen Rochko
7571c37c99 Bump version to 2.1.1 (#6164) 2018-01-04 16:40:26 +01:00
Yamagishi Kazutoshi
3c18964256 Fallback default thumbnail in instance status API (#6177) 2018-01-04 15:36:55 +01:00
Marcin Mikołajczak
c61dd918a2 i18n: Update Polish translation (#6176)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-04 23:15:29 +09:00
Marcin Mikołajczak
0f69a90588 i18n: Update Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-04 14:42:58 +01:00
Eugen Rochko
02ba03d6db Send one Delete of Actor in ActivityPub when account is suspended (#6172) 2018-01-04 14:40:49 +01:00
ThibG
3bee0996c5 Make sure private toots remain private and do not end up in HTTP caches (#6175) 2018-01-04 14:39:38 +01:00
muan
89daeb43a8 Improve Traditional Chinese translation (#6166)
* Improve Traditional Chinese translations

* Sort alphabetically
2018-01-04 05:00:50 +01:00
Eugen Rochko
7d4f4f9aab Fix FetchAtomService not finding alternatives if there's a Link header (#6170)
without them, such as is the case with GNU social

Fixes the ability to find GNU social accounts via URL in search and
when using remote follow function
2018-01-04 04:56:04 +01:00
Akihiko Odaki
256c2b1de0 Rearrange items in Getting Started navigation (#6126)
Though the subsections are representing features such as navigation and
settings, they are categorized by the ways how they are implemented
(internal navigation or external links.) They are irrelevant and some
arrangements were confusing because of that. (It is nonsense that instance
information is in settings subsection, for example.)

This fixes the issue by rearranging.
2018-01-04 10:56:54 +09:00
Eugen Rochko
02e3e1ec09 Fix nil error in log_target_from_history helper (#6173) 2018-01-04 10:56:23 +09:00
Eugen Rochko
ff924f95bb Fix OpenSSL dependency in ostatus2 (#6174) 2018-01-04 10:56:00 +09:00
Eugen Rochko
c10f4bdb03 Cache JSON of immutable ActivityPub representations (#6171) 2018-01-04 01:21:38 +01:00
cwm
72b99f6ee4 bug fix (tootsuite pr #6120) 2017-12-31 08:26:50 -06:00
cwm
4ce44ba470 remove unused 'saveSettings' from column_settings_container 2017-12-30 16:42:26 -06:00
cwm
0dce26b82b web push updates (tootsuite PRs #5879, #5941, #6047) 2017-12-30 11:45:01 -06:00
167 changed files with 3537 additions and 1070 deletions

View File

@@ -61,6 +61,9 @@ RUN apk -U upgrade \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
COPY stack-fix.c /lib
RUN gcc -shared -fPIC /lib/stack-fix.c -o /lib/stack-fix.so
RUN rm /lib/stack-fix.c
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \ RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \ && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \

View File

@@ -299,13 +299,11 @@ GEM
sidekiq (>= 3.5.0) sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0) statsd-ruby (~> 1.2.0)
oj (3.3.9) oj (3.3.9)
openssl (2.0.6)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostatus2 (2.0.1) ostatus2 (2.0.2)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.8.2) ox (2.8.2)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)

View File

@@ -2,7 +2,8 @@
class AccountsController < ApplicationController class AccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureVerification
before_action :set_cache_headers
def show def show
respond_to do |format| respond_to do |format|
@@ -27,10 +28,11 @@ class AccountsController < ApplicationController
end end
format.json do format.json do
render json: @account, skip_session!
serializer: ActivityPub::ActorSerializer,
adapter: ActivityPub::Adapter, render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
content_type: 'application/activity+json' ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end
end end
end end
end end

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FollowsController < Api::BaseController
include SignatureVerification
def show
render(
json: FollowRequest.includes(:account).references(:account).find_by!(
id: params.require(:id),
accounts: { domain: nil, username: params.require(:account_username) },
target_account: signed_request_account
),
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
)
end
end

View File

@@ -6,8 +6,8 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
skip_before_action :verify_authenticity_token
skip_before_action :store_current_location skip_before_action :store_current_location
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422

View File

@@ -21,9 +21,9 @@ class Api::V1::Instances::ActivityController < Api::BaseController
weeks << { weeks << {
week: week.to_time.to_i.to_s, week: week.to_time.to_i.to_s,
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0, statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
logins: Redis.current.pfcount("activity:logins:#{week_id}"), logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0, registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
} }
end end

View File

@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
private private
def https_enabled? def https_enabled?
Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true' Rails.env.production?
end end
def store_current_location def store_current_location
@@ -192,17 +192,31 @@ class ApplicationController < ActionController::Base
format.any { head code } format.any { head code }
format.html do format.html do
set_locale set_locale
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code render "errors/#{code}", layout: 'error', status: code
end end
end end
end end
def render_cached_json(cache_key, **options) def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
cache_public = options.key?(:public) ? options.delete(:public) : true
content_type = options.delete(:content_type) || 'application/json'
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json yield.to_json
end end
expires_in options[:expires_in], public: true expires_in options[:expires_in], public: cache_public
render json: data render json: data, content_type: content_type
end
def set_cache_headers
response.headers['Vary'] = 'Accept'
end
def skip_session!
request.session_options[:skip] = true
end end
end end

View File

@@ -2,14 +2,16 @@
class EmojisController < ApplicationController class EmojisController < ApplicationController
before_action :set_emoji before_action :set_emoji
before_action :set_cache_headers
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: @emoji, skip_session!
serializer: ActivityPub::EmojiSerializer,
adapter: ActivityPub::Adapter, render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
content_type: 'application/activity+json' ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end
end end
end end
end end

View File

@@ -1,13 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class Settings::FlavoursController < Settings::BaseController class Settings::FlavoursController < Settings::BaseController
def index def index
redirect_to action: 'show', flavour: current_flavour redirect_to action: 'show', flavour: current_flavour
end end
def show def show
unless Themes.instance.flavours.include?(params[:flavour]) or params[:flavour] == current_flavour unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour)
redirect_to action: 'show', flavour: current_flavour redirect_to action: 'show', flavour: current_flavour
end end
@@ -16,7 +15,7 @@ class Settings::FlavoursController < Settings::BaseController
end end
def update def update
user_settings.update(user_settings_params(params[:flavour]).to_h) user_settings.update(user_settings_params)
redirect_to action: 'show', flavour: params[:flavour] redirect_to action: 'show', flavour: params[:flavour]
end end
@@ -26,10 +25,8 @@ class Settings::FlavoursController < Settings::BaseController
UserSettingsDecorator.new(current_user) UserSettingsDecorator.new(current_user)
end end
def user_settings_params(flavour) def user_settings_params
params.require(:user).merge({ setting_flavour: flavour }).permit( { setting_flavour: params.require(:flavour),
:setting_flavour, setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access
:setting_skin
)
end end
end end

View File

@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :check_account_suspension
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
before_action { response.headers['Vary'] = 'Accept' } before_action :set_cache_headers
def show def show
respond_to do |format| respond_to do |format|
@@ -23,25 +23,21 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
render json: @status, skip_session! unless @stream_entry.hidden?
serializer: ActivityPub::NoteSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
# Allow HTTP caching for 3 minutes if the status is public render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
unless @stream_entry.hidden? ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
request.session_options[:skip] = true
expires_in(3.minutes, public: true)
end end
end end
end end
end end
def activity def activity
render json: @status, skip_session!
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter, render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
content_type: 'application/activity+json' ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end
end end
def embed def embed

View File

@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
link_to attributes['domain'], "https://#{attributes['domain']}" link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status' when 'Status'
tmp_status = Status.new(attributes) tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status) link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
end end
end end

View File

@@ -39,6 +39,10 @@ module JsonLdHelper
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) !json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end end
def unsupported_uri_scheme?(uri)
!uri.start_with?('http://', 'https://')
end
def canonicalize(json) def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json) graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
graph.dump(:normalize) graph.dump(:normalize)

View File

@@ -4,6 +4,7 @@ module RoutingHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::AssetTagHelper
include Webpacker::Helper
included do included do
def default_url_options def default_url_options
@@ -17,6 +18,10 @@ module RoutingHelper
URI.join(root_url, source).to_s URI.join(root_url, source).to_s
end end
def full_pack_url(source, **options)
full_asset_url(asset_pack_path(source, options))
end
private private
def use_storage? def use_storage?

View File

@@ -61,7 +61,7 @@ export function replyCompose(status, router) {
status: status, status: status,
}); });
if (!getState().getIn(['compose', 'mounted'])) { if (router && !getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new'); router.push('/statuses/new');
} }
}; };
@@ -118,6 +118,11 @@ export function submitCompose() {
}).then(function (response) { }).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
// If the response has no data then we can't do anything else.
if (!response.data) {
return;
}
// To make the app more responsive, immediately get the status into the columns // To make the app more responsive, immediately get the status into the columns
const insertOrRefresh = (timelineId, refreshAction) => { const insertOrRefresh = (timelineId, refreshAction) => {
@@ -341,10 +346,11 @@ export function unmountCompose() {
}; };
}; };
export function toggleComposeAdvancedOption(option) { export function changeComposeAdvancedOption(option, value) {
return { return {
option,
type: COMPOSE_ADVANCED_OPTIONS_CHANGE, type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
option: option, value,
}; };
} }

View File

@@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA
export function fetchFavouritedStatuses() { export function fetchFavouritedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return;
}
dispatch(fetchFavouritedStatusesRequest()); dispatch(fetchFavouritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => { api(getState).get('/api/v1/favourites').then(response => {
@@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'favourites', 'next'], null); const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
if (url === null) { if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return; return;
} }

View File

@@ -42,7 +42,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
const unescapeHTML = (html) => { const unescapeHTML = (html) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
html = html.replace(/<br \/>|<br>|\n/, ' '); html = html.replace(/<br \/>|<br>|\n/g, ' ');
wrapper.innerHTML = html; wrapper.innerHTML = html;
return wrapper.textContent; return wrapper.textContent;
}; };

View File

@@ -0,0 +1,23 @@
import {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
setAlerts,
} from './setter';
import { register, saveSettings } from './registerer';
export {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
register,
};
export function changeAlerts(path, value) {
return dispatch => {
dispatch(setAlerts(path, value));
dispatch(saveSettings());
};
}

View File

@@ -0,0 +1,149 @@
import api from 'flavours/glitch/util/api';
import { pushNotificationsSetting } from 'flavours/glitch/util/settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
const subscribe = (registration) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
});
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (getState, subscription, me) => {
const params = { subscription };
if (me) {
const data = pushNotificationsSetting.get(me);
if (data) {
params.data = data;
}
}
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
export function register () {
return (dispatch, getState) => {
dispatch(setBrowserSupport(supportsPushNotifications));
const me = getState().getIn(['meta', 'me']);
if (me && !pushNotificationsSetting.get(me)) {
const alerts = getState().getIn(['push_notifications', 'alerts']);
if (alerts) {
pushNotificationsSetting.set(me, { alerts: alerts });
}
}
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
return;
}
getRegistration()
.then(getPushSubscription)
.then(({ registration, subscription }) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return subscription;
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
}
}
// No subscription, try to subscribe
return subscribe(registration).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
dispatch(setSubscription(subscription));
if (me) {
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
}
}
})
.catch(error => {
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
}
// Clear alerts and hide UI settings
dispatch(clearSubscription());
if (me) {
pushNotificationsSetting.remove(me);
}
try {
getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
} catch (e) {
}
});
} else {
console.warn('Your browser does not support Web Push Notifications.');
}
};
}
export function saveSettings() {
return (_, getState) => {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
const data = { alerts };
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data,
}).then(() => {
const me = getState().getIn(['meta', 'me']);
if (me) {
pushNotificationsSetting.set(me, data);
}
});
};
}

View File

@@ -1,9 +1,7 @@
import axios from 'axios';
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
export function setBrowserSupport (value) { export function setBrowserSupport (value) {
return { return {
@@ -25,28 +23,12 @@ export function clearSubscription () {
}; };
} }
export function changeAlerts(key, value) { export function setAlerts (path, value) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
type: ALERTS_CHANGE, type: SET_ALERTS,
key, path,
value, value,
}); });
dispatch(saveSettings());
};
}
export function saveSettings() {
return (_, getState) => {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data: {
alerts,
},
});
}; };
} }

View File

@@ -1,14 +1,14 @@
import axios from 'axios'; import api from 'flavours/glitch/util/api';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE'; export const SETTING_SAVE = 'SETTING_SAVE';
export function changeSetting(key, value) { export function changeSetting(path, value) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
type: SETTING_CHANGE, type: SETTING_CHANGE,
key, path,
value, value,
}); });
@@ -21,9 +21,9 @@ const debouncedSave = debounce((dispatch, getState) => {
return; return;
} }
const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS(); const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
}, 5000, { trailing: true }); }, 5000, { trailing: true });
export function saveSettings() { export function saveSettings() {

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from './avatar'; import Avatar from './avatar';
@@ -94,21 +94,33 @@ export default class Account extends ImmutablePureComponent {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />; hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
} }
buttons = ( buttons = (
<div> <Fragment>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} /> <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton} {hidingNotificationsButton}
</div> </Fragment>
); );
} else { } else if (!account.get('moved')) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
} }
} }
return small ? ( return small ? (
<div className='account small'> <Permalink
<div className='account__avatar-wrapper'><Avatar account={account} size={18} /></div> className='account small'
<DisplayName account={account} /> href={account.get('url')}
</div> to={`/accounts/${account.get('id')}`}
>
<div className='account__avatar-wrapper'>
<Avatar
account={account}
size={24}
/>
</div>
<DisplayName
account={account}
inline
/>
</Permalink>
) : ( ) : (
<div className='account'> <div className='account'>
<div className='account__wrapper'> <div className='account__wrapper'>

View File

@@ -1,28 +1,30 @@
// Package imports.
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
export default class DisplayName extends React.PureComponent { // The component.
export default function DisplayName ({
static propTypes = { account,
account: ImmutablePropTypes.map.isRequired, className,
className: PropTypes.string, inline,
}; }) {
const computedClass = classNames('display-name', { inline }, className);
render () {
const {
account,
className,
} = this.props;
const computedClass = classNames('display-name', className);
const displayNameHtml = { __html: account.get('display_name_html') };
return (
<span className={computedClass}>
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
</span>
);
}
// The result.
return account ? (
<span className={computedClass}>
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
{inline ? ' ' : null}
<span className='display-name__account'>@{account.get('acct')}</span>
</span>
) : null;
} }
// Props.
DisplayName.propTypes = {
account: ImmutablePropTypes.map,
className: PropTypes.string,
inline: PropTypes.bool,
};

View File

@@ -137,7 +137,7 @@ export default class Dropdown extends React.PureComponent {
(item, i) => item ? { (item, i) => item ? {
...item, ...item,
name: `${item.text}-${i}`, name: `${item.text}-${i}`,
onClick: this.handleItemClick.bind(i), onClick: this.handleItemClick.bind(this, i),
} : null } : null
), ),
}); });

View File

@@ -9,7 +9,26 @@ import classNames from 'classnames';
import { autoPlayGif } from 'flavours/glitch/util/initial_state'; import { autoPlayGif } from 'flavours/glitch/util/initial_state';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, hidden: {
defaultMessage: 'Media hidden',
id: 'status.media_hidden',
},
sensitive: {
defaultMessage: 'Sensitive',
id: 'media_gallery.sensitive',
},
toggle: {
defaultMessage: 'Click to view',
id: 'status.sensitive_toggle',
},
toggle_visible: {
defaultMessage: 'Toggle visibility',
id: 'media_gallery.toggle_visible',
},
warning: {
defaultMessage: 'Sensitive content',
id: 'status.sensitive_warning',
},
}); });
class Item extends React.PureComponent { class Item extends React.PureComponent {
@@ -206,48 +225,79 @@ export default class MediaGallery extends React.PureComponent {
this.props.onOpenMedia(this.props.media, index); this.props.onOpenMedia(this.props.media, index);
} }
isStandaloneEligible() {
const { media, standalone } = this.props;
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
}
render () { render () {
const { media, intl, sensitive, letterbox, fullwidth } = this.props; const {
handleClick,
handleOpen,
} = this;
const {
fullwidth,
intl,
letterbox,
media,
sensitive,
standalone,
} = this.props;
const { visible } = this.state; const { visible } = this.state;
const size = media.take(4).size; const size = media.take(4).size;
const computedClass = classNames('media-gallery', `size-${size}`, { 'full-width': fullwidth });
let children;
if (!visible) {
let warning;
if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else {
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
}
children = (
<button className='media-spoiler' onClick={this.handleOpen}>
<span className='media-spoiler__warning'>{warning}</span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</button>
);
} else {
if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />);
}
}
return ( return (
<div className={`media-gallery size-${size} ${fullwidth ? 'full-width' : ''}`}> <div className={computedClass}>
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> {visible ? (
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> <div className='sensitive-info'>
</div> <IconButton
icon='eye'
{children} onClick={handleOpen}
overlay
title={intl.formatMessage(messages.toggle_visible)}
/>
{sensitive ? (
<span className='sensitive-marker'>
<FormattedMessage {...messages.sensitive} />
</span>
) : null}
</div>
) : null}
{function () {
switch (true) {
case !visible:
return (
<button
className='media-spoiler'
onClick={handleOpen}
>
<span className='media-spoiler__warning'>
<FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />
</span>
<span className='media-spoiler__trigger'>
<FormattedMessage {...messages.toggle} />
</span>
</button>
);
case standalone && media.size === 1 && !!media.getIn([0, 'meta', 'small', 'aspect']):
return (
<Item
attachment={media.get(0)}
onClick={handleClick}
standalone
/>
);
default:
return media.take(4).map(
(attachment, i) => (
<Item
attachment={attachment}
index={i}
key={attachment.get('id')}
letterbox={letterbox}
onClick={handleClick}
size={size}
/>
)
);
}
}()}
</div> </div>
); );
} }

View File

@@ -22,7 +22,13 @@ export default class Permalink extends React.PureComponent {
} }
render () { render () {
const { href, children, className, ...other } = this.props; const {
children,
className,
href,
to,
...other
} = this.props;
return ( return (
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>

View File

@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChange (key, checked) { onChange (path, checked) {
dispatch(changeSetting(['community', ...key], checked)); dispatch(changeSetting(['community', ...path], checked));
}, },
}); });

View File

@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { import {
cancelReplyCompose, cancelReplyCompose,
changeCompose, changeCompose,
changeComposeAdvancedOption,
changeComposeSensitivity, changeComposeSensitivity,
changeComposeSpoilerText, changeComposeSpoilerText,
changeComposeSpoilerness, changeComposeSpoilerness,
@@ -15,10 +16,11 @@ import {
clearComposeSuggestions, clearComposeSuggestions,
fetchComposeSuggestions, fetchComposeSuggestions,
insertEmojiCompose, insertEmojiCompose,
mountCompose,
selectComposeSuggestion, selectComposeSuggestion,
submitCompose, submitCompose,
toggleComposeAdvancedOption,
undoUploadCompose, undoUploadCompose,
unmountCompose,
uploadCompose, uploadCompose,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
import { import {
@@ -47,8 +49,8 @@ function mapStateToProps (state) {
const inReplyTo = state.getIn(['compose', 'in_reply_to']); const inReplyTo = state.getIn(['compose', 'in_reply_to']);
return { return {
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
advancedOptions: state.getIn(['compose', 'advanced_options']),
amUnlocked: !state.getIn(['accounts', me, 'locked']), amUnlocked: !state.getIn(['accounts', me, 'locked']),
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
focusDate: state.getIn(['compose', 'focusDate']), focusDate: state.getIn(['compose', 'focusDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']), isSubmitting: state.getIn(['compose', 'is_submitting']),
isUploading: state.getIn(['compose', 'is_uploading']), isUploading: state.getIn(['compose', 'is_uploading']),
@@ -57,7 +59,7 @@ function mapStateToProps (state) {
preselectDate: state.getIn(['compose', 'preselectDate']), preselectDate: state.getIn(['compose', 'preselectDate']),
privacy: state.getIn(['compose', 'privacy']), privacy: state.getIn(['compose', 'privacy']),
progress: state.getIn(['compose', 'progress']), progress: state.getIn(['compose', 'progress']),
replyAccount: inReplyTo ? state.getIn(['accounts', state.getIn(['statuses', inReplyTo, 'account'])]) : null, replyAccount: inReplyTo ? state.getIn(['statuses', inReplyTo, 'account']) : null,
replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null, replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
sideArm: state.getIn(['local_settings', 'side_arm']), sideArm: state.getIn(['local_settings', 'side_arm']),
@@ -74,6 +76,7 @@ function mapStateToProps (state) {
// Dispatch mapping. // Dispatch mapping.
const mapDispatchToProps = { const mapDispatchToProps = {
onCancelReply: cancelReplyCompose, onCancelReply: cancelReplyCompose,
onChangeAdvancedOption: changeComposeAdvancedOption,
onChangeDescription: changeUploadCompose, onChangeDescription: changeUploadCompose,
onChangeSensitivity: changeComposeSensitivity, onChangeSensitivity: changeComposeSensitivity,
onChangeSpoilerText: changeComposeSpoilerText, onChangeSpoilerText: changeComposeSpoilerText,
@@ -84,12 +87,13 @@ const mapDispatchToProps = {
onCloseModal: closeModal, onCloseModal: closeModal,
onFetchSuggestions: fetchComposeSuggestions, onFetchSuggestions: fetchComposeSuggestions,
onInsertEmoji: insertEmojiCompose, onInsertEmoji: insertEmojiCompose,
onMount: mountCompose,
onOpenActionsModal: openModal.bind(null, 'ACTIONS'), onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }), onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
onSelectSuggestion: selectComposeSuggestion, onSelectSuggestion: selectComposeSuggestion,
onSubmit: submitCompose, onSubmit: submitCompose,
onToggleAdvancedOption: toggleComposeAdvancedOption,
onUndoUpload: undoUploadCompose, onUndoUpload: undoUploadCompose,
onUnmount: unmountCompose,
onUpload: uploadCompose, onUpload: uploadCompose,
}; };
@@ -188,6 +192,22 @@ class Composer extends React.Component {
} }
} }
// Tells our state the composer has been mounted.
componentDidMount () {
const { onMount } = this.props;
if (onMount) {
onMount();
}
}
// Tells our state the composer has been unmounted.
componentWillUnmount () {
const { onUnmount } = this.props;
if (onUnmount) {
onUnmount();
}
}
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end // - Replying to zero or one users, places the cursor at the end
@@ -245,17 +265,17 @@ class Composer extends React.Component {
handleSubmit, handleSubmit,
handleRefTextarea, handleRefTextarea,
} = this.handlers; } = this.handlers;
const { history } = this.context;
const { const {
acceptContentTypes, acceptContentTypes,
advancedOptions,
amUnlocked, amUnlocked,
doNotFederate,
intl, intl,
isSubmitting, isSubmitting,
isUploading, isUploading,
layout, layout,
media, media,
onCancelReply, onCancelReply,
onChangeAdvancedOption,
onChangeDescription, onChangeDescription,
onChangeSensitivity, onChangeSensitivity,
onChangeSpoilerness, onChangeSpoilerness,
@@ -266,7 +286,6 @@ class Composer extends React.Component {
onFetchSuggestions, onFetchSuggestions,
onOpenActionsModal, onOpenActionsModal,
onOpenDoodleModal, onOpenDoodleModal,
onToggleAdvancedOption,
onUndoUpload, onUndoUpload,
onUpload, onUpload,
privacy, privacy,
@@ -297,12 +316,12 @@ class Composer extends React.Component {
<ComposerReply <ComposerReply
account={replyAccount} account={replyAccount}
content={replyContent} content={replyContent}
history={history}
intl={intl} intl={intl}
onCancel={onCancelReply} onCancel={onCancelReply}
/> />
) : null} ) : null}
<ComposerTextarea <ComposerTextarea
advancedOptions={advancedOptions}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
disabled={isSubmitting} disabled={isSubmitting}
intl={intl} intl={intl}
@@ -329,19 +348,19 @@ class Composer extends React.Component {
) : null} ) : null}
<ComposerOptions <ComposerOptions
acceptContentTypes={acceptContentTypes} acceptContentTypes={acceptContentTypes}
advancedOptions={advancedOptions}
disabled={isSubmitting} disabled={isSubmitting}
doNotFederate={doNotFederate}
full={media.size >= 4 || media.some( full={media.size >= 4 || media.some(
item => item.get('type') === 'video' item => item.get('type') === 'video'
)} )}
hasMedia={!!media.size} hasMedia={!!media.size}
intl={intl} intl={intl}
onChangeAdvancedOption={onChangeAdvancedOption}
onChangeSensitivity={onChangeSensitivity} onChangeSensitivity={onChangeSensitivity}
onChangeVisibility={onChangeVisibility} onChangeVisibility={onChangeVisibility}
onDoodleOpen={onOpenDoodleModal} onDoodleOpen={onOpenDoodleModal}
onModalClose={onCloseModal} onModalClose={onCloseModal}
onModalOpen={onOpenActionsModal} onModalOpen={onOpenActionsModal}
onToggleAdvancedOption={onToggleAdvancedOption}
onToggleSpoiler={onChangeSpoilerness} onToggleSpoiler={onChangeSpoilerness}
onUpload={onUpload} onUpload={onUpload}
privacy={privacy} privacy={privacy}
@@ -350,7 +369,7 @@ class Composer extends React.Component {
spoiler={spoiler} spoiler={spoiler}
/> />
<ComposerPublisher <ComposerPublisher
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`} countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length} disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
intl={intl} intl={intl}
onSecondarySubmit={handleSecondarySubmit} onSecondarySubmit={handleSecondarySubmit}
@@ -364,19 +383,14 @@ class Composer extends React.Component {
} }
// Context
Composer.contextTypes = {
history: PropTypes.object,
};
// Props. // Props.
Composer.propTypes = { Composer.propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
// State props. // State props.
acceptContentTypes: PropTypes.string, acceptContentTypes: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
amUnlocked: PropTypes.bool, amUnlocked: PropTypes.bool,
doNotFederate: PropTypes.bool,
focusDate: PropTypes.instanceOf(Date), focusDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool, isSubmitting: PropTypes.bool,
isUploading: PropTypes.bool, isUploading: PropTypes.bool,
@@ -385,7 +399,7 @@ Composer.propTypes = {
preselectDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date),
privacy: PropTypes.string, privacy: PropTypes.string,
progress: PropTypes.number, progress: PropTypes.number,
replyAccount: ImmutablePropTypes.map, replyAccount: PropTypes.string,
replyContent: PropTypes.string, replyContent: PropTypes.string,
resetFileKey: PropTypes.number, resetFileKey: PropTypes.number,
sideArm: PropTypes.string, sideArm: PropTypes.string,
@@ -399,6 +413,7 @@ Composer.propTypes = {
// Dispatch props. // Dispatch props.
onCancelReply: PropTypes.func, onCancelReply: PropTypes.func,
onChangeAdvancedOption: PropTypes.func,
onChangeDescription: PropTypes.func, onChangeDescription: PropTypes.func,
onChangeSensitivity: PropTypes.func, onChangeSensitivity: PropTypes.func,
onChangeSpoilerText: PropTypes.func, onChangeSpoilerText: PropTypes.func,
@@ -409,12 +424,13 @@ Composer.propTypes = {
onCloseModal: PropTypes.func, onCloseModal: PropTypes.func,
onFetchSuggestions: PropTypes.func, onFetchSuggestions: PropTypes.func,
onInsertEmoji: PropTypes.func, onInsertEmoji: PropTypes.func,
onMount: PropTypes.func,
onOpenActionsModal: PropTypes.func, onOpenActionsModal: PropTypes.func,
onOpenDoodleModal: PropTypes.func, onOpenDoodleModal: PropTypes.func,
onSelectSuggestion: PropTypes.func, onSelectSuggestion: PropTypes.func,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onUndoUpload: PropTypes.func, onUndoUpload: PropTypes.func,
onUnmount: PropTypes.func,
onUpload: PropTypes.func, onUpload: PropTypes.func,
}; };

View File

@@ -104,7 +104,10 @@ export default class ComposerOptionsDropdownContentItem extends React.PureCompon
<strong>{text}</strong> <strong>{text}</strong>
{meta} {meta}
</div> </div>
) : <div className='content'>{text}</div>} ) :
<div className='content'>
<strong>{text}</strong>
</div>}
</div> </div>
); );
} }

View File

@@ -1,6 +1,7 @@
// Package imports. // Package imports.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { import {
FormattedMessage, FormattedMessage,
defineMessages, defineMessages,
@@ -47,11 +48,11 @@ const messages = defineMessages({
}, },
local_only_long: { local_only_long: {
defaultMessage: 'Do not post to other instances', defaultMessage: 'Do not post to other instances',
id: 'advanced-options.local-only.long', id: 'advanced_options.local-only.long',
}, },
local_only_short: { local_only_short: {
defaultMessage: 'Local-only', defaultMessage: 'Local-only',
id: 'advanced-options.local-only.short', id: 'advanced_options.local-only.short',
}, },
private_long: { private_long: {
defaultMessage: 'Post to followers only', defaultMessage: 'Post to followers only',
@@ -77,6 +78,14 @@ const messages = defineMessages({
defaultMessage: 'Hide text behind warning', defaultMessage: 'Hide text behind warning',
id: 'compose_form.spoiler', id: 'compose_form.spoiler',
}, },
threaded_mode_long: {
defaultMessage: 'Automatically opens a reply on posting',
id: 'advanced_options.threaded_mode.long',
},
threaded_mode_short: {
defaultMessage: 'Threaded mode',
id: 'advanced_options.threaded_mode.short',
},
unlisted_long: { unlisted_long: {
defaultMessage: 'Do not show in public timelines', defaultMessage: 'Do not show in public timelines',
id: 'privacy.unlisted.long', id: 'privacy.unlisted.long',
@@ -149,16 +158,16 @@ export default class ComposerOptions extends React.PureComponent {
} = this.handlers; } = this.handlers;
const { const {
acceptContentTypes, acceptContentTypes,
advancedOptions,
disabled, disabled,
doNotFederate,
full, full,
hasMedia, hasMedia,
intl, intl,
onChangeAdvancedOption,
onChangeSensitivity, onChangeSensitivity,
onChangeVisibility, onChangeVisibility,
onModalClose, onModalClose,
onModalOpen, onModalOpen,
onToggleAdvancedOption,
onToggleSpoiler, onToggleSpoiler,
privacy, privacy,
resetFileKey, resetFileKey,
@@ -283,23 +292,31 @@ export default class ComposerOptions extends React.PureComponent {
onClick={onToggleSpoiler} onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)} title={intl.formatMessage(messages.spoiler)}
/> />
<Dropdown {advancedOptions ? (
active={doNotFederate} <Dropdown
disabled={disabled} active={advancedOptions.some(value => !!value)}
icon='home' disabled={disabled}
items={[ icon='ellipsis-h'
{ items={[
meta: <FormattedMessage {...messages.local_only_long} />, {
name: 'do_not_federate', meta: <FormattedMessage {...messages.local_only_long} />,
on: doNotFederate, name: 'do_not_federate',
text: <FormattedMessage {...messages.local_only_short} />, on: advancedOptions.get('do_not_federate'),
}, text: <FormattedMessage {...messages.local_only_short} />,
]} },
onChange={onToggleAdvancedOption} {
onModalClose={onModalClose} meta: <FormattedMessage {...messages.threaded_mode_long} />,
onModalOpen={onModalOpen} name: 'threaded_mode',
title={intl.formatMessage(messages.advanced_options_icon_title)} on: advancedOptions.get('threaded_mode'),
/> text: <FormattedMessage {...messages.threaded_mode_short} />,
},
]}
onChange={onChangeAdvancedOption}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.advanced_options_icon_title)}
/>
) : null}
</div> </div>
); );
} }
@@ -309,17 +326,17 @@ export default class ComposerOptions extends React.PureComponent {
// Props. // Props.
ComposerOptions.propTypes = { ComposerOptions.propTypes = {
acceptContentTypes: PropTypes.string, acceptContentTypes: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
disabled: PropTypes.bool, disabled: PropTypes.bool,
doNotFederate: PropTypes.bool,
full: PropTypes.bool, full: PropTypes.bool,
hasMedia: PropTypes.bool, hasMedia: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onChangeAdvancedOption: PropTypes.func,
onChangeSensitivity: PropTypes.func, onChangeSensitivity: PropTypes.func,
onChangeVisibility: PropTypes.func, onChangeVisibility: PropTypes.func,
onDoodleOpen: PropTypes.func, onDoodleOpen: PropTypes.func,
onModalClose: PropTypes.func, onModalClose: PropTypes.func,
onModalOpen: PropTypes.func, onModalOpen: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onToggleSpoiler: PropTypes.func, onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func, onUpload: PropTypes.func,
privacy: PropTypes.string, privacy: PropTypes.string,

View File

@@ -85,6 +85,7 @@ export default function ComposerPublisher ({
unlisted: 'unlock-alt', unlisted: 'unlock-alt',
}[privacy]} }[privacy]}
/> />
{' '}
<FormattedMessage {...messages.publish} /> <FormattedMessage {...messages.publish} />
</span> </span>
); );

View File

@@ -1,12 +1,10 @@
// Package imports. // Package imports.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
// Components. // Components.
import Avatar from 'flavours/glitch/components/avatar'; import AccountContainer from 'flavours/glitch/containers/account_container';
import DisplayName from 'flavours/glitch/components/display_name';
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/components/icon_button';
// Utils. // Utils.
@@ -31,17 +29,6 @@ const handlers = {
onCancel(); onCancel();
} }
}, },
// Handles a click on the status's account.
handleClickAccount () {
const {
account,
history,
} = this.props;
if (history) {
history.push(`/accounts/${account.get('id')}`);
}
},
}; };
// The component. // The component.
@@ -55,10 +42,7 @@ export default class ComposerReply extends React.PureComponent {
// Rendering. // Rendering.
render () { render () {
const { const { handleClick } = this.handlers;
handleClick,
handleClickAccount,
} = this.handlers;
const { const {
account, account,
content, content,
@@ -76,21 +60,10 @@ export default class ComposerReply extends React.PureComponent {
title={intl.formatMessage(messages.cancel)} title={intl.formatMessage(messages.cancel)}
/> />
{account ? ( {account ? (
<a <AccountContainer
className='account' id={account}
href={account.get('url')} small
onClick={handleClickAccount} />
>
<Avatar
account={account}
className='avatar'
size={24}
/>
<DisplayName
account={account}
className='display_name'
/>
</a>
) : null} ) : null}
</header> </header>
<div <div
@@ -105,9 +78,8 @@ export default class ComposerReply extends React.PureComponent {
} }
ComposerReply.propTypes = { ComposerReply.propTypes = {
account: ImmutablePropTypes.map, account: PropTypes.string,
content: PropTypes.string, content: PropTypes.string,
history: PropTypes.object,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onCancel: PropTypes.func, onCancel: PropTypes.func,
}; };

View File

@@ -0,0 +1,60 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages } from 'react-intl';
// Components.
import Icon from 'flavours/glitch/components/icon';
// Messages.
const messages = defineMessages({
localOnly: {
defaultMessage: 'This post is local-only',
id: 'advanced_options.local-only.tooltip',
},
threadedMode: {
defaultMessage: 'Threaded mode enabled',
id: 'advanced_options.threaded_mode.tooltip',
},
});
// We use an array of tuples here instead of an object because it
// preserves order.
const iconMap = [
['do_not_federate', 'home', messages.localOnly],
['threaded_mode', 'comments', messages.threadedMode],
];
// The component.
export default function ComposerTextareaIcons ({
advancedOptions,
intl,
}) {
// The result. We just map every active option to its icon.
return (
<div className='composer--textarea--icons'>
{advancedOptions ? iconMap.map(
([key, icon, message]) => advancedOptions.get(key) ? (
<span
className='textarea_icon'
key={key}
title={intl.formatMessage(message)}
>
<Icon
fullwidth
icon={icon}
/>
</span>
) : null
) : null}
</div>
);
}
// Props.
ComposerTextareaIcons.propTypes = {
advancedOptions: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
};

View File

@@ -10,6 +10,7 @@ import Textarea from 'react-textarea-autosize';
// Components. // Components.
import EmojiPicker from 'flavours/glitch/features/emoji_picker'; import EmojiPicker from 'flavours/glitch/features/emoji_picker';
import ComposerTextareaIcons from './icons';
import ComposerTextareaSuggestions from './suggestions'; import ComposerTextareaSuggestions from './suggestions';
// Utils. // Utils.
@@ -32,7 +33,7 @@ const handlers = {
// When blurring the textarea, suggestions are hidden. // When blurring the textarea, suggestions are hidden.
handleBlur () { handleBlur () {
//this.setState({ suggestionsHidden: true }); this.setState({ suggestionsHidden: true });
}, },
// When the contents of the textarea change, we have to pull up new // When the contents of the textarea change, we have to pull up new
@@ -57,7 +58,7 @@ const handlers = {
const right = value.slice(selectionStart).search(/[\s\u200B]/); const right = value.slice(selectionStart).search(/[\s\u200B]/);
const token = function () { const token = function () {
switch (true) { switch (true) {
case left < 0 || /[@:]/.test(!value[left]): case left < 0 || !/[@:]/.test(value[left]):
return null; return null;
case right < 0: case right < 0:
return value.slice(left); return value.slice(left);
@@ -127,6 +128,11 @@ const handlers = {
return; return;
} }
// We submit the status on control/meta + enter.
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
onSubmit();
}
// Switches over the pressed key. // Switches over the pressed key.
switch(e.key) { switch(e.key) {
@@ -156,11 +162,6 @@ const handlers = {
} }
return; return;
} }
// We submit the status on control/meta + enter.
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
onSubmit();
}
}, },
// When the escape key is released, we either close the suggestions // When the escape key is released, we either close the suggestions
@@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
handleRefTextarea, handleRefTextarea,
} = this.handlers; } = this.handlers;
const { const {
advancedOptions,
autoFocus, autoFocus,
disabled, disabled,
intl, intl,
@@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
<div className='composer--textarea'> <div className='composer--textarea'>
<label> <label>
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span> <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
<ComposerTextareaIcons
advancedOptions={advancedOptions}
intl={intl}
/>
<Textarea <Textarea
aria-autocomplete='list' aria-autocomplete='list'
autoFocus={autoFocus} autoFocus={autoFocus}
@@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
// Props. // Props.
ComposerTextarea.propTypes = { ComposerTextarea.propTypes = {
advancedOptions: ImmutablePropTypes.map,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@@ -24,9 +24,16 @@ const handlers = {
} = this.props; } = this.props;
if (onClick) { if (onClick) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); // Prevents following account links
onClick(index); onClick(index);
} }
}, },
// This prevents the focus from changing, which would mess with
// our suggestion code.
handleMouseDown (e) {
e.preventDefault();
},
}; };
// The component. // The component.
@@ -40,7 +47,10 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
// Rendering. // Rendering.
render () { render () {
const { handleClick } = this.handlers; const {
handleMouseDown,
handleClick,
} = this.handlers;
const { const {
selected, selected,
suggestion, suggestion,
@@ -51,7 +61,8 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
return ( return (
<div <div
className={computedClass} className={computedClass}
onMouseDown={handleClick} onMouseDown={handleMouseDown}
onClickCapture={handleClick} // Jumps in front of contents
role='button' role='button'
tabIndex='0' tabIndex='0'
> >

View File

@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChange (key, checked) { onChange (path, checked) {
dispatch(changeSetting(['direct', ...key], checked)); dispatch(changeSetting(['direct', ...path], checked));
}, },
}); });

View File

@@ -45,10 +45,10 @@ const handlers = {
const { const {
onClear, onClear,
submitted, submitted,
value: { length }, value,
} = this.props; } = this.props;
e.preventDefault(); // Prevents focus change ?? e.preventDefault(); // Prevents focus change ??
if (onClear && (submitted || length)) { if (onClear && (submitted || value && value.length)) {
onClear(); onClear();
} }
}, },
@@ -100,7 +100,8 @@ export default class DrawerSearch extends React.PureComponent {
value, value,
} = this.props; } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const computedClass = classNames('drawer--search', { active: value.length || submitted }); const active = value && value.length || submitted;
const computedClass = classNames('drawer--search', { active });
return ( return (
<div className={computedClass}> <div className={computedClass}>
@@ -126,11 +127,11 @@ export default class DrawerSearch extends React.PureComponent {
tabIndex='0' tabIndex='0'
> >
<Icon icon='search' /> <Icon icon='search' />
<Icon icon='fa-times-circle' /> <Icon icon='times-circle' />
</div> </div>
<Overlay <Overlay
placement='bottom' placement='bottom'
show={expanded && !(value || '').length && !submitted} show={expanded && !active}
target={this} target={this}
><DrawerSearchPopout /></Overlay> ><DrawerSearchPopout /></Overlay>
</div> </div>

View File

@@ -42,56 +42,61 @@ export default function DrawerSearchPopout ({ style }) {
// The result. // The result.
return ( return (
<Motion <div
defaultStyle={{ className='drawer--search--popout'
opacity: 0,
scaleX: 0.85,
scaleY: 0.75,
}}
style={{ style={{
opacity: motionSpring, ...style,
scaleX: motionSpring, position: 'absolute',
scaleY: motionSpring, width: 285,
}} }}
> >
{({ opacity, scaleX, scaleY }) => ( <Motion
<div defaultStyle={{
className='drawer--search--popout' opacity: 0,
style={{ scaleX: 0.85,
...style, scaleY: 0.75,
position: 'absolute', }}
width: 285, style={{
opacity: opacity, opacity: motionSpring,
transform: `scale(${scaleX}, ${scaleY})`, scaleX: motionSpring,
}} scaleY: motionSpring,
> }}
<h4><FormattedMessage {...messages.format} /></h4> >
<ul> {({ opacity, scaleX, scaleY }) => (
<li> <div
<em>#example</em> style={{
{' '} opacity: opacity,
<FormattedMessage {...messages.hashtag} /> transform: `scale(${scaleX}, ${scaleY})`,
</li> }}
<li> >
<em>@username@domain</em> <h4><FormattedMessage {...messages.format} /></h4>
{' '} <ul>
<FormattedMessage {...messages.user} /> <li>
</li> <em>#example</em>
<li> {' '}
<em>URL</em> <FormattedMessage {...messages.hashtag} />
{' '} </li>
<FormattedMessage {...messages.user} /> <li>
</li> <em>@username@domain</em>
<li> {' '}
<em>URL</em> <FormattedMessage {...messages.user} />
{' '} </li>
<FormattedMessage {...messages.status} /> <li>
</li> <em>URL</em>
</ul> {' '}
<FormattedMessage {...messages.text} /> <FormattedMessage {...messages.user} />
</div> </li>
)} <li>
</Motion> <em>URL</em>
{' '}
<FormattedMessage {...messages.status} />
</li>
</ul>
<FormattedMessage {...messages.text} />
</div>
)}
</Motion>
</div>
); );
} }

View File

@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
import StatusList from 'flavours/glitch/components/status_list'; import StatusList from 'flavours/glitch/components/status_list';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
@@ -16,6 +17,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']), statusIds: state.getIn(['status_lists', 'favourites', 'items']),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
}); });
@@ -30,6 +32,7 @@ export default class Favourites extends ImmutablePureComponent {
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
}; };
componentWillMount () { componentWillMount () {
@@ -59,12 +62,12 @@ export default class Favourites extends ImmutablePureComponent {
this.column = c; this.column = c;
} }
handleScrollToBottom = () => { handleScrollToBottom = debounce(() => {
this.props.dispatch(expandFavouritedStatuses()); this.props.dispatch(expandFavouritedStatuses());
} }, 300, { leading: true })
render () { render () {
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props; const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@@ -85,6 +88,7 @@ export default class Favourites extends ImmutablePureComponent {
statusIds={statusIds} statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`} scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading}
onScrollToBottom={this.handleScrollToBottom} onScrollToBottom={this.handleScrollToBottom}
/> />
</Column> </Column>

View File

@@ -79,7 +79,7 @@ export default class GettingStarted extends ImmutablePureComponent {
render () { render () {
const { intl, myAccount, columns, multiColumn, lists } = this.props; const { intl, myAccount, columns, multiColumn, lists } = this.props;
let navItems = []; const navItems = [];
let listItems = []; let listItems = [];
if (multiColumn) { if (multiColumn) {
@@ -111,10 +111,10 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />); navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
listItems = listItems.concat([ listItems = listItems.concat([
<div> <div key='7'>
<ColumnLink key='7' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> <ColumnLink key='8' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
{lists.map(list => {lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> <ColumnLink key={(8 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
)} )}
</div>, </div>,
]); ]);

View File

@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChange (key, checked) { onChange (path, checked) {
dispatch(changeSetting(['home', ...key], checked)); dispatch(changeSetting(['home', ...path], checked));
}, },
onSave () { onSave () {

View File

@@ -11,12 +11,11 @@ export default class ColumnSettings extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
pushSettings: ImmutablePropTypes.map.isRequired, pushSettings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired,
}; };
onPushChange = (key, checked) => { onPushChange = (path, checked) => {
this.props.onChange(['push', ...key], checked); this.props.onChange(['push', ...path], checked);
} }
render () { render () {

View File

@@ -1,9 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ColumnSettings from '../components/column_settings'; import ColumnSettings from '../components/column_settings';
import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings'; import { changeSetting } from 'flavours/glitch/actions/settings';
import { clearNotifications } from 'flavours/glitch/actions/notifications'; import { clearNotifications } from 'flavours/glitch/actions/notifications';
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications'; import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
const messages = defineMessages({ const messages = defineMessages({
@@ -18,19 +18,14 @@ const mapStateToProps = state => ({
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onChange (key, checked) { onChange (path, checked) {
if (key[0] === 'push') { if (path[0] === 'push') {
dispatch(changePushNotifications(key.slice(1), checked)); dispatch(changePushNotifications(path.slice(1), checked));
} else { } else {
dispatch(changeSetting(['notifications', ...key], checked)); dispatch(changeSetting(['notifications', ...path], checked));
} }
}, },
onSave () {
dispatch(saveSettings());
dispatch(savePushNotificationSettings());
},
onClear () { onClear () {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.clearMessage), message: intl.formatMessage(messages.clearMessage),

View File

@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChange (key, checked) { onChange (path, checked) {
dispatch(changeSetting(['public', ...key], checked)); dispatch(changeSetting(['public', ...path], checked));
}, },
}); });

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import axios from 'axios'; import api from 'flavours/glitch/util/api';
@injectIntl @injectIntl
export default class EmbedModal extends ImmutablePureComponent { export default class EmbedModal extends ImmutablePureComponent {
@@ -23,7 +23,7 @@ export default class EmbedModal extends ImmutablePureComponent {
this.setState({ loading: true }); this.setState({ loading: true });
axios.post('/api/web/embed', { url }).then(res => { api().post('/api/web/embed', { url }).then(res => {
this.setState({ loading: false, oembed: res.data }); this.setState({ loading: false, oembed: res.data });
const iframeDocument = this.iframe.contentWindow.document; const iframeDocument = this.iframe.contentWindow.document;

View File

@@ -34,6 +34,8 @@ const messages = {
'status.collapse': 'Collapse', 'status.collapse': 'Collapse',
'status.uncollapse': 'Uncollapse', 'status.uncollapse': 'Uncollapse',
'media_gallery.sensitive': 'Sensitive',
'favourite_modal.combo': 'You can press {combo} to skip this next time', 'favourite_modal.combo': 'You can press {combo} to skip this next time',
'home.column_settings.show_direct': 'Show DMs', 'home.column_settings.show_direct': 'Show DMs',
@@ -52,9 +54,13 @@ const messages = {
'compose.attach.doodle': 'Draw something', 'compose.attach.doodle': 'Draw something',
'compose.attach': 'Attach...', 'compose.attach': 'Attach...',
'advanced-options.local-only.short': 'Local-only', 'advanced_options.local-only.short': 'Local-only',
'advanced-options.local-only.long': 'Do not post to other instances', 'advanced_options.local-only.long': 'Do not post to other instances',
'advanced_options.local-only.tooltip': 'This post is local-only',
'advanced_options.icon_title': 'Advanced options', 'advanced_options.icon_title': 'Advanced options',
'advanced_options.threaded_mode.short': 'Threaded mode',
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
}; };
export default Object.assign({}, inherited, messages); export default Object.assign({}, inherited, messages);

View File

@@ -55,9 +55,13 @@ const messages = {
'compose.attach.doodle': '落書きをする', 'compose.attach.doodle': '落書きをする',
'compose.attach': 'アタッチ...', 'compose.attach': 'アタッチ...',
'advanced-options.local-only.short': 'ローカル限定', 'advanced_options.local-only.short': 'ローカル限定',
'advanced-options.local-only.long': '他のインスタンスには投稿されません', 'advanced_options.local-only.long': '他のインスタンスには投稿されません',
'advanced_options.local-only.tooltip': 'この投稿はローカル限定投稿です',
'advanced_options.icon_title': '高度な設定', 'advanced_options.icon_title': '高度な設定',
'advanced_options.threaded_mode.short': 'スレッドモード',
'advanced_options.threaded_mode.long': '投稿時に自動的に返信するように設定します',
'advanced_options.threaded_mode.tooltip': 'スレッドモードを有効にする',
}; };
export default Object.assign({}, inherited, messages); export default Object.assign({}, inherited, messages);

View File

@@ -28,12 +28,18 @@ const messages = {
'settings.media': 'Zawartość multimedialna', 'settings.media': 'Zawartość multimedialna',
'settings.media_letterbox': 'Letterbox media', 'settings.media_letterbox': 'Letterbox media',
'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości', 'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości',
'settings.preferences': 'Preferencje użyytkownika', 'settings.preferences': 'Preferencje użytkownika',
'settings.wide_view': 'Szeroki widok (tylko w trybie desktopowym)', 'settings.wide_view': 'Szeroki widok (tylko w trybie desktopowym)',
'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)', 'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
'status.collapse': 'Zwiń', 'status.collapse': 'Zwiń',
'status.uncollapse': 'Rozwiń', 'status.uncollapse': 'Rozwiń',
'media_gallery.sensitive': 'Zawartość wrażliwa',
'favourite_modal.combo': 'Możesz nacisnąć {combo}, aby pominąć to następnym razem',
'home.column_settings.show_direct': 'Pokaż wiadomości bezpośrednie',
'notification.markForDeletion': 'Oznacz do usunięcia', 'notification.markForDeletion': 'Oznacz do usunięcia',
'notifications.clear': 'Wyczyść wszystkie powiadomienia', 'notifications.clear': 'Wyczyść wszystkie powiadomienia',
'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?', 'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
@@ -43,6 +49,18 @@ const messages = {
'notification_purge.btn_none': 'Odznacz\nwszystkie', 'notification_purge.btn_none': 'Odznacz\nwszystkie',
'notification_purge.btn_invert': 'Odwróć\nzaznaczenie', 'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
'notification_purge.btn_apply': 'Usuń\nzaznaczone', 'notification_purge.btn_apply': 'Usuń\nzaznaczone',
'compose.attach.upload': 'Wyślij plik',
'compose.attach.doodle': 'Narysuj coś',
'compose.attach': 'Załącz coś',
'advanced_options.local-only.short': 'Tylko lokalnie',
'advanced_options.local-only.long': 'Nie wysyłaj na inne instancje',
'advanced_options.local-only.tooltip': 'Ten wpis jest widoczny tylko lokalnie',
'advanced_options.icon_title': 'Ustawienia zaawansowane',
'advanced_options.threaded_mode.short': 'Tryb wątków',
'advanced_options.threaded_mode.long': 'Przechodzi do tworzenia odpowiedzi po publikacji wpisu',
'advanced_options.threaded_mode.tooltip': 'Włączono tryb wątków',
}; };
export default Object.assign({}, inherited, messages); export default Object.assign({}, inherited, messages);

View File

@@ -6,3 +6,10 @@ en:
skins: skins:
glitch: glitch:
default: Default default: Default
pl:
flavours:
glitch:
description: Domyślny motyw instancji GlitchSoc.
skins:
glitch:
default: Domyślny

View File

@@ -33,11 +33,13 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from 'flavours/glitch/util/uuid'; import uuid from 'flavours/glitch/util/uuid';
import { me } from 'flavours/glitch/util/initial_state'; import { me } from 'flavours/glitch/util/initial_state';
import { overwrite } from 'flavours/glitch/util/js_helpers';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
mounted: false, mounted: false,
advanced_options: ImmutableMap({ advanced_options: ImmutableMap({
do_not_federate: false, do_not_federate: false,
threaded_mode: false,
}), }),
sensitive: false, sensitive: false,
spoiler: false, spoiler: false,
@@ -55,6 +57,7 @@ const initialState = ImmutableMap({
suggestions: ImmutableList(), suggestions: ImmutableList(),
default_advanced_options: ImmutableMap({ default_advanced_options: ImmutableMap({
do_not_federate: false, do_not_federate: false,
threaded_mode: null, // Do not reset
}), }),
default_privacy: 'public', default_privacy: 'public',
default_sensitive: false, default_sensitive: false,
@@ -83,6 +86,20 @@ function statusToTextMentions(state, status) {
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
}; };
function apiStatusToTextMentions (state, status) {
let set = ImmutableOrderedSet([]);
if (status.account.id !== me) {
set = set.add(`@${status.account.acct} `);
}
return set.union(status.mentions.filter(
mention => mention.id !== me
).map(
mention => `@${mention.acct} `
)).join('');
}
function clearAll(state) { function clearAll(state) {
return state.withMutations(map => { return state.withMutations(map => {
map.set('text', ''); map.set('text', '');
@@ -90,7 +107,10 @@ function clearAll(state) {
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('is_submitting', false); map.set('is_submitting', false);
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('advanced_options', state.get('default_advanced_options')); map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
);
map.set('privacy', state.get('default_privacy')); map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false); map.set('sensitive', false);
map.update('media_attachments', list => list.clear()); map.update('media_attachments', list => list.clear());
@@ -98,6 +118,31 @@ function clearAll(state) {
}); });
}; };
function continueThread (state, status) {
return state.withMutations(function (map) {
map.set('text', apiStatusToTextMentions(state, status));
if (status.spoiler_text) {
map.set('spoiler', true);
map.set('spoiler_text', status.spoiler_text);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
map.set('is_submitting', false);
map.set('in_reply_to', status.id);
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
);
map.set('privacy', status.visibility);
map.set('sensitive', false);
map.update('media_attachments', list => list.clear());
map.set('idempotencyKey', uuid());
map.set('focusDate', new Date());
map.set('preselectDate', new Date());
});
}
function appendMedia(state, media) { function appendMedia(state, media) {
const prevSize = state.get('media_attachments').size; const prevSize = state.get('media_attachments').size;
@@ -182,8 +227,7 @@ export default function compose(state = initialState, action) {
return state.set('mounted', false); return state.set('mounted', false);
case COMPOSE_ADVANCED_OPTIONS_CHANGE: case COMPOSE_ADVANCED_OPTIONS_CHANGE:
return state return state
.set('advanced_options', .set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
.set('idempotencyKey', uuid()); .set('idempotencyKey', uuid());
case COMPOSE_SENSITIVITY_CHANGE: case COMPOSE_SENSITIVITY_CHANGE:
return state.withMutations(map => { return state.withMutations(map => {
@@ -220,9 +264,10 @@ export default function compose(state = initialState, action) {
map.set('in_reply_to', action.status.get('id')); map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status)); map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('advanced_options', new ImmutableMap({ map.update(
do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')), 'advanced_options',
})); map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
);
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('preselectDate', new Date()); map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
@@ -243,14 +288,17 @@ export default function compose(state = initialState, action) {
map.set('spoiler', false); map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('privacy', state.get('default_privacy')); map.set('privacy', state.get('default_privacy'));
map.set('advanced_options', state.get('default_advanced_options')); map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
}); });
case COMPOSE_SUBMIT_REQUEST: case COMPOSE_SUBMIT_REQUEST:
case COMPOSE_UPLOAD_CHANGE_REQUEST: case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_submitting', true); return state.set('is_submitting', true);
case COMPOSE_SUBMIT_SUCCESS: case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state); return action.status && state.getIn(['advanced_options', 'threaded_mode']) ? continueThread(state, action.status) : clearAll(state);
case COMPOSE_SUBMIT_FAIL: case COMPOSE_SUBMIT_FAIL:
case COMPOSE_UPLOAD_CHANGE_FAIL: case COMPOSE_UPLOAD_CHANGE_FAIL:
return state.set('is_submitting', false); return state.set('is_submitting', false);

View File

@@ -1,5 +1,5 @@
import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications'; import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from 'flavours/glitch/actions/push_notifications';
import Immutable from 'immutable'; import Immutable from 'immutable';
const initialState = Immutable.Map({ const initialState = Immutable.Map({
@@ -43,8 +43,8 @@ export default function push_subscriptions(state = initialState, action) {
return state.set('browserSupport', action.value); return state.set('browserSupport', action.value);
case CLEAR_SUBSCRIPTION: case CLEAR_SUBSCRIPTION:
return initialState; return initialState;
case ALERTS_CHANGE: case SET_ALERTS:
return state.setIn(action.key, action.value); return state.setIn(action.path, action.value);
default: default:
return state; return state;
} }

View File

@@ -101,7 +101,7 @@ export default function settings(state = initialState, action) {
return hydrate(state, action.state.get('settings')); return hydrate(state, action.state.get('settings'));
case SETTING_CHANGE: case SETTING_CHANGE:
return state return state
.setIn(action.key, action.value) .setIn(action.path, action.value)
.set('saved', false); .set('saved', false);
case COLUMN_ADD: case COLUMN_ADD:
return state return state

View File

@@ -1,6 +1,10 @@
import { import {
FAVOURITED_STATUSES_FETCH_REQUEST,
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_FETCH_FAIL,
FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL,
} from 'flavours/glitch/actions/favourites'; } from 'flavours/glitch/actions/favourites';
import { import {
PINNED_STATUSES_FETCH_SUCCESS, PINNED_STATUSES_FETCH_SUCCESS,
@@ -30,6 +34,7 @@ const normalizeList = (state, listType, statuses, next) => {
return state.update(listType, listMap => listMap.withMutations(map => { return state.update(listType, listMap => listMap.withMutations(map => {
map.set('next', next); map.set('next', next);
map.set('loaded', true); map.set('loaded', true);
map.set('isLoading', false);
map.set('items', ImmutableList(statuses.map(item => item.id))); map.set('items', ImmutableList(statuses.map(item => item.id)));
})); }));
}; };
@@ -37,6 +42,7 @@ const normalizeList = (state, listType, statuses, next) => {
const appendToList = (state, listType, statuses, next) => { const appendToList = (state, listType, statuses, next) => {
return state.update(listType, listMap => listMap.withMutations(map => { return state.update(listType, listMap => listMap.withMutations(map => {
map.set('next', next); map.set('next', next);
map.set('isLoading', false);
map.set('items', map.get('items').concat(statuses.map(item => item.id))); map.set('items', map.get('items').concat(statuses.map(item => item.id)));
})); }));
}; };
@@ -55,6 +61,12 @@ const removeOneFromList = (state, listType, status) => {
export default function statusLists(state = initialState, action) { export default function statusLists(state = initialState, action) {
switch(action.type) { switch(action.type) {
case FAVOURITED_STATUSES_FETCH_REQUEST:
case FAVOURITED_STATUSES_EXPAND_REQUEST:
return state.setIn(['favourites', 'isLoading'], true);
case FAVOURITED_STATUSES_FETCH_FAIL:
case FAVOURITED_STATUSES_EXPAND_FAIL:
return state.setIn(['favourites', 'isLoading'], false);
case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'favourites', action.statuses, action.next); return normalizeList(state, 'favourites', action.statuses, action.next);
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:

View File

@@ -396,10 +396,12 @@
} }
} }
&__content {
max-width: calc(100% - 90px);
}
&__title { &__title {
overflow: hidden; word-wrap: break-word;
text-overflow: ellipsis;
white-space: nowrap;
} }
&__timestamp { &__timestamp {
@@ -413,7 +415,7 @@
color: $ui-primary-color; color: $ui-primary-color;
font-family: 'mastodon-font-monospace', monospace; font-family: 'mastodon-font-monospace', monospace;
font-size: 12px; font-size: 12px;
white-space: nowrap; word-wrap: break-word;
min-height: 20px; min-height: 20px;
} }

View File

@@ -52,22 +52,7 @@
margin-bottom: 5px; margin-bottom: 5px;
overflow: hidden; overflow: hidden;
& > .account { & > .account.small { color: $ui-base-color }
& > .avatar {
float: left;
margin-right: 5px;
}
& > .display_name {
color: $ui-base-color;
display: block;
padding-right: 25px;
max-width: 100%;
line-height: 24px;
text-decoration: none;
overflow: hidden;
}
}
& > .cancel { & > .cancel {
float: right; float: right;
@@ -87,6 +72,27 @@
overflow: visible; overflow: visible;
white-space: pre-wrap; white-space: pre-wrap;
padding-top: 5px; padding-top: 5px;
p {
margin-bottom: 20px;
&:last-child { margin-bottom: 0 }
}
a {
color: lighten($ui-base-color, 20%);
text-decoration: none;
&:hover { text-decoration: underline }
&.mention {
&:hover {
text-decoration: none;
span { text-decoration: underline }
}
}
}
} }
.emojione { .emojione {
@@ -94,27 +100,6 @@
height: 20px; height: 20px;
margin: -5px 0 0; margin: -5px 0 0;
} }
p {
margin-bottom: 20px;
&:last-child { margin-bottom: 0 }
}
a {
color: lighten($ui-base-color, 20%);
text-decoration: none;
&:hover { text-decoration: underline }
&.mention {
&:hover {
text-decoration: none;
span { text-decoration: underline }
}
}
}
} }
.composer--textarea { .composer--textarea {
@@ -149,6 +134,27 @@
} }
} }
.composer--textarea--icons {
display: block;
position: absolute;
top: 29px;
right: 5px;
bottom: 5px;
overflow: hidden;
& > .textarea_icon {
display: block;
margin: 2px 0 0 2px;
width: 24px;
height: 24px;
color: darken($ui-primary-color, 24%);
font-size: 18px;
line-height: 24px;
text-align: center;
opacity: .8;
}
}
.composer--textarea--suggestions { .composer--textarea--suggestions {
display: block; display: block;
position: absolute; position: absolute;
@@ -175,6 +181,7 @@
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;
overflow: hidden;
cursor: pointer; cursor: pointer;
&:hover, &:hover,
@@ -191,6 +198,12 @@
height: 18px; height: 18px;
} }
} }
& > .account.small {
.display-name {
& > span { color: lighten($ui-base-color, 36%) }
}
}
} }
.composer--upload_form { .composer--upload_form {

View File

@@ -114,19 +114,27 @@
} }
& > .icon { & > .icon {
display: block;
position: absolute;
top: 10px;
right: 10px;
width: 18px;
height: 18px;
color: $ui-secondary-color;
font-size: 18px;
line-height: 18px;
z-index: 2;
.fa { .fa {
display: inline-block; display: inline-block;
position: absolute; position: absolute;
top: 10px; top: 0;
right: 10px; bottom: 0;
width: 18px; left: 0;
height: 18px; right: 0;
color: $ui-secondary-color;
font-size: 18px;
opacity: 0; opacity: 0;
cursor: default; cursor: default;
pointer-events: none; pointer-events: none;
z-index: 2;
transition: all 100ms linear; transition: all 100ms linear;
} }
@@ -136,14 +144,15 @@
} }
.fa-times-circle { .fa-times-circle {
top: 11px;
transform: rotate(-90deg); transform: rotate(-90deg);
cursor: pointer; cursor: pointer;
&:hover { color: $primary-text-color } &:hover { color: $primary-text-color }
} }
}
&.active { &.active {
& > .icon {
.fa-search { .fa-search {
opacity: 0; opacity: 0;
transform: rotate(90deg); transform: rotate(90deg);
@@ -158,6 +167,32 @@
} }
} }
.drawer--search--popout {
box-sizing: border-box;
margin-top: 10px;
border-radius: 4px;
padding: 10px 14px 14px 14px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
color: $ui-primary-color;
background: $simple-background-color;
h4 {
margin-bottom: 10px;
color: $ui-primary-color;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
}
ul { margin-bottom: 10px }
li { padding: 4px 0 }
em {
color: $ui-base-color;
font-weight: 500;
}
}
.drawer--account { .drawer--account {
padding: 10px; padding: 10px;
color: $ui-primary-color; color: $ui-primary-color;

View File

@@ -745,6 +745,8 @@
.account { .account {
padding: 10px; padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%); border-bottom: 1px solid lighten($ui-base-color, 8%);
color: inherit;
text-decoration: none;
.account__display-name { .account__display-name {
flex: 1 1 auto; flex: 1 1 auto;
@@ -762,27 +764,8 @@
& > .account__avatar-wrapper { margin: 0 8px 0 0 } & > .account__avatar-wrapper { margin: 0 8px 0 0 }
& > .display-name { & > .display-name {
display: block; height: 24px;
padding: 0; line-height: 24px;
height: auto;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
& > strong {
display: inline;
font-size: inherit;
line-height: inherit;
}
& > span {
display: inline;
color: lighten($ui-base-color, 36%);
font-size: inherit;
line-height: inherit;
&::before { content: " " }
}
} }
} }
} }
@@ -1243,6 +1226,30 @@
text-decoration: underline; text-decoration: underline;
} }
} }
&.inline {
padding: 0;
height: 18px;
font-size: 15px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
strong {
display: inline;
height: auto;
font-size: inherit;
line-height: inherit;
}
span {
display: inline;
height: auto;
font-size: inherit;
line-height: inherit;
}
}
} }
.status__relative-time, .status__relative-time,
@@ -1561,6 +1568,39 @@
} }
} }
.drawer__pager {
box-sizing: border-box;
padding: 0;
flex-grow: 1;
position: relative;
overflow: hidden;
display: flex;
}
.drawer__inner {
position: absolute;
top: 0;
left: 0;
background: lighten($ui-base-color, 13%) url('~images/wave-drawer.png') no-repeat bottom / 100% auto;
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
width: 100%;
height: 100%;
&.darker {
background: $ui-base-color;
}
> .mastodon {
background: url('~images/mastodon-ui.png') no-repeat left bottom / contain;
flex: 1;
}
}
.pseudo-drawer { .pseudo-drawer {
background: lighten($ui-base-color, 13%); background: lighten($ui-base-color, 13%);
font-size: 13px; font-size: 13px;
@@ -1841,6 +1881,11 @@
cursor: default; cursor: default;
} }
.getting-started__wrapper,
.getting_started {
background: $ui-base-color;
}
.getting-started__wrapper { .getting-started__wrapper {
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
@@ -2447,17 +2492,29 @@
font-weight: 500; font-weight: 500;
} }
.spoiler-button { .sensitive-info {
display: none; display: flex;
left: 4px; flex-direction: row;
align-items: center;
position: absolute; position: absolute;
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
top: 4px; top: 4px;
left: 4px;
z-index: 100; z-index: 100;
}
&.spoiler-button--visible { .sensitive-marker {
display: block; margin: 0 3px;
} border-radius: 2px;
padding: 2px 6px;
color: rgba($primary-text-color, 0.8);
background: rgba($base-overlay-background, 0.5);
font-size: 12px;
line-height: 15px;
text-transform: uppercase;
opacity: .9;
transition: opacity .1s ease;
.media-gallery:hover & { opacity: 1 }
} }
.modal-container--preloader { .modal-container--preloader {
@@ -2774,6 +2831,112 @@
filter: none; filter: none;
} }
.search {
position: relative;
}
.search__input {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
padding-right: 30px;
font-family: inherit;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {
.fa {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
font-size: 18px;
width: 18px;
height: 18px;
color: $ui-secondary-color;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.fa-search {
transform: rotate(90deg);
&.active {
pointer-events: none;
transform: rotate(0deg);
}
}
.fa-times-circle {
top: 11px;
transform: rotate(0deg);
cursor: pointer;
&.active {
transform: rotate(90deg);
}
&:hover {
color: $primary-text-color;
}
}
}
.search-results__header {
color: $ui-base-lighter-color;
background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 4%);
padding: 15px 10px;
font-size: 14px;
font-weight: 500;
}
.search-results__hashtag {
display: block;
padding: 10px;
color: $ui-secondary-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: lighten($ui-secondary-color, 4%);
text-decoration: underline;
}
}
.modal-root { .modal-root {
transition: opacity 0.3s linear; transition: opacity 0.3s linear;
will-change: opacity; will-change: opacity;
@@ -3911,37 +4074,6 @@
border-radius: 0; border-radius: 0;
} }
.search-popout {
background: $simple-background-color;
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: $ui-primary-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
color: $ui-primary-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: $ui-base-color;
}
}
noscript { noscript {
text-align: center; text-align: center;

View File

@@ -1,7 +1,7 @@
# (REQUIRED) The location of the pack files. # (REQUIRED) The location of the pack files.
pack: pack:
about: packs/about.js about: packs/about.js
admin: admin: packs/public.js
auth: auth:
common: common:
filename: packs/common.js filename: packs/common.js

View File

@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import ready from './ready';
import LinkHeader from './link_header'; import LinkHeader from './link_header';
export const getLinks = response => { export const getLinks = response => {
@@ -11,10 +12,17 @@ export const getLinks = response => {
return LinkHeader.parse(value); return LinkHeader.parse(value);
}; };
let csrfHeader = {};
function setCSRFHeader() {
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
csrfHeader['X-CSRF-Token'] = csrfToken;
}
ready(setCSRFHeader);
export default getState => axios.create({ export default getState => axios.create({
headers: { headers: Object.assign(csrfHeader, getState ? {
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, 'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
}, } : {}),
transformResponse: [function (data) { transformResponse: [function (data) {
try { try {

View File

@@ -27,15 +27,15 @@ export function HashtagTimeline () {
} }
export function ListTimeline () { export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'flavours/glitch/features/list_timeline'); return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'flavours/glitch/features/list_timeline');
} }
export function Lists () { export function Lists () {
return import(/* webpackChunkName: "features/lists" */'flavours/glitch/features/lists'); return import(/* webpackChunkName: "flavours/glitch/async/lists" */'flavours/glitch/features/lists');
} }
export function ListEditor () { export function ListEditor () {
return import(/* webpackChunkName: "features/list_editor" */'flavours/glitch/features/list_editor'); return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
} }
export function DirectTimeline() { export function DirectTimeline() {
@@ -51,7 +51,7 @@ export function GettingStarted () {
} }
export function KeyboardShortcuts () { export function KeyboardShortcuts () {
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts'); return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
} }
export function PinnedStatuses () { export function PinnedStatuses () {

View File

@@ -18,6 +18,6 @@ export const boostModal = getMeta('boost_modal');
export const favouriteModal = getMeta('favourite_modal'); export const favouriteModal = getMeta('favourite_modal');
export const deleteModal = getMeta('delete_modal'); export const deleteModal = getMeta('delete_modal');
export const me = getMeta('me'); export const me = getMeta('me');
export const maxChars = getMeta('max_toot_chars') || 500; export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export default initialState; export default initialState;

View File

@@ -0,0 +1,5 @@
// This function returns the new value unless it is `null` or
// `undefined`, in which case it returns the old one.
export function overwrite (oldVal, newVal) {
return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
}

View File

@@ -1,5 +1,5 @@
import * as WebPushSubscription from './web_push_subscription'; import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
import Mastodon from 'flavours/glitch/containers/mastodon'; import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import ready from './ready'; import ready from './ready';
@@ -25,7 +25,7 @@ function main() {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
// avoid offline in dev mode because it's harder to debug // avoid offline in dev mode because it's harder to debug
require('offline-plugin/runtime').install(); require('offline-plugin/runtime').install();
WebPushSubscription.register(); store.dispatch(registerPushNotifications.register());
} }
perf.stop('main()'); perf.stop('main()');

View File

@@ -0,0 +1,46 @@
export default class Settings {
constructor(keyBase = null) {
this.keyBase = keyBase;
}
generateKey(id) {
return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
}
set(id, data) {
const key = this.generateKey(id);
try {
const encodedData = JSON.stringify(data);
localStorage.setItem(key, encodedData);
return data;
} catch (e) {
return null;
}
}
get(id) {
const key = this.generateKey(id);
try {
const rawData = localStorage.getItem(key);
return JSON.parse(rawData);
} catch (e) {
return null;
}
}
remove(id) {
const data = this.get(id);
if (data) {
const key = this.generateKey(id);
try {
localStorage.removeItem(key);
} catch (e) {
}
}
return data;
}
}
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');

View File

@@ -1,105 +0,0 @@
import axios from 'axios';
import { store } from 'flavours/glitch/containers/mastodon';
import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
const subscribe = (registration) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
});
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription) =>
axios.post('/api/web/push_subscriptions', {
subscription,
}).then(response => response.data);
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
export function register () {
store.dispatch(setBrowserSupport(supportsPushNotifications));
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
return;
}
getRegistration()
.then(getPushSubscription)
.then(({ registration, subscription }) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']);
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return subscription;
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend);
}
}
// No subscription, try to subscribe
return subscribe(registration).then(sendSubscriptionToBackend);
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
store.dispatch(setSubscription(subscription));
}
})
.catch(error => {
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
}
// Clear alerts and hide UI settings
store.dispatch(clearSubscription());
try {
getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
} catch (e) {
}
});
} else {
console.warn('Your browser does not support Web Push Notifications.');
}
}

View File

@@ -6,3 +6,11 @@ en:
skins: skins:
vanilla: vanilla:
default: Default default: Default
pl:
flavours:
vanilla:
description: Motyw używany przez instancje czystego Mastodona. Może nie obsługiwać wszystkich funkcji GlitchSoc.
name: Mastodon Vanilla
skins:
vanilla:
default: Domyślny

View File

@@ -1,7 +1,7 @@
# (REQUIRED) The location of the pack files inside `pack_directory`. # (REQUIRED) The location of the pack files inside `pack_directory`.
pack: pack:
about: about.js about: about.js
admin: admin: public.js
auth: auth:
common: common:
filename: common.js filename: common.js

View File

@@ -1,4 +1,4 @@
import axios from 'axios'; import api from '../../api';
import { pushNotificationsSetting } from '../../settings'; import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
@@ -35,7 +35,7 @@ const subscribe = (registration) =>
const unsubscribe = ({ registration, subscription }) => const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration; subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription, me) => { const sendSubscriptionToBackend = (getState, subscription, me) => {
const params = { subscription }; const params = { subscription };
if (me) { if (me) {
@@ -45,7 +45,7 @@ const sendSubscriptionToBackend = (subscription, me) => {
} }
} }
return axios.post('/api/web/push_subscriptions', params).then(response => response.data); return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
}; };
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
@@ -85,13 +85,13 @@ export function register () {
} else { } else {
// Something went wrong, try to subscribe again // Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then( return unsubscribe({ registration, subscription }).then(subscribe).then(
subscription => sendSubscriptionToBackend(subscription, me)); subscription => sendSubscriptionToBackend(getState, subscription, me));
} }
} }
// No subscription, try to subscribe // No subscription, try to subscribe
return subscribe(registration).then( return subscribe(registration).then(
subscription => sendSubscriptionToBackend(subscription, me)); subscription => sendSubscriptionToBackend(getState, subscription, me));
}) })
.then(subscription => { .then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend) // If we got a PushSubscription (and not a subscription object from the backend)
@@ -137,7 +137,7 @@ export function saveSettings() {
const alerts = state.get('alerts'); const alerts = state.get('alerts');
const data = { alerts }; const data = { alerts };
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data, data,
}).then(() => { }).then(() => {
const me = getState().getIn(['meta', 'me']); const me = getState().getIn(['meta', 'me']);

View File

@@ -1,4 +1,4 @@
import axios from 'axios'; import api from '../api';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_CHANGE = 'SETTING_CHANGE';
@@ -23,7 +23,7 @@ const debouncedSave = debounce((dispatch, getState) => {
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS(); const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
}, 5000, { trailing: true }); }, 5000, { trailing: true });
export function saveSettings() { export function saveSettings() {

View File

@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import ready from './ready';
import LinkHeader from './link_header'; import LinkHeader from './link_header';
export const getLinks = response => { export const getLinks = response => {
@@ -11,10 +12,17 @@ export const getLinks = response => {
return LinkHeader.parse(value); return LinkHeader.parse(value);
}; };
let csrfHeader = {};
function setCSRFHeader() {
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
csrfHeader['X-CSRF-Token'] = csrfToken;
}
ready(setCSRFHeader);
export default getState => axios.create({ export default getState => axios.create({
headers: { headers: Object.assign(csrfHeader, getState ? {
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, 'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
}, } : {}),
transformResponse: [function (data) { transformResponse: [function (data) {
try { try {

View File

@@ -23,7 +23,6 @@ export default class ColumnHeader extends React.PureComponent {
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
active: PropTypes.bool, active: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
focusable: PropTypes.bool,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
children: PropTypes.node, children: PropTypes.node,
pinned: PropTypes.bool, pinned: PropTypes.bool,
@@ -32,10 +31,6 @@ export default class ColumnHeader extends React.PureComponent {
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
static defaultProps = {
focusable: true,
}
state = { state = {
collapsed: true, collapsed: true,
animating: false, animating: false,
@@ -68,7 +63,7 @@ export default class ColumnHeader extends React.PureComponent {
} }
render () { render () {
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props; const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
const { collapsed, animating } = this.state; const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', {
@@ -135,11 +130,13 @@ export default class ColumnHeader extends React.PureComponent {
return ( return (
<div className={wrapperClassName}> <div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> <h1 className={buttonClassName}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <button onClick={this.handleTitleClick}>
<span className='column-header__title'> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title} <span className='column-header__title'>
</span> {title}
</span>
</button>
<div className='column-header__buttons'> <div className='column-header__buttons'>
{backButton} {backButton}

View File

@@ -94,7 +94,7 @@ export default class Compose extends React.PureComponent {
<div className='drawer__inner' onFocus={this.onFocus}> <div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} /> <NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer /> <ComposeFormContainer />
<div className='mastodon' /> {multiColumn && <div className='mastodon' />}
</div> </div>
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>

View File

@@ -70,30 +70,28 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems.push( navItems.push(
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, <ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
<ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
); );
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
} }
navItems.push(
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
);
if (multiColumn) { if (multiColumn) {
navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />); navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
} }
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
return ( return (
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
<div className='getting-started__wrapper'> <div className='getting-started__wrapper'>
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
{navItems} {navItems}
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> <ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div> </div>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import axios from 'axios'; import api from '../../../api';
@injectIntl @injectIntl
export default class EmbedModal extends ImmutablePureComponent { export default class EmbedModal extends ImmutablePureComponent {
@@ -23,7 +23,7 @@ export default class EmbedModal extends ImmutablePureComponent {
this.setState({ loading: true }); this.setState({ loading: true });
axios.post('/api/web/embed', { url }).then(res => { api().post('/api/web/embed', { url }).then(res => {
this.setState({ loading: false, oembed: res.data }); this.setState({ loading: false, oembed: res.data });
const iframeDocument = this.iframe.contentWindow.document; const iframeDocument = this.iframe.contentWindow.document;

View File

@@ -67,7 +67,7 @@
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟", "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
"confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا", "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "متأكد من أنك تود حظر إسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.",
"confirmations.mute.confirm": "أكتم", "confirmations.mute.confirm": "أكتم",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.unfollow.confirm": "إلغاء المتابعة", "confirmations.unfollow.confirm": "إلغاء المتابعة",
@@ -92,7 +92,7 @@
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.", "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
"empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.", "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
"empty_column.home.public_timeline": "الخيط العام", "empty_column.home.public_timeline": "الخيط العام",
"empty_column.list": "هذه القائمة فارغة.", "empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر تبويقات.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام", "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
"follow_request.authorize": "ترخيص", "follow_request.authorize": "ترخيص",
@@ -123,7 +123,7 @@
"keyboard_shortcuts.reply": "للردّ", "keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.search": "للتركيز على البحث", "keyboard_shortcuts.search": "للتركيز على البحث",
"keyboard_shortcuts.toot": "لتحرير تبويق جديد", "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
"keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة", "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
"lightbox.close": "إغلاق", "lightbox.close": "إغلاق",
"lightbox.next": "التالي", "lightbox.next": "التالي",

View File

@@ -92,7 +92,7 @@
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.", "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.", "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
"empty_column.home.public_timeline": "la línia de temps pública", "empty_column.home.public_timeline": "la línia de temps pública",
"empty_column.list": "Encara no hi ha res en aquesta llista.", "empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous estats, apareixeran aquí.",
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.", "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
"empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho", "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
"follow_request.authorize": "Autoritzar", "follow_request.authorize": "Autoritzar",

View File

@@ -7,22 +7,22 @@
"account.followers": "پیگیران", "account.followers": "پیگیران",
"account.follows": "پی می‌گیرد", "account.follows": "پی می‌گیرد",
"account.follows_you": "پیگیر شماست", "account.follows_you": "پیگیر شماست",
"account.hide_reblogs": "Hide boosts from @{name}", "account.hide_reblogs": "پنهان کردن بازبوق‌های @{name}",
"account.media": "رسانه", "account.media": "رسانه",
"account.mention": "نام‌بردن از @{name}", "account.mention": "نام‌بردن از @{name}",
"account.moved_to": "{name} has moved to:", "account.moved_to": "{name} منتقل شده است به:",
"account.mute": "بی‌صدا کردن @{name}", "account.mute": "بی‌صدا کردن @{name}",
"account.mute_notifications": "Mute notifications from @{name}", "account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",
"account.posts": "نوشته‌ها", "account.posts": "نوشته‌ها",
"account.report": "گزارش @{name}", "account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش", "account.requested": "در انتظار پذیرش",
"account.share": "هم‌رسانی نمایهٔ @{name}", "account.share": "هم‌رسانی نمایهٔ @{name}",
"account.show_reblogs": "Show boosts from @{name}", "account.show_reblogs": "نشان‌دادن بازبوق‌های @{name}",
"account.unblock": "رفع انسداد @{name}", "account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "رفع پنهان‌سازی از {domain}", "account.unblock_domain": "رفع پنهان‌سازی از {domain}",
"account.unfollow": "پایان پیگیری", "account.unfollow": "پایان پیگیری",
"account.unmute": "باصدا کردن @{name}", "account.unmute": "باصدا کردن @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "باصداکردن اعلان‌ها از طرف @{name}",
"account.view_full_profile": "نمایش نمایهٔ کامل", "account.view_full_profile": "نمایش نمایهٔ کامل",
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.", "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
@@ -36,7 +36,7 @@
"column.favourites": "پسندیده‌ها", "column.favourites": "پسندیده‌ها",
"column.follow_requests": "درخواست‌های پیگیری", "column.follow_requests": "درخواست‌های پیگیری",
"column.home": "خانه", "column.home": "خانه",
"column.lists": "Lists", "column.lists": "فهرست‌ها",
"column.mutes": "کاربران بی‌صداشده", "column.mutes": "کاربران بی‌صداشده",
"column.notifications": "اعلان‌ها", "column.notifications": "اعلان‌ها",
"column.pins": "نوشته‌های ثابت", "column.pins": "نوشته‌های ثابت",
@@ -65,7 +65,7 @@
"confirmations.delete.confirm": "پاک کن", "confirmations.delete.confirm": "پاک کن",
"confirmations.delete.message": "آیا واقعاً می‌خواهید این نوشته را پاک کنید؟", "confirmations.delete.message": "آیا واقعاً می‌خواهید این نوشته را پاک کنید؟",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "آیا واقعاً می‌خواهید این فهرست را برای همیشه پاک کنید؟",
"confirmations.domain_block.confirm": "پنهان‌سازی کل دامین", "confirmations.domain_block.confirm": "پنهان‌سازی کل دامین",
"confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.", "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
"confirmations.mute.confirm": "بی‌صدا کن", "confirmations.mute.confirm": "بی‌صدا کن",
@@ -92,7 +92,7 @@
"empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.", "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
"empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.", "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
"empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا", "empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا",
"empty_column.list": "There is nothing in this list yet.", "empty_column.list": "در این فهرست هنوز چیزی نیست. وقتی اعضای این فهرست چیزی بنویسند، این‌جا ظاهر خواهد شد.",
"empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.", "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
"empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود", "empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود",
"follow_request.authorize": "اجازه دهید", "follow_request.authorize": "اجازه دهید",
@@ -108,46 +108,46 @@
"home.column_settings.show_reblogs": "نمایش بازبوق‌ها", "home.column_settings.show_reblogs": "نمایش بازبوق‌ها",
"home.column_settings.show_replies": "نمایش پاسخ‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.settings": "تنظیمات ستون", "home.settings": "تنظیمات ستون",
"keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.back": "برای بازگشت",
"keyboard_shortcuts.boost": "to boost", "keyboard_shortcuts.boost": "برای بازبوقیدن",
"keyboard_shortcuts.column": "to focus a status in one of the columns", "keyboard_shortcuts.column": "برای برجسته‌کردن یک نوشته در یکی از ستون‌ها",
"keyboard_shortcuts.compose": "to focus the compose textarea", "keyboard_shortcuts.compose": "برای فعال‌کردن کادر نوشتهٔ تازه",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.down": "to move down in the list", "keyboard_shortcuts.down": "برای پایین‌رفتن در فهرست",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "to open status",
"keyboard_shortcuts.favourite": "to favourite", "keyboard_shortcuts.favourite": "برای پسندیدن",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.hotkey": "Hotkey", "keyboard_shortcuts.hotkey": "میان‌بر",
"keyboard_shortcuts.legend": "to display this legend", "keyboard_shortcuts.legend": "برای نمایش این راهنما",
"keyboard_shortcuts.mention": "to mention author", "keyboard_shortcuts.mention": "برای نام‌بردن از نویسنده",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "برای پاسخ‌دادن",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "برای فعال‌کردن جستجو",
"keyboard_shortcuts.toot": "to start a brand new toot", "keyboard_shortcuts.toot": "برای آغاز یک بوق تازه",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "برای برداشتن توجه از نوشتن/جستجو",
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.up": "برای بالا رفتن در فهرست",
"lightbox.close": "بستن", "lightbox.close": "بستن",
"lightbox.next": "بعدی", "lightbox.next": "بعدی",
"lightbox.previous": "قبلی", "lightbox.previous": "قبلی",
"lists.account.add": "Add to list", "lists.account.add": "افزودن به فهرست",
"lists.account.remove": "Remove from list", "lists.account.remove": "پاک‌کردن از فهرست",
"lists.delete": "Delete list", "lists.delete": "حذف فهرست",
"lists.edit": "Edit list", "lists.edit": "ویرایش فهرست",
"lists.new.create": "Add list", "lists.new.create": "افزودن فهرست",
"lists.new.title_placeholder": "New list title", "lists.new.title_placeholder": "نام فهرست تازه",
"lists.search": "Search among people you follow", "lists.search": "بین کسانی که پی می‌گیرید بگردید",
"lists.subheading": "Your lists", "lists.subheading": "فهرست‌های شما",
"loading_indicator.label": "بارگیری...", "loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی", "media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد", "missing_indicator.label": "پیدا نشد",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "اعلان‌های این کاربر پنهان شود؟",
"navigation_bar.blocks": "کاربران مسدودشده", "navigation_bar.blocks": "کاربران مسدودشده",
"navigation_bar.community_timeline": "نوشته‌های محلی", "navigation_bar.community_timeline": "نوشته‌های محلی",
"navigation_bar.edit_profile": "ویرایش نمایه", "navigation_bar.edit_profile": "ویرایش نمایه",
"navigation_bar.favourites": "پسندیده‌ها", "navigation_bar.favourites": "پسندیده‌ها",
"navigation_bar.follow_requests": "درخواست‌های پیگیری", "navigation_bar.follow_requests": "درخواست‌های پیگیری",
"navigation_bar.info": "اطلاعات تکمیلی", "navigation_bar.info": "اطلاعات تکمیلی",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.keyboard_shortcuts": "میان‌برهای صفحه‌کلید",
"navigation_bar.lists": "Lists", "navigation_bar.lists": "فهرست‌ها",
"navigation_bar.logout": "خروج", "navigation_bar.logout": "خروج",
"navigation_bar.mutes": "کاربران بی‌صداشده", "navigation_bar.mutes": "کاربران بی‌صداشده",
"navigation_bar.pins": "نوشته‌های ثابت", "navigation_bar.pins": "نوشته‌های ثابت",
@@ -174,7 +174,7 @@
"onboarding.page_four.home": "ستون «خانه» نوشته‌های کسانی را نشان می‌دهد که شما پی می‌گیرید.", "onboarding.page_four.home": "ستون «خانه» نوشته‌های کسانی را نشان می‌دهد که شما پی می‌گیرید.",
"onboarding.page_four.notifications": "ستون «اعلان‌ها» ارتباط‌های شما با دیگران را نشان می‌دهد.", "onboarding.page_four.notifications": "ستون «اعلان‌ها» ارتباط‌های شما با دیگران را نشان می‌دهد.",
"onboarding.page_one.federation": "ماستدون شبکه‌ای از سرورهای مستقل است که با پیوستن به یکدیگر یک شبکهٔ اجتماعی بزرگ را تشکیل می‌دهند.", "onboarding.page_one.federation": "ماستدون شبکه‌ای از سرورهای مستقل است که با پیوستن به یکدیگر یک شبکهٔ اجتماعی بزرگ را تشکیل می‌دهند.",
"onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است.", "onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است",
"onboarding.page_one.welcome": "به ماستدون خوش آمدید!", "onboarding.page_one.welcome": "به ماستدون خوش آمدید!",
"onboarding.page_six.admin": "نشانی مسئول سرور شما {admin} است.", "onboarding.page_six.admin": "نشانی مسئول سرور شما {admin} است.",
"onboarding.page_six.almost_done": "الان تقریباً آماده‌اید...", "onboarding.page_six.almost_done": "الان تقریباً آماده‌اید...",
@@ -199,7 +199,7 @@
"privacy.unlisted.short": "فهرست‌نشده", "privacy.unlisted.short": "فهرست‌نشده",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.hours": "{number}h", "relative_time.hours": "{number}h",
"relative_time.just_now": "now", "relative_time.just_now": "الان",
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number}s",
"reply_indicator.cancel": "لغو", "reply_indicator.cancel": "لغو",
@@ -222,7 +222,7 @@
"status.load_more": "بیشتر نشان بده", "status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده", "status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}", "status.mention": "نام‌بردن از @{name}",
"status.more": "More", "status.more": "بیشتر",
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "بی‌صداکردن گفتگو", "status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن", "status.open": "این نوشته را باز کن",
@@ -244,7 +244,7 @@
"tabs_bar.home": "خانه", "tabs_bar.home": "خانه",
"tabs_bar.local_timeline": "محلی", "tabs_bar.local_timeline": "محلی",
"tabs_bar.notifications": "اعلان‌ها", "tabs_bar.notifications": "اعلان‌ها",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "اگر از ماستدون خارج شوید پیش‌نویس شما پاک خواهد شد.",
"upload_area.title": "برای بارگذاری به این‌جا بکشید", "upload_area.title": "برای بارگذاری به این‌جا بکشید",
"upload_button.label": "افزودن تصویر", "upload_button.label": "افزودن تصویر",
"upload_form.description": "نوشتهٔ توضیحی برای کم‌بینایان و نابینایان", "upload_form.description": "نوشتهٔ توضیحی برای کم‌بینایان و نابینایان",

View File

@@ -92,7 +92,7 @@
"empty_column.hashtag": "Aínda non hai nada con esta etiqueta.", "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
"empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.", "empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
"empty_column.home.public_timeline": "a liña temporal pública", "empty_column.home.public_timeline": "a liña temporal pública",
"empty_column.list": "Aínda non hai nada en esta lista.", "empty_column.list": "Aínda non hai nada en esta lista. Cando as usuarias incluídas na lista publiquen mensaxes, aparecerán aquí.",
"empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.", "empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.",
"empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa", "empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
@@ -109,7 +109,7 @@
"home.column_settings.show_replies": "Mostrar respostas", "home.column_settings.show_replies": "Mostrar respostas",
"home.settings": "Axustes da columna", "home.settings": "Axustes da columna",
"keyboard_shortcuts.back": "voltar atrás", "keyboard_shortcuts.back": "voltar atrás",
"keyboard_shortcuts.boost": "repetir", "keyboard_shortcuts.boost": "promover",
"keyboard_shortcuts.column": "destacar un estado en unha das columnas", "keyboard_shortcuts.column": "destacar un estado en unha das columnas",
"keyboard_shortcuts.compose": "Foco no área de escritura", "keyboard_shortcuts.compose": "Foco no área de escritura",
"keyboard_shortcuts.description": "Descrición", "keyboard_shortcuts.description": "Descrición",
@@ -227,8 +227,8 @@
"status.mute_conversation": "Acalar conversa", "status.mute_conversation": "Acalar conversa",
"status.open": "Expandir este estado", "status.open": "Expandir este estado",
"status.pin": "Fixar no perfil", "status.pin": "Fixar no perfil",
"status.reblog": "Promocionar", "status.reblog": "Promover",
"status.reblogged_by": "{name} promocionado", "status.reblogged_by": "{name} promoveu",
"status.reply": "Resposta", "status.reply": "Resposta",
"status.replyAll": "Resposta a conversa", "status.replyAll": "Resposta a conversa",
"status.report": "Informar @{name}", "status.report": "Informar @{name}",

View File

@@ -25,11 +25,11 @@
"account.unmute_notifications": "@{name}의 알림 뮤트 해제", "account.unmute_notifications": "@{name}의 알림 뮤트 해제",
"account.view_full_profile": "전체 프로필 보기", "account.view_full_profile": "전체 프로필 보기",
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.", "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
"bundle_column_error.retry": "다시 시도", "bundle_column_error.retry": "다시 시도",
"bundle_column_error.title": "네트워크 에러", "bundle_column_error.title": "네트워크 에러",
"bundle_modal_error.close": "닫기", "bundle_modal_error.close": "닫기",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
"bundle_modal_error.retry": "다시 시도", "bundle_modal_error.retry": "다시 시도",
"column.blocks": "차단 중인 사용자", "column.blocks": "차단 중인 사용자",
"column.community": "로컬 타임라인", "column.community": "로컬 타임라인",
@@ -50,7 +50,7 @@
"column_header.unpin": "고정 해제", "column_header.unpin": "고정 해제",
"column_subheading.navigation": "내비게이션", "column_subheading.navigation": "내비게이션",
"column_subheading.settings": "설정", "column_subheading.settings": "설정",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", "compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.", "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
"compose_form.lock_disclaimer.lock": "비공개", "compose_form.lock_disclaimer.lock": "비공개",
"compose_form.placeholder": "지금 무엇을 하고 있나요?", "compose_form.placeholder": "지금 무엇을 하고 있나요?",
@@ -135,7 +135,7 @@
"lists.new.create": "리스트 추가", "lists.new.create": "리스트 추가",
"lists.new.title_placeholder": "새 리스트의 이름", "lists.new.title_placeholder": "새 리스트의 이름",
"lists.search": "팔로우 중인 사람들 중에서 찾기", "lists.search": "팔로우 중인 사람들 중에서 찾기",
"lists.subheading": "Your lists", "lists.subheading": "당신의 리스트",
"loading_indicator.label": "불러오는 중...", "loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환", "media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다", "missing_indicator.label": "찾을 수 없습니다",
@@ -178,7 +178,7 @@
"onboarding.page_one.welcome": "Mastodon에 어서 오세요!", "onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
"onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.", "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
"onboarding.page_six.almost_done": "이상입니다.", "onboarding.page_six.almost_done": "이상입니다.",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "본 아페툿!",
"onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.", "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.", "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
"onboarding.page_six.guidelines": "커뮤니티 가이드라인", "onboarding.page_six.guidelines": "커뮤니티 가이드라인",
@@ -213,7 +213,7 @@
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다", "search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
"search_popout.tips.user": "유저", "search_popout.tips.user": "유저",
"search_results.total": "{count, number}건의 결과", "search_results.total": "{count, number}건의 결과",
"standalone.public_title": "A look inside...", "standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
"status.block": "@{name} 차단", "status.block": "@{name} 차단",
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
"status.delete": "삭제", "status.delete": "삭제",
@@ -247,7 +247,7 @@
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.", "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
"upload_area.title": "드래그 & 드롭으로 업로드", "upload_area.title": "드래그 & 드롭으로 업로드",
"upload_button.label": "미디어 추가", "upload_button.label": "미디어 추가",
"upload_form.description": "Describe for the visually impaired", "upload_form.description": "시각장애인을 위한 설명",
"upload_form.undo": "재시도", "upload_form.undo": "재시도",
"upload_progress.label": "업로드 중...", "upload_progress.label": "업로드 중...",
"video.close": "동영상 닫기", "video.close": "동영상 닫기",

View File

@@ -100,10 +100,10 @@
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Beginnen", "getting_started.heading": "Beginnen",
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.", "getting_started.open_source_notice": "Mastodon is vrije software. Je kunt bijdragen of problemen melden op GitHub via {github}.",
"getting_started.userguide": "Gebruikersgids", "getting_started.userguide": "Gebruikersgids",
"home.column_settings.advanced": "Geavanceerd", "home.column_settings.advanced": "Geavanceerd",
"home.column_settings.basic": "Basis", "home.column_settings.basic": "Algemeen",
"home.column_settings.filter_regex": "Wegfilteren met reguliere expressies", "home.column_settings.filter_regex": "Wegfilteren met reguliere expressies",
"home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_reblogs": "Boosts tonen",
"home.column_settings.show_replies": "Reacties tonen", "home.column_settings.show_replies": "Reacties tonen",
@@ -146,7 +146,7 @@
"navigation_bar.favourites": "Favorieten", "navigation_bar.favourites": "Favorieten",
"navigation_bar.follow_requests": "Volgverzoeken", "navigation_bar.follow_requests": "Volgverzoeken",
"navigation_bar.info": "Uitgebreide informatie", "navigation_bar.info": "Uitgebreide informatie",
"navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen", "navigation_bar.keyboard_shortcuts": "Sneltoetsen",
"navigation_bar.lists": "Lijsten", "navigation_bar.lists": "Lijsten",
"navigation_bar.logout": "Afmelden", "navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers", "navigation_bar.mutes": "Genegeerde gebruikers",
@@ -180,7 +180,7 @@
"onboarding.page_six.almost_done": "Bijna klaar...", "onboarding.page_six.almost_done": "Bijna klaar...",
"onboarding.page_six.appetoot": "Veel succes!", "onboarding.page_six.appetoot": "Veel succes!",
"onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.", "onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.",
"onboarding.page_six.github": "Mastodon kost niets, en is open-source- en vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.", "onboarding.page_six.github": "Mastodon kost niets en is vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.",
"onboarding.page_six.guidelines": "communityrichtlijnen", "onboarding.page_six.guidelines": "communityrichtlijnen",
"onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!", "onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!",
"onboarding.page_six.various_app": "mobiele apps", "onboarding.page_six.various_app": "mobiele apps",

View File

@@ -33,6 +33,7 @@
"bundle_modal_error.retry": "Spróbuj ponownie", "bundle_modal_error.retry": "Spróbuj ponownie",
"column.blocks": "Zablokowani użytkownicy", "column.blocks": "Zablokowani użytkownicy",
"column.community": "Lokalna oś czasu", "column.community": "Lokalna oś czasu",
"column.direct": "Wiadomości bezpośrednie",
"column.favourites": "Ulubione", "column.favourites": "Ulubione",
"column.follow_requests": "Prośby o śledzenie", "column.follow_requests": "Prośby o śledzenie",
"column.home": "Strona główna", "column.home": "Strona główna",
@@ -48,6 +49,9 @@
"column_header.pin": "Przypnij", "column_header.pin": "Przypnij",
"column_header.show_settings": "Pokaż ustawienia", "column_header.show_settings": "Pokaż ustawienia",
"column_header.unpin": "Cofnij przypięcie", "column_header.unpin": "Cofnij przypięcie",
"column.heading": "Różne",
"column.subheading": "Różne opcje",
"column_subheading.lists": "Listy",
"column_subheading.navigation": "Nawigacja", "column_subheading.navigation": "Nawigacja",
"column_subheading.settings": "Ustawienia", "column_subheading.settings": "Ustawienia",
"compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.", "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
@@ -89,10 +93,11 @@
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Podróże i miejsca", "emoji_button.travel": "Podróże i miejsca",
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Jeżeli wyślesz lub otrzymasz jakąś, będzie tu widoczna.",
"empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
"empty_column.home.public_timeline": "publiczna oś czasu", "empty_column.home.public_timeline": "publiczna oś czasu",
"empty_column.list": "Nie ma nic na tej liście.", "empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.",
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.", "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.", "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
"follow_request.authorize": "Autoryzuj", "follow_request.authorize": "Autoryzuj",
@@ -142,6 +147,7 @@
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
"navigation_bar.blocks": "Zablokowani użytkownicy", "navigation_bar.blocks": "Zablokowani użytkownicy",
"navigation_bar.community_timeline": "Lokalna oś czasu", "navigation_bar.community_timeline": "Lokalna oś czasu",
"navigation_bar.direct": "Wiadomości bezpośrednie",
"navigation_bar.edit_profile": "Edytuj profil", "navigation_bar.edit_profile": "Edytuj profil",
"navigation_bar.favourites": "Ulubione", "navigation_bar.favourites": "Ulubione",
"navigation_bar.follow_requests": "Prośby o śledzenie", "navigation_bar.follow_requests": "Prośby o śledzenie",
@@ -149,6 +155,7 @@
"navigation_bar.keyboard_shortcuts": "Skróty klawiszowe", "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
"navigation_bar.lists": "Listy", "navigation_bar.lists": "Listy",
"navigation_bar.logout": "Wyloguj", "navigation_bar.logout": "Wyloguj",
"navigation_bar.misc": "Różne",
"navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy", "navigation_bar.pins": "Przypięte wpisy",
"navigation_bar.preferences": "Preferencje", "navigation_bar.preferences": "Preferencje",

View File

@@ -92,7 +92,7 @@
"empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.", "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
"empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.", "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
"empty_column.home.public_timeline": "global", "empty_column.home.public_timeline": "global",
"empty_column.list": "Ainda não há nada nesta lista.", "empty_column.list": "Ainda não há nada nesta lista. Quando membros dessa lista fizerem novas postagens, elas aparecerão aqui.",
"empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.", "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.",
"empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias", "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",

View File

@@ -35,11 +35,11 @@
"column.community": "Local", "column.community": "Local",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.follow_requests": "Seguidores Pendentes", "column.follow_requests": "Seguidores Pendentes",
"column.home": "Home", "column.home": "Início",
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Utilizadores silenciados", "column.mutes": "Utilizadores silenciados",
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.pins": "Pinned toot", "column.pins": "Posts fixos",
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Esconder preferências", "column_header.hide_settings": "Esconder preferências",
@@ -47,7 +47,7 @@
"column_header.moveRight_settings": "Mover coluna para a direita", "column_header.moveRight_settings": "Mover coluna para a direita",
"column_header.pin": "Fixar", "column_header.pin": "Fixar",
"column_header.show_settings": "Mostrar preferências", "column_header.show_settings": "Mostrar preferências",
"column_header.unpin": "Remover fixar", "column_header.unpin": "Desafixar",
"column_subheading.navigation": "Navegação", "column_subheading.navigation": "Navegação",
"column_subheading.settings": "Preferências", "column_subheading.settings": "Preferências",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
@@ -92,7 +92,7 @@
"empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
"empty_column.home.public_timeline": "global", "empty_column.home.public_timeline": "global",
"empty_column.list": "Ainda não existem publicações nesta lista.", "empty_column.list": "Ainda não existem publicações nesta lista. Quando membros desta lista fizerem novas publicações, elas aparecerão aqui.",
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos", "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
@@ -226,7 +226,7 @@
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Silenciar conversa", "status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir", "status.open": "Expandir",
"status.pin": "Pin on profile", "status.pin": "Fixar no perfil",
"status.reblog": "Partilhar", "status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou", "status.reblogged_by": "{name} partilhou",
"status.reply": "Responder", "status.reply": "Responder",
@@ -234,7 +234,7 @@
"status.report": "Denunciar @{name}", "status.report": "Denunciar @{name}",
"status.sensitive_toggle": "Clique para ver", "status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível", "status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share", "status.share": "Compartilhar",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Deixar de silenciar esta conversa", "status.unmute_conversation": "Deixar de silenciar esta conversa",

View File

@@ -11,7 +11,7 @@
"account.media": "Mediji", "account.media": "Mediji",
"account.mention": "Pomeni korisnika @{name}", "account.mention": "Pomeni korisnika @{name}",
"account.moved_to": "{name} se pomerio na:", "account.moved_to": "{name} se pomerio na:",
"account.mute": "Mutiraj @{name}", "account.mute": "Ućutkaj korisnika @{name}",
"account.mute_notifications": "Isključi obaveštenja od korisnika @{name}", "account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
"account.posts": "Statusa", "account.posts": "Statusa",
"account.report": "Prijavi @{name}", "account.report": "Prijavi @{name}",
@@ -21,7 +21,7 @@
"account.unblock": "Odblokiraj korisnika @{name}", "account.unblock": "Odblokiraj korisnika @{name}",
"account.unblock_domain": "Odblokiraj domen {domain}", "account.unblock_domain": "Odblokiraj domen {domain}",
"account.unfollow": "Otprati", "account.unfollow": "Otprati",
"account.unmute": "Odmutiraj @{name}", "account.unmute": "Ukloni ućutkavanje korisniku @{name}",
"account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}", "account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
"account.view_full_profile": "Vidi ceo profil", "account.view_full_profile": "Vidi ceo profil",
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put", "boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
@@ -37,10 +37,10 @@
"column.follow_requests": "Zahtevi za praćenje", "column.follow_requests": "Zahtevi za praćenje",
"column.home": "Početna", "column.home": "Početna",
"column.lists": "Liste", "column.lists": "Liste",
"column.mutes": "Mutirani korisnici", "column.mutes": "Ućutkani korisnici",
"column.notifications": "Obaveštenja", "column.notifications": "Obaveštenja",
"column.pins": "Prikačeni tutovi", "column.pins": "Prikačeni tutovi",
"column.public": "Združena lajna", "column.public": "Federisana lajna",
"column_back_button.label": "Nazad", "column_back_button.label": "Nazad",
"column_header.hide_settings": "Sakrij postavke", "column_header.hide_settings": "Sakrij postavke",
"column_header.moveLeft_settings": "Pomeri kolonu ulevo", "column_header.moveLeft_settings": "Pomeri kolonu ulevo",
@@ -50,6 +50,7 @@
"column_header.unpin": "Otkači", "column_header.unpin": "Otkači",
"column_subheading.navigation": "Navigacija", "column_subheading.navigation": "Navigacija",
"column_subheading.settings": "Postavke", "column_subheading.settings": "Postavke",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
"compose_form.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.", "compose_form.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.",
"compose_form.lock_disclaimer.lock": "zaključan", "compose_form.lock_disclaimer.lock": "zaključan",
"compose_form.placeholder": "Šta Vam je na umu?", "compose_form.placeholder": "Šta Vam je na umu?",
@@ -66,9 +67,9 @@
"confirmations.delete_list.confirm": "Obriši", "confirmations.delete_list.confirm": "Obriši",
"confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?", "confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?",
"confirmations.domain_block.confirm": "Sakrij ceo domen", "confirmations.domain_block.confirm": "Sakrij ceo domen",
"confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili mutiranja su dovoljna i preporučljiva.", "confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili ućutkavanja su dovoljna i preporučljiva.",
"confirmations.mute.confirm": "Mutiraj", "confirmations.mute.confirm": "Ućutkaj",
"confirmations.mute.message": "Da li stvarno želite da mutirate korisnika {name}?", "confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?",
"confirmations.unfollow.confirm": "Otprati", "confirmations.unfollow.confirm": "Otprati",
"confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?", "confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
"embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.", "embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.",
@@ -148,10 +149,10 @@
"navigation_bar.keyboard_shortcuts": "Prečice na tastaturi", "navigation_bar.keyboard_shortcuts": "Prečice na tastaturi",
"navigation_bar.lists": "Liste", "navigation_bar.lists": "Liste",
"navigation_bar.logout": "Odjava", "navigation_bar.logout": "Odjava",
"navigation_bar.mutes": "Mutirani korisnici", "navigation_bar.mutes": "Ućutkani korisnici",
"navigation_bar.pins": "Prikačeni tutovi", "navigation_bar.pins": "Prikačeni tutovi",
"navigation_bar.preferences": "Podešavanja", "navigation_bar.preferences": "Podešavanja",
"navigation_bar.public_timeline": "Združena lajna", "navigation_bar.public_timeline": "Federisana lajna",
"notification.favourite": "{name} je stavio Vaš status kao omiljeni", "notification.favourite": "{name} je stavio Vaš status kao omiljeni",
"notification.follow": "{name} Vas je zapratio", "notification.follow": "{name} Vas je zapratio",
"notification.mention": "{name} Vas je pomenuo", "notification.mention": "{name} Vas je pomenuo",
@@ -169,7 +170,7 @@
"notifications.column_settings.sound": "Puštaj zvuk", "notifications.column_settings.sound": "Puštaj zvuk",
"onboarding.done": "Gotovo", "onboarding.done": "Gotovo",
"onboarding.next": "Sledeće", "onboarding.next": "Sledeće",
"onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Združena lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.", "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
"onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.", "onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.",
"onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.", "onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.",
"onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.", "onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.",
@@ -213,6 +214,7 @@
"search_popout.tips.user": "korisnik", "search_popout.tips.user": "korisnik",
"search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}", "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
"standalone.public_title": "Pogled iznutra...", "standalone.public_title": "Pogled iznutra...",
"status.block": "Block @{name}",
"status.cannot_reblog": "Ovaj status ne može da se podrži", "status.cannot_reblog": "Ovaj status ne može da se podrži",
"status.delete": "Obriši", "status.delete": "Obriši",
"status.embed": "Ugradi na sajt", "status.embed": "Ugradi na sajt",
@@ -221,7 +223,8 @@
"status.media_hidden": "Multimedija sakrivena", "status.media_hidden": "Multimedija sakrivena",
"status.mention": "Pomeni korisnika @{name}", "status.mention": "Pomeni korisnika @{name}",
"status.more": "Još", "status.more": "Još",
"status.mute_conversation": "Mutiraj prepisku", "status.mute": "Mute @{name}",
"status.mute_conversation": "Ućutkaj prepisku",
"status.open": "Proširi ovaj status", "status.open": "Proširi ovaj status",
"status.pin": "Prikači na profil", "status.pin": "Prikači na profil",
"status.reblog": "Podrži", "status.reblog": "Podrži",
@@ -237,7 +240,7 @@
"status.unmute_conversation": "Uključi prepisku", "status.unmute_conversation": "Uključi prepisku",
"status.unpin": "Otkači sa profila", "status.unpin": "Otkači sa profila",
"tabs_bar.compose": "Napiši", "tabs_bar.compose": "Napiši",
"tabs_bar.federated_timeline": "Združeno", "tabs_bar.federated_timeline": "Federisano",
"tabs_bar.home": "Početna", "tabs_bar.home": "Početna",
"tabs_bar.local_timeline": "Lokalno", "tabs_bar.local_timeline": "Lokalno",
"tabs_bar.notifications": "Obaveštenja", "tabs_bar.notifications": "Obaveštenja",

View File

@@ -11,7 +11,7 @@
"account.media": "Медији", "account.media": "Медији",
"account.mention": "Помени корисника @{name}", "account.mention": "Помени корисника @{name}",
"account.moved_to": "{name} се померио на:", "account.moved_to": "{name} се померио на:",
"account.mute": "Мутирај @{name}", "account.mute": "Ућуткај корисника @{name}",
"account.mute_notifications": "Искључи обавештења од корисника @{name}", "account.mute_notifications": "Искључи обавештења од корисника @{name}",
"account.posts": "Статуса", "account.posts": "Статуса",
"account.report": "Пријави @{name}", "account.report": "Пријави @{name}",
@@ -21,7 +21,7 @@
"account.unblock": "Одблокирај корисника @{name}", "account.unblock": "Одблокирај корисника @{name}",
"account.unblock_domain": "Одблокирај домен {domain}", "account.unblock_domain": "Одблокирај домен {domain}",
"account.unfollow": "Отпрати", "account.unfollow": "Отпрати",
"account.unmute": "Одмутирај @{name}", "account.unmute": "Уклони ућуткавање кориснику @{name}",
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}", "account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
"account.view_full_profile": "Види цео профил", "account.view_full_profile": "Види цео профил",
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут", "boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
@@ -37,10 +37,10 @@
"column.follow_requests": "Захтеви за праћење", "column.follow_requests": "Захтеви за праћење",
"column.home": "Почетна", "column.home": "Почетна",
"column.lists": "Листе", "column.lists": "Листе",
"column.mutes": "Мутирани корисници", "column.mutes": "Ућуткани корисници",
"column.notifications": "Обавештења", "column.notifications": "Обавештења",
"column.pins": "Прикачени тутови", "column.pins": "Прикачени тутови",
"column.public": "Здружена лајна", "column.public": "Федерисана лајна",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Сакриј поставке", "column_header.hide_settings": "Сакриј поставке",
"column_header.moveLeft_settings": "Помери колону улево", "column_header.moveLeft_settings": "Помери колону улево",
@@ -67,9 +67,9 @@
"confirmations.delete_list.confirm": "Обриши", "confirmations.delete_list.confirm": "Обриши",
"confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?", "confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
"confirmations.domain_block.confirm": "Сакриј цео домен", "confirmations.domain_block.confirm": "Сакриј цео домен",
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или мутирања су довољна и препоручљива.", "confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или ућуткавања су довољна и препоручљива.",
"confirmations.mute.confirm": "Мутирај", "confirmations.mute.confirm": "Ућуткај",
"confirmations.mute.message": "Да ли стварно желите да мутирате корисника {name}?", "confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
"confirmations.unfollow.confirm": "Отпрати", "confirmations.unfollow.confirm": "Отпрати",
"confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?", "confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
"embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.", "embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
@@ -149,10 +149,10 @@
"navigation_bar.keyboard_shortcuts": "Пречице на тастатури", "navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
"navigation_bar.lists": "Листе", "navigation_bar.lists": "Листе",
"navigation_bar.logout": "Одјава", "navigation_bar.logout": "Одјава",
"navigation_bar.mutes": "Мутирани корисници", "navigation_bar.mutes": "Ућуткани корисници",
"navigation_bar.pins": "Прикачени тутови", "navigation_bar.pins": "Прикачени тутови",
"navigation_bar.preferences": "Подешавања", "navigation_bar.preferences": "Подешавања",
"navigation_bar.public_timeline": "Здружена лајна", "navigation_bar.public_timeline": "Федерисана лајна",
"notification.favourite": "{name} је ставио Ваш статус као омиљени", "notification.favourite": "{name} је ставио Ваш статус као омиљени",
"notification.follow": "{name} Вас је запратио", "notification.follow": "{name} Вас је запратио",
"notification.mention": "{name} Вас је поменуо", "notification.mention": "{name} Вас је поменуо",
@@ -170,7 +170,7 @@
"notifications.column_settings.sound": "Пуштај звук", "notifications.column_settings.sound": "Пуштај звук",
"onboarding.done": "Готово", "onboarding.done": "Готово",
"onboarding.next": "Следеће", "onboarding.next": "Следеће",
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Здружена лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.", "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
"onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.", "onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
"onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.", "onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
"onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.", "onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
@@ -224,7 +224,7 @@
"status.mention": "Помени корисника @{name}", "status.mention": "Помени корисника @{name}",
"status.more": "Још", "status.more": "Још",
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Мутирај преписку", "status.mute_conversation": "Ућуткај преписку",
"status.open": "Прошири овај статус", "status.open": "Прошири овај статус",
"status.pin": "Прикачи на профил", "status.pin": "Прикачи на профил",
"status.reblog": "Подржи", "status.reblog": "Подржи",
@@ -240,7 +240,7 @@
"status.unmute_conversation": "Укључи преписку", "status.unmute_conversation": "Укључи преписку",
"status.unpin": "Откачи са профила", "status.unpin": "Откачи са профила",
"tabs_bar.compose": "Напиши", "tabs_bar.compose": "Напиши",
"tabs_bar.federated_timeline": "Здружено", "tabs_bar.federated_timeline": "Федерисано",
"tabs_bar.home": "Почетна", "tabs_bar.home": "Почетна",
"tabs_bar.local_timeline": "Локално", "tabs_bar.local_timeline": "Локално",
"tabs_bar.notifications": "Обавештења", "tabs_bar.notifications": "Обавештења",

View File

@@ -0,0 +1,2 @@
[
]

View File

@@ -50,7 +50,7 @@
"column_header.unpin": "取消固定", "column_header.unpin": "取消固定",
"column_subheading.navigation": "导航", "column_subheading.navigation": "导航",
"column_subheading.settings": "设置", "column_subheading.settings": "设置",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", "compose_form.hashtag_warning": "这条嘟文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的嘟文才能通过话题标签进行搜索。",
"compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。", "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
"compose_form.lock_disclaimer.lock": "开启保护", "compose_form.lock_disclaimer.lock": "开启保护",
"compose_form.placeholder": "在想啥?", "compose_form.placeholder": "在想啥?",
@@ -214,7 +214,7 @@
"search_popout.tips.user": "用户", "search_popout.tips.user": "用户",
"search_results.total": "共 {count, number} 个结果", "search_results.total": "共 {count, number} 个结果",
"standalone.public_title": "大家都在干啥?", "standalone.public_title": "大家都在干啥?",
"status.block": "Block @{name}", "status.block": "屏蔽 @{name}",
"status.cannot_reblog": "无法转嘟这条嘟文", "status.cannot_reblog": "无法转嘟这条嘟文",
"status.delete": "删除", "status.delete": "删除",
"status.embed": "嵌入", "status.embed": "嵌入",
@@ -223,7 +223,7 @@
"status.media_hidden": "隐藏媒体内容", "status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}", "status.mention": "提及 @{name}",
"status.more": "更多", "status.more": "更多",
"status.mute": "Mute @{name}", "status.mute": "隐藏 @{name}",
"status.mute_conversation": "隐藏此对话", "status.mute_conversation": "隐藏此对话",
"status.open": "展开嘟文", "status.open": "展开嘟文",
"status.pin": "在个人资料页面置顶", "status.pin": "在个人资料页面置顶",

View File

@@ -64,8 +64,8 @@
"confirmations.block.message": "你確定要封鎖 {name} ", "confirmations.block.message": "你確定要封鎖 {name} ",
"confirmations.delete.confirm": "刪除", "confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這個狀態?", "confirmations.delete.message": "你確定要刪除這個狀態?",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "刪除",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "確定要永久性地刪除這個名單嗎?",
"confirmations.domain_block.confirm": "隱藏整個網域", "confirmations.domain_block.confirm": "隱藏整個網域",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。", "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.mute.confirm": "消音", "confirmations.mute.confirm": "消音",
@@ -128,14 +128,14 @@
"lightbox.close": "關閉", "lightbox.close": "關閉",
"lightbox.next": "繼續", "lightbox.next": "繼續",
"lightbox.previous": "回退", "lightbox.previous": "回退",
"lists.account.add": "Add to list", "lists.account.add": "加到名單裡",
"lists.account.remove": "Remove from list", "lists.account.remove": "從名單中移除",
"lists.delete": "Delete list", "lists.delete": "刪除名單",
"lists.edit": "Edit list", "lists.edit": "修改名單",
"lists.new.create": "Add list", "lists.new.create": "新增名單",
"lists.new.title_placeholder": "New list title", "lists.new.title_placeholder": "名單名稱",
"lists.search": "Search among people you follow", "lists.search": "搜尋您關注的使用者",
"lists.subheading": "Your lists", "lists.subheading": "您的名單",
"loading_indicator.label": "讀取中...", "loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性", "media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到", "missing_indicator.label": "找不到",
@@ -146,8 +146,8 @@
"navigation_bar.favourites": "最愛", "navigation_bar.favourites": "最愛",
"navigation_bar.follow_requests": "關注請求", "navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本站", "navigation_bar.info": "關於本站",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.keyboard_shortcuts": "快速鍵",
"navigation_bar.lists": "Lists", "navigation_bar.lists": "名單",
"navigation_bar.logout": "登出", "navigation_bar.logout": "登出",
"navigation_bar.mutes": "消音的使用者", "navigation_bar.mutes": "消音的使用者",
"navigation_bar.pins": "置頂貼文", "navigation_bar.pins": "置頂貼文",

View File

@@ -398,10 +398,12 @@
} }
} }
&__content {
max-width: calc(100% - 90px);
}
&__title { &__title {
overflow: hidden; word-wrap: break-word;
text-overflow: ellipsis;
white-space: nowrap;
} }
&__timestamp { &__timestamp {
@@ -415,7 +417,7 @@
color: $ui-primary-color; color: $ui-primary-color;
font-family: 'mastodon-font-monospace', monospace; font-family: 'mastodon-font-monospace', monospace;
font-size: 12px; font-size: 12px;
white-space: nowrap; word-wrap: break-word;
min-height: 20px; min-height: 20px;
} }

View File

@@ -2350,6 +2350,19 @@
position: relative; position: relative;
z-index: 2; z-index: 2;
outline: 0; outline: 0;
overflow: hidden;
& > button {
display: flex;
flex: auto;
margin: 0;
border: none;
padding: 0;
color: inherit;
background: transparent;
font: inherit;
text-align: left;
}
&.active { &.active {
box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3); box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3);

View File

@@ -2,18 +2,16 @@
class ActivityPub::Activity::Accept < ActivityPub::Activity class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform def perform
if @object.respond_to?(:[]) && case @object['type']
@object['type'] == 'Follow' && @object['actor'].present? when 'Follow'
accept_follow_from @object['actor'] accept_follow
else
accept_follow_object @object
end end
end end
private private
def accept_follow_from(actor) def accept_follow
target_account = account_from_uri(value_or_id(actor)) target_account = account_from_uri(target_uri)
return if target_account.nil? || !target_account.local? return if target_account.nil? || !target_account.local?
@@ -21,8 +19,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
follow_request&.authorize! follow_request&.authorize!
end end
def accept_follow_object(object) def target_uri
follow_request = ActivityPub::TagManager.instance.uri_to_resource(value_or_id(object), FollowRequest) @target_uri ||= value_or_id(@object['actor'])
follow_request&.authorize!
end end
end end

View File

@@ -5,7 +5,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
original_status = status_from_uri(object_uri) original_status = status_from_uri(object_uri)
original_status ||= fetch_remote_original_status original_status ||= fetch_remote_original_status
return if original_status.nil? || delete_arrived_first?(@json['id']) return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
status = Status.find_by(account: @account, reblog: original_status) status = Status.find_by(account: @account, reblog: original_status)
@@ -33,4 +33,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
::FetchRemoteStatusService.new.call(@object['url']) ::FetchRemoteStatusService.new.call(@object['url'])
end end
end end
def announceable?(status)
status.public_visibility? || status.unlisted_visibility?
end
end end

View File

@@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::Activity::Create < ActivityPub::Activity class ActivityPub::Activity::Create < ActivityPub::Activity
SUPPORTED_TYPES = %w(Article Note).freeze SUPPORTED_TYPES = %w(Note).freeze
CONVERTED_TYPES = %w(Image Video).freeze CONVERTED_TYPES = %w(Image Video Article).freeze
def perform def perform
return if delete_arrived_first?(object_uri) || unsupported_object_type? return if delete_arrived_first?(object_uri) || unsupported_object_type? || invalid_origin?(@object['id'])
RedisLock.acquire(lock_options) do |lock| RedisLock.acquire(lock_options) do |lock|
if lock.acquired? if lock.acquired?
@@ -213,7 +213,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def object_url def object_url
return if @object['url'].blank? return if @object['url'].blank?
url_to_href(@object['url'], 'text/html')
url_candidate = url_to_href(@object['url'], 'text/html')
if invalid_origin?(url_candidate)
nil
else
url_candidate
end
end end
def content_language_map? def content_language_map?
@@ -245,6 +252,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
end end
def invalid_origin?(url)
return true if unsupported_uri_scheme?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@account.uri).host
!haystack.casecmp(needle).zero?
end
def reply_to_local? def reply_to_local?
!replied_to_status.nil? && replied_to_status.account.local? !replied_to_status.nil? && replied_to_status.account.local?
end end

View File

@@ -28,8 +28,6 @@ class ActivityPub::TagManager
return target.uri if target.respond_to?(:local?) && !target.local? return target.uri if target.respond_to?(:local?) && !target.local?
case target.object_type case target.object_type
when :follow
account_follow_url(target.account.username, target)
when :person when :person
account_url(target) account_url(target)
when :note, :comment, :activity when :note, :comment, :activity
@@ -69,6 +67,8 @@ class ActivityPub::TagManager
def cc(status) def cc(status)
cc = [] cc = []
cc << uri_for(status.reblog.account) if status.reblog?
case status.visibility case status.visibility
when 'public' when 'public'
cc << account_followers_url(status.account) cc << account_followers_url(status.account)
@@ -99,12 +99,6 @@ class ActivityPub::TagManager
case klass.name case klass.name
when 'Account' when 'Account'
klass.find_local(uri_to_local_id(uri, :username)) klass.find_local(uri_to_local_id(uri, :username))
when 'FollowRequest'
params = Rails.application.routes.recognize_path(uri)
klass.joins(:account).find_by!(
accounts: { domain: nil, username: params[:account_username] },
id: params[:id]
)
else else
StatusFinder.new(uri).status StatusFinder.new(uri).status
end end

View File

@@ -26,6 +26,9 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
cached_reblog = reblog cached_reblog = reblog
status = nil status = nil
# Skip if the reblogged status is not public
return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?)
media_attachments = save_media media_attachments = save_media
ApplicationRecord.transaction do ApplicationRecord.transaction do

View File

@@ -21,10 +21,6 @@ class FollowRequest < ApplicationRecord
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
def object_type
:follow
end
def authorize! def authorize!
account.follow!(target_account, reblogs: show_reblogs) account.follow!(target_account, reblogs: show_reblogs)
MergeWorker.perform_async(target_account.id, account.id) MergeWorker.perform_async(target_account.id, account.id)

View File

@@ -126,18 +126,18 @@ class User < ApplicationRecord
end end
def confirm def confirm
return if confirmed? new_user = !confirmed?
super super
update_statistics! update_statistics! if new_user
end end
def confirm! def confirm!
return if confirmed? new_user = !confirmed?
skip_confirmation! skip_confirmation!
save! save!
update_statistics! update_statistics! if new_user
end end
def promote! def promote!

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
attributes :id, :type, :actor
attribute :virtual_object, key: :object
def id
[ActivityPub::TagManager.instance.uri_for(object), '#delete'].join
end
def type
'Delete'
end
def actor
ActivityPub::TagManager.instance.uri_for(object)
end
def virtual_object
actor
end
end

View File

@@ -1,12 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::FollowSerializer < ActiveModel::Serializer class ActivityPub::FollowSerializer < ActiveModel::Serializer
attributes :type, :actor attributes :id, :type, :actor
attribute :id, if: :dereferencable?
attribute :virtual_object, key: :object attribute :virtual_object, key: :object
def id def id
ActivityPub::TagManager.instance.uri_for(object) [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join
end end
def type def type
@@ -20,8 +19,4 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer
def virtual_object def virtual_object
ActivityPub::TagManager.instance.uri_for(object.target_account) ActivityPub::TagManager.instance.uri_for(object.target_account)
end end
def dereferencable?
object.respond_to?(:object_type)
end
end end

View File

@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
end end
def thumbnail def thumbnail
full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
end end
def max_toot_chars def max_toot_chars

View File

@@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
# Should be called with confirmed valid JSON # Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain # and WebFinger-resolved username and domain
def call(username, domain, json) def call(username, domain, json)
return if json['inbox'].blank? return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
@json = json @json = json
@uri = @json['id'] @uri = @json['id']
@@ -107,7 +107,21 @@ class ActivityPub::ProcessAccountService < BaseService
def url def url
return if @json['url'].blank? return if @json['url'].blank?
url_to_href(@json['url'], 'text/html')
url_candidate = url_to_href(@json['url'], 'text/html')
if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
nil
else
url_candidate
end
end
def mismatching_origin?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@uri).host
!haystack.casecmp(needle).zero?
end end
def outbox_total_items def outbox_total_items

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