Compare commits

...

134 Commits

Author SHA1 Message Date
Eugen Rochko
e168e8afe5 Merge branch 'master' into for-upstream/optional-notification-muting 2017-11-15 03:36:01 +01:00
aschmitz
75c21adfbf Remove /api/v2/mutes 2017-11-14 20:26:22 -06:00
ysksn
6d7e05ec1f Add tests for StreamEntry (#5687)
* Add tests for StreamEntry

- `#object_type`
- `#verb`
- `#mentions`

* Fix to test results instead of implementations
2017-11-15 02:00:58 +01:00
ThibG
58bca7b1e4 Filter searched toots to be consistent with blocking behaviors (#5383) 2017-11-15 01:53:33 +01:00
ThibG
1c25853842 Use already-known remote user data if resolving temporarily fails in mentions (#5702) 2017-11-15 01:06:49 +01:00
Eugen Rochko
546257bc7f Allow specifying STATSD_NAMESPACE (#5700) 2017-11-15 07:22:43 +09:00
Eugen Rochko
fbef909c2a Add option to block direct messages from people you don't follow (#5669)
* Add option to block direct messages from people you don't follow

Fix #5326

* If the DM responds to a toot by recipient, allow it through

* i18n: Update Polish translation (for #5669) (#5673)
2017-11-14 21:12:57 +01:00
SerCom_KC
c3ec1e87b8 Updating Chinese (Simplified) translations (#5643)
* i18n: (zh-CN) Bug fix for note-counter.

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Add missing translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Add support.array key for better wording

* Revert "i18n: (zh-CN) Add support.array key for better wording"

This reverts commit 27bf9a946e886213e827cd985d4f62419db57534.
Looks like this commit can't get pass the checks, revert it for now.

* i18n: (zh-CN) Change `客户端` to `应用`

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Add missing translations (#5635)

* i18n: (zh-CN) Change `两步验证` to `双重认证`

* i18n: (zh-CN) Improve translations
2017-11-14 20:44:42 +01:00
ysksn
48e27c47a7 Add a test for SiteUpload#cache_key (#5685) 2017-11-14 20:44:11 +01:00
Yamagishi Kazutoshi
1f1838420f Refactor remote_follow_spec.rb (#5690) 2017-11-14 20:41:17 +01:00
Yamagishi Kazutoshi
20150659e6 Add uniqueness to block email domains (#5692) 2017-11-14 20:37:17 +01:00
Marcin Mikołajczak
8087aa83d4 i18n: Update Polish translation (#5699)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-14 20:36:11 +01:00
mayaeh
249b0fe107 Add Japanese translations for moderator roles and its own admin actions. (#5689) 2017-11-14 15:53:14 +09:00
Marcin Mikołajczak
a6682a3000 i18n: fix typo in Polish translation (#5688) 2017-11-14 15:52:32 +09:00
ysksn
4112a0631f Add tests for Setting (#5683) 2017-11-14 11:08:04 +09:00
Anna e só
0e6c4cb796 l10n: PT-BR translation updated (#5681)
* Improved e-mail messages; delted repeated words

* pt-BR.json translations updated

* Revert "pt-BR.json translations updated"

This reverts commit 108c460531196fed6e6d14f93e8d8d047c835ffd.

* Updated pt-BR.json

* pt-BR.yml updated
2017-11-14 11:07:38 +09:00
Eugen Rochko
92aaa55f06 Add code of conduct from GitHub generator (#5674) 2017-11-13 17:28:55 +01:00
takayamaki
5df8e30415 fix Code Climate badge on README.md (#5671)
and change badge's URI to https
2017-11-13 11:49:54 +09:00
ysksn
60f247c2e7 Add tests for SessionActivation (#5668)
* Fabricate SessionActivation

not only user_id but user association.

* Add tests for SessionActivation
2017-11-13 09:54:48 +09:00
Daniel Hunsaker
cf7e840990 Update model annotations to use BIGINT for IDs (#5461)
All the migrations have been updated to use BIGINTs for ID fields in the DB, but ActiveRecord needs to be told to treat those values as BIGINT as well. This PR does that.
2017-11-12 16:18:50 +01:00
KEINOS
252d0fe020 Fix #5652 - Notify too short when abbrev in JA (#5664)
* Fix #5652 - Notify too short when abbrev in JA

Fix #5652 of the notification message to be understandable when abbreviated.

* Changed quotes as original

Double quote to single and single quote as none. But I am not convinced of this fix.

* Added a single quote as YAMLlint's suggestion

* `bundle exec i18n-tasks normalize`
2017-11-12 16:51:47 +09:00
ysksn
2fb722397d Add tests for RemoteProfile (#5665) 2017-11-12 16:23:31 +09:00
Eugen Rochko
07f7192bc3 Fix #5632 - Reset column loading status after fetch fail (#5659) 2017-11-12 12:51:07 +09:00
Marcin Mikołajczak
fcb9533549 i18n: Update Polish translation (for #5635) (#5661)
* i18n: Update Polish translation (for #5635)

* 😑🔫
2017-11-11 22:31:20 +01:00
Eugen Rochko
7bb8b0b2fc Add moderator role and add pundit policies for admin actions (#5635)
* Add moderator role and add pundit policies for admin actions

* Add rake task for turning user into mod and revoking it again

* Fix handling of unauthorized exception

* Deliver new report e-mails to staff, not just admins

* Add promote/demote to admin UI, hide some actions conditionally

* Fix unused i18n
2017-11-11 20:23:33 +01:00
ThibG
2b1190065c Retry thread resolving (#5599)
Thread resolving is one of the few tasks that isn't retried on failure.
One common cause for failure of this task is a well-connected user replying to
a toot from a little-connected user on a small instance: the small instance
will get many requests at once, and will often fail to answer requests within
the 10 seconds timeout used by Mastodon.

This changes makes the ThreadResolveWorker retry a few times, with a
rapidly-increasing time before retries and large random contribution in order
to spread the load over time.
2017-11-11 16:49:04 +01:00
ysksn
56720ba590 Add tests for RemoteFollow (#5651)
* Add tests for RemoteFollow.initialize

* Add tests for RemoteFollow#valid?

* Add tests for RemoteFollow#subscribe_address_for
2017-11-10 16:56:02 +01:00
Matt
e5aa4128f6 Update en.yml (#5648)
Changed "Toots with replies" to read "Toots and replies"
2017-11-10 02:58:33 +01:00
Lynx Kotoura
f9e7336296 Fix focused background color of direct toots (#5642) 2017-11-09 19:40:34 +01:00
ysksn
07cca6e364 Add tests for Notification (#5640)
* Add tests for Notification#target_status

* Add tests for Notification#browserable?

* Add tests for Notification.reload_stale_associations!
2017-11-09 14:37:10 +01:00
ysksn
54b42901df Add and Remove tests for FollowRequest (#5622)
* Add a test for FollowRequest#authorize!

* Remove tests

There is no need to test
ActiveModel::Validations::ClassMethods#validates.

* Make an alias of destroy! as reject!

Instead of defining the method,
make an alias of destroy! as reject! because of reducing test.
2017-11-09 14:36:52 +01:00
Nanamachi
d200e041fe Rewrite account_controller_spec (#5633)
* make accounts_controller_spec DRY

* Add blocked user spec
2017-11-09 14:36:17 +01:00
unarist
49a285ce15 Show confirmation dialog on leaving WebUI while composing (#5616)
* Show confirmation dialog on leaving WebUI while composing

Currently, Back button and Back hotkey can cause leaving from WebUI, as well as browser's back button. Users may hit those buttons accidentally, and their composing text will be lost.

So this prevents it by showing confirmation dialog from `onbeforeunload` event.

* Fix message and comments
2017-11-09 14:34:41 +01:00
Marcin Mikołajczak
cfd7b7a0b7 i18n: Update Polish translation (#5639)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-09 14:23:06 +01:00
nullkal
36376b5e23 Translate ja (#5637) 2017-11-09 14:22:55 +01:00
Quenty31
eb97bd8af6 i10n OC: Memorial (#5615) + #5467 (#5623)
* Changed ĩ => ï

* Changed ĩ => ï

* Add ability to disable login and mark accounts as memorial (#5615)
2017-11-08 15:19:49 +01:00
Naoki Kosaka
4c0a85ef9b In remove_remote, exclude removed media attachments. (#5626) 2017-11-08 15:19:22 +01:00
ysksn
64cc129225 Add tests for MediaAttachment (#5620)
- `#local?`
- `#needs_redownload?`
- `#to_param`
2017-11-08 15:29:07 +09:00
ysksn
97fc2da2e0 Add tests for CustomEmoji#local? and #object_type (#5621) 2017-11-08 15:28:17 +09:00
ThibG
889ada5ee2 Fix process mentions for local users, as local users are considered to use OStatus (#5618) 2017-11-07 22:15:15 +01:00
nullkal
3f16caaa50 Eliminate space around emoji (#5474)
* Eliminate space around emoji

* More improve emoji style

* Make more compatible with Twemoji

* Make scss-lint happy

* Make not modify normal emoji's behavior

* Decrease status__action-bar's margin-top to 5px

* Make the test be passed

* Revert "Make the test be passed"

This reverts commit 54a8c60e5907ef20a5ceb5ab2c86a933e06f3ece.

* Revert "Make not modify normal emoji's behavior"

This reverts commit 6a5bdf0c11df16ebd190cb3ab9d2e8f1349f435a.
2017-11-07 19:09:53 +01:00
ThibG
5d5c0f4f43 Twidere mention workaround (#5552)
* Work around Twidere and Tootdon bug

Tootdon and Twidere construct @user@domain handles from mentions in toots based
solely on the mention text and account URI's domain without performing any
webfinger call or retrieving account info from the Mastodon server.

As a result, when a remote user has WEB_DOMAIN ≠ LOCAL_DOMAIN, Twidere and
Tootdon will construct the mention as @user@WEB_DOMAIN. Now, this will usually
resolve to the correct account (since the recommended configuration is to have
WEB_DOMAIN perform webfinger redirections to LOCAL_DOMAIN) when processing
mentions, but won't do so when displaying them (as it does not go through the
whole account resolution at that time).

This change rewrites mentions to the resolved account, so that displaying the
mentions will work.

* Use lookbehind instead of non-capturing group in MENTION_RE

Indeed, substitutions with the previous regexp would erroneously eat any
preceding whitespace, which would lead to concatenated mentions in the
previous commit.

Note that users will “lose” up to one character space per mention for their
toots, as that regexp is also used to remove the domain-part of mentioned
users for character counting purposes, and it also erroneously removed the
preceding character if it was a space.
2017-11-07 19:08:14 +01:00
Eugen Rochko
1032f3994f Add ability to disable login and mark accounts as memorial (#5615)
Fix #5597
2017-11-07 19:06:44 +01:00
MitarashiDango
cbbeec05be Fix spell miss (SWIIFT_OBJECT_URL -> SWIFT_OBJECT_URL) (#5617) 2017-11-07 19:06:30 +01:00
voidSatisfaction
e618edf85a fix: slang to adequate word (#5453) 2017-11-07 14:49:50 +01:00
nullkal
b6e2e999bd Show the local couterpart of emoji when it exists in /admin/custom_emojis (#5467)
* Show the local couterpart of emoji when it exists in admin/custom_emojis

* Fix indentation

* Fix error

* Add class table-action-link to Overwrite link

* Make it enable to overwrite emojis

* Make Code Climate happy
2017-11-07 14:49:32 +01:00
MIYAGI Hikaru
782224c991 Avoid emojifying on invisible text (#5558) 2017-11-07 14:48:13 +01:00
ThibG
84cfee2488 Do not process undeliverable mentions (#5598)
* Resolve remote accounts when mentioned even if they are already known

This commit reduces the risk of not having up-to-date public key or protocol
information for a remote account, which is required to deliver toots
(especially direct messages).

* Do not add mentions in private messages for remote users we cannot deliver to

Mastodon does not deliver private and direct toots to OStatus users, as there
is no guarantee the remote software understands the toot's privacy. However,
users currently do not get any feedback on it (Mastodon won't attempt delivery,
but the toot will be displayed exactly the same way to the user).

This change introduces *some* feedback by not processing mentions that are
not going to be delivered. A long-term solution is still needed to have
delivery receipts or at least some better indication of what is going on, but
at least an user can see *something* is up.
2017-11-07 14:47:39 +01:00
ThibG
7bea1530f4 Resolve remote accounts when mentioned even if they are already known (#5539)
This commit reduces the risk of not having up-to-date public key or protocol
information for a remote account, which is required to deliver toots
(especially direct messages).
2017-11-07 14:31:57 +01:00
Yamagishi Kazutoshi
47b0c61853 Unify file upload to using fog (#5604) 2017-11-07 14:30:31 +01:00
nullkal
864c4d869f Make fullscreen video in detailed status plays in fullscreen (Partly Fix #5160) (#5611)
* Make fullscreen video in detailed status plays in fullscreen (Fix #5160)

* Directly assign the initial state
2017-11-07 14:24:55 +01:00
kedama
d8cd9000d9 Hide disabled custom emojis from emoji picker and emoji auto suggestions. (#5613)
Make the same behavior as /api/v1/custom_emojis.
2017-11-07 14:24:21 +01:00
ysksn
d307ee79e9 Implement tests for Account#refresh! (#5601) 2017-11-06 13:54:41 +09:00
ysksn
cf01326cc1 Add test for Account#save_with_optional_media! (#5603)
There was a test when some of the properties are invalid, but none when all
of them are valid.
2017-11-06 13:54:12 +09:00
Marcin Mikołajczak
d48779cf7b i18n: Improve Polish translation (#5596)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-06 07:06:54 +09:00
Yamagishi Kazutoshi
8a588145d5 Update extract-text-webpack-plugin to version 3.0.2 (#5584) 2017-11-05 13:07:59 +01:00
MIYAGI Hikaru
8abe9e9058 don't display any descendants of .invisible (#5567)
* don't display any descendants of .invisible

* reduce the scope of selector

* remove some rules for image

* lint
2017-11-05 13:05:50 +01:00
ysksn
15c0f6ae56 Implement tests for Account#possibly_stale? (#5591) 2017-11-05 17:20:05 +09:00
Quenty31
da3adc0a73 l10n Occitan (#5586)
* Update OC: time format

Correction for time format according to: https://opinion.jornalet.com/conselh-linguistic-de-jornalet/blog/2379/la-notacion-oraria-en-occitan
Harmonisation words in menu and confirmation windows

* Update for unlisted custum emoji + #5577

* correction subjonctiu

It's either siasque or siague

* Corrections

Any : qual que, in two words, else it means "some".
And "siasque" with S even if I don't pronounce it at all.

* Update oc.json
2017-11-03 17:42:30 +09:00
Yamagishi Kazutoshi
0338c16f9f Remove babel-plugin-react-transform (#5582) 2017-11-01 17:49:46 +01:00
MitarashiDango
38d072446b add account search condition (instance domain) (#5577) 2017-11-01 14:46:05 +01:00
Yamagishi Kazutoshi
8ae9bd0eea Upgrade compression-webpack-plugin to version 1.0.1 (#5581) 2017-11-01 14:42:19 +01:00
Sorin Davidoi
5521e94e24 refactor(features/ui): Avoid useless renders in WrappedSwitch (#5580) 2017-11-01 12:17:53 +01:00
Yamagishi Kazutoshi
763a2f8511 Replace react-router-scroll to react-router-scroll-4 (#5568) 2017-11-01 06:58:38 +09:00
Nolan Lawson
60f962eedc Refactor initial state: auto_play_gif (#5576) 2017-11-01 06:58:07 +09:00
K.SHIRAKASHI
47d56438da Revert ruby-jwt version (#5575)
jwt 2.1.0 still does not work well.
ref. https://github.com/zaru/webpush/issues/42
2017-11-01 00:47:35 +09:00
Nolan Lawson
0692991b54 Add ServiceWorker caching for static assets (#5524) 2017-10-31 12:25:51 +01:00
Yamagishi Kazutoshi
6705463ed0 Update dependencies for Node.js (2017-10-30) (#5565)
* Update autoprefixer to version 7.1.6

* Update babel-plugin-preval to version 1.6.1

* Update babel-plugin-transform-react-remove-prop-types to version 0.4.10

* Update babel-preset-env to version 1.6.1

* Update cross-env to version 5.1.1

* Update enzyme-adapter-react-16 to version 1.0.2

* Update eslint-plugin-import to version 2.8.0

* Update immutable to version 3.8.2

* Update express to 4.16.2

* Update intl-relativeformat to version 2.1.0

* Update postcss-loader to version 2.0.8

* Update react-immutable-pure-component to version 1.1.1

* Update react-motion to version 0.5.2

* Update react-notification to version 6.8.2

* Update react-overlays to version 0.8.3

* Update react-redux-loading-bar to version 2.9.3

* Update resolve-url-loader to version 2.2.0

* Update style-loader to version 0.19.0

* Update webpack to version 3.8.1

* Update webpack-dev-server to version 2.9.3

* yarn upgrade
2017-10-31 12:23:24 +01:00
Yamagishi Kazutoshi
a2a4bf4e78 Update dependencies for Ruby (2017-10-30) (#5566)
* Update better_errors to version 2.4.0

* Update binding_of_caller to version 0.7.3

* Update bootsnap to version 1.1.5

* Update browser to version 2.5.2

* Update capistrano to version 3.10.0

* Update capistrano-bundler to version 1.3.0

* Update capistrano-rbenv to version 2.1.2

* Update capybara to version 2.15.4

* Update cld3 to version 3.2.1

* Update fabrication to version 2.18.0

* Update fog-openstack to version 0.1.22

* Update kaminari to version 1.1.1

* Update lograge to version 0.7.1

* Update nokogiri to version 1.8.1

* Update oj to version 3.3.9

* Update ox to version 2.8.1

* Update parallel_tests to version 2.17.0

* Update pkg-config to version 1.2.8

* Update rspec-rails to version 3.7.1

* Update rubocop to version 0.51.0

* Update scss_lint to version 0.55.0

* Update sidekiq to version 5.0.5

* Update sidekiq-scheduler to version 2.1.10

* Update tzinfo-data to version 1.2017.3

* Update webpacker to version 3.0.2

* bundle update
2017-10-31 12:22:32 +01:00
Nolan Lawson
b254e6ca5f Refactor initial state: "me" (#5563)
* Refactor initial state: "me"

* remove "me" from reducers/meta.js
2017-10-31 11:27:48 +09:00
SerCom_KC
29609fbb6a Updating Chinese (Simplified) translations (#5508)
* i18n: (zh-CN) fix punctuations and spaces
Spaces are fixed according to https://github.com/sparanoid/chinese-copywriting-guidelines

* i18n: (zh-CN) fix punctuation

* i18n: (zh-CN) Adapt official translation of Discourse Privacy Policy from GitHub, with minor fixes
https://github.com/discourse/discourse/blob/master/config/locales/server.zh_CN.yml#L2677

* i18n: (zh-CN) Update missing translations

* i18n: (zh-CN) Fixing errors

* i18n: (zh-CN) Fix indent error

* i18n: (zh-CN) Fix language tag

* i18n: (zh-CN) Remove quotes

* i18n: (zh-CN) Update translation (#5485)

* i18n: (zh-CN) Remove whitespaces, x -> ×

* i18n: (zh-CN) Rewording on time distance

* i18n: (zh-CN) Overall improvements

* i18n: (zh-CN) i18n-tasks normalization

* i18n: (zh-CN) Add missing translation
2017-10-30 12:34:58 +09:00
ThibG
d37a56c07c Update remote ActivityPub users when fetching their toots (#5545) 2017-10-30 00:24:16 +09:00
Nolan Lawson
2cea4592a3 Avoid modifying emoji data inline (#5548) 2017-10-30 00:23:38 +09:00
Nolan Lawson
512feab222 Add margin to account for Edge disappearing scrollbar (#5522)
* Add margin to account for Edge disappearing scrollbar

* Fix 16px margin for DMs and horizontal line
2017-10-30 00:11:32 +09:00
Nolan Lawson
5e111ce16d Reactor unfollow_modal, boost_modal, delete_modal (#5505) 2017-10-30 00:10:15 +09:00
Alda Marteau-Hardi
4080569c2d Fix a grammatical error in the notifications. (#5555) 2017-10-29 02:08:37 +09:00
Marcin Mikołajczak
2cbb8e8cd1 i18n: Update Polish translation (#5547) 2017-10-28 12:43:20 +09:00
Herbert Kagumba
3e9236b343 Separate Follow/Unfollow and back buttons (#5496) 2017-10-27 19:14:11 +02:00
ThibG
89c77fe225 Instantiate service classes for each call (fixes #5540) (#5543) 2017-10-27 19:08:30 +02:00
Nolan Lawson
e843f62f47 Avoid unnecessary Motion components in icon_button.js (#5544) 2017-10-27 19:08:07 +02:00
Nolan Lawson
ec487166db Directly use <Motion/> if not reducing motion (#5546) 2017-10-27 19:06:54 +02:00
David Yip
37b267e2ab Add artist, title, and date metadata to boop.{mp3,ogg} (#5531)
For boop.mp3, this commit adds both ID3v1 and ID3v2 tags.  For boop.ogg,
we use Vorbis metadata.

In the case of boop.mp3, this also adds a cover image. Interestingly, it
didn't seem to affect the size of boop.mp3 much, despite being ~8k.
boop.ogg seemed to be much more affected and so no cover image was added
to that version.
2017-10-28 00:05:04 +09:00
Nolan Lawson
3de22a82bf Refactor initial state: reduce_motion and auto_play_gif (#5501) 2017-10-28 00:04:44 +09:00
Akihiko Odaki
e4080772b5 Use contenthash for ExtractTextWebpackPlugin (#5462)
[hash] is not documented.
2017-10-27 23:54:20 +09:00
nullkal
781105293c Feature: Unlisted custom emojis (#5485) 2017-10-27 16:11:30 +02:00
puckipedia
0cb329f63a Allow ActivityPub Note's tag and attachment to be single objects (#5534) 2017-10-27 16:10:36 +02:00
unarist
0129f5eada Optimize FixReblogsInFeeds migration (#5538)
We have changed how we store reblogs in the redis for bigint IDs. This process is done by 1) scan all entries in users feed, and 2) re-store reblogs by 3 write commands.

However, this operation is really slow for large instances. e.g. 1hrs on friends.nico (w/ 50k users). So I have tried below tweaks.

* It checked non-reblogs by `entry[0] == entry[1]`, but this condition won't work because `entry[0]` is String while `entry[1]` is Float. Changing `entry[0].to_i == entry[1]` seems work.
  -> about 4-20x faster (feed with less reblogs will be faster)
* Write operations can be batched by pipeline
  -> about 6x faster
* Wrap operation by Lua script and execute by EVALSHA command. This really reduces packets between Ruby and Redis.
  -> about 3x faster

I've taken Lua script way, though doing other optimizations may be enough.
2017-10-27 16:10:22 +02:00
erin
22da775a85 Fix copying emojos: redirect to the page you were on (#5509) 2017-10-26 23:44:24 +09:00
りんすき
d556be2968 Fix column design broken with very long title (#5493)
* Fix #5314

* fix not beautiful code

* fix broken design with mobile view

* remove no longer needed code
2017-10-26 22:52:48 +09:00
unarist
4f337c020a Fix Cocaine::ExitStatusError when upload small non-animated GIF (#5489)
Looks like copied tempfile need to be flushed before further processing. This issue won't happen if the uploaded file has enough file size.
2017-10-26 22:48:35 +09:00
Nolan Lawson
02f7f3619a Remove translateZ(0) on modal overlay (#5478) 2017-10-26 22:46:50 +09:00
Ratmir Karabut
20fee786b1 Update Russian translation (#5517)
* 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

* Update Russian translation

* Fix merge conflicts (revert)

* Update Russian translation

* Update Russian translation (fix)

* Update Russian translation (fix quotes)

* Update Russian translation (fix quotes)

* Update Russian translation (fix)

* Update Russian translation

* Add quotes

* bundle exec i18n-tasks normalize
2017-10-26 00:21:58 +09:00
Anna e só
74777599cf l10n: PT-BR translation updated (#5530) 2017-10-25 23:11:03 +09:00
Olivier Nicole
1ba3725473 Complete Esperanto translation (#5520) 2017-10-25 22:38:37 +09:00
aschmitz
685fafcc27 Fix up migration things 2017-10-22 21:37:44 -05:00
aschmitz
0e210b5c63 Make AddHideNotificationsToMute Concurrent
It's not clear how much this will benefit instances in practice, as the
number of mutes tends to be pretty small, but this should prevent any
blocking migrations nonetheless.
2017-10-22 20:52:54 -05:00
aschmitz
6b70c2ca12 Code review suggestions from akihikodaki
Also fixed a syntax error in tests for AccountInteractions.
2017-10-22 20:52:54 -05:00
Surinna Curtis
8c6ecd5616 Code style changes in specs and removed an extra space 2017-10-22 20:52:54 -05:00
Surinna Curtis
22cf9bcff6 Make the Toggle in the mute modal look better 2017-10-22 20:52:54 -05:00
Surinna Curtis
21de45c7d2 Use Toggle in place of checkbox in the mute modal. 2017-10-22 20:52:54 -05:00
Surinna Curtis
88e52f3a2a Fix wrong variable name in api/v2/mutes 2017-10-22 20:52:54 -05:00
Surinna Curtis
d4bb04c45c Don't serialize "account" in MuteSerializer
Doing so is somewhat unnecessary since it's always the current user's account.
2017-10-22 20:52:54 -05:00
Surinna Curtis
33c56212b9 Rename /api/v1/mutes/details -> /api/v2/mutes 2017-10-22 20:52:54 -05:00
Surinna Curtis
bcd4b72223 Remove superfluous blank line 2017-10-22 20:52:54 -05:00
Surinna Curtis
f39bba9a90 Fix code style issues 2017-10-22 20:52:54 -05:00
Surinna Curtis
290c6b0f2e Apply white-space: nowrap to account relationships icons 2017-10-22 20:52:54 -05:00
Surinna Curtis
d5d1dcab77 Fixed a typo that was breaking the account mute API endpoint 2017-10-22 20:52:54 -05:00
Surinna Curtis
ef5ebdd544 minor code style fixes oops 2017-10-22 20:52:54 -05:00
Surinna Curtis
da85bfc252 Refactor handling of default params for muting to make code cleaner 2017-10-22 20:52:54 -05:00
Surinna Curtis
d17255c0e0 add an explanatory comment to AccountInteractions 2017-10-22 20:52:54 -05:00
Surinna Curtis
74ce229101 fix a missing import 2017-10-22 20:52:54 -05:00
Surinna Curtis
fb8613d09b In probably dead code, replace a dispatch of muteAccount that was skipping the modal with launching the mute modal. 2017-10-22 20:52:54 -05:00
Surinna Curtis
d73f437419 satisfy eslint 2017-10-22 20:52:54 -05:00
Surinna Curtis
343c358bb2 make the hide/unhide notifications buttons work 2017-10-22 20:52:54 -05:00
Surinna Curtis
8a6ad735f1 Allow modifying the hide_notifications of a mute with the /api/v1/accounts/:id/mute endpoint 2017-10-22 20:52:54 -05:00
Surinna Curtis
9fb8a6f231 Show whether muted users' notifications are muted in account lists 2017-10-22 20:52:54 -05:00
Surinna Curtis
f90ac53dc5 Expose whether a mute hides notifications in the api/v1/relationships endpoint 2017-10-22 20:52:54 -05:00
Surinna Curtis
c814d19672 Add more specs for the /api/v1/mutes/details endpoint 2017-10-22 20:52:54 -05:00
Surinna Curtis
27eb75878e Define a serializer for /api/v1/mutes/details 2017-10-22 20:52:54 -05:00
Surinna Curtis
a81d3a2842 Add a /api/v1/mutes/details route that just returns the array of mutes. 2017-10-22 20:52:54 -05:00
Surinna Curtis
1fc0382d29 Put the label for the hide notifications checkbox in a label element. 2017-10-22 20:52:54 -05:00
Surinna Curtis
90db8b63b0 add trailing newlines to files for Pork :) 2017-10-22 20:52:54 -05:00
Surinna Curtis
15a6cb5ca9 specs for MuteService notifications params 2017-10-22 20:52:54 -05:00
Surinna Curtis
d5dc4696a7 Satisfy eslint. 2017-10-22 20:52:54 -05:00
Surinna Curtis
eb6543b36b Convert profile header mute to use mute modal 2017-10-22 20:52:54 -05:00
Surinna Curtis
920c7cdaf3 Break out a separate mute modal with a hide-notifications checkbox. 2017-10-22 20:52:54 -05:00
Surinna Curtis
f3aac23099 Less gross passing of notifications flag 2017-10-22 20:52:53 -05:00
Surinna Curtis
aded1acfda API support for muting notifications (and specs) 2017-10-22 20:52:53 -05:00
Surinna Curtis
9855529a10 Add support for muting notifications in MuteService 2017-10-22 20:52:53 -05:00
Surinna Curtis
0ccfbe5747 specs testing that hide_notifications in mutes actually hides notifications 2017-10-22 20:52:53 -05:00
Surinna Curtis
921a1b7b2a Add specs for how mute! interacts with muting_notifications? 2017-10-22 20:52:53 -05:00
Surinna Curtis
aa7e541780 block notifications in notify_service from hard muted accounts 2017-10-22 20:52:53 -05:00
Surinna Curtis
dac67960e0 Add muting_notifications? and a notifications argument to mute! 2017-10-22 20:52:53 -05:00
Surinna Curtis
90a0176b0b Add a hide_notifications column to mutes 2017-10-22 20:52:53 -05:00
243 changed files with 5103 additions and 2457 deletions

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

32
Gemfile
View File

@@ -14,8 +14,10 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.9'
gem 'fog-openstack', '~> 0.1'
gem 'fog-aws', '~> 1.4', require: false
gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.4', require: false
gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
@@ -38,14 +40,14 @@ gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.0'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.7'
gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.0'
gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.5'
gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0'
@@ -75,15 +77,15 @@ gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do
gem 'fabrication', '~> 2.16'
gem 'fabrication', '~> 2.18'
gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.6'
gem 'rspec-rails', '~> 3.7'
end
group :test do
gem 'capybara', '~> 2.14'
gem 'capybara', '~> 2.15'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'microformats', '~> 4.0'
@@ -91,13 +93,13 @@ group :test do
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false
gem 'webmock', '~> 3.0'
gem 'parallel_tests', '~> 2.14'
gem 'parallel_tests', '~> 2.17'
end
group :development do
gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.1'
gem 'better_errors', '~> 2.4'
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5'
gem 'letter_opener', '~> 1.4'
@@ -105,15 +107,15 @@ group :development do
gem 'rubocop', require: false
gem 'brakeman', '~> 4.0', require: false
gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.53', require: false
gem 'scss_lint', '~> 0.55', require: false
gem 'capistrano', '~> 3.8'
gem 'capistrano-rails', '~> 1.2'
gem 'capistrano', '~> 3.10'
gem 'capistrano-rails', '~> 1.3'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'
end
group :production do
gem 'lograge', '~> 0.5'
gem 'lograge', '~> 0.7'
gem 'redis-rails', '~> 5.0'
end

View File

@@ -57,25 +57,17 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.46)
aws-sdk-resources (= 2.10.46)
aws-sdk-core (2.10.46)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.46)
aws-sdk-core (= 2.10.46)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
better_errors (2.3.0)
better_errors (2.4.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
binding_of_caller (0.7.3)
debug_inspector (>= 0.0.1)
bootsnap (1.1.3)
bootsnap (1.1.5)
msgpack (~> 1.0)
brakeman (4.0.1)
browser (2.5.1)
browser (2.5.2)
builder (3.2.3)
bullet (5.6.1)
activesupport (>= 3.0.0)
@@ -83,23 +75,23 @@ GEM
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
capistrano (3.9.1)
capistrano (3.10.0)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
capistrano-bundler (1.2.0)
capistrano-bundler (1.3.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.3.0)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.1)
capistrano-rbenv (2.1.2)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.15.1)
capybara (2.15.4)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
@@ -110,7 +102,7 @@ GEM
activesupport
charlock_holmes (0.7.5)
chunky_png (1.3.8)
cld3 (3.2.0)
cld3 (3.2.1)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
@@ -150,16 +142,21 @@ GEM
thread
thread_safe
encryptor (3.0.0)
erubi (1.6.1)
et-orbi (1.0.5)
erubi (1.7.0)
et-orbi (1.0.8)
tzinfo
excon (0.59.0)
execjs (2.7.0)
fabrication (2.16.3)
fabrication (2.18.0)
faker (1.8.4)
i18n (~> 0.5)
fast_blank (1.0.0)
ffi (1.9.18)
fog-aws (1.4.1)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.45.0)
builder
excon (~> 0.58)
@@ -167,15 +164,20 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-openstack (0.1.21)
fog-local (0.4.0)
fog-core (~> 1.27)
fog-openstack (0.1.22)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (0.4.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldfinger (2.0.1)
addressable (~> 2.5)
@@ -211,7 +213,8 @@ GEM
httplog (0.99.7)
colorize
rack
i18n (0.8.6)
i18n (0.9.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.18)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
@@ -225,29 +228,28 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jmespath (1.3.1)
json (2.1.0)
json-ld (2.1.5)
json-ld (2.1.7)
multi_json (~> 1.12)
rdf (~> 2.2)
rdf (~> 2.2, >= 2.2.8)
json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
kaminari (1.0.1)
kaminari (1.1.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
kaminari-actionview (= 1.1.1)
kaminari-activerecord (= 1.1.1)
kaminari-core (= 1.1.1)
kaminari-actionview (1.1.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
kaminari-core (= 1.1.1)
kaminari-activerecord (1.1.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kaminari-core (= 1.1.1)
kaminari-core (1.1.1)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -257,18 +259,19 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
lograge (0.6.0)
lograge (0.7.1)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
request_store (~> 1.0)
loofah (2.0.3)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.8.2)
method_source (0.9.0)
microformats (4.0.7)
json
nokogiri
@@ -277,7 +280,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_mime (0.1.4)
mini_portile2 (2.2.0)
mini_portile2 (2.3.0)
minitest (5.10.3)
msgpack (1.1.0)
multi_json (1.12.2)
@@ -285,8 +288,8 @@ GEM
net-ssh (>= 2.6.5)
net-ssh (4.2.0)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13)
nokogiri
nsa (0.2.4)
@@ -294,15 +297,15 @@ GEM
concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
oj (3.3.5)
openssl (2.0.5)
oj (3.3.9)
openssl (2.0.6)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.6.0)
ox (2.8.1)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -313,19 +316,18 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.12.0)
parallel_tests (2.15.0)
parallel_tests (2.17.0)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.7)
pkg-config (1.2.8)
powerpack (0.1.1)
pry (0.10.4)
pry (0.11.2)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
method_source (~> 0.9.0)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.0)
@@ -379,31 +381,31 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.1.0)
rdf (2.2.9)
rake (12.2.1)
rdf (2.2.11)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
rdf (~> 2.0)
redis (3.3.3)
redis-actionpack (5.0.1)
redis (3.3.5)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.3)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (~> 1.3.0)
redis-store (>= 1.3, < 2)
redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.2)
redis-rack (2.0.3)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 1.4)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.3.0)
redis (>= 2.2)
redis-store (1.4.1)
redis (>= 2.2, < 5)
request_store (1.3.2)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
@@ -411,27 +413,27 @@ GEM
rotp (2.1.2)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
rspec-core (3.7.0)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-mocks (3.6.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-rails (3.6.1)
rspec-support (~> 3.7.0)
rspec-rails (3.7.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-support (~> 3.6.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.6.0)
rubocop (0.50.0)
rspec-support (3.7.0)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
@@ -439,7 +441,7 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.8.3)
ruby-progressbar (1.9.0)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
safe_yaml (1.0.4)
@@ -448,19 +450,19 @@ GEM
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.25)
scss_lint (0.54.0)
scss_lint (0.55.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sidekiq (5.0.4)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
redis (>= 3.3.4, < 5)
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.9)
redis (~> 3)
sidekiq-scheduler (2.1.10)
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
@@ -477,7 +479,6 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slop (3.6.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -500,9 +501,9 @@ GEM
tilt (2.0.8)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.3)
tzinfo (1.2.4)
thread_safe (~> 0.1)
tzinfo-data (1.2017.2)
tzinfo-data (1.2017.3)
tzinfo (>= 1.0.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
@@ -517,7 +518,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpacker (3.0.1)
webpacker (3.0.2)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
@@ -538,19 +539,18 @@ DEPENDENCIES
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
aws-sdk (~> 2.9)
better_errors (~> 2.1)
better_errors (~> 2.4)
binding_of_caller (~> 0.7)
bootsnap
brakeman (~> 4.0)
browser
bullet (~> 5.5)
bundler-audit (~> 0.6)
capistrano (~> 3.8)
capistrano-rails (~> 1.2)
capistrano (~> 3.10)
capistrano-rails (~> 1.3)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 2.14)
capybara (~> 2.15)
charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
@@ -558,9 +558,12 @@ DEPENDENCIES
devise-two-factor (~> 3.0)
doorkeeper (~> 4.2)
dotenv-rails (~> 2.2)
fabrication (~> 2.16)
fabrication (~> 2.18)
faker (~> 1.7)
fast_blank (~> 1.0)
fog-aws (~> 1.4)
fog-core (~> 1.45)
fog-local (~> 0.4)
fog-openstack (~> 0.1)
fuubar (~> 2.2)
goldfinger (~> 2.0)
@@ -574,22 +577,22 @@ DEPENDENCIES
idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0)
kaminari (~> 1.1)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
link_header (~> 0.0)
lograge (~> 0.5)
lograge (~> 0.7)
mario-redis-lock (~> 1.2)
microformats (~> 4.0)
mime-types (~> 3.1)
nokogiri (~> 1.7)
nokogiri (~> 1.8)
nsa (~> 0.2)
oj (~> 3.0)
oj (~> 3.3)
ostatus2 (~> 2.0)
ox (~> 2.5)
ox (~> 2.8)
paperclip (~> 5.1)
paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.14)
parallel_tests (~> 2.17)
pg (~> 0.20)
pghero (~> 1.7)
pkg-config (~> 1.2)
@@ -609,12 +612,12 @@ DEPENDENCIES
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
rqrcode (~> 0.10)
rspec-rails (~> 3.6)
rspec-rails (~> 3.7)
rspec-sidekiq (~> 3.0)
rubocop
ruby-oembed (~> 0.12)
sanitize (~> 4.4)
scss_lint (~> 0.53)
scss_lint (~> 0.55)
sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1)

View File

@@ -1,8 +1,8 @@
![Mastodon](https://i.imgur.com/NhZc40l.png)
========
[![Build Status](http://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/github/tootsuite/mastodon.svg)][code_climate]
[![Build Status](https://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon

View File

@@ -1,31 +1,41 @@
# frozen_string_literal: true
class Admin::AccountModerationNotesController < Admin::BaseController
def create
@account_moderation_note = current_account.account_moderation_notes.new(resource_params)
if @account_moderation_note.save
@target_account = @account_moderation_note.target_account
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show'
module Admin
class AccountModerationNotesController < BaseController
before_action :set_account_moderation_note, only: [:destroy]
def create
authorize AccountModerationNote, :create?
@account_moderation_note = current_account.account_moderation_notes.new(resource_params)
if @account_moderation_note.save
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show'
end
end
def destroy
authorize @account_moderation_note, :destroy?
@account_moderation_note.destroy
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
def set_account_moderation_note
@account_moderation_note = AccountModerationNote.find(params[:id])
end
end
def destroy
@account_moderation_note = AccountModerationNote.find(params[:id])
@target_account = @account_moderation_note.target_account
@account_moderation_note.destroy
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
end

View File

@@ -2,29 +2,54 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index
authorize :account, :index?
@accounts = filtered_accounts.page(params[:page])
end
def show
authorize @account, :show?
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
end
def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
redirect_to admin_account_path(@account.id)
end
def redownload
authorize @account, :redownload?
@account.reset_avatar!
@account.reset_header!
@account.save!
@@ -42,6 +67,10 @@ module Admin
redirect_to admin_account_path(@account.id) if @account.local?
end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def filtered_accounts
AccountFilter.new(filter_params).results
end

View File

@@ -2,7 +2,9 @@
module Admin
class BaseController < ApplicationController
before_action :require_admin!
include Authorization
before_action :require_staff!
layout 'admin'
end

View File

@@ -2,15 +2,18 @@
module Admin
class ConfirmationsController < BaseController
before_action :set_user
def create
account_user.confirm
authorize @user, :confirm?
@user.confirm!
redirect_to admin_accounts_path
end
private
def account_user
Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -5,14 +5,18 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create]
def index
@custom_emojis = filtered_custom_emojis.page(params[:page])
authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
end
def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new
end
def create
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
@@ -22,29 +26,44 @@ module Admin
end
end
def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
def copy
emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
authorize @custom_emoji, :copy?
if emoji.save
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode)
if emoji.update(image: @custom_emoji.image)
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(params[:page])
redirect_to admin_custom_emojis_path(page: params[:page])
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end
@@ -56,7 +75,7 @@ module Admin
end
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image)
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
def filtered_custom_emojis

View File

@@ -5,14 +5,18 @@ module Admin
before_action :set_domain_block, only: [:show, :destroy]
def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page])
end
def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new
end
def create
authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params)
if @domain_block.save
@@ -23,9 +27,12 @@ module Admin
end
end
def show; end
def show
authorize @domain_block, :show?
end
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end

View File

@@ -5,14 +5,18 @@ module Admin
before_action :set_email_domain_block, only: [:show, :destroy]
def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page])
end
def new
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new
end
def create
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save
@@ -23,6 +27,7 @@ module Admin
end
def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end

View File

@@ -3,10 +3,12 @@
module Admin
class InstancesController < BaseController
def index
authorize :instance, :index?
@instances = ordered_instances
end
def resubscribe
authorize :instance, :resubscribe?
params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path

View File

@@ -2,19 +2,20 @@
module Admin
class ReportedStatusesController < BaseController
include Authorization
before_action :set_report
before_action :set_status, only: [:update, :destroy]
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
end
def update
authorize @status, :update?
@status.update(status_params)
redirect_to admin_report_path(@report)
end

View File

@@ -5,14 +5,17 @@ module Admin
before_action :set_report, except: [:index]
def index
authorize :report, :index?
@reports = filtered_reports.page(params[:page])
end
def show
authorize @report, :show?
@form = Form::StatusBatch.new
end
def update
authorize @report, :update?
process_report
redirect_to admin_report_path(@report)
end

View File

@@ -2,17 +2,18 @@
module Admin
class ResetsController < BaseController
before_action :set_account
before_action :set_user
def create
@account.user.send_reset_password_instructions
authorize @user, :reset_password?
@user.send_reset_password_instructions
redirect_to admin_accounts_path
end
private
def set_account
@account = Account.find(params[:account_id])
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Admin
class RolesController < BaseController
before_action :set_user
def promote
authorize @user, :promote?
@user.promote!
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
redirect_to admin_account_path(@user.account_id)
end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -28,10 +28,13 @@ module Admin
).freeze
def edit
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new
end
def update
authorize :settings, :update?
settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)

View File

@@ -5,11 +5,13 @@ module Admin
before_action :set_account
def create
authorize @account, :silence?
@account.update(silenced: true)
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsilence?
@account.update(silenced: false)
redirect_to admin_accounts_path
end

View File

@@ -2,8 +2,6 @@
module Admin
class StatusesController < BaseController
include Authorization
helper_method :current_params
before_action :set_account
@@ -12,24 +10,30 @@ module Admin
PER_PAGE = 20
def index
authorize :status, :index?
@statuses = @account.statuses
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def update
authorize @status, :update?
@status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params)
end
@@ -60,6 +64,7 @@ module Admin
def current_params
page = (params[:page] || 1).to_i
{
media: params[:media],
page: page > 1 && page,

View File

@@ -3,6 +3,7 @@
module Admin
class SubscriptionsController < BaseController
def index
authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page)
end

View File

@@ -5,12 +5,14 @@ module Admin
before_action :set_account
def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id)
redirect_to admin_accounts_path
end
def destroy
@account.update(suspended: false)
authorize @account, :unsuspend?
@account.unsuspend!
redirect_to admin_accounts_path
end

View File

@@ -5,6 +5,7 @@ module Admin
before_action :set_user
def destroy
authorize @user, :disable_2fa?
@user.disable_two_factor!
redirect_to admin_accounts_path
end

View File

@@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def mute
MuteService.new.call(current_user.account, @account)
MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end

View File

@@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment]
)
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read }
@@ -9,12 +11,24 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json
def index
@search = Search.new(search_results)
@search = Search.new(search)
render json: @search, serializer: REST::SearchSerializer
end
private
def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end
def search_results
SearchService.new.call(
params[:q],

View File

@@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in?
@@ -40,6 +41,10 @@ class ApplicationController < ActionController::Base
redirect_to root_path unless current_user&.admin?
end
def require_staff!
redirect_to root_path unless current_user&.staff?
end
def check_suspension
forbidden if current_user.account.suspended?
end

View File

@@ -2,6 +2,7 @@
module Authorization
extend ActiveSupport::Concern
include Pundit
def pundit_user

View File

@@ -26,7 +26,7 @@ class Settings::NotificationsController < ApplicationController
def user_settings_params
params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end
end

View File

@@ -35,6 +35,11 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
def can?(action, record)
return false if record.nil?
policy(record).public_send("#{action}?")
end
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || []
class_names << 'fa'
@@ -43,6 +48,10 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def custom_emoji_tag(custom_emoji)
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
end
def opengraph(property, content)
tag(:meta, content: content, property: property)
end

View File

@@ -9,6 +9,10 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
def as_array(value)
value.is_a?(Array) ? value : [value]
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end

View File

@@ -241,11 +241,11 @@ export function unblockAccountFail(error) {
};
export function muteAccount(id) {
export function muteAccount(id, notifications) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => {
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {

View File

@@ -1,5 +1,6 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { openModal } from '../../mastodon/actions/modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
@@ -9,6 +10,9 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@@ -80,3 +84,20 @@ export function expandMutesFail(error) {
error,
};
};
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal('MUTE'));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}

View File

@@ -4,12 +4,13 @@ export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from '../initial_state';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
const accountId = getState().getIn(['meta', 'me']);
api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));

View File

@@ -7,6 +7,7 @@ import Permalink from './permalink';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -14,6 +15,8 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
@injectIntl
@@ -21,7 +24,6 @@ export default class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
@@ -41,8 +43,16 @@ export default class Account extends ImmutablePureComponent {
this.props.onMute(this.props.account);
}
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
render () {
const { account, me, intl, hidden } = this.props;
const { account, intl, hidden } = this.props;
if (!account) {
return <div />;
@@ -70,7 +80,18 @@ export default class Account extends ImmutablePureComponent {
} else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
let hidingNotificationsButton;
if (muting.get('notifications')) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<div>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</div>
);
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}

View File

@@ -137,7 +137,9 @@ export default class ColumnHeader extends React.PureComponent {
<div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title}
<span className='column-header__title'>
{title}
</span>
<div className='column-header__buttons'>
{backButton}

View File

@@ -72,6 +72,25 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay,
});
if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
>
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
</button>
);
}
return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) =>

View File

@@ -6,6 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif } from '../initial_state';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@@ -23,11 +24,9 @@ class Item extends React.PureComponent {
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
standalone: false,
index: 0,
size: 1,
@@ -47,7 +46,7 @@ class Item extends React.PureComponent {
}
hoverToPlay () {
const { attachment, autoPlayGif } = this.props;
const { attachment } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
@@ -139,7 +138,7 @@ class Item extends React.PureComponent {
</a>
);
} else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif;
const autoPlay = !isIOS() && autoPlayGif;
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@@ -181,11 +180,9 @@ export default class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
standalone: false,
};
@@ -261,9 +258,9 @@ export default class MediaGallery extends React.PureComponent {
const size = media.take(4).size;
if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
}
}

View File

@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';

View File

@@ -36,9 +36,6 @@ export default class Status extends ImmutablePureComponent {
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
me: PropTypes.string,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
hidden: PropTypes.bool,
onMoveUp: PropTypes.func,
@@ -54,9 +51,6 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
'me',
'boostModal',
'autoPlayGif',
'muted',
'hidden',
]
@@ -197,7 +191,7 @@ export default class Status extends ImmutablePureComponent {
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />}
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
</Bundle>
);
}

View File

@@ -5,6 +5,7 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -47,7 +48,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
me: PropTypes.string,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -56,7 +56,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'me',
'withDismiss',
]
@@ -116,7 +115,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
}
render () {
const { status, me, intl, withDismiss } = this.props;
const { status, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted');
const anonymousAccess = !me;

View File

@@ -12,6 +12,8 @@ import {
unmuteAccount,
} from '../actions/accounts';
import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes';
import { unfollowModal } from '../initial_state';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -22,8 +24,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
me: state.getIn(['meta', 'me']),
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
});
return mapStateToProps;
@@ -33,7 +33,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
@@ -59,10 +59,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(muteAccount(account.get('id')));
dispatch(initMuteModal(account));
}
},
onMuteNotifications (account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

View File

@@ -6,15 +6,14 @@ import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Compose from '../features/standalone/compose';
import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
if (initialState) {
store.dispatch(hydrateStore(initialState));
}

View File

@@ -4,18 +4,19 @@ import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { showOnboardingOnce } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export const store = configureStore();
const hydrateAction = hydrateStore(JSON.parse(document.getElementById('initial-state').textContent));
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
export default class Mastodon extends React.PureComponent {

View File

@@ -14,20 +14,18 @@ import {
pin,
unpin,
} from '../actions/interactions';
import {
blockAccount,
muteAccount,
} from '../actions/accounts';
import { blockAccount } from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initMuteModal } from '../actions/mutes';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
});
const makeMapStateToProps = () => {
@@ -35,10 +33,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id),
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
@@ -58,7 +52,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
if (e.shiftKey || !this.boostModal) {
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
@@ -87,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onDelete (status) {
if (!this.deleteModal) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
@@ -123,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onMute (account) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id'))),
}));
dispatch(initMuteModal(account));
},
onMuteConversation (status) {

View File

@@ -7,15 +7,14 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline';
import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
if (initialState) {
store.dispatch(hydrateStore(initialState));
}

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Link } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
import { me } from '../../../initial_state';
const messages = defineMessages({
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -26,7 +27,6 @@ export default class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@@ -44,7 +44,7 @@ export default class ActionBar extends React.PureComponent {
}
render () {
const { account, me, intl } = this.props;
const { account, intl } = this.props;
let menu = [];
let extraInfo = '';

View File

@@ -5,8 +5,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me } from '../../../initial_state';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -14,19 +14,10 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
});
const makeMapStateToProps = () => {
const mapStateToProps = state => ({
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
};
class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
state = {
@@ -44,7 +35,7 @@ class Avatar extends ImmutablePureComponent {
}
render () {
const { account, autoPlayGif } = this.props;
const { account } = this.props;
const { isHovered } = this.state;
return (
@@ -71,20 +62,17 @@ class Avatar extends ImmutablePureComponent {
}
@connect(makeMapStateToProps)
@injectIntl
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
render () {
const { account, me, intl } = this.props;
const { account, intl } = this.props;
if (!account) {
return null;
@@ -124,7 +112,7 @@ export default class Header extends ImmutablePureComponent {
return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<Avatar account={account} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>

View File

@@ -12,14 +12,13 @@ import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import { FormattedMessage } from 'react-intl';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
@connect(mapStateToProps)
@@ -31,7 +30,6 @@ export default class AccountGallery extends ImmutablePureComponent {
medias: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
autoPlayGif: PropTypes.bool,
};
componentDidMount () {
@@ -67,7 +65,7 @@ export default class AccountGallery extends ImmutablePureComponent {
}
render () {
const { medias, autoPlayGif, isLoading, hasMore } = this.props;
const { medias, isLoading, hasMore } = this.props;
let loadMore = null;
@@ -100,7 +98,6 @@ export default class AccountGallery extends ImmutablePureComponent {
<MediaItem
key={media.get('id')}
media={media}
autoPlayGif={autoPlayGif}
/>
)}
{loadMore}

View File

@@ -10,7 +10,6 @@ export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@@ -61,7 +60,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account, me } = this.props;
const { account } = this.props;
if (account === null) {
return <MissingIndicator />;
@@ -71,13 +70,11 @@ export default class Header extends ImmutablePureComponent {
<div className='account-timeline__header'>
<InnerHeader
account={account}
me={me}
onFollow={this.handleFollow}
/>
<ActionBar
account={account}
me={me}
onBlock={this.handleBlock}
onMention={this.handleMention}
onReport={this.handleReport}

View File

@@ -7,19 +7,19 @@ import {
unfollowAccount,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from '../../../initial_state';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
@@ -28,8 +28,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
me: state.getIn(['meta', 'me']),
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
});
return mapStateToProps;
@@ -39,7 +37,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
@@ -77,11 +75,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id'))),
}));
dispatch(initMuteModal(account));
}
},

View File

@@ -16,7 +16,6 @@ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
me: state.getIn(['meta', 'me']),
});
@connect(mapStateToProps)
@@ -28,7 +27,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
me: PropTypes.string.isRequired,
};
componentWillMount () {
@@ -50,7 +48,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
}
render () {
const { statusIds, isLoading, hasMore, me } = this.props;
const { statusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) {
return (
@@ -70,7 +68,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds={statusIds}
isLoading={isLoading}
hasMore={hasMore}
me={me}
onScrollToBottom={this.handleScrollToBottom}
/>
</Column>

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';

View File

@@ -41,7 +41,6 @@ export default class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool,
me: PropTypes.string,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,

View File

@@ -157,7 +157,6 @@ class EmojiPickerMenu extends React.PureComponent {
intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired,
autoPlay: PropTypes.bool,
};
static defaultProps = {
@@ -235,7 +234,7 @@ class EmojiPickerMenu extends React.PureComponent {
}
render () {
const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props;
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return <div style={{ width: 299 }} />;
@@ -250,7 +249,7 @@ class EmojiPickerMenu extends React.PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
custom={buildCustomEmojis(custom_emojis, autoPlay)}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''
set='twitter'
@@ -284,7 +283,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
autoPlay: PropTypes.bool,
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
@@ -346,7 +344,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
}
render () {
const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading } = this.state;
@@ -366,7 +364,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
loading={loading}
onClose={this.onHideDropdown}
onPick={onPickEmoji}
autoPlay={autoPlay}
onSkinTone={onSkinTone}
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}

View File

@@ -22,7 +22,6 @@ const mapStateToProps = state => ({
preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
me: state.getIn(['compose', 'me']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
});

View File

@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.sort((a, b) => {
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
@@ -61,7 +61,6 @@ const getCustomEmojis = createSelector([
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
autoPlay: state.getIn(['meta', 'auto_play_gif']),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});

View File

@@ -1,9 +1,10 @@
import { connect } from 'react-redux';
import NavigationBar from '../components/navigation_bar';
import { me } from '../../../initial_state';
const mapStateToProps = state => {
return {
account: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
account: state.getIn(['accounts', me]),
};
};

View File

@@ -3,9 +3,10 @@ import { connect } from 'react-redux';
import Warning from '../components/warning';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state';
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']),
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
});
const WarningWrapper = ({ needsLockWarning }) => {

View File

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

View File

@@ -57,5 +57,21 @@ describe('emoji', () => {
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
});
it('avoid emojifying on invisible text', () => {
expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
.toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
.toEqual('<span class="invisible">:luigi:</span>');
});
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
});
});
});

View File

@@ -1,3 +1,4 @@
import { autoPlayGif } from '../../initial_state';
import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie';
@@ -5,13 +6,13 @@ const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || '';
let allowAnimations = false;
const emojify = (str, customEmojis = {}) => {
let rtn = '';
const tagCharsWithoutEmojis = '<&';
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
for (;;) {
let match, i = 0, tag;
while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
let rend, replacement = '';
@@ -27,7 +28,7 @@ const emojify = (str, customEmojis = {}) => {
// now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url;
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
return true;
}
@@ -35,7 +36,26 @@ const emojify = (str, customEmojis = {}) => {
})()) rend = ++i;
} else if (tag >= 0) { // <, &
rend = str.indexOf('>;'[tag], i + 1) + 1;
if (!rend) break;
if (!rend) {
break;
}
if (tag === 0) {
if (invisible) {
if (str[i + 1] === '/') { // closing tag
if (!--invisible) {
tagChars = tagCharsWithEmojis;
}
} else if (str[rend - 2] !== '/') { // opening tag
invisible++;
}
} else {
if (str.startsWith('<span class="invisible">', i)) {
// avoid emojifying on invisible text
invisible = 1;
tagChars = tagCharsWithoutEmojis;
}
}
}
i = rend;
} else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
@@ -51,14 +71,12 @@ const emojify = (str, customEmojis = {}) => {
export default emojify;
export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => {
export const buildCustomEmojis = (customEmojis) => {
const emojis = [];
allowAnimations = overrideAllowAnimations;
customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode');
const url = allowAnimations ? emoji.get('url') : emoji.get('static_url');
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
const name = shortcode.replace(':', '');
emojis.push({

View File

@@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => {
Object.keys(emojiIndex.emojis).forEach(key => {
const { native } = emojiIndex.emojis[key];
const { short_names, search, unified } = emojiMartData.emojis[key];
let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) {
throw new Error('The compresser expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.');
}
short_names.splice(0, 1); // first short name can be inferred from the key
short_names = short_names.slice(1); // first short name can be inferred from the key
const searchData = [native, short_names, search];
if (unicodeToUnifiedName(native) !== unified) {

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountAuthorizeContainer from './containers/account_authorize_container';

View File

@@ -8,7 +8,7 @@ import {
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';

View File

@@ -8,7 +8,7 @@ import {
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';

View File

@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../../initial_state';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -27,7 +28,7 @@ const messages = defineMessages({
});
const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
myAccount: state.getIn(['accounts', me]),
columns: state.getIn(['settings', 'columns']),
});
@@ -37,13 +38,13 @@ export default class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
me: ImmutablePropTypes.map.isRequired,
myAccount: ImmutablePropTypes.map.isRequired,
columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
};
render () {
const { intl, me, columns, multiColumn } = this.props;
const { intl, myAccount, columns, multiColumn } = this.props;
let navItems = [];
@@ -70,7 +71,7 @@ export default class GettingStarted extends ImmutablePureComponent {
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
if (myAccount.get('locked')) {
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
}

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';

View File

@@ -4,6 +4,7 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -36,7 +37,6 @@ export default class ActionBar extends React.PureComponent {
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
me: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent {
}
render () {
const { status, me, intl } = this.props;
const { status, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));

View File

@@ -22,7 +22,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool,
};
handleAccountClick = (e) => {
@@ -70,7 +69,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
media={status.get('media_attachments')}
height={300}
onOpenMedia={this.props.onOpenMedia}
autoPlayGif={this.props.autoPlayGif}
/>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator';
@@ -22,13 +23,15 @@ import {
import { deleteStatus } from '../../actions/statuses';
import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll';
import { ScrollContainer } from 'react-router-scroll-4';
import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -42,10 +45,6 @@ const makeMapStateToProps = () => {
status: getStatus(state, props.params.statusId),
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
@@ -65,17 +64,21 @@ export default class Status extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
me: PropTypes.string,
boostModal: PropTypes.bool,
deleteModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
state = {
fullscreen: false,
};
componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false;
@@ -111,7 +114,7 @@ export default class Status extends ImmutablePureComponent {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
if (e.shiftKey || !this.props.boostModal) {
if (e.shiftKey || !boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
@@ -122,7 +125,7 @@ export default class Status extends ImmutablePureComponent {
handleDeleteClick = (status) => {
const { dispatch, intl } = this.props;
if (!this.props.deleteModal) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
@@ -255,9 +258,18 @@ export default class Status extends ImmutablePureComponent {
}
}
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
render () {
let ancestors, descendants;
const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
const { status, ancestorsIds, descendantsIds } = this.props;
const { fullscreen } = this.state;
if (status === null) {
return (
@@ -291,22 +303,19 @@ export default class Status extends ImmutablePureComponent {
<ColumnBackButton />
<ScrollContainer scrollKey='thread'>
<div className='scrollable detailed-status__wrapper' ref={this.setRef}>
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className='focusable' tabIndex='0'>
<DetailedStatus
status={status}
autoPlayGif={autoPlayGif}
me={me}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
/>
<ActionBar
status={status}
me={me}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}

View File

@@ -10,6 +10,7 @@ import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import {
OnboardingModal,
MuteModal,
ReportModal,
EmbedModal,
} from '../../../features/ui/util/async-components';
@@ -20,6 +21,7 @@ const MODAL_COMPONENTS = {
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Button from '../../../components/button';
import { closeModal } from '../../../actions/modal';
import { muteAccount } from '../../../actions/accounts';
import { toggleHideNotifications } from '../../../actions/mutes';
const mapStateToProps = state => {
return {
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
account: state.getIn(['mutes', 'new', 'account']),
notifications: state.getIn(['mutes', 'new', 'notifications']),
};
};
const mapDispatchToProps = dispatch => {
return {
onConfirm(account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
onClose() {
dispatch(closeModal());
},
onToggleNotifications() {
dispatch(toggleHideNotifications());
},
};
};
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class MuteModal extends React.PureComponent {
static propTypes = {
isSubmitting: PropTypes.bool.isRequired,
account: PropTypes.object.isRequired,
notifications: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications);
}
handleCancel = () => {
this.props.onClose();
}
setRef = (c) => {
this.button = c;
}
toggleNotifications = () => {
this.props.onToggleNotifications();
}
render () {
const { account, notifications } = this.props;
return (
<div className='modal-root__modal mute-modal'>
<div className='mute-modal__container'>
<p>
<FormattedMessage
id='confirmations.mute.message'
defaultMessage='Are you sure you want to mute {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
</p>
<div>
<label htmlFor='mute-modal__hide-notifications-checkbox'>
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
{' '}
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
</label>
</div>
</div>
<div className='mute-modal__action-bar'>
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleClick} ref={this.setRef}>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button>
</div>
</div>
);
}
}

View File

@@ -11,6 +11,7 @@ import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import { List as ImmutableList } from 'immutable';
import { me } from '../../../initial_state';
const noop = () => { };
@@ -40,11 +41,11 @@ PageOne.propTypes = {
domain: PropTypes.string.isRequired,
};
const PageTwo = ({ me }) => (
const PageTwo = ({ myAccount }) => (
<div className='onboarding-modal__page onboarding-modal__page-two'>
<div className='figure non-interactive'>
<div className='pseudo-drawer'>
<NavigationBar account={me} />
<NavigationBar account={myAccount} />
</div>
<ComposeForm
text='Awoo! #introductions'
@@ -68,10 +69,10 @@ const PageTwo = ({ me }) => (
);
PageTwo.propTypes = {
me: ImmutablePropTypes.map.isRequired,
myAccount: ImmutablePropTypes.map.isRequired,
};
const PageThree = ({ me }) => (
const PageThree = ({ myAccount }) => (
<div className='onboarding-modal__page onboarding-modal__page-three'>
<div className='figure non-interactive'>
<Search
@@ -83,7 +84,7 @@ const PageThree = ({ me }) => (
/>
<div className='pseudo-drawer'>
<NavigationBar account={me} />
<NavigationBar account={myAccount} />
</div>
</div>
@@ -93,7 +94,7 @@ const PageThree = ({ me }) => (
);
PageThree.propTypes = {
me: ImmutablePropTypes.map.isRequired,
myAccount: ImmutablePropTypes.map.isRequired,
};
const PageFour = ({ domain, intl }) => (
@@ -161,7 +162,7 @@ PageSix.propTypes = {
};
const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
myAccount: state.getIn(['accounts', me]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
domain: state.getIn(['meta', 'domain']),
});
@@ -173,7 +174,7 @@ export default class OnboardingModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
me: ImmutablePropTypes.map.isRequired,
myAccount: ImmutablePropTypes.map.isRequired,
domain: PropTypes.string.isRequired,
admin: ImmutablePropTypes.map,
};
@@ -183,11 +184,11 @@ export default class OnboardingModal extends React.PureComponent {
};
componentWillMount() {
const { me, admin, domain, intl } = this.props;
const { myAccount, admin, domain, intl } = this.props;
this.pages = [
<PageOne acct={me.get('acct')} domain={domain} />,
<PageTwo me={me} />,
<PageThree me={me} />,
<PageOne acct={myAccount.get('acct')} domain={domain} />,
<PageTwo myAccount={myAccount} />,
<PageThree myAccount={myAccount} />,
<PageFour domain={domain} intl={intl} />,
<PageSix admin={admin} domain={domain} />,
];

View File

@@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
import { me } from '../../../initial_state';
const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
(state) => state.get('statuses'),
(state) => state.getIn(['meta', 'me']),
], (columnSettings, statusIds, statuses, me) => {
], (columnSettings, statusIds, statuses) => {
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
let regex = null;

View File

@@ -38,14 +38,20 @@ import {
PinnedStatuses,
} from './util/async-components';
import { HotKeys } from 'react-hotkeys';
import { me } from '../../initial_state';
import { defineMessages, injectIntl } from 'react-intl';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
const messages = defineMessages({
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
});
const mapStateToProps = state => ({
me: state.getIn(['meta', 'me']),
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']) !== '',
});
const keyMap = {
@@ -75,6 +81,7 @@ const keyMap = {
};
@connect(mapStateToProps)
@injectIntl
@withRouter
export default class UI extends React.Component {
@@ -86,8 +93,9 @@ export default class UI extends React.Component {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
isComposing: PropTypes.bool,
me: PropTypes.string,
hasComposingText: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired,
};
state = {
@@ -95,6 +103,17 @@ export default class UI extends React.Component {
draggingOver: false,
};
handleBeforeUnload = (e) => {
const { intl, isComposing, hasComposingText } = this.props;
if (isComposing && hasComposingText) {
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,
// but we set user-friendly message for other browsers, e.g. Edge.
e.returnValue = intl.formatMessage(messages.beforeUnload);
}
}
handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
@@ -169,6 +188,7 @@ export default class UI extends React.Component {
}
componentWillMount () {
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
@@ -210,6 +230,7 @@ export default class UI extends React.Component {
}
componentWillUnmount () {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);
@@ -305,7 +326,7 @@ export default class UI extends React.Component {
}
handleHotkeyGoToProfile = () => {
this.context.router.history.push(`/accounts/${this.props.me}`);
this.context.router.history.push(`/accounts/${me}`);
}
handleHotkeyGoToBlocked = () => {

View File

@@ -86,6 +86,10 @@ export function OnboardingModal () {
return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
}
export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}

View File

@@ -1,56 +1,5 @@
// Like react-motion's Motion, but checks to see if the user prefers
// reduced motion and uses a cross-fade in those cases.
import React from 'react';
import { reduceMotion } from '../../../initial_state';
import ReducedMotion from './reduced_motion';
import Motion from 'react-motion/lib/Motion';
import PropTypes from 'prop-types';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
let reduceMotion;
const extractValue = (value) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
class OptionalMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
render() {
const { style, defaultStyle, children } = this.props;
if (typeof reduceMotion !== 'boolean') {
// This never changes without a page reload, so we can just grab it
// once from the body classes as opposed to using Redux's connect(),
// which would unnecessarily update every state change
reduceMotion = document.body.classList.contains('reduce-motion');
}
if (reduceMotion) {
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
}
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default OptionalMotion;
export default reduceMotion ? ReducedMotion : Motion;

View File

@@ -7,11 +7,19 @@ import BundleColumnError from '../components/bundle_column_error';
import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components
export const WrappedSwitch = ({ multiColumn, children }) => (
<Switch>
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
</Switch>
);
export class WrappedSwitch extends React.PureComponent {
render () {
const { multiColumn, children } = this.props;
return (
<Switch>
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
</Switch>
);
}
}
WrappedSwitch.propTypes = {
multiColumn: PropTypes.bool,

View File

@@ -0,0 +1,44 @@
// Like react-motion's Motion, but reduces all animations to cross-fades
// for the benefit of users with motion sickness.
import React from 'react';
import Motion from 'react-motion/lib/Motion';
import PropTypes from 'prop-types';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
const extractValue = (value) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
class ReducedMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
render() {
const { style, defaultStyle, children } = this.props;
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default ReducedMotion;

View File

@@ -0,0 +1,13 @@
const element = document.getElementById('initial-state');
const initialState = element && JSON.parse(element.textContent);
const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
export const reduceMotion = getMeta('reduce_motion');
export const autoPlayGif = getMeta('auto_play_gif');
export const unfollowModal = getMeta('unfollow_modal');
export const boostModal = getMeta('boost_modal');
export const deleteModal = getMeta('delete_modal');
export const me = getMeta('me');
export default initialState;

View File

@@ -1,221 +1,221 @@
{
"account.block": "Bloki @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.block_domain": "Kaŝi ĉion el {domain}",
"account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
"account.edit_profile": "Redakti la profilon",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.follows": "Sekvatoj",
"account.follows_you": "Sekvas vin",
"account.media": "Media",
"account.media": "Sonbildaĵoj",
"account.mention": "Mencii @{name}",
"account.mute": "Mute @{name}",
"account.mute": "Silentigi @{name}",
"account.posts": "Mesaĝoj",
"account.report": "Report @{name}",
"account.report": "Signali @{name}",
"account.requested": "Atendas aprobon",
"account.share": "Share @{name}'s profile",
"account.share": "Diskonigi la profilon de @{name}",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi",
"account.unmute": "Unmute @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"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",
"column.blocks": "Blocked users",
"account.unblock_domain": "Malkaŝi {domain}",
"account.unfollow": "Ne plus sekvi",
"account.unmute": "Malsilentigi @{name}",
"account.view_full_profile": "Vidi plenan profilon",
"boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
"bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_column_error.retry": "Bonvolu reprovi",
"bundle_column_error.title": "Reta eraro",
"bundle_modal_error.close": "Fermi",
"bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_modal_error.retry": "Bonvolu reprovi",
"column.blocks": "Blokitaj uzantoj",
"column.community": "Loka tempolinio",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.favourites": "Favoritoj",
"column.follow_requests": "Abonpetoj",
"column.home": "Hejmo",
"column.mutes": "Muted users",
"column.mutes": "Silentigitaj uzantoj",
"column.notifications": "Sciigoj",
"column.pins": "Pinned toot",
"column.pins": "Alpinglitaj pepoj",
"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",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"column_header.hide_settings": "Kaŝi agordojn",
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
"column_header.moveRight_settings": "Movi kolumnon dekstren",
"column_header.pin": "Alpingli",
"column_header.show_settings": "Malkaŝi agordojn",
"column_header.unpin": "Depingli",
"column_subheading.navigation": "Navigado",
"column_subheading.settings": "Agordoj",
"compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
"compose_form.lock_disclaimer.lock": "ŝlosita",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
"compose_form.spoiler_placeholder": "Content warning",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"compose_form.spoiler_placeholder": "Skribu tie vian averton",
"confirmation_modal.cancel": "Malfari",
"confirmations.block.confirm": "Bloki",
"confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
"confirmations.delete.confirm": "Malaperigi",
"confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
"confirmations.domain_block.confirm": "Kaŝi la tutan reton",
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
"confirmations.mute.confirm": "Silentigi",
"confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
"confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
"embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
"embed.preview": "Ĝi aperos tiel:",
"emoji_button.activity": "Aktivecoj",
"emoji_button.custom": "Personaj",
"emoji_button.flags": "Flagoj",
"emoji_button.food": "Manĝi kaj trinki",
"emoji_button.label": "Enmeti mieneton",
"emoji_button.nature": "Naturo",
"emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objektoj",
"emoji_button.people": "Homoj",
"emoji_button.recent": "Ofte uzataj",
"emoji_button.search": "Serĉo…",
"emoji_button.search_results": "Rezultatoj de serĉo",
"emoji_button.symbols": "Simboloj",
"emoji_button.travel": "Vojaĝoj & lokoj",
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
"empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
"empty_column.home.public_timeline": "la publika tempolinio",
"empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
"follow_request.authorize": "Akcepti",
"follow_request.reject": "Rifuzi",
"getting_started.appsshort": "Aplikaĵoj",
"getting_started.faq": "Oftaj demandoj",
"getting_started.heading": "Por komenci",
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}.",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advanced",
"home.column_settings.basic": "Basic",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
"getting_started.userguide": "Gvidilo de uzo",
"home.column_settings.advanced": "Precizaj agordoj",
"home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.filter_regex": "Forfiltri per regulesprimo",
"home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn",
"home.settings": "Agordoj de la kolumno",
"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",
"navigation_bar.blocks": "Blocked users",
"lightbox.next": "Malantaŭa",
"lightbox.previous": "Antaŭa",
"loading_indicator.label": "Ŝarganta",
"media_gallery.toggle_visible": "Baskuli videblecon",
"missing_indicator.label": "Ne trovita",
"navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.edit_profile": "Redakti la profilon",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
"navigation_bar.favourites": "Favoritaj",
"navigation_bar.follow_requests": "Abonpetoj",
"navigation_bar.info": "Plia informo",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.pins": "Alpinglitaj pepoj",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.follow": "{name} sekvis vin",
"notification.mention": "{name} menciis vin",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear": "Forviŝi la sciigojn",
"notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
"notifications.column_settings.alert": "Retumilaj atentigoj",
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.favourite": "Favoritoj:",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "Puŝsciigoj",
"notifications.column_settings.push_meta": "Tiu ĉi aparato",
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.sound": "Play sound",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"notifications.column_settings.sound": "Eligi sonon",
"onboarding.done": "Farita",
"onboarding.next": "Malantaŭa",
"onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.",
"onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
"onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
"onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
"onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}",
"onboarding.page_one.welcome": "Bonvenon al Mastodono!",
"onboarding.page_six.admin": "Via instancestro estas {admin}.",
"onboarding.page_six.almost_done": "Estas preskaŭ finita…",
"onboarding.page_six.appetoot": "Bonan apepiton!",
"onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan apepiton!",
"onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.",
"onboarding.page_six.guidelines": "komunreguloj",
"onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
"onboarding.page_six.various_app": "telefon-aplikaĵoj",
"onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
"onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.",
"onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn («content warning») dank' al la piktogramoj malsupre.",
"onboarding.skip": "Pasigi",
"privacy.change": "Austigi la privateco de la mesaĝo",
"privacy.direct.long": "Vidigi nur al la menciitaj personoj",
"privacy.direct.short": "Rekta",
"privacy.private.long": "Vidigi nur al viaj sekvantoj",
"privacy.private.short": "Nursekvanta",
"privacy.public.long": "Vidigi en publikaj tempolinioj",
"privacy.public.short": "Publika",
"privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
"privacy.unlisted.short": "Nelistigita",
"relative_time.days": "{number}t",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.just_now": "nun",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Rezigni",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting",
"reply_indicator.cancel": "Malfari",
"report.placeholder": "Pliaj komentoj",
"report.submit": "Sendi",
"report.target": "Signalaĵo",
"search.placeholder": "Serĉi",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"search_popout.search_format": "Detala serĉo",
"search_popout.tips.hashtag": "kradvorto",
"search_popout.tips.status": "statkonigo",
"search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
"search_popout.tips.user": "uzanto",
"search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
"standalone.public_title": "Rigardeti…",
"status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
"status.delete": "Forigi",
"status.embed": "Embed",
"status.embed": "Enmeti",
"status.favourite": "Favori",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.load_more": "Ŝargi plie",
"status.media_hidden": "Sonbildaĵo kaŝita",
"status.mention": "Mencii @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.more": "Pli",
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Disfaldi statkonigon",
"status.pin": "Pingli al la profilo",
"status.reblog": "Diskonigi",
"status.reblogged_by": "{name} diskonigita",
"status.reblogged_by": "{name} diskonigis",
"status.reply": "Respondi",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.replyAll": "Respondi al la fadeno",
"status.report": "Signali @{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",
"status.unpin": "Unpin from profile",
"status.share": "Diskonigi",
"status.show_less": "Refaldi",
"status.show_more": "Disfaldi",
"status.unmute_conversation": "Malsilentigi konversacion",
"status.unpin": "Depingli de profilo",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.federated_timeline": "Federacia tempolinio",
"tabs_bar.home": "Hejmo",
"tabs_bar.local_timeline": "Local",
"tabs_bar.local_timeline": "Loka tempolinio",
"tabs_bar.notifications": "Sciigoj",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.description": "Describe for the visually impaired",
"upload_area.title": "Algliti por alŝuti",
"upload_button.label": "Aldoni sonbildaĵon",
"upload_form.description": "Priskribi por la misvidantaj",
"upload_form.undo": "Malfari",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
"upload_progress.label": "Alŝutanta…",
"video.close": "Fermi videon",
"video.exit_fullscreen": "Eliri el plenekrano",
"video.expand": "Vastigi videon",
"video.fullscreen": "Igi plenekrane",
"video.hide": "Kaŝi videon",
"video.mute": "Silentigi",
"video.pause": "Paŭzi",
"video.play": "Legi",
"video.unmute": "Malsilentigi"
}

View File

@@ -63,7 +63,7 @@
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
"embed.preview": "다음과 같이 표시됩니다:",
"emoji_button.activity": "활동",
"emoji_button.custom": "Custom",

View File

@@ -31,7 +31,7 @@
"column.favourites": "Favorits",
"column.follow_requests": "Demandas dabonament",
"column.home": "Acuèlh",
"column.mutes": "Personas en silenci",
"column.mutes": "Personas rescondudas",
"column.notifications": "Notificacions",
"column.pins": "Tuts penjats",
"column.public": "Flux public global",
@@ -55,12 +55,12 @@
"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.delete.confirm": "Escafar",
"confirmations.delete.message": "Sètz segur de voler escafar lestatut?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
"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.mute.confirm": "Rescondre",
"confirmations.mute.message": "Sètz segur de voler rescondre {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
@@ -135,7 +135,7 @@
"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 la gent que los de {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 interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai 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}.",
@@ -159,11 +159,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
"privacy.unlisted.short": "Pas-listat",
"relative_time.days": "fa {number}j",
"relative_time.hours": "fa {number}h",
"relative_time.days": "fa {number} d",
"relative_time.hours": "fa {number} h",
"relative_time.just_now": "ara",
"relative_time.minutes": "fa {number} minutas",
"relative_time.seconds": "fa {number} segondas",
"relative_time.minutes": "fa {number} min",
"relative_time.seconds": "fa {number} s",
"reply_indicator.cancel": "Anullar",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Mandar",
@@ -197,7 +197,7 @@
"status.share": "Partejar",
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat",
"status.unmute_conversation": "Tornar mostrar la conversacion",
"status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global",

View File

@@ -89,7 +89,7 @@
"follow_request.reject": "Odrzuć",
"getting_started.appsshort": "Aplikacje",
"getting_started.faq": "FAQ",
"getting_started.heading": "Naucz się korzystać",
"getting_started.heading": "Rozpocznij",
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
"getting_started.userguide": "Podręcznik użytkownika",
"home.column_settings.advanced": "Zaawansowane",
@@ -174,7 +174,7 @@
"search_popout.tips.status": "wpis",
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
"search_popout.tips.user": "użytkownik",
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
"search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
"standalone.public_title": "Spojrzenie w głąb…",
"status.cannot_reblog": "Ten wpis nie może zostać podbity",
"status.delete": "Usuń",

View File

@@ -161,7 +161,7 @@
"privacy.unlisted.short": "Não listada",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.just_now": "agora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancelar",

View File

@@ -63,20 +63,20 @@
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
"confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.",
"embed.preview": "Так это будет выглядеть:",
"emoji_button.activity": "Занятия",
"emoji_button.custom": "Custom",
"emoji_button.custom": "Собственные",
"emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки",
"emoji_button.label": "Вставить эмодзи",
"emoji_button.nature": "Природа",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предметы",
"emoji_button.people": "Люди",
"emoji_button.recent": "Frequently used",
"emoji_button.recent": "Последние",
"emoji_button.search": "Найти...",
"emoji_button.search_results": "Search results",
"emoji_button.search_results": "Результаты поиска",
"emoji_button.symbols": "Символы",
"emoji_button.travel": "Путешествия",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
@@ -159,34 +159,34 @@
"privacy.public.short": "Публичный",
"privacy.unlisted.long": "Не показывать в лентах",
"privacy.unlisted.short": "Скрытый",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.days": "{number}д",
"relative_time.hours": "{number}ч",
"relative_time.just_now": "только что",
"relative_time.minutes": "{number}м",
"relative_time.seconds": "{number}с",
"reply_indicator.cancel": "Отмена",
"report.placeholder": "Комментарий",
"report.submit": "Отправить",
"report.target": "Жалуемся на",
"search.placeholder": "Поиск",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_popout.search_format": "Продвинутый формат поиска",
"search_popout.tips.hashtag": "хэштег",
"search_popout.tips.status": "статус",
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
"search_popout.tips.user": "пользователь",
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
"standalone.public_title": "A look inside...",
"standalone.public_title": "Прямо сейчас",
"status.cannot_reblog": "Этот статус не может быть продвинут",
"status.delete": "Удалить",
"status.embed": "Embed",
"status.embed": "Встроить",
"status.favourite": "Нравится",
"status.load_more": "Показать еще",
"status.media_hidden": "Медиаконтент скрыт",
"status.mention": "Упомянуть @{name}",
"status.more": "More",
"status.more": "Больше",
"status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус",
"status.pin": "Pin on profile",
"status.pin": "Закрепить в профиле",
"status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)",
"status.reply": "Ответить",
@@ -194,11 +194,11 @@
"status.report": "Пожаловаться",
"status.sensitive_toggle": "Нажмите для просмотра",
"status.sensitive_warning": "Чувствительный контент",
"status.share": "Share",
"status.share": "Поделиться",
"status.show_less": "Свернуть",
"status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда",
"status.unpin": "Unpin from profile",
"status.unpin": "Открепить от профиля",
"tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",
@@ -206,16 +206,16 @@
"tabs_bar.notifications": "Уведомления",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Добавить медиаконтент",
"upload_form.description": "Describe for the visually impaired",
"upload_form.description": "Описать для людей с нарушениями зрения",
"upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
"video.close": "Закрыть видео",
"video.exit_fullscreen": "Покинуть полноэкранный режим",
"video.expand": "Развернуть видео",
"video.fullscreen": "Полноэкранный режим",
"video.hide": "Скрыть видео",
"video.mute": "Заглушить звук",
"video.pause": "Пауза",
"video.play": "Пуск",
"video.unmute": "Включить звук"
}

View File

@@ -1,25 +1,25 @@
{
"account.block": "屏蔽 @{name}",
"account.block_domain": "隐藏一切来自 {domain} 的嘟文",
"account.disclaimer_full": "下列资料不一定完整。",
"account.disclaimer_full": "此处显示的信息可能不是全部内容。",
"account.edit_profile": "修改个人资料",
"account.follow": "关注",
"account.followers": "关注者",
"account.follows": "正关注",
"account.follows_you": "关注你",
"account.follows": "正关注",
"account.follows_you": "关注你",
"account.media": "媒体",
"account.mention": "提及 @{name}",
"account.mute": " @{name} 静音",
"account.mute": "静音 @{name}",
"account.posts": "嘟文",
"account.report": "举报 @{name}",
"account.requested": "等待审批",
"account.share": "分享 @{name}的个人资料",
"account.unblock": "解除对 @{name} 的屏蔽",
"account.requested": "正在等待对方同意。点击以取消发送关注请求",
"account.share": "分享 @{name} 的个人资料",
"account.unblock": "不再屏蔽 @{name}",
"account.unblock_domain": "不再隐藏 {domain}",
"account.unfollow": "取消关注",
"account.unmute": "取消 @{name} 的静音",
"account.unmute": "不再静音 @{name}",
"account.view_full_profile": "查看完整资料",
"boost_modal.combo": "如你想在下次路过时显示,请按{combo}",
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
"bundle_column_error.body": "载入组件出错。",
"bundle_column_error.retry": "重试",
"bundle_column_error.title": "网络错误",
@@ -37,72 +37,72 @@
"column.public": "跨站公共时间轴",
"column_back_button.label": "返回",
"column_header.hide_settings": "隐藏设置",
"column_header.moveLeft_settings": "将栏左移",
"column_header.moveRight_settings": "将栏右移",
"column_header.moveLeft_settings": "将栏左移",
"column_header.moveRight_settings": "将栏右移",
"column_header.pin": "固定",
"column_header.show_settings": "显示设置",
"column_header.unpin": "取",
"column_header.unpin": "取消固定",
"column_subheading.navigation": "导航",
"column_subheading.settings": "设置",
"compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer": "你的帐户没{locked}任何人可以通过关注你来查看关注者可见的嘟文",
"compose_form.lock_disclaimer.lock": "被保护",
"compose_form.placeholder": "在想啥?",
"compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "将媒体文件标示为“敏感内容",
"compose_form.spoiler": "将部分文本藏于警告消息之后",
"compose_form.spoiler_placeholder": "敏感内容的警告消息",
"compose_form.sensitive": "将媒体文件标记为敏感内容",
"compose_form.spoiler": "折叠嘟文内容",
"compose_form.spoiler_placeholder": "折叠部分的警告消息",
"confirmation_modal.cancel": "取消",
"confirmations.block.confirm": "屏蔽",
"confirmations.block.message": "想好了,真的要屏蔽 {name}?",
"confirmations.block.message": "想好了,真的要屏蔽 {name}",
"confirmations.delete.confirm": "删除",
"confirmations.delete.message": "想好了,真的要删除这条嘟文?",
"confirmations.delete.message": "想好了,真的要删除这条嘟文",
"confirmations.domain_block.confirm": "隐藏整个网站",
"confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
"confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain}?多数情况下,屏蔽或静音几个特定的用户就应该能满足你的需要了。",
"confirmations.mute.confirm": "静音",
"confirmations.mute.message": "想好了,真的要静音 {name}?",
"confirmations.mute.message": "想好了,真的要静音 {name}",
"confirmations.unfollow.confirm": "取消关注",
"confirmations.unfollow.message": "确定要取消关注 {name}吗?",
"embed.instructions": "要内嵌此嘟文,请以下代码贴进你的网站。",
"embed.preview": "到时大概长这样",
"confirmations.unfollow.message": "确定要取消关注 {name} 吗?",
"embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
"embed.preview": "它会像这样显示出来",
"emoji_button.activity": "活动",
"emoji_button.custom": "Custom",
"emoji_button.custom": "自定义",
"emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料",
"emoji_button.label": "加入表情符号",
"emoji_button.nature": "自然",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "木有这个表情符号!(╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物体",
"emoji_button.people": "人物",
"emoji_button.recent": "Frequently used",
"emoji_button.recent": "常用",
"emoji_button.search": "搜索…",
"emoji_button.search_results": "Search results",
"emoji_button.search_results": "搜索结果",
"emoji_button.symbols": "符号",
"emoji_button.travel": "旅和地点",
"empty_column.community": "本站时间轴暂时有内容,快嘟几个来抢头香啊!",
"empty_column.hashtag": "这个标签暂时有内容。",
"emoji_button.travel": "旅和地点",
"empty_column.community": "本站时间轴暂时有内容,快嘟几个来抢头香啊!",
"empty_column.hashtag": "这个话题标签暂时有内容。",
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
"empty_column.home.public_timeline": "公共时间轴",
"empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
"follow_request.authorize": "批准",
"empty_column.notifications": "你没有收到过通知信息,快向其他用户搭讪吧。",
"empty_column.public": "这里神马都没有!写一些公的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
"follow_request.authorize": "同意",
"follow_request.reject": "拒绝",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.appsshort": "应用",
"getting_started.faq": "常见问题",
"getting_started.heading": "开始使用",
"getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
"getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub{github})贡献代码或反馈问题。",
"getting_started.userguide": "用户指南",
"home.column_settings.advanced": "高",
"home.column_settings.basic": "基本",
"home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
"home.column_settings.show_reblogs": "显示被转的嘟文",
"home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置",
"home.column_settings.advanced": "高级设置",
"home.column_settings.basic": "基本设置",
"home.column_settings.filter_regex": "使用正则表达式regex过滤",
"home.column_settings.show_reblogs": "显示转嘟",
"home.column_settings.show_replies": "显示回",
"home.settings": "栏目设置",
"lightbox.close": "关闭",
"lightbox.next": "下一步",
"lightbox.previous": "上一步",
"loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上",
"media_gallery.toggle_visible": "切换显示/隐藏",
"missing_indicator.label": "找不到内容",
"navigation_bar.blocks": "被屏蔽的用户",
"navigation_bar.community_timeline": "本站时间轴",
@@ -119,9 +119,9 @@
"notification.follow": "{name} 开始关注你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 转嘟了你的嘟文",
"notifications.clear": "清空通知纪录",
"notifications.clear_confirmation": "你确定要清空通知纪录吗?",
"notifications.column_settings.alert": "显示桌面通知",
"notifications.clear": "清空通知列表",
"notifications.clear_confirmation": "你确定要清空通知列表吗?",
"notifications.column_settings.alert": "桌面通知",
"notifications.column_settings.favourite": "你的嘟文被收藏:",
"notifications.column_settings.follow": "关注你:",
"notifications.column_settings.mention": "提及你:",
@@ -132,90 +132,91 @@
"notifications.column_settings.sound": "播放音效",
"onboarding.done": "出发!",
"onboarding.next": "下一步",
"onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
"onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
"onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
"onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络我们将这些独立但又相互连接的服务器叫做服务器实例。",
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。",
"onboarding.page_one.welcome": "欢迎来到 Mastodon!",
"onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
"onboarding.page_four.home": "你的主页上的时间轴上显示的是你关注对象的嘟文",
"onboarding.page_four.notifications": "如果有人与你互动,便会出现在通知栏中哦~",
"onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立但又相互连接的服务器叫做实例。",
"onboarding.page_one.handle": "你在 {domain}{handle} 就是你的完整帐户名称。",
"onboarding.page_one.welcome": "欢迎来到 Mastodon",
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
"onboarding.page_six.almost_done": "差不多了…",
"onboarding.page_six.appetoot": "嗷呜~",
"onboarding.page_six.apps_available": "有适用于 iOS, Android 和其它平台的 {apps} 咯~",
"onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
"onboarding.page_six.almost_done": "差不多了…",
"onboarding.page_six.appetoot": "嗷呜",
"onboarding.page_six.apps_available": "我们还有适用于 iOSAndroid 和其它平台的{apps}哦~",
"onboarding.page_six.github": "Mastodon 是自由的开软件。欢迎前往 {github} 反馈问题、提出对新功能的建议或贡献代码 :-)",
"onboarding.page_six.guidelines": "社区指南",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}",
"onboarding.page_six.various_app": "移动应用程序",
"onboarding.page_three.profile": "修改你的个人资料,比如头像、简介和昵称等等。在那还可以找到其它首选项。",
"onboarding.page_three.search": "用搜索来找人和标签,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)。",
"onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
"onboarding.skip": "好啦好啦我知道啦",
"privacy.change": "调整隐私设置",
"privacy.direct.long": "只有提及的用户能看到",
"privacy.direct.short": "私人消息",
"privacy.private.long": "只有关注你用户能看到",
"privacy.private.short": "关注者",
"privacy.public.long": "在公共时间轴显示",
"privacy.public.short": "公",
"privacy.unlisted.long": "公开,但不在公共时间轴显示",
"privacy.unlisted.short": "公开",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}",
"onboarding.page_six.various_app": "移动设备应用",
"onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。",
"onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整帐户名称(用户名@域名)。",
"onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别用来上传图片,修改嘟文可见范围,以及添加警告信息。",
"onboarding.skip": "跳过",
"privacy.change": "设置嘟文可见范围",
"privacy.direct.long": "只有提及的用户能看到",
"privacy.direct.short": "私",
"privacy.private.long": "只有关注你用户能看到",
"privacy.private.short": "关注者",
"privacy.public.long": "所有人可见,并会出现在公共时间轴",
"privacy.public.short": "公",
"privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴",
"privacy.unlisted.short": "公开",
"relative_time.days": "{number}",
"relative_time.hours": "{number}",
"relative_time.just_now": "刚刚",
"relative_time.minutes": "{number}",
"relative_time.seconds": "{number}",
"reply_indicator.cancel": "取消",
"report.placeholder": "额外消息",
"report.placeholder": "附言",
"report.submit": "提交",
"report.target": "Reporting",
"report.target": "举报 {target}",
"search.placeholder": "搜索",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"search_popout.search_format": "高级搜索格式",
"search_popout.tips.hashtag": "话题标签",
"search_popout.tips.status": "嘟文",
"search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
"search_popout.tips.user": "用户",
"search_results.total": "{count, number} 个结果",
"standalone.public_title": "大家都在干啥?",
"status.cannot_reblog": "法转嘟这条嘟文啦……",
"status.cannot_reblog": "法转嘟这条嘟文",
"status.delete": "删除",
"status.embed": "嵌入",
"status.favourite": "收藏",
"status.load_more": "加载更多",
"status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}",
"status.more": "More",
"status.mute_conversation": "静音对话",
"status.more": "更多",
"status.mute_conversation": "静音对话",
"status.open": "展开嘟文",
"status.pin": "置顶到资料",
"status.pin": "在个人资料页面置顶",
"status.reblog": "转嘟",
"status.reblogged_by": "{name} 转嘟",
"status.reply": "回",
"status.replyAll": "回应整串",
"status.reblogged_by": "{name} 转嘟",
"status.reply": "回",
"status.replyAll": "回复所有人",
"status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容",
"status.share": "Share",
"status.show_less": "减少显示",
"status.show_more": "显示更多",
"status.unmute_conversation": "解禁对话",
"status.unpin": "解除置顶",
"status.share": "分享",
"status.show_less": "隐藏内容",
"status.show_more": "显示内容",
"status.unmute_conversation": "不再静音此对话",
"status.unpin": "在个人资料页面取消置顶",
"tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页",
"tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知",
"upload_area.title": "将文件拖放至此上传",
"ui.beforeunload": "如果你现在离开 Mastodon你的草稿内容将会被丢弃。",
"upload_area.title": "将文件拖放到此处开始上传",
"upload_button.label": "上传媒体文件",
"upload_form.description": "Describe for the visually impaired",
"upload_form.undo": "还原",
"upload_progress.label": "上传中…",
"video.close": "关闭影片",
"upload_form.description": "为视觉障碍人士添加文字说明",
"upload_form.undo": "取消上传",
"upload_progress.label": "上传中…",
"video.close": "关闭视频",
"video.exit_fullscreen": "退出全屏",
"video.expand": "展开影片",
"video.expand": "展开视频",
"video.fullscreen": "全屏",
"video.hide": "隐藏影片",
"video.hide": "隐藏视频",
"video.mute": "静音",
"video.pause": "暂停",
"video.play": "播放",
"video.unmute": "解除静音"
"video.unmute": "取消静音"
}

View File

@@ -31,6 +31,7 @@ import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from '../uuid';
import { me } from '../initial_state';
const initialState = ImmutableMap({
mounted: false,
@@ -49,7 +50,6 @@ const initialState = ImmutableMap({
media_attachments: ImmutableList(),
suggestion_token: null,
suggestions: ImmutableList(),
me: null,
default_privacy: 'public',
default_sensitive: false,
resetFileKey: Math.floor((Math.random() * 0x10000)),
@@ -58,7 +58,6 @@ const initialState = ImmutableMap({
function statusToTextMentions(state, status) {
let set = ImmutableOrderedSet([]);
let me = state.get('me');
if (status.getIn(['account', 'id']) !== me) {
set = set.add(`@${status.getIn(['account', 'acct'])} `);

View File

@@ -8,7 +8,7 @@ const initialState = ImmutableList();
export default function custom_emojis(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', []), action.state.getIn(['meta', 'auto_play_gif'], false)) });
emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) });
return action.state.get('custom_emojis');
default:
return state;

View File

@@ -13,6 +13,7 @@ import settings from './settings';
import push_notifications from './push_notifications';
import status_lists from './status_lists';
import cards from './cards';
import mutes from './mutes';
import reports from './reports';
import contexts from './contexts';
import compose from './compose';
@@ -37,6 +38,7 @@ const reducers = {
settings,
push_notifications,
cards,
mutes,
reports,
contexts,
compose,

View File

@@ -4,7 +4,6 @@ import { Map as ImmutableMap } from 'immutable';
const initialState = ImmutableMap({
streaming_api_base_url: null,
access_token: null,
me: null,
});
export default function meta(state = initialState, action) {

View File

@@ -0,0 +1,29 @@
import Immutable from 'immutable';
import {
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
} from '../actions/mutes';
const initialState = Immutable.Map({
new: Immutable.Map({
isSubmitting: false,
account: null,
notifications: true,
}),
});
export default function mutes(state = initialState, action) {
switch (action.type) {
case MUTES_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['new', 'isSubmitting'], false);
state.setIn(['new', 'account'], action.account);
state.setIn(['new', 'notifications'], true);
});
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
return state.updateIn(['new', 'notifications'], (old) => !old);
default:
return state;
}
}

View File

@@ -99,9 +99,10 @@ export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_REFRESH_REQUEST:
case NOTIFICATIONS_EXPAND_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_REFRESH_FAIL:
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', true);
return state.set('isLoading', false);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:

View File

@@ -244,6 +244,15 @@
width: 0;
height: 0;
position: absolute;
img,
svg {
margin: 0 !important;
border: 0 !important;
padding: 0 !important;
width: 0 !important;
height: 0 !important;
}
}
.ellipsis {
@@ -510,6 +519,7 @@
font-weight: 400;
overflow: hidden;
white-space: pre-wrap;
padding-top: 5px;
&.status__content--with-spoiler {
white-space: normal;
@@ -520,8 +530,9 @@
}
.emojione {
width: 18px;
height: 18px;
width: 20px;
height: 20px;
margin: -5px 0 0;
}
p {
@@ -601,7 +612,7 @@
outline: 0;
background: lighten($ui-base-color, 4%);
&.status-direct {
.status.status-direct {
background: lighten($ui-base-color, 12%);
}
@@ -620,6 +631,12 @@
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
@supports (-ms-overflow-style: -ms-autohiding-scrollbar) {
// Add margin to avoid Edge auto-hiding scrollbar appearing over content.
// On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px.
padding-right: 26px; // 10px + 16px
}
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
@@ -751,7 +768,7 @@
.status__action-bar {
align-items: center;
display: flex;
margin-top: 10px;
margin-top: 5px;
}
.status__action-bar-button {
@@ -782,8 +799,9 @@
line-height: 24px;
.emojione {
width: 22px;
height: 22px;
width: 24px;
height: 24px;
margin: -5px 0 0;
}
}
@@ -888,6 +906,7 @@
.account__relationship {
height: 18px;
padding: 10px;
white-space: nowrap;
}
.account__header {
@@ -2280,6 +2299,7 @@ button.icon-button.active i.fa-retweet {
}
.column-header {
display: flex;
padding: 15px;
font-size: 16px;
background: lighten($ui-base-color, 4%);
@@ -2305,12 +2325,10 @@ button.icon-button.active i.fa-retweet {
}
.column-header__buttons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
height: 48px;
display: flex;
margin: -15px;
margin-left: 0;
}
.column-header__button {
@@ -2378,6 +2396,14 @@ button.icon-button.active i.fa-retweet {
}
}
.column-header__title {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
}
.text-btn {
display: inline-block;
padding: 0;
@@ -2581,7 +2607,7 @@ button.icon-button.active i.fa-retweet {
color: $primary-text-color;
position: absolute;
top: 10px;
right: 10px;
left: 10px;
opacity: 0.7;
display: inline-block;
vertical-align: top;
@@ -2596,7 +2622,7 @@ button.icon-button.active i.fa-retweet {
.account--action-button {
position: absolute;
top: 10px;
left: 20px;
right: 20px;
}
.setting-toggle {
@@ -3067,7 +3093,6 @@ button.icon-button.active i.fa-retweet {
right: 0;
bottom: 0;
background: rgba($base-overlay-background, 0.7);
transform: translateZ(0);
}
.modal-root__container {
@@ -3491,7 +3516,8 @@ button.icon-button.active i.fa-retweet {
.boost-modal,
.confirmation-modal,
.report-modal,
.actions-modal {
.actions-modal,
.mute-modal {
background: lighten($ui-secondary-color, 8%);
color: $ui-base-color;
border-radius: 8px;
@@ -3541,6 +3567,7 @@ button.icon-button.active i.fa-retweet {
.boost-modal__action-bar,
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.report-modal__action-bar {
display: flex;
justify-content: space-between;
@@ -3577,6 +3604,14 @@ button.icon-button.active i.fa-retweet {
}
}
.mute-modal {
line-height: 24px;
}
.mute-modal .react-toggle {
vertical-align: middle;
}
.report-modal__statuses,
.report-modal__comment {
padding: 10px;
@@ -3649,8 +3684,10 @@ button.icon-button.active i.fa-retweet {
}
}
.confirmation-modal__action-bar {
.confirmation-modal__cancel-button {
.confirmation-modal__action-bar,
.mute-modal__action-bar {
.confirmation-modal__cancel-button,
.mute-modal__cancel-button {
background-color: transparent;
color: darken($ui-secondary-color, 34%);
font-size: 14px;
@@ -3665,6 +3702,7 @@ button.icon-button.active i.fa-retweet {
}
.confirmation-modal__container,
.mute-modal__container,
.report-modal__target {
padding: 30px;
font-size: 16px;

View File

@@ -1,4 +1,5 @@
.landing-strip {
.landing-strip,
.memoriam-strip {
background: rgba(darken($ui-base-color, 7%), 0.8);
color: $ui-primary-color;
font-weight: 400;
@@ -29,3 +30,7 @@
margin-bottom: 0;
}
}
.memoriam-strip {
background: rgba($base-shadow-color, 0.7);
}

View File

@@ -53,9 +53,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_tags(status)
return unless @object['tag'].is_a?(Array)
return if @object['tag'].nil?
@object['tag'].each do |tag|
as_array(@object['tag']).each do |tag|
case tag['type']
when 'Hashtag'
process_hashtag tag, status
@@ -103,9 +103,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_attachments(status)
return unless @object['attachment'].is_a?(Array)
return if @object['attachment'].nil?
@object['attachment'].each do |attachment|
as_array(@object['attachment']).each do |attachment|
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s

View File

@@ -89,20 +89,28 @@ class Formatter
end
end
def count_tag_nesting(tag)
if tag[1] == '/' then -1
elsif tag[-2] == '/' then 0
else 1
end
end
def encode_custom_emojis(html, emojis)
return html if emojis.empty?
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
i = -1
inside_tag = false
tag_open_index = nil
inside_shortname = false
shortname_start_index = -1
invisible_depth = 0
while i + 1 < html.size
i += 1
if inside_shortname && html[i] == ':'
if invisible_depth.zero? && inside_shortname && html[i] == ':'
shortcode = html[shortname_start_index + 1..i - 1]
emoji = emoji_map[shortcode]
@@ -116,12 +124,18 @@ class Formatter
end
inside_shortname = false
elsif inside_tag && html[i] == '>'
inside_tag = false
elsif tag_open_index && html[i] == '>'
tag = html[tag_open_index..i]
tag_open_index = nil
if invisible_depth.positive?
invisible_depth += count_tag_nesting(tag)
elsif tag == '<span class="invisible">'
invisible_depth = 1
end
elsif html[i] == '<'
inside_tag = true
tag_open_index = i
inside_shortname = false
elsif !inside_tag && html[i] == ':'
elsif !tag_open_index && html[i] == ':'
inside_shortname = true
shortname_start_index = i
end

View File

@@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
@@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@account = notification.from_account
return if @me.user.disabled?
locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end
@@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account
@status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
@@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account
@status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
@@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@account = notification.from_account
return if @me.user.disabled?
locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end
@@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
return if @notifications.empty?
return if @me.user.disabled? || @notifications.empty?
locale_for_account(@me) do
mail to: @me.user.email,
subject: I18n.t(
:subject,
scope: [:notification_mailer, :digest],
count: @notifications.size
)
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
end
end

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