Compare commits

..

137 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
Marcin Mikołajczak
fdb0848e08 i18n: Update Polish Translation (#5494) 2017-10-22 08:34:39 +09:00
Nolan Lawson
8392ddbf87 Remove unnecessary translateZ(0) when doing scale() (#5473) 2017-10-19 18:27:55 +02:00
masarakki
049381b284 remove-duplicated-jest-config (#5465) 2017-10-19 13:51:38 +02:00
366 changed files with 5256 additions and 9844 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/

View File

@@ -1,36 +1,3 @@
# Contributing to Mastodon Glitch Edition #
Thank you for your interest in contributing to the `glitch-soc` project!
Here are some guidelines, and ways you can help.
> (This document is a bit of a work-in-progress, so please bear with us.
> If you don't see what you're looking for here, please don't hesitate to reach out!)
## Planning ##
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.
We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler.
## Documentation ##
The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
Right now, we've mostly focused on the features that make this fork different from upstream in some manner.
Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code.
## Frontend Development ##
Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information.
## Backend Development ##
See the guidelines below.
- - -
You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `tootsuite/mastodon`, reproduced below.
<blockquote>
CONTRIBUTING
============
@@ -82,5 +49,3 @@ It is expected that you have a working development environment set up (see back-
* If you are introducing new strings, they must be using localization methods
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
</blockquote>

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,10 +1,85 @@
# Mastodon Glitch Edition #
![Mastodon](https://i.imgur.com/NhZc40l.png)
========
> Now with automated deploys!
[![Build Status](https://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Build Status](https://travis-ci.org/glitch-soc/mastodon.svg?branch=master)](https://travis-ci.org/glitch-soc/mastodon)
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools.
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).
Click on the screenshot below to watch a demo of the UI:
[![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
**Ruby on Rails** is used for the back-end, while **React.js** and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
[patreon]: https://www.patreon.com/user?u=619786
---
## Resources
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [Use this tool to find Twitter friends on Mastodon](https://bridge.joinmastodon.org)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
- [List of sponsors](https://joinmastodon.org/sponsors)
## Features
**No vendor lock-in: Fully interoperable with any conforming platform**
It doesn't have to be Mastodon, whatever implements ActivityPub or OStatus is part of the social network!
**Real-time timeline updates**
See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
**Federated thread resolving**
If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
**Media attachments like images and short videos**
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
**OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API
**Fast response times**
Mastodon tries to be as fast and responsive as possible, so all long-running tasks are delegated to background processing
**Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
---
## Development
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
## Deployment
There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon).
## Contributing
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md)
**IRC channel**: #mastodon on irc.freenode.net
---
## Extra credits
The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo)

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

@@ -8,15 +8,10 @@ class Api::V1::MutesController < Api::BaseController
respond_to :json
def index
@data = @accounts = load_accounts
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
def details
@data = @mutes = load_mutes
render json: @mutes, each_serializer: REST::MuteSerializer
end
private
def load_accounts
@@ -27,10 +22,6 @@ class Api::V1::MutesController < Api::BaseController
Account.includes(:muted_by).references(:muted_by)
end
def load_mutes
paginated_mutes.includes(:account, :target_account).to_a
end
def paginated_mutes
Mute.where(account: current_account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
@@ -45,34 +36,26 @@ class Api::V1::MutesController < Api::BaseController
def next_path
if records_continue?
url_for pagination_params(max_id: pagination_max_id)
api_v1_mutes_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless@data.empty?
url_for pagination_params(since_id: pagination_since_id)
unless @accounts.empty?
api_v1_mutes_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
if params[:action] == "details"
@mutes.last.id
else
@accounts.last.muted_by_ids.last
end
@accounts.last.muted_by_ids.last
end
def pagination_since_id
if params[:action] == "details"
@mutes.first.id
else
@accounts.first.muted_by_ids.first
end
@accounts.first.muted_by_ids.first
end
def records_continue?
@data.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)

View File

@@ -24,20 +24,11 @@ class Api::V1::NotificationsController < Api::BaseController
render_empty
end
def destroy
dismiss
end
def dismiss
current_account.notifications.find_by!(id: params[:id]).destroy!
render_empty
end
def destroy_multiple
current_account.notifications.where(id: params[:ids]).destroy_all
render_empty
end
private
def load_notifications

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,7 +1,9 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
RESULTS_LIMIT = 10
include Authorization
RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
@@ -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

@@ -6,7 +6,6 @@ class HomeController < ApplicationController
def index
@body_classes = 'app-body'
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
end
private

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

@@ -48,7 +48,7 @@ class StreamEntriesController < ApplicationController
@type = @stream_entry.activity_type.downcase
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
authorize @stream_entry.activity, :show? if @stream_entry.hidden?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound

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

@@ -1,93 +0,0 @@
/*
`actions/local_settings`
========================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
This file provides our Redux actions related to local settings. It
consists of the following:
- __`changesLocalSetting(key, value)` :__
Changes the local setting with the given `key` to the given
`value`. `key` **MUST** be an array of strings, as required by
`Immutable.Map.prototype.getIn()`.
- __`saveLocalSettings()` :__
Saves the local settings to `localStorage` as a JSON object. We
shouldn't ever need to call this ourselves.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Constants:
----------
We provide the following constants:
- __`LOCAL_SETTING_CHANGE` :__
This string constant is used to dispatch a setting change to our
reducer in `reducers/local_settings`, where the setting is
actually changed.
*/
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
`changeLocalSetting(key, value)`:
---------------------------------
Changes the local setting with the given `key` to the given `value`.
`key` **MUST** be an array of strings, as required by
`Immutable.Map.prototype.getIn()`.
To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our
reducer in `reducers/local_settings`.
*/
export function changeLocalSetting(key, value) {
return dispatch => {
dispatch({
type: LOCAL_SETTING_CHANGE,
key,
value,
});
dispatch(saveLocalSettings());
};
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
`saveLocalSettings()`:
----------------------
Saves the local settings to `localStorage` as a JSON object.
`changeLocalSetting()` calls this whenever it changes a setting. We
shouldn't ever need to call this ourselves.
> __TODO :__
> Right now `saveLocalSettings()` doesn't keep track of which user
> is currently signed in, but it might be better to give each user
> their *own* local settings.
*/
export function saveLocalSettings() {
return (_, getState) => {
const localSettings = getState().get('local_settings').toJS();
localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
};
};

View File

@@ -1,227 +0,0 @@
/*
`<AccountHeader>`
=================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
Original file by @gargron@mastodon.social et al as part of
tootsuite/mastodon. We've expanded it in order to handle user bio
frontmatter.
The `<AccountHeader>` component provides the header for account
timelines. It is a fairly simple component which mostly just consists
of a `render()` method.
__Props:__
- __`account` (`ImmutablePropTypes.map`) :__
The account to render a header for.
- __`me` (`PropTypes.number.isRequired`) :__
The id of the currently-signed-in account.
- __`onFollow` (`PropTypes.func.isRequired`) :__
The function to call when the user clicks the "follow" button.
- __`intl` (`PropTypes.object.isRequired`) :__
Our internationalization object, inserted by `@injectIntl`.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import emojify from '../../../mastodon/features/emoji/emoji';
import IconButton from '../../../mastodon/components/icon_button';
import Avatar from '../../../mastodon/components/avatar';
// Our imports //
import { processBio } from '../../util/bio_metadata';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Inital setup:
-------------
The `messages` constant is used to define any messages that we need
from inside props. In our case, these are the `unfollow`, `follow`, and
`requested` messages used in the `title` of our buttons.
*/
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Implementation:
---------------
*/
@injectIntl
export default class AccountHeader extends ImmutablePureComponent {
static propTypes = {
account : ImmutablePropTypes.map,
me : PropTypes.string.isRequired,
onFollow : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
/*
### `render()`
The `render()` function is used to render our component.
*/
render () {
const { account, me, intl } = this.props;
/*
If no `account` is provided, then we can't render a header. Otherwise,
we get the `displayName` for the account, if available. If it's blank,
then we set the `displayName` to just be the `username` of the account.
*/
if (!account) {
return null;
}
let displayName = account.get('display_name_html');
let info = '';
let actionBtn = '';
let following = false;
/*
Next, we handle the account relationships. If the account follows the
user, then we add an `info` message. If the user has requested a
follow, then we disable the `actionBtn` and display an hourglass.
Otherwise, if the account isn't blocked, we set the `actionBtn` to the
appropriate icon.
*/
if (me !== account.get('id')) {
if (account.getIn(['relationship', 'followed_by'])) {
info = (
<span className='account--follows-info'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
);
}
if (account.getIn(['relationship', 'requested'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
</div>
);
} else if (!account.getIn(['relationship', 'blocking'])) {
following = account.getIn(['relationship', 'following']);
actionBtn = (
<div className='account--action-button'>
<IconButton
size={26}
icon={following ? 'user-times' : 'user-plus'}
active={following}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.props.onFollow}
/>
</div>
);
}
}
/*
we extract the `text` and
`metadata` from our account's `note` using `processBio()`.
*/
const { text, metadata } = processBio(account.get('note'));
/*
Here, we render our component using all the things we've defined above.
*/
return (
<div className='account__header__wrapper'>
<div
className='account__header'
style={{ backgroundImage: `url(${account.get('header')})` }}
>
<div>
<a href={account.get('url')} target='_blank' rel='noopener'>
<span className='account__header__avatar'>
<Avatar account={account} size={90} />
</span>
<span
className='account__header__display-name'
dangerouslySetInnerHTML={{ __html: displayName }}
/>
</a>
<span className='account__header__username'>
@{account.get('acct')}
{account.get('locked') ? <i className='fa fa-lock' /> : null}
</span>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
{info}
{actionBtn}
</div>
</div>
{metadata.length && (
<table className='account__metadata'>
<tbody>
{(() => {
let data = [];
for (let i = 0; i < metadata.length; i++) {
data.push(
<tr key={i}>
<th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th>
<td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td>
</tr>
);
}
return data;
})()}
</tbody>
</table>
) || null}
</div>
);
}
}

View File

@@ -1,80 +0,0 @@
/*
`<NotificationPurgeButtonsContainer>`
=========================
This container connects `<NotificationPurgeButtons>`s to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Our imports //
import NotificationPurgeButtons from './notification_purge_buttons';
import {
deleteMarkedNotifications,
enterNotificationClearingMode,
markAllNotifications,
} from '../../../../mastodon/actions/notifications';
import { defineMessages, injectIntl } from 'react-intl';
import { openModal } from '../../../../mastodon/actions/modal';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We only need to provide a dispatch for
deleting notifications.
*/
const messages = defineMessages({
clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' },
});
const mapDispatchToProps = (dispatch, { intl }) => ({
onEnterCleaningMode(yes) {
dispatch(enterNotificationClearingMode(yes));
},
onDeleteMarked() {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(deleteMarkedNotifications()),
}));
},
onMarkAll() {
dispatch(markAllNotifications(true));
},
onMarkNone() {
dispatch(markAllNotifications(false));
},
onInvert() {
dispatch(markAllNotifications(null));
},
});
const mapStateToProps = state => ({
markNewForDelete: state.getIn(['notifications', 'markNewForDelete']),
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons));

View File

@@ -1,62 +0,0 @@
/**
* Buttons widget for controlling the notification clearing mode.
* In idle state, the cleaning mode button is shown. When the mode is active,
* a Confirm and Abort buttons are shown in its place.
*/
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
});
@injectIntl
export default class NotificationPurgeButtons extends ImmutablePureComponent {
static propTypes = {
onDeleteMarked : PropTypes.func.isRequired,
onMarkAll : PropTypes.func.isRequired,
onMarkNone : PropTypes.func.isRequired,
onInvert : PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
markNewForDelete: PropTypes.bool,
};
render () {
const { intl, markNewForDelete } = this.props;
//className='active'
return (
<div className='column-header__notif-cleaning-buttons'>
<button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
<b></b><br />{intl.formatMessage(messages.btnAll)}
</button>
<button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
<b></b><br />{intl.formatMessage(messages.btnNone)}
</button>
<button onClick={this.props.onInvert}>
<b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
</button>
<button onClick={this.props.onDeleteMarked}>
<i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
</button>
</div>
);
}
}

View File

@@ -1,66 +0,0 @@
/*
`<ComposeAdvancedOptionsContainer>`
===================================
This container connects `<ComposeAdvancedOptions>` to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Mastodon imports //
import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose';
// Our imports //
import ComposeAdvancedOptions from '.';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. The only property we care about is
`compose.advanced_options`.
*/
const mapStateToProps = state => ({
values: state.getIn(['compose', 'advanced_options']),
});
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We just need to provide a dispatch for
when an advanced option toggle changes.
*/
const mapDispatchToProps = dispatch => ({
onChange (option) {
dispatch(toggleComposeAdvancedOption(option));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions);

View File

@@ -1,241 +0,0 @@
/*
`<ComposeAdvancedOptions>`
==========================
> For more information on the contents of this file, please contact:
>
> - surinna [@srn@dev.glitch.social]
This adds an advanced options dropdown to the toot compose box, for
toggles that don't necessarily fit elsewhere.
__Props:__
- __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__
An Immutable map with the following values:
- __`do_not_federate` (`PropTypes.bool.isRequired`) :__
Specifies whether or not to federate the status.
- __`onChange` (`PropTypes.func.isRequired`) :__
The function to call when a toggle is changed. We pass this from
our container to the toggle.
- __`intl` (`PropTypes.object.isRequired`) :__
Our internationalization object, inserted by `@injectIntl`.
__State:__
- __`open` :__
This tells whether the dropdown is currently open or closed.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
// Our imports //
import ComposeAdvancedOptionsToggle from './toggle';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Inital setup:
-------------
The `messages` constant is used to define any messages that we need
from inside props. These are the various titles and labels on our
toggles.
`iconStyle` styles the icon used for the dropdown button.
*/
const messages = defineMessages({
local_only_short :
{ id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
local_only_long :
{ id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' },
advanced_options_icon_title :
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
});
const iconStyle = {
height : null,
lineHeight : '27px',
};
/*
Implementation:
---------------
*/
@injectIntl
export default class ComposeAdvancedOptions extends React.PureComponent {
static propTypes = {
values : ImmutablePropTypes.contains({
do_not_federate : PropTypes.bool.isRequired,
}).isRequired,
onChange : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
state = {
open: false,
};
/*
### `onToggleDropdown()`
This function toggles the opening and closing of the advanced options
dropdown.
*/
onToggleDropdown = () => {
this.setState({ open: !this.state.open });
};
/*
### `onGlobalClick(e)`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
}
/*
### `componentDidMount()`, `componentWillUnmount()`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
/*
### `setRef(c)`
`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
*/
setRef = (c) => {
this.node = c;
}
/*
### `render()`
`render()` actually puts our component on the screen.
*/
render () {
const { open } = this.state;
const { intl, values } = this.props;
/*
The `options` array provides all of the available advanced options
alongside their icon, text, and name.
*/
const options = [
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' },
];
/*
`anyEnabled` tells us if any of our advanced options have been enabled.
*/
const anyEnabled = values.some((enabled) => enabled);
/*
`optionElems` takes our `options` and creates
`<ComposeAdvancedOptionsToggle>`s out of them. We use the `name` of the
toggle as its `key` so that React can keep track of it.
*/
const optionElems = options.map((option) => {
return (
<ComposeAdvancedOptionsToggle
onChange={this.props.onChange}
active={values.get(option.name)}
key={option.name}
name={option.name}
shortText={intl.formatMessage(option.shortText)}
longText={intl.formatMessage(option.longText)}
/>
);
});
/*
Finally, we can render our component.
*/
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className='advanced-options-dropdown__value'
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='ellipsis-h' active={open || anyEnabled}
size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>
);
}
}

View File

@@ -1,103 +0,0 @@
/*
`<ComposeAdvancedOptionsToggle>`
================================
> For more information on the contents of this file, please contact:
>
> - surinna [@srn@dev.glitch.social]
This creates the toggle used by `<ComposeAdvancedOptions>`.
__Props:__
- __`onChange` (`PropTypes.func`) :__
This provides the function to call when the toggle is
(de-?)activated.
- __`active` (`PropTypes.bool`) :__
This prop controls whether the toggle is currently active or not.
- __`name` (`PropTypes.string`) :__
This identifies the toggle, and is sent to `onChange()` when it is
called.
- __`shortText` (`PropTypes.string`) :__
This is a short string used as the title of the toggle.
- __`longText` (`PropTypes.string`) :__
This is a longer string used as a subtitle for the toggle.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import Toggle from 'react-toggle';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Implementation:
---------------
*/
export default class ComposeAdvancedOptionsToggle extends React.PureComponent {
static propTypes = {
onChange: PropTypes.func.isRequired,
active: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
shortText: PropTypes.string.isRequired,
longText: PropTypes.string.isRequired,
}
/*
### `onToggle()`
The `onToggle()` function simply calls the `onChange()` prop with the
toggle's `name`.
*/
onToggle = () => {
this.props.onChange(this.props.name);
}
/*
### `render()`
The `render()` function is used to render our component. We just render
a `<Toggle>` and place next to it our text.
*/
render() {
const { active, shortText, longText } = this.props;
return (
<div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}>
<div className='advanced-options-dropdown__option__toggle'>
<Toggle checked={active} onChange={this.onToggle} />
</div>
<div className='advanced-options-dropdown__option__content'>
<strong>{shortText}</strong>
{longText}
</div>
</div>
);
}
}

View File

@@ -1,24 +0,0 @@
// Package imports //
import { connect } from 'react-redux';
// Mastodon imports //
import { closeModal } from '../../../mastodon/actions/modal';
// Our imports //
import { changeLocalSetting } from '../../../glitch/actions/local_settings';
import LocalSettings from '.';
const mapStateToProps = state => ({
settings: state.get('local_settings'),
});
const mapDispatchToProps = dispatch => ({
onChange (setting, value) {
dispatch(changeLocalSetting(setting, value));
},
onClose () {
dispatch(closeModal());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings);

View File

@@ -1,50 +0,0 @@
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Our imports
import LocalSettingsPage from './page';
import LocalSettingsNavigation from './navigation';
// Stylesheet imports
import './style.scss';
export default class LocalSettings extends React.PureComponent {
static propTypes = {
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
settings: ImmutablePropTypes.map.isRequired,
};
state = {
currentIndex: 0,
};
navigateTo = (index) =>
this.setState({ currentIndex: +index });
render () {
const { navigateTo } = this;
const { onChange, onClose, settings } = this.props;
const { currentIndex } = this.state;
return (
<div className='glitch modal-root__modal local-settings'>
<LocalSettingsNavigation
index={currentIndex}
onClose={onClose}
onNavigate={navigateTo}
/>
<LocalSettingsPage
index={currentIndex}
onChange={onChange}
settings={settings}
/>
</div>
);
}
}

View File

@@ -1,74 +0,0 @@
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
// Our imports
import LocalSettingsNavigationItem from './item';
// Stylesheet imports
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
general: { id: 'settings.general', defaultMessage: 'General' },
collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
media: { id: 'settings.media', defaultMessage: 'Media' },
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
close: { id: 'settings.close', defaultMessage: 'Close' },
});
@injectIntl
export default class LocalSettingsNavigation extends React.PureComponent {
static propTypes = {
index : PropTypes.number,
intl : PropTypes.object.isRequired,
onClose : PropTypes.func.isRequired,
onNavigate : PropTypes.func.isRequired,
};
render () {
const { index, intl, onClose, onNavigate } = this.props;
return (
<nav className='glitch local-settings__navigation'>
<LocalSettingsNavigationItem
active={index === 0}
index={0}
onNavigate={onNavigate}
title={intl.formatMessage(messages.general)}
/>
<LocalSettingsNavigationItem
active={index === 1}
index={1}
onNavigate={onNavigate}
title={intl.formatMessage(messages.collapsed)}
/>
<LocalSettingsNavigationItem
active={index === 2}
index={2}
onNavigate={onNavigate}
title={intl.formatMessage(messages.media)}
/>
<LocalSettingsNavigationItem
active={index === 3}
href='/settings/preferences'
index={3}
icon='cog'
title={intl.formatMessage(messages.preferences)}
/>
<LocalSettingsNavigationItem
active={index === 4}
className='close'
index={4}
onNavigate={onClose}
title={intl.formatMessage(messages.close)}
/>
</nav>
);
}
}

View File

@@ -1,69 +0,0 @@
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
// Stylesheet imports
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
export default class LocalSettingsPage extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
className: PropTypes.string,
href: PropTypes.string,
icon: PropTypes.string,
index: PropTypes.number.isRequired,
onNavigate: PropTypes.func,
title: PropTypes.string,
};
handleClick = (e) => {
const { index, onNavigate } = this.props;
if (onNavigate) {
onNavigate(index);
e.preventDefault();
}
}
render () {
const { handleClick } = this;
const {
active,
className,
href,
icon,
onNavigate,
title,
} = this.props;
const finalClassName = classNames('glitch', 'local-settings__navigation__item', {
active,
}, className);
const iconElem = icon ? <i className={`fa fa-fw fa-${icon}`} /> : null;
if (href) return (
<a
href={href}
className={finalClassName}
>
{iconElem} {title}
</a>
);
else if (onNavigate) return (
<a
onClick={handleClick}
role='button'
tabIndex='0'
className={finalClassName}
>
{iconElem} {title}
</a>
);
else return null;
}
}

View File

@@ -1,27 +0,0 @@
@import 'styles/mastodon/variables';
.glitch.local-settings__navigation__item {
display: block;
padding: 15px 20px;
color: inherit;
background: $primary-text-color;
border-bottom: 1px $ui-primary-color solid;
cursor: pointer;
text-decoration: none;
outline: none;
transition: background .3s;
&:hover {
background: $ui-secondary-color;
}
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
}
&.close, &.close:hover {
background: $error-value-color;
color: $primary-text-color;
}
}

View File

@@ -1,10 +0,0 @@
@import 'styles/mastodon/variables';
.glitch.local-settings__navigation {
background: $primary-text-color;
color: $ui-base-color;
width: 200px;
font-size: 15px;
line-height: 20px;
overflow-y: auto;
}

View File

@@ -1,202 +0,0 @@
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
// Our imports
import LocalSettingsPageItem from './item';
// Stylesheet imports
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' },
layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' },
side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
});
@injectIntl
export default class LocalSettingsPage extends React.PureComponent {
static propTypes = {
index : PropTypes.number,
intl : PropTypes.object.isRequired,
onChange : PropTypes.func.isRequired,
settings : ImmutablePropTypes.map.isRequired,
};
pages = [
({ intl, onChange, settings }) => (
<div className='glitch local-settings__page general'>
<h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
<LocalSettingsPageItem
settings={settings}
item={['layout']}
id='mastodon-settings--layout'
options={[
{ value: 'auto', message: intl.formatMessage(messages.layout_auto) },
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
{ value: 'single', message: intl.formatMessage(messages.layout_mobile) },
]}
onChange={onChange}
>
<FormattedMessage id='settings.layout' defaultMessage='Layout:' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['stretch']}
id='mastodon-settings--stretch'
onChange={onChange}
>
<FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['navbar_under']}
id='mastodon-settings--navbar_under'
onChange={onChange}
>
<FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
</LocalSettingsPageItem>
<section>
<h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2>
<LocalSettingsPageItem
settings={settings}
item={['side_arm']}
id='mastodon-settings--side_arm'
options={[
{ value: 'none', message: intl.formatMessage(messages.side_arm_none) },
{ value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) },
{ value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) },
{ value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) },
{ value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) },
]}
onChange={onChange}
>
<FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' />
</LocalSettingsPageItem>
</section>
</div>
),
({ onChange, settings }) => (
<div className='glitch local-settings__page collapsed'>
<h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'enabled']}
id='mastodon-settings--collapsed-enabled'
onChange={onChange}
>
<FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
</LocalSettingsPageItem>
<section>
<h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'all']}
id='mastodon-settings--collapsed-auto-all'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'notifications']}
id='mastodon-settings--collapsed-auto-notifications'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'lengthy']}
id='mastodon-settings--collapsed-auto-lengthy'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'replies']}
id='mastodon-settings--collapsed-auto-replies'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'media']}
id='mastodon-settings--collapsed-auto-media'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
</LocalSettingsPageItem>
</section>
<section>
<h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'backgrounds', 'user_backgrounds']}
id='mastodon-settings--collapsed-user-backgrouns'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'backgrounds', 'preview_images']}
id='mastodon-settings--collapsed-preview-images'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
</LocalSettingsPageItem>
</section>
</div>
),
({ onChange, settings }) => (
<div className='glitch local-settings__page media'>
<h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1>
<LocalSettingsPageItem
settings={settings}
item={['media', 'letterbox']}
id='mastodon-settings--media-letterbox'
onChange={onChange}
>
<FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['media', 'fullwidth']}
id='mastodon-settings--media-fullwidth'
onChange={onChange}
>
<FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' />
</LocalSettingsPageItem>
</div>
),
];
render () {
const { pages } = this;
const { index, intl, onChange, settings } = this.props;
const CurrentPage = pages[index] || pages[0];
return <CurrentPage intl={intl} onChange={onChange} settings={settings} />;
}
}

View File

@@ -1,90 +0,0 @@
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Stylesheet imports
import './style.scss';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
export default class LocalSettingsPageItem extends React.PureComponent {
static propTypes = {
children: PropTypes.element.isRequired,
dependsOn: PropTypes.array,
dependsOnNot: PropTypes.array,
id: PropTypes.string.isRequired,
item: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
})),
settings: ImmutablePropTypes.map.isRequired,
};
handleChange = e => {
const { target } = e;
const { item, onChange, options } = this.props;
if (options && options.length > 0) onChange(item, target.value);
else onChange(item, target.checked);
}
render () {
const { handleChange } = this;
const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props;
let enabled = true;
if (dependsOn) {
for (let i = 0; i < dependsOn.length; i++) {
enabled = enabled && settings.getIn(dependsOn[i]);
}
}
if (dependsOnNot) {
for (let i = 0; i < dependsOnNot.length; i++) {
enabled = enabled && !settings.getIn(dependsOnNot[i]);
}
}
if (options && options.length > 0) {
const currentValue = settings.getIn(item);
const optionElems = options && options.length > 0 && options.map((opt) => (
<option
key={opt.value}
value={opt.value}
>
{opt.message}
</option>
));
return (
<label className='glitch local-settings__page__item' htmlFor={id}>
<p>{children}</p>
<p>
<select
id={id}
disabled={!enabled}
onBlur={handleChange}
onChange={handleChange}
value={currentValue}
>
{optionElems}
</select>
</p>
</label>
);
} else return (
<label className='glitch local-settings__page__item' htmlFor={id}>
<input
id={id}
type='checkbox'
checked={settings.getIn(item)}
onChange={handleChange}
disabled={!enabled}
/>
{children}
</label>
);
}
}

View File

@@ -1,7 +0,0 @@
@import 'styles/mastodon/variables';
.glitch.local-settings__page__item {
select {
margin-bottom: 5px;
}
}

View File

@@ -1,9 +0,0 @@
@import 'styles/mastodon/variables';
.glitch.local-settings__page {
display: block;
flex: auto;
padding: 15px 20px 15px 20px;
width: 360px;
overflow-y: auto;
}

View File

@@ -1,34 +0,0 @@
@import 'styles/mastodon/variables';
.glitch.local-settings {
position: relative;
display: flex;
flex-direction: row;
background: $ui-secondary-color;
color: $ui-base-color;
border-radius: 8px;
height: 80vh;
width: 80vw;
max-width: 740px;
max-height: 450px;
overflow: hidden;
label {
display: block;
}
h1 {
font-size: 18px;
font-weight: 500;
line-height: 24px;
margin-bottom: 20px;
}
h2 {
font-size: 15px;
font-weight: 500;
line-height: 20px;
margin-top: 20px;
margin-bottom: 10px;
}
}

View File

@@ -1,48 +0,0 @@
/*
`<NotificationContainer>`
=========================
This container connects `<Notification>`s to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Our imports //
import Notification from '.';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const mapStateToProps = (state, props) => {
// replace account id with object
let leNotif = props.notification.set('account', state.getIn(['accounts', props.notification.get('account')]));
// populate markedForDelete from state - is mysteriously lost somewhere
for (let n of state.getIn(['notifications', 'items'])) {
if (n.get('id') === props.notification.get('id')) {
leNotif = leNotif.set('markedForDelete', n.get('markedForDelete'));
break;
}
}
return ({
notification: leNotif,
settings: state.get('local_settings'),
notifCleaning: state.getIn(['notifications', 'cleaningMode']),
});
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
export default connect(mapStateToProps)(Notification);

View File

@@ -1,72 +0,0 @@
// `<NotificationFollow>`
// ======================
// * * * * * * * //
// Imports
// -------
// Package imports.
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports.
import Permalink from '../../../mastodon/components/permalink';
import AccountContainer from '../../../mastodon/containers/account_container';
// Our imports.
import NotificationOverlayContainer from '../notification/overlay/container';
// * * * * * * * //
// Implementation
// --------------
export default class NotificationFollow extends ImmutablePureComponent {
static propTypes = {
id : PropTypes.string.isRequired,
account : ImmutablePropTypes.map.isRequired,
notification : ImmutablePropTypes.map.isRequired,
};
render () {
const { account, notification } = this.props;
// Links to the display name.
const displayName = account.get('display_name_html') || account.get('username');
const link = (
<Permalink
className='notification__display-name'
href={account.get('url')}
title={account.get('acct')}
to={`/accounts/${account.get('id')}`}
dangerouslySetInnerHTML={{ __html: displayName }}
/>
);
// Renders.
return (
<div className='notification notification-follow'>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={{ name: link }}
/>
</div>
<AccountContainer id={account.get('id')} withNote={false} />
<NotificationOverlayContainer notification={notification} />
</div>
);
}
}

View File

@@ -1,82 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
// Our imports //
import StatusContainer from '../status/container';
import NotificationFollow from './follow';
export default class Notification extends ImmutablePureComponent {
static propTypes = {
notification: ImmutablePropTypes.map.isRequired,
settings: ImmutablePropTypes.map.isRequired,
};
renderFollow (notification) {
return (
<NotificationFollow
id={notification.get('id')}
account={notification.get('account')}
notification={notification}
/>
);
}
renderMention (notification) {
return (
<StatusContainer
id={notification.get('status')}
notification={notification}
withDismiss
/>
);
}
renderFavourite (notification) {
return (
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
prepend='favourite'
muted
notification={notification}
withDismiss
/>
);
}
renderReblog (notification) {
return (
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
prepend='reblog'
muted
notification={notification}
withDismiss
/>
);
}
render () {
const { notification } = this.props;
switch(notification.get('type')) {
case 'follow':
return this.renderFollow(notification);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification);
case 'reblog':
return this.renderReblog(notification);
}
return null;
}
}

View File

@@ -1,49 +0,0 @@
/*
`<NotificationOverlayContainer>`
=========================
This container connects `<NotificationOverlay>`s to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Our imports //
import NotificationOverlay from './notification_overlay';
import { markNotificationForDelete } from '../../../../mastodon/actions/notifications';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We only need to provide a dispatch for
deleting notifications.
*/
const mapDispatchToProps = dispatch => ({
onMarkForDelete(id, yes) {
dispatch(markNotificationForDelete(id, yes));
},
});
const mapStateToProps = state => ({
show: state.getIn(['notifications', 'cleaningMode']),
});
export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);

View File

@@ -1,61 +0,0 @@
/**
* Notification overlay
*/
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
// Mastodon imports //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' },
});
@injectIntl
export default class NotificationOverlay extends ImmutablePureComponent {
static propTypes = {
notification : ImmutablePropTypes.map.isRequired,
onMarkForDelete : PropTypes.func.isRequired,
show : PropTypes.bool.isRequired,
intl : PropTypes.object.isRequired,
};
onToggleMark = () => {
const mark = !this.props.notification.get('markedForDelete');
const id = this.props.notification.get('id');
this.props.onMarkForDelete(id, mark);
}
render () {
const { notification, show, intl } = this.props;
const active = notification.get('markedForDelete');
const label = intl.formatMessage(messages.markForDeletion);
return show ? (
<div
aria-label={label}
role='checkbox'
aria-checked={active}
tabIndex={0}
className={`notification__dismiss-overlay ${active ? 'active' : ''}`}
onClick={this.onToggleMark}
>
<div className='wrappy'>
<div className='ckbox' aria-hidden='true' title={label}>
{active ? (<i className='fa fa-check' />) : ''}
</div>
</div>
</div>
) : null;
}
}

View File

@@ -1,188 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
import IconButton from '../../../mastodon/components/icon_button';
import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
share: { id: 'status.share', defaultMessage: 'Share' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
});
@injectIntl
export default class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
onBlock: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
me: PropTypes.string,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'me',
'withDismiss',
]
handleReplyClick = () => {
this.props.onReply(this.props.status, this.context.router.history);
}
handleShareClick = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
}
handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e);
}
handleDeleteClick = () => {
this.props.onDelete(this.props.status);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
handleMuteClick = () => {
this.props.onMute(this.props.status.get('account'));
}
handleBlockClick = () => {
this.props.onBlock(this.props.status.get('account'));
}
handleOpen = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
handleReport = () => {
this.props.onReport(this.props.status);
}
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}
render () {
const { status, me, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
let menu = [];
let reblogIcon = 'retweet';
let replyIcon;
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
);
}
}

View File

@@ -1,265 +0,0 @@
/*
`<StatusContainer>`
===================
Original file by @gargron@mastodon.social et al as part of
tootsuite/mastodon. Documentation by @kibi@glitch.social. The code
detecting reblogs has been moved here from <Status>.
*/
/* * * * */
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import { connect } from 'react-redux';
import {
defineMessages,
injectIntl,
FormattedMessage,
} from 'react-intl';
// Mastodon imports //
import { makeGetStatus } from '../../../mastodon/selectors';
import {
replyCompose,
mentionCompose,
} from '../../../mastodon/actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
pin,
unpin,
} from '../../../mastodon/actions/interactions';
import { blockAccount } from '../../../mastodon/actions/accounts';
import { initMuteModal } from '../../../mastodon/actions/mutes';
import {
muteStatus,
unmuteStatus,
deleteStatus,
} from '../../../mastodon/actions/statuses';
import { initReport } from '../../../mastodon/actions/reports';
import { openModal } from '../../../mastodon/actions/modal';
// Our imports //
import Status from '.';
/* * * * */
/*
Inital setup:
-------------
The `messages` constant is used to define any messages that we will
need in our component. In our case, these are the various confirmation
messages used with statuses.
*/
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',
},
});
/* * * * */
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. We wrap this in a `makeMapStateToProps()`
function to give us closure and preserve `getStatus()` across function
calls.
*/
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, ownProps) => {
let status = getStatus(state, ownProps.id);
if(status === null) {
console.error(`ERROR! NULL STATUS! ${ownProps.id}`);
// work-around: find first good status
for (let k of state.get('statuses').keys()) {
status = getStatus(state, k);
if (status !== null) break;
}
}
let reblogStatus = status.get('reblog', null);
let account = undefined;
let prepend = undefined;
/*
Here we process reblogs. If our status is a reblog, then we create a
`prependMessage` to pass along to our `<Status>` along with the
reblogger's `account`, and set `coreStatus` (the one we will actually
render) to the status which has been reblogged.
*/
if (reblogStatus !== null && typeof reblogStatus === 'object') {
account = status.get('account');
status = reblogStatus;
prepend = 'reblogged_by';
}
/*
Here are the props we pass to `<Status>`.
*/
return {
status : status,
account : account || ownProps.account,
me : state.getIn(['meta', 'me']),
settings : state.get('local_settings'),
prepend : prepend || ownProps.prepend,
reblogModal : state.getIn(['meta', 'boost_modal']),
deleteModal : state.getIn(['meta', 'delete_modal']),
autoPlayGif : state.getIn(['meta', 'auto_play_gif']),
};
};
return mapStateToProps;
};
/* * * * */
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We need to provide dispatches for all
of the things you can do with a status: reply, reblog, favourite, et
cetera.
For a few of these dispatches, we open up confirmation modals; the rest
just immediately execute their corresponding actions.
*/
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
dispatch(replyCompose(status, router));
},
onModalReblog (status) {
dispatch(reblog(status));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
if (e.shiftKey || !this.reblogModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
}
}
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
} else {
dispatch(pin(status));
}
},
onEmbed (status) {
dispatch(openModal('EMBED', { url: status.get('url') }));
},
onDelete (status) {
if (!this.deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'))),
}));
}
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
},
onOpenMedia (media, index) {
dispatch(openModal('MEDIA', { media, index }));
},
onOpenVideo (media, time) {
dispatch(openModal('VIDEO', { media, time }));
},
onBlock (account) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
}));
},
onReport (status) {
dispatch(initReport(status.get('account'), status));
},
onMute (account) {
dispatch(initMuteModal(account));
},
onMuteConversation (status) {
if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id')));
} else {
dispatch(muteStatus(status.get('id')));
}
},
});
export default injectIntl(
connect(makeMapStateToProps, mapDispatchToProps)(Status)
);

View File

@@ -1,241 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classnames from 'classnames';
// Mastodon imports //
import { isRtl } from '../../../mastodon/rtl';
import Permalink from '../../../mastodon/components/permalink';
export default class StatusContent extends React.PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
expanded: PropTypes.oneOf([true, false, null]),
setExpansion: PropTypes.func,
onHeightUpdate: PropTypes.func,
media: PropTypes.element,
mediaIcon: PropTypes.string,
parseClick: PropTypes.func,
disabled: PropTypes.bool,
};
state = {
hidden: true,
};
componentDidMount () {
const node = this.node;
const links = node.querySelectorAll('a');
for (let i = 0; i < links.length; ++i) {
let link = links[i];
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', mention.get('acct'));
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else {
link.addEventListener('click', this.onLinkClick.bind(this), false);
link.setAttribute('title', link.href);
}
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
}
componentDidUpdate () {
if (this.props.onHeightUpdate) {
this.props.onHeightUpdate();
}
}
onLinkClick = (e) => {
if (this.props.expanded === false) {
if (this.props.parseClick) this.props.parseClick(e);
}
}
onMentionClick = (mention, e) => {
if (this.props.parseClick) {
this.props.parseClick(e, `/accounts/${mention.get('id')}`);
}
}
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (this.props.parseClick) {
this.props.parseClick(e, `/timelines/tag/${hashtag}`);
}
}
handleMouseDown = (e) => {
this.startXY = [e.clientX, e.clientY];
}
handleMouseUp = (e) => {
const { parseClick } = this.props;
if (!this.startXY) {
return;
}
const [ startX, startY ] = this.startXY;
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
return;
}
if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
parseClick(e);
}
this.startXY = null;
}
handleSpoilerClick = (e) => {
e.preventDefault();
if (this.props.setExpansion) {
this.props.setExpansion(this.props.expanded ? null : true);
} else {
this.setState({ hidden: !this.state.hidden });
}
}
setRef = (c) => {
this.node = c;
}
render () {
const {
status,
media,
mediaIcon,
parseClick,
disabled,
} = this.props;
const hidden = (
this.props.setExpansion ?
!this.props.expanded :
this.state.hidden
);
const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': parseClick && !disabled,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
}
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
const mentionLinks = status.get('mentions').map(item => (
<Permalink
to={`/accounts/${item.get('id')}`}
href={item.get('url')}
key={item.get('id')}
className='mention'
>
@<span>{item.get('username')}</span>
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
const toggleText = hidden ? [
<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/>,
mediaIcon ? (
<i
className={
`fa fa-fw fa-${mediaIcon} status__content__spoiler-icon`
}
aria-hidden='true'
key='1'
/>
) : null,
] : [
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/>,
];
if (hidden) {
mentionsPlaceholder = <div>{mentionLinks}</div>;
}
return (
<div className={classNames}>
<p
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
<button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
{toggleText}
</button>
</p>
{mentionsPlaceholder}
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
<div
ref={this.setRef}
style={directionStyle}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
{media}
</div>
</div>
);
} else if (parseClick) {
return (
<div
className={classNames}
style={directionStyle}
>
<div
ref={this.setRef}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
{media}
</div>
);
} else {
return (
<div
className='status__content'
style={directionStyle}
>
<div ref={this.setRef} dangerouslySetInnerHTML={content} />
{media}
</div>
);
}
}
}

View File

@@ -1,79 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
// Our imports //
import StatusGalleryItem from './item';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
});
@injectIntl
export default class StatusGallery extends React.PureComponent {
static propTypes = {
sensitive: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired,
letterbox: PropTypes.bool,
fullwidth: PropTypes.bool,
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
state = {
visible: !this.props.sensitive,
};
handleOpen = () => {
this.setState({ visible: !this.state.visible });
}
handleClick = (index) => {
this.props.onOpenMedia(this.props.media, index);
}
render () {
const { media, intl, sensitive, letterbox, fullwidth } = this.props;
let children;
if (!this.state.visible) {
let warning;
if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else {
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
}
children = (
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
<span className='media-spoiler__warning'>{warning}</span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else {
const size = media.take(4).size;
children = media.take(4).map((attachment, i) => <StatusGalleryItem key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} letterbox={letterbox} />);
}
return (
<div className={`media-gallery ${fullwidth ? 'full-width' : ''}`} style={{ height: `${this.props.height}px` }}>
<div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
</div>
{children}
</div>
);
}
}

View File

@@ -1,158 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
// Mastodon imports //
import { isIOS } from '../../../../mastodon/is_mobile';
export default class StatusGalleryItem extends React.PureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
letterbox: PropTypes.bool,
onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
handleMouseEnter = (e) => {
if (this.hoverToPlay()) {
e.target.play();
}
}
handleMouseLeave = (e) => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
hoverToPlay () {
const { attachment, autoPlayGif } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
handleClick = (e) => {
const { index, onClick } = this.props;
if (e.button === 0) {
e.preventDefault();
onClick(index);
}
e.stopPropagation();
}
render () {
const { attachment, index, size, letterbox } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
let thumbnail = '';
if (attachment.get('type') === 'image') {
const previewUrl = attachment.get('preview_url');
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
const originalUrl = attachment.get('url');
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
>
<img
className={letterbox ? 'letterbox' : ''}
src={previewUrl} srcSet={srcSet}
sizes={sizes}
alt={attachment.get('description')}
title={attachment.get('description')}
/>
</a>
);
} else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif;
thumbnail = (
<div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
<video
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
role='application'
src={attachment.get('url')}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
/>
<span className='media-gallery__gifv__label'>GIF</span>
</div>
);
}
return (
<div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
{thumbnail}
</div>
);
}
}

View File

@@ -1,146 +0,0 @@
/*
`<StatusHeader>`
================
Originally a part of `<Status>`, but extracted into a separate
component for better documentation and maintainance by
@kibi@glitch.social as a part of glitch-soc/mastodon.
*/
// * * * * * * * //
// Imports
// -------
// Package imports.
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
// Mastodon imports.
import Avatar from '../../../mastodon/components/avatar';
import AvatarOverlay from '../../../mastodon/components/avatar_overlay';
import DisplayName from '../../../mastodon/components/display_name';
import IconButton from '../../../mastodon/components/icon_button';
import VisibilityIcon from './visibility_icon';
// * * * * * * * //
// Initial setup
// -------------
// Messages for use with internationalization stuff.
const messages = defineMessages({
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
// * * * * * * * //
// The component
// -------------
@injectIntl
export default class StatusHeader extends React.PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map,
mediaIcon: PropTypes.string,
collapsible: PropTypes.bool,
collapsed: PropTypes.bool,
parseClick: PropTypes.func.isRequired,
setExpansion: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
// Handles clicks on collapsed button
handleCollapsedClick = (e) => {
const { collapsed, setExpansion } = this.props;
if (e.button === 0) {
setExpansion(collapsed ? null : false);
e.preventDefault();
}
}
// Handles clicks on account name/image
handleAccountClick = (e) => {
const { status, parseClick } = this.props;
parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`);
}
// Rendering.
render () {
const {
status,
friend,
mediaIcon,
collapsible,
collapsed,
intl,
} = this.props;
const account = status.get('account');
return (
<header className='status__info'>
<a
href={account.get('url')}
target='_blank'
className='status__avatar'
onClick={this.handleAccountClick}
>
{
friend ? (
<AvatarOverlay account={account} friend={friend} />
) : (
<Avatar account={account} size={48} />
)
}
</a>
<a
href={account.get('url')}
target='_blank'
className='status__display-name'
onClick={this.handleAccountClick}
>
<DisplayName account={account} />
</a>
<div className='status__info__icons'>
{mediaIcon ? (
<i
className={`fa fa-fw fa-${mediaIcon}`}
aria-hidden='true'
/>
) : null}
{(
<VisibilityIcon visibility={status.get('visibility')} />
)}
{collapsible ? (
<IconButton
className='status__collapse-button'
animate flip
active={collapsed}
title={
collapsed ?
intl.formatMessage(messages.uncollapse) :
intl.formatMessage(messages.collapse)
}
icon='angle-double-up'
onClick={this.handleCollapsedClick}
/>
) : null}
</div>
</header>
);
}
}

View File

@@ -1,766 +0,0 @@
/*
`<Status>`
==========
Original file by @gargron@mastodon.social et al as part of
tootsuite/mastodon. *Heavily* rewritten (and documented!) by
@kibi@glitch.social as a part of glitch-soc/mastodon. The following
features have been added:
- Better separating the "guts" of statuses from their wrapper(s)
- Collapsing statuses
- Moving images inside of CWs
A number of aspects of this original file have been split off into
their own components for better maintainance; for these, see:
- <StatusHeader>
- <StatusPrepend>
…And, of course, the other <Status>-related components as well.
*/
/* * * * */
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
// Our imports //
import StatusPrepend from './prepend';
import StatusHeader from './header';
import StatusContent from './content';
import StatusActionBar from './action_bar';
import StatusGallery from './gallery';
import StatusPlayer from './player';
import NotificationOverlayContainer from '../notification/overlay/container';
/* * * * */
/*
The `<Status>` component:
-------------------------
The `<Status>` component is a container for statuses. It consists of a
few parts:
- The `<StatusPrepend>`, which contains tangential information about
the status, such as who reblogged it.
- The `<StatusHeader>`, which contains the avatar and username of the
status author, as well as a media icon and the "collapse" toggle.
- The `<StatusContent>`, which contains the content of the status.
- The `<StatusActionBar>`, which provides actions to be performed
on statuses, like reblogging or sending a reply.
### Context
- __`router` (`PropTypes.object`) :__
We need to get our router from the surrounding React context.
### Props
- __`id` (`PropTypes.number`) :__
The id of the status.
- __`status` (`ImmutablePropTypes.map`) :__
The status object, straight from the store.
- __`account` (`ImmutablePropTypes.map`) :__
Don't be confused by this one! This is **not** the account which
posted the status, but the associated account with any further
action (eg, a reblog or a favourite).
- __`settings` (`ImmutablePropTypes.map`) :__
These are our local settings, fetched from our store. We need this
to determine how best to collapse our statuses, among other things.
- __`me` (`PropTypes.number`) :__
This is the id of the currently-signed-in user.
- __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`,
`onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
`onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
These are all functions passed through from the
`<StatusContainer>`. We don't deal with them directly here.
- __`reblogModal`, `deleteModal` (`PropTypes.bool`) :__
These tell whether or not the user has modals activated for
reblogging and deleting statuses. They are used by the `onReblog`
and `onDelete` functions, but we don't deal with them here.
- __`autoPlayGif` (`PropTypes.bool`) :__
This tells the frontend whether or not to autoplay gifs!
- __`muted` (`PropTypes.bool`) :__
This has nothing to do with a user or conversation mute! "Muted" is
what Mastodon internally calls the subdued look of statuses in the
notifications column. This should be `true` for notifications, and
`false` otherwise.
- __`collapse` (`PropTypes.bool`) :__
This prop signals a directive from a higher power to (un)collapse
a status. Most of the time it should be `undefined`, in which case
we do nothing.
- __`prepend` (`PropTypes.string`) :__
The type of prepend: `'reblogged_by'`, `'reblog'`, or
`'favourite'`.
- __`withDismiss` (`PropTypes.bool`) :__
Whether or not the status can be dismissed. Used for notifications.
- __`intersectionObserverWrapper` (`PropTypes.object`) :__
This holds our intersection observer. In Mastodon parlance,
an "intersection" is just when the status is viewable onscreen.
### State
- __`isExpanded` :__
Should be either `true`, `false`, or `null`. The meanings of
these values are as follows:
- __`true` :__ The status contains a CW and the CW is expanded.
- __`false` :__ The status is collapsed.
- __`null` :__ The status is not collapsed or expanded.
- __`isIntersecting` :__
This boolean tells us whether or not the status is currently
onscreen.
- __`isHidden` :__
This boolean tells us if the status has been unrendered to save
CPUs.
*/
export default class Status extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes.object,
};
static propTypes = {
id : PropTypes.string,
status : ImmutablePropTypes.map,
account : ImmutablePropTypes.map,
settings : ImmutablePropTypes.map,
notification : ImmutablePropTypes.map,
me : PropTypes.string,
onFavourite : PropTypes.func,
onReblog : PropTypes.func,
onModalReblog : PropTypes.func,
onDelete : PropTypes.func,
onPin : PropTypes.func,
onMention : PropTypes.func,
onMute : PropTypes.func,
onMuteConversation : PropTypes.func,
onBlock : PropTypes.func,
onEmbed : PropTypes.func,
onHeightChange : PropTypes.func,
onReport : PropTypes.func,
onOpenMedia : PropTypes.func,
onOpenVideo : PropTypes.func,
reblogModal : PropTypes.bool,
deleteModal : PropTypes.bool,
autoPlayGif : PropTypes.bool,
muted : PropTypes.bool,
collapse : PropTypes.bool,
prepend : PropTypes.string,
withDismiss : PropTypes.bool,
intersectionObserverWrapper : PropTypes.object,
};
state = {
isExpanded : null,
isIntersecting : true,
isHidden : false,
markedForDelete : false,
}
/*
### Implementation
#### `updateOnProps` and `updateOnStates`.
`updateOnProps` and `updateOnStates` tell the component when to update.
We specify them explicitly because some of our props are dynamically=
generated functions, which would otherwise always trigger an update.
Of course, this means that if we add an important prop, we will need
to remember to specify it here.
*/
updateOnProps = [
'status',
'account',
'settings',
'prepend',
'me',
'boostModal',
'autoPlayGif',
'muted',
'collapse',
'notification',
]
updateOnStates = [
'isExpanded',
'markedForDelete',
]
/*
#### `componentWillReceiveProps()`.
If our settings have changed to disable collapsed statuses, then we
need to make sure that we uncollapse every one. We do that by watching
for changes to `settings.collapsed.enabled` in
`componentWillReceiveProps()`.
We also need to watch for changes on the `collapse` prop---if this
changes to anything other than `undefined`, then we need to collapse or
uncollapse our status accordingly.
*/
componentWillReceiveProps (nextProps) {
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
if (this.state.isExpanded === false) {
this.setExpansion(null);
}
} else if (
nextProps.collapse !== this.props.collapse &&
nextProps.collapse !== undefined
) this.setExpansion(nextProps.collapse ? false : null);
}
/*
#### `componentDidMount()`.
When mounting, we just check to see if our status should be collapsed,
and collapse it if so. We don't need to worry about whether collapsing
is enabled here, because `setExpansion()` already takes that into
account.
The cases where a status should be collapsed are:
- The `collapse` prop has been set to `true`
- The user has decided in local settings to collapse all statuses.
- The user has decided to collapse all notifications ('muted'
statuses).
- The user has decided to collapse long statuses and the status is
over 400px (without media, or 650px with).
- The status is a reply and the user has decided to collapse all
replies.
- The status contains media and the user has decided to collapse all
statuses with media.
We also start up our intersection observer to monitor our statuses.
`componentMounted` lets us know that everything has been set up
properly and our intersection observer is good to go.
*/
componentDidMount () {
const { node, handleIntersection } = this;
const {
status,
settings,
collapse,
muted,
id,
intersectionObserverWrapper,
} = this.props;
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
if (
collapse ||
autoCollapseSettings.get('all') || (
autoCollapseSettings.get('notifications') && muted
) || (
autoCollapseSettings.get('lengthy') &&
node.clientHeight > (
status.get('media_attachments').size && !muted ? 650 : 400
)
) || (
autoCollapseSettings.get('replies') &&
status.get('in_reply_to_id', null) !== null
) || (
autoCollapseSettings.get('media') &&
!(status.get('spoiler_text').length) &&
status.get('media_attachments').size
)
) this.setExpansion(false);
if (!intersectionObserverWrapper) return;
else intersectionObserverWrapper.observe(
id,
node,
handleIntersection
);
this.componentMounted = true;
}
/*
#### `shouldComponentUpdate()`.
If the status is about to be both offscreen (not intersecting) and
hidden, then we only need to update it if it's not that way currently.
If the status is moving from offscreen to onscreen, then we *have* to
re-render, so that we can unhide the element if necessary.
If neither of these cases are true, we can leave it up to our
`updateOnProps` and `updateOnStates` arrays.
*/
shouldComponentUpdate (nextProps, nextState) {
switch (true) {
case !nextState.isIntersecting && nextState.isHidden:
return this.state.isIntersecting || !this.state.isHidden;
case nextState.isIntersecting && !this.state.isIntersecting:
return true;
default:
return super.shouldComponentUpdate(nextProps, nextState);
}
}
/*
#### `componentDidUpdate()`.
If our component is being rendered for any reason and an update has
triggered, this will save its height.
This is, frankly, a bit overkill, as the only instance when we
actually *need* to update the height right now should be when the
value of `isExpanded` has changed. But it makes for more readable
code and prevents bugs in the future where the height isn't set
properly after some change.
*/
componentDidUpdate () {
if (
this.state.isIntersecting || !this.state.isHidden
) this.saveHeight();
}
/*
#### `componentWillUnmount()`.
If our component is about to unmount, then we'd better unset
`this.componentMounted`.
*/
componentWillUnmount () {
this.componentMounted = false;
}
/*
#### `handleIntersection()`.
`handleIntersection()` either hides the status (if it is offscreen) or
unhides it (if it is onscreen). It's called by
`intersectionObserverWrapper.observe()`.
If our status isn't intersecting, we schedule an idle task (using the
aptly-named `scheduleIdleTask()`) to hide the status at the next
available opportunity.
tootsuite/mastodon left us with the following enlightening comment
regarding this function:
> Edge 15 doesn't support isIntersecting, but we can infer it
It then implements a polyfill (intersectionRect.height > 0) which isn't
actually sufficient. The short answer is, this behaviour isn't really
supported on Edge but we can get kinda close.
*/
handleIntersection = (entry) => {
const isIntersecting = (
typeof entry.isIntersecting === 'boolean' ?
entry.isIntersecting :
entry.intersectionRect.height > 0
);
this.setState(
(prevState) => {
if (prevState.isIntersecting && !isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting : isIntersecting,
isHidden : false,
};
}
);
}
/*
#### `hideIfNotIntersecting()`.
This function will hide the status if we're still not intersecting.
Hiding the status means that it will just render an empty div instead
of actual content, which saves RAMS and CPUs or some such.
*/
hideIfNotIntersecting = () => {
if (!this.componentMounted) return;
this.setState(
(prevState) => ({ isHidden: !prevState.isIntersecting })
);
}
/*
#### `saveHeight()`.
`saveHeight()` saves the height of our status so that when whe hide it
we preserve its dimensions. We only want to store our height, though,
if our status has content (otherwise, it would imply that it is
already hidden).
*/
saveHeight = () => {
if (this.node && this.node.children.length) {
this.height = this.node.getBoundingClientRect().height;
}
}
/*
#### `setExpansion()`.
`setExpansion()` sets the value of `isExpanded` in our state. It takes
one argument, `value`, which gives the desired value for `isExpanded`.
The default for this argument is `null`.
`setExpansion()` automatically checks for us whether toot collapsing
is enabled, so we don't have to.
We use a `switch` statement to simplify our code.
*/
setExpansion = (value) => {
switch (true) {
case value === undefined || value === null:
this.setState({ isExpanded: null });
break;
case !value && this.props.settings.getIn(['collapsed', 'enabled']):
this.setState({ isExpanded: false });
break;
case !!value:
this.setState({ isExpanded: true });
break;
}
}
/*
#### `handleRef()`.
`handleRef()` just saves a reference to our status node to `this.node`.
It also saves our height, in case the height of our node has changed.
*/
handleRef = (node) => {
this.node = node;
this.saveHeight();
}
/*
#### `parseClick()`.
`parseClick()` takes a click event and responds appropriately.
If our status is collapsed, then clicking on it should uncollapse it.
If `Shift` is held, then clicking on it should collapse it.
Otherwise, we open the url handed to us in `destination`, if
applicable.
*/
parseClick = (e, destination) => {
const { router } = this.context;
const { status } = this.props;
const { isExpanded } = this.state;
if (!router) return;
if (destination === undefined) {
destination = `/statuses/${
status.getIn(['reblog', 'id'], status.get('id'))
}`;
}
if (e.button === 0) {
if (isExpanded === false) this.setExpansion(null);
else if (e.shiftKey) {
this.setExpansion(false);
document.getSelection().removeAllRanges();
} else router.history.push(destination);
e.preventDefault();
}
}
/*
#### `render()`.
`render()` actually puts our element on the screen. The particulars of
this operation are further explained in the code below.
*/
render () {
const {
parseClick,
setExpansion,
saveHeight,
handleRef,
} = this;
const { router } = this.context;
const {
status,
account,
settings,
collapsed,
muted,
prepend,
intersectionObserverWrapper,
onOpenVideo,
onOpenMedia,
autoPlayGif,
notification,
...other
} = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
let background = null;
let attachments = null;
let media = null;
let mediaIcon = null;
/*
If we don't have a status, then we don't render anything.
*/
if (status === null) {
return null;
}
/*
If our status is offscreen and hidden, then we render an empty <div> in
its place. We fill it with "content" but note that opacity is set to 0.
*/
if (!isIntersecting && isHidden) {
return (
<div
ref={this.handleRef}
data-id={status.get('id')}
style={{
height : `${this.height}px`,
opacity : 0,
overflow : 'hidden',
}}
>
{
status.getIn(['account', 'display_name']) ||
status.getIn(['account', 'username'])
}
{status.get('content')}
</div>
);
}
/*
If user backgrounds for collapsed statuses are enabled, then we
initialize our background accordingly. This will only be rendered if
the status is collapsed.
*/
if (
settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])
) background = status.getIn(['account', 'header']);
/*
This handles our media attachments. Note that we don't show media on
muted (notification) statuses. If the media type is unknown, then we
simply ignore it.
After we have generated our appropriate media element and stored it in
`media`, we snatch the thumbnail to use as our `background` if media
backgrounds for collapsed statuses are enabled.
*/
attachments = status.get('media_attachments');
if (attachments.size && !muted) {
if (attachments.some((item) => item.get('type') === 'unknown')) {
} else if (
attachments.getIn([0, 'type']) === 'video'
) {
media = ( // Media type is 'video'
<StatusPlayer
media={attachments.get(0)}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
height={250}
onOpenVideo={onOpenVideo}
/>
);
mediaIcon = 'video-camera';
} else { // Media type is 'image' or 'gifv'
media = (
<StatusGallery
media={attachments}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
height={250}
onOpenMedia={onOpenMedia}
autoPlayGif={autoPlayGif}
/>
);
mediaIcon = 'picture-o';
}
if (
!status.get('sensitive') &&
!(status.get('spoiler_text').length > 0) &&
settings.getIn(['collapsed', 'backgrounds', 'preview_images'])
) background = attachments.getIn([0, 'preview_url']);
}
/*
Here we prepare extra data-* attributes for CSS selectors.
Users can use those for theming, hiding avatars etc via UserStyle
*/
const selectorAttribs = {
'data-status-by': `@${status.getIn(['account', 'acct'])}`,
};
if (prepend && account) {
const notifKind = {
favourite: 'favourited',
reblog: 'boosted',
reblogged_by: 'boosted',
}[prepend];
selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
}
/*
Finally, we can render our status. We just put the pieces together
from above. We only render the action bar if the status isn't
collapsed.
*/
return (
<article
className={
`status${
muted ? ' muted' : ''
} status-${status.get('visibility')}${
isExpanded === false ? ' collapsed' : ''
}${
isExpanded === false && background ? ' has-background' : ''
}${
this.state.markedForDelete ? ' marked-for-delete' : ''
}`
}
style={{
backgroundImage: (
isExpanded === false && background ?
`url(${background})` :
'none'
),
}}
ref={handleRef}
{...selectorAttribs}
>
{prepend && account ? (
<StatusPrepend
type={prepend}
account={account}
parseClick={parseClick}
notificationId={this.props.notificationId}
/>
) : null}
<StatusHeader
status={status}
friend={account}
mediaIcon={mediaIcon}
collapsible={settings.getIn(['collapsed', 'enabled'])}
collapsed={isExpanded === false}
parseClick={parseClick}
setExpansion={setExpansion}
/>
<StatusContent
status={status}
media={media}
mediaIcon={mediaIcon}
expanded={isExpanded}
setExpansion={setExpansion}
onHeightUpdate={saveHeight}
parseClick={parseClick}
disabled={!router}
/>
{isExpanded !== false ? (
<StatusActionBar
{...other}
status={status}
account={status.get('account')}
/>
) : null}
{notification ? (
<NotificationOverlayContainer
notification={notification}
/>
) : null}
</article>
);
}
}

View File

@@ -1,203 +0,0 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../mastodon/components/icon_button';
import { isIOS } from '../../../mastodon/is_mobile';
const messages = defineMessages({
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
});
@injectIntl
export default class StatusPlayer extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
letterbox: PropTypes.bool,
fullwidth: PropTypes.bool,
height: PropTypes.number,
sensitive: PropTypes.bool,
intl: PropTypes.object.isRequired,
autoplay: PropTypes.bool,
onOpenVideo: PropTypes.func.isRequired,
};
static defaultProps = {
height: 110,
};
state = {
visible: !this.props.sensitive,
preview: true,
muted: true,
hasAudio: true,
videoError: false,
};
handleClick = () => {
this.setState({ muted: !this.state.muted });
}
handleVideoClick = (e) => {
e.stopPropagation();
const node = this.video;
if (node.paused) {
node.play();
} else {
node.pause();
}
}
handleOpen = () => {
this.setState({ preview: !this.state.preview });
}
handleVisibility = () => {
this.setState({
visible: !this.state.visible,
preview: true,
});
}
handleExpand = () => {
this.video.pause();
this.props.onOpenVideo(this.props.media, this.video.currentTime);
}
setRef = (c) => {
this.video = c;
}
handleLoadedData = () => {
if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
this.setState({ hasAudio: false });
}
}
handleVideoError = () => {
this.setState({ videoError: true });
}
componentDidMount () {
if (!this.video) {
return;
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
}
componentDidUpdate () {
if (!this.video) {
return;
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
}
componentWillUnmount () {
if (!this.video) {
return;
}
this.video.removeEventListener('loadeddata', this.handleLoadedData);
this.video.removeEventListener('error', this.handleVideoError);
}
render () {
const { media, intl, letterbox, fullwidth, height, sensitive, autoplay } = this.props;
let spoilerButton = (
<div className={`status__video-player-spoiler ${this.state.visible ? 'status__video-player-spoiler--visible' : ''}`}>
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
</div>
);
let expandButton = !this.context.router ? '' : (
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
let muteButton = '';
if (this.state.hasAudio) {
muteButton = (
<div className='status__video-player-mute'>
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
</div>
);
}
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{ height: `${height}px` }} className={`media-spoiler ${fullwidth ? 'full-width' : ''}`} onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else {
return (
<div role='button' tabIndex='0' style={{ height: `${height}px` }} className={`media-spoiler ${fullwidth ? 'full-width' : ''}`} onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
}
}
if (this.state.preview && !autoplay) {
return (
<div role='button' tabIndex='0' className={`media-spoiler-video ${fullwidth ? 'full-width' : ''}`} style={{ height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
{spoilerButton}
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
</div>
);
}
if (this.state.videoError) {
return (
<div style={{ height: `${height}px` }} className='video-error-cover' >
<span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
</div>
);
}
return (
<div className={`status__video-player ${fullwidth ? 'full-width' : ''}`} style={{ height: `${height}px` }}>
{spoilerButton}
{muteButton}
{expandButton}
<video
className={`status__video-player-video${letterbox ? ' letterbox' : ''}`}
role='button'
tabIndex='0'
ref={this.setRef}
src={media.get('url')}
autoPlay={!isIOS()}
loop
muted={this.state.muted}
onClick={this.handleVideoClick}
/>
</div>
);
}
}

View File

@@ -1,159 +0,0 @@
/*
`<StatusPrepend>`
=================
Originally a part of `<Status>`, but extracted into a separate
component for better documentation and maintainance by
@kibi@glitch.social as a part of glitch-soc/mastodon.
*/
/* * * * */
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
/* * * * */
/*
The `<StatusPrepend>` component:
--------------------------------
The `<StatusPrepend>` component holds a status's prepend, ie the text
that says “X reblogged this,” etc. It is represented by an `<aside>`
element.
### Props
- __`type` (`PropTypes.string`) :__
The type of prepend. One of `'reblogged_by'`, `'reblog'`,
`'favourite'`.
- __`account` (`ImmutablePropTypes.map`) :__
The account associated with the prepend.
- __`parseClick` (`PropTypes.func.isRequired`) :__
Our click parsing function.
*/
export default class StatusPrepend extends React.PureComponent {
static propTypes = {
type: PropTypes.string.isRequired,
account: ImmutablePropTypes.map.isRequired,
parseClick: PropTypes.func.isRequired,
notificationId: PropTypes.number,
};
/*
### Implementation
#### `handleClick()`.
This is just a small wrapper for `parseClick()` that gets fired when
an account link is clicked.
*/
handleClick = (e) => {
const { account, parseClick } = this.props;
parseClick(e, `/accounts/${+account.get('id')}`);
}
/*
#### `<Message>`.
`<Message>` is a quick functional React component which renders the
actual prepend message based on our provided `type`. First we create a
`link` for the account's name, and then use `<FormattedMessage>` to
generate the message.
*/
Message = () => {
const { type, account } = this.props;
let link = (
<a
onClick={this.handleClick}
href={account.get('url')}
className='status__display-name'
>
<b
dangerouslySetInnerHTML={{
__html : account.get('display_name_html') || account.get('username'),
}}
/>
</a>
);
switch (type) {
case 'reblogged_by':
return (
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} boosted'
values={{ name : link }}
/>
);
case 'favourite':
return (
<FormattedMessage
id='notification.favourite'
defaultMessage='{name} favourited your status'
values={{ name : link }}
/>
);
case 'reblog':
return (
<FormattedMessage
id='notification.reblog'
defaultMessage='{name} boosted your status'
values={{ name : link }}
/>
);
}
return null;
}
/*
#### `render()`.
Our `render()` is incredibly simple; we just render the icon and then
the `<Message>` inside of an <aside>.
*/
render () {
const { Message } = this;
const { type } = this.props;
return !type ? null : (
<aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}>
<div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
<i
className={`fa fa-fw fa-${
type === 'favourite' ? 'star star-icon' : 'retweet'
} status__prepend-icon`}
/>
</div>
<Message />
</aside>
);
}
}

View File

@@ -1,48 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
@injectIntl
export default class VisibilityIcon extends ImmutablePureComponent {
static propTypes = {
visibility: PropTypes.string,
intl: PropTypes.object.isRequired,
withLabel: PropTypes.bool,
};
render() {
const { withLabel, visibility, intl } = this.props;
const visibilityClass = {
public: 'globe',
unlisted: 'unlock-alt',
private: 'lock',
direct: 'envelope',
}[visibility];
const label = intl.formatMessage(messages[visibility]);
const icon = (<i
className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`}
title={label}
aria-hidden='true'
/>);
if (withLabel) {
return (<span style={{ whiteSpace: 'nowrap' }}>{icon} {label}</span>);
} else {
return icon;
}
}
}

View File

@@ -1,43 +0,0 @@
{
"getting_started.open_source_notice": "Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.",
"layout.auto": "Auto",
"layout.current_is": "Your current layout is:",
"layout.desktop": "Desktop",
"layout.mobile": "Mobile",
"navigation_bar.app_settings": "App settings",
"getting_started.onboarding": "Show me around",
"onboarding.page_one.federation": "{domain} is an 'instance' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.welcome": "Welcome to {domain}!",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.",
"settings.auto_collapse": "Automatic collapsing",
"settings.auto_collapse_all": "Everything",
"settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_replies": "Replies",
"settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots",
"settings.enable_collapsed": "Enable collapsed toots",
"settings.general": "General",
"settings.image_backgrounds": "Image backgrounds",
"settings.image_backgrounds_media": "Preview collapsed toot media",
"settings.image_backgrounds_users": "Give collapsed toots an image background",
"settings.media": "Media",
"settings.media_letterbox": "Letterbox media",
"settings.media_fullwidth": "Full-width media previews",
"settings.preferences": "User preferences",
"settings.wide_view": "Wide view (Desktop mode only)",
"settings.navbar_under": "Navbar at the bottom (Mobile only)",
"status.collapse": "Collapse",
"status.uncollapse": "Uncollapse",
"notification.markForDeletion": "Mark for deletion",
"notifications.clear": "Clear all my notifications",
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
"notifications.marked_clear": "Clear selected notifications",
"notification_purge.btn_all": "Select\nall",
"notification_purge.btn_none": "Select\nnone",
"notification_purge.btn_invert": "Invert\nselection",
"notification_purge.btn_apply": "Clear\nselected"
}

View File

@@ -1,125 +0,0 @@
/*
`reducers/local_settings`
========================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
This file provides our Redux reducers related to local settings. The
associated actions are:
- __`STORE_HYDRATE` :__
Used to hydrate the store with its initial values.
- __`LOCAL_SETTING_CHANGE` :__
Used to change the value of a local setting in the store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { Map as ImmutableMap } from 'immutable';
// Mastodon imports //
import { STORE_HYDRATE } from '../../mastodon/actions/store';
// Our imports //
import { LOCAL_SETTING_CHANGE } from '../actions/local_settings';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
initialState:
-------------
You can see the default values for all of our local settings here.
These are only used if no previously-saved values exist.
*/
const initialState = ImmutableMap({
layout : 'auto',
stretch : true,
navbar_under : false,
side_arm : 'none',
collapsed : ImmutableMap({
enabled : true,
auto : ImmutableMap({
all : false,
notifications : true,
lengthy : true,
replies : false,
media : false,
}),
backgrounds : ImmutableMap({
user_backgrounds : false,
preview_images : false,
}),
}),
media : ImmutableMap({
letterbox : true,
fullwidth : true,
}),
});
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Helper functions:
-----------------
### `hydrate(state, localSettings)`
`hydrate()` is used to hydrate the `local_settings` part of our store
with its initial values. The `state` will probably just be the
`initialState`, and the `localSettings` should be whatever we pulled
from `localStorage`.
*/
const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
`localSettings(state = initialState, action)`:
----------------------------------------------
This function holds our actual reducer.
If our action is `STORE_HYDRATE`, then we call `hydrate()` with the
`local_settings` property of the provided `action.state`.
If our action is `LOCAL_SETTING_CHANGE`, then we set `action.key` in
our state to the provided `action.value`. Note that `action.key` MUST
be an array, since we use `setIn()`.
> __Note :__
> We call this function `localSettings`, but its associated object
> in the store is `local_settings`.
*/
export default function localSettings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return hydrate(state, action.state.get('local_settings'));
case LOCAL_SETTING_CHANGE:
return state.setIn(action.key, action.value);
default:
return state;
}
};

View File

@@ -1,331 +0,0 @@
/*
`util/bio_metadata`
===================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
This file provides two functions for dealing with bio metadata. The
functions are:
- __`processBio(content)` :__
Processes `content` to extract any frontmatter. The returned
object has two properties: `text`, which contains the text of
`content` sans-frontmatter, and `metadata`, which is an array
of key-value pairs (in two-element array format). If no
frontmatter was provided in `content`, then `metadata` will be
an empty array.
- __`createBio(note, data)` :__
Reverses the process in `processBio()`; takes a `note` and an
array of two-element arrays (which should give keys and values)
and outputs a string containing a well-formed bio with
frontmatter.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*********************************************************************\
To my lovely code maintainers,
The syntax recognized by the Mastodon frontend for its bio metadata
feature is a subset of that provided by the YAML 1.2 specification.
In particular, Mastodon recognizes metadata which is provided as an
implicit YAML map, where each key-value pair takes up only a single
line (no multi-line values are permitted). To simplify the level of
processing required, Mastodon metadata frontmatter has been limited
to only allow those characters in the `c-printable` set, as defined
by the YAML 1.2 specification, instead of permitting those from the
`nb-json` characters inside double-quoted strings like YAML proper.
¶ It is important to note that Mastodon only borrows the *syntax*
of YAML, not its semantics. This is to say, Mastodon won't make any
attempt to interpret the data it receives. `true` will not become a
boolean; `56` will not be interpreted as a number. Rather, each key
and every value will be read as a string, and as a string they will
remain. The order of the pairs is unchanged, and any duplicate keys
are preserved. However, YAML escape sequences will be replaced with
the proper interpretations according to the YAML 1.2 specification.
¶ The implementation provided below interprets `<br>` as `\n` and
allows for an open <p> tag at the beginning of the bio. It replaces
the escaped character entities `&apos;` and `&quot;` with single or
double quotes, respectively, prior to processing. However, no other
escaped characters are replaced, not even those which might have an
impact on the syntax otherwise. These minor allowances are provided
because the Mastodon backend will insert these things automatically
into a bio before sending it through the API, so it is important we
account for them. Aside from this, the YAML frontmatter must be the
very first thing in the bio, leading with three consecutive hyphen-
minues (`---`), and ending with the same or, alternatively, instead
with three periods (`...`). No limits have been set with respect to
the number of characters permitted in the frontmatter, although one
should note that only limited space is provided for them in the UI.
¶ The regular expression used to check the existence of, and then
process, the YAML frontmatter has been split into a number of small
components in the code below, in the vain hope that it will be much
easier to read and to maintain. I leave it to the future readers of
this code to determine the extent of my successes in this endeavor.
UPDATE 19 Oct 2017: We no longer allow character escapes inside our
double-quoted strings for ease of processing. We now internally use
the name "ƔAML" in our code to clarify that this is Not Quite YAML.
Sending love + warmth eternal,
- kibigo [@kibi@glitch.social]
\*********************************************************************/
/* "u" FLAG COMPATABILITY */
let compat_mode = false;
try {
new RegExp('.', 'u');
} catch (e) {
compat_mode = true;
}
/* CONVENIENCE FUNCTIONS */
const unirex = str => compat_mode ? new RegExp(str) : new RegExp(str, 'u');
const rexstr = exp => '(?:' + exp.source + ')';
/* CHARACTER CLASSES */
const DOCUMENT_START = /^/;
const DOCUMENT_END = /$/;
const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec.
compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]'
);
const WHITE_SPACE = /[ \t]/;
const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/;
const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/;
const FLOW_CHAR = /[,[\]{}]/;
/* NEGATED CHARACTER CLASSES */
const NOT_WHITE_SPACE = unirex('(?!' + rexstr(WHITE_SPACE) + ')[^]');
const NOT_LINE_BREAK = unirex('(?!' + rexstr(LINE_BREAK) + ')[^]');
const NOT_INDICATOR = unirex('(?!' + rexstr(INDICATOR) + ')[^]');
const NOT_FLOW_CHAR = unirex('(?!' + rexstr(FLOW_CHAR) + ')[^]');
const NOT_ALLOWED_CHAR = unirex(
'(?!' + rexstr(ALLOWED_CHAR) + ')[^]'
);
/* BASIC CONSTRUCTS */
const ANY_WHITE_SPACE = unirex(rexstr(WHITE_SPACE) + '*');
const ANY_ALLOWED_CHARS = unirex(rexstr(ALLOWED_CHAR) + '*');
const NEW_LINE = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK)
);
const SOME_NEW_LINES = unirex(
'(?:' + rexstr(NEW_LINE) + ')+'
);
const POSSIBLE_STARTS = unirex(
rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?'
);
const POSSIBLE_ENDS = unirex(
rexstr(SOME_NEW_LINES) + '|' +
rexstr(DOCUMENT_END) + '|' +
rexstr(/<\/p>/)
);
const QUOTE_CHAR = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]'
);
const ANY_QUOTE_CHAR = unirex(
rexstr(QUOTE_CHAR) + '*'
);
const ESCAPED_APOS = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/)
);
const ANY_ESCAPED_APOS = unirex(
rexstr(ESCAPED_APOS) + '*'
);
const FIRST_KEY_CHAR = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')' +
rexstr(NOT_INDICATOR) + '|' +
rexstr(/[?:-]/) +
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')' +
'(?=' + rexstr(NOT_FLOW_CHAR) + ')'
);
const FIRST_VALUE_CHAR = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')' +
rexstr(NOT_INDICATOR) + '|' +
rexstr(/[?:-]/) +
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')'
// Flow indicators are allowed in values.
);
const LATER_KEY_CHAR = unirex(
rexstr(WHITE_SPACE) + '|' +
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')' +
'(?=' + rexstr(NOT_FLOW_CHAR) + ')' +
rexstr(/[^:#]#?/) + '|' +
rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')'
);
const LATER_VALUE_CHAR = unirex(
rexstr(WHITE_SPACE) + '|' +
'(?=' + rexstr(NOT_LINE_BREAK) + ')' +
'(?=' + rexstr(NOT_WHITE_SPACE) + ')' +
// Flow indicators are allowed in values.
rexstr(/[^:#]#?/) + '|' +
rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')'
);
/* YAML CONSTRUCTS */
const ƔAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + '---'
);
const ƔAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)'
);
const ƔAML_LOOKAHEAD = unirex(
'(?=' +
rexstr(ƔAML_START) +
rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) +
')'
);
const ƔAML_DOUBLE_QUOTE = unirex(
'"' + rexstr(ANY_QUOTE_CHAR) + '"'
);
const ƔAML_SINGLE_QUOTE = unirex(
'\'' + rexstr(ANY_ESCAPED_APOS) + '\''
);
const ƔAML_SIMPLE_KEY = unirex(
rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*'
);
const ƔAML_SIMPLE_VALUE = unirex(
rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*'
);
const ƔAML_KEY = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_KEY)
);
const ƔAML_VALUE = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_VALUE)
);
const ƔAML_SEPARATOR = unirex(
rexstr(ANY_WHITE_SPACE) +
':' + rexstr(WHITE_SPACE) +
rexstr(ANY_WHITE_SPACE)
);
const ƔAML_LINE = unirex(
'(' + rexstr(ƔAML_KEY) + ')' +
rexstr(ƔAML_SEPARATOR) +
'(' + rexstr(ƔAML_VALUE) + ')'
);
/* FRONTMATTER REGEX */
const ƔAML_FRONTMATTER = unirex(
rexstr(POSSIBLE_STARTS) +
rexstr(ƔAML_LOOKAHEAD) +
rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) +
'(?:' +
rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,5}' +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS)
);
/* SEARCHES */
const FIND_ƔAML_LINE = unirex(
rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE)
);
/* STRING PROCESSING */
function processString (str) {
switch (str.charAt(0)) {
case '"':
return str.substring(1, str.length - 1);
case '\'':
return str
.substring(1, str.length - 1)
.replace(/''/g, '\'');
default:
return str;
}
}
/* BIO PROCESSING */
export function processBio(content) {
content = content.replace(/&quot;/g, '"').replace(/&apos;/g, '\'');
let result = {
text: content,
metadata: [],
};
let ɣaml = content.match(ƔAML_FRONTMATTER);
if (!ɣaml) {
return result;
} else {
ɣaml = ɣaml[0];
}
const start = content.search(ƔAML_START);
const end = start + ɣaml.length - ɣaml.search(ƔAML_START);
result.text = content.substr(end);
let metadata = null;
let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings
while ((metadata = query.exec(ɣaml))) {
result.metadata.push([
processString(metadata[1]),
processString(metadata[2]),
]);
}
return result;
}
/* BIO CREATION */
export function createBio(note, data) {
if (!note) note = '';
let frontmatter = '';
if ((data && data.length) || note.match(/^\s*---\s+/)) {
if (!data) frontmatter = '---\n...\n';
else {
frontmatter += '---\n';
for (let i = 0; i < data.length; i++) {
let key = '' + data[i][0];
let val = '' + data[i][1];
// Key processing
if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"';
else {
key = key
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<27>');
key = '\'' + key + '\'';
}
// Value processing
if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"';
else {
key = key
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<27>');
key = '\'' + key + '\'';
}
frontmatter += key + ': ' + val + '\n';
}
frontmatter += '...\n';
}
}
return frontmatter + note;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -31,7 +31,6 @@ export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
@@ -45,8 +44,6 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@@ -94,16 +91,14 @@ export function mentionCompose(account, router) {
export function submitCompose() {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
const status = getState().getIn(['compose', 'text'], '');
if (!status || !status.length) {
return;
}
dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
}
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
@@ -160,13 +155,6 @@ export function submitComposeFail(error) {
};
};
export function doodleSet(options) {
return {
type: COMPOSE_DOODLE_SET,
options: options,
};
};
export function uploadCompose(files) {
return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
@@ -346,13 +334,6 @@ export function unmountCompose() {
};
};
export function toggleComposeAdvancedOption(option) {
return {
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
option: option,
};
}
export function changeComposeSensitivity() {
return {
type: COMPOSE_SENSITIVITY_CHANGE,

View File

@@ -6,17 +6,6 @@ import { defineMessages } from 'react-intl';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
// tracking the notif cleaning request
export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL';
export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE';
export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
// Unmark notifications (when the cleaning mode is left)
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
// Mark one for delete
export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE';
export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST';
export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS';
export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL';
@@ -199,67 +188,3 @@ export function scrollTopNotifications(top) {
top,
};
};
export function deleteMarkedNotifications() {
return (dispatch, getState) => {
dispatch(deleteMarkedNotificationsRequest());
let ids = [];
getState().getIn(['notifications', 'items']).forEach((n) => {
if (n.get('markedForDelete')) {
ids.push(n.get('id'));
}
});
if (ids.length === 0) {
return;
}
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
dispatch(deleteMarkedNotificationsSuccess());
}).catch(error => {
console.error(error);
dispatch(deleteMarkedNotificationsFail(error));
});
};
};
export function enterNotificationClearingMode(yes) {
return {
type: NOTIFICATIONS_ENTER_CLEARING_MODE,
yes: yes,
};
};
export function markAllNotifications(yes) {
return {
type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
yes: yes, // true, false or null. null = invert
};
};
export function deleteMarkedNotificationsRequest() {
return {
type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
};
};
export function deleteMarkedNotificationsFail() {
return {
type: NOTIFICATIONS_DELETE_MARKED_FAIL,
};
};
export function markNotificationForDelete(id, yes) {
return {
type: NOTIFICATION_MARK_FOR_DELETE,
id: id,
yes: yes,
};
};
export function deleteMarkedNotificationsSuccess() {
return {
type: NOTIFICATIONS_DELETE_MARKED_SUCCESS,
};
};

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

@@ -3,7 +3,6 @@
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
<div
className="account__avatar"
data-avatar-of="@alice"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
@@ -20,7 +19,6 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
exports[`<Avatar /> Still renders a still avatar 1`] = `
<div
className="account__avatar"
data-avatar-of="@alice"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={

View File

@@ -6,7 +6,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
>
<div
className="account__avatar-overlay-base"
data-avatar-of="@alice"
style={
Object {
"backgroundImage": "url(/static/alice.jpg)",
@@ -15,7 +14,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
/>
<div
className="account__avatar-overlay-overlay"
data-avatar-of="@eve@blackhat.lair"
style={
Object {
"backgroundImage": "url(/static/eve.jpg)",

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,8 +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: 'You are not currently muting notifications from @{name}. Click to mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
@injectIntl
@@ -23,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,
@@ -52,7 +52,7 @@ export default class Account extends ImmutablePureComponent {
}
render () {
const { account, me, intl, hidden } = this.props;
const { account, intl, hidden } = this.props;
if (!account) {
return <div />;
@@ -82,7 +82,7 @@ export default class Account extends ImmutablePureComponent {
} else if (muting) {
let hidingNotificationsButton;
if (muting.get('notifications')) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
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} />;
}

View File

@@ -11,8 +11,8 @@ import classNames from 'classnames';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
let right = str.slice(caretPosition).search(/[\s\u200B]/);
let left = str.slice(0, caretPosition).search(/\S+$/);
let right = str.slice(caretPosition).search(/\s/);
if (right < 0) {
word = str.slice(left);

View File

@@ -64,7 +64,6 @@ export default class Avatar extends React.PureComponent {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
data-avatar-of={`@${account.get('acct')}`}
/>
);
}

View File

@@ -21,8 +21,8 @@ export default class AvatarOverlay extends React.PureComponent {
return (
<div className='account__avatar-overlay'>
<div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} />
<div className='account__avatar-overlay-base' style={baseStyle} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} />
</div>
);
}

View File

@@ -7,8 +7,6 @@ export default class Column extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
extraClasses: PropTypes.string,
name: PropTypes.string,
};
scrollTop () {
@@ -42,10 +40,10 @@ export default class Column extends React.PureComponent {
}
render () {
const { children, extraClasses, name } = this.props;
const { children } = this.props;
return (
<div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
<div role='region' className='column' ref={this.setRef}>
{children}
</div>
);

View File

@@ -9,8 +9,7 @@ export default class ColumnBackButton extends React.PureComponent {
};
handleClick = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();

View File

@@ -9,12 +9,8 @@ export default class ColumnBackButtonSlim extends React.PureComponent {
};
handleClick = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
}
render () {

View File

@@ -1,18 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Glitch imports
import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
});
@injectIntl
@@ -27,19 +22,14 @@ export default class ColumnHeader extends React.PureComponent {
title: PropTypes.node.isRequired,
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
localSettings : ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
focusable: PropTypes.bool,
showBackButton: PropTypes.bool,
notifCleaning: PropTypes.bool, // true only for the notification column
notifCleaningActive: PropTypes.bool,
onEnterCleaningMode: PropTypes.func,
children: PropTypes.node,
pinned: PropTypes.bool,
onPin: PropTypes.func,
onMove: PropTypes.func,
onClick: PropTypes.func,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
@@ -49,7 +39,6 @@ export default class ColumnHeader extends React.PureComponent {
state = {
collapsed: true,
animating: false,
animatingNCD: false,
};
handleToggleClick = (e) => {
@@ -70,32 +59,17 @@ export default class ColumnHeader extends React.PureComponent {
}
handleBackClick = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
handleTransitionEndNCD = () => {
this.setState({ animatingNCD: false });
}
onEnterCleaningMode = () => {
this.setState({ animatingNCD: true });
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
}
render () {
const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
const { collapsed, animating, animatingNCD } = this.state;
let title = this.props.title;
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'active': active,
@@ -114,20 +88,8 @@ export default class ColumnHeader extends React.PureComponent {
'active': !collapsed,
});
const notifCleaningButtonClassName = classNames('column-header__button', {
'active': notifCleaningActive,
});
const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
'collapsed': !notifCleaningActive,
'animating': animatingNCD,
});
let extraContent, pinButton, moveButtons, backButton, collapseButton;
//*glitch
const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
if (children) {
extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'>
@@ -175,31 +137,16 @@ 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}
{ notifCleaning ? (
<button
aria-label={msgEnterNotifCleaning}
title={msgEnterNotifCleaning}
onClick={this.onEnterCleaningMode}
className={notifCleaningButtonClassName}
>
<i className='fa fa-eraser' />
</button>
) : null}
{collapseButton}
</div>
</h1>
{ notifCleaning ? (
<div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
<div className='column-header__collapsible-inner nopad-drawer'>
{(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
</div>
</div>
) : null}
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}

View File

@@ -20,10 +20,8 @@ export default class IconButton extends React.PureComponent {
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
flip: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
label: PropTypes.string,
};
static defaultProps = {
@@ -44,18 +42,14 @@ export default class IconButton extends React.PureComponent {
}
render () {
let style = {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
if (!this.props.label) {
style.width = `${this.props.size * 1.28571429}px`;
} else {
style.textAlign = 'left';
}
const {
active,
@@ -78,23 +72,27 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay,
});
const flipDeg = this.props.flip ? -180 : -360;
const rotateDeg = this.props.active ? flipDeg : 0;
const motionDefaultStyle = {
rotate: rotateDeg,
};
const springOpts = {
stiffness: this.props.flip ? 60 : 120,
damping: 7,
};
const motionStyle = {
rotate: animate ? spring(rotateDeg, springOpts) : 0,
};
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={motionDefaultStyle} style={motionStyle}>
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) =>
<button
aria-label={title}
@@ -107,7 +105,6 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
{this.props.label}
</button>
}
</Motion>

View File

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/status/gallery
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@@ -9,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' },
@@ -26,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,
@@ -50,7 +46,7 @@ class Item extends React.PureComponent {
}
hoverToPlay () {
const { attachment, autoPlayGif } = this.props;
const { attachment } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
@@ -142,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 })}>
@@ -184,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,
};
@@ -264,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

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/status
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@@ -39,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,
@@ -57,9 +51,6 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
'me',
'boostModal',
'autoPlayGif',
'muted',
'hidden',
]
@@ -200,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

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/status/action_bar
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@@ -8,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' },
@@ -50,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,
};
@@ -59,7 +56,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'me',
'withDismiss',
]
@@ -119,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

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/status/content
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import StatusContainer from '../../glitch/components/status/container';
import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ScrollableList from './scrollable_list';

View File

@@ -13,6 +13,7 @@ import {
} 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' },
@@ -23,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;
@@ -34,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),

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,23 +4,18 @@ 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 initialState = JSON.parse(document.getElementById('initial-state').textContent);
try {
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
} catch (e) {
initialState.local_settings = {};
}
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);

View File

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/status/container
import React from 'react';
import { connect } from 'react-redux';
import Status from '../components/status';
@@ -17,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 = () => {
@@ -38,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;
@@ -61,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 }));
@@ -90,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onDelete (status) {
if (!this.deleteModal) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
@@ -126,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

@@ -1,6 +1,3 @@
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
// SEE INSTEAD : glitch/components/account/header
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@@ -8,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' },
@@ -17,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 = {
@@ -47,7 +35,7 @@ class Avatar extends ImmutablePureComponent {
}
render () {
const { account, autoPlayGif } = this.props;
const { account } = this.props;
const { isHovered } = this.state;
return (
@@ -74,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;
@@ -127,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

@@ -1,7 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from '../../../../glitch/components/account/header';
import InnerHeader from '../../account/components/header';
import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -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}

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