Compare commits

..

324 Commits

Author SHA1 Message Date
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
Eugen Rochko
946a166791 Revert #3851 (#3878) 2017-06-21 01:37:15 +02:00
Eugen Rochko
31cd649041 Revert "Don't attach IntersectionObserver for wrapped statuses" (#3877)
* Revert "Bump version to 1.4.4"

This reverts commit 1585b0c6cc.

* Revert "Fix conversations (fixes #3869) (#3870)"

This reverts commit 15b43f555d.

* Revert "Fix streaming server. Redis connection subscribe for each channel. (#3828)"

This reverts commit d8ec832806.

* Revert "Filter direct statuses in Status.as_home_timeline (#3842)"

This reverts commit bab5a18232.

* Revert "Fix RemoteFollow behavior (#3868)"

This reverts commit a20cf3b64e.

* Revert "Update fabricator for MediaAttachment to attach a file according to type (#3862)"

This reverts commit 356df7ae6b.

* Revert "Upgrade React Router (#3677)"

This reverts commit 8f03fdce7f.

* Revert "Do not call setState from unmounted component (#3853)"

This reverts commit 1fc6cb4997.

* Revert "Replace TextIconButton for SensitiveButton to IconButton (#3759)"

This reverts commit eb832e88f4.

* Revert "Fix RTL detection on Ruby side (#3867)"

This reverts commit b16b69350e.

* Revert "i18n: Fixed typo in Polish translation (#3864)"

This reverts commit da6fa029f6.

* Revert "Don't attach IntersectionObserver for wrapped statuses (#3863)"

This reverts commit 94ad0706f5.
2017-06-21 01:33:14 +02:00
Eugen Rochko
1585b0c6cc Bump version to 1.4.4 2017-06-20 21:32:37 +02:00
ThibG
15b43f555d Fix conversations (fixes #3869) (#3870)
* Actually create conversations given explicit URIs

* Try to get the parent toot in before validation, to avoid creating a new conversation
2017-06-20 20:44:32 +02:00
猫吸血鬼ディフリス / 猫ロキP
d8ec832806 Fix streaming server. Redis connection subscribe for each channel. (#3828) 2017-06-20 20:41:41 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
bab5a18232 Filter direct statuses in Status.as_home_timeline (#3842)
The classes using Status.as_home_timeline, namely Feed and
PrecomputeFeedService are expected to filter direct statuses as
FanOutWriteService does, but their filtering were incomplete or missing.

This commit solves the problem by filtering direct statuses in
as_home_timeline as the other similar methods such as as_public_timeline
does.
2017-06-20 20:41:23 +02:00
unarist
a20cf3b64e Fix RemoteFollow behavior (#3868)
* Invalid acct is an error. not "2 errors".
* Empty input should be different error from invalid acct
2017-06-20 20:40:56 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
356df7ae6b Update fabricator for MediaAttachment to attach a file according to type (#3862)
This fixes a random spec failures since commit
d55f207274.
2017-06-20 20:40:45 +02:00
Sorin Davidoi
8f03fdce7f Upgrade React Router (#3677)
* chore(yarn): Remove react-router

* chore(yarn): Remove react-router-scroll

* chore(yarn): Remove history

* chore(yarn): Add react-router-dom

* chore: Remove usages of react-router-scroll

* refactor: Upgrade to react-router-web

* refactor: Use fork of react-router-scroll

This reverts commit 2ddea9a6c8d39fc64b7d0b587f3fbda7a45a7fa2.

* fix: Issues mentions in the PR feedback
2017-06-20 20:40:03 +02:00
alpaca-tc
1fc6cb4997 Do not call setState from unmounted component (#3853)
Stop an executing task if the component already unmounted.
2017-06-20 20:37:09 +02:00
Yamagishi Kazutoshi
eb832e88f4 Replace TextIconButton for SensitiveButton to IconButton (#3759)
* Replace TextIconButton for SensitiveButton to IconButton

* line-height
2017-06-20 19:43:09 +02:00
unarist
b16b69350e Fix RTL detection on Ruby side (#3867)
This fixes below bugs:

* pipe characters being counted as RTL character
* only first word being checked
2017-06-20 18:45:09 +02:00
m4sk1n
da6fa029f6 i18n: Fixed typo in Polish translation (#3864) 2017-06-20 18:34:27 +02:00
unarist
94ad0706f5 Don't attach IntersectionObserver for wrapped statuses (#3863)
This fixes a bug that sometimes boosted statuses being hidden on scrolling.

Previously, we've attached IntersectionObserver twice for boosted statuses:
wrapper Status and wrapped Status. This will call intersection handler twice,
so this may results race condition...probably.
2017-06-20 04:12:51 +02:00
unarist
bf8c2c4348 Clicking on the CW text should expand the status (#3855) 2017-06-20 01:34:10 +02:00
unarist
aa58cca040 Set cursor:pointer only when necessary (#3857) 2017-06-19 18:27:07 +02:00
のら
5cc7cd8518 Add Japanese translation "Action taken by" (#3850) 2017-06-19 15:12:41 +02:00
masarakki
ff142eb64d setting-for-account-deletable (#3852) 2017-06-19 15:12:31 +02:00
Yamagishi Kazutoshi
500e28442f Re-add disableHostCheck (regression #3729) (#3854)
ref #2790
2017-06-19 14:01:31 +02:00
Daigo 3 Dango
5bd3715a4c Link to /about from public page on a single user instance (#3814)
There was no link for visitors to follow to see the about page.
2017-06-19 11:32:28 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
3d13f6ea0c Cover Block more (#3837) 2017-06-19 11:31:37 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
6eefccdacc Cover DomainBlock more (#3838) 2017-06-19 11:31:27 +02:00
Gô Shoemake
29a22691d2 Fix character/grapheme count stuff (#3839)
* Bring Toot button in line with counter

Both should use stringz I guess

* Use grapheme_length for character count
2017-06-19 11:31:14 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
d55f207274 Cover Export more (#3840) 2017-06-19 11:30:27 +02:00
alpaca-tc
cf6fe4f8cb Unobserve status on unmount (#3851) 2017-06-19 11:29:57 +02:00
spla
4367443287 Added new Catalan strings (#3843)
* Add Catalan language

* Add Catalan language

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update settings_helper.rb

* Update mastodon.js

* Update index.js

* Update application.rb

* Update ca.yml

* removed extra spaces at line 225

* Catalan translation update

added activerecord.ca.yml

* Update activerecord.ca.yml

Done

* Updated activerecord.ca.yml

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Update ca.json

Removed :

<<<<<<< HEAD
  "getting_started.support": "{faq} • {userguide} • {apps}",
=======
>>>>>>> upstream/master
2017-06-19 10:29:18 +02:00
alpaca-tc
8d2b3ada80 Fixes streaming callbacks of HashtagTimeline (#3849) 2017-06-19 10:28:35 +02:00
Eugen Rochko
f3be605286 Rename FollowRemoteAccountService to ResolveRemoteAccountService (#3847)
Rename Activitypub to ActivityPub
2017-06-19 01:51:04 +02:00
Eugen Rochko
aebebdc5d1 Debounce autosuggestions (#3836)
* Debounce autosuggestions

* Remove duplicate import
2017-06-19 01:50:56 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
05e4728de7 Cover Favourite more (#3841) 2017-06-19 01:38:50 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
b51945f096 Spec AccountDomainBlock (#3816) 2017-06-18 02:59:49 +02:00
Nolan Lawson
1f2abd8d67 Fix jittery scrolling for Chromium browsers (#3776) (#3832) 2017-06-18 02:59:29 +02:00
Nolan Lawson
1d9f9352a6 handle resize in a debounce() (#3834) 2017-06-18 02:57:41 +02:00
Yamagishi Kazutoshi
53e42bf91e Upgrade Webpacker to version 2.0 (#3729) 2017-06-18 02:57:09 +02:00
nightpool
94d0e012de Whitelist allowed classes for federated statuses (#3810)
* Whitelist allowed classes for federated statuses

Allowed classes are currently:

 - Any microformats class (h/p/u/dt/e-*)
 - the classes mention, hashtag, ellipses and invisible.

this last one is somewhat suspect, but Mastodon currently uses it to render hidden link text.

resolved #3790

* Fix code style
2017-06-17 20:26:05 +02:00
Quent-in
8fd931dc12 l10n update for account deletion (#3820)
* l10n update for acount deletion

* l10n little change

* l10n little changes

Less passive voice, more natural.

* Update oc.yml

Correction, " added
2017-06-17 20:21:25 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
74d10b9b9d Spec UserMailer (#3757) 2017-06-17 01:15:00 +02:00
unarist
2356580cee Use cross-env for npm run on Windows (#3789) 2017-06-17 01:14:42 +02:00
abcang
1840a352f5 Fix ogp url (#3802) 2017-06-16 22:38:26 +02:00
Yamagishi Kazutoshi
c93d0978f2 Upgrade react-immutable-pure-component to version 1.0.0 (#3786) 2017-06-16 01:46:12 +02:00
Yamagishi Kazutoshi
df4f4e94b3 Add alt attribute to ImageLoader (#3765) 2017-06-15 14:10:41 +02:00
ThibG
51b2f789bd Fix #3633 by not spawning RemoteProfileUpdateWorker from FetchRemoteAccountService (#3642) 2017-06-15 11:04:23 +02:00
Eugen Rochko
947887f261 Bump version to 1.4.3 2017-06-15 03:03:42 +02:00
m4sk1n
6f34fdb616 updated Polish translation (#3751)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-06-15 02:00:23 +02:00
Eugen Rochko
8518d005fd Fix regression from #3490 - filter out hidden statuses from ancestors/descendants even if the viewer is anonymous (#3752) 2017-06-15 02:00:08 +02:00
Eugen Rochko
bb911043de Fix regression from #3748 - properly set defaultColumns (#3750) 2017-06-14 23:07:06 +02:00
unarist
da0333f1cb Add Japanese translation for Account deletion (#3747) 2017-06-14 22:00:08 +02:00
unarist
d8a0ee1956 Fix merge default columns (#3748)
mergeDeep also merges columns, but it should be replaced simply.

So in the new function, first apply mergeDeep except columns, and set default columns if columns unset.
2017-06-14 21:59:52 +02:00
Eugen Rochko
91c71471ab Fix account delete form not accepting password, update suspended (#3745)
account before removing content for quicker feedback to end-users
2017-06-14 20:30:12 +02:00
m4sk1n
98eacb2238 i18n: updated Polish translation (#3744) 2017-06-14 20:08:00 +02:00
Eugen Rochko
80c13bf0ef Save settings when they are changed (#3743) 2017-06-14 18:59:02 +02:00
Eugen Rochko
e17c2e5da5 Batched remove status service (#3735)
* Make Pubsubhubbub::DistributionWorker handle both single stream entry
arguments, as well as arrays of stream entries

* Add BatchedRemoveStatusService, make SuspendAccountService use it

* Improve method names

* Add test

* Add more tests

* Use PuSH payloads of 100 to have a clear mapping of
1000 input statuses -> 10 PuSH payloads

It was nice while it lasted
2017-06-14 18:01:35 +02:00
Eugen Rochko
4a618908e8 Account deletion (#3728)
* Add form for account deletion

* If avatar or header are gone from source, remove them

* Add option to have SuspendAccountService remove user record, add tests

* Exclude suspended accounts from search
2017-06-14 18:01:27 +02:00
Yamagishi Kazutoshi
a208e7d655 Update React to version v15.6 (#3736) 2017-06-14 16:16:00 +02:00
Yamagishi Kazutoshi
c1b9ae7fc2 Enable useBuiltIns option of transform-object-rest-spread (#3737) 2017-06-14 13:42:00 +02:00
Eugen Rochko
dc8a6244fc Fix #2619 - When redis feed is empty, fall back to database (#3721)
* Fix #2619 - When redis feed is empty, fall back to database

* Use redis value to return feed from database only while RegenerationWorker
hasn't finished running

* Fix specs

* Replace usage of reject!
2017-06-14 13:37:03 +02:00
Sorin Davidoi
0f52e42c2d fix(status): Content jump due to height changes (#3734) 2017-06-13 20:46:21 +02:00
Eugen Rochko
85af2405cf Exclude packs/custom.js from webpack compilation to prevent breakage (#3719)
due to the change in #3373
2017-06-13 03:55:36 +02:00
Eugen Rochko
47ace633dc Simplify getting started links localization. Link "powered by Mastodon" to joinmastodon.org (#3725) 2017-06-13 03:55:28 +02:00
Eugen Rochko
85d5518b6b Fix #3675 - Adjust quality settings of converted GIFs to reduce filesize (#3723) 2017-06-13 00:51:48 +02:00
Eugen Rochko
5104bd7988 Fix unclickable onboardin modal regression (#3724) 2017-06-13 00:39:31 +02:00
m4sk1n
3e425b51fd i18n: minor fix in Polish translation (#3726)
* i18n: minor fix in Polish translation

* i18n
2017-06-12 23:55:32 +02:00
Eugen Rochko
37dbfa4cd7 Unread indicator was invisible behind column header, adjusted (#3720)
* Unread indicator was invisible behind column header, adjusted

* Unread indicator now a CSS pseudo-element

* Adjust flex
2017-06-12 20:02:17 +02:00
Ratmir Karabut
0d23c81662 Update Russian translation (pin) (#3712)
* Update Russian translation (pin)
2017-06-12 12:53:20 +02:00
Yamagishi Kazutoshi
b436b31d5a Regenerate defaultMessages.json (#3709)
follow up #3564
2017-06-12 12:41:19 +02:00
Yamagishi Kazutoshi
72133fbed6 Re-add clear notifications button (#3708)
* Re-add clear notifications button

* remove connect() in column_settings

* one line

* remove unused props
2017-06-12 12:26:23 +02:00
unarist
abbdacedc5 Fix locale related specs (#3707)
* Use I18n.locale instead of ":en"
* Reset I18n.locale value after locale changing tests
2017-06-12 10:58:03 +02:00
Yamagishi Kazutoshi
ddd3251912 Update dependencies for Node.js (#3705)
* Update @storybook/addon-actions to v3.1.2

* Update @storybook/react to v3.1.2

* Update babel-core to v6.25.0

* Update babel-preset-env to v1.5.2

* Update chai to version v4.0.2

* Update extract-text-webpack-plugin to v2.1.2

* Update file-loader to v0.11.2

* Update intersection-observer to v0.3.0

* Update pg to v6.2.4

* Update sinon to v2.3.4

* Update style-loader to v0.18.2

* Update websocket.js to v0.1.10

* Update react-redux-loading-bar to v2.9.2

* yarn upgrade
2017-06-12 10:54:02 +02:00
Eugen Rochko
605e2a417c Fix regression from #3672 - Do not use pipeline around zscore (#3704) 2017-06-12 03:11:12 +02:00
ThibG
f8fe394e7a Fix an error when TagManager.local_url? is called with a bad URI (#3701)
TagManager.local_url? was sometimes called with an URI with a nil host,
leading to a crash in TagManager.local_url?. This fixes moves the
already-existing uri.host.blank? check in front to avoid this case.
2017-06-11 22:53:12 +02:00
Eugen Rochko
2a545e0fb1 Fix #3582 - Update OStatus2 gem (#3699) 2017-06-11 17:47:29 +02:00
Eugen Rochko
ce812466c7 Fix removal of status sending the original status to mentioned users instead of delete Salmon (#3672)
* Fix removal of status sending the original status to mentioned users instead
of delete Salmon, add test

* Create remove_status_service_spec.rb
2017-06-11 17:13:43 +02:00
Eugen Rochko
47bf7a8047 Fix #3665 - Refactor timelines reducer (#3686)
* Move ancestors/descendants out of timelines reducer

* Refactor timelines reducer

All types of timelines now have a flat structure and use the same
reducer functions and actions

* Reintroduce some missing behaviours

* Fix wrong import in reports

* Fix includes typo

* Fix issue related to "next" pagination in timelines and notifications

* Fix bug with timeline's initial state, expandNotifications
2017-06-11 17:07:35 +02:00
René Klačan
85d405c810 Fix Account model deprecation warnings (#3689)
```
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:60)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:60)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:60)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:61)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:62)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:Account> at /Users/rene/Workspace/personal/ruby/mastodon/app/models/account.rb:63)
```

Here's PR describing changes to Dirty API https://github.com/rails/rails/pull/25337
2017-06-11 17:01:32 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
f596a413ef Localize date in digest and cover NotificationMailer more (#3694)
* Localize date in digest

* Cover NotificationMailer more
2017-06-11 12:04:35 +02:00
Ratmir Karabut
9e53fe5c29 Update Russian translation (#3692) 2017-06-11 10:43:01 +02:00
Yamagishi Kazutoshi
3690f04e4a Remove comments for eslint-disable (#3691) 2017-06-11 10:42:42 +02:00
Yamagishi Kazutoshi
f3e8bc9f8f Refactor UpdateRemoteProfileService (#3690) 2017-06-11 10:41:59 +02:00
René Klačan
dcf0530218 Make sure email is case insensitive on all places (#3688)
When case insensitivity is enabled via devise's `config.case_insensitive_keys` then `.find_for_authentication` method needs to be used instead of `.find_by` because second mentioned returns `nil` when valid email with different cases is passed.

More info https://github.com/plataformatec/devise/wiki/How-To:-Use-case-insensitive-emails
2017-06-11 02:29:08 +02:00
Matt Jankowski
47338bc13d Gem versions, including security-related mail gem update (#3687)
* Update mail to version 2.6.6

* Update aws-sdk to version 2.9.37

* Update capybara to version 2.14.2

* Update oj to version 3.1.0

* Update sidekiq to version 5.0.2

* Update puma to version 3.9.1

* Update sanitize to version 4.5.0

* Update capistrano-rails to version 1.3.0
2017-06-10 20:26:50 +02:00
Jeroen
6fb9726b99 Update NL for 1.4.2 (#3685) 2017-06-10 17:26:01 +02:00
Eugen Rochko
8015fd7600 Improve RTL detection (#3682)
- Use plaintext
- Strip out URLs
- Strip out mentions
- Strip out hashtags
- Strip out whitespace from "overall" count
- Consistent between JS and Ruby
2017-06-10 15:06:50 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
4919b89ab8 Improve default language decision and spec (#3351)
* Improve default language decision

This change allows to takes account of accepted language determined by
the user agent even if the custom default locale of the instance is
configured.

* Cover Localized more

* Fix code style
2017-06-10 09:44:02 +02:00
Matt Jankowski
2925372ff4 Move create/destroy actions for api/v1/statuses to namespace (#3678)
Each of mute, favourite, reblog has been updated to:

- Have a separate controller with just a create and destroy action
- Preserve historical route names to not break the API
- Mild refactoring to break up long methods
2017-06-10 09:39:26 +02:00
m4sk1n
778430b54a i18n: Minor fixes in Polish translation (#3674)
* i18n

* i18n

* i18n

* i18n

* i18n
2017-06-09 23:08:09 +02:00
Matt Jankowski
5282ba862a Move reblogged_by and favourited_by actions out of api/v1/statuses and into unique controllers (#3646)
* Add specs for api statuses routes

* Update favourited_by and reblogged_by api routes

* Move methods into new controllers

* Use load_accounts methods to simplify index actions

* Clean up load_accounts methods

* Clean up link header generation

* Check for link headers in specs

* Remove unused actions from api/v1/statuses controller

* Remove specs for moved actions
2017-06-09 14:12:40 -04:00
Eugen Rochko
0464602978 Fix issue with some Node.js versions not having Array.prototype.includes (#3667)
by using Array.prototype.indexOf instead
2017-06-09 19:46:33 +02:00
Eugen Rochko
9b03cf0ddd Fix #3658 - Update RTL styles (#3669) 2017-06-09 19:46:25 +02:00
Eugen Rochko
cdff1da901 Correct validators so that existing error messages would look correct (#3668) 2017-06-09 19:46:01 +02:00
m4sk1n
1a065fb146 i18n: updated Polish translation (#3670) 2017-06-09 19:15:55 +02:00
Matt Jankowski
022008a2a6 Language detection defaults to nil (#3666)
* Default to nil for statuses.language

* Language detection defaults to nil instead of instance UI default
2017-06-09 18:09:37 +02:00
Clworld
a3715598cc adjust the size of 'column-header__back-button' (retry) (#3662) 2017-06-09 16:22:28 +02:00
Yamagishi Kazutoshi
1be48dd805 Refactor views/admin/reports/show.html.haml (#3656) 2017-06-09 15:08:06 +02:00
Yamagishi Kazutoshi
6384041d17 Add includes to Report#statuses (#3655) 2017-06-09 15:07:02 +02:00
Yamagishi Kazutoshi
140e73bc82 Check ready state of document in public pages (#3652)
* Check ready state of document in public pages

* add check interactive
2017-06-09 15:06:38 +02:00
ふぁぼ原
e3fae6f52c adjust the size of column-header__back-button (#3654) 2017-06-09 15:05:26 +02:00
unarist
65d8c73bae Add Japanese translation for #3640 (#3650) 2017-06-09 03:14:15 +02:00
Eugen Rochko
177dd8bb53 Fix regression from #3592 - validation condition nesting (#3644) 2017-06-08 19:10:48 +02:00
Eugen Rochko
380b20eed6 Bump version to 1.4.2 2017-06-08 15:30:43 +02:00
Eugen Rochko
c207b4bb33 Fix db:seed - only run some validations when the field was changed (#3592)
* Fix db:seed - only run some validations when the field was changed

* Add tests
2017-06-08 09:22:01 -04:00
Eugen Rochko
b87eb8ea14 Fix #3378 - If favourite/reblog already exists, return it instead of failing (#3641) 2017-06-08 15:07:39 +02:00
Eugen Rochko
8902e265b4 Add explit admin actions to (re)subscribe/unsubscribe remote accounts (#3640)
* Add explit admin actions to (re)subscribe/unsubscribe remote accounts
and re-download avatar/header

* Improve how admin NSFW toggle looks
2017-06-08 14:58:22 +02:00
nullkal
b8ea28d6d0 Support multiple trusted proxy ip addresses (#3639)
* Support multiple trusted proxy ip addresses

* correct coding style
2017-06-08 14:33:28 +02:00
ThibG
f741673638 Fixes #3605 by returning account from database in case of race condition (#3606) 2017-06-08 13:40:11 +02:00
Yamagishi Kazutoshi
0a0b9a271a Improve RuboCop rules (compatibility to Code Climate) (#3636)
08f8de84eb/Gemfile.lock (L38)
Code Climate is using RuboCop v0.46.0.

Change several rules to maintain compatibility.
2017-06-08 13:24:28 +02:00
Yamagishi Kazutoshi
7d2b4186c3 Add Japanese translations (#3635) 2017-06-08 13:23:56 +02:00
m4sk1n
90689190a3 i18n: Update Polish translation (#3637) 2017-06-08 13:23:40 +02:00
Sorin Davidoi
8acadeea76 fix: Warn if JavaScript is disabled (#3634) 2017-06-08 00:52:56 +02:00
Eugen Rochko
75c6513c67 Add rake task to prepare database for foreign keys introduced by #3562 (#3614)
* Add rake task to prepare database for foreign keys introduced by #3562

* Fix typo

* Do not delete OAuth values where NULL values may be permitted

* Fix typo
2017-06-07 20:16:53 +02:00
Matt Jankowski
73540ffe6b Clean up for api/base controller (#3629)
* Move ApiController to Api/BaseController

* API controllers inherit from Api::BaseController

* Add coverage for various error cases in api/base controller
2017-06-07 20:09:25 +02:00
jeroenpraat
92bb166246 Rest of 2FA translated + small fix (#3630) 2017-06-07 20:08:34 +02:00
Sorin Davidoi
8cf8ce4ac0 fix(components/autosuggest_textarea): Race condition regarding onBlur (#3631) 2017-06-07 20:05:53 +02:00
unarist
0f1b1d78b1 Use "match_array" only for order independent assertions (#3626) 2017-06-07 12:59:28 -04:00
Eugen Rochko
d3bbef27e7 Fix broken screenshot 2017-06-07 17:53:47 +02:00
Matt Jankowski
f0634ba876 Coverage improvement and concern extraction for rate limit headers in API controller (#3625)
* Coverage for rate limit headers

* Move rate limit headers methods to concern

* Move throttle check to condition on before_action

* Move match_data variable into method

* Move utc timestamp to separate method

* Move header setting into smaller methods

* specs cleanup
2017-06-07 17:23:26 +02:00
Yamagishi Kazutoshi
1d68fe1a60 Fix Code Climate failed (regression from #3622) (#3624) 2017-06-07 15:57:59 +02:00
unarist
6bd6dcf6df Allow "class" attribute on the "a" tag in sanitization (#3623)
This preserves `<a ... class="u-url mention">` from other Mastodon instances.
2017-06-07 15:57:30 +02:00
ThibG
28d2920472 Fixes #3388 by moving re-entrant shared_status_from_xml before transaction block (#3622)
Steps to reproduce the original issue:
1. Have two remote accounts, A that you don't follow, and B that you follow.
2. Have A post a toot and reply to it.
3. Boost A's reply from remote account B.

This used to cause the local instance to get A's reply but fail to link it to
the original post.
2017-06-07 12:28:16 +02:00
Yamagishi Kazutoshi
34bfea8bbf Small changes to japanese translation (#3620) 2017-06-07 10:15:35 +02:00
STJrInuyasha
2d91944285 Don't show business e-mail if it's blank (#3619) 2017-06-07 09:57:47 +02:00
Daigo 3 Dango
0026ba2751 Update nokogumbo to 1.4.13 (#3617)
$ bundle update --source nokogumbo# Please enter the commit message for your changes. Lines starting

nokogumbo 1.4.11 and 1.4.12 don't work on Heroku.
2017-06-06 20:43:02 +02:00
Eugen Rochko
b623dd12c1 Use preview image in <ImageLoader /> to provide immediate visual feedback (#3595)
before the full-size image is loaded
2017-06-06 19:30:17 +02:00
Eugen Rochko
722d152082 Fix #3063 - Add dynamic app manifest (#3563)
* Fix #3063 - Add dynamic app manifest

* Added short_name

* Add background_color
2017-06-06 19:29:42 +02:00
ふぁぼ原
7623766241 Add regex filters on the community timeline and the public timeline. (#3564)
* Add regex filter on the community timeline and the public timeline

* correcting

* Adjust the height of header buttons

* Remove trailing spaces

* Remove trailing spaces

* Solve some code duplication

* reset the state of the locale files in app/javascript/mastodon/locales

* adjust to upstream

* adjust to upstream

* change keys of locale settings
2017-06-06 16:56:10 +02:00
unarist
e34c5a3503 Fix 500 errors on searching invalid URLs (#3613) 2017-06-06 16:44:48 +02:00
unarist
004672aa6c Fix tag search order and not to use tsvector (#3611)
* Sort results by the name
* Switch search method to simple `LIKE` matching instead of tsvector/tsquery

Previously we used scores from ts_rank_cd() to sort results, but it didn't work
because the function returns same score for all results. It's not for calculate
similarity of single words. Sometimes this bug even push out exact matching tag
from results.

Additionally, PostgreSQL supports prefix searching with standard btree index.
Using it offers simpler code, but also less index size and some speed.
2017-06-06 16:07:06 +02:00
Yamagishi Kazutoshi
ad4a28f4f6 Refactor translationRunner.js (#3604)
- Use yargs instead of minimist
- Simplify validators
- Fix typo (RFC5626 -> RFC5646)
2017-06-06 13:31:57 +02:00
Yamagishi Kazutoshi
d8ae3efec3 Improve ESLint rules for JSX (#3608)
* Add react/no-string-refs ESLint rule

* Add react/jsx-boolean-value ESLint rule

* Add react/jsx-closing-bracket-location ESLint rule

* Add react/jsx-indent ESLint rule

* Add react/jsx-curly-spacing ESLint rule

* Add react/jsx-equals-spacing ESLint rule

* Add react/jsx-first-prop-new-line ESLint rule

* Add react/jsx-no-duplicate-props ESLint rule

* Add react/jsx-tag-spacing ESLint rule
2017-06-06 13:20:07 +02:00
Yamagishi Kazutoshi
cd81a1c52a Add missing key attribute to .search-results__hashtag (#3607) 2017-06-06 13:19:29 +02:00
May Kittens Devour Your Soul
dcf73ddeff Update hr.json (#3609) 2017-06-06 13:19:05 +02:00
Yamagishi Kazutoshi
d81b706f12 Fix typo (storyboard -> storybook) (#3603) 2017-06-06 04:07:34 +02:00
m4sk1n
30fa5fe1a4 i18n: updated Polish translation (#3597)
* i18n: updated Polish translation

* i18n: updated Polish translation

btw it would be nice to have master-based Mastodon instance (even isolated from others) to test translation.
2017-06-06 03:59:13 +02:00
Yamagishi Kazutoshi
7a7bfa5170 Add quotes ESLint rules (#3602)
* Add quotes ESLint rule

* Add jsx-quotes ESlint rule

* Sort ESLint rules
2017-06-06 03:56:36 +02:00
Yamagishi Kazutoshi
e969c78645 Disable ESLint rule jsx/anchor-has-content (#3601) 2017-06-06 03:55:56 +02:00
ThibG
7adac1bc51 Try fixing ThreadResolveWorker calls (#3599)
* Try fixing ThreadResolveWorker calls

From my understanding of ActiveRecord, a transaction is commited as soon as
the exit of the outmost ActiveRecord.transaction block. However, inner
transaction blocks will exit without the transaction being commited.

In this case, ThreadResolveWorker were fired *within* a transaction block,
so moving the call out of it should do the trick. However, this is somewhat
fragile, as this whole codepath could be called within yet another transaction.

* Set status thread within the transaction block if it is immediately available from database
2017-06-06 00:09:14 +02:00
Naoki Kosaka
e859d6f259 Fix LoadMore in Notifications. (#3590) 2017-06-05 19:18:26 +02:00
Naoki Kosaka
a0880edc6e Fix (PR #3585) Add hasMore to propTypes and cover handleScroll. (#3589) 2017-06-05 18:18:56 +02:00
unarist
61fcdbbf7e Add back button to hashtag timeline again (#3587) 2017-06-05 17:10:40 +02:00
Shunsuke Michii
43af695ba1 Add th.json. (#3588) 2017-06-05 17:02:41 +02:00
IkUrA
facd90e7a6 Add streaming_api_base_url to /api/v1/instance (#3556)
* Add streaming_api_base_url to /api/v1/instance

* Adjust spaces

* nested view

* re-adjust spaces
2017-06-05 16:37:44 +02:00
Matt Jankowski
6201f96b8a Introduce StatusThreadingConcern (#3490)
* Add a StatusFilter class to identify visibility of statuses by accounts

* Extract StatusThreadingConcern from Status

* Clarify purpose of checking for nil account
2017-06-05 16:07:44 +02:00
unarist
c26cea262b Prevent pinned columns from scroll to top on URL changing (#3586) 2017-06-05 15:20:46 +02:00
Naoki Kosaka
1f1d6bf2a0 Fix LoadMore in following and followers. (#3585) 2017-06-05 14:13:20 +02:00
Eugen Rochko
4c06d1cb24 Fix #3550 - Add all missing foreign keys (#3562)
* Fix #3550 - Add all missing foreign keys

* Add missing foreign keys
2017-06-05 13:24:00 +02:00
Daigo 3 Dango
2985d08951 Redirect to streaming_api_base_url (#3579)
* Redirect to streaming_api_base_url

When Rails receives a request to streaming API, it most likely
means that there is another host which is configured to respond
to it. This is to redirect clients to that host if
`STREAMING_API_BASE_URL` is set as another host.

* Use the new Ruby 1.9 hash syntax
2017-06-05 12:09:29 +02:00
Yamagishi Kazutoshi
66ca7157db Add support key shortcut to Onboarding Modal (#3517)
* Add support key shortcut to Onboarding Modal

* this.state.pages -> this.pages
2017-06-05 10:09:14 +02:00
Yamagishi Kazutoshi
4addf051d4 Fix broken embed page (#3577) 2017-06-05 10:08:31 +02:00
Yamagishi Kazutoshi
ab914ce6d5 Add OnboadingModal story (#3578) 2017-06-05 10:08:07 +02:00
Yamagishi Kazutoshi
6a4b224397 Fix randomly fail (regression from #3560) (#3580) 2017-06-05 10:03:36 +02:00
Yamagishi Kazutoshi
6adbd114c1 Skip image length check (regression from #3528) (#3581) 2017-06-05 10:03:01 +02:00
René Klačan
037f96c5ae Don't follow account if it's already followed (#3575)
Closes https://github.com/tootsuite/mastodon/issues/3102
2017-06-05 03:24:18 +02:00
René Klačan
f54dca06a9 Add migration versions (#3574)
Since Rails 5.1 missing migration version results in following error:

```
StandardError: Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:
```

This PR fixes all migration files.
2017-06-05 02:43:02 +02:00
Quent-in
370fa70924 l10n update for occitan language (#3557)
* i18n Update : Add preference setting for delete toot modal

Adding a line for "Add preference setting for delete toot modal"

* i18n update for pin/unpin

Update to add two more translations

* i18n update to have the dates in plain occitan

* Removed the blank line

* %{selft} back in the translation
2017-06-05 01:31:10 +02:00
Matt Jankowski
5be1214c26 Gem version bumps (#3524)
* Update annotate to version 2.7.2

* Update puma to version 3.9.0

* Update aws-sdk to version 2.9.28

* Update bootsnap to version 1.0.0

* Update nio4r to version 2.1.0

* Update nokogumbo to version 1.4.12

* Update oj to version 3.0.11

* Update pkg-config to version 1.2.3

* Update rubocop to version 0.49.1

* Update sidekiq-scheduler to version 2.1.5
2017-06-05 01:10:13 +02:00
Eugen Rochko
f7a30e2fae Added support for configurable reserved usernames (fix of #1382) (#3566)
* Added support for configurable reserved usernames

* Added reserved usernames from mastodon issue 1355

* Fix reserved usernames
2017-06-05 01:03:45 +02:00
Yamagishi Kazutoshi
3f815b2052 Add date to ignore_missing and ignore_unused in config/i18n-tasks.yml (#3572) 2017-06-05 01:02:19 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
defe4f9bc3 Cover WebfingerResource more (#3560) 2017-06-04 17:13:37 +02:00
Yamagishi Kazutoshi
943775fd90 Update dependencies of Node.js (#3561)
* Update axios to version 0.16.2

* Update css-loader to version 0.28.4

* Update postcss-smart-import to version 0.7.4

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

* Update stringz to version 0.2.1

* Update style-loader to version 0.18.1

* Update websocket.js to version 0.1.9

* yarn upgrade
2017-06-04 17:13:21 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
42844df966 Spec ScopedSettings (#3559) 2017-06-04 17:07:39 +02:00
Yamagishi Kazutoshi
b0fe58dc69 Upgrade storybook to v3 (#3558) 2017-06-04 16:26:07 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
e07b57852e Remove some code in TagManager and spec (#3547)
* Do not fall back to StreamEntry if object_type is unavailable in TagManager

Since 6d6a429af8, when Status, the only model
with stream_entry, and StreamEntry got its own logic in uri_for and
url_for, the purpose of the fallbacks to activity_type of StreamEntry
became unclear.

This commit removes the fallbacks. When adding another model with
stream_entry in future, consider to update uri_for and url_for.

* Cover TagManager more
2017-06-04 16:14:25 +02:00
m4sk1n
7c7c18fdea i18n: Updated Polish translation (#3555) 2017-06-04 15:18:42 +02:00
lindwurm
a84664026e l10n: ja: Change about/more link text (#3539)
ref: #3538 ("Extended information" -> "About this instance") and #3519

Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-06-04 15:00:15 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
02a0fd5b64 Spec InlineRablScope (#3542) 2017-06-04 15:00:08 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
6505a42be0 Spec HashObject (#3544) 2017-06-04 14:59:52 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
e674608d10 A minor change for ProviderDiscovery and spec (#3543)
* Do not default the format in ProviderDiscovery

The format should be determined when discovering, as it is in the current
implementation, and it is a flaw if it is not determined.

* Spec ProviderDiscovery
2017-06-04 14:59:40 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
c7af8cbc90 Remove some arguments of Formatter.instance.format and spec (#3541)
* Remove some arguments of Formatter.instance.format

* Improve spec for Formatter
2017-06-04 14:58:57 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
9475fbae78 Spec Extractor (#3540) 2017-06-04 14:57:02 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
00e61d6807 Spec action verification of StreamEntryFinder (#3549) 2017-06-04 14:56:31 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
19084d3c6c Remove Targetable (#3548) 2017-06-04 14:56:10 +02:00
Naoki Kosaka
e014bf8ed0 Fix limit_param in favourites_controller.rb (#3553) 2017-06-04 14:52:26 +02:00
Yamagishi Kazutoshi
f6e2309e70 Fix column buttons overflow in Japanese (#3552) 2017-06-04 14:52:02 +02:00
Yamagishi Kazutoshi
9d2154c4ab Add Japanese translations (pin) (#3551)
* Add Japanese translations (pin)

* "ピン留め" is more general
2017-06-04 14:51:35 +02:00
unarist
1dfd27a028 Fix all icons in the column header being hilighted (#3554) 2017-06-04 14:51:06 +02:00
Mingye Wang
b97ebaf620 en.json: change about/more link text to "About this instance" (#3538)
This change follows #3519, which does the same for the Chinese variants. Or should it be simply "About"?
2017-06-04 04:25:18 +02:00
Eugen Rochko
8ee2eb5d2e Allow mounting arbitrary columns (#3207)
* Allow mounting arbitrary columns

* Refactor column headers, allow pinning/unpinning and moving columns around

* Collapse animation

* Re-introduce scroll to top

* Save column settings properly, do not display pin options in
single-column view, do not display collapse icon if there is
nothing to collapse

* Fix one instance of public timeline being closed closing the stream
Fix back buttons inconsistently sending you back to / even if history exists

* Getting started displays links to columns that are not mounted
2017-06-04 01:39:38 +02:00
masarakki
20b647020b refocus-after-toot (#3537) 2017-06-04 01:22:37 +02:00
takayamaki
3eedad2737 change sidekiq queueing to bulk push (#3536) 2017-06-04 00:11:15 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
ce7c0def88 Remove redundant code in AtomSerializer and spec (#3528)
* Remove redundant status.reply? in AtomSerializer#object

* Cover AtomSerializer more
2017-06-03 20:51:08 +02:00
Clworld
dab8fc4584 Execute PushUpdateWorker only for accounts who uses StreamingAPI just now. (#3278)
* Add redis key "subscribed:timeline:#{account.id}" to indicate active streaming API listeners exists.

* Add endpoint for notification only stream.

* Run PushUpdateWorker only for users uses Streaming API now.

* Move close hander streamTo(Http/Ws) -> stream(Http/Ws)End (Deal with #3370)

* Add stream type for stream start log message.
2017-06-03 20:50:53 +02:00
Yamagishi Kazutoshi
8a597f0138 Cache assets in travis (#3532) 2017-06-03 15:13:12 +02:00
Nolan Lawson
3363f2f4d6 Fix isIntersecting in Chrome (#3525) 2017-06-03 14:43:10 +02:00
Mingye Wang
c7f2d6af55 zh-cn&tw.json: "extended info" -> "about this site" (#3519)
* zh-cn.json: "additional info" -> "more info"

This commit changes the "extended info" (about/more) text to something that translates to "more info", as "additional info" in zh can sound like appending things to the (ugh) navbar, I guess.

Or should I just change it to "about this site" (关于本站)?

* zh-cn.json: navbar.info -> "about this site"

* zh-臺灣.json: navbar.info -> "about this site"
2017-06-03 14:42:47 +02:00
Yamagishi Kazutoshi
e878ddb7c0 Fix spec for #2388 (#3526) 2017-06-03 14:40:55 +02:00
Yamagishi Kazutoshi
336f0b0823 Remove WarmCacheService (#3527)
WarmCacheService is no longer used (removed with 5442083b3c ).
2017-06-03 14:40:47 +02:00
Yamagishi Kazutoshi
3ea3f24a02 Change toggle state when click label (#3530) 2017-06-03 14:40:24 +02:00
ThibG
d567a382e3 Ensure well_known controllers use WEB_DOMAIN by including RoutingHelper (#2388)
This fixes #2375.
2017-06-02 22:21:36 +02:00
Clworld
18fe77084f bump rails version in Gemfile: '~> 5.0.0' -> '~> 5.1.0' (#3523) 2017-06-02 20:11:13 +02:00
Naoki Kosaka
dc253ea234 Ignore packs folder when COPY . /mastodon (#3522) 2017-06-02 18:09:28 +02:00
Sorin Davidoi
9304114b57 Ignore .idea folder (#3516)
Used by Intellij IDEA / WebStorm / RubyMine.
2017-06-02 16:19:11 +02:00
Matt Jankowski
1fd5251376 Add index to media_attachments.account_id (#3510) 2017-06-02 16:18:54 +02:00
Nolan Lawson
edddc7c791 Enable stats.json analysis with Webpack Visualizer (#3509) 2017-06-02 03:49:56 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
10768aa204 Spec response for forgery (#3248)
Remove protect_from_forgery in ApiController, which is disabled by the
following skip_before_action, as well.
2017-06-01 20:56:55 +02:00
Akihiko Odaki (@fn_aki@pawoo.net)
e98559c3ff Resolve custom application stylesheet with Webpack (#3373)
This implementation is a bit smaller and still has the following benefits:

* No need of app/javascript/packs/custom.js
For custom stylesheet, it typically has only
"require('../styles/custom.scss')" and is redundant.

* No need to extract vendor stylesheet to another asset
Extracting vendor stylesheet could be forgotten by developers who do not
use custom stylesheet.
2017-06-01 20:56:32 +02:00
Matt Jankowski
2212dc4aaa Update Rails to version 5.1.1 (#3121)
* Update rails to version 5.1.1

* Run `rails app:update`

* Remove the override of polymorphic activity relationship

* Silence warning about otp_secret attribute being unknown to rails

* We will only introduce form_with where we want to use remote data
2017-06-01 20:53:37 +02:00
Daniel Hunsaker
e1fdac3e9a [nanobox] Add python for node-gyp (#3491)
Needed since merge of #3067.
2017-06-01 17:33:57 +02:00
Yamagishi Kazutoshi
1162f61ca3 Remove unused loaders of webpack (#3495) 2017-06-01 17:27:35 +02:00
Yamagishi Kazutoshi
39ea5c0e2e Improve tests for JavaScript (#3496)
- Upgrade dependencies
    - chai (3.5.0 -> 4.0.1)
    - chai-enzyme (0.6.1 -> 0.7.1)
    - sinon (2.2.0 -> 2.3.2)
- Change extensions from .jsx to .js
- Don't assign `React` to `global`
- Check code format using ESLint
2017-06-01 17:27:15 +02:00
Yamagishi Kazutoshi
509b0cfafc Add scenarios for log in (#3497) 2017-06-01 17:25:59 +02:00
Yamagishi Kazutoshi
fda5c699c2 Add ESLint rule (object-curly-spacing) (#3498) 2017-06-01 17:25:10 +02:00
Matt Jankowski
cb7ee4698f Rubocop changes (#3501)
* Update renamed rubocop configuration options

* Remove duplicate rubocop configuration, sort options
2017-06-01 17:24:30 +02:00
Matt Jankowski
d010e270e6 Remove usernames and hashtags from language detection (#3503)
* Add failing specs for hashtag and username extraction in language detector

* Remove usernames and hashtags from text before language detection

* Handle multiple instances of special case, and reduce whitespace
2017-06-01 09:29:14 -04:00
Matt Jankowski
d1e08bd38c Handle nil and blank cases in Account finders (#3500) 2017-06-01 08:20:36 -04:00
unarist
dbccdcc1b1 Focus the submit button on the boost modal (#3494)
Focus the submit button when boost modal is opened.
2017-06-01 04:20:10 +02:00
Matt Jankowski
5c63523972 Spec coverage and refactor for the api/v1/accounts controllers (#3451) 2017-05-31 21:36:24 +02:00
Matt Jankowski
de4681b2be Move admin/pubsubhubbub controller to admin/subscriptions (#3442) 2017-05-31 20:39:35 +02:00
Matt Jankowski
a132332b86 Add Instance class to list admin records (#3443) 2017-05-31 20:38:44 +02:00
Matt Jankowski
b25e42a77f Misc tidying and clean ups (#3445)
* Remove trailing whitespace in i18n mailers

* Use query methods instead of #present? on AR attributes

* Delegate Status#account_domain method

* Delegate Mention #account_username and #account_acct methods
2017-05-31 20:38:17 +02:00
Matt Jankowski
5236a62861 Improve spec coverage and clean up api/v1/blocks controller (#3464) 2017-05-31 20:34:51 +02:00
Matt Jankowski
0f155829b7 Improve spec coverage and clean up api/v1/follow_requests controller (#3465) 2017-05-31 20:32:11 +02:00
Matt Jankowski
84dda45df9 Improve spec coverage and clean up api/v1/domain_blocks controller (#3466) 2017-05-31 20:31:14 +02:00
Matt Jankowski
9c7505489f Refactor api/v1/notifications controller (#3470) 2017-05-31 20:30:55 +02:00
Matt Jankowski
75cad1d9d6 Improve spec coverage and clean up api/v1/favourites controller (#3472) 2017-05-31 20:30:39 +02:00
Matt Jankowski
2cc3111a77 Expand spec coverage and refactor the Account.find_ methods (#3485)
* Move specs for account finder methods to concern spec

* Move account finder methods to concern

* Improve spec wording

* Use more explicit comparison to ensure correct return value

* Add coverage for .find_local! and .find_remote!

* Add some methods to the finder

* Use arel on matching_username method

* Avoid ternary in matching domain method

* Simplify finder methods

* Use an AccountFinder class to simplify lookup
2017-05-31 20:28:45 +02:00
Matt Jankowski
bf811e4d4a Improve spec coverage and clean up api/v1/mutes controller (#3481) 2017-05-31 20:27:34 +02:00
Matt Jankowski
d6774d2ca3 Refactor and spec coverage for api/v1/timelines actions (#3482) 2017-05-31 20:27:17 +02:00
caasi Huang
bd669e3907 Update zh-TW.json (#3480)
* Update zh-TW.json

* Update `search_results.total`
2017-05-31 18:46:57 +02:00
Yamagishi Kazutoshi
1a4860a57a Hold value of I18n.locale with ApplicationHelperSpec (#3474) 2017-05-31 17:09:28 +02:00
Yamagishi Kazutoshi
41fa53253c Keep ENV['LOCAL_HTTPS'] with ApplicationControllerSpec (fix random fail) (#3479)
* Keep ENV['LOCAL_HTTPS'] with ApplicationControllerSpec (fix random fail)

* use climate_control
2017-05-31 17:09:17 +02:00
Nolan Lawson
c00ead8a72 Remove react-imageloader (#3423)
* Remove react-imageloader

* add eslint-disable-line

* improve image loading experience

* remove unneeded import

* use PureComponent

* Use componentWillMount instead of constructor
2017-05-31 17:07:25 +02:00
unarist
e49dc6a06e Fix load more on account timelines (regression from #3311) (#3475)
This prevents `next` state from being overridden on the loading *new* statuses.
2017-05-31 15:30:26 +02:00
Nolan Lawson
0e12a8dab9 Improve scheduling of requestIdleCallback tasks (#3477) 2017-05-31 15:11:33 +02:00
alpaca-tc
3652a39de0 Account.find_remote should be raise ActiveRecord::RecordNotFound (#3476) 2017-05-31 15:10:26 +02:00
Matt Jankowski
79335e46fd Refactor api/v1/apps controller (#3471) 2017-05-30 21:16:28 -04:00
Matt Jankowski
7c6e02aaf3 Refactor api/v1/reports controller (#3469) 2017-05-30 21:13:31 -04:00
Matt Jankowski
7f55430652 Refactor api/v1/search controller (#3468) 2017-05-30 21:11:54 -04:00
Matt Jankowski
8235623362 Improve spec coverage and clean up api/v1/media controller (#3467) 2017-05-30 21:11:29 -04:00
Matt Jankowski
83435c49ea Clean up api/subscriptions controller (#3448) 2017-05-31 02:15:09 +02:00
spla
93de41b39b Catalan language updates (#3454)
* Add Catalan language

* Add Catalan language

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update settings_helper.rb

* Update mastodon.js

* Update index.js

* Update application.rb

* Update ca.yml

* removed extra spaces at line 225

* Catalan translation update

added activerecord.ca.yml

* Update activerecord.ca.yml

Done

* Updated activerecord.ca.yml

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated
2017-05-31 02:14:52 +02:00
Yamagishi Kazutoshi
b1d4b74a44 Change PureComponent to ImmutablePureComponent (#3460)
* Change PureComponent to ImmutablePureComponent

Change PureComponent to ImmutablePureComponent in AttachmentList.

* rm React.
2017-05-31 02:14:26 +02:00
Yamagishi Kazutoshi
bfdf47bc98 Add zh-TW.json (#3459) 2017-05-30 23:09:02 +02:00
Jack Jennings
33f669a5f8 Add status destroy authorization to policy (#3453)
* Add status destroy authorization to policy

* Create explicit unreblog status authorization
2017-05-30 22:56:31 +02:00
Matt Jankowski
3576fa0d59 Improve api oembed controller (#3450)
* Add StreamEntryFinder class to parse URLs

* Use StreamEntryFinder and clean up api/oembed controller
2017-05-30 16:30:06 -04:00
Matt Jankowski
1dcfb90202 Clean up api/salmon controller (#3449) 2017-05-30 16:28:58 -04:00
Jack Jennings
22cf18e16f Fix incorrect visibility setter in StatusPolicySpec (#3456) 2017-05-30 22:14:32 +02:00
Matt Jankowski
0ebe7d6d23 Remove exports/base controller in favor of shared concern (#3444) 2017-05-30 19:06:01 +02:00
Chris
23081bb299 added 'https://' to CDN_HOST variable example (#3446) 2017-05-30 18:39:28 +02:00
Yamagishi Kazutoshi
4c7fe48c40 Add "private: true" to package.json (#3441) 2017-05-30 16:11:45 +02:00
Naouak
499cc7b803 Fix webpack building on Windows (#3426)
* Path should not be constructed manually. Use path.join to ensure compatibility.

* Path should not be constructed manually. Use path.join to ensure compatibility.

* Fix regexp.

* Fix my own stupidity.
I forgot to check outside my test script the regexp...
2017-05-30 15:30:59 +02:00
Akihiko Odaki
7db98aa70e Refactor User and spec (#3431)
* Protect send_devise_notification of User

* Improve spec for User
2017-05-30 15:28:56 +02:00
Jack Jennings
e031fd60ad Move status reblog authorization into policy (#3425) 2017-05-30 15:16:14 +02:00
Yamagishi Kazutoshi
bc4fad9e22 Change libraries path to relative (#3433) 2017-05-30 15:13:04 +02:00
Yamagishi Kazutoshi
5ac4d677e9 Remove unused React Storybook addon (#3434) 2017-05-30 15:12:12 +02:00
Nolan Lawson
b42bdd80e8 Extract polyfill loading into single module (#3421) 2017-05-30 15:11:15 +02:00
Akihiko Odaki
76fa9d2488 Use around hook to restore context in InstancePresenter spec (#3430) 2017-05-30 09:03:35 -04:00
Akihiko Odaki
dfc43a6d3d Use around hook to restore context in InstanceHelper spec (#3429) 2017-05-30 06:32:33 -04:00
Akihiko Odaki
67bc58dd60 Use around hook to restore context in Admin::SettingsController spec (#3428) 2017-05-30 06:32:09 -04:00
Akihiko Odaki
2d39560dc1 Use around hook to restore context in ApplicationHelper spec (#3432) 2017-05-30 06:29:48 -04:00
Yamagishi Kazutoshi
c49ff7395e Run bundle command (follow up #3403) (#3427) 2017-05-30 03:04:44 +02:00
Yamagishi Kazutoshi
e0ada97770 Update dependencies of Node.js (#3067)
* Update autoprefixer to version 7.1.0

* Update axios to version 0.16.1

* Update babel-cli to version 6.24.1

* Update babel-eslint to version 7.2.3

* Update babel-preset-react to version 6.24.1

* Update coffee-script to version 1.12.6

* Update css-loader to version 0.28.1

* Update express to version 4.15.2

* Update js-yaml to version 3.8.4

* Update mocha to version 3.4.1

* Update jsdom to version 10.1.0

* Update npmlog to version 4.1.0

* Update postcss-smart-import to version 0.7.0

* Update postcss-loader to version 2.0.5

* Update pg to version 6.1.5

* Update prop-types to version 15.5.10

* Update rails-ujs to version 5.1.1

* Update redis to version 2.7.1

* Update sass-loader to version 6.0.5

* Update stringz to version 0.2.0

* Update webpack to version 2.5.1

* Update style-loader to version 0.17.0

* Update sinon to version 2.2.0

* Update react-motion to version 0.5.0

* Update react-notification to version 6.7.0

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

* Update react-intl to version 2.3.0

* Update redux-immutable to version 4.0.0

* Update reselect to version 3.0.1

* Update react-toggle to version 3.0.1

* Update react-simple-dropdown to version 3.0.0

* Update react-router to version 3.0.5

* Update react-router-scroll to version 0.4.2

* Update react-toggle to version 4.0.1

* yarn upgrade
2017-05-29 18:23:28 +02:00
Jack Jennings
3a2003ba86 Extract authorization policy for viewing statuses (#3150) 2017-05-29 18:22:22 +02:00
unarist
9a81be0d37 [RFC] Return 401 for an authentication error on WebSockets (#3411)
* Return 401 for an authentication error on WebSocket

* Use upgradeReq instead of a custom object
2017-05-29 18:20:53 +02:00
Matt Jankowski
5e2c5e95b6 Version bumps for non-rails gems (#3398)
* Update xpath to version 2.1.0

* Update aws-sdk to version 2.9.25

* Update oj to version 3.0.10

* Update brakeman to version 3.6.2

* Update rufus-scheduler to version 3.4.2

* Update doorkeeper to version 4.2.6

* Update rubocop to version 0.49.0
2017-05-29 18:18:06 +02:00
Nolan Lawson
34a93ccf57 Add IntersectionObserverWrapper to cut down on re-renders (#3406) 2017-05-29 18:17:51 +02:00
Akihiko Odaki
922fb74197 Remove methods from ObfuscateFilename and spec (#3347)
* Remove methods from ObfuscateFilename

* Spec ObfuscateFilename
2017-05-29 18:14:16 +02:00
Akihiko Odaki
7bf2d6cb06 Spec Auth::ConfirmationsController (#3348) 2017-05-29 18:13:11 +02:00
Akihiko Odaki
11e5c965c3 Spec AccountControllerConcern (#3349) 2017-05-29 18:12:54 +02:00
Akihiko Odaki
34157d118c Cover Admin::ReportsController more (#3346) 2017-05-29 18:12:34 +02:00
Akihiko Odaki
7b92950f1c Cover InstancesController more (#3342) 2017-05-29 18:12:17 +02:00
Akihiko Odaki
97d7028c31 Cover Admin::SuspensionsController more (#3350) 2017-05-29 18:11:16 +02:00
Akihiko Odaki
a7f2961621 Spec Auth::PasswordsController (#3352) 2017-05-29 18:10:50 +02:00
Akihiko Odaki
00dda99789 Spec Admin::ResetsController calls send_reset_password_instructions (#3354) 2017-05-29 18:09:17 +02:00
Akihiko Odaki
2e27ce3b61 Spec #destroy feature of Admin::TwoFactorAuthenticationsController (#3355) 2017-05-29 18:08:58 +02:00
Akihiko Odaki
2c10c5a069 Coever Admin::SilencesController more (#3356) 2017-05-29 18:08:42 +02:00
Akihiko Odaki
bd4dd4c4a0 Cover Settings::ExportsController more (#3358) 2017-05-29 18:08:26 +02:00
Akihiko Odaki
7d33b60f3f Cover Oauth::AuthorizedApplicationsController (#3359) 2017-05-29 18:08:05 +02:00
Akihiko Odaki
aecce5694b Cover Oauth::AuthorizationsController more (#3361) 2017-05-29 18:07:39 +02:00
Akihiko Odaki
0e4ca51951 Cover Settings::TwoFactorAuthenticationsController more (#3376) 2017-05-29 18:07:07 +02:00
Akihiko Odaki
dde043f6cd Cover WellKnown::HostMetaController more (#3382) 2017-05-29 18:05:30 +02:00
Akihiko Odaki
c778a60e4f Cover Settings::TwoFactorAuthentication::RecoveryCodesController more (#3383) 2017-05-29 18:05:20 +02:00
Akihiko Odaki
c347327d54 Spec Settings::Exports (#3384)
* Spec output of Settings::Exports::BlockedAccountsController

* Spec output of Settings::Exports::FollowingAccountsController

* Spec output of Settings::Exports::MutedAccountsController

* Spec Settings::Exports::BaseController

This commit removes duplicate specs in classes inheriting
Settings::Exports::BaseController as well.
2017-05-29 18:05:01 +02:00
Akihiko Odaki
fd328cf6e8 Cover WellKnown::WebfingerController more (#3385) 2017-05-29 18:04:37 +02:00
Akihiko Odaki
7b473d7514 Cover Settings::TwoFactorAuthentication::ConfirmationsController more (#3386) 2017-05-29 18:04:00 +02:00
Akihiko Odaki
dff576b75d Cover ApplicationHelper more (#3412) 2017-05-29 18:03:37 +02:00
Akihiko Odaki
52ae83d008 Spec Activitystreams2BuilderHelper (#3413) 2017-05-29 18:03:15 +02:00
Akihiko Odaki
5aacd9d4c7 Spec FlashesHelper (#3414) 2017-05-29 18:03:02 +02:00
Akihiko Odaki
d24d3fa283 Spec HomeHelper (#3415) 2017-05-29 18:02:44 +02:00
Akihiko Odaki
c8a226f61c Spec HttpHelper (#3416) 2017-05-29 18:02:32 +02:00
Akihiko Odaki
7a281c477a Cover Admin::PubSubHubHubController more (#3343) 2017-05-29 18:01:37 +02:00
unarist
91c789ec63 Don't stream toots from users who have blocked the recipient user (#3300)
* Don't stream toots from users who have blocked the recipient user

This filter was already applied on the `/api/v1/timelines/public` API, but
not yet for the Streaming API.

* Boosted status' account_id doesn't need for filtering in streaming/index.js

This filtering is only for public/hashtag timelines, but boosts already filtered on FanOutOnWriteService because those timelines don't show boosts.
2017-05-29 18:01:08 +02:00
Daniel Hunsaker
9ead3d1cdb [nanobox] Adjustments for Nanobox development (#3295)
Because Nanobox doesn't run data components in the same container as the code, there are a few tweaks that need to be made in the configuration to get WebPack to work properly in development mode.

The same differences lead to needing to use `DATABASE_URL` by default in the `.env` file for Rails to work correctly.

Limitations of our `.env` loader for Node.js mean the `.env` file needs to be compiled everywhere in order to work, so we compile it in development, now, too. Also, all the `.env.production` tweaks have been consolidated into a single command.

Finally, since Nanobox actually creates the database when it sets up the database server, using the existence of the database alone to determine whether to migrate or setup is insufficient. So we add a condition to `rake db:migrate:setup` to check whether any migrations have run - if the database doesn't exist yet, `db:setup` will be called; if it does, but no migrations have been run, `db:migrate` and `db:seed` are called instead (the same basic idea as what `db:setup` does, but it skips `db:create`, which will only cause problems with an existing DB); otherwise, only `db:migrate` is called.

None of these changes should affect development, and all are designed not to interfere with existing behaviors in other environments.
2017-05-29 17:59:18 +02:00
Atsushi Yamamoto
402c19a924 Add preference setting for delete toot modal (#3368)
* Set delete_modal preference to true by default
* Does not show confirmation modal if delete_modal is false
* Add ja translation for preference setting page
2017-05-29 17:56:13 +02:00
Nolan Lawson
b5e8994844 Prevent wasted render in load_more.js (#3402) 2017-05-29 17:52:45 +02:00
masarakki
4bd327a0c5 fix-rails-minor-version (#3403) 2017-05-29 17:52:26 +02:00
lindwurm
184325077e l10n: Fix Japanese translation (#3410)
* "メデイア" -> "メディア"

Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-05-29 17:52:02 +02:00
Nolan Lawson
8236f942ff Use travis_retry to reduce spurious test failures (#3418) 2017-05-29 17:49:54 +02:00
Eugen Rochko
8963f8c3c2 Bump version to 1.4.1 2017-05-28 19:30:38 +02:00
Nolan Lawson
5e41c26203 Use immutable list in UploadButton to avoid wasteful re-render (#3394) 2017-05-28 19:15:35 +02:00
Clworld
45837c533e Re-add stream end log for WebSocket (#3397) 2017-05-28 19:14:44 +02:00
Eugen Rochko
3fa8512474 Fix video having black border on top due to regression from #2608 (#3392)
The combination of object-fit, relative position 50% from top and translating it
back upwards 50% is what allows us to crop the video properly, so it needs to
be +50%-50%
2017-05-28 19:11:47 +02:00
ster
0e20de9f89 Ukrainian translation: fix typo (#3393) 2017-05-28 19:11:29 +02:00
Nolan Lawson
24d645b7d0 Fix IntersectionObserver isIntersecting in Edge (#3365) 2017-05-28 16:45:42 +02:00
Eugen Rochko
7b23f79d41 Bump version to 1.4.0.6 2017-05-28 16:32:53 +02:00
Eugen Rochko
3b4095cf1b Update bootsnap to 0.3.0 (fix xattr.h error) (#3390) 2017-05-28 16:32:29 +02:00
Nolan Lawson
28cbfb9f10 Simplify isIntersecting in status_list.js (#3371) 2017-05-28 16:26:35 +02:00
Akihiko Odaki
189a06d2a2 Fix Webpack Bundle Analyzer output for Webpacker (#3374)
Webpacker failed to parse output of Webpack when a module requires
non-existent module or has similar errors. This commit fixes the bug.
2017-05-28 16:26:16 +02:00
vidarlee
450441fc11 i18n: Add some tags for Chinese translation (#3379)
* Enhance for the Chinese translation

* Add filtered_languages for Chinese Translation include CN/HK/TW for PR #3175

* i18n: Add some tags for Chinese translation

* i18n: Add some tags for Chinese translation
2017-05-28 16:25:54 +02:00
Masoud Abkenar
b619362a36 Persian translation fixes and updates (#3380)
* Persian translation fix

* Persian translation of new strings
2017-05-28 16:25:45 +02:00
Eugen Rochko
425d02287a Improve streaming API cluster logging (#3370)
* Improve streaming API cluster logging

* Less verbose error middleware logging (stack trace useless there)

* Fix error logging

* Prevent potential issue

* Add missing "done()" in catch of Promise.all, websocket heartbeat re-implemented like in example

* I actually forgot a done(), the absolute madman
2017-05-28 16:25:26 +02:00
Clworld
2e429c0c25 Reject revoked access_token on Streaming API. (#3367) 2017-05-27 23:27:54 +02:00
Eugen Rochko
e0e12b0fee Bump version to 1.4.0.5 2017-05-27 16:56:47 +02:00
Eugen Rochko
62ca37884a Fix #2922 - Load stylesheet from "custom.css" entrypoint when present (#3332)
* Fix #2922 - Load stylesheet from "custom.css" entrypoint when present

This is pretty much the same way it worked as before, albeit with
having to create app/javascript/packs/custom.js with
require('../styles/custom.scss') (or whatever you want really), which
will be a blank slate for you to import whatever you want

* Remove old assets directory

* Extract font-awesome into common.css and always load it
2017-05-27 16:55:09 +02:00
jeroenpraat
f9180823bc Update Dutch strings for 1.4 (#3363)
* Update nl strings for 1.4

* Update nl strings for 1.4

* Update nl strings for 1.4

* nl strings (+1)

More new OTP strings will be translated another time
2017-05-27 16:28:32 +02:00
Quent-in
4b0c667c09 i18n update for Occitan language (#3362)
* Update simple_form.oc.yml

* Added check spam folder

* Update oc.json

abonats => seguidors

* Update oc.yml

* Update oc.json

Added translations + corrections

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* i18n mailer

* Update reset_password_instructions.oc.html.erb
2017-05-27 15:50:40 +02:00
m4sk1n
1b732cad61 i18n: pl: pluralized (#3344)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-05-27 13:04:42 +02:00
unarist
ecef03bb15 Fix empty flash message on the settings page (#3345) 2017-05-27 13:04:28 +02:00
happycoloredbanana
9642601126 Avoid comparing domains when looking for an exact match of a local account (#3336) 2017-05-27 00:55:08 +02:00
Eugen Rochko
3836d293a1 Add missing background center on public profile headers (#3340) 2017-05-27 00:54:00 +02:00
Eugen Rochko
0734e1fe33 Language filtering in streaming API (#3339) 2017-05-27 00:53:48 +02:00
Eugen Rochko
44cb08297c Fix some nil errors (#3338)
* Fix nil input not handled well in AuthorExtractor concern

* Fix hard error in ProcessFeedService when replied-to status has been deleted

* Fix nil errors in ProcessInteractionService when favourited status
cannot be found
2017-05-27 00:53:38 +02:00
Eugen Rochko
bd21afb5ed Replace onboarding elephant with friendlier graphic, shorter animation (#3337)
on status fade-in, fix buttons not having pointer cursor
2017-05-27 00:53:25 +02:00
m4sk1n
ef80ad17b3 Updated Polish translation (#3335)
* i18n: updated Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: completed Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: corrected Polish translation

Signed-off-by: Marcin Mikołajczak <m4sk1n@vivaldi.net>

* i18n: Updated Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Update simple_form.pl.yml

* Update simple_form.pl.yml

* updated Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Update pl.yml
2017-05-26 19:59:46 +02:00
Eugen Rochko
9ea4f37e78 Add "meta" attribute to return of POST /api/v1/media method as well (#3333) 2017-05-26 18:22:30 +02:00
Eugen Rochko
c48772fd3f Introduce react-textarea-autosize instead of using style.height side effects (#3334) 2017-05-26 18:22:23 +02:00
Akihiko Odaki
860e257a68 Remove redundant call of recent scope in AccountsController (#3330)
recent is included in paginate_by_max_id.
2017-05-26 16:35:25 +02:00
unarist
902d9e34b4 Remove status context construction in the React side (#3331)
because it may causes flicker on the conversation when it contains blocked/muted user's status.

We use `/api/v1/statuses/{id}/context` to obtain status ids in the
conversation which filters blocked/muted user, but also uses internal
cache constructed from `in_reply_to_id` by `normalizeStatus()` in
`reducers/timelines.js` on each status loading which doesn't filter.

So statuses appears in conversation if those are cached, even those
statuses are from blocked/muted user. Then context cache will be updated
with the result of the context API and those statuses will be removed.

I have left the `normalizeStatus()` function itself which is called many
functions in the file as a placeholder for now, but maybe it should be
removed completely.
2017-05-26 16:34:08 +02:00
539 changed files with 14588 additions and 6638 deletions

View File

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

View File

@@ -2,6 +2,7 @@
.env.*
public/system
public/assets
public/packs
node_modules
storybook
neo4j

View File

@@ -11,6 +11,8 @@ DB_NAME=gonano
DB_PASS=$DATA_DB_PASS
DB_PORT=5432
DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Federation
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.

View File

@@ -65,7 +65,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# CDN_HOST=assets.example.com
# CDN_HOST=https://assets.example.com
# S3 (optional)
# S3_ENABLED=true

View File

@@ -21,22 +21,10 @@ parserOptions:
rules:
no-cond-assign: error
no-console: warn
no-irregular-whitespace: error
no-unreachable: error
valid-typeof: error
consistent-return: error
dot-notation: error
eqeqeq: error
no-fallthrough: error
no-unused-expressions: error
strict: off
no-catch-shadow: error
indent:
- warn
- 2
brace-style: warn
comma-dangle:
- error
- always-multiline
comma-spacing:
- warn
- before: false
@@ -44,22 +32,61 @@ rules:
comma-style:
- warn
- last
consistent-return: error
dot-notation: error
eqeqeq: error
indent:
- warn
- 2
jsx-quotes:
- error
- prefer-single
no-catch-shadow: error
no-cond-assign: error
no-console:
- warn
- allow:
- error
no-fallthrough: error
no-irregular-whitespace: error
no-mixed-spaces-and-tabs: warn
no-nested-ternary: warn
no-trailing-spaces: warn
semi: error
no-unreachable: error
no-unused-expressions: error
object-curly-spacing:
- error
- always
padded-blocks:
- error
- classes: always
comma-dangle:
quotes:
- error
- always-multiline
- single
semi: error
strict: off
valid-typeof: error
react/jsx-wrap-multilines: error
react/jsx-boolean-value: error
react/jsx-closing-bracket-location:
- error
- line-aligned
react/jsx-curly-spacing: error
react/jsx-equals-spacing: error
react/jsx-first-prop-new-line:
- error
- multiline-multiprop
react/jsx-indent:
- error
- 2
react/jsx-no-bind: error
react/self-closing-comp: error
react/prop-types: error
react/jsx-no-duplicate-props: error
react/jsx-tag-spacing: error
react/jsx-wrap-multilines: error
react/no-multi-comp: off
react/no-string-refs: error
react/prop-types: error
react/self-closing-comp: error
jsx-a11y/accessible-emoji: warn
jsx-a11y/anchor-has-content: warn

2
.gitignore vendored
View File

@@ -20,6 +20,7 @@ coverage
public/system
public/assets
public/packs
public/packs-test
.env
.env.production
node_modules/
@@ -33,6 +34,7 @@ config/deploy/*
# Ignore IDE files
.vscode/
.idea/
# Ignore postgres + redis volume optionally created by docker-compose
postgres

View File

@@ -1,26 +1,35 @@
Rails:
Enabled: true
AllCops:
TargetRubyVersion: 2.3
Exclude:
- 'spec/**/*'
- 'db/**/*'
- 'app/views/**/*'
- 'config/**/*'
- 'bin/*'
- 'Rakefile'
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
Style/PerlBackrefs:
AutoCorrect: false
Style/ClassAndModuleChildren:
Bundler/OrderedGems:
Enabled: false
Metrics/BlockNesting:
Max: 2
Layout/AccessModifierIndentation:
EnforcedStyle: indent
Metrics/LineLength:
AllowURI: true
Layout/EmptyLineAfterMagicComment:
Enabled: false
Metrics/MethodLength:
CountComments: false
Max: 10
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space
Metrics/AbcSize:
Max: 100
Metrics/BlockLength:
Exclude:
- 'lib/tasks/**/*'
Metrics/BlockNesting:
Max: 3
@@ -31,22 +40,36 @@ Metrics/ClassLength:
Metrics/CyclomaticComplexity:
Max: 15
Metrics/LineLength:
AllowURI: true
Enabled: false
Metrics/MethodLength:
CountComments: false
Max: 55
Metrics/ModuleLength:
CountComments: false
Max: 200
Metrics/PerceivedComplexity:
Max: 10
Metrics/ParameterLists:
Max: 4
CountKeywordArgs: true
Style/AccessModifierIndentation:
EnforcedStyle: indent
Metrics/PerceivedComplexity:
Max: 10
Rails:
Enabled: true
Rails/HasAndBelongsToMany:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/CollectionMethods:
Enabled: true
@@ -62,36 +85,25 @@ Style/DoubleNegation:
Style/FrozenStringLiteralComment:
Enabled: true
Style/SpaceInsideHashLiteralBraces:
EnforcedStyle: space
Style/TrailingCommaInLiteral:
EnforcedStyleForMultiline: 'comma'
Style/RegexpLiteral:
Style/GuardClause:
Enabled: false
Style/Lambda:
Enabled: false
Style/GuardClause:
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%i': '()'
'%w': '()'
Style/PerlBackrefs:
AutoCorrect: false
Style/RegexpLiteral:
Enabled: false
Rails/HasAndBelongsToMany:
Style/SymbolArray:
Enabled: false
Bundler/OrderedGems:
Enabled: false
AllCops:
TargetRubyVersion: 2.3
Exclude:
- 'spec/**/*'
- 'db/**/*'
- 'app/views/**/*'
- 'config/**/*'
- 'bin/*'
- 'Rakefile'
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
Style/TrailingCommaInLiteral:
EnforcedStyleForMultiline: 'comma'

View File

@@ -4,8 +4,10 @@ cache:
yarn: true
directories:
- node_modules
- public/assets
- public/packs-test
dist: trusty
sudo: false
sudo: required
notifications:
email: false
@@ -50,6 +52,6 @@ before_script:
- ln -s /usr/bin/x86_64-linux-gnu-g++-6 "$HOME/g++"
script:
- bundle exec parallel_test spec/ --group-by filesize --type rspec
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
- npm test
- bundle exec i18n-tasks unused

View File

@@ -6,7 +6,7 @@ ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.8'
gem 'rails', '~> 5.0'
gem 'rails', '~> 5.1.0'
gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2'
@@ -38,6 +38,7 @@ gem 'nokogiri', '~> 1.7'
gem 'oj', '~> 3.0'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.5'
gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
@@ -51,13 +52,14 @@ gem 'sanitize', '~> 4.4'
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-scheduler', '~> 2.1'
gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'statsd-instrument', '~> 2.1'
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 1.2'
gem 'webpacker', '~> 2.0'
group :development, :test do
gem 'fabrication', '~> 2.16'
@@ -69,6 +71,7 @@ end
group :test do
gem 'capybara', '~> 2.14'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'microformats2', '~> 3.0'
gem 'rails-controller-testing', '~> 1.0'
@@ -86,7 +89,7 @@ group :development do
gem 'bullet', '~> 5.5'
gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3'
gem 'rubocop', '~> 0.48', require: false
gem 'rubocop', require: false
gem 'brakeman', '~> 3.6', require: false
gem 'bundler-audit', '~> 0.5', require: false
gem 'scss_lint', '~> 0.53', require: false

View File

@@ -1,40 +1,40 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.0.3)
actionpack (= 5.0.3)
nio4r (>= 1.2, < 3.0)
actioncable (5.1.1)
actionpack (= 5.1.1)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.0.3)
actionpack (= 5.0.3)
actionview (= 5.0.3)
activejob (= 5.0.3)
actionmailer (5.1.1)
actionpack (= 5.1.1)
actionview (= 5.1.1)
activejob (= 5.1.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.0.3)
actionview (= 5.0.3)
activesupport (= 5.0.3)
actionpack (5.1.1)
actionview (= 5.1.1)
activesupport (= 5.1.1)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.0.3)
activesupport (= 5.0.3)
actionview (5.1.1)
activesupport (= 5.1.1)
builder (~> 3.1)
erubis (~> 2.7.0)
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.0.3)
activesupport (= 5.0.3)
activejob (5.1.1)
activesupport (= 5.1.1)
globalid (>= 0.3.6)
activemodel (5.0.3)
activesupport (= 5.0.3)
activerecord (5.0.3)
activemodel (= 5.0.3)
activesupport (= 5.0.3)
arel (~> 7.0)
activesupport (5.0.3)
activemodel (5.1.1)
activesupport (= 5.1.1)
activerecord (5.1.1)
activemodel (= 5.1.1)
activesupport (= 5.1.1)
arel (~> 8.0)
activesupport (5.1.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
@@ -43,22 +43,22 @@ GEM
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.2.0)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.1)
annotate (2.7.2)
activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 12.0)
arel (7.1.4)
rake (>= 10.4, < 13.0)
arel (8.0.0)
ast (2.3.0)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.9.21)
aws-sdk-resources (= 2.9.21)
aws-sdk-core (2.9.21)
aws-sdk (2.9.37)
aws-sdk-resources (= 2.9.37)
aws-sdk-core (2.9.37)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.9.21)
aws-sdk-core (= 2.9.21)
aws-sdk-resources (2.9.37)
aws-sdk-core (= 2.9.37)
aws-sigv4 (1.0.0)
bcrypt (3.1.11)
better_errors (2.1.1)
@@ -67,9 +67,9 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (0.2.14)
bootsnap (1.0.0)
msgpack (~> 1.0)
brakeman (3.6.1)
brakeman (3.6.2)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
@@ -85,7 +85,7 @@ GEM
capistrano-bundler (1.2.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.2.3)
capistrano-rails (1.3.0)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.1)
@@ -93,7 +93,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.14.0)
capybara (2.14.2)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@@ -130,7 +130,7 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.5)
doorkeeper (4.2.6)
railties (>= 4.2)
dotenv (2.2.1)
dotenv-rails (2.2.1)
@@ -141,6 +141,7 @@ GEM
thread
thread_safe
encryptor (3.0.0)
erubi (1.6.0)
erubis (2.7.0)
et-orbi (1.0.4)
tzinfo
@@ -185,7 +186,7 @@ GEM
httplog (0.99.3)
colorize
rack
i18n (0.8.1)
i18n (0.8.4)
i18n-tasks (0.9.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
@@ -225,7 +226,7 @@ GEM
railties (>= 4, < 5.2)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.5)
mail (2.6.6)
mime-types (>= 1.16, < 4)
method_source (0.8.2)
microformats2 (3.1.0)
@@ -235,22 +236,22 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_portile2 (2.1.0)
mini_portile2 (2.2.0)
minitest (5.10.2)
msgpack (1.1.0)
multi_json (1.12.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.1.0)
nio4r (2.0.0)
nokogiri (1.7.2)
mini_portile2 (~> 2.1.0)
nokogumbo (1.4.11)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
oj (3.0.9)
oj (3.1.0)
openssl (2.0.3)
orm_adapter (0.5.0)
ostatus2 (2.0.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
@@ -273,7 +274,7 @@ GEM
pg (0.20.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.0)
pkg-config (1.2.3)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -282,7 +283,9 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (2.0.5)
puma (3.8.2)
puma (3.9.1)
pundit (1.1.0)
activesupport (>= 3.0.0)
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (2.0.3)
@@ -294,17 +297,17 @@ GEM
rack-test (0.6.3)
rack (>= 1.0)
rack-timeout (0.4.2)
rails (5.0.3)
actioncable (= 5.0.3)
actionmailer (= 5.0.3)
actionpack (= 5.0.3)
actionview (= 5.0.3)
activejob (= 5.0.3)
activemodel (= 5.0.3)
activerecord (= 5.0.3)
activesupport (= 5.0.3)
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)
bundler (>= 1.3.0, < 2.0)
railties (= 5.0.3)
railties (= 5.1.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -320,15 +323,15 @@ GEM
railties (~> 5.0)
rails-settings-cached (0.6.5)
rails (>= 4.2.0)
railties (5.0.3)
actionpack (= 5.0.3)
activesupport (= 5.0.3)
railties (5.1.1)
actionpack (= 5.1.1)
activesupport (= 5.1.1)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (11.3.0)
rake (12.0.0)
redis (3.3.3)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
@@ -374,7 +377,8 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.6.0)
rubocop (0.48.1)
rubocop (0.49.1)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
@@ -382,10 +386,10 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.8.1)
rufus-scheduler (3.4.0)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
safe_yaml (1.0.4)
sanitize (4.4.0)
sanitize (4.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
@@ -393,12 +397,15 @@ GEM
scss_lint (0.53.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sidekiq (5.0.0)
sidekiq (5.0.2)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
sidekiq-scheduler (2.1.4)
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.5)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
@@ -454,14 +461,14 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpacker (1.2)
webpacker (2.0)
activesupport (>= 4.2)
multi_json (~> 1.2)
railties (>= 4.2)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
xpath (2.0.0)
xpath (2.1.0)
nokogiri (~> 1.3)
PLATFORMS
@@ -484,6 +491,7 @@ DEPENDENCIES
capistrano-yarn (~> 2.0)
capybara (~> 2.14)
cld3 (~> 3.1)
climate_control (~> 0.2)
devise (~> 4.2)
devise-two-factor (~> 3.0)
doorkeeper (~> 4.2)
@@ -518,11 +526,12 @@ DEPENDENCIES
pkg-config (~> 1.2)
pry-rails (~> 0.3)
puma (~> 3.8)
pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
rails (~> 5.0)
rails (~> 5.1.0)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6)
@@ -532,11 +541,12 @@ DEPENDENCIES
rqrcode (~> 0.10)
rspec-rails (~> 3.6)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.48)
rubocop
ruby-oembed (~> 0.12)
sanitize (~> 4.4)
scss_lint (~> 0.53)
sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1)
sidekiq-unique-jobs (~> 5.0)
simple-navigation (~> 4.0)
@@ -548,10 +558,10 @@ DEPENDENCIES
tzinfo-data (~> 1.2017)
uglifier (~> 3.2)
webmock (~> 3.0)
webpacker (~> 1.2)
webpacker (~> 2.0)
RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH
1.14.6
1.15.1

View File

@@ -13,7 +13,7 @@ An alternative implementation of the GNU social project. Based on [ActivityStrea
Click on the screenshot to watch a demo of the UI:
[![Screenshot](http://puu.sh/vMcvm/290f459dd4.jpg)][youtube_demo]
[![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU

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

@@ -6,12 +6,12 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
@statuses = @account.statuses.permitted_for(@account, current_account).recent.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
end
format.atom do
@entries = @account.stream_entries.recent.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end

View File

@@ -2,16 +2,43 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
def index
@accounts = filtered_accounts.page(params[:page])
end
def show
@account = Account.find(params[:id])
def show; end
def subscribe
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
UnsubscribeService.new.call(@account)
redirect_to admin_account_path(@account.id)
end
def redownload
@account.avatar = @account.avatar_remote_url
@account.header = @account.header_remote_url
@account.save!
redirect_to admin_account_path(@account.id)
end
private
def set_account
@account = Account.find(params[:id])
end
def require_remote_account!
redirect_to admin_account_path(@account.id) if @account.local?
end
def filtered_accounts
AccountFilter.new(filter_params).results
end

View File

@@ -3,13 +3,18 @@
module Admin
class InstancesController < BaseController
def index
@instances = ordered_instances.page(params[:page])
@instances = ordered_instances
end
private
def paginated_instances
Account.remote.by_domain_accounts.page(params[:page])
end
helper_method :paginated_instances
def ordered_instances
Account.remote.by_domain_accounts
paginated_instances.map { |account| Instance.new(account) }
end
end
end

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
module Admin
class PubsubhubbubController < BaseController
def index
@subscriptions = Subscription.order(id: :desc).includes(:account).page(params[:page])
end
end
end

View File

@@ -2,6 +2,8 @@
module Admin
class ReportedStatusesController < BaseController
include Authorization
before_action :set_report
before_action :set_status
@@ -11,6 +13,7 @@ module Admin
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
redirect_to admin_report_path(@report)
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Admin
class SubscriptionsController < BaseController
def index
@subscriptions = ordered_subscriptions.page(requested_page)
end
private
def ordered_subscriptions
Subscription.order(id: :desc).includes(:account)
end
def requested_page
params[:page].to_i
end
end
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::Activitypub::ActivitiesController < ApiController
class Api::ActivityPub::ActivitiesController < Api::BaseController
include Authorization
# before_action :set_follow, only: [:show_follow]
before_action :set_status, only: [:show_status]
@@ -8,7 +10,7 @@ class Api::Activitypub::ActivitiesController < ApiController
# Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity.
def show_status
return forbidden unless @status.permitted?
authorize @status, :show?
if @status.reblog?
render :show_status_announce

View File

@@ -1,12 +1,14 @@
# frozen_string_literal: true
class Api::Activitypub::NotesController < ApiController
class Api::ActivityPub::NotesController < Api::BaseController
include Authorization
before_action :set_status
respond_to :activitystreams2
def show
forbidden unless @status.permitted?
authorize @status, :show?
end
private

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::Activitypub::OutboxController < ApiController
class Api::ActivityPub::OutboxController < Api::BaseController
before_action :set_account
respond_to :activitystreams2

View File

@@ -1,16 +1,14 @@
# frozen_string_literal: true
class ApiController < ApplicationController
class Api::BaseController < ApplicationController
DEFAULT_STATUSES_LIMIT = 20
DEFAULT_ACCOUNTS_LIMIT = 40
protect_from_forgery with: :null_session
include RateLimitHeaders
skip_before_action :verify_authenticity_token
skip_before_action :store_current_location
before_action :set_rate_limit_headers
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
@@ -45,17 +43,6 @@ class ApiController < ApplicationController
protected
def set_rate_limit_headers
return if request.env['rack.attack.throttle_data'].nil?
now = Time.now.utc
match_data = request.env['rack.attack.throttle_data']['api']
response.headers['X-RateLimit-Limit'] = match_data[:limit].to_s
response.headers['X-RateLimit-Remaining'] = (match_data[:limit] - match_data[:count]).to_s
response.headers['X-RateLimit-Reset'] = (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6)
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path

View File

@@ -1,33 +1,25 @@
# frozen_string_literal: true
class Api::OEmbedController < ApiController
class Api::OEmbedController < Api::BaseController
respond_to :json
def show
@stream_entry = stream_entry_from_url(params[:url])
@width = params[:maxwidth].present? ? params[:maxwidth].to_i : 400
@height = params[:maxheight].present? ? params[:maxheight].to_i : nil
@stream_entry = find_stream_entry.stream_entry
@width = maxwidth_or_default
@height = maxheight_or_default
end
private
def stream_entry_from_url(url)
params = Rails.application.routes.recognize_path(url)
raise ActiveRecord::RecordNotFound unless recognized_stream_entry_url?(params)
stream_entry(params)
def find_stream_entry
StreamEntryFinder.new(params[:url])
end
def recognized_stream_entry_url?(params)
%w(stream_entries statuses).include?(params[:controller]) && params[:action] == 'show'
def maxwidth_or_default
(params[:maxwidth].presence || 400).to_i
end
def stream_entry(params)
if params[:controller] == 'stream_entries'
StreamEntry.find(params[:id])
else
Status.find(params[:id]).stream_entry
end
def maxheight_or_default
params[:maxheight].present? ? params[:maxheight].to_i : nil
end
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::PushController < ApiController
class Api::PushController < Api::BaseController
def update
response, status = process_push_request
render plain: response, status: status

View File

@@ -1,14 +1,12 @@
# frozen_string_literal: true
class Api::SalmonController < ApiController
class Api::SalmonController < Api::BaseController
before_action :set_account
respond_to :txt
def update
payload = request.body.read
if !payload.nil? && verify?(payload)
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
if verify_payload?
process_salmon
head 201
else
head 202
@@ -21,7 +19,15 @@ class Api::SalmonController < ApiController
@account = Account.find(params[:id])
end
def verify?(payload)
VerifySalmonService.new.call(payload)
def payload
@_payload ||= request.body.read
end
def verify_payload?
payload.present? && VerifySalmonService.new.call(payload)
end
def process_salmon
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
end
end

View File

@@ -1,22 +1,19 @@
# frozen_string_literal: true
class Api::SubscriptionsController < ApiController
class Api::SubscriptionsController < Api::BaseController
before_action :set_account
respond_to :txt
def show
if @account.subscription(api_subscription_url(@account.id)).valid?(params['hub.topic'])
@account.update(subscription_expires_at: Time.now.utc + (params['hub.lease_seconds'] || 86_400).to_i.seconds)
render plain: HTMLEntities.new.encode(params['hub.challenge']), status: 200
if subscription.valid?(params['hub.topic'])
@account.update(subscription_expires_at: future_expires)
render plain: encoded_challenge, status: 200
else
head 404
end
end
def update
body = request.body.read
subscription = @account.subscription(api_subscription_url(@account.id))
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
end
@@ -26,6 +23,28 @@ class Api::SubscriptionsController < ApiController
private
def subscription
@_subscription ||= @account.subscription(
api_subscription_url(@account.id)
)
end
def body
@_body ||= request.body.read
end
def encoded_challenge
HTMLEntities.new.encode(params['hub.challenge'])
end
def future_expires
Time.now.utc + lease_seconds_or_default
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 86_400).to_i.seconds
end
def set_account
@account = Account.find(params[:id])
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Accounts::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write }, only: [:update]
before_action :require_user!
def show
@account = current_account
render 'api/v1/accounts/show'
end
def update
current_account.update!(account_params)
@account = current_account
render 'api/v1/accounts/show'
end
private
def account_params
params.permit(:display_name, :note, :avatar, :header)
end
end

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :set_account
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render 'api/v1/accounts/index'
end
private
def set_account
@account = Account.find(params[:account_id])
end
def load_accounts
default_accounts.merge(paginated_follows).to_a
end
def default_accounts
Account.includes(:active_relationships).references(:active_relationships)
end
def paginated_follows
Follow.where(target_account: @account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_account_followers_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_account_followers_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.active_relationships.first.id
end
def pagination_since_id
@accounts.first.active_relationships.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :set_account
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render 'api/v1/accounts/index'
end
private
def set_account
@account = Account.find(params[:account_id])
end
def load_accounts
default_accounts.merge(paginated_follows).to_a
end
def default_accounts
Account.includes(:passive_relationships).references(:passive_relationships)
end
def paginated_follows
Follow.where(account: @account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_account_following_index_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_account_following_index_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.passive_relationships.first.id
end
def pagination_since_id
@accounts.first.passive_relationships.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
respond_to :json
def index
@accounts = Account.where(id: account_ids).select('id')
@following = Account.following_map(account_ids, current_user.account_id)
@followed_by = Account.followed_by_map(account_ids, current_user.account_id)
@blocking = Account.blocking_map(account_ids, current_user.account_id)
@muting = Account.muting_map(account_ids, current_user.account_id)
@requested = Account.requested_map(account_ids, current_user.account_id)
@domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id)
end
private
def account_ids
@_account_ids ||= Array(params[:id]).map(&:to_i)
end
end

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
class Api::V1::Accounts::SearchController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
respond_to :json
def show
@accounts = account_search
render 'api/v1/accounts/index'
end
private
def account_search
AccountSearchService.new.call(
params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolving_search?,
current_account
)
end
def resolving_search?
params[:resolve] == 'true'
end
end

View File

@@ -0,0 +1,92 @@
# frozen_string_literal: true
class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :set_account
after_action :insert_pagination_headers
respond_to :json
def index
@statuses = load_statuses
end
private
def set_account
@account = Account.find(params[:account_id])
end
def load_statuses
cached_account_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_account_statuses
cache_collection account_statuses, Status
end
def account_statuses
default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if params[:only_media]
statuses.merge!(no_replies_scope) if params[:exclude_replies]
end
end
def default_statuses
permitted_account_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def permitted_account_statuses
@account.statuses.permitted_for(@account, current_account)
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end
def no_replies_scope
Status.without_replies
end
def pagination_params(core_params)
params.permit(:limit, :only_media, :exclude_replies).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_account_statuses_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @statuses.empty?
api_v1_account_statuses_url pagination_params(since_id: pagination_since_id)
end
end
def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -1,73 +1,15 @@
# frozen_string_literal: true
class Api::V1::AccountsController < ApiController
before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute, :update_credentials]
class Api::V1::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :write }, only: [:update_credentials]
before_action :require_user!, except: [:show, :following, :followers, :statuses]
before_action :set_account, except: [:verify_credentials, :update_credentials, :suggestions, :search]
before_action :require_user!, except: [:show]
before_action :set_account
respond_to :json
def show; end
def verify_credentials
@account = current_user.account
render :show
end
def update_credentials
current_account.update!(account_params)
@account = current_account
render :show
end
def following
@accounts = Account.includes(:passive_relationships)
.references(:passive_relationships)
.merge(Follow.where(account: @account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = following_api_v1_account_url(pagination_params(max_id: @accounts.last.passive_relationships.first.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = following_api_v1_account_url(pagination_params(since_id: @accounts.first.passive_relationships.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
render :index
end
def followers
@accounts = Account.includes(:active_relationships)
.references(:active_relationships)
.merge(Follow.where(target_account: @account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]))
.to_a
next_path = followers_api_v1_account_url(pagination_params(max_id: @accounts.last.active_relationships.first.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = followers_api_v1_account_url(pagination_params(since_id: @accounts.first.active_relationships.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
render :index
end
def statuses
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
@statuses = @statuses.where(id: MediaAttachment.where(account: @account).where.not(status_id: nil).reorder('').select('distinct status_id')) if params[:only_media]
@statuses = @statuses.without_replies if params[:exclude_replies]
@statuses = cache_collection(@statuses, Status)
set_maps(@statuses)
next_path = statuses_api_v1_account_url(statuses_pagination_params(max_id: @statuses.last.id)) if @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
prev_path = statuses_api_v1_account_url(statuses_pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
set_pagination_headers(next_path, prev_path)
end
def follow
FollowService.new.call(current_user.account, @account.acct)
set_relationship
@@ -111,24 +53,6 @@ class Api::V1::AccountsController < ApiController
render :relationship
end
def relationships
ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i]
@accounts = Account.where(id: ids).select('id')
@following = Account.following_map(ids, current_user.account_id)
@followed_by = Account.followed_by_map(ids, current_user.account_id)
@blocking = Account.blocking_map(ids, current_user.account_id)
@muting = Account.muting_map(ids, current_user.account_id)
@requested = Account.requested_map(ids, current_user.account_id)
@domain_blocking = Account.domain_blocking_map(ids, current_user.account_id)
end
def search
@accounts = AccountSearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account)
render :index
end
private
def set_account
@@ -143,16 +67,4 @@ class Api::V1::AccountsController < ApiController
@requested = Account.requested_map([@account.id], current_user.account_id)
@domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
def statuses_pagination_params(core_params)
params.permit(:limit, :only_media, :exclude_replies).merge(core_params)
end
def account_params
params.permit(:display_name, :note, :avatar, :header)
end
end

View File

@@ -1,14 +1,27 @@
# frozen_string_literal: true
class Api::V1::AppsController < ApiController
class Api::V1::AppsController < Api::BaseController
respond_to :json
def create
@app = Doorkeeper::Application.create!(name: app_params[:client_name], redirect_uri: app_params[:redirect_uris], scopes: (app_params[:scopes] || Doorkeeper.configuration.default_scopes), website: app_params[:website])
@app = Doorkeeper::Application.create!(application_options)
end
private
def application_options
{
name: app_params[:client_name],
redirect_uri: app_params[:redirect_uris],
scopes: app_scopes_or_default,
website: app_params[:website],
}
end
def app_scopes_or_default
app_params[:scopes] || Doorkeeper.configuration.default_scopes
end
def app_params
params.permit(:client_name, :redirect_uris, :scopes, :website)
end

View File

@@ -1,26 +1,62 @@
# frozen_string_literal: true
class Api::V1::BlocksController < ApiController
class Api::V1::BlocksController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = Account.includes(:blocked_by)
.references(:blocked_by)
.merge(Block.where(account: current_account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = api_v1_blocks_url(pagination_params(max_id: @accounts.last.blocked_by_ids.last)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_blocks_url(pagination_params(since_id: @accounts.first.blocked_by_ids.first)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
@accounts = load_accounts
end
private
def load_accounts
default_accounts.merge(paginated_blocks).to_a
end
def default_accounts
Account.includes(:blocked_by).references(:blocked_by)
end
def paginated_blocks
Block.where(account: current_account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_blocks_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_blocks_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.blocked_by_ids.last
end
def pagination_since_id
@accounts.first.blocked_by_ids.first
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end

View File

@@ -1,18 +1,16 @@
# frozen_string_literal: true
class Api::V1::DomainBlocksController < ApiController
class Api::V1::DomainBlocksController < Api::BaseController
BLOCK_LIMIT = 100
before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!
after_action :insert_pagination_headers, only: :show
respond_to :json
def show
@blocks = AccountDomainBlock.where(account: current_account).paginate_by_max_id(limit_param(100), params[:max_id], params[:since_id])
next_path = api_v1_domain_blocks_url(pagination_params(max_id: @blocks.last.id)) if @blocks.size == limit_param(100)
prev_path = api_v1_domain_blocks_url(pagination_params(since_id: @blocks.first.id)) unless @blocks.empty?
set_pagination_headers(next_path, prev_path)
@blocks = load_domain_blocks
render json: @blocks.map(&:domain)
end
@@ -28,6 +26,46 @@ class Api::V1::DomainBlocksController < ApiController
private
def load_domain_blocks
account_domain_blocks.paginate_by_max_id(
limit_param(BLOCK_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_domain_blocks
current_account.domain_blocks
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @blocks.empty?
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@blocks.last.id
end
def pagination_since_id
@blocks.first.id
end
def records_continue?
@blocks.size == limit_param(BLOCK_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end

View File

@@ -1,25 +1,73 @@
# frozen_string_literal: true
class Api::V1::FavouritesController < ApiController
class Api::V1::FavouritesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
results = Favourite.where(account: current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
@statuses = cache_collection(Status.where(id: results.map(&:status_id)), Status)
set_maps(@statuses)
next_path = api_v1_favourites_url(pagination_params(max_id: results.last.id)) if results.size == limit_param(DEFAULT_STATUSES_LIMIT)
prev_path = api_v1_favourites_url(pagination_params(since_id: results.first.id)) unless results.empty?
set_pagination_headers(next_path, prev_path)
@statuses = load_statuses
end
private
def load_statuses
cached_favourites.tap do |statuses|
set_maps(statuses)
end
end
def cached_favourites
cache_collection(
Status.where(
id: results.map(&:status_id)
),
Status
)
end
def results
@_results ||= account_favourites.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_favourites
current_account.favourites
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_favourites_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless results.empty?
api_v1_favourites_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
end
def records_continue?
results.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end

View File

@@ -1,34 +1,74 @@
# frozen_string_literal: true
class Api::V1::FollowRequestsController < ApiController
class Api::V1::FollowRequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!
after_action :insert_pagination_headers, only: :index
def index
@accounts = Account.includes(:follow_requests)
.references(:follow_requests)
.merge(FollowRequest.where(target_account: current_account)
.paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]))
.to_a
next_path = api_v1_follow_requests_url(pagination_params(max_id: @accounts.last.follow_requests.last.id)) if @accounts.size == DEFAULT_ACCOUNTS_LIMIT
prev_path = api_v1_follow_requests_url(pagination_params(since_id: @accounts.first.follow_requests.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
@accounts = load_accounts
end
def authorize
AuthorizeFollowService.new.call(Account.find(params[:id]), current_account)
AuthorizeFollowService.new.call(account, current_account)
render_empty
end
def reject
RejectFollowService.new.call(Account.find(params[:id]), current_account)
RejectFollowService.new.call(account, current_account)
render_empty
end
private
def account
Account.find(params[:id])
end
def load_accounts
default_accounts.merge(paginated_follow_requests).to_a
end
def default_accounts
Account.includes(:follow_requests).references(:follow_requests)
end
def paginated_follow_requests
FollowRequest.where(target_account: current_account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_follow_requests_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_follow_requests_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.follow_requests.last.id
end
def pagination_since_id
@accounts.first.follow_requests.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::FollowsController < ApiController
class Api::V1::FollowsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::InstancesController < ApiController
class Api::V1::InstancesController < Api::BaseController
respond_to :json
def show; end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::MediaController < ApiController
class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
@@ -10,11 +10,11 @@ class Api::V1::MediaController < ApiController
respond_to :json
def create
@media = MediaAttachment.create!(account: current_user.account, file: media_params[:file])
@media = current_account.media_attachments.create!(file: media_params[:file])
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: { error: 'File type of uploaded media could not be verified' }, status: 422
render json: file_type_error, status: 422
rescue Paperclip::Error
render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500
render json: processing_error, status: 500
end
private
@@ -22,4 +22,12 @@ class Api::V1::MediaController < ApiController
def media_params
params.permit(:file)
end
def file_type_error
{ error: 'File type of uploaded media could not be verified' }
end
def processing_error
{ error: 'Error processing thumbnail for uploaded media' }
end
end

View File

@@ -1,26 +1,62 @@
# frozen_string_literal: true
class Api::V1::MutesController < ApiController
class Api::V1::MutesController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = Account.includes(:muted_by)
.references(:muted_by)
.merge(Mute.where(account: current_account)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = api_v1_mutes_url(pagination_params(max_id: @accounts.last.muted_by_ids.last)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_mutes_url(pagination_params(since_id: @accounts.first.muted_by_ids.first)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
@accounts = load_accounts
end
private
def load_accounts
default_accounts.merge(paginated_mutes).to_a
end
def default_accounts
Account.includes(:muted_by).references(:muted_by)
end
def paginated_mutes
Mute.where(account: current_account).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_mutes_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_mutes_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.muted_by_ids.last
end
def pagination_since_id
@accounts.first.muted_by_ids.first
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end

View File

@@ -1,42 +1,83 @@
# frozen_string_literal: true
class Api::V1::NotificationsController < ApiController
class Api::V1::NotificationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
after_action :insert_pagination_headers, only: :index
respond_to :json
DEFAULT_NOTIFICATIONS_LIMIT = 15
def index
@notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
@notifications = cache_collection(@notifications, Notification)
statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
set_maps(statuses)
next_path = api_v1_notifications_url(pagination_params(max_id: @notifications.last.id)) unless @notifications.empty?
prev_path = api_v1_notifications_url(pagination_params(since_id: @notifications.first.id)) unless @notifications.empty?
set_pagination_headers(next_path, prev_path)
@notifications = load_notifications
set_maps_for_notification_target_statuses
end
def show
@notification = Notification.where(account: current_account).find(params[:id])
@notification = current_account.notifications.find(params[:id])
end
def clear
Notification.where(account: current_account).delete_all
current_account.notifications.delete_all
render_empty
end
def dismiss
Notification.find_by!(account: current_account, id: params[:id]).destroy!
current_account.notifications.find_by!(id: params[:id]).destroy!
render_empty
end
private
def load_notifications
cache_collection paginated_notifications, Notification
end
def paginated_notifications
browserable_account_notifications.paginate_by_max_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def browserable_account_notifications
current_account.notifications.browserable(exclude_types)
end
def set_maps_for_notification_target_statuses
set_maps target_statuses_from_notifications
end
def target_statuses_from_notifications
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
unless @notifications.empty?
api_v1_notifications_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @notifications.empty?
api_v1_notifications_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@notifications.last.id
end
def pagination_since_id
@notifications.first.id
end
def exclude_types
val = params.permit(exclude_types: [])[:exclude_types] || []
val = [val] unless val.is_a?(Enumerable)

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::ReportsController < ApiController
class Api::V1::ReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, except: [:create]
before_action -> { doorkeeper_authorize! :write }, only: [:create]
before_action :require_user!
@@ -8,22 +8,32 @@ class Api::V1::ReportsController < ApiController
respond_to :json
def index
@reports = Report.where(account: current_account)
@reports = current_account.reports
end
def create
status_ids = report_params[:status_ids].is_a?(Enumerable) ? report_params[:status_ids] : [report_params[:status_ids]]
@report = Report.create!(account: current_account,
target_account: Account.find(report_params[:account_id]),
status_ids: Status.find(status_ids).pluck(:id),
comment: report_params[:comment])
@report = current_account.reports.create!(
target_account: reported_account,
status_ids: reported_status_ids,
comment: report_params[:comment]
)
render :show
end
private
def reported_status_ids
Status.find(status_ids).pluck(:id)
end
def status_ids
Array(report_params[:status_ids])
end
def reported_account
Account.find(report_params[:account_id])
end
def report_params
params.permit(:account_id, :comment, status_ids: [])
end

View File

@@ -1,9 +1,26 @@
# frozen_string_literal: true
class Api::V1::SearchController < ApiController
class Api::V1::SearchController < Api::BaseController
RESULTS_LIMIT = 5
respond_to :json
def index
@search = OpenStruct.new(SearchService.new.call(params[:q], 5, params[:resolve] == 'true', current_account))
@search = OpenStruct.new(search_results)
end
private
def search_results
SearchService.new.call(
params[:q],
RESULTS_LIMIT,
resolving_search?,
current_account
)
end
def resolving_search?
params[:resolve] == 'true'
end
end

View File

@@ -0,0 +1,82 @@
# frozen_string_literal: true
class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
include Authorization
before_action :authorize_if_got_token
before_action :set_status
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render 'api/v1/statuses/accounts'
end
private
def load_accounts
default_accounts.merge(paginated_favourites).to_a
end
def default_accounts
Account
.includes(:favourites)
.references(:favourites)
.where(favourites: { status_id: @status.id })
end
def paginated_favourites
Favourite.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_status_favourited_by_index_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.favourites.last.id
end
def pagination_since_id
@accounts.first.favourites.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end
def authorize_if_got_token
request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods)
doorkeeper_authorize! :read if request_token
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
class Api::V1::Statuses::FavouritesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
respond_to :json
def create
@status = favourited_status
render 'api/v1/statuses/show'
end
def destroy
@status = requested_status
@favourites_map = { @status.id => false }
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
render 'api/v1/statuses/show'
end
private
def favourited_status
service_result.status.reload
end
def service_result
FavouriteService.new.call(current_user.account, requested_status)
end
def requested_status
Status.find(params[:status_id])
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
class Api::V1::Statuses::MutesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
before_action :set_status
before_action :set_conversation
respond_to :json
def create
current_account.mute_conversation!(@conversation)
@mutes_map = { @conversation.id => true }
render 'api/v1/statuses/show'
end
def destroy
current_account.unmute_conversation!(@conversation)
@mutes_map = { @conversation.id => false }
render 'api/v1/statuses/show'
end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end
def set_conversation
@conversation = @status.conversation
raise Mastodon::ValidationError if @conversation.nil?
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
include Authorization
before_action :authorize_if_got_token
before_action :set_status
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render 'api/v1/statuses/accounts'
end
private
def load_accounts
default_accounts.merge(paginated_statuses).to_a
end
def default_accounts
Account.includes(:statuses).references(:statuses)
end
def paginated_statuses
Status.where(reblog_of_id: @status.id).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @accounts.empty?
api_v1_status_reblogged_by_index_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.statuses.last.id
end
def pagination_since_id
@accounts.first.statuses.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end
def authorize_if_got_token
request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods)
doorkeeper_authorize! :read if request_token
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
class Api::V1::Statuses::ReblogsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
respond_to :json
def create
@status = ReblogService.new.call(current_user.account, status_for_reblog)
render 'api/v1/statuses/show'
end
def destroy
@status = status_for_destroy.reblog
@reblogs_map = { @status.id => false }
authorize status_for_destroy, :unreblog?
RemovalWorker.perform_async(status_for_destroy.id)
render 'api/v1/statuses/show'
end
private
def status_for_reblog
Status.find params[:status_id]
end
def status_for_destroy
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
end
end

View File

@@ -1,11 +1,12 @@
# frozen_string_literal: true
class Api::V1::StatusesController < ApiController
before_action :authorize_if_got_token, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite, :mute, :unmute]
before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite, :mute, :unmute]
before_action :require_user!, except: [:show, :context, :card, :reblogged_by, :favourited_by]
before_action :set_status, only: [:show, :context, :card, :reblogged_by, :favourited_by, :mute, :unmute]
before_action :set_conversation, only: [:mute, :unmute]
class Api::V1::StatusesController < Api::BaseController
include Authorization
before_action :authorize_if_got_token, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy]
before_action :require_user!, except: [:show, :context, :card]
before_action :set_status, only: [:show, :context, :card]
respond_to :json
@@ -31,36 +32,6 @@ class Api::V1::StatusesController < ApiController
render_empty if @card.nil?
end
def reblogged_by
@accounts = Account.includes(:statuses)
.references(:statuses)
.merge(Status.where(reblog_of_id: @status.id)
.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = reblogged_by_api_v1_status_url(pagination_params(max_id: @accounts.last.statuses.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = reblogged_by_api_v1_status_url(pagination_params(since_id: @accounts.first.statuses.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
render :accounts
end
def favourited_by
@accounts = Account.includes(:favourites)
.references(:favourites)
.where(favourites: { status_id: @status.id })
.merge(Favourite.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]))
.to_a
next_path = favourited_by_api_v1_status_url(pagination_params(max_id: @accounts.last.favourites.last.id)) if @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = favourited_by_api_v1_status_url(pagination_params(since_id: @accounts.first.favourites.first.id)) unless @accounts.empty?
set_pagination_headers(next_path, prev_path)
render :accounts
end
def create
@status = PostStatusService.new.call(current_user.account,
status_params[:status],
@@ -77,65 +48,21 @@ class Api::V1::StatusesController < ApiController
def destroy
@status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
render_empty
end
def reblog
@status = ReblogService.new.call(current_user.account, Status.find(params[:id]))
render :show
end
def unreblog
reblog = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!
@status = reblog.reblog
@reblogs_map = { @status.id => false }
RemovalWorker.perform_async(reblog.id)
render :show
end
def favourite
@status = FavouriteService.new.call(current_user.account, Status.find(params[:id])).status.reload
render :show
end
def unfavourite
@status = Status.find(params[:id])
@favourites_map = { @status.id => false }
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
render :show
end
def mute
current_account.mute_conversation!(@conversation)
@mutes_map = { @conversation.id => true }
render :show
end
def unmute
current_account.unmute_conversation!(@conversation)
@mutes_map = { @conversation.id => false }
render :show
end
private
def set_status
@status = Status.find(params[:id])
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
end
def set_conversation
@conversation = @status.conversation
raise Mastodon::ValidationError if @conversation.nil?
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end
def status_params

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class Api::V1::StreamingController < Api::BaseController
respond_to :json
def index
if Rails.configuration.x.streaming_api_base_url != request.host
uri = URI.parse(request.url)
uri.host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
redirect_to uri.to_s, status: 301
else
raise ActiveRecord::RecordNotFound
end
end
end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
module Api::V1::Timelines
class BaseController < ApiController
respond_to :json
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
private
def cache_collection(raw)
super(raw, Status)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
raise 'Override in child controllers'
end
def prev_path
raise 'Override in child controllers'
end
end
end

View File

@@ -1,44 +1,62 @@
# frozen_string_literal: true
module Api::V1::Timelines
class HomeController < BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
class Api::V1::Timelines::HomeController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
@statuses = load_statuses
end
respond_to :json
private
def show
@statuses = load_statuses
render 'api/v1/timelines/show'
end
def load_statuses
cached_home_statuses.tap do |statuses|
set_maps(statuses)
end
end
private
def cached_home_statuses
cache_collection home_statuses
end
def home_statuses
account_home_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_home_feed
Feed.new(:home, current_account)
end
def next_path
api_v1_timelines_home_url pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_home_url pagination_params(since_id: @statuses.first.id)
def load_statuses
cached_home_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_home_statuses
cache_collection home_statuses, Status
end
def home_statuses
account_home_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_home_feed
Feed.new(:home, current_account)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_home_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_home_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -1,41 +1,60 @@
# frozen_string_literal: true
module Api::V1::Timelines
class PublicController < BaseController
def show
@statuses = load_statuses
end
class Api::V1::Timelines::PublicController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
private
respond_to :json
def load_statuses
cached_public_statuses.tap do |statuses|
set_maps(statuses)
end
end
def show
@statuses = load_statuses
render 'api/v1/timelines/show'
end
def cached_public_statuses
cache_collection public_statuses
end
private
def public_statuses
public_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def public_timeline_statuses
Status.as_public_timeline(current_account, params[:local])
end
def next_path
api_v1_timelines_public_url pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_public_url pagination_params(since_id: @statuses.first.id)
def load_statuses
cached_public_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_public_statuses
cache_collection public_statuses, Status
end
def public_statuses
public_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def public_timeline_statuses
Status.as_public_timeline(current_account, params[:local])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_public_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_public_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -1,51 +1,69 @@
# frozen_string_literal: true
module Api::V1::Timelines
class TagController < BaseController
before_action :load_tag
class Api::V1::Timelines::TagController < Api::BaseController
before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
@statuses = load_statuses
end
respond_to :json
private
def show
@statuses = load_statuses
render 'api/v1/timelines/show'
end
def load_tag
@tag = Tag.find_by(name: params[:id].downcase)
end
private
def load_statuses
cached_tagged_statuses.tap do |statuses|
set_maps(statuses)
end
end
def load_tag
@tag = Tag.find_by(name: params[:id].downcase)
end
def cached_tagged_statuses
cache_collection tagged_statuses
end
def tagged_statuses
if @tag.nil?
[]
else
tag_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def tag_timeline_statuses
Status.as_tag_timeline(@tag, current_account, params[:local])
end
def next_path
api_v1_timelines_tag_url params[:id], pagination_params(max_id: @statuses.last.id)
end
def prev_path
api_v1_timelines_tag_url params[:id], pagination_params(since_id: @statuses.first.id)
def load_statuses
cached_tagged_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_tagged_statuses
cache_collection tagged_statuses, Status
end
def tagged_statuses
if @tag.nil?
[]
else
tag_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def tag_timeline_statuses
Status.as_tag_timeline(@tag, current_account, params[:local])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_tag_url params[:id], pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_tag_url params[:id], pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::Web::SettingsController < ApiController
class Api::Web::SettingsController < Api::BaseController
respond_to :json
before_action :require_user!

View File

@@ -12,13 +12,13 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
remember_me(resource)
flash[:notice] = nil
flash.delete(:notice)
end
end
def destroy
super
flash[:notice] = nil
flash.delete(:notice)
end
protected
@@ -27,7 +27,7 @@ class Auth::SessionsController < Devise::SessionsController
if session[:otp_user_id]
User.find(session[:otp_user_id])
elsif user_params[:email]
User.find_by(email: user_params[:email])
User.find_for_authentication(email: user_params[:email])
end
end

View File

@@ -40,7 +40,7 @@ class AuthorizeFollowsController < ApplicationController
end
def account_from_remote_follow
FollowRemoteAccountService.new.call(acct_without_prefix)
ResolveRemoteAccountService.new.call(acct_without_prefix)
end
def acct_param_is_url?

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Authorization
extend ActiveSupport::Concern
include Pundit
def pundit_user
current_account
end
def authorize(*)
super
rescue Pundit::NotAuthorizedError
raise Mastodon::NotPermittedError
end
def authorize_with(user, record, query)
Pundit.authorize(user, record, query)
rescue Pundit::NotAuthorizedError
raise Mastodon::NotPermittedError
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module ExportControllerConcern
extend ActiveSupport::Concern
included do
before_action :authenticate_user!
before_action :load_export
end
private
def load_export
@export = Export.new(current_account)
end
def send_export_file
respond_to do |format|
format.csv { send_data export_data, filename: export_filename }
end
end
def export_data
raise 'Override in controller'
end
def export_filename
"#{controller_name}.csv"
end
end

View File

@@ -17,12 +17,24 @@ module Localized
end
def default_locale
ENV.fetch('DEFAULT_LOCALE') do
user_supplied_locale || I18n.default_locale
end
request_locale || env_locale || I18n.default_locale
end
def user_supplied_locale
http_accept_language.language_region_compatible_from(I18n.available_locales)
def env_locale
ENV['DEFAULT_LOCALE']
end
def request_locale
preferred_locale || compatible_locale
end
def preferred_locale
http_accept_language.preferred_language_from([env_locale]) ||
http_accept_language.preferred_language_from(I18n.available_locales)
end
def compatible_locale
http_accept_language.compatible_language_from([env_locale]) ||
http_accept_language.compatible_language_from(I18n.available_locales)
end
end

View File

@@ -4,19 +4,13 @@ module ObfuscateFilename
extend ActiveSupport::Concern
class_methods do
def obfuscate_filename(*args)
before_action { obfuscate_filename(*args) }
def obfuscate_filename(path)
before_action do
file = params.dig(*path)
next if file.nil?
file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
end
end
end
def obfuscate_filename(path)
file = params.dig(*path)
return if file.nil?
file.original_filename = secure_token + File.extname(file.original_filename)
end
def secure_token(length = 16)
SecureRandom.hex(length / 2)
end
end

View File

@@ -0,0 +1,57 @@
# frozen_string_literal: true
module RateLimitHeaders
extend ActiveSupport::Concern
included do
before_action :set_rate_limit_headers, if: :rate_limited_request?
end
private
def set_rate_limit_headers
apply_header_limit
apply_header_remaining
apply_header_reset
end
def rate_limited_request?
!request.env['rack.attack.throttle_data'].nil?
end
def apply_header_limit
response.headers['X-RateLimit-Limit'] = rate_limit_limit
end
def rate_limit_limit
api_throttle_data[:limit].to_s
end
def apply_header_remaining
response.headers['X-RateLimit-Remaining'] = rate_limit_remaining
end
def rate_limit_remaining
(api_throttle_data[:limit] - api_throttle_data[:count]).to_s
end
def apply_header_reset
response.headers['X-RateLimit-Reset'] = rate_limit_reset
end
def rate_limit_reset
(request_time + reset_period_offset).iso8601(6)
end
def api_throttle_data
request.env['rack.attack.throttle_data']['api']
end
def request_time
@_request_time ||= Time.now.utc
end
def reset_period_offset
api_throttle_data[:period] - request_time.to_i % api_throttle_data[:period]
end
end

View File

@@ -17,7 +17,7 @@ module UserTrackingConcern
current_user.update_tracked_fields!(request)
# Regenerate feed if needed
RegenerationWorker.perform_async(current_user.account_id) if user_needs_feed_update?
regenerate_feed! if user_needs_feed_update?
end
def user_needs_sign_in_update?
@@ -27,4 +27,9 @@ module UserTrackingConcern
def user_needs_feed_update?
current_user.last_sign_in_at < REGENERATE_FEED_DAYS.days.ago
end
def regenerate_feed!
Redis.current.setnx("account:#{current_user.account_id}:regeneration", true) == 1 && Redis.current.expire("account:#{current_user.account_id}:regeneration", 3_600 * 24)
RegenerationWorker.perform_async(current_user.account_id)
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
class ManifestsController < ApplicationController
before_action :set_instance_presenter
def show; end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MediaController < ApplicationController
include Authorization
before_action :verify_permitted_status
def show
@@ -14,6 +16,9 @@ class MediaController < ApplicationController
end
def verify_permitted_status
raise ActiveRecord::RecordNotFound unless media_attachment.status.permitted?(current_account)
authorize media_attachment.status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
class Settings::DeletesController < ApplicationController
layout 'admin'
before_action :check_enabled_deletion
before_action :authenticate_user!
def show
@confirmation = Form::DeleteConfirmation.new
end
def destroy
if current_user.valid_password?(delete_params[:password])
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
sign_out
redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg')
else
redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg')
end
end
private
def check_enabled_deletion
redirect_to root_path unless Setting.open_deletion
end
def delete_params
params.require(:form_delete_confirmation).permit(:password)
end
end

View File

@@ -1,23 +0,0 @@
# frozen_string_literal: true
module Settings
module Exports
class BaseController < ApplicationController
before_action :authenticate_user!
def index
@export = Export.new(current_account)
respond_to do |format|
format.csv { send_data export_data, filename: export_filename }
end
end
private
def export_filename
"#{controller_name}.csv"
end
end
end
end

View File

@@ -2,7 +2,13 @@
module Settings
module Exports
class BlockedAccountsController < BaseController
class BlockedAccountsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data

View File

@@ -2,7 +2,13 @@
module Settings
module Exports
class FollowingAccountsController < BaseController
class FollowingAccountsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data

View File

@@ -2,7 +2,13 @@
module Settings
module Exports
class MutedAccountsController < BaseController
class MutedAccountsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data

View File

@@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'sidekiq-bulk'
class Settings::FollowerDomainsController < ApplicationController
layout 'admin'
@@ -13,8 +15,8 @@ class Settings::FollowerDomainsController < ApplicationController
def update
domains = bulk_params[:select] || []
domains.each do |domain|
SoftBlockDomainFollowersWorker.perform_async(current_account.id, domain)
SoftBlockDomainFollowersWorker.push_bulk(domains) do |domain|
[current_account.id, domain]
end
redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)

View File

@@ -35,6 +35,7 @@ class Settings::PreferencesController < ApplicationController
params.require(:user).permit(
:setting_default_privacy,
:setting_boost_modal,
:setting_delete_modal,
:setting_auto_play_gif,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class StatusesController < ApplicationController
include Authorization
layout 'public'
before_action :set_account
@@ -30,7 +32,10 @@ class StatusesController < ApplicationController
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
def check_account_suspension

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class StreamEntriesController < ApplicationController
include Authorization
layout 'public'
before_action :set_account
@@ -42,7 +44,11 @@ class StreamEntriesController < ApplicationController
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
@type = @stream_entry.activity_type.downcase
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? || (@stream_entry.hidden? && !@stream_entry.activity.permitted?(current_account))
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
authorize @stream_entry.activity, :show? if @stream_entry.hidden?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
def check_account_suspension

View File

@@ -2,6 +2,8 @@
module WellKnown
class HostMetaController < ApplicationController
include RoutingHelper
def show
@webfinger_template = "#{webfinger_url}?resource={uri}"

View File

@@ -2,6 +2,8 @@
module WellKnown
class WebfingerController < ApplicationController
include RoutingHelper
def show
@account = Account.find_local!(username_from_resource)
@canonical_account_uri = @account.to_webfinger_s

View File

@@ -13,6 +13,10 @@ module ApplicationHelper
Setting.open_registrations
end
def open_deletion?
Setting.open_deletion
end
def add_rtl_body_class(other_classes)
other_classes = "#{other_classes} rtl" if [:ar, :fa, :he].include?(I18n.locale)
other_classes

View File

@@ -1,8 +1,8 @@
# frozen_string_literal: true
module StreamEntriesHelper
EMBEDDED_CONTROLLER = 'stream_entries'.freeze
EMBEDDED_ACTION = 'embed'.freeze
EMBEDDED_CONTROLLER = 'stream_entries'
EMBEDDED_ACTION = 'embed'
def display_name(account)
account.display_name.presence || account.username
@@ -47,12 +47,17 @@ module StreamEntriesHelper
end
end
def rtl?(text)
rtl_characters = /[\p{Hebrew}|\p{Arabic}|\p{Syriac}|\p{Thaana}|\p{Nko}]+/m.match(text)
def rtl_status?(status)
status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text))
end
if rtl_characters.present?
total_size = text.strip.size.to_f
rtl_size(rtl_characters.to_a) / total_size > 0.3
def rtl?(text)
text = simplified_text(text)
rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m)
if rtl_words.present?
total_size = text.size.to_f
rtl_size(rtl_words) / total_size > 0.3
else
false
end
@@ -60,8 +65,20 @@ module StreamEntriesHelper
private
def rtl_size(characters)
characters.reduce(0) { |acc, elem| acc + elem.size }.to_f
def simplified_text(text)
text.dup.tap do |new_text|
URI.extract(new_text).each do |url|
new_text.gsub!(url, '')
end
new_text.gsub!(Account::MENTION_RE, '')
new_text.gsub!(Tag::HASHTAG_RE, '')
new_text.gsub!(/\s+/, '')
end
end
def rtl_size(words)
words.reduce(0) { |acc, elem| acc + elem.size }.to_f
end
def embedded_view?

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -29,22 +29,6 @@ export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST';
export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS';
export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL';
export const ACCOUNT_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_TIMELINE_EXPAND_REQUEST';
export const ACCOUNT_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_TIMELINE_EXPAND_SUCCESS';
export const ACCOUNT_TIMELINE_EXPAND_FAIL = 'ACCOUNT_TIMELINE_EXPAND_FAIL';
export const ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST';
export const ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS';
export const ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL = 'ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL';
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST';
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS';
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL';
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
@@ -99,95 +83,6 @@ export function fetchAccount(id) {
};
};
export function fetchAccountTimeline(id, replace = false) {
return (dispatch, getState) => {
const ids = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List());
const newestId = ids.size > 0 ? ids.first() : null;
let params = {};
let skipLoading = false;
if (newestId !== null && !replace) {
params.since_id = newestId;
skipLoading = true;
}
dispatch(fetchAccountTimelineRequest(id, skipLoading));
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading, next));
}).catch(error => {
dispatch(fetchAccountTimelineFail(id, error, skipLoading));
});
};
};
export function fetchAccountMediaTimeline(id, replace = false) {
return (dispatch, getState) => {
const ids = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List());
const newestId = ids.size > 0 ? ids.first() : null;
let params = { only_media: 'true', limit: 12 };
let skipLoading = false;
if (newestId !== null && !replace) {
params.since_id = newestId;
skipLoading = true;
}
dispatch(fetchAccountMediaTimelineRequest(id, skipLoading));
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading, next));
}).catch(error => {
dispatch(fetchAccountMediaTimelineFail(id, error, skipLoading));
});
};
};
export function expandAccountTimeline(id) {
return (dispatch, getState) => {
const lastId = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()).last();
dispatch(expandAccountTimelineRequest(id));
api(getState).get(`/api/v1/accounts/${id}/statuses`, {
params: {
limit: 10,
max_id: lastId,
},
}).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandAccountTimelineSuccess(id, response.data, next));
}).catch(error => {
dispatch(expandAccountTimelineFail(id, error));
});
};
};
export function expandAccountMediaTimeline(id) {
return (dispatch, getState) => {
const lastId = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()).last();
dispatch(expandAccountMediaTimelineRequest(id));
api(getState).get(`/api/v1/accounts/${id}/statuses`, {
params: {
limit: 12,
only_media: 'true',
max_id: lastId,
},
}).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandAccountMediaTimelineSuccess(id, response.data, next));
}).catch(error => {
dispatch(expandAccountMediaTimelineFail(id, error));
});
};
};
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,
@@ -277,112 +172,6 @@ export function unfollowAccountFail(error) {
};
};
export function fetchAccountTimelineRequest(id, skipLoading) {
return {
type: ACCOUNT_TIMELINE_FETCH_REQUEST,
id,
skipLoading,
};
};
export function fetchAccountTimelineSuccess(id, statuses, replace, skipLoading, next) {
return {
type: ACCOUNT_TIMELINE_FETCH_SUCCESS,
id,
statuses,
replace,
skipLoading,
next,
};
};
export function fetchAccountTimelineFail(id, error, skipLoading) {
return {
type: ACCOUNT_TIMELINE_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: error.response.status === 404,
};
};
export function fetchAccountMediaTimelineRequest(id, skipLoading) {
return {
type: ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST,
id,
skipLoading,
};
};
export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading, next) {
return {
type: ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS,
id,
statuses,
replace,
skipLoading,
next,
};
};
export function fetchAccountMediaTimelineFail(id, error, skipLoading) {
return {
type: ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: error.response.status === 404,
};
};
export function expandAccountTimelineRequest(id) {
return {
type: ACCOUNT_TIMELINE_EXPAND_REQUEST,
id,
};
};
export function expandAccountTimelineSuccess(id, statuses, next) {
return {
type: ACCOUNT_TIMELINE_EXPAND_SUCCESS,
id,
statuses,
next,
};
};
export function expandAccountTimelineFail(id, error) {
return {
type: ACCOUNT_TIMELINE_EXPAND_FAIL,
id,
error,
};
};
export function expandAccountMediaTimelineRequest(id) {
return {
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST,
id,
};
};
export function expandAccountMediaTimelineSuccess(id, statuses, next) {
return {
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS,
id,
statuses,
next,
};
};
export function expandAccountMediaTimelineFail(id, error) {
return {
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL,
id,
error,
};
};
export function blockAccount(id) {
return (dispatch, getState) => {
dispatch(blockAccountRequest(id));

View File

@@ -0,0 +1,40 @@
import { saveSettings } from './settings';
export const COLUMN_ADD = 'COLUMN_ADD';
export const COLUMN_REMOVE = 'COLUMN_REMOVE';
export const COLUMN_MOVE = 'COLUMN_MOVE';
export function addColumn(id, params) {
return dispatch => {
dispatch({
type: COLUMN_ADD,
id,
params,
});
dispatch(saveSettings());
};
};
export function removeColumn(uuid) {
return dispatch => {
dispatch({
type: COLUMN_REMOVE,
uuid,
});
dispatch(saveSettings());
};
};
export function moveColumn(uuid, direction) {
return dispatch => {
dispatch({
type: COLUMN_MOVE,
uuid,
direction,
});
dispatch(saveSettings());
};
};

View File

@@ -124,25 +124,22 @@ export function refreshNotificationsFail(error, skipLoading) {
export function expandNotifications() {
return (dispatch, getState) => {
const url = getState().getIn(['notifications', 'next'], null);
const lastId = getState().getIn(['notifications', 'items']).last();
const items = getState().getIn(['notifications', 'items'], Immutable.List());
if (url === null || getState().getIn(['notifications', 'isLoading'])) {
if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
return;
}
dispatch(expandNotificationsRequest());
const params = {
max_id: lastId,
max_id: items.last().get('id'),
limit: 20,
exclude_types: excludeTypesFromSettings(getState()),
};
params.exclude_types = excludeTypesFromSettings(getState());
dispatch(expandNotificationsRequest());
api(getState).get(url, params).then(response => {
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
fetchRelatedRelationships(dispatch, response.data);
}).catch(error => {

View File

@@ -3,10 +3,14 @@ import axios from 'axios';
export const SETTING_CHANGE = 'SETTING_CHANGE';
export function changeSetting(key, value) {
return {
type: SETTING_CHANGE,
key,
value,
return dispatch => {
dispatch({
type: SETTING_CHANGE,
key,
value,
});
dispatch(saveSettings());
};
};

View File

@@ -56,91 +56,89 @@ export function deleteFromTimelines(id) {
};
};
export function refreshTimelineRequest(timeline, id, skipLoading) {
export function refreshTimelineRequest(timeline, skipLoading) {
return {
type: TIMELINE_REFRESH_REQUEST,
timeline,
id,
skipLoading,
};
};
export function refreshTimeline(timeline, id = null) {
export function refreshTimeline(timelineId, path, params = {}) {
return function (dispatch, getState) {
if (getState().getIn(['timelines', timeline, 'isLoading'])) {
const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
if (timeline.get('isLoading') || timeline.get('online')) {
return;
}
const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
const ids = timeline.get('items', Immutable.List());
const newestId = ids.size > 0 ? ids.first() : null;
let params = getState().getIn(['timelines', timeline, 'params'], {});
const path = getState().getIn(['timelines', timeline, 'path'])(id);
let skipLoading = false;
let skipLoading = timeline.get('loaded');
if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
if (id === null && getState().getIn(['timelines', timeline, 'online'])) {
// Skip refreshing when timeline is live anyway
return;
}
params = { ...params, since_id: newestId };
skipLoading = true;
} else if (getState().getIn(['timelines', timeline, 'loaded'])) {
skipLoading = true;
if (newestId !== null) {
params.since_id = newestId;
}
dispatch(refreshTimelineRequest(timeline, id, skipLoading));
dispatch(refreshTimelineRequest(timelineId, skipLoading));
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading, next ? next.uri : null));
dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null));
}).catch(error => {
dispatch(refreshTimelineFail(timeline, error, skipLoading));
dispatch(refreshTimelineFail(timelineId, error, skipLoading));
});
};
};
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export function refreshTimelineFail(timeline, error, skipLoading) {
return {
type: TIMELINE_REFRESH_FAIL,
timeline,
error,
skipLoading,
skipAlert: error.response.status === 404,
};
};
export function expandTimeline(timeline) {
export function expandTimeline(timelineId, path, params = {}) {
return (dispatch, getState) => {
if (getState().getIn(['timelines', timeline, 'isLoading'])) {
const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
const ids = timeline.get('items', Immutable.List());
if (timeline.get('isLoading') || ids.size === 0) {
return;
}
if (getState().getIn(['timelines', timeline, 'items']).size === 0) {
return;
}
params.max_id = ids.last();
params.limit = 10;
const path = getState().getIn(['timelines', timeline, 'path'])(getState().getIn(['timelines', timeline, 'id']));
const params = getState().getIn(['timelines', timeline, 'params'], {});
const lastId = getState().getIn(['timelines', timeline, 'items']).last();
dispatch(expandTimelineRequest(timelineId));
dispatch(expandTimelineRequest(timeline));
api(getState).get(path, {
params: {
...params,
max_id: lastId,
limit: 10,
},
}).then(response => {
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandTimelineSuccess(timeline, response.data, next ? next.uri : null));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandTimelineFail(timeline, error));
dispatch(expandTimelineFail(timelineId, error));
});
};
};
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export function expandTimelineRequest(timeline) {
return {
type: TIMELINE_EXPAND_REQUEST,

View File

@@ -55,11 +55,11 @@ class Account extends ImmutablePureComponent {
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />;
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}

View File

@@ -1,9 +1,10 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
class AttachmentList extends React.PureComponent {
class AttachmentList extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,

View File

@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
@@ -69,10 +70,6 @@ class AutosuggestTextarea extends ImmutablePureComponent {
this.props.onSuggestionsClearRequested();
}
// auto-resize textarea
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
this.props.onChange(e);
}
@@ -127,13 +124,7 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}
onBlur = () => {
// If we hide the suggestions immediately, then this will prevent the
// onClick for the suggestions themselves from firing.
// Setting a short window for that to take place before hiding the
// suggestions ensures that can't happen.
setTimeout(() => {
this.setState({ suggestionsHidden: true });
}, 100);
this.setState({ suggestionsHidden: true });
}
onSuggestionClick = (e) => {
@@ -160,10 +151,6 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}
}
reset () {
this.textarea.style.height = 'auto';
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
const { suggestionsHidden, selectedSuggestion } = this.state;
@@ -175,8 +162,8 @@ class AutosuggestTextarea extends ImmutablePureComponent {
return (
<div className='autosuggest-textarea'>
<textarea
ref={this.setTextarea}
<Textarea
inputRef={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
@@ -198,7 +185,8 @@ class AutosuggestTextarea extends ImmutablePureComponent {
key={suggestion}
data-index={suggestion}
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
onClick={this.onSuggestionClick}>
onMouseDown={this.onSuggestionClick}
>
<AutosuggestAccountContainer id={suggestion} />
</div>
))}

View File

@@ -9,7 +9,7 @@ class AvatarOverlay extends React.PureComponent {
};
render() {
const {staticSrc, overlaySrc} = this.props;
const { staticSrc, overlaySrc } = this.props;
const baseStyle = {
backgroundImage: `url(${staticSrc})`,
@@ -21,8 +21,8 @@ class AvatarOverlay extends React.PureComponent {
return (
<div className='account__avatar-overlay'>
<div className="account__avatar-overlay-base" style={baseStyle} />
<div className="account__avatar-overlay-overlay" style={overlayStyle} />
<div className='account__avatar-overlay-base' style={baseStyle} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} />
</div>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import scrollTop from '../scroll';
class Column extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
};
scrollTop () {
const scrollable = this.node.querySelector('.scrollable');
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = c => {
this.node = c;
}
render () {
const { children } = this.props;
return (
<div role='region' className='column' ref={this.setRef} onWheel={this.handleWheel}>
{children}
</div>
);
}
}
export default Column;

View File

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

View File

@@ -9,7 +9,8 @@ class ColumnBackButtonSlim extends React.PureComponent {
};
handleClick = () => {
this.context.router.push('/');
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
}
render () {

View File

@@ -0,0 +1,145 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
class ColumnHeader extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
multiColumn: PropTypes.bool,
showBackButton: PropTypes.bool,
children: PropTypes.node,
pinned: PropTypes.bool,
onPin: PropTypes.func,
onMove: PropTypes.func,
onClick: PropTypes.func,
};
state = {
collapsed: true,
animating: false,
};
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
}
handleTitleClick = () => {
this.props.onClick();
}
handleMoveLeft = () => {
this.props.onMove(-1);
}
handleMoveRight = () => {
this.props.onMove(1);
}
handleBackClick = () => {
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
render () {
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'active': active,
});
const buttonClassName = classNames('column-header', {
'active': active,
});
const collapsibleClassName = classNames('column-header__collapsible', {
'collapsed': collapsed,
'animating': animating,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'active': !collapsed,
});
let extraContent, pinButton, moveButtons, backButton, collapseButton;
if (children) {
extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
);
}
if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
</div>
);
} else if (multiColumn) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}
if (!pinned && (multiColumn || showBackButton)) {
backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
}
const collapsedContent = [
extraContent,
];
if (multiColumn) {
collapsedContent.push(moveButtons);
collapsedContent.push(pinButton);
}
if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
}
return (
<div className={wrapperClassName}>
<div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title}
<div className='column-header__buttons'>
{backButton}
{collapseButton}
</div>
</div>
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
<div>
{(!collapsed || animating) && collapsedContent}
</div>
</div>
</div>
);
}
}
export default ColumnHeader;

View File

@@ -17,7 +17,7 @@ class DropdownMenu extends React.PureComponent {
};
static defaultProps = {
ariaLabel: "Menu",
ariaLabel: 'Menu',
};
state = {
@@ -41,7 +41,7 @@ class DropdownMenu extends React.PureComponent {
action();
} else if (to) {
e.preventDefault();
this.context.router.push(to);
this.context.router.history.push(to);
}
this.dropdown.hide();
@@ -53,13 +53,13 @@ class DropdownMenu extends React.PureComponent {
renderItem = (item, i) => {
if (item === null) {
return <li key={ 'sep' + i } className='dropdown__sep' />;
return <li key={`sep-${i}`} className='dropdown__sep' />;
}
const { text, action, href = '#' } = item;
return (
<li className='dropdown__content-list-item' key={ text + i }>
<li className='dropdown__content-list-item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
{text}
</a>
@@ -70,7 +70,7 @@ class DropdownMenu extends React.PureComponent {
render () {
const { icon, items, size, direction, ariaLabel } = this.props;
const { expanded } = this.state;
const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right";
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const dropdownItems = expanded && (
<ul className='dropdown__content-list'>
@@ -81,7 +81,7 @@ class DropdownMenu extends React.PureComponent {
return (
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
<i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} />
<i className={`fa fa-fw fa-${icon} dropdown__icon`} aria-hidden />
</DropdownTrigger>
<DropdownContent className={directionClass}>

View File

@@ -76,7 +76,8 @@ class IconButton extends React.PureComponent {
title={this.props.title}
className={classes.join(' ')}
onClick={this.handleClick}
style={style}>
style={style}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
</button>
}

View File

@@ -2,14 +2,20 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const LoadMore = ({ onClick }) => (
<button className='load-more' onClick={onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);
class LoadMore extends React.PureComponent {
LoadMore.propTypes = {
onClick: PropTypes.func,
};
static propTypes = {
onClick: PropTypes.func,
}
render() {
return (
<button className='load-more' onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);
}
}
export default LoadMore;

View File

@@ -86,7 +86,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'image') {
thumbnail = (
<a
<a // eslint-disable-line jsx-a11y/anchor-has-content
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || attachment.get('url')}
onClick={this.handleClick}
@@ -105,8 +105,8 @@ class Item extends React.PureComponent {
src={attachment.get('url')}
onClick={this.handleClick}
autoPlay={autoPlay}
loop={true}
muted={true}
loop
muted
/>
<span className='media-gallery__gifv__label'>GIF</span>

View File

@@ -17,7 +17,7 @@ class Permalink extends React.PureComponent {
handleClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.push(this.props.to);
this.context.router.history.push(this.props.to);
}
}

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