Compare commits

...

107 Commits

Author SHA1 Message Date
Eugen Rochko
559fd08845 Bump to 1.5.0 2017-08-01 15:12:07 +02:00
Jeroen
202942a76f Update Dutch (nl) strings (#4480) 2017-08-01 15:00:41 +02:00
ScienJus
c3e355388a Show SMTP_TLS in config sample (#4477) 2017-08-01 15:00:29 +02:00
Sorin Davidoi
d4c4820c03 fix(web_push_notifications): Send message with welcome notifications (#4471)
* fix(web_push_notifications): Send grouping title in welcome notification

* fix(web_push_notifications): Read message from last notification
2017-08-01 06:00:13 +02:00
Yamagishi Kazutoshi
e05606c8d0 Change logo URL for Heroku and Scalingo (#4476) 2017-08-01 05:59:11 +02:00
Yamagishi Kazutoshi
161f72cce3 Add libidn11 to Aptfile (#4475)
Fix builds that fail on Heroku.
2017-08-01 05:58:08 +02:00
Yamagishi Kazutoshi
8ccb3b96ab Re-add outline (#4474)
* Re-add outline

* respect default of web browser
2017-08-01 01:40:31 +02:00
Ondřej Hruška
e9ee249fd5 do not emojify tm, (R) and (C) (#4472) 2017-08-01 00:16:05 +02:00
Masoud Abkenar
4b6cd1dfdb i18n: update Persian translation (#4470)
* i18n: update Persian translation

* i18n: fix quotes
2017-07-31 19:40:57 +02:00
Yamagishi Kazutoshi
b9ec3b7e7c Load extra polyfills when isIntersecting is undefined (#4469) 2017-07-31 19:40:20 +02:00
Eugen Rochko
9b247c3d88 Bump to 1.5.0rc3 2017-07-31 15:28:36 +02:00
Eugen Rochko
c7cc806251 Simplify web UI character counter logic (#4463) 2017-07-31 15:19:48 +02:00
unarist
82b4cf4acb Fix button overflow on confirmation modal for mobile (#4465) 2017-07-31 15:19:30 +02:00
Yamagishi Kazutoshi
3e7a541e09 Change RuboCop rules to loose (#4464) 2017-07-31 15:19:13 +02:00
m4sk1n
93aafa8549 i18n: Update Polish translation (#4467) 2017-07-31 19:40:25 +09:00
Yamagishi Kazutoshi
bb85043f46 Disable sensitive button when with content warnings (#4460) 2017-07-31 05:06:56 +02:00
Eugen Rochko
e1fcad34a9 Fix length validator counting things that look like URIs like URLs (#4462)
URI.extract is too strong, not limited to URLs, matched real text.
Same issue was present in language detector.
2017-07-31 05:06:20 +02:00
unarist
155ba8fd3a Make salutation of email localizable (#4454) 2017-07-31 00:18:44 +02:00
Sorin Davidoi
e44f03bc71 Improve accessibility (part 7) (#4457)
* fix(media_modal): Keyboard navigation

* fix(column_back_button): Use native button

* fix(media_gallery): Keyboard navigation

* fix(status_content): Make CW content focusable
2017-07-31 00:18:15 +02:00
Yamagishi Kazutoshi
970297a138 Change to sensitive when adding content warning from web UI (#4456) 2017-07-30 16:27:57 +02:00
Yamagishi Kazutoshi
29abc9438c Remove outline from focused toot (#4448)
* Remove outline from focused toot

* change style
2017-07-30 16:27:51 +02:00
Yamagishi Kazutoshi
f91284d230 Do not create empty a element when there is no e-mail (#4455)
Empty a element is created when there is no business e-mail input.
2017-07-30 16:24:18 +02:00
unarist
feadf7553d Update Japanese translations (#4453) 2017-07-30 14:14:41 +02:00
Lynx Kotoura
ea33cdc30b Set contact address in about/more as mailto link (#4450)
* Set contact address as mailto link

To be able to copy long email address

* Set style for mailto link
2017-07-30 12:56:04 +02:00
masarakki
579e85f606 add-libpq-dev (#4445) 2017-07-30 01:05:27 +02:00
Damien Erambert
ea144ba302 Use a fainted text color for <hr> elements in the landing page (#4443)
* Use a fainted text color for <hr> elements in the landing page

* remove trailing whitespace
2017-07-29 21:20:52 +02:00
Sorin Davidoi
4f04981dde feat(tabs_bar): Avoid optimization for non-touch devices (#4444)
* fix(tabs_bar): Check if transition is necessary

* feat(tabs_bar): Only apply optimization for touch devices
2017-07-29 21:20:34 +02:00
Jeroen
990cea471e Dutch: Small fix (#4441) 2017-07-29 18:46:40 +02:00
Jeroen
0913351dcf Dutch: only one (new) string (#4440) 2017-07-29 23:54:27 +09:00
Lynx Kotoura
57a794d8eb Fix autocomplete option in haml files (#4438)
* Fix autocomplete in two_factor.html.haml

* Fix autocomplete in registrations edit.html.haml

* Fix autocomplete in passwords edit.html.haml

* Fix autocomplete in _registration.html.haml

* Fix autocomplete in new.html.haml

* Fix autocomplete in show.html.haml

* Add autocomplete option to sessions new.html.haml

* Add autocomplete option to 2FA new.html.haml

* Add autocomplete option to 2FA show.html.haml
2017-07-29 14:20:31 +02:00
unarist
a5e0cf2450 fix(status): Don't add tabIndex to wrapped status (#4437) 2017-07-29 14:20:05 +02:00
みたらしだんご
a46ba4a8f5 fix tabs_bar.js (#4436) 2017-07-29 14:19:49 +02:00
Sorin Davidoi
c71874b84c Improve accessibility (part 6) (#4435)
* fix(status_action_bar): Use aria-pressed for reblog and favourite button

* fix(column_back_button): Keyboard accessible

* fix(status_content): Make focusable and accessible

* fix(dropdown_menu): Use aria-expanded instead of aria-pressed

* fix(emoji_picker_dropdown): Use aria-expanded instead of aria-hidden

* feat(icon_button): Add aria-expanded

* fix(privacy_dropdown): Use aria-expanded instead of aria-hidden
2017-07-29 01:58:53 +02:00
Eugen Rochko
53b2b1b238 Count all URLs in text as 23 characters flat, do not count domain part of usernames (#4427)
* Count all URLs in text as 23 characters flat, do not count domain part of usernames

* Add new status text counting logic to web UI
2017-07-29 00:06:29 +02:00
Sorin Davidoi
634b71ed1d Accessability fixes (#4432)
* fix(modal_root): Read type from props, not from component

* fix(status_list): Do not user event.path
2017-07-28 22:55:19 +02:00
Sorin Davidoi
3d378ed0b4 fix(tabs_bar): Allow animation to end before navigating (#4429)
* fix(tabs_bar): Allow animation to end before navigating

* fix(tabs_bar): Do not use event.path
2017-07-28 22:55:13 +02:00
Sorin Davidoi
7e0c00a555 fix(mocha): Run all tests (#4433) 2017-07-28 22:53:16 +02:00
Eugen Rochko
f0bb2c6d1e Fix web push notifications "boost" icon not being loaded (regression from #4426) (#4431) 2017-07-28 20:26:54 +02:00
Eugen Rochko
13bb1ddc7f Use consistent icons for web push notifications, same as web UI (#4426) 2017-07-28 18:13:42 +02:00
Eugen Rochko
fdb65dcbee Bump to 1.5.0rc2 2017-07-28 17:22:41 +02:00
Eugen Rochko
4e2f2fab73 Fix guard clause in WebPushNotificationWorker (#4421) 2017-07-28 17:21:28 +02:00
Eugen Rochko
6e186b9c77 When PuSH subscribe attempts are exhausted, unsubscribe (#4422) 2017-07-28 17:21:14 +02:00
Quent-in
ff9d344d4c i18n update of OC version (#4425)
* Correction of misspellings

* Improve the OC version

* Improve of the OC version

* Improvement of OC version
2017-07-29 00:09:38 +09:00
Eugen Rochko
b3c44e95a9 Improve actions modal style (#4423) 2017-07-28 15:30:53 +02:00
Eugen Rochko
8c0dd33ce4 Fix current session not being displayed in sessions list (#4424) 2017-07-28 15:30:06 +02:00
Yamagishi Kazutoshi
12874eafa6 Unify webpackChunkName to lowercase (#4412) 2017-07-28 13:34:55 +02:00
Lynx Kotoura
afb593b44e Fix protruded infomation board section in about/more page (#4415)
* Fix protruded infomation board section

Set "flexwrap: wrap" for sections. Set upper and lower padding for each section, and adjust clearances around sections accordingly. Settings for viewport threshold 500px is no more needed.

* Fix mistake of reducing information-board padding

In according with this fix, additional padding setting  for maximum 840px width is no more needed.
2017-07-28 13:34:44 +02:00
Sorin Davidoi
296bfa23aa fix(actions-modal): Inconsistent margins (#4418) 2017-07-28 13:34:06 +02:00
Sorin Davidoi
534da4f24f Improve accessibility (part 5) (#4417)
* fix(status): Add tabIndex=0

* fix(status_list): Make keyboard navigable
2017-07-28 13:33:40 +02:00
Yamagishi Kazutoshi
62a9da62a6 Fix column header in landing page (regression from #4405) (#4416) 2017-07-28 13:32:41 +02:00
Yamagishi Kazutoshi
58eea59864 Fix broken PropTypes (#4413) 2017-07-28 13:32:05 +02:00
m4sk1n
c7de92e0df i18n: Update Polish translation (#4414)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-28 17:53:44 +09:00
Yamagishi Kazutoshi
c1633eeb0f Update webpack and related dependencies (without extract-text-webpack-plugin) (#4410) 2017-07-28 05:16:12 +02:00
Yamagishi Kazutoshi
f93f306053 Remove hash from chunk filename when dev env (#4411) 2017-07-28 05:14:01 +02:00
Sorin Davidoi
e67fc997dc feat(web_push_notifications): Group notifications (#4388) 2017-07-28 05:06:22 +02:00
Sorin Davidoi
3e01a7e677 fix(web_push_notification): Do not hard reload tab (#4380)
* fix(web_push_notification): Do not hard reload tab

* fix(web_push_notification_worker): Guard against null subscription

* refactor: Incorporate pull request feedback
2017-07-28 05:06:01 +02:00
Akihiko Odaki
0f92119ceb Cover Api::V1:FavouritesController more (#4409) 2017-07-28 04:37:44 +02:00
Sorin Davidoi
b7d47c2aef Improve accessibility (part 4) (#4408)
* fix(dropdown_menu): Keyboard navigation

* fix(icon_button): Add aria-pressed attribute

* fix(privacy_dropdown): Make accessible

* fix(emoji_picker_dropdown): Make accessible

* fix(icon_button): Support tabIndex

* fix(actions_modal): Remove icon from tab order

* fix(dropdown_menu): Add role=group

* fix(setting_toggle): Toggle via space key

* fix(dropdown_menu): Remove redundant handling of Space key

* fix(emoji_picker_dropdown): Remove redundant Space key handling

* fix(privacy_dropdown): Remove redundant Space key handling

* fix(status): Switch to article and add aria-posinset, aria-setsize

* fix(status_list): Use role=feed and pass more ARIA props to Status

* chore(eslint): jsx-a11y/role-supports-aria-props
2017-07-28 04:37:30 +02:00
Eugen Rochko
6270f9ce34 Fix #3189 - Share CSRF secret between main app and Sidekiq Web (#4383) 2017-07-28 04:36:42 +02:00
Satoshi KOJIMA
e54cc15cbd fix #4356 : place sw.js to assets/sw.js (#4357) 2017-07-28 01:55:52 +02:00
Ratmir Karabut
2654f3be82 Update Russian translation (#4407)
* Add Russian translation (ru)

* Fix a missing comma

* Fix the wording for better consistency

* Update Russian translation

* Arrange Russian setting alphabetically

* Fix syntax error

* Update Russian translation

* Fix formatting error

* Update Russian translation

* Update Russian translation

* Update ru.jsx

* Fix syntax error

* Remove two_factor_auth.warning (appears obsolete)

* Add missing strings in ru.yml

A lot of new strings translated, especially for the newly added admin section

* Fix translation consistency

* Update Russian translation

* Update Russian translation (pluralizations)

* Update Russian translation

* Update Russian translation

* Update Russian translation (pin)

* Update Russian translation (account deletion)

* Fix extra line

* Update Russian translation (sessions)

* Update Russian translation
2017-07-28 08:16:30 +09:00
Sorin Davidoi
9004151e34 feat: Web Share for detailed status and account (#4402)
* feat: Web Share for detailed status and account

* fix(account/action_bar): Move share under mention
2017-07-28 00:55:15 +02:00
Sorin Davidoi
6884dd79ba Improve accessibility (part 3) (#4405)
* fix(compose): Add aria-label for the navigation links

* fix(search): Add input label

* fix(navigation_bar): Link description

* fix(autosuggest_textarea): Add input label

* fix(compose_form): Add input label

* fix(upload_button): Add input label

* fix(account/header): Add link content

* fix(column_header): Use h1 tag

* fix(column_header): Labels move buttons moving column

* fix(settings_text): Add label to input

* fix(column_header): Remove role from h1

* fix(modal_root): Use role=dialog

* fix(modal_root): Focus restauration

* fix(modal_root): Apply inert to sibligs

* fix(column_header): Add role=button

* chore(eslint): Disable jsx-a11y/label-has-for
2017-07-28 00:54:48 +02:00
Sorin Davidoi
f9075577e4 fix(actions_modal): Warning about missing prop (#4406) 2017-07-27 23:01:50 +02:00
Sorin Davidoi
50d38d7605 fix(dropdown_menu): Open as modal on mobile (#4295)
* fix(dropdown_menu): Open as modal on mobile

* fix(dropdown_menu): Open modal on touch

* fix(dropdown_menu): Show status

* fix(dropdown_menu): Max dimensions and reduce padding

* chore(dropdown_menu): Test new functionality

* refactor: Use DropdownMenuContainer instead of DropdownMenu

* feat(privacy_dropdown): Open as modal on touch devices

* feat(modal_root): Do not load actions-modal async
2017-07-27 22:31:59 +02:00
Yamagishi Kazutoshi
aa803153e2 Remove Storybook (#4397) 2017-07-27 22:30:27 +02:00
Yamagishi Kazutoshi
f2233c3e25 Update intersection-observer to version 0.4.0 (#4398) 2017-07-27 16:20:48 +02:00
unarist
73890c3cac Fix timeline height on landing page for Safari (#4392)
`height: 100%` in `align-self: stretch` flexboxes doesn't work on Safari < 11.

https://bugs.webkit.org/show_bug.cgi?id=137730

This workaround uses flexbox instead of `height: 100%` to stretch height.
2017-07-27 15:49:56 +02:00
Sorin Davidoi
e1798d0eb0 fix(push_subscription): Use star icon for favourite action (#4396) 2017-07-27 15:46:33 +02:00
Akihiko Odaki
4f0b638cda Introduce access token fabricators (#4401) 2017-07-27 15:16:07 +02:00
Yamagishi Kazutoshi
bb96ba13cf Fallback to site_hostname when site_title is empty (#4394) 2017-07-27 15:13:49 +02:00
Daniel Hunsaker
5bf4838e2f [nanobox] Minor tweaks for 1.5 (#4395)
- Be less strict about the Ruby version, which resolves a build failure.
- Add libidn as a dependency (until Nanobox adds idn-ruby to the list of gems with a dependency on it).
- Remove redundant bundler commands (Nanobox's Ruby engine handles these things cleanly on its own, now).
2017-07-27 15:13:32 +02:00
Akihiko Odaki
bdf573d140 Remove redundant fabrication in the spec for Api::V1::FavouritesController (#4391) 2017-07-27 15:13:10 +02:00
Yamagishi Kazutoshi
97a48f237d Add default settings for user (#4393) 2017-07-27 15:12:51 +02:00
nullkal
6654c30033 Fix an error when a user tries to search nonexistent remote user (regression from #4275) (#4400) 2017-07-27 15:11:59 +02:00
Ondřej Hruška
f49339ca9c Fix multipoint shortcode bug (#4387)
* Fix multipoint shortcode bug

* Added testcase for composite emoji shortcode
2017-07-27 01:02:15 +02:00
Clworld
994d948c39 Add callback_url/acct information for Sidekiq PuSH workers Exception. (#4281)
* Add destination informations to exception on SubscribeWorker and DeliveryWorker.

* Simplify delivery error message.

* Prevent changing Exception type...

* fix typo.
2017-07-27 00:38:20 +02:00
Jeroen
f5e228ad2e Update Dutch strings (#4389) 2017-07-27 00:37:35 +02:00
Eugen Rochko
92cb451da8 Fix #4026 - Accept backup codes for disabling 2FA (#4382) 2017-07-26 23:36:33 +02:00
Eugen Rochko
55bee84c97 Fix infinite scroll fluidity (#4381) 2017-07-26 23:35:03 +02:00
Sorin Davidoi
a248be4fce fix(columns_area): Manually set tabs style when swiping (#4320) 2017-07-26 19:03:56 +02:00
Sorin Davidoi
8b43d6bf9c fix(web_push_notification_worker): Guard against deleted notifications (#4379) 2017-07-26 16:14:39 +02:00
Sorin Davidoi
b8adb4d7fa fix(column): Crash when heading is undefined (#4378) 2017-07-26 15:03:23 +02:00
mayaeh
4ba33f99fc Update Japanese translations (#4376)
* Update Japanese translations for remote profile, settings toggle button and web share button.

* Update Japanese translation for remote profile.
2017-07-26 21:57:51 +09:00
m4sk1n
7905739c2a s/PubSubHubbub/WebSub/g (#4372)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-26 13:47:41 +02:00
Sorin Davidoi
6a6a62f13f Improve accessibility (part 2) (#4377)
* fix(column_header): Invalid ARIA role

* fix(column): Remove hidden nodes from the DOM

* refactor(column_link): Remove unused property hideOnMobile

* fix(column_header): Use aria-pressed

* fix(column_header): Make collapsed content not focusable, add focusable property

* fix(column_loading): Make header non-focusable

* fix(column_settings): Use role to group the toggles
2017-07-26 13:46:53 +02:00
Lynx Kotoura
aa8fa71df6 Fix padding in hero container of landing page (#4373)
* Fix padding in hero container, landing page

Erase hero container padding to fit registration form to full width. By this setting, heading padding disappears, so I adjust heading padding.

* Specify attribute strictly for heading padding

To overwrite padding-bottom in superior settings.

* Make padding shorthand more concise
2017-07-26 13:45:49 +02:00
m4sk1n
7874c6d630 i18n: Update Polish translation (#4371) 2017-07-26 16:09:16 +09:00
Akihiko Odaki
7bf0afb1dc Add index favourites on account_id and id (#4360) 2017-07-26 03:35:25 +02:00
Sorin Davidoi
2f8bfb3d38 Improve accessibility (#4369)
* fix(compose): Use nav and remove redundant aria-label

* fix(tabs_tab): Use nav and add aria-label

* fix(app): Add aria-label for settings toggle button

* chore: Run yarn manage:translations
2017-07-26 02:01:27 +02:00
Ondřej Hruška
4115043dc7 Added external link icons to joinmastodon navbar buttons (#4368) 2017-07-26 02:00:36 +02:00
m4sk1n
7062cb764f i18n: Update Polish translation (#4367) 2017-07-25 23:56:04 +02:00
Lynx Kotoura
9891ff80f9 Adjust mobile landing page (#4366)
* Adjust mobile landing page

Change mobile viewport threshold to 840px in consideration of padding. Fix loss of "container hero" padding in about/more under 675px.

* Fix indent
2017-07-25 21:31:56 +02:00
Sorin Davidoi
7232cdf7e8 feat(status_action_bar): Web share button (#4365) 2017-07-25 20:38:39 +02:00
Lynx Kotoura
9f97c8c750 Fix multiple-gif display in user page (#4364)
Divide media-items to equal width, and hide overlaps.
2017-07-25 19:12:57 +02:00
Clworld
edadc93757 Allow longer link text on landing page (reduce padding for Mobile) (#4363)
* Allow longer link text on landing page (reduce padding for Mobile)

* remove empty line
2017-07-25 16:20:26 +02:00
Akihiko Odaki
a6ea7e282f Merge queries in Api::V1::FavouritesController (#4359) 2017-07-25 16:01:03 +02:00
Yamagishi Kazutoshi
e5c0aa6493 Send short account URL to remote instance (regression from #3844) (#4355)
* Send short account URL to remote instance (regression from #3844)

Resolve #4347

* remove format: 'json'
2017-07-25 15:59:43 +02:00
Clworld
02744f29ef Update Japanese translations for landing page (for Mobile). (#4362) 2017-07-25 22:57:40 +09:00
m4sk1n
a31d24ee18 i18n: Update Polish translation (#4361) 2017-07-25 22:39:49 +09:00
Komic
6957c5b5c6 Small fr.json update (#4354)
* Small fr.json update

• Translation for #4342
• Fixed a couple of typos as well as a few weird phrasings

* insert final newline
2017-07-25 12:01:47 +09:00
Sorin Davidoi
696bcff6bf fix(status_list): Guard against missing ref (#4353) 2017-07-25 02:13:05 +02:00
Jeroen
f52ce92f2b Update and (almost) completing Dutch strings for 1.5 (#4351)
* Update and expanding Dutch strings for 1.5

Only privacy policy is not translated, but is included. Hopefully I have time to translate this another time (you can set your own privacy policy now anyway).

* Missing file

* A few changes cause of changes in another file

* Fix

* Update Dutch strings for 1.5

* nl

* fix

don't know how that ended up there

* Update nl.yml
2017-07-25 02:12:50 +02:00
Petko Bordjukov
c80046a77b Update Capistrano lock version in config/deploy.rb (#4352)
Deployment with Capistrano was failing due to a mismatch between the loaded
version of Capistrano and the one specified in the deployment
configuration. Bumping the version in config/deploy.rb fixes this issue.
2017-07-25 01:14:01 +02:00
Sorin Davidoi
ebf5a06084 fix(column): Use passive wheel listener (#4348)
* chore(yarn): Install detect-passive-events

* fix(column): Use passive listener
2017-07-25 01:05:51 +02:00
Sorin Davidoi
23e854cb91 fix(components/bundle): Exit early in load function (#4349) 2017-07-25 01:05:44 +02:00
Sorin Davidoi
de105d64d5 fix(getting_started): No scrollbar if not needed (#4350) 2017-07-25 01:05:35 +02:00
217 changed files with 2311 additions and 1586 deletions

View File

@@ -4,7 +4,6 @@ public/system
public/assets
public/packs
node_modules
storybook
neo4j
vendor/bundle
.DS_Store

View File

@@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system

View File

@@ -112,7 +112,7 @@ rules:
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-has-alt: warn
jsx-a11y/img-redundant-alt: warn
jsx-a11y/label-has-for: warn
jsx-a11y/label-has-for: off
jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
@@ -121,6 +121,6 @@ rules:
jsx-a11y/onclick-has-focus: warn
jsx-a11y/onclick-has-role: warn
jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: warn
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@ public/system
public/assets
public/packs
public/packs-test
public/sw.js
.env
.env.production
node_modules/

View File

@@ -14,7 +14,6 @@ node_modules/
public/assets/
public/system/
spec/
storybook/
tmp/
.vagrant/
vendor/bundle/

View File

@@ -27,6 +27,7 @@ Metrics/AbcSize:
Max: 100
Metrics/BlockLength:
Max: 35
Exclude:
- 'lib/tasks/**/*'
@@ -35,10 +36,10 @@ Metrics/BlockNesting:
Metrics/ClassLength:
CountComments: false
Max: 200
Max: 300
Metrics/CyclomaticComplexity:
Max: 15
Max: 25
Metrics/LineLength:
AllowURI: true
@@ -53,11 +54,11 @@ Metrics/ModuleLength:
Max: 200
Metrics/ParameterLists:
Max: 4
Max: 5
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 10
Max: 20
Rails:
Enabled: true

View File

@@ -2,4 +2,3 @@ node_modules/
.cache/
docs/
spec/
storybook/

10
Aptfile
View File

@@ -1,7 +1,9 @@
protobuf-compiler
libprotobuf-dev
ffmpeg
libicu-dev
libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
libicu-dev
libidn11-dev
protobuf-compiler

View File

@@ -9,7 +9,7 @@
Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [WebSub](https://en.wikipedia.org/wiki/WebSub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
Click on the screenshot to watch a demo of the UI:
@@ -34,7 +34,7 @@ If you would like, you can [support the development of this project on Patreon][
## Features
- **Fully interoperable with GNU social and any OStatus platform**
Whatever implements Atom feeds, ActivityStreams, Salmon, PubSubHubbub and Webfinger is part of the network
Whatever implements Atom feeds, ActivityStreams, Salmon, WebSub and Webfinger is part of the network
- **Real-time timeline updates**
See the updates of people you're following appear in real-time in the UI via WebSockets
- **Federated thread resolving**

View File

@@ -2,7 +2,7 @@
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/javascript/images/logo.svg",
"logo": "https://github.com/tootsuite.png",
"env": {
"HEROKU": {
"description": "Leave this as true",

View File

@@ -20,9 +20,7 @@ class Api::V1::FavouritesController < Api::BaseController
def cached_favourites
cache_collection(
Status.where(
id: results.map(&:status_id)
),
Status.reorder(nil).joins(:favourites).merge(results),
Status
)
end

View File

@@ -18,7 +18,7 @@ module Settings
end
def destroy
if current_user.validate_and_consume_otp!(confirmation_params[:code])
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
@@ -38,5 +38,10 @@ module Settings
def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
end
end
end

View File

@@ -2,7 +2,7 @@
module InstanceHelper
def site_title
Setting.site_title.to_s
Setting.site_title.presence || site_hostname
end
def site_hostname

View File

@@ -162,20 +162,23 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return (
<div className='autosuggest-textarea'>
<Textarea
inputRef={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
/>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
inputRef={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map((suggestion, i) => (

View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import detectPassiveEvents from 'detect-passive-events';
import scrollTop from '../scroll';
export default class Column extends React.PureComponent {
@@ -30,11 +31,19 @@ export default class Column extends React.PureComponent {
this.node = c;
}
componentDidMount () {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents ? { passive: true } : false);
}
componentWillUnmount () {
this.node.removeEventListener('wheel', this.handleWheel);
}
render () {
const { children } = this.props;
return (
<div role='region' className='column' ref={this.setRef} onWheel={this.handleWheel}>
<div role='region' className='column' ref={this.setRef}>
{children}
</div>
);

View File

@@ -9,16 +9,19 @@ export default class ColumnBackButton extends React.PureComponent {
};
handleClick = () => {
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
}
render () {
return (
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
<button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</button>
);
}

View File

@@ -1,8 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
});
@injectIntl
export default class ColumnHeader extends React.PureComponent {
static contextTypes = {
@@ -10,10 +18,12 @@ export default class ColumnHeader extends React.PureComponent {
};
static propTypes = {
intl: PropTypes.object.isRequired,
title: PropTypes.node.isRequired,
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
multiColumn: PropTypes.bool,
focusable: PropTypes.bool,
showBackButton: PropTypes.bool,
children: PropTypes.node,
pinned: PropTypes.bool,
@@ -22,6 +32,10 @@ export default class ColumnHeader extends React.PureComponent {
onClick: PropTypes.func,
};
static defaultProps = {
focusable: true,
}
state = {
collapsed: true,
animating: false,
@@ -54,7 +68,7 @@ export default class ColumnHeader extends React.PureComponent {
}
render () {
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton } = this.props;
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
@@ -89,8 +103,8 @@ export default class ColumnHeader extends React.PureComponent {
moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
</div>
);
} else if (multiColumn) {
@@ -116,12 +130,12 @@ export default class ColumnHeader extends React.PureComponent {
}
if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
}
return (
<div className={wrapperClassName}>
<div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
<h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title}
@@ -129,9 +143,9 @@ export default class ColumnHeader extends React.PureComponent {
{backButton}
{collapseButton}
</div>
</div>
</h1>
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
<div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PropTypes from 'prop-types';
@@ -9,16 +10,23 @@ export default class DropdownMenu extends React.PureComponent {
};
static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
direction: PropTypes.string,
status: ImmutablePropTypes.map,
ariaLabel: PropTypes.string,
disabled: PropTypes.bool,
};
static defaultProps = {
ariaLabel: 'Menu',
isModalOpen: false,
isUserTouching: () => false,
};
state = {
@@ -34,6 +42,10 @@ export default class DropdownMenu extends React.PureComponent {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
if (this.props.isModalOpen) {
this.props.onModalClose();
}
// Don't call e.preventDefault() when the item uses 'href' property.
// ex. "Edit profile" on the account action bar
@@ -48,10 +60,32 @@ export default class DropdownMenu extends React.PureComponent {
this.dropdown.hide();
}
handleShow = () => this.setState({ expanded: true })
handleShow = () => {
if (this.props.isUserTouching()) {
this.props.onModalOpen({
status: this.props.status,
actions: this.props.items,
onClick: this.handleClick,
});
} else {
this.setState({ expanded: true });
}
}
handleHide = () => this.setState({ expanded: false })
handleToggle = (e) => {
if (e.key === 'Enter') {
if (this.props.isUserTouching()) {
this.handleShow();
} else {
this.setState({ expanded: !this.state.expanded });
}
} else if (e.key === 'Escape') {
this.setState({ expanded: false });
}
}
renderItem = (item, i) => {
if (item === null) {
return <li key={`sep-${i}`} className='dropdown__sep' />;
@@ -61,7 +95,7 @@ export default class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown__content-list-item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
{text}
</a>
</li>
@@ -71,6 +105,7 @@ export default class DropdownMenu extends React.PureComponent {
render () {
const { icon, items, size, direction, ariaLabel, disabled } = this.props;
const { expanded } = this.state;
const isUserTouching = this.props.isUserTouching();
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
@@ -84,20 +119,26 @@ export default class DropdownMenu extends React.PureComponent {
}
const dropdownItems = expanded && (
<ul className='dropdown__content-list'>
<ul role='group' className='dropdown__content-list' onClick={this.handleHide}>
{items.map(this.renderItem)}
</ul>
);
// No need to render the actual dropdown if we use the modal. If we
// don't render anything <Dropdow /> breaks, so we just put an empty div.
const dropdownContent = !isUserTouching ? (
<DropdownContent className={directionClass} >
{dropdownItems}
</DropdownContent>
) : <div />;
return (
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
<Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>
<DropdownContent className={directionClass}>
{dropdownItems}
</DropdownContent>
{dropdownContent}
</Dropdown>
);
}

View File

@@ -12,12 +12,15 @@ export default class IconButton extends React.PureComponent {
onClick: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
};
static defaultProps = {
@@ -26,6 +29,7 @@ export default class IconButton extends React.PureComponent {
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
};
handleClick = (e) => {
@@ -73,10 +77,13 @@ export default class IconButton extends React.PureComponent {
{({ rotate }) =>
<button
aria-label={this.props.title}
aria-pressed={this.props.pressed}
aria-expanded={this.props.expanded}
title={this.props.title}
className={classes.join(' ')}
onClick={this.handleClick}
style={style}
tabIndex={this.props.tabIndex}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
</button>

View File

@@ -212,10 +212,10 @@ export default class MediaGallery extends React.PureComponent {
}
children = (
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
<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>
</div>
</button>
);
} else {
const size = media.take(4).size;

View File

@@ -19,12 +19,15 @@ export default class SettingText extends React.PureComponent {
const { settings, settingKey, label } = this.props;
return (
<input
className='setting-text'
value={settings.getIn(settingKey)}
onChange={this.handleChange}
placeholder={label}
/>
<label>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text'
value={settings.getIn(settingKey)}
onChange={this.handleChange}
placeholder={label}
/>
</label>
);
}

View File

@@ -41,6 +41,8 @@ export default class Status extends ImmutablePureComponent {
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
state = {
@@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent {
'boostModal',
'autoPlayGif',
'muted',
'listLength',
]
updateOnStates = ['isExpanded']
@@ -67,8 +70,8 @@ export default class Status extends ImmutablePureComponent {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
return this.state.isIntersecting || !this.state.isHidden;
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
@@ -107,17 +110,12 @@ export default class Status extends ImmutablePureComponent {
this.height = getRectFromEntry(entry).height;
}
// Edge 15 doesn't support isIntersecting, but we can infer it
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
// https://github.com/WICG/IntersectionObserver/issues/211
const isIntersecting = (typeof entry.isIntersecting === 'boolean') ?
entry.isIntersecting : entry.intersectionRect.height > 0;
this.setState((prevState) => {
if (prevState.isIntersecting && !isIntersecting) {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: isIntersecting,
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
@@ -174,7 +172,7 @@ export default class Status extends ImmutablePureComponent {
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, ...other } = this.props;
const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) {
@@ -183,10 +181,10 @@ export default class Status extends ImmutablePureComponent {
if (!isIntersecting && isHidden) {
return (
<div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
</article>
);
}
@@ -200,14 +198,14 @@ export default class Status extends ImmutablePureComponent {
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} >
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
</div>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
</div>
</article>
);
}
@@ -236,7 +234,7 @@ export default class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'} ref={this.handleRef}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@@ -254,7 +252,7 @@ export default class Status extends ImmutablePureComponent {
{media}
<StatusActionBar {...this.props} />
</div>
</article>
);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -12,6 +12,7 @@ const messages = defineMessages({
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
share: { id: 'status.share', defaultMessage: 'Share' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
@@ -57,6 +58,13 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.props.onReply(this.props.status, this.context.router.history);
}
handleShareClick = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
}
@@ -136,14 +144,19 @@ export default class StatusActionBar extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll);
}
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
</div>
);

View File

@@ -146,7 +146,7 @@ export default class StatusContent extends React.PureComponent {
}
return (
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className={classNames} ref={this.setRef} tabIndex='0' aria-label={status.get('search_index')} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
@@ -155,13 +155,15 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder}
<div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
</div>
);
} else if (this.props.onClick) {
return (
<div
ref={this.setRef}
tabIndex='0'
aria-label={status.get('search_index')}
className={classNames}
style={directionStyle}
onMouseDown={this.handleMouseDown}
@@ -172,6 +174,8 @@ export default class StatusContent extends React.PureComponent {
} else {
return (
<div
tabIndex='0'
aria-label={status.get('search_index')}
ref={this.setRef}
className='status__content'
style={directionStyle}

View File

@@ -6,7 +6,7 @@ import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { debounce } from 'lodash';
import { throttle } from 'lodash';
export default class StatusList extends ImmutablePureComponent {
@@ -30,19 +30,21 @@ export default class StatusList extends ImmutablePureComponent {
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = debounce(() => {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 200, {
}, 150, {
trailing: true,
});
@@ -102,6 +104,32 @@ export default class StatusList extends ImmutablePureComponent {
this.props.onScrollToBottom();
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
@@ -111,11 +139,11 @@ export default class StatusList extends ImmutablePureComponent {
if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={this.setRef}>
<div className='status-list'>
<div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
{prepend}
{statusIds.map((statusId) => {
return <StatusContainer key={statusId} id={statusId} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
{statusIds.map((statusId, index) => {
return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
})}
{loadMore}

View File

@@ -0,0 +1,16 @@
import { openModal, closeModal } from '../actions/modal';
import { connect } from 'react-redux';
import DropdownMenu from '../components/dropdown_menu';
import { isUserTouching } from '../is_mobile';
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
});
const mapDispatchToProps = dispatch => ({
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View File

@@ -3,6 +3,8 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping));
const excluded = ['™', '©', '®'];
function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
// and replacing valid unicode strings
@@ -19,7 +21,7 @@ function emojify(str) {
insideTag = true;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in unicodeMapping) {
if (unicodeStr in unicodeMapping && excluded.indexOf(unicodeStr) === -1) {
const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import DropdownMenu from '../../../components/dropdown_menu';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import Link from 'react-router-dom/Link';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
@@ -15,6 +15,7 @@ const messages = defineMessages({
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
@@ -36,6 +37,12 @@ export default class ActionBar extends React.PureComponent {
intl: PropTypes.object.isRequired,
};
handleShare = () => {
navigator.share({
url: this.props.account.get('url'),
});
}
render () {
const { account, me, intl } = this.props;
@@ -43,6 +50,9 @@ export default class ActionBar extends React.PureComponent {
let extraInfo = '';
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
if ('share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
}
menu.push(null);
menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` });
menu.push(null);
@@ -96,7 +106,7 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'>
<div className='account__action-bar-dropdown'>
<DropdownMenu items={menu} icon='bars' size={24} direction='right' />
<DropdownMenuContainer items={menu} icon='bars' size={24} direction='right' />
</div>
<div className='account__action-bar-links'>

View File

@@ -52,9 +52,10 @@ class Avatar extends ImmutablePureComponent {
return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) =>
<a // eslint-disable-line jsx-a11y/anchor-has-content
<a
href={account.get('url')}
className='account__header__avatar'
role='presentation'
target='_blank'
rel='noopener'
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
@@ -62,7 +63,9 @@ class Avatar extends ImmutablePureComponent {
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut}
/>
>
<span style={{ display: 'none' }}>{account.get('acct')}</span>
</a>
}
</Motion>
);

View File

@@ -13,12 +13,12 @@ export default class CharacterCounter extends React.PureComponent {
if (diff < 0) {
return <span className='character-counter character-counter--over'>{diff}</span>;
}
return <span className='character-counter'>{diff}</span>;
}
render () {
const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff);
}

View File

@@ -18,6 +18,7 @@ import WarningContainer from '../containers/warning_container';
import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -145,9 +146,9 @@ export default class ComposeForm extends ImmutablePureComponent {
render () {
const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, this.props.text].join('');
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
let publishText = '';
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
@@ -159,7 +160,10 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form'>
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
</label>
</div>
</Collapsable>
@@ -200,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !==0 && text.trim().length === 0)} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
</div>
</div>

View File

@@ -65,6 +65,22 @@ export default class EmojiPickerDropdown extends React.PureComponent {
this.setState({ active: false });
}
onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
if (this.state.active) {
this.onHideDropdown();
} else {
this.onShowDropdown();
}
}
}
onEmojiPickerKeyDown = (e) => {
if (e.key === 'Escape') {
this.onHideDropdown();
}
}
render () {
const { intl } = this.props;
@@ -104,10 +120,11 @@ export default class EmojiPickerDropdown extends React.PureComponent {
};
const { active, loading } = this.state;
const title = intl.formatMessage(messages.emoji);
return (
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}>
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
<img
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
alt='🙂'
@@ -118,7 +135,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
<DropdownContent className='dropdown__left'>
{
this.state.active && !this.state.loading &&
(<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search />)
(<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />)
}
</DropdownContent>
</Dropdown>

View File

@@ -18,6 +18,7 @@ export default class NavigationBar extends ImmutablePureComponent {
return (
<div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar src={this.props.account.get('avatar')} animate size={40} />
</Permalink>

View File

@@ -24,6 +24,10 @@ const iconStyle = {
export default class PrivacyDropdown extends React.PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
@@ -34,14 +38,36 @@ export default class PrivacyDropdown extends React.PureComponent {
};
handleToggle = () => {
this.setState({ open: !this.state.open });
if (this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose();
} else {
this.props.onModalOpen({
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
onClick: this.handleModalActionClick,
});
}
} else {
this.setState({ open: !this.state.open });
}
}
handleModalActionClick = (e) => {
e.preventDefault();
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
this.props.onModalClose();
this.props.onChange(value);
}
handleClick = (e) => {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
if (e.key === 'Escape') {
this.setState({ open: false });
} else if (!e.key || e.key === 'Enter') {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
}
}
onGlobalClick = (e) => {
@@ -50,6 +76,17 @@ export default class PrivacyDropdown extends React.PureComponent {
}
}
componentWillMount () {
const { intl: { formatMessage } } = this.props;
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
];
}
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
@@ -68,25 +105,18 @@ export default class PrivacyDropdown extends React.PureComponent {
const { value, intl } = this.props;
const { open } = this.state;
const options = [
{ icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) },
{ icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) },
];
const valueOption = options.find(item => item.value === value);
const valueOption = this.options.find(item => item.value === value);
return (
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} expanded={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'>
{open && options.map(item =>
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
{open && this.options.map(item =>
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong>
{item.longText}
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
)}

View File

@@ -52,15 +52,18 @@ export default class Search extends React.PureComponent {
return (
<div className='search'>
<input
className='search__input'
type='text'
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyDown}
onFocus={this.handleFocus}
/>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
<input
className='search__input'
type='text'
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyDown}
onFocus={this.handleFocus}
/>
</label>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />

View File

@@ -57,16 +57,19 @@ export default class UploadButton extends ImmutablePureComponent {
return (
<div className='compose-form__upload-button'>
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
<input
key={resetFileKey}
ref={this.setRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}
/>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
<input
key={resetFileKey}
ref={this.setRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</label>
</div>
);
}

View File

@@ -1,8 +1,11 @@
import { connect } from 'react-redux';
import PrivacyDropdown from '../components/privacy_dropdown';
import { changeComposeVisibility } from '../../../actions/compose';
import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile';
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']),
});
@@ -12,6 +15,10 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeVisibility(value));
},
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
});
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);

View File

@@ -15,6 +15,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
visible: state.getIn(['compose', 'media_attachments']).size > 0,
active: state.getIn(['compose', 'sensitive']),
disabled: state.getIn(['compose', 'spoiler']),
});
const mapDispatchToProps = dispatch => ({
@@ -30,12 +31,13 @@ class SensitiveButton extends React.PureComponent {
static propTypes = {
visible: PropTypes.bool,
active: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { visible, active, onClick, intl } = this.props;
const { visible, active, disabled, onClick, intl } = this.props;
return (
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
@@ -53,6 +55,7 @@ class SensitiveButton extends React.PureComponent {
onClick={onClick}
size={18}
active={active}
disabled={disabled}
style={{ lineHeight: null, height: null }}
inverted
/>

View File

@@ -64,23 +64,23 @@ export default class Compose extends React.PureComponent {
if (multiColumn) {
const { columns } = this.props;
header = (
<div className='drawer__header'>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
<nav className='drawer__header'>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link>
{!columns.some(column => column.get('id') === 'HOME') && (
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link>
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link>
)}
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link>
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link>
)}
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link>
)}
{!columns.some(column => column.get('id') === 'PUBLIC') && (
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link>
)}
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role='img' aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
</div>
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
</nav>
);
}

View File

@@ -0,0 +1,7 @@
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(/https?:\/\/\S+/g, urlPlaceholder)
.replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
};

View File

@@ -36,40 +36,48 @@ export default class ColumnSettings extends React.PureComponent {
<ClearColumnButton onClick={onClear} />
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div role='group' aria-labelledby='notifications-follow'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div role='group' aria-labelledby='notifications-reblog'>
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>
);

View File

@@ -18,13 +18,19 @@ export default class SettingToggle extends React.PureComponent {
this.props.onChange(this.props.settingKey, target.checked);
}
onKeyDown = e => {
if (e.key === ' ') {
this.props.onChange(this.props.settingKey, !e.target.checked);
}
}
render () {
const { prefix, settings, settingKey, label, meta } = this.props;
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} />
<Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>}
</div>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenu from '../../../components/dropdown_menu';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
@@ -13,6 +13,7 @@ const messages = defineMessages({
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' },
});
@injectIntl
@@ -58,6 +59,13 @@ export default class ActionBar extends React.PureComponent {
this.props.onReport(this.props.status);
}
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
render () {
const { status, me, intl } = this.props;
@@ -71,6 +79,10 @@ export default class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
@@ -82,9 +94,10 @@ export default class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
{shareButton}
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenu size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' />
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' />
</div>
</div>
);

View File

@@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar';
import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
actions: PropTypes.array,
onClick: PropTypes.func,
};
renderAction = (action, i) => {
if (action === null) {
return <li key={`sep-${i}`} className='dropdown__sep' />;
}
const { icon = null, text, meta = null, active = false, href = '#' } = action;
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={active && 'active'}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />}
<div>
<div>{text}</div>
<div>{meta}</div>
</div>
</a>
</li>
);
}
render () {
const status = this.props.status && (
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
</a>
</div>
<a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
<Avatar src={this.props.status.getIn(['account', 'avatar'])} staticSrc={this.props.status.getIn(['account', 'avatar_static'])} size={48} />
</div>
<DisplayName account={this.props.status.get('account')} />
</a>
</div>
<StatusContent status={this.props.status} />
</div>
);
return (
<div className='modal-root__modal actions-modal'>
{status}
<ul>
{this.props.actions.map(this.renderAction)}
</ul>
</div>
);
}
}

View File

@@ -52,14 +52,8 @@ class Bundle extends React.Component {
load = (props) => {
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
this.setState({ mod: undefined });
onFetch();
if (renderDelay !== 0) {
this.timestamp = new Date();
this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
}
if (Bundle.cache[fetchComponent.name]) {
const mod = Bundle.cache[fetchComponent.name];
@@ -68,6 +62,13 @@ class Bundle extends React.Component {
return Promise.resolve();
}
this.setState({ mod: undefined });
if (renderDelay !== 0) {
this.timestamp = new Date();
this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
}
return fetchComponent()
.then((mod) => {
Bundle.cache[fetchComponent.name] = mod;

View File

@@ -3,6 +3,7 @@ import ColumnHeader from './column_header';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import scrollTop from '../../../scroll';
import { isMobile } from '../../../is_mobile';
export default class Column extends React.PureComponent {
@@ -37,13 +38,12 @@ export default class Column extends React.PureComponent {
render () {
const { heading, icon, children, active, hideHeadingOnMobile } = this.props;
let columnHeaderId = null;
let header = '';
const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth)));
if (heading) {
columnHeaderId = heading.replace(/ /g, '-');
header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId} />;
}
const columnHeaderId = showHeading && heading.replace(/ /g, '-');
const header = showHeading && (
<ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} />
);
return (
<div
ref={this.setRef}

View File

@@ -8,7 +8,6 @@ export default class ColumnHeader extends React.PureComponent {
type: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func,
hideOnMobile: PropTypes.bool,
columnHeaderId: PropTypes.string,
};
@@ -17,7 +16,7 @@ export default class ColumnHeader extends React.PureComponent {
}
render () {
const { type, active, hideOnMobile, columnHeaderId } = this.props;
const { type, active, columnHeaderId } = this.props;
let icon = '';
@@ -26,7 +25,7 @@ export default class ColumnHeader extends React.PureComponent {
}
return (
<div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}>
<div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}>
{icon}
{type}
</div>

View File

@@ -2,17 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import Link from 'react-router-dom/Link';
const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
const ColumnLink = ({ icon, text, to, href, method }) => {
if (href) {
return (
<a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
<a href={href} className='column-link' data-method={method}>
<i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text}
</a>
);
} else {
return (
<Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}>
<Link to={to} className='column-link'>
<i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text}
</Link>

View File

@@ -6,7 +6,7 @@ import ColumnHeader from '../../../components/column_header';
const ColumnLoading = ({ title = '', icon = ' ' }) => (
<Column>
<ColumnHeader icon={icon} title={title} multiColumn={false} />
<ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
<div className='scrollable' />
</Column>
);

View File

@@ -56,6 +56,15 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleSwipe = (index) => {
this.pendingIndex = index;
const nextLinkTranslationId = links[index].props['data-preview-title-id'];
const currentLinkSelector = '.tabs-bar__link.active';
const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
// HACK: Remove the active class from the current link and set it to the next one
// React-router does this for us, but too late, feeling laggy.
document.querySelector(currentLinkSelector).classList.remove('active');
document.querySelector(nextLinkSelector).classList.add('active');
}
handleAnimationEnd = () => {

View File

@@ -10,6 +10,8 @@ import ImageLoader from './image_loader';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
@injectIntl
@@ -66,16 +68,10 @@ export default class MediaModal extends ImmutablePureComponent {
const index = this.getIndex();
let leftNav, rightNav, content;
const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
leftNav = rightNav = content = '';
if (media.size > 1) {
leftNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
}
content = media.map((image) => {
const content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null;

View File

@@ -5,6 +5,7 @@ import spring from 'react-motion/lib/spring';
import BundleContainer from '../containers/bundle_container';
import BundleModalError from './bundle_modal_error';
import ModalLoading from './modal_loading';
import ActionsModal from '../components/actions_modal';
import {
MediaModal,
OnboardingModal,
@@ -21,6 +22,7 @@ const MODAL_COMPONENTS = {
'BOOST': BoostModal,
'CONFIRM': ConfirmationModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
};
export default class ModalRoot extends React.PureComponent {
@@ -42,10 +44,34 @@ export default class ModalRoot extends React.PureComponent {
window.addEventListener('keyup', this.handleKeyUp, false);
}
componentWillReceiveProps (nextProps) {
if (!!nextProps.type && !this.props.type) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
}
}
componentDidUpdate (prevProps) {
if (!this.props.type && !!prevProps.type) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.activeElement.focus();
this.activeElement = null;
}
}
componentWillUnmount () {
window.removeEventListener('keyup', this.handleKeyUp);
}
getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
}
setRef = ref => {
this.node = ref;
}
willEnter () {
return { opacity: 0, scale: 0.98 };
}
@@ -84,11 +110,11 @@ export default class ModalRoot extends React.PureComponent {
willLeave={this.willLeave}
>
{interpolatedStyles =>
<div className='modal-root'>
<div className='modal-root' ref={this.setRef}>
{interpolatedStyles.map(({ key, data: { type, props }, style }) => (
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<div role='dialog' className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
</BundleContainer>

View File

@@ -1,16 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import NavLink from 'react-router-dom/NavLink';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
export const links = [
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' activeClassName='active' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='tabs_bar.federated_timeline' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>,
<NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>,
];
export function getIndex (path) {
@@ -21,13 +24,60 @@ export function getLink (index) {
return links[index].props.to;
}
@injectIntl
export default class TabsBar extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = {
intl: PropTypes.object.isRequired,
}
setRef = ref => {
this.node = ref;
}
handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
if (isUserTouching()) {
e.preventDefault();
e.persist();
requestAnimationFrame(() => {
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
const currentTab = tabs.find(tab => tab.classList.contains('active'));
const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) {
if (currentTab) {
currentTab.classList.remove('active');
}
const listener = debounce(() => {
nextTab.removeEventListener('transitionend', listener);
this.context.router.history.push(to);
}, 50);
nextTab.addEventListener('transitionend', listener);
nextTab.classList.add('active');
}
});
}
}
render () {
const { intl: { formatMessage } } = this.props;
return (
<div className='tabs-bar'>
{React.Children.toArray(links)}
</div>
<nav className='tabs-bar' ref={this.setRef}>
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
</nav>
);
}

View File

@@ -49,6 +49,10 @@ const mapStateToProps = state => ({
@connect(mapStateToProps)
export default class UI extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
@@ -123,6 +127,14 @@ export default class UI extends React.PureComponent {
this.setState({ draggingOver: false });
}
handleServiceWorkerPostMessage = ({ data }) => {
if (data.type === 'navigate') {
this.context.router.history.push(data.path);
} else {
console.warn('Unknown message type:', data.type); // eslint-disable-line no-console
}
}
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
@@ -131,6 +143,10 @@ export default class UI extends React.PureComponent {
document.addEventListener('dragleave', this.handleDragLeave, false);
document.addEventListener('dragend', this.handleDragEnd, false);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
this.props.dispatch(refreshHomeTimeline());
this.props.dispatch(refreshNotifications());
}

View File

@@ -103,9 +103,9 @@ export function ReportModal () {
}
export function MediaGallery () {
return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery');
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
}
export function VideoPlayer () {
return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player');
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
}

View File

@@ -5,6 +5,15 @@ export function isMobile(width) {
};
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let userTouching = false;
window.addEventListener('touchstart', () => {
userTouching = true;
}, { once: true });
export function isUserTouching() {
return userTouching;
}
export function isIOS() {
return iOS;

View File

@@ -24,6 +24,8 @@ function loadPolyfills() {
// This avoids shipping them all the polyfills.
const needsExtraPolyfills = !(
window.IntersectionObserver &&
window.IntersectionObserverEntry &&
'isIntersecting' in IntersectionObserverEntry.prototype &&
window.requestIdleCallback &&
'object-fit' in (new Image()).style
);

View File

@@ -13,6 +13,7 @@
"account.posts": "المشاركات",
"account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة",
"account.share": "Share @{name}'s profile",
"account.unblock": "إلغاء الحظر عن @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "إلغاء المتابعة",
@@ -34,7 +35,11 @@
"column.notifications": "الإشعارات",
"column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "التصفح",
"column_subheading.settings": "الإعدادات",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "عرض الردود",
"home.settings": "إعدادات العمود",
"lightbox.close": "إغلاق",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "تحميل ...",
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه",
@@ -168,6 +175,7 @@
"status.report": "إبلِغ عن @{name}",
"status.sensitive_toggle": "اضغط للعرض",
"status.sensitive_warning": "محتوى حساس",
"status.share": "Share",
"status.show_less": "إعرض أقلّ",
"status.show_more": "أظهر المزيد",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Публикации",
"account.report": "Report @{name}",
"account.requested": "В очакване на одобрение",
"account.share": "Share @{name}'s profile",
"account.unblock": "Не блокирай",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Не следвай",
@@ -34,7 +35,11 @@
"column.notifications": "Известия",
"column.public": "Публичен канал",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Затвори",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Покажи",
"status.sensitive_warning": "Деликатно съдържание",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Publicacions",
"account.report": "Informe @{name}",
"account.requested": "Esperant aprovació",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquejar @{name}",
"account.unblock_domain": "Mostra {domain}",
"account.unfollow": "Deixar de seguir",
@@ -34,7 +35,11 @@
"column.notifications": "Notificacions",
"column.public": "Línia de temps federada",
"column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navegació",
"column_subheading.settings": "Configuració",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar respostes",
"home.settings": "Ajustos de columna",
"lightbox.close": "Tancar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",
@@ -168,6 +175,7 @@
"status.report": "Informar sobre @{name}",
"status.sensitive_toggle": "Clic per veure",
"status.sensitive_warning": "Contingut sensible",
"status.share": "Share",
"status.show_less": "Mostra menys",
"status.show_more": "Mostra més",
"status.unmute_conversation": "Activar conversació",

View File

@@ -13,6 +13,7 @@
"account.posts": "Beiträge",
"account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis",
"account.share": "Share @{name}'s profile",
"account.unblock": "@{name} entblocken",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Entfolgen",
@@ -34,7 +35,11 @@
"column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Antworten anzeigen",
"home.settings": "Spalteneinstellungen",
"lightbox.close": "Schließen",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden",
@@ -168,6 +175,7 @@
"status.report": "@{name} melden",
"status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.sensitive_warning": "Heikle Inhalte",
"status.share": "Share",
"status.show_less": "Weniger anzeigen",
"status.show_more": "Mehr anzeigen",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -53,6 +53,22 @@
},
{
"descriptors": [
{
"defaultMessage": "Show settings",
"id": "column_header.show_settings"
},
{
"defaultMessage": "Hide settings",
"id": "column_header.hide_settings"
},
{
"defaultMessage": "Move column to the left",
"id": "column_header.moveLeft_settings"
},
{
"defaultMessage": "Move column to the right",
"id": "column_header.moveRight_settings"
},
{
"defaultMessage": "Unpin",
"id": "column_header.unpin"
@@ -138,6 +154,10 @@
"defaultMessage": "Reply",
"id": "status.reply"
},
{
"defaultMessage": "Share",
"id": "status.share"
},
{
"defaultMessage": "Reply to thread",
"id": "status.replyAll"
@@ -354,6 +374,10 @@
"defaultMessage": "Report @{name}",
"id": "account.report"
},
{
"defaultMessage": "Share @{name}'s profile",
"id": "account.share"
},
{
"defaultMessage": "Media",
"id": "account.media"
@@ -1007,6 +1031,10 @@
{
"defaultMessage": "Report @{name}",
"id": "status.report"
},
{
"defaultMessage": "Share",
"id": "status.share"
}
],
"path": "app/javascript/mastodon/features/status/components/action_bar.json"
@@ -1085,6 +1113,14 @@
{
"defaultMessage": "Close",
"id": "lightbox.close"
},
{
"defaultMessage": "Previous",
"id": "lightbox.previous"
},
{
"defaultMessage": "Next",
"id": "lightbox.next"
}
],
"path": "app/javascript/mastodon/features/ui/components/media_modal.json"

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow",
@@ -34,7 +35,11 @@
"column.notifications": "Notifications",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Mesaĝoj",
"account.report": "Report @{name}",
"account.requested": "Atendas aprobon",
"account.share": "Share @{name}'s profile",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi",
@@ -34,7 +35,11 @@
"column.notifications": "Sciigoj",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Fermi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ŝarĝanta...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Alklaki por vidi",
"status.sensitive_warning": "Tikla enhavo",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Publicaciones",
"account.report": "Report @{name}",
"account.requested": "Esperando aprobación",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquear",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Dejar de seguir",
@@ -34,7 +35,11 @@
"column.notifications": "Notificaciones",
"column.public": "Historia federada",
"column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Cerrar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Reportar",
"status.sensitive_toggle": "Click para ver",
"status.sensitive_warning": "Contenido sensible",
"status.share": "Share",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,18 +13,19 @@
"account.posts": "نوشته‌ها",
"account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش",
"account.share": "Share @{name}'s profile",
"account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "رفع پنهان‌سازی از {domain}",
"account.unfollow": "پایان پیگیری",
"account.unmute": "باصدا کردن @{name}",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "نمایش نمایهٔ کامل",
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
"bundle_column_error.retry": "تلاش دوباره",
"bundle_column_error.title": "خطای شبکه",
"bundle_modal_error.close": "بستن",
"bundle_modal_error.message": "هنگام بازکردن این بخش خطایی رخ داد.",
"bundle_modal_error.retry": "تلاش دوباره",
"column.blocks": "کاربران مسدودشده",
"column.community": "نوشته‌های محلی",
"column.favourites": "پسندیده‌ها",
@@ -34,8 +35,12 @@
"column.notifications": "اعلان‌ها",
"column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت",
"column_header.pin": "Pin",
"column_header.unpin": "Unpin",
"column_header.hide_settings": "نهفتن تنظیمات",
"column_header.moveLeft_settings": "انتقال ستون به چپ",
"column_header.moveRight_settings": "انتقال ستون به راست",
"column_header.pin": "ثابت‌کردن",
"column_header.show_settings": "نمایش تنظیمات",
"column_header.unpin": "رهاکردن",
"column_subheading.navigation": "گشت و گذار",
"column_subheading.settings": "تنظیمات",
"compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.",
@@ -56,8 +61,8 @@
"confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
"confirmations.mute.confirm": "بی‌صدا کن",
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.confirm": "لغو پیگیری",
"confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
"emoji_button.activity": "فعالیت",
"emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.settings": "تنظیمات ستون",
"lightbox.close": "بستن",
"lightbox.next": "بعدی",
"lightbox.previous": "قبلی",
"loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد",
@@ -112,8 +119,8 @@
"notifications.column_settings.favourite": "پسندیده‌ها:",
"notifications.column_settings.follow": "پیگیران تازه:",
"notifications.column_settings.mention": "نام‌بردن‌ها:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "اعلان‌ها از سمت سرور",
"notifications.column_settings.push_meta": "این دستگاه",
"notifications.column_settings.reblog": "بازبوق‌ها:",
"notifications.column_settings.show": "نمایش در ستون",
"notifications.column_settings.sound": "پخش صدا",
@@ -152,7 +159,7 @@
"report.target": "گزارش‌دادن",
"search.placeholder": "جستجو",
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
"standalone.public_title": "A look inside...",
"standalone.public_title": "نگاهی به کاربران این سرور...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن",
"status.favourite": "پسندیدن",
@@ -168,6 +175,7 @@
"status.report": "گزارش دادن @{name}",
"status.sensitive_toggle": "برای دیدن کلیک کنید",
"status.sensitive_warning": "محتوای حساس",
"status.share": "هم‌رسانی",
"status.show_less": "نهفتن",
"status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو",

View File

@@ -13,6 +13,7 @@
"account.posts": "Postit",
"account.report": "Report @{name}",
"account.requested": "Odottaa hyväksyntää",
"account.share": "Share @{name}'s profile",
"account.unblock": "Salli @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Lopeta seuraaminen",
@@ -34,7 +35,11 @@
"column.notifications": "Ilmoitukset",
"column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Sulje",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ladataan...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Klikkaa nähdäksesi",
"status.sensitive_warning": "Arkaluontoista sisältöä",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -1,7 +1,7 @@
{
"account.block": "Bloquer",
"account.block_domain": "Tout masquer de {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
"account.edit_profile": "Modifier le profil",
"account.follow": "Suivre",
"account.followers": "Abonné⋅e⋅s",
@@ -13,11 +13,12 @@
"account.posts": "Statuts",
"account.report": "Signaler",
"account.requested": "Invitation envoyée",
"account.share": "Share @{name}'s profile",
"account.unblock": "Débloquer",
"account.unblock_domain": "Ne plus masquer {domain}",
"account.unfollow": "Ne plus suivre",
"account.unmute": "Ne plus masquer",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "Afficher le profil complet",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.",
"bundle_column_error.retry": "Réessayer",
@@ -34,7 +35,11 @@
"column.notifications": "Notifications",
"column.public": "Fil public global",
"column_back_button.label": "Retour",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Épingler",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Retirer",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Paramètres",
@@ -42,9 +47,9 @@
"compose_form.lock_disclaimer.lock": "verrouillé",
"compose_form.placeholder": "Quavez-vous en tête?",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {nest pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il ny aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible dune autre manière à dautres personnes imprévues.",
"compose_form.publish": "Pouet ",
"compose_form.publish": "Pouet ",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marquer le média comme délicat",
"compose_form.sensitive": "Marquer le média comme sensible",
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
"compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
"confirmation_modal.cancel": "Annuler",
@@ -69,13 +74,13 @@
"emoji_button.symbols": "Symboles",
"emoji_button.travel": "Lieux et voyages",
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir!",
"empty_column.hashtag": "Il ny a encore aucun contenu relatif à ce hashtag",
"empty_column.hashtag": "Il ny a encore aucun contenu associé à ce hashtag",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à dautres utilisateur⋅ice⋅s.",
"empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres utilisateur⋅ice⋅s pour débuter la conversation.",
"empty_column.public": "Il ny a rien ici! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s dautres instances pour remplir le fil public.",
"follow_request.authorize": "Autoriser",
"follow_request.authorize": "Accepter",
"follow_request.reject": "Rejeter",
"getting_started.appsshort": "Applications",
"getting_started.faq": "FAQ",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Afficher les réponses",
"home.settings": "Paramètres de la colonne",
"lightbox.close": "Fermer",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Chargement…",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",
@@ -127,8 +134,8 @@
"onboarding.page_one.welcome": "Bienvenue sur Mastodon!",
"onboarding.page_six.admin": "Ladministrateur⋅trice de votre instance est {admin}",
"onboarding.page_six.almost_done": "Nous y sommes presque…",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!",
"onboarding.page_six.appetoot": "Bon Appétoot!",
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appétoot!",
"onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
"onboarding.page_six.guidelines": "règles de la communauté",
"onboarding.page_six.read_guidelines": "Sil vous plaît, noubliez pas de lire les {guidelines}!",
@@ -152,7 +159,7 @@
"report.target": "Signalement",
"search.placeholder": "Rechercher",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"standalone.public_title": "Coup d'oeil",
"standalone.public_title": "Coup d'œil",
"status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer",
"status.favourite": "Ajouter aux favoris",
@@ -166,8 +173,9 @@
"status.reply": "Répondre",
"status.replyAll": "Répondre au fil",
"status.report": "Signaler @{name}",
"status.sensitive_toggle": "Cliquer pour dévoiler",
"status.sensitive_warning": "Contenu délicat",
"status.sensitive_toggle": "Cliquer pour afficher",
"status.sensitive_warning": "Contenu sensible",
"status.share": "Share",
"status.show_less": "Replier",
"status.show_more": "Déplier",
"status.unmute_conversation": "Ne plus masquer la conversation",
@@ -181,7 +189,7 @@
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"video_player.expand": "Agrandir la vidéo",
"video_player.toggle_sound": "Mettre/Couper le son",
"video_player.toggle_sound": "Activer/Désactiver le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
"video_player.video_error": "Erreur lors de la lecture de la vidéo"
}

View File

@@ -13,6 +13,7 @@
"account.posts": "הודעות",
"account.report": "לדווח על @{name}",
"account.requested": "בהמתנה לאישור",
"account.share": "Share @{name}'s profile",
"account.unblock": "הסרת חסימה מעל @{name}",
"account.unblock_domain": "הסר חסימה מקהילת {domain}",
"account.unfollow": "הפסקת מעקב",
@@ -34,7 +35,11 @@
"column.notifications": "התראות",
"column.public": "בפרהסיה",
"column_back_button.label": "חזרה",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "ניווט",
"column_subheading.settings": "אפשרויות",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "הצגת תגובות",
"home.settings": "הגדרות טור",
"lightbox.close": "סגירה",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "טוען...",
"media_gallery.toggle_visible": "נראה\\בלתי נראה",
"missing_indicator.label": "לא נמצא",
@@ -168,6 +175,7 @@
"status.report": "דיווח על @{name}",
"status.sensitive_toggle": "לחצו כדי לראות",
"status.sensitive_warning": "תוכן רגיש",
"status.share": "Share",
"status.show_less": "הראה פחות",
"status.show_more": "הראה יותר",
"status.unmute_conversation": "הסרת השתקת שיחה",

View File

@@ -13,6 +13,7 @@
"account.posts": "Postovi",
"account.report": "Prijavi @{name}",
"account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}",
"account.unfollow": "Prestani slijediti",
@@ -34,7 +35,11 @@
"column.notifications": "Notifikacije",
"column.public": "Federalni timeline",
"column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigacija",
"column_subheading.settings": "Postavke",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca",
"lightbox.close": "Zatvori",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Učitavam...",
"media_gallery.toggle_visible": "Preklopi vidljivost",
"missing_indicator.label": "Nije nađen",
@@ -168,6 +175,7 @@
"status.report": "Prijavi @{name}",
"status.sensitive_toggle": "Klikni da bi vidio",
"status.sensitive_warning": "Osjetljiv sadržaj",
"status.share": "Share",
"status.show_less": "Pokaži manje",
"status.show_more": "Pokaži više",
"status.unmute_conversation": "Poništi utišavanje razgovora",

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Blokkolás levétele",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Követés abbahagyása",
@@ -34,7 +35,11 @@
"column.notifications": "Értesítések",
"column.public": "Nyilvános",
"column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Bezárás",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Betöltés...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Katt a megtekintéshez",
"status.sensitive_warning": "Érzékeny tartalom",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Postingan",
"account.report": "Laporkan @{name}",
"account.requested": "Menunggu persetujuan",
"account.share": "Share @{name}'s profile",
"account.unblock": "Hapus blokir @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Berhenti mengikuti",
@@ -34,7 +35,11 @@
"column.notifications": "Notifikasi",
"column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasi",
"column_subheading.settings": "Pengaturan",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Tampilkan balasan",
"home.settings": "Pengaturan kolom",
"lightbox.close": "Tutup",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Tunggu sebentar...",
"media_gallery.toggle_visible": "Tampil/Sembunyikan",
"missing_indicator.label": "Tidak ditemukan",
@@ -168,6 +175,7 @@
"status.report": "Laporkan @{name}",
"status.sensitive_toggle": "Klik untuk menampilkan",
"status.sensitive_warning": "Konten sensitif",
"status.share": "Share",
"status.show_less": "Tampilkan lebih sedikit",
"status.show_more": "Tampilkan semua",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Mesaji",
"account.report": "Denuncar @{name}",
"account.requested": "Vartante aprobo",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Ne plus sequar",
@@ -34,7 +35,11 @@
"column.notifications": "Savigi",
"column.public": "Federata tempolineo",
"column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Montrar respondi",
"home.settings": "Aranji di la kolumno",
"lightbox.close": "Klozar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Kargante...",
"media_gallery.toggle_visible": "Chanjar videbleso",
"missing_indicator.label": "Ne trovita",
@@ -168,6 +175,7 @@
"status.report": "Denuncar @{name}",
"status.sensitive_toggle": "Kliktar por vidar",
"status.sensitive_warning": "Trubliva kontenajo",
"status.share": "Share",
"status.show_less": "Montrar mine",
"status.show_more": "Montrar plue",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Segnala @{name}",
"account.requested": "In attesa di approvazione",
"account.share": "Share @{name}'s profile",
"account.unblock": "Sblocca @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Non seguire",
@@ -34,7 +35,11 @@
"column.notifications": "Notifiche",
"column.public": "Timeline federata",
"column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostra risposte",
"home.settings": "Impostazioni colonna",
"lightbox.close": "Chiudi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carico...",
"media_gallery.toggle_visible": "Imposta visibilità",
"missing_indicator.label": "Non trovato",
@@ -168,6 +175,7 @@
"status.report": "Segnala @{name}",
"status.sensitive_toggle": "Clicca per vedere",
"status.sensitive_warning": "Materiale sensibile",
"status.share": "Share",
"status.show_less": "Mostra meno",
"status.show_more": "Mostra di più",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -1,7 +1,7 @@
{
"account.block": "ブロック",
"account.block_domain": "{domain}全体を非表示",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "以下の情報は不正確な可能性があります。",
"account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー",
"account.followers": "フォロワー",
@@ -13,11 +13,12 @@
"account.posts": "投稿",
"account.report": "通報",
"account.requested": "承認待ち",
"account.share": "@{name} のプロフィールを共有する",
"account.unblock": "ブロック解除",
"account.unblock_domain": "{domain}を表示",
"account.unfollow": "フォロー解除",
"account.unmute": "ミュート解除",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "全ての情報を見る",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
@@ -34,7 +35,11 @@
"column.notifications": "通知",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する",
"column_header.moveRight_settings": "カラムを右に移動する",
"column_header.pin": "ピン留めする",
"column_header.show_settings": "設定を表示",
"column_header.unpin": "ピン留めを外す",
"column_subheading.navigation": "ナビゲーション",
"column_subheading.settings": "設定",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定",
"lightbox.close": "閉じる",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
@@ -149,7 +156,7 @@
"reply_indicator.cancel": "キャンセル",
"report.placeholder": "コメント",
"report.submit": "通報する",
"report.target": "問題のユーザー",
"report.target": "{target} を通報する",
"search.placeholder": "検索",
"search_results.total": "{count, number}件の結果",
"standalone.public_title": "連合タイムライン",
@@ -168,6 +175,7 @@
"status.report": "通報",
"status.sensitive_toggle": "クリックして表示",
"status.sensitive_warning": "閲覧注意",
"status.share": "共有",
"status.show_less": "隠す",
"status.show_more": "もっと見る",
"status.unmute_conversation": "会話のミュートを解除",

View File

@@ -13,6 +13,7 @@
"account.posts": "포스트",
"account.report": "신고",
"account.requested": "승인 대기 중",
"account.share": "Share @{name}'s profile",
"account.unblock": "차단 해제",
"account.unblock_domain": "{domain} 숨김 해제",
"account.unfollow": "팔로우 해제",
@@ -34,7 +35,11 @@
"column.notifications": "알림",
"column.public": "연합 타임라인",
"column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "고정하기",
"column_header.show_settings": "Show settings",
"column_header.unpin": "고정 해제",
"column_subheading.navigation": "내비게이션",
"column_subheading.settings": "설정",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "답글 표시",
"home.settings": "컬럼 설정",
"lightbox.close": "닫기",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다",
@@ -168,6 +175,7 @@
"status.report": "신고",
"status.sensitive_toggle": "클릭해서 표시하기",
"status.sensitive_warning": "민감한 미디어",
"status.share": "Share",
"status.show_less": "숨기기",
"status.show_more": "더 보기",
"status.unmute_conversation": "이 대화의 뮤트 해제하기",

View File

@@ -1,7 +1,7 @@
{
"account.block": "Blokkeer @{name}",
"account.block_domain": "Negeer alles van {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "De informatie hieronder kan mogelijk een incompleet beeld geven van dit gebruikersprofiel.",
"account.edit_profile": "Profiel bewerken",
"account.follow": "Volgen",
"account.followers": "Volgers",
@@ -13,18 +13,19 @@
"account.posts": "Toots",
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring",
"account.share": "Profiel van @{name} delen",
"account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen",
"account.unmute": "@{name} niet meer negeren",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "Volledig profiel tonen",
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
"bundle_column_error.retry": "Opnieuw proberen",
"bundle_column_error.title": "Netwerkfout",
"bundle_modal_error.close": "Sluiten",
"bundle_modal_error.message": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
"bundle_modal_error.retry": "Opnieuw proberen",
"column.blocks": "Geblokkeerde gebruikers",
"column.community": "Lokale tijdlijn",
"column.favourites": "Favorieten",
@@ -34,7 +35,11 @@
"column.notifications": "Meldingen",
"column.public": "Globale tijdlijn",
"column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Vastmaken",
"column_header.show_settings": "Instellingen tonen",
"column_header.unpin": "Losmaken",
"column_subheading.navigation": "Navigatie",
"column_subheading.settings": "Instellingen",
@@ -44,20 +49,20 @@
"compose_form.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {die server} other {die servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Wanneer {domains} {domainsCount, plural, one {geen Mastodon-server is} other {geen Mastodon-servers zijn}}, dan wordt er niet aangegeven dat de toot privé is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Media als gevoelig markeren",
"compose_form.sensitive": "Media als gevoelig markeren (nsfw)",
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
"compose_form.spoiler_placeholder": "Waarschuwingstekst",
"confirmation_modal.cancel": "Annuleren",
"confirmations.block.confirm": "Blokkeren",
"confirmations.block.message": "Weet je zeker dat je {name} wilt blokkeren?",
"confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
"confirmations.delete.confirm": "Verwijderen",
"confirmations.delete.message": "Weet je zeker dat je deze toot wilt verwijderen?",
"confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
"confirmations.domain_block.confirm": "Negeer alles van deze server",
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
"confirmations.mute.confirm": "Negeren",
"confirmations.mute.message": "Weet je zeker dat je {name} wilt negeren?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
"confirmations.unfollow.confirm": "Ontvolgen",
"confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
"emoji_button.activity": "Activiteiten",
"emoji_button.flags": "Vlaggen",
"emoji_button.food": "Eten en drinken",
@@ -68,7 +73,7 @@
"emoji_button.search": "Zoeken...",
"emoji_button.symbols": "Symbolen",
"emoji_button.travel": "Reizen en plekken",
"empty_column.community": "De lokale tijdlijn is leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
"empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
"empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
"empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
"empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Reacties tonen",
"home.settings": "Kolom-instellingen",
"lightbox.close": "Sluiten",
"lightbox.next": "Volgende",
"lightbox.previous": "Vorige",
"loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media wel/niet tonen",
"missing_indicator.label": "Niet gevonden",
@@ -107,13 +114,13 @@
"notification.mention": "{name} vermeldde jou",
"notification.reblog": "{name} boostte jouw toot",
"notifications.clear": "Meldingen verwijderen",
"notifications.clear_confirmation": "Weet je zeker dat je al jouw meldingen wilt verwijderen?",
"notifications.clear_confirmation": "Weet je het zeker dat je al jouw meldingen wilt verwijderen?",
"notifications.column_settings.alert": "Desktopmeldingen",
"notifications.column_settings.favourite": "Favorieten:",
"notifications.column_settings.follow": "Nieuwe volgers:",
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "Pushmeldingen",
"notifications.column_settings.push_meta": "Dit apparaat",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "In kolom tonen",
"notifications.column_settings.sound": "Geluid afspelen",
@@ -152,7 +159,7 @@
"report.target": "Rapporteren van",
"search.placeholder": "Zoeken",
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
"standalone.public_title": "A look inside...",
"standalone.public_title": "Een kijkje binnenin...",
"status.cannot_reblog": "Deze toot kan niet geboost worden",
"status.delete": "Verwijderen",
"status.favourite": "Favoriet",
@@ -166,8 +173,9 @@
"status.reply": "Reageren",
"status.replyAll": "Reageer op iedereen",
"status.report": "Rapporteer @{name}",
"status.sensitive_toggle": "Klik om te zien",
"status.sensitive_toggle": "Klik om te bekijken",
"status.sensitive_warning": "Gevoelige inhoud",
"status.share": "Delen",
"status.show_less": "Minder tonen",
"status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren",

View File

@@ -13,6 +13,7 @@
"account.posts": "Innlegg",
"account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse",
"account.share": "Share @{name}'s profile",
"account.unblock": "Avblokker @{name}",
"account.unblock_domain": "Vis {domain}",
"account.unfollow": "Avfølg",
@@ -34,7 +35,11 @@
"column.notifications": "Varsler",
"column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasjon",
"column_subheading.settings": "Innstillinger",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Vis svar",
"home.settings": "Kolonneinnstillinger",
"lightbox.close": "Lukk",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Laster...",
"media_gallery.toggle_visible": "Veksle synlighet",
"missing_indicator.label": "Ikke funnet",
@@ -168,6 +175,7 @@
"status.report": "Rapporter @{name}",
"status.sensitive_toggle": "Klikk for å vise",
"status.sensitive_warning": "Følsomt innhold",
"status.share": "Share",
"status.show_less": "Vis mindre",
"status.show_more": "Vis mer",
"status.unmute_conversation": "Ikke demp samtale",

View File

@@ -1,7 +1,7 @@
{
"account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.",
"account.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre",
"account.followers": "Seguidors",
@@ -13,18 +13,19 @@
"account.posts": "Estatuts",
"account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada",
"account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre @{name}",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "Veire lo perfil complet",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
"bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_column_error.retry": "Tornar ensejar",
"bundle_column_error.retry": "Tornar ensajar",
"bundle_column_error.title": "Error de ret",
"bundle_modal_error.close": "Tampar",
"bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensejar",
"bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensajar",
"column.blocks": "Personas blocadas",
"column.community": "Flux public local",
"column.favourites": "Favorits",
@@ -34,7 +35,11 @@
"column.notifications": "Notificacions",
"column.public": "Flux public global",
"column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
"column_header.moveRight_settings": "Desplaçar la colomna a man esquèrra",
"column_header.pin": "Penjar",
"column_header.show_settings": "Mostrar los paramètres",
"column_header.unpin": "Despenjar",
"column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres",
@@ -46,35 +51,35 @@
"compose_form.publish_loud": "{publish} !",
"compose_form.sensitive": "Marcar lo mèdia coma sensible",
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
"compose_form.spoiler_placeholder": "Avertiment",
"compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí",
"confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
"confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
"emoji_button.activity": "Activitat",
"emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar",
"emoji_button.label": "Inserir un emoji",
"emoji_button.nature": "Natura",
"emoji_button.objects": "Objèctes",
"emoji_button.people": "Gents",
"emoji_button.search": "Cercar...",
"emoji_button.search": "Cercar",
"emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Pel moment segètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
"empty_column.home.public_timeline": "lo flux public",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
"empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar las responsas",
"home.settings": "Paramètres de la colomna",
"lightbox.close": "Tampar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat",
"missing_indicator.label": "Pas trobat",
@@ -103,11 +110,11 @@
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :",
"notification.follow": "{name} vos sèc.",
"notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat :",
"notification.reblog": "{name} a partejat vòstre estatut :",
"notifications.clear": "Levar",
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?",
"notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus seguidors :",
@@ -119,15 +126,15 @@
"notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach",
"onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun enteragís amb vos",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.",
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
"onboarding.page_six.guidelines": "guida de la comunitat",
@@ -168,6 +175,7 @@
"status.report": "Senhalar @{name}",
"status.sensitive_toggle": "Clicar per mostrar",
"status.sensitive_warning": "Contengut sensible",
"status.share": "Partejar",
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat",

View File

@@ -1,7 +1,7 @@
{
"account.block": "Blokuj @{name}",
"account.block_domain": "Blokuj wszystko z {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "Poniższe informacje mogą nie odwzorowywać bezbłędnie profilu użytkownika.",
"account.edit_profile": "Edytuj profil",
"account.follow": "Śledź",
"account.followers": "Śledzący",
@@ -13,11 +13,12 @@
"account.posts": "Posty",
"account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba",
"account.share": "Udostępnij profil @{name}",
"account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań śledzić",
"account.unmute": "Cofnij wyciszenie @{name}",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "Wyświetl pełny profil",
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
"bundle_column_error.retry": "Spróbuj ponownie",
@@ -34,7 +35,11 @@
"column.notifications": "Powiadomienia",
"column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia",
"column_header.moveLeft_settings": "Przesuń kolumnę w lewo",
"column_header.moveRight_settings": "Przesuń kolumnę w prawo",
"column_header.pin": "Przypnij",
"column_header.show_settings": "Pokaż ustawienia",
"column_header.unpin": "Cofnij przypięcie",
"column_subheading.navigation": "Nawigacja",
"column_subheading.settings": "Ustawienia",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Pokazuj odpowiedzi",
"home.settings": "Ustawienia kolumny",
"lightbox.close": "Zamknij",
"lightbox.next": "Następne",
"lightbox.previous": "Poprzednie",
"loading_indicator.label": "Ładowanie...",
"media_gallery.toggle_visible": "Przełącz widoczność",
"missing_indicator.label": "Nie znaleziono",
@@ -168,6 +175,7 @@
"status.report": "Zgłoś @{name}",
"status.sensitive_toggle": "Naciśnij aby wyświetlić",
"status.sensitive_warning": "Wrażliwa zawartość",
"status.share": "Udostępnij",
"status.show_less": "Pokaż mniej",
"status.show_more": "Pokaż więcej",
"status.unmute_conversation": "Cofnij wyciezenie konwersacji",

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir",
@@ -34,7 +35,11 @@
"column.notifications": "Notificações",
"column.public": "Global",
"column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
@@ -168,6 +175,7 @@
"status.report": "Denúnciar @{name}",
"status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir",
@@ -34,7 +35,11 @@
"column.notifications": "Notificações",
"column.public": "Global",
"column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
@@ -168,6 +175,7 @@
"status.report": "Denúnciar @{name}",
"status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Посты",
"account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения",
"account.share": "Share @{name}'s profile",
"account.unblock": "Разблокировать",
"account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться",
@@ -34,7 +35,11 @@
"column.notifications": "Уведомления",
"column.public": "Глобальная лента",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Закрепить",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Открепить",
"column_subheading.navigation": "Навигация",
"column_subheading.settings": "Настройки",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Показывать ответы",
"home.settings": "Настройки колонки",
"lightbox.close": "Закрыть",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть",
"missing_indicator.label": "Не найдено",
@@ -168,6 +175,7 @@
"status.report": "Пожаловаться",
"status.sensitive_toggle": "Нажмите для просмотра",
"status.sensitive_warning": "Чувствительный контент",
"status.share": "Share",
"status.show_less": "Свернуть",
"status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда",

View File

@@ -13,6 +13,7 @@
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow",
@@ -34,7 +35,11 @@
"column.notifications": "Notifications",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@@ -168,6 +175,7 @@
"status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Gönderiler",
"account.report": "Rapor et @{name}",
"account.requested": "Onay bekleniyor",
"account.share": "Share @{name}'s profile",
"account.unblock": "Engeli kaldır @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Takipten vazgeç",
@@ -34,7 +35,11 @@
"column.notifications": "Bildirimler",
"column.public": "Federe zaman tüneli",
"column_back_button.label": "Geri",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasyon",
"column_subheading.settings": "Ayarlar",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Cevapları göster",
"home.settings": "Kolon ayarları",
"lightbox.close": "Kapat",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Yükleniyor...",
"media_gallery.toggle_visible": "Görünürlüğü değiştir",
"missing_indicator.label": "Bulunamadı",
@@ -168,6 +175,7 @@
"status.report": "@{name}'i raporla",
"status.sensitive_toggle": "Görmek için tıklayınız",
"status.sensitive_warning": "Hassas içerik",
"status.share": "Share",
"status.show_less": "Daha azı",
"status.show_more": "Daha fazlası",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "Пости",
"account.report": "Поскаржитися",
"account.requested": "Очікує підтвердження",
"account.share": "Share @{name}'s profile",
"account.unblock": "Розблокувати",
"account.unblock_domain": "Розблокувати {domain}",
"account.unfollow": "Відписатися",
@@ -34,7 +35,11 @@
"column.notifications": "Сповіщення",
"column.public": "Глобальна стрічка",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Навігація",
"column_subheading.settings": "Налаштування",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Показувати відповіді",
"home.settings": "Налаштування колонок",
"lightbox.close": "Закрити",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Завантаження...",
"media_gallery.toggle_visible": "Показати/приховати",
"missing_indicator.label": "Не знайдено",
@@ -168,6 +175,7 @@
"status.report": "Поскаржитися",
"status.sensitive_toggle": "Натисніть, щоб подивитися",
"status.sensitive_warning": "Непристойний зміст",
"status.share": "Share",
"status.show_less": "Згорнути",
"status.show_more": "Розгорнути",
"status.unmute_conversation": "Зняти глушення з діалогу",

View File

@@ -13,6 +13,7 @@
"account.posts": "嘟文",
"account.report": "举报 @{name}",
"account.requested": "等待审批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消关注",
@@ -34,7 +35,11 @@
"column.notifications": "通知",
"column.public": "跨站公共时间轴",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "导航",
"column_subheading.settings": "设置",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置",
"lightbox.close": "关闭",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上",
"missing_indicator.label": "找不到内容",
@@ -168,6 +175,7 @@
"status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容",
"status.share": "Share",
"status.show_less": "减少显示",
"status.show_more": "显示更多",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "文章",
"account.report": "舉報 @{name}",
"account.requested": "等候審批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消關注",
@@ -34,7 +35,11 @@
"column.notifications": "通知",
"column.public": "跨站時間軸",
"column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",
@@ -168,6 +175,7 @@
"status.report": "舉報 @{name}",
"status.sensitive_toggle": "點擊顯示",
"status.sensitive_warning": "敏感內容",
"status.share": "Share",
"status.show_less": "減少顯示",
"status.show_more": "顯示更多",
"status.unmute_conversation": "Unmute conversation",

View File

@@ -13,6 +13,7 @@
"account.posts": "貼文",
"account.report": "檢舉 @{name}",
"account.requested": "正在等待許可",
"account.share": "Share @{name}'s profile",
"account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注",
@@ -34,7 +35,11 @@
"column.notifications": "通知",
"column.public": "聯盟時間軸",
"column_back_button.label": "上一頁",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定",
@@ -89,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",
@@ -168,6 +175,7 @@
"status.report": "通報 @{name}",
"status.sensitive_toggle": "點來看",
"status.sensitive_warning": "敏感內容",
"status.share": "Share",
"status.show_less": "看少點",
"status.show_more": "看更多",
"status.unmute_conversation": "不消音對話",

View File

@@ -88,7 +88,7 @@ function appendMedia(state, media) {
map.set('focusDate', new Date());
map.set('idempotencyKey', uuid());
if (prevSize === 0 && state.get('default_sensitive')) {
if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
map.set('sensitive', true);
}
});
@@ -152,14 +152,22 @@ export default function compose(state = initialState, action) {
.set('mounted', false)
.set('is_composing', false);
case COMPOSE_SENSITIVITY_CHANGE:
return state
.set('sensitive', !state.get('sensitive'))
.set('idempotencyKey', uuid());
return state.withMutations(map => {
if (!state.get('spoiler')) {
map.set('sensitive', !state.get('sensitive'));
}
map.set('idempotencyKey', uuid());
});
case COMPOSE_SPOILERNESS_CHANGE:
return state.withMutations(map => {
map.set('spoiler_text', '');
map.set('spoiler', !state.get('spoiler'));
map.set('idempotencyKey', uuid());
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
map.set('sensitive', true);
}
});
case COMPOSE_SPOILER_TEXT_CHANGE:
return state

View File

@@ -1,3 +1,45 @@
const MAX_NOTIFICATIONS = 5;
const GROUP_TAG = 'tag';
// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
const formatGroupTitle = (message, count) => message.replace('%{count}', count);
const notify = options =>
self.registration.getNotifications().then(notifications => {
if (notifications.length === MAX_NOTIFICATIONS) {
// Reached the maximum number of notifications, proceed with grouping
const group = {
title: formatGroupTitle(options.data.message, notifications.length + 1),
body: notifications
.sort((n1, n2) => n1.timestamp < n2.timestamp)
.map(notification => notification.title).join('\n'),
badge: '/badge.png',
icon: '/android-chrome-192x192.png',
tag: GROUP_TAG,
data: {
url: (new URL('/web/notifications', self.location)).href,
count: notifications.length + 1,
message: options.data.message,
},
};
notifications.forEach(notification => notification.close());
return self.registration.showNotification(group.title, group);
} else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
// Already grouped, proceed with appending the notification to the group
const group = cloneNotification(notifications[0]);
group.title = formatGroupTitle(group.data.message, group.data.count + 1);
group.body = `${options.title}\n${group.body}`;
group.data = { ...group.data, count: group.data.count + 1 };
return self.registration.showNotification(group.title, group);
}
return self.registration.showNotification(options.title, options);
});
const handlePush = (event) => {
const options = event.data.json();
@@ -17,7 +59,7 @@ const handlePush = (event) => {
options.actions = options.data.actions;
}
event.waitUntil(self.registration.showNotification(options.title, options));
event.waitUntil(notify(options));
};
const cloneNotification = (notification) => {
@@ -50,22 +92,37 @@ const makeRequest = (notification, action) =>
credentials: 'include',
});
const findBestClient = clients => {
const focusedClient = clients.find(client => client.focused);
const visibleClient = clients.find(client => client.visibilityState === 'visible');
return focusedClient || visibleClient || clients[0];
};
const openUrl = url =>
self.clients.matchAll({ type: 'window' }).then(clientList => {
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
const webClients = clientList
.filter(client => /\/web\//.test(client.url))
.sort(client => client !== 'visible');
if (clientList.length !== 0) {
const webClients = clientList.filter(client => /\/web\//.test(client.url));
const visibleClient = clientList.find(client => client.visibilityState === 'visible');
const focusedClient = clientList.find(client => client.focused);
if (webClients.length !== 0) {
const client = findBestClient(webClients);
const client = webClients[0] || visibleClient || focusedClient || clientList[0];
const { pathname } = new URL(url);
return client.navigate(url).then(client => client.focus());
} else {
return self.clients.openWindow(url);
if (pathname.startsWith('/web/')) {
return client.focus().then(client => client.postMessage({
type: 'navigate',
path: pathname.slice('/web/'.length - 1),
}));
}
} else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
const client = findBestClient(clientList);
return client.navigate(url).then(client => client.focus());
}
}
return self.clients.openWindow(url);
});
const removeActionFromNotification = (notification, action) => {

View File

@@ -121,7 +121,7 @@
.information-board {
background: darken($ui-base-color, 4%);
padding: 40px 0;
padding: 20px 0;
.panel {
position: absolute;
@@ -147,10 +147,15 @@
white-space: nowrap;
overflow: hidden;
a,
span {
font-weight: 400;
color: lighten($ui-base-color, 34%);
}
a {
text-decoration: none;
}
}
}
@@ -162,13 +167,14 @@
.information-board-sections {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.section {
flex: 1 0 0;
font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
text-align: right;
padding: 0 15px;
padding: 10px 15px;
span,
strong {
@@ -190,14 +196,6 @@
color: $primary-text-color;
}
}
@media screen and (max-width: 500px) {
flex-direction: column;
.section {
text-align: left;
}
}
}
.owner {
@@ -316,6 +314,10 @@
margin-bottom: 0;
}
hr {
border-color: rgba($ui-base-lighter-color, .6);
}
.header {
line-height: 30px;
overflow: hidden;
@@ -547,6 +549,7 @@
}
#mastodon-timeline {
display: flex;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
font-family: 'mastodon-font-sans-serif', sans-serif;
@@ -561,11 +564,20 @@
overflow: hidden;
box-shadow: 0 0 6px rgba($black, 0.1);
.column-header {
color: inherit;
font-family: inherit;
font-size: 16px;
line-height: inherit;
font-weight: inherit;
margin: 0;
padding: 15px;
}
.column {
padding: 0;
border-radius: 4px;
overflow: hidden;
height: 100%;
}
.scrollable {
@@ -646,21 +658,17 @@
}
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 840px) {
.container {
padding: 0 20px;
}
.information-board {
padding-bottom: 20px;
}
.information-board .container {
padding-right: 20px;
.panel {
position: static;
margin-top: 30px;
margin-top: 20px;
width: 100%;
border-radius: 4px;
@@ -688,6 +696,10 @@
@media screen and (max-width: 675px) {
.header-wrapper {
padding-top: 0;
&.compact .hero .heading {
padding-bottom: 20px;
}
}
.header .container,
@@ -701,14 +713,13 @@
}
.header {
padding-top: 0;
.hero {
margin-top: 30px;
padding: 0;
.heading {
padding-bottom: 20px;
padding: 0 20px 20px;
}
}
@@ -721,6 +732,16 @@
text-align: center;
}
.nav {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
}
.links a {
padding: 12px 8px;
}
.heading h1 {
padding: 30px 0;
}

View File

@@ -148,13 +148,17 @@
color: $ui-base-lighter-color;
}
&.active {
color: $ui-highlight-color;
}
&.disabled {
color: $ui-primary-color;
}
&.active {
color: $ui-highlight-color;
&.disabled {
color: lighten($ui-highlight-color, 13%);
}
}
}
&.overlayed {
@@ -214,16 +218,18 @@
}
.dropdown--active::after {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 0 4.5px 7.8px;
border-color: transparent transparent $ui-secondary-color;
bottom: 8px;
right: 104px;
@media screen and (min-width: 1025px) {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 0 4.5px 7.8px;
border-color: transparent transparent $ui-secondary-color;
bottom: 8px;
right: 104px;
}
}
.invisible {
@@ -1583,6 +1589,8 @@
cursor: pointer;
flex: 0 0 auto;
font-size: 16px;
border: 0;
text-align: start;
padding: 15px;
z-index: 3;
@@ -1743,12 +1751,6 @@
&:hover {
background: lighten($ui-base-color, 11%);
}
&.hidden-on-mobile {
@media screen and (max-width: 1024px) {
display: none;
}
}
}
.column-link__icon {
@@ -1878,7 +1880,7 @@
.getting-started__wrapper {
position: relative;
overflow-y: scroll;
overflow-y: auto;
}
.getting-started__footer {
@@ -2132,12 +2134,6 @@ button.icon-button.active i.fa-retweet {
}
}
&.hidden-on-mobile {
@media screen and (max-width: 1024px) {
display: none;
}
}
&:focus,
&:active {
outline: 0;
@@ -2325,6 +2321,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer;
display: flex;
flex-direction: column;
border: 0;
width: 100%;
height: 100%;
justify-content: center;
position: relative;
@@ -2398,6 +2396,7 @@ button.icon-button.active i.fa-retweet {
align-items: center;
background: rgba($base-overlay-background, 0.5);
box-sizing: border-box;
border: 0;
color: $primary-text-color;
cursor: pointer;
display: flex;
@@ -3414,7 +3413,8 @@ button.icon-button.active i.fa-retweet {
.boost-modal,
.confirmation-modal,
.report-modal {
.report-modal,
.actions-modal {
background: lighten($ui-secondary-color, 8%);
color: $ui-base-color;
border-radius: 8px;
@@ -3439,6 +3439,15 @@ button.icon-button.active i.fa-retweet {
}
}
.actions-modal {
.status {
background: $white;
border-bottom-color: $ui-secondary-color;
padding-top: 10px;
padding-bottom: 10px;
}
}
.boost-modal__container {
overflow-x: scroll;
padding: 10px;
@@ -3480,7 +3489,7 @@ button.icon-button.active i.fa-retweet {
}
.confirmation-modal {
max-width: 280px;
max-width: 85vw;
@media screen and (min-width: 480px) {
max-width: 380px;
@@ -3505,6 +3514,47 @@ button.icon-button.active i.fa-retweet {
}
}
.actions-modal {
.status {
overflow-y: auto;
max-height: 300px;
}
max-height: 80vh;
max-width: 80vw;
ul {
overflow-y: auto;
flex-shrink: 0;
li:empty {
margin: 0;
}
li:not(:empty) {
a {
color: $ui-base-color;
display: flex;
padding: 10px;
align-items: center;
text-decoration: none;
&.active {
&,
button {
background: $ui-highlight-color;
color: $primary-text-color;
}
}
button:first-child {
margin-right: 10px;
}
}
}
}
}
.confirmation-modal__action-bar {
.confirmation-modal__cancel-button {
background-color: transparent;

View File

@@ -263,7 +263,9 @@
border: medium none;
display: block;
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: hidden;
margin-right: 2px;
&:last-child {

View File

@@ -6,7 +6,7 @@ class Emoji
include Singleton
def initialize
data = Oj.load(File.open(File.join(Rails.root, 'lib', 'assets', 'emoji.json')))
data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
@map = {}
@@ -32,7 +32,7 @@ class Emoji
def codepoint_to_unicode(codepoint)
if codepoint.include?('-')
codepoint.split('-').map(&:hex).pack('U')
codepoint.split('-').map(&:hex).pack('U*')
else
[codepoint.hex].pack('U')
end

View File

@@ -8,11 +8,11 @@ module Mastodon
class UnexpectedResponseError < Error
def initialize(response = nil)
@response = response
end
def to_s
"#{@response.uri} returned code #{@response.code}"
if response.respond_to? :uri
super("#{response.uri} returned code #{response.code}")
else
super
end
end
end
end

View File

@@ -33,9 +33,7 @@ class LanguageDetector
def simplified_text
text.dup.tap do |new_text|
URI.extract(new_text).each do |url|
new_text.gsub!(url, '')
end
new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
new_text.gsub!(Account::MENTION_RE, '')
new_text.gsub!(Tag::HASHTAG_RE, '')
new_text.gsub!(/\s+/, ' ')

View File

@@ -44,7 +44,7 @@
#
class Account < ApplicationRecord
MENTION_RE = /(?:^|[^\/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
include AccountAvatar
include AccountFinderConcern

View File

@@ -53,6 +53,7 @@ class Web::PushSubscription < ApplicationRecord
url: url,
actions: actions,
access_token: access_token,
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker
}
),
endpoint: endpoint,
@@ -117,7 +118,7 @@ class Web::PushSubscription < ApplicationRecord
when :mention then [
{
title: translate('push_notifications.mention.action_favourite'),
icon: full_asset_url('emoji/2764.png', skip_pipeline: true),
icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true),
todo: 'request',
method: 'POST',
action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
@@ -130,11 +131,11 @@ class Web::PushSubscription < ApplicationRecord
can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
if should_hide
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand')
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), todo: 'expand', action: 'expand')
end
if can_boost
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
end
actions
@@ -160,6 +161,7 @@ class Web::PushSubscription < ApplicationRecord
content: translate('push_notifications.subscribed.body'),
actions: [],
url: web_url('notifications'),
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker
}
),
endpoint: endpoint,

View File

@@ -18,7 +18,7 @@ class AccountSearchService < BaseService
return [] if query_blank_or_hashtag? || limit < 1
if resolving_non_matching_remote_account?
[ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")]
[ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")].compact
else
search_results_and_exact_match.compact.uniq.slice(0, limit)
end

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