Compare commits

..

67 Commits

Author SHA1 Message Date
kibigo!
6839ee390f Many improvements to images in collapsed toots
- Now works on detailed and static pages
- Fixed bug with nested CW / Sensitive Media
- Now apparent which toots contain media
2017-06-29 23:31:22 -07:00
kibigo!
54c1f56c9a Put images behind CWs 2017-06-27 17:01:46 -07:00
m4sk1n
2a9805b987 i18n: Minor fix in devise.pl.yml (#3978) 2017-06-27 23:14:02 +02:00
m4sk1n
126f929c39 i18n: Use instance name in email notifications instead of Mastodon (pl) (#3976)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-06-27 23:10:43 +02:00
m4sk1n
da42bfadb5 i18n: E-mail notifications to admins about new reports (pl) (#3975) 2017-06-27 22:21:35 +02:00
m4sk1n
6ad72728f6 i18n: Turn report screen into a modal (pl) (#3974) 2017-06-27 22:14:31 +02:00
Sorin Davidoi
64d9c016bd fix(components/status): Up & down jump due to content being added to the DOM (#3972) 2017-06-27 18:43:53 +02:00
Eugen Rochko
12e7c81dd8 Turn report screen into a modal (#3965) 2017-06-27 18:07:21 +02:00
Midgard
16d0aed403 Use instance name in email notifications instead of "Mastodon" (#3763)
* Use instance name in "password changed" mail

instead of "Mastodon".

Fixes tootsuite#2620.

* Use instance name in password reset mail

instead of "Mastodon".
2017-06-27 14:22:36 +02:00
Debanshu Kundu
da9317fa56 #1456 Added rake task to add a user. (#1482) 2017-06-27 14:18:53 +02:00
Sorin Davidoi
be92babd00 Responsive images in media gallery (#3963)
* feat(components/media_gallery): Responsive images

* fix(components/media_gallery): Link to image URL
2017-06-27 13:46:37 +02:00
Yamagishi Kazutoshi
e2dd576a1b Update dependencies for Node.js (#3967)
* Update @storybook/addon-actions to v3.1.6

* Update @storybook/react to v3.1.6

* Update babel-loader to v7.1.0

* Update babel-plugin-transform-react-remove-prop-types to v0.4.6

* Update enzyme to v2.9.1

* Update fsevents to v1.1.2

* Update intersection-observer to v0.3.2

* Update npmlog to v4.1.2

* Update pg to v6.4.0

* Update postcss-loader to v2.0.6

* Update rails-ujs to v5.1.2

* Update react to v15.6.1

* Update react-addons-shallow-compare to v15.6.0

* Update react-dom to v15.6.0

* Update react-notification to v6.7.1

* Update react-test-renderer to v15.6.1

* Update react-textarea-autosize to v5.0.7

* Update redux to v3.7.1

* Update resolve-url-loader to v2.1.0

* Update sass-loader to v6.0.6

* Update sinon to v2.3.5

* Update stringz to v0.2.2

* Update uuid to v3.1.0

* Update websocket.js to v0.1.12

* Update yargs to v8.0.2

* yarn upgrade
2017-06-27 13:46:11 +02:00
Yamagishi Kazutoshi
8f2c91568c Maintain aspect ratio for preview image (#3966) 2017-06-27 13:43:53 +02:00
Yamagishi Kazutoshi
98eaa2aa27 Update Rails to v5.1.2 (#3968) 2017-06-27 13:41:03 +02:00
Eugen Rochko
42b8220632 Fix #1624 - Send e-mail notifications to admins about new reports (#3949) 2017-06-27 00:04:00 +02:00
ThibG
a91d968cab Raise an error if salmon request response is unsatisfactory (#3960) 2017-06-26 19:39:58 +02:00
m4sk1n
646de92781 i18n: Updated Polish translation (#3956)
* i18n: Updated Polish translation

* Update pl.yml
2017-06-26 17:18:45 +02:00
m4sk1n
ae2b722f55 i18n: Warning to look into the spam folder (pl) (#3955) 2017-06-26 17:10:54 +02:00
Daniel Hunsaker
7aeb9168b0 Add .gitattributes file to avoid unwanted CRLF (#3954)
When Windows checks out files, it defaults to changing line endings to CRLF. If these files are then copied to a Linux system to be run, and the endings aren't changed at some point in that process, things break. This file forces git to use LF for all text files on all systems (except the request testing specfiles) to prevent issues everywhere.
2017-06-26 13:15:24 +02:00
Alda Marteau-Hardi
f53ed108b0 Translate pin/unpin and fix some inconsistencies in gender neutral strings (#3952) 2017-06-26 13:04:36 +02:00
Yamagishi Kazutoshi
285038972b Stop using Babel with streaming server (#3950) 2017-06-26 04:49:39 +02:00
Takuya Yoshida
e5563843a2 Re-fix errorMiddleware (#3922) 2017-06-26 01:46:15 +02:00
unarist
c972e1ee1f Ignore DB_NAME for development env on streaming as well as rails side (#3948) 2017-06-26 01:45:50 +02:00
Eugen Rochko
5e8d037e27 Fix #3910 - Require OTP authentication to disable 2FA (#3935)
* Fix #3910 - Require OTP authentication to disable 2FA. Also, remove ability
to generate new OTP backup codes *after* initial backup codes were handed
out during activation

* Restore recovery code re-generation

* Improve display of some 2FA elements
2017-06-25 23:51:46 +02:00
Eugen Rochko
ed7dc1704d Bind web UI access tokens to sessions (#3940)
* Add overview of active sessions

* Better display of browser/platform name

* Improve how browser information is stored and displayed for sessions overview

* Fix test

* Fix #2347 - Bind web UI access token to session

When you logout, session also destroys the access token, so it's no longer
valid. If access token is destroyed some other way, the session is also
destroyed, requiring a re-login.

Fix #1681 - Add scheduler to remove revoked access tokens and grants

* Fix test
2017-06-25 23:51:32 +02:00
amazedkoumei
436ce03772 fix unnecessary variable (#3947) 2017-06-25 23:29:22 +02:00
Eugen Rochko
d821aba002 Rename "Credentials" page to "Security" for clarity (#3941)
* Rename "Credentials" page to "Security" for clarity

* Change "security" icon from cog to lock
2017-06-25 22:13:02 +02:00
Sorin Davidoi
4ce1540094 fix(features/compose): Handle external changes to the textarea (#3632) 2017-06-25 21:43:27 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
67243bda31 Cover Auth::RegistrationsController more (#3353) 2017-06-25 21:42:55 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
8f991831b8 Cover Admin::DomainBlocksController more (#3329)
Also domain_block fabricator now sets unique domains
2017-06-25 21:42:36 +02:00
amazedkoumei
87efa38721 more free pgconfig by .env (#3909)
* more free pgconfig for streaming by .env

* fix wrong default values

* database.yml read ENV as same as streaming server
2017-06-25 18:13:31 +02:00
Eugen Rochko
f7301bd5b9 Add overview of active sessions (#3929)
* Add overview of active sessions

* Better display of browser/platform name

* Improve how browser information is stored and displayed for sessions overview

* Fix test
2017-06-25 16:54:30 +02:00
PFM
099a3b4eac Fix "undefined" in className (#3939) 2017-06-25 16:02:56 +02:00
unarist
3d4e21f1ec Don't set ASSET_HOST on build:development (#3936)
Setting ASSET_HOST to `http://0.0.0.0:8080` makes urls in manifest.json to
be invalid, e.g. `http://0.0.0.0:8080/packs/application.js`.

Anyway, we don't need set this on build:development because assets would
be delivered from same origin in development (and w/o dev-server).
2017-06-25 12:52:42 +02:00
unarist
68dca26a5d Fix react-intl/locale-data import issue on production build (#3937)
Webpack seems to fail to import `react-intl/locale-data/*.js` if those
files has been proceed by babel, and this also breaks applying our translation.

Note that this won't be a problem on English locale, because react-intl
includes it as default and works fine without manually added locale-data.
Also this issue seems to only occurs on production build, but I'm not sure
about reason.
2017-06-25 12:49:53 +02:00
unarist
1fc096ec75 Fix elephant in onboarding modal being very small sized on small devices (#3932) 2017-06-24 23:18:32 +02:00
unarist
21c2bc119c Clean column collapsible (#3931)
* Remove unused column_collapsable.js
* Remove old styles
* Extract `> div`  style to independent class
2017-06-24 23:18:11 +02:00
Sorin Davidoi
d23293c762 feat(components/onboarding_modal): Swipe between pages (#3934) 2017-06-24 23:17:39 +02:00
unarist
138e5a0b1e Fix webpack config for Windows (#3926) 2017-06-24 14:03:52 +02:00
Yamagishi Kazutoshi
79dacea962 Fix #3924 (regression from #3906) (#3925) 2017-06-24 12:24:02 +02:00
unarist
4e6b5e7879 Use debounce for dispatch scrollTopNotification and expandNotifications (#3700) 2017-06-24 02:43:26 +02:00
Daniel Hunsaker
c0979381a4 Fix a typo and give CW'd statuses the right cursor (#3918) 2017-06-23 23:13:27 +02:00
Eugen Rochko
676f577e7e Fix webpack-dev-server until it's fixed upstream (#3916) 2017-06-23 19:40:51 +02:00
Yamagishi Kazutoshi
c1a8e3d1eb Use Class and Property Decorators (#3730)
ref https://tc39.github.io/proposal-decorators/
2017-06-23 19:36:54 +02:00
Takuya Yoshida
0c44316b22 Fix errorMiddleware to prevent "TypeError: res.writeHead is not a function" (#3913)
* Fix errorMiddleware

* Add "eslint-disable-line no-unused-vars"
2017-06-23 19:22:02 +02:00
Sorin Davidoi
2211e8d1cd Revocable sessions (#3616)
* feat: Revocable sessions

* fix: Tests using sign_in

* feat: Configuration entry for the maximum number of session activations
2017-06-23 18:50:53 +02:00
Nolan Lawson
3783cadf2d Apply babel to react-intl to remove prop-types (#3914) 2017-06-23 18:21:33 +02:00
Eugen Rochko
a071047c13 Merge branch 'sorin-davidoi-swipe-gestures' 2017-06-23 17:52:56 +02:00
Eugen Rochko
281f07244b Merge branch 'swipe-gestures' of git://github.com/sorin-davidoi/mastodon into sorin-davidoi-swipe-gestures 2017-06-23 17:52:39 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
6f34a6a77f Add index statuses on account_id and id (#3895) 2017-06-23 17:46:00 +02:00
Nolan Lawson
e078919f07 Upgrade to Webpack 3 with module concatenation (#3912) 2017-06-23 17:44:55 +02:00
Eugen Rochko
7b13e6efc2 Bump version to 1.4.6 2017-06-23 17:02:14 +02:00
Eugen Rochko
3f59238207 Add important test for full-width hashtags (#3911) 2017-06-23 17:01:53 +02:00
Yamagishi Kazutoshi
eff9416469 Remove unused variables (#3906) 2017-06-23 16:05:04 +02:00
Yamagishi Kazutoshi
6fbb3841a6 Add prefix to setting toggle ID (#3907) 2017-06-23 13:55:05 +02:00
Sorin Davidoi
d8c4781377 fix: Apply :hover, :focus and :active only when multiple columns 2017-06-23 13:48:46 +02:00
Sorin Davidoi
bc6e958229 feat: Swipeable media 2017-06-23 13:48:46 +02:00
Sorin Davidoi
a6d8d1036a feat: Swipeable columns 2017-06-23 13:48:46 +02:00
Sorin Davidoi
3d403a013d chore(yarn): Install react-swipeable 2017-06-23 13:40:58 +02:00
Ratmir Karabut
9ca02a00a6 Update Russian translation (#3902)
* 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
2017-06-22 23:28:58 +02:00
nightpool
3e8e9c8ae4 Use the stable RVM installer (#3901)
as mentioned by ElvenSpellmaker here: https://github.com/rvm/rvm/issues/4068

Adds a workaround for the issue mentioned by @abcang here: https://github.com/tootsuite/mastodon/pull/3897#issuecomment-310436668 and makes sure that we're using the stable installer to install the stable version.
2017-06-22 23:28:52 +02:00
Charlotte Fields
7bc1805827 fixed vagrantfile (#3897) 2017-06-22 18:35:27 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
e27f792c24 Some minor change and spec for Account (#3813)
* Introduce domains method to Account relation

Account had followers_domains method, which was excessively specific.
Let relation of Account have domains method instead.

* Move follow_mapping in Account to AccountInteractions

* Introduce shared examples for AccountAvatar inclusion

* Cover Account more
2017-06-22 18:34:27 +02:00
Quent-in
98fab24bea Update of doorkeeper.oc.yml (#3896)
Just some adjustements
2017-06-22 13:39:13 +02:00
Eugen Rochko
f566c47dda Fix travis builds
https://github.com/travis-ci/travis-ci/issues/7941#issuecomment-310320597
2017-06-22 13:24:00 +02:00
Eugen Rochko
0190aac240 Fix regression from #3842 (#3892)
* Fix regression from #3842

Simplify the query by omitting all direct statuses. Private statuses
are allowed because they are from accounts we are following (so
by definition)

Resolves #3887 (alternative)

* Adjust test
2017-06-22 02:38:50 +02:00
unarist
cc382c5006 Don't attach IntersectionObserver for wrapped statuses (#3883)
(This patch has been merged as bugfix and reverted, but still valuable as
improvement)

Previously, we've attached IntersectionObserver twice for boosted statuses:
wrapper Status and wrapped Status. but wrapped Status don't need to manage
intersection and visibility by itself, because it's a part of wrapper Status.
2017-06-21 06:47:36 +02:00
239 changed files with 2489 additions and 1805 deletions

View File

@@ -15,6 +15,7 @@
"plugins": [
"syntax-dynamic-import",
["transform-object-rest-spread", { "useBuiltIns": true }],
"transform-decorators-legacy",
"transform-class-properties",
[
"react-intl",

View File

@@ -1,7 +1,9 @@
---
root: true
env:
browser: true
node: false
node: true
es6: true
parser: babel-eslint
@@ -52,8 +54,14 @@ rules:
no-mixed-spaces-and-tabs: warn
no-nested-ternary: warn
no-trailing-spaces: warn
no-undef: error
no-unreachable: error
no-unused-expressions: error
no-unused-vars:
- error
- vars: all
args: after-used
ignoreRestSiblings: true
object-curly-spacing:
- error
- always
@@ -81,7 +89,10 @@ rules:
- 2
react/jsx-no-bind: error
react/jsx-no-duplicate-props: error
react/jsx-no-undef: error
react/jsx-tag-spacing: error
react/jsx-uses-react: error
react/jsx-uses-vars: error
react/jsx-wrap-multilines: error
react/no-multi-comp: off
react/no-string-refs: error

14
.gitattributes vendored Normal file
View File

@@ -0,0 +1,14 @@
* text=auto eol=lf
*.eot -text
*.gif -text
*.gz -text
*.ico -text
*.jpg -text
*.mp3 -text
*.ogg -text
*.png -text
*.ttf -text
*.webm -text
*.woff -text
*.woff2 -text
spec/fixtures/requests/** -text !eol

View File

@@ -7,7 +7,7 @@ cache:
- public/assets
- public/packs-test
dist: trusty
sudo: false
sudo: required
notifications:
email: false

View File

@@ -20,6 +20,7 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'browser'
gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'

View File

@@ -1,40 +1,40 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.1)
actionpack (= 5.1.1)
actioncable (5.1.2)
actionpack (= 5.1.2)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.1)
actionpack (= 5.1.1)
actionview (= 5.1.1)
activejob (= 5.1.1)
actionmailer (5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.1)
actionview (= 5.1.1)
activesupport (= 5.1.1)
actionpack (5.1.2)
actionview (= 5.1.2)
activesupport (= 5.1.2)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.1)
activesupport (= 5.1.1)
actionview (5.1.2)
activesupport (= 5.1.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_record_query_trace (1.5.4)
activejob (5.1.1)
activesupport (= 5.1.1)
activejob (5.1.2)
activesupport (= 5.1.2)
globalid (>= 0.3.6)
activemodel (5.1.1)
activesupport (= 5.1.1)
activerecord (5.1.1)
activemodel (= 5.1.1)
activesupport (= 5.1.1)
activemodel (5.1.2)
activesupport (= 5.1.2)
activerecord (5.1.2)
activemodel (= 5.1.2)
activesupport (= 5.1.2)
arel (~> 8.0)
activesupport (5.1.1)
activesupport (5.1.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
@@ -70,6 +70,7 @@ GEM
bootsnap (1.0.0)
msgpack (~> 1.0)
brakeman (3.6.2)
browser (2.4.0)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
@@ -297,17 +298,17 @@ GEM
rack-test (0.6.3)
rack (>= 1.0)
rack-timeout (0.4.2)
rails (5.1.1)
actioncable (= 5.1.1)
actionmailer (= 5.1.1)
actionpack (= 5.1.1)
actionview (= 5.1.1)
activejob (= 5.1.1)
activemodel (= 5.1.1)
activerecord (= 5.1.1)
activesupport (= 5.1.1)
rails (5.1.2)
actioncable (= 5.1.2)
actionmailer (= 5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
activemodel (= 5.1.2)
activerecord (= 5.1.2)
activesupport (= 5.1.2)
bundler (>= 1.3.0, < 2.0)
railties (= 5.1.1)
railties (= 5.1.2)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -323,9 +324,9 @@ GEM
railties (~> 5.0)
rails-settings-cached (0.6.5)
rails (>= 4.2.0)
railties (5.1.1)
actionpack (= 5.1.1)
activesupport (= 5.1.1)
railties (5.1.2)
actionpack (= 5.1.2)
activesupport (= 5.1.2)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -483,6 +484,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7)
bootsnap
brakeman (~> 3.6)
browser
bullet (~> 5.5)
bundler-audit (~> 0.5)
capistrano (~> 3.8)

View File

@@ -1,5 +1,70 @@
Mastodon Glitch Edition
Mastodon
========
Now with automated deploys!
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?
[![Build Status](http://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/github/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
Click on the screenshot 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
The project focus is a clean REST API and a good user interface. 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
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
## Features
- **Fully interoperable with GNU social and any OStatus platform**
Whatever implements Atom feeds, ActivityStreams, Salmon, PubSubHubbub and Webfinger is part of the network
- **Real-time timeline updates**
See the updates of people you're following appear in real-time in the UI via WebSockets
- **Federated thread resolving**
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 WebM**
Upload and view images and WebM videos attached to the updates
- **OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API, which is RESTful and simple
- **Background processing for long-running tasks**
Mastodon tries to be as fast and responsive as possible, so all long-running tasks that can be delegated to background processing, are
- **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 [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis
- The error page image courtesy of [Dopatwo](https://www.youtube.com/user/dopatwo)
![Mastodon error image](https://mastodon.social/oops.png)

5
Vagrantfile vendored
View File

@@ -42,9 +42,12 @@ sudo apt-get install \
# Install rvm
read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io | bash -s stable --ruby=$RUBY_VERSION
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm
# Install Ruby
rvm install ruby-$RUBY_VERSION
# Configure database
sudo -u postgres createuser -U postgres vagrant -s
sudo -u postgres createdb -U postgres mastodon_development

View File

@@ -17,6 +17,9 @@ class Api::V1::ReportsController < Api::BaseController
status_ids: reported_status_ids,
comment: report_params[:comment]
)
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render :show
end

View File

@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include UserTrackingConcern
helper_method :current_account
helper_method :current_session
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
@@ -68,6 +69,10 @@ class ApplicationController < ActionController::Base
@current_account ||= current_user.try(:account)
end
def current_session
@current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)

View File

@@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
def destroy
not_found
@@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth'
end
def set_sessions
@sessions = current_user.session_activations
end
end

View File

@@ -5,11 +5,10 @@ class HomeController < ApplicationController
def index
@body_classes = 'app-body'
@token = find_or_create_access_token.token
@token = current_session.token
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
@admin = Account.find_local(Setting.site_contact_username)
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
end
private
@@ -17,14 +16,4 @@ class HomeController < ApplicationController
def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
end
def find_or_create_access_token
Doorkeeper::AccessToken.find_or_create_for(
Doorkeeper::Application.where(superapp: true).first,
current_user.id,
Doorkeeper::OAuth::Scopes.from_string('read write follow'),
Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled?
)
end
end

View File

@@ -7,7 +7,9 @@ module Settings
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
def show; end
def show
@confirmation = Form::TwoFactorConfirmation.new
end
def create
current_user.otp_secret = User.generate_otp_secret(32)
@@ -16,13 +18,23 @@ module Settings
end
def destroy
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
if current_user.validate_and_consume_otp!(confirmation_params[:code])
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
@confirmation = Form::TwoFactorConfirmation.new
render :show
end
end
private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:code)
end
def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end

View File

@@ -41,4 +41,16 @@ module SettingsHelper
def hash_to_object(hash)
HashObject.new(hash)
end
def session_device_icon(session)
device = session.detection.device
if device.mobile?
'mobile'
elsif device.tablet?
'tablet'
else
'desktop'
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,5 +1,4 @@
import api, { getLinks } from '../api';
import Immutable from 'immutable';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@@ -597,7 +596,7 @@ export function authorizeFollowRequest(id) {
api(getState)
.post(`/api/v1/follow_requests/${id}/authorize`)
.then(response => dispatch(authorizeFollowRequestSuccess(id)))
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
};
};
@@ -631,7 +630,7 @@ export function rejectFollowRequest(id) {
api(getState)
.post(`/api/v1/follow_requests/${id}/reject`)
.then(response => dispatch(rejectFollowRequestSuccess(id)))
.then(() => dispatch(rejectFollowRequestSuccess(id)))
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
};
};

View File

@@ -16,7 +16,7 @@ export function blockDomain(domain, accountId) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post('/api/v1/domain_blocks', { domain }).then(response => {
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
dispatch(blockDomainSuccess(domain, accountId));
}).catch(err => {
dispatch(blockDomainFail(domain, err));
@@ -51,7 +51,7 @@ export function unblockDomain(domain, accountId) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(response => {
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
dispatch(unblockDomainSuccess(domain, accountId));
}).catch(err => {
dispatch(unblockDomainFail(domain, err));

View File

@@ -17,7 +17,7 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
const messages = defineMessages({
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});

View File

@@ -1,4 +1,5 @@
import api from '../api';
import { openModal, closeModal } from './modal';
export const REPORT_INIT = 'REPORT_INIT';
export const REPORT_CANCEL = 'REPORT_CANCEL';
@@ -11,10 +12,14 @@ export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
export function initReport(account, status) {
return {
type: REPORT_INIT,
account,
status,
return dispatch => {
dispatch({
type: REPORT_INIT,
account,
status,
});
dispatch(openModal('REPORT'));
};
};
@@ -40,7 +45,10 @@ export function submitReport() {
account_id: getState().getIn(['reports', 'new', 'account_id']),
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
comment: getState().getIn(['reports', 'new', 'comment']),
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error)));
}).then(response => {
dispatch(closeModal());
dispatch(submitReportSuccess(response.data));
}).catch(error => dispatch(submitReportFail(error)));
};
};

View File

@@ -74,7 +74,7 @@ export function deleteStatus(id) {
return (dispatch, getState) => {
dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
}).catch(error => {
@@ -152,7 +152,7 @@ export function muteStatus(id) {
return (dispatch, getState) => {
dispatch(muteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/mute`).then(response => {
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
dispatch(muteStatusSuccess(id));
}).catch(error => {
dispatch(muteStatusFail(id, error));
@@ -186,7 +186,7 @@ export function unmuteStatus(id) {
return (dispatch, getState) => {
dispatch(unmuteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(response => {
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
dispatch(unmuteStatusSuccess(id));
}).catch(error => {
dispatch(unmuteStatusFail(id, error));

View File

@@ -16,7 +16,8 @@ const messages = defineMessages({
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
});
class Account extends ImmutablePureComponent {
@injectIntl
export default class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -82,5 +83,3 @@ class Account extends ImmutablePureComponent {
}
}
export default injectIntl(Account);

View File

@@ -4,7 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
class AttachmentList extends ImmutablePureComponent {
export default class AttachmentList extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
@@ -31,5 +31,3 @@ class AttachmentList extends ImmutablePureComponent {
}
}
export default AttachmentList;

View File

@@ -31,7 +31,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
}
};
class AutosuggestTextarea extends ImmutablePureComponent {
export default class AutosuggestTextarea extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
@@ -196,5 +196,3 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}
}
export default AutosuggestTextarea;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class Avatar extends React.PureComponent {
export default class Avatar extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
@@ -66,5 +66,3 @@ class Avatar extends React.PureComponent {
}
}
export default Avatar;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class AvatarOverlay extends React.PureComponent {
export default class AvatarOverlay extends React.PureComponent {
static propTypes = {
staticSrc: PropTypes.string.isRequired,
@@ -28,5 +28,3 @@ class AvatarOverlay extends React.PureComponent {
}
}
export default AvatarOverlay;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class Button extends React.PureComponent {
export default class Button extends React.PureComponent {
static propTypes = {
text: PropTypes.node,
@@ -61,5 +61,3 @@ class Button extends React.PureComponent {
}
}
export default Button;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import scrollTop from '../scroll';
class Column extends React.PureComponent {
export default class Column extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
@@ -41,5 +41,3 @@ class Column extends React.PureComponent {
}
}
export default Column;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
class ColumnBackButton extends React.PureComponent {
export default class ColumnBackButton extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -23,5 +23,3 @@ class ColumnBackButton extends React.PureComponent {
}
}
export default ColumnBackButton;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
class ColumnBackButtonSlim extends React.PureComponent {
export default class ColumnBackButtonSlim extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -25,5 +25,3 @@ class ColumnBackButtonSlim extends React.PureComponent {
}
}
export default ColumnBackButtonSlim;

View File

@@ -1,52 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class ColumnCollapsable extends React.PureComponent {
static propTypes = {
icon: PropTypes.string.isRequired,
title: PropTypes.string,
fullHeight: PropTypes.number.isRequired,
children: PropTypes.node,
onCollapse: PropTypes.func,
};
state = {
collapsed: true,
animating: false,
};
handleToggleCollapsed = () => {
const currentState = this.state.collapsed;
this.setState({ collapsed: !currentState, animating: true });
if (!currentState && this.props.onCollapse) {
this.props.onCollapse();
}
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
render () {
const { icon, title, fullHeight, children } = this.props;
const { collapsed, animating } = this.state;
return (
<div className={`column-collapsable ${collapsed ? 'collapsed' : ''}`} onTransitionEnd={this.handleTransitionEnd}>
<div role='button' tabIndex='0' title={`${title}`} className='column-collapsable__button column-icon' onClick={this.handleToggleCollapsed}>
<i className={`fa fa-${icon}`} />
</div>
<div className='column-collapsable__content' style={{ height: `${fullHeight}px` }}>
{(!collapsed || animating) && children}
</div>
</div>
);
}
}
export default ColumnCollapsable;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
class ColumnHeader extends React.PureComponent {
export default class ColumnHeader extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -132,7 +132,7 @@ class ColumnHeader extends React.PureComponent {
</div>
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
<div>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>
</div>
@@ -141,5 +141,3 @@ class ColumnHeader extends React.PureComponent {
}
}
export default ColumnHeader;

View File

@@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../emoji';
class DisplayName extends React.PureComponent {
export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -21,5 +21,3 @@ class DisplayName extends React.PureComponent {
}
}
export default DisplayName;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PropTypes from 'prop-types';
class DropdownMenu extends React.PureComponent {
export default class DropdownMenu extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -56,7 +56,7 @@ class DropdownMenu extends React.PureComponent {
return <li key={`sep-${i}`} className='dropdown__sep' />;
}
const { text, action, href = '#' } = item;
const { text, href = '#' } = item;
return (
<li className='dropdown__content-list-item' key={`${text}-${i}`}>
@@ -92,5 +92,3 @@ class DropdownMenu extends React.PureComponent {
}
}
export default DropdownMenu;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class ExtendedVideoPlayer extends React.PureComponent {
export default class ExtendedVideoPlayer extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
@@ -44,5 +44,3 @@ class ExtendedVideoPlayer extends React.PureComponent {
}
}
export default ExtendedVideoPlayer;

View File

@@ -3,7 +3,7 @@ import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
class IconButton extends React.PureComponent {
export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
@@ -17,7 +17,6 @@ class IconButton extends React.PureComponent {
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
flip: PropTypes.bool,
overlay: PropTypes.bool,
};
@@ -70,7 +69,7 @@ class IconButton extends React.PureComponent {
}
return (
<Motion defaultStyle={{ rotate: this.props.active ? (this.props.flip ? -180 : -360) : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? (this.props.flip ? -180 : -360) : 0, { stiffness: this.props.flip ? 60 : 120, damping: 7 }) : 0 }}>
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) =>
<button
aria-label={this.props.title}
@@ -87,5 +86,3 @@ class IconButton extends React.PureComponent {
}
}
export default IconButton;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
class LoadMore extends React.PureComponent {
export default class LoadMore extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func,
@@ -17,5 +17,3 @@ class LoadMore extends React.PureComponent {
}
}
export default LoadMore;

View File

@@ -85,14 +85,24 @@ class Item extends React.PureComponent {
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 // eslint-disable-line jsx-a11y/anchor-has-content
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || attachment.get('url')}
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }}
/>
>
<img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
</a>
);
} else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif;
@@ -123,7 +133,8 @@ class Item extends React.PureComponent {
}
class MediaGallery extends React.PureComponent {
@injectIntl
export default class MediaGallery extends React.PureComponent {
static propTypes = {
sensitive: PropTypes.bool,
@@ -138,7 +149,7 @@ class MediaGallery extends React.PureComponent {
visible: !this.props.sensitive,
};
handleOpen = (e) => {
handleOpen = () => {
this.setState({ visible: !this.state.visible });
}
@@ -183,5 +194,3 @@ class MediaGallery extends React.PureComponent {
}
}
export default injectIntl(MediaGallery);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class Permalink extends React.PureComponent {
export default class Permalink extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -25,12 +25,10 @@ class Permalink extends React.PureComponent {
const { href, children, className, ...other } = this.props;
return (
<a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
{children}
</a>
);
}
}
export default Permalink;

View File

@@ -11,7 +11,8 @@ const dateFormatOptions = {
minute: '2-digit',
};
class RelativeTimestamp extends React.Component {
@injectIntl
export default class RelativeTimestamp extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,
@@ -37,5 +38,3 @@ class RelativeTimestamp extends React.Component {
}
}
export default injectIntl(RelativeTimestamp);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
class SettingText extends React.PureComponent {
export default class SettingText extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
@@ -29,5 +29,3 @@ class SettingText extends React.PureComponent {
}
}
export default SettingText;

View File

@@ -3,25 +3,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import MediaGallery from './media_gallery';
import VideoPlayer from './video_player';
import AttachmentList from './attachment_list';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import emojify from '../emoji';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
const messages = defineMessages({
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
});
class Status extends ImmutablePureComponent {
export default class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -43,14 +37,12 @@ class Status extends ImmutablePureComponent {
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object,
intl: PropTypes.object.isRequired,
};
state = {
isExpanded: false,
isIntersecting: true, // assume intersecting until told otherwise
isHidden: false, // set to true in requestIdleCallback to trigger un-render
isCollapsed: false,
}
// Avoid checking props that are functions (and whose equality will always
@@ -68,11 +60,7 @@ class Status extends ImmutablePureComponent {
updateOnStates = ['isExpanded']
shouldComponentUpdate (nextProps, nextState) {
if (nextState.isCollapsed !== this.state.isCollapsed) {
// If the collapsed state of the element has changed then we definitely
// need to re-update.
return true;
} else if (!nextState.isIntersecting && nextState.isHidden) {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
@@ -86,13 +74,7 @@ class Status extends ImmutablePureComponent {
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidUpdate (prevProps, prevState) {
if (prevState.isCollapsed !== this.state.isCollapsed) this.saveHeight();
}
componentDidMount () {
const node = this.node;
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
@@ -104,8 +86,6 @@ class Status extends ImmutablePureComponent {
this.handleIntersection
);
if (node.clientHeight > 400) this.setState({ isCollapsed: true });
this.componentMounted = true;
}
@@ -144,7 +124,7 @@ class Status extends ImmutablePureComponent {
saveHeight = () => {
if (this.node && this.node.children.length !== 0) {
this.height = this.node.clientHeight;
this.height = this.node.getBoundingClientRect().height;
}
}
@@ -170,15 +150,15 @@ class Status extends ImmutablePureComponent {
this.setState({ isExpanded: !this.state.isExpanded });
};
handleCollapsedClick = () => {
this.setState({ isCollapsed: !this.state.isCollapsed });
}
render () {
let media = null;
let mediaIcon = null;
let statusAvatar;
const { status, account, intl, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state;
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) {
return null;
@@ -219,8 +199,10 @@ class Status extends ImmutablePureComponent {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
mediaIcon = 'video-camera';
} else {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
mediaIcon = 'picture-o';
}
}
@@ -231,17 +213,9 @@ class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef}>
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
<div className='status__info'>
<IconButton
className='status__collapse-button'
animate flip
active={isCollapsed}
title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)}
icon='angle-double-up'
onClick={this.handleCollapsedClick}
/>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
@@ -250,18 +224,17 @@ class Status extends ImmutablePureComponent {
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight} />
<StatusContent status={status} mediaIcon={mediaIcon} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight}>
{isCollapsed ? null : media}
{media}
{isCollapsed ? null : <StatusActionBar {...this.props} />}
</StatusContent>
<StatusActionBar {...this.props} />
</div>
);
}
}
export default injectIntl(Status);

View File

@@ -5,7 +5,6 @@ import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import RelativeTimestamp from './relative_timestamp';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -23,7 +22,8 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
});
class StatusActionBar extends ImmutablePureComponent {
@injectIntl
export default class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -87,7 +87,6 @@ class StatusActionBar extends ImmutablePureComponent {
handleReport = () => {
this.props.onReport(this.props.status);
this.context.router.history.push('/report');
}
handleConversationMuteClick = () => {
@@ -145,12 +144,8 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar-dropdown'>
<DropdownMenu 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>
);
}
}
export default injectIntl(StatusActionBar);

View File

@@ -7,7 +7,7 @@ import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
class StatusContent extends React.PureComponent {
export default class StatusContent extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -19,6 +19,8 @@ class StatusContent extends React.PureComponent {
onExpandedToggle: PropTypes.func,
onHeightUpdate: PropTypes.func,
onClick: PropTypes.func,
mediaIcon: PropTypes.string,
children: PropTypes.element,
};
state = {
@@ -32,7 +34,6 @@ class StatusContent extends React.PureComponent {
for (var i = 0; i < links.length; ++i) {
let link = links[i];
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || (item.get('remote_url').length > 0 && link.href === item.get('remote_url')));
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
@@ -108,7 +109,7 @@ class StatusContent extends React.PureComponent {
}
render () {
const { status } = this.props;
const { status, children, mediaIcon } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
@@ -129,15 +130,19 @@ class StatusContent extends React.PureComponent {
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
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='status__content status__content--with_action' ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<div className='status__content status__content--with-action' ref={this.setRef}>
<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>
@@ -145,7 +150,15 @@ class StatusContent extends React.PureComponent {
{mentionsPlaceholder}
<div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
<div
style={directionStyle}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
{children}
</div>
</div>
);
} else if (this.props.onClick) {
@@ -154,10 +167,14 @@ class StatusContent extends React.PureComponent {
ref={this.setRef}
className='status__content status__content--with-action'
style={directionStyle}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
>
<div
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
{children}
</div>
);
} else {
return (
@@ -165,12 +182,12 @@ class StatusContent extends React.PureComponent {
ref={this.setRef}
className='status__content'
style={directionStyle}
dangerouslySetInnerHTML={content}
/>
>
<div dangerouslySetInnerHTML={content} />
{children}
</div>
);
}
}
}
export default StatusContent;

View File

@@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { debounce } from 'lodash';
class StatusList extends ImmutablePureComponent {
export default class StatusList extends ImmutablePureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
@@ -99,7 +99,7 @@ class StatusList extends ImmutablePureComponent {
}
render () {
const { statusIds, onScrollToBottom, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
let loadMore = null;
let scrollableArea = null;
@@ -142,5 +142,3 @@ class StatusList extends ImmutablePureComponent {
}
}
export default StatusList;

View File

@@ -11,7 +11,8 @@ const messages = defineMessages({
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
});
class VideoPlayer extends React.PureComponent {
@injectIntl
export default class VideoPlayer extends React.PureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
@@ -193,5 +194,3 @@ class VideoPlayer extends React.PureComponent {
}
}
export default injectIntl(VideoPlayer);

View File

@@ -3,7 +3,6 @@ import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import {
refreshTimelineSuccess,
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
@@ -27,7 +26,11 @@ const store = configureStore();
const initialState = JSON.parse(document.getElementById('initial-state').textContent);
store.dispatch(hydrateStore(initialState));
class Mastodon extends React.PureComponent {
export default class Mastodon extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
componentDidMount() {
const { locale } = this.props;
@@ -118,9 +121,3 @@ class Mastodon extends React.PureComponent {
}
}
Mastodon.propTypes = {
locale: PropTypes.string.isRequired,
};
export default Mastodon;

View File

@@ -19,8 +19,6 @@ import {
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { createSelector } from 'reselect';
import { isMobile } from '../is_mobile';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({

View File

@@ -21,7 +21,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
class ActionBar extends React.PureComponent {
@injectIntl
export default class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -105,5 +106,3 @@ class ActionBar extends React.PureComponent {
}
}
export default injectIntl(ActionBar);

View File

@@ -16,59 +16,8 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});
/*
THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
with love,
@kibi@glitch.social <3
*/
const NEW_LINE = /(?:^|\r?\n|<br\s*\/?>)/g
const YAML_OPENER = /---/;
const YAML_CLOSER = /(?:---|\.\.\.)/;
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
const YAML_LINE = new RegExp("\\s*" + YAML_STRING.source + "\\s*:\\s*" + YAML_STRING.source + "\\s*", "g");
const BIO_REGEX = new RegExp(NEW_LINE.source + "*" + YAML_OPENER.source + NEW_LINE.source + "+(?:" + YAML_LINE.source + NEW_LINE.source + "+){0,4}" + YAML_CLOSER.source + NEW_LINE.source + "*");
const processBio = (data) => {
let props = {text: data, metadata: []};
let yaml = data.match(BIO_REGEX);
if (!yaml) return props;
else yaml = yaml[0];
let start = props.text.indexOf(yaml);
let end = start + yaml.length;
props.text = props.text.substr(0, start) + props.text.substr(end);
yaml = yaml.replace(NEW_LINE, "\n");
let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
for (let i = 0; i < metadata.length; i++) {
let result = metadata[i].match(YAML_STRING);
if (result[0][0] === '"' || result[0][0] === "'") result[0] = result[0].substr(1, result[0].length - 2);
if (result[1][0] === '"' || result[1][0] === "'") result[0] = result[1].substr(1, result[1].length - 2);
props.metadata.push(result);
}
return props;
};
const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
const mapStateToProps = state => ({
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
@@ -121,7 +70,9 @@ class Avatar extends ImmutablePureComponent {
}
class Header extends ImmutablePureComponent {
@connect(makeMapStateToProps)
@injectIntl
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
@@ -171,48 +122,23 @@ class Header extends ImmutablePureComponent {
lockedIcon = <i className='fa fa-lock' />;
}
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const { text, metadata } = processBio(account.get('note'));
const content = { __html: emojify(account.get('note')) };
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<div className='account__header__wrapper'>
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
<div className='account__header__content' dangerouslySetInnerHTML={{__html: emojify(text)}} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
<div className='account__header__content' dangerouslySetInnerHTML={content} />
{info}
{actionBtn}
</div>
{info}
{actionBtn}
</div>
{metadata.length && (
<div className='account__metadata'>
{(() => {
let data = [];
for (let i = 0; i < metadata.length; i++) {
data.push(
<div
className='account__metadata-item'
title={metadata[i][0] + ":" + metadata[i][1]}
key={i}
>
<span dangerouslySetInnerHTML={{__html: emojify(metadata[i][0])}} />
<strong dangerouslySetInnerHTML={{__html: emojify(metadata[i][1])}} />
</div>
);
}
return data;
})()}
</div>
) || null}
</div>
);
}
}
export default connect(makeMapStateToProps)(injectIntl(Header));

View File

@@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink';
class MediaItem extends ImmutablePureComponent {
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
@@ -37,5 +37,3 @@ class MediaItem extends ImmutablePureComponent {
}
}
export default MediaItem;

View File

@@ -7,7 +7,6 @@ import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from '../../a
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
import Immutable from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item';
@@ -23,7 +22,8 @@ const mapStateToProps = (state, props) => ({
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
class AccountGallery extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class AccountGallery extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -112,5 +112,3 @@ class AccountGallery extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(AccountGallery);

View File

@@ -6,7 +6,7 @@ import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator';
import ImmutablePureComponent from 'react-immutable-pure-component';
class Header extends ImmutablePureComponent {
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
@@ -38,7 +38,6 @@ class Header extends ImmutablePureComponent {
handleReport = () => {
this.props.onReport(this.props.account);
this.context.router.history.push('/report');
}
handleMute = () => {
@@ -91,5 +90,3 @@ class Header extends ImmutablePureComponent {
}
}
export default Header;

View File

@@ -19,7 +19,8 @@ const mapStateToProps = (state, props) => ({
me: state.getIn(['meta', 'me']),
});
class AccountTimeline extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -77,5 +78,3 @@ class AccountTimeline extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(AccountTimeline);

View File

@@ -19,7 +19,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
});
class Blocks extends ImmutablePureComponent {
@connect(mapStateToProps)
@injectIntl
export default class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -66,5 +68,3 @@ class Blocks extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Blocks));

View File

@@ -2,8 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable';
import SettingToggle from '../../notifications/components/setting_toggle';
import SettingText from '../../../components/setting_text';
const messages = defineMessages({
@@ -11,17 +9,17 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
});
class ColumnSettings extends React.PureComponent {
@injectIntl
export default class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { settings, onChange, onSave, intl } = this.props;
const { settings, onChange, intl } = this.props;
return (
<div>
@@ -35,5 +33,3 @@ class ColumnSettings extends React.PureComponent {
}
}
export default injectIntl(ColumnSettings);

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting, saveSettings } from '../../../actions/settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'community']),
@@ -12,10 +12,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeSetting(['community', ...key], checked));
},
onSave () {
dispatch(saveSettings());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@@ -14,7 +14,6 @@ import {
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
@@ -28,7 +27,9 @@ const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token']),
});
class CommunityTimeline extends React.PureComponent {
@connect(mapStateToProps)
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@@ -142,5 +143,3 @@ class CommunityTimeline extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));

View File

@@ -4,7 +4,7 @@ import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
class AutosuggestAccount extends ImmutablePureComponent {
export default class AutosuggestAccount extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -22,5 +22,3 @@ class AutosuggestAccount extends ImmutablePureComponent {
}
}
export default AutosuggestAccount;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { length } from 'stringz';
class CharacterCounter extends React.PureComponent {
export default class CharacterCounter extends React.PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
@@ -23,5 +23,3 @@ class CharacterCounter extends React.PureComponent {
}
}
export default CharacterCounter;

View File

@@ -7,15 +7,13 @@ import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { debounce } from 'lodash';
import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import { defineMessages, injectIntl } from 'react-intl';
import Collapsable from '../../../components/collapsable';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SensitiveButtonContainer from '../containers/sensitive_button_container';
import EmojiPickerDropdown from './emoji_picker_dropdown';
import UploadFormContainer from '../containers/upload_form_container';
import TextIconButton from './text_icon_button';
import WarningContainer from '../containers/warning_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
@@ -27,7 +25,8 @@ const messages = defineMessages({
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
});
class ComposeForm extends ImmutablePureComponent {
@injectIntl
export default class ComposeForm extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@@ -68,6 +67,12 @@ class ComposeForm extends ImmutablePureComponent {
}
handleSubmit = () => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
this.props.onChange(this.autosuggestTextarea.textarea.value);
}
this.props.onSubmit();
}
@@ -141,7 +146,6 @@ class ComposeForm extends ImmutablePureComponent {
const text = [this.props.spoiler_text, this.props.text].join('');
let publishText = '';
let reply_to_other = false;
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
@@ -202,5 +206,3 @@ class ComposeForm extends ImmutablePureComponent {
}
}
export default injectIntl(ComposeForm);

View File

@@ -24,7 +24,8 @@ const settings = {
let EmojiPicker; // load asynchronously
class EmojiPickerDropdown extends React.PureComponent {
@injectIntl
export default class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@@ -52,7 +53,7 @@ class EmojiPickerDropdown extends React.PureComponent {
import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => {
EmojiPicker = TheEmojiPicker.default;
this.setState({ loading: false });
}).catch(err => {
}).catch(() => {
// TODO: show the user an error?
this.setState({ loading: false });
});
@@ -123,5 +124,3 @@ class EmojiPickerDropdown extends React.PureComponent {
}
}
export default injectIntl(EmojiPickerDropdown);

View File

@@ -1,14 +1,11 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import Permalink from '../../../components/permalink';
import { FormattedMessage } from 'react-intl';
import Link from 'react-router-dom/Link';
import ImmutablePureComponent from 'react-immutable-pure-component';
class NavigationBar extends ImmutablePureComponent {
export default class NavigationBar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -33,5 +30,3 @@ class NavigationBar extends ImmutablePureComponent {
}
}
export default NavigationBar;

View File

@@ -20,7 +20,8 @@ const iconStyle = {
lineHeight: '27px',
};
class PrivacyDropdown extends React.PureComponent {
@injectIntl
export default class PrivacyDropdown extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
@@ -64,7 +65,7 @@ class PrivacyDropdown extends React.PureComponent {
}
render () {
const { value, onChange, intl } = this.props;
const { value, intl } = this.props;
const { open } = this.state;
const options = [
@@ -95,5 +96,3 @@ class PrivacyDropdown extends React.PureComponent {
}
}
export default injectIntl(PrivacyDropdown);

View File

@@ -12,7 +12,8 @@ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
class ReplyIndicator extends ImmutablePureComponent {
@injectIntl
export default class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -61,5 +62,3 @@ class ReplyIndicator extends ImmutablePureComponent {
}
}
export default injectIntl(ReplyIndicator);

View File

@@ -1,12 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
});
class Search extends React.PureComponent {
@injectIntl
export default class Search extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
@@ -70,5 +71,3 @@ class Search extends React.PureComponent {
}
}
export default injectIntl(Search);

View File

@@ -1,12 +1,12 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import Link from 'react-router-dom/Link';
import ImmutablePureComponent from 'react-immutable-pure-component';
class SearchResults extends ImmutablePureComponent {
export default class SearchResults extends ImmutablePureComponent {
static propTypes = {
results: ImmutablePropTypes.map.isRequired,
@@ -63,5 +63,3 @@ class SearchResults extends ImmutablePureComponent {
}
}
export default SearchResults;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class TextIconButton extends React.PureComponent {
export default class TextIconButton extends React.PureComponent {
static propTypes = {
label: PropTypes.string.isRequired,
@@ -27,5 +27,3 @@ class TextIconButton extends React.PureComponent {
}
}
export default TextIconButton;

View File

@@ -11,7 +11,7 @@ const messages = defineMessages({
});
const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
const mapStateToProps = state => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
@@ -23,7 +23,9 @@ const iconStyle = {
lineHeight: '27px',
};
class UploadButton extends ImmutablePureComponent {
@connect(makeMapStateToProps)
@injectIntl
export default class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
@@ -70,5 +72,3 @@ class UploadButton extends ImmutablePureComponent {
}
}
export default connect(makeMapStateToProps)(injectIntl(UploadButton));

View File

@@ -11,7 +11,8 @@ const messages = defineMessages({
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' },
});
class UploadForm extends React.PureComponent {
@injectIntl
export default class UploadForm extends React.PureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
@@ -48,5 +49,3 @@ class UploadForm extends React.PureComponent {
}
}
export default injectIntl(UploadForm);

View File

@@ -4,7 +4,7 @@ import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';
class UploadProgress extends React.PureComponent {
export default class UploadProgress extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
@@ -40,5 +40,3 @@ class UploadProgress extends React.PureComponent {
}
}
export default UploadProgress;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class Warning extends React.PureComponent {
export default class Warning extends React.PureComponent {
static propTypes = {
message: PropTypes.node.isRequired,
@@ -18,5 +18,3 @@ class Warning extends React.PureComponent {
}
}
export default Warning;

View File

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

View File

@@ -6,7 +6,7 @@ import ReplyIndicator from '../components/reply_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({
const mapStateToProps = state => ({
status: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
});

View File

@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import UploadForm from '../components/upload_form';
import { undoUploadCompose } from '../../../actions/compose';
const mapStateToProps = (state, props) => ({
const mapStateToProps = state => ({
media: state.getIn(['compose', 'media_attachments']),
});

View File

@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import UploadProgress from '../components/upload_progress';
const mapStateToProps = (state, props) => ({
const mapStateToProps = state => ({
active: state.getIn(['compose', 'is_uploading']),
progress: state.getIn(['compose', 'progress']),
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import ComposeFormContainer from './containers/compose_form_container';
import UploadFormContainer from './containers/upload_form_container';
import NavigationContainer from './containers/navigation_container';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@@ -24,7 +23,9 @@ const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
});
class Compose extends React.PureComponent {
@connect(mapStateToProps)
@injectIntl
export default class Compose extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@@ -83,5 +84,3 @@ class Compose extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Compose));

View File

@@ -20,7 +20,9 @@ const mapStateToProps = state => ({
me: state.getIn(['meta', 'me']),
});
class Favourites extends ImmutablePureComponent {
@connect(mapStateToProps)
@injectIntl
export default class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@@ -39,7 +41,7 @@ class Favourites extends ImmutablePureComponent {
}
render () {
const { statusIds, loaded, intl, me } = this.props;
const { loaded, intl } = this.props;
if (!loaded) {
return (
@@ -58,5 +60,3 @@ class Favourites extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Favourites));

View File

@@ -14,7 +14,8 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]),
});
class Favourites extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class Favourites extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -57,5 +58,3 @@ class Favourites extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(Favourites);

View File

@@ -14,7 +14,8 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
class AccountAuthorize extends ImmutablePureComponent {
@injectIntl
export default class AccountAuthorize extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -47,5 +48,3 @@ class AccountAuthorize extends ImmutablePureComponent {
}
}
export default injectIntl(AccountAuthorize);

View File

@@ -14,11 +14,11 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch, { id }) => ({
onAuthorize (account) {
onAuthorize () {
dispatch(authorizeFollowRequest(id));
},
onReject (account) {
onReject () {
dispatch(rejectFollowRequest(id));
},
});

View File

@@ -19,7 +19,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
});
class FollowRequests extends ImmutablePureComponent {
@connect(mapStateToProps)
@injectIntl
export default class FollowRequests extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -67,5 +69,3 @@ class FollowRequests extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(FollowRequests));

View File

@@ -21,7 +21,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'next']),
});
class Followers extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -90,5 +91,3 @@ class Followers extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(Followers);

View File

@@ -21,7 +21,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'following', Number(props.params.accountId), 'next']),
});
class Following extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -90,5 +91,3 @@ class Following extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(Following);

View File

@@ -2,7 +2,6 @@ import React from 'react';
import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import Link from 'react-router-dom/Link';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
@@ -31,7 +30,9 @@ const mapStateToProps = state => ({
columns: state.getIn(['settings', 'columns']),
});
class GettingStarted extends ImmutablePureComponent {
@connect(mapStateToProps)
@injectIntl
export default class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@@ -106,5 +107,3 @@ class GettingStarted extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(GettingStarted));

View File

@@ -11,7 +11,6 @@ import {
deleteFromTimelines,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import { FormattedMessage } from 'react-intl';
import createStream from '../../stream';
@@ -21,7 +20,8 @@ const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token']),
});
class HashtagTimeline extends React.PureComponent {
@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -137,5 +137,3 @@ class HashtagTimeline extends React.PureComponent {
}
}
export default connect(mapStateToProps)(HashtagTimeline);

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable';
import SettingToggle from '../../notifications/components/setting_toggle';
import SettingText from '../../../components/setting_text';
@@ -11,39 +10,37 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
});
class ColumnSettings extends React.PureComponent {
@injectIntl
export default class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { settings, onChange, onSave, intl } = this.props;
const { settings, onChange, intl } = this.props;
return (
<div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
</div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
</div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<div className='column-settings__row'>
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
<SettingText prefix='home_timeline' settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div>
</div>
);
}
}
export default injectIntl(ColumnSettings);

View File

@@ -19,7 +19,9 @@ const mapStateToProps = state => ({
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
});
class HomeTimeline extends React.PureComponent {
@connect(mapStateToProps)
@injectIntl
export default class HomeTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@@ -96,5 +98,3 @@ class HomeTimeline extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(HomeTimeline));

View File

@@ -19,7 +19,16 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
});
class Mutes extends ImmutablePureComponent {
@connect(mapStateToProps)
@injectIntl
export default class Mutes extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchMutes());
@@ -59,12 +68,3 @@ class Mutes extends ImmutablePureComponent {
}
}
Mutes.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
export default connect(mapStateToProps)(injectIntl(Mutes));

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
class ClearColumnButton extends React.Component {
export default class ClearColumnButton extends React.Component {
static propTypes = {
onClick: PropTypes.func.isRequired,
@@ -15,5 +15,3 @@ class ClearColumnButton extends React.Component {
}
}
export default ClearColumnButton;

View File

@@ -2,11 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable';
import ClearColumnButton from './clear_column_button';
import SettingToggle from './setting_toggle';
class ColumnSettings extends React.PureComponent {
export default class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
@@ -16,7 +15,7 @@ class ColumnSettings extends React.PureComponent {
};
render () {
const { settings, onChange, onSave, onClear } = this.props;
const { settings, onChange, onClear } = this.props;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
@@ -31,38 +30,36 @@ class ColumnSettings extends React.PureComponent {
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
<SettingToggle settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
<SettingToggle settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
<SettingToggle settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
<SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>
</div>
);
}
}
export default ColumnSettings;

View File

@@ -2,14 +2,13 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContainer from '../../../containers/status_container';
import AccountContainer from '../../../containers/account_container';
import Avatar from '../../../components/avatar';
import { FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
class Notification extends ImmutablePureComponent {
export default class Notification extends ImmutablePureComponent {
static propTypes = {
notification: ImmutablePropTypes.map.isRequired,
@@ -87,5 +86,3 @@ class Notification extends ImmutablePureComponent {
}
}
export default Notification;

View File

@@ -3,22 +3,23 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
class SettingToggle extends React.PureComponent {
export default class SettingToggle extends React.PureComponent {
static propTypes = {
prefix: PropTypes.string,
settings: ImmutablePropTypes.map.isRequired,
settingKey: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired,
}
onChange = (e) => {
this.props.onChange(this.props.settingKey, e.target.checked);
onChange = ({ target }) => {
this.props.onChange(this.props.settingKey, target.checked);
}
render () {
const { settings, settingKey, label, onChange } = this.props;
const id = `setting-toggle-${settingKey.join('-')}`;
const { prefix, settings, settingKey, label } = this.props;
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
@@ -29,5 +30,3 @@ class SettingToggle extends React.PureComponent {
}
}
export default SettingToggle;

View File

@@ -13,6 +13,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
import { createSelector } from 'reselect';
import Immutable from 'immutable';
import LoadMore from '../../components/load_more';
import { debounce } from 'lodash';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -30,7 +31,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['notifications', 'next']),
});
class Notifications extends React.PureComponent {
@connect(mapStateToProps)
@injectIntl
export default class Notifications extends React.PureComponent {
static propTypes = {
columnId: PropTypes.string,
@@ -48,19 +51,27 @@ class Notifications extends React.PureComponent {
trackScroll: true,
};
dispatchExpandNotifications = debounce(() => {
this.props.dispatch(expandNotifications());
}, 300, { leading: true });
dispatchScrollToTop = debounce((top) => {
this.props.dispatch(scrollTopNotifications(top));
}, 100);
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && !this.props.isLoading) {
if (this.props.hasMore) {
this.props.dispatch(expandNotifications());
}
} else if (scrollTop < 100) {
this.props.dispatch(scrollTopNotifications(true));
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
this.dispatchExpandNotifications();
}
if (scrollTop < 100) {
this.dispatchScrollToTop(true);
} else {
this.props.dispatch(scrollTopNotifications(false));
this.dispatchScrollToTop(false);
}
}
@@ -72,7 +83,7 @@ class Notifications extends React.PureComponent {
handleLoadMore = (e) => {
e.preventDefault();
this.props.dispatch(expandNotifications());
this.dispatchExpandNotifications();
}
handlePin = () => {
@@ -173,5 +184,3 @@ class Notifications extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Notifications));

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting, saveSettings } from '../../../actions/settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'public']),
@@ -12,10 +12,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeSetting(['public', ...key], checked));
},
onSave () {
dispatch(saveSettings());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@@ -14,7 +14,6 @@ import {
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
@@ -28,7 +27,9 @@ const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token']),
});
class PublicTimeline extends React.PureComponent {
@connect(mapStateToProps)
@injectIntl
export default class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@@ -142,5 +143,3 @@ class PublicTimeline extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(PublicTimeline));

View File

@@ -14,7 +14,8 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]),
});
class Reblogs extends ImmutablePureComponent {
@connect(mapStateToProps)
export default class Reblogs extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
@@ -57,5 +58,3 @@ class Reblogs extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(Reblogs);

View File

@@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import emojify from '../../../emoji';
import Toggle from 'react-toggle';
class StatusCheckBox extends React.PureComponent {
export default class StatusCheckBox extends React.PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
@@ -36,5 +36,3 @@ class StatusCheckBox extends React.PureComponent {
}
}
export default StatusCheckBox;

View File

@@ -15,7 +15,8 @@ const messages = defineMessages({
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
});
class ActionBar extends React.PureComponent {
@injectIntl
export default class ActionBar extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -55,7 +56,6 @@ class ActionBar extends React.PureComponent {
handleReport = () => {
this.props.onReport(this.props.status);
this.context.router.history.push('/report');
}
render () {
@@ -91,5 +91,3 @@ class ActionBar extends React.PureComponent {
}
}
export default injectIntl(ActionBar);

View File

@@ -17,7 +17,7 @@ const getHostname = url => {
return parser.hostname;
};
class Card extends React.PureComponent {
export default class Card extends React.PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
@@ -97,5 +97,3 @@ class Card extends React.PureComponent {
}
}
export default Card;

View File

@@ -12,7 +12,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
class DetailedStatus extends ImmutablePureComponent {
export default class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -38,6 +38,7 @@ class DetailedStatus extends ImmutablePureComponent {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
let media = '';
let mediaIcon = null;
let applicationLink = '';
if (status.get('media_attachments').size > 0) {
@@ -45,12 +46,12 @@ class DetailedStatus extends ImmutablePureComponent {
media = <AttachmentList media={status.get('media_attachments')} />;
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
mediaIcon = 'video-camera';
} else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
mediaIcon = 'picture-o';
}
} else if (status.get('spoiler_text').length === 0) {
media = <CardContainer statusId={status.get('id')} />;
}
} else media = <CardContainer statusId={status.get('id')} />;
if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
@@ -63,9 +64,7 @@ class DetailedStatus extends ImmutablePureComponent {
<DisplayName account={status.get('account')} />
</a>
<StatusContent status={status} />
{media}
<StatusContent status={status} mediaIcon={mediaIcon}>{media}</StatusContent>
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
@@ -87,5 +86,3 @@ class DetailedStatus extends ImmutablePureComponent {
}
}
export default DetailedStatus;

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