Compare commits

...

176 Commits

Author SHA1 Message Date
kibigo!
2f2467ce8e Add option to remove DMs from home (#126) 2017-12-06 17:50:27 -08:00
kibigo!
061211a1e3 Fix common packs when other pack also there 2017-12-06 15:34:19 -08:00
David Yip
161d286df2 Use settings pack for InvitesController (#229) 2017-12-06 17:20:04 -06:00
beatrix
81b0145759 Merge pull request #229 from glitch-soc/glitch-theme
Advanced Next-Level Flavours And Skins For Mastodon™
2017-12-06 17:44:07 -05:00
David Yip
017fc81caf Prepend check_enabled_deletion to Settings::DeletesController (#229)
The specs for Settings::DeletesController include an example that
sets Settings.open_deletion to false and expects the "if deletion is not
available, redirect to root" logic to run.  However, this spec does not
set up a user, which means that the spec (intentionally or otherwise)
expects this redirection to work with unauthenticated access.

We should preserve that behavior.  To do so, we prepend the deletion
check to the action chain set up by Settings::BaseController, so that
said check occurs before the authenticate_user! check.
2017-12-06 16:19:43 -06:00
David Yip
f1cbea77a4 Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2017-12-06 15:40:59 -06:00
beatrix
21e28a5caa Merge pull request #238 from ncls7615/config-locales-ja
i18n: Update Japanese translation
2017-12-06 16:24:16 -05:00
NCLS
fde91988ab Update ja.yml 2017-12-07 06:10:15 +09:00
Jenkins
8ca91cef45 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-12-06 20:17:13 +00:00
Marcin Mikołajczak
a0047fdca0 i18n: 🇵🇱 (#5903)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-12-07 05:04:35 +09:00
Lynx Kotoura
a20509b41e Fix list editor modal on narrow devices (#5904) 2017-12-06 19:48:48 +01:00
Lynx Kotoura
281c577cf8 Fix extra space between status text and username (#5902) 2017-12-06 17:53:25 +01:00
Eugen Rochko
f9a0d8f2b9 Bump version to 2.1.0rc1 (#5834) 2017-12-06 16:13:32 +01:00
aschmitz
4de211b80a Break out nested relationship API keys (#5887)
* Break out nested relationship API keys

This closes #5856 by restoring the existing behavior of the `muting`
and `following` keys (returning booleans rather than truthy or false).
It adds `showing_reblogs` and `muting_notifications` keys:

* `showing_reblogs` returns true if:
  1. You've requested to follow the user, with reblogs shown, or
  2. You are following the user, with reblogs shown.
* `muting_notifications` returns true if you have muted the user and
  their notifications as well.

* Rubocop fix

* Fix pulling reblog/mute status from relationships

I could swear this had passed tests before, but apparently not.
Works now.

* More test fixes

Really, you'd expect this to be more straightforward.
2017-12-06 16:10:54 +01:00
Yamagishi Kazutoshi
063a1c2a8b Fix emoji picker scrollbar (regression from #5046) (#5901) 2017-12-06 12:18:27 +01:00
Yamagishi Kazutoshi
a9ca5ce920 Handle submit event of new list form (#5895) 2017-12-06 12:18:10 +01:00
Yamagishi Kazutoshi
d7a17b5e8b Search only from followees (#5897) 2017-12-06 11:44:23 +01:00
Yamagishi Kazutoshi
34e2a06de0 Update Yarn to version 1.3.2 (#5900) 2017-12-06 11:42:51 +01:00
Yamagishi Kazutoshi
4c1a02fa73 Yarn install from Debian package repository (#5899) 2017-12-06 11:42:30 +01:00
Yamagishi Kazutoshi
b21db9bbde Using double splat operator (#5859) 2017-12-06 11:41:57 +01:00
Eugen Rochko
42bcbd36b7 Remove rabl dependency (#5894)
* Remove rabl dependency

* Replicate old Oj configuration
2017-12-06 15:04:49 +09:00
mayaeh
0393a64a90 Update Japanese translations. (#5893) 2017-12-06 03:22:11 +01:00
Eugen Rochko
d68868ca14 Lists redis clean-up (#5886)
* When list is deleted, remove feed from redis

* Clean up list feeds of inactive users
2017-12-05 23:20:27 +01:00
Eugen Rochko
e20895f251 Add list of lists component to web UI (#5811)
* Add list of lists component to web UI

* Add list adding

* Add list removing

* List editor modal

* Add API account search limited by following=true relation

* Rework list editor modal

* Remove mandatory pagination of GET /api/v1/lists/:id/accounts

* Adjust search input placeholder

* Fix rspec (#5890)

* i18n: (zh-CN) Add missing translations for #5811 (#5891)

* i18n: (zh-CN) yarn manage:translations -- zh-CN

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

* Fix some issues

- Display loading/missing state for list timelines
- Order lists alphabetically in overview
- Fix async list editor reset
- Redirect to /lists after deleting unpinned list
- Redirect to / after pinning a list

* Remove dead list columns when a list is deleted or fetch returns 404
2017-12-05 23:02:27 +01:00
SerCom_KC
12cea76634 Update Chinese (Simplified) translations for version 2.1.0rc1 (#5849)
* i18n: (zh-CN) Add missing translations for multiple PRs.
Related PRs: #5838 #5762 #5835 #5837 #5832 #5823 #5814 #5757

* i18n: (zh-CN) Fix translation for #5823 / #5835

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Change `发送者` to `作者`

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

* i18n: (zh-CN) Add missing translation for #5874

* i18n: (zh-CN) Improve translations for keyboard shortcuts
2017-12-06 00:46:04 +09:00
Neetshin
b4bc594c5a Add aria-autocomplete='list' in Textaria (#5889)
ref: https://www.w3.org/TR/wai-aria-1.1/#aria-autocomplete
2017-12-05 11:32:00 +01:00
kibigo!
5bed0f10ed Various fixes 2017-12-04 21:58:10 -08:00
David Yip
a807e3b71b current_theme -> current_flavour (#229) 2017-12-04 11:35:35 -06:00
David Yip
d9800a5647 Merge branch 'gs-master' into glitch-theme 2017-12-04 11:07:01 -06:00
Jenkins
f2f2f10320 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-12-04 12:17:13 +00:00
Neetshin
82884ac5c4 Update mutes.js (#5882) 2017-12-04 20:52:14 +09:00
kibigo!
1c74ede69e Fixed a name + localizations 2017-12-04 00:19:39 -08:00
kibigo!
7c75702d05 Fixed image require 2017-12-03 23:52:13 -08:00
kibigo!
bc4fa6b198 Rename themes -> flavours ? ? 2017-12-03 23:26:40 -08:00
kibigo!
d216547382 Fixed typos 2017-12-03 22:30:45 -08:00
Jenkins
757f52ff2e Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-12-03 21:17:13 +00:00
Lynx Kotoura
886829e96c Add keyboard shortcuts link to getting-started navigation (#5874)
* Add keyboard shortcuts link to getting-started navigation

* i18n: 🇵🇱

* yarn manage:translations
2017-12-03 21:29:51 +01:00
Lynx Kotoura
62a94ebed4 Add back button to keyboard shortcuts legend column (#5872) 2017-12-03 21:29:41 +01:00
Yamagishi Kazutoshi
ac17309faf Update dependencies for Node.js (2017-12-03) (#5876)
* Update babel-plugin-lodash to version 3.3.2

* Update enzyme to version 3.2.0

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

* Update intersection-observer to version 0.5.0

* Update intl-messageformat to version 2.2.0

* Update node-sass to version 4.7.2

* Update postcss-loader to version 2.0.9

* Update React to version 16.2.0

* Update react-textarea-autosize to version 5.2.1

* Update stringz to version 0.3.0

* Update webpack to version 3.9.1

* Update webpack-bundle-analyzer to version 2.9.1

* Update webpack-dev-server to version 2.9.5

* Update webpack-merge to version 4.1.1

* Update fsevents to version 1.1.3

* yarn upgrade
2017-12-03 16:55:53 +01:00
Yamagishi Kazutoshi
dd23ae031f Update dependencies for Ruby (2017-12-03) (#5878)
* Update active_model_serializers to version 0.10.7

* Update capistrano-rails to version 1.3.1

* Update capistrano-rbenv to version 2.1.3

* Update capybara to version 2.16.1

* Update devise-two-factor to version 3.0.2

* Update i18n-tasks to version 0.8.19

* Update ox to version 2.8.2

* Update parallel_tests to version 2.19.0

* Update puma to version 3.11.0

* Update redis-namespace to version 1.6.0

* Update rspec-rails to version 3.7.2

* Update scss_lint to version 0.56.0

* Update webmock to version 3.1.1

* Update webpush to version 0.3.3

* bundle update
2017-12-03 16:55:27 +01:00
Jenkins
933eafdcd3 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-12-02 14:17:11 +00:00
mayaeh
51f2eca887 Add Japanese translations for invite filter. (#5869) 2017-12-02 15:09:56 +01:00
cormo
bdf6d0a684 Remove redundant import statement in SCSS (#5864)
* Remove redudant import statement in SCSS

* Fix wrong chmod
2017-12-02 08:42:20 +09:00
Marcin Mikołajczak
b15482ce71 i18n: 🇵🇱 (#5865) 2017-12-02 03:19:30 +09:00
Yamagishi Kazutoshi
74320971e2 Add invite filter (#5862) 2017-12-01 16:40:02 +01:00
Yamagishi Kazutoshi
eee3b32b77 Fix invites form path (#5861) 2017-12-01 12:26:57 +01:00
Yamagishi Kazutoshi
df03042a6e Allow admin to deactivate invite created by users (#5860) 2017-12-01 12:26:19 +01:00
kibigo!
541fe9b110 Skins support 2017-11-30 19:29:47 -08:00
mayaeh
9927df83ad Update Japanese translations for KeyboardShortcuts. (#5858) 2017-12-01 01:35:47 +01:00
Jenkins
ad46bc9772 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-30 03:17:12 +00:00
Eugen Rochko
4c6b5dbe96 Add semi-support for Video/Image objects in ActivityPub (#5848)
* Add semi-support for Video/Image objects in ActivityPub

Video and Image objects will create corresponding status records
with manually crafted text contents (title + URL)

* Extract html-url-finding logic into JsonLdHelper

* Fallback to id when url missing, extract supported object types
2017-11-30 04:06:20 +01:00
Eugen Rochko
85e97ecab6 Fix too many forwards (#5854)
* Avoid sending explicit Undo->Announce when original deleted

* Do not forward a reply back to the server that sent it

* Deduplicate inboxes of rebloggers' followers for delete forwarding

* Adjust test

* Fix wrong class, bad SQL, wrong variable, outdated comment
2017-11-30 03:50:05 +01:00
takayamaki
dc1ebd45a3 add index on stream_entries table (#5793) 2017-11-30 03:35:54 +01:00
Jenkins
d020ed1e05 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-29 22:17:13 +00:00
Mayu Laierlence
f0d4c7d7ab Fix Korean translation (#5853)
"어플리케이션" -> "애플리케이션"
2017-11-30 07:16:29 +09:00
Marcin Mikołajczak
82ab9736d5 i18n: 🇵🇱 (#5851)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-29 20:51:24 +09:00
mayaeh
a62039df27 Update Japanese translations. (#5844) 2017-11-29 15:41:21 +09:00
David Yip
84840e8d8c Merge pull request #232 from glitch-soc/hotkeys-glitch
Implement status hotkeys + spoiler expanding
2017-11-28 17:34:03 -06:00
David Yip
3614912be2 Normalize pl translation. 2017-11-28 12:17:36 -06:00
David Yip
95c270f5b1 Merge remote-tracking branch 'origin/master' into gs-master 2017-11-28 11:45:13 -06:00
Yamagishi Kazutoshi
15fab79cfa Show "expired" in invite index (#5838)
* Show "expired" in invite index

* "Force expire" -> "Deactivate"
2017-11-28 15:41:02 +01:00
aschmitz
eeaec39888 Allow hiding of reblogs from followed users (#5762)
* Allow hiding of reblogs from followed users

This adds a new entry to the account menu to allow users to hide
future reblogs from a user (and then if they've done that, to show
future reblogs instead).

This does not remove or add historical reblogs from/to the user's
timeline; it only affects new statuses.

The API for this operates by sending a "reblogs" key to the follow
endpoint. If this is sent when starting a new follow, it will be
respected from the beginning of the follow relationship (even if
the follow request must be approved by the followee). If this is
sent when a follow relationship already exists, it will simply
update the existing follow relationship. As with the notification
muting, this will now return an object ({reblogs: [true|false]}) or
false for each follow relationship when requesting relationship
information for an account. This should cause few issues due to an
object being truthy in many languages, but some modifications may
need to be made in pickier languages.

Database changes: adds a show_reblogs column (default true,
non-nullable) to the follows and follow_requests tables. Because
these are non-nullable, we use the existing MigrationHelpers to
perform this change without locking those tables, although the
tables are likely to be small anyway.

Tests included.

See also <https://github.com/glitch-soc/mastodon/pull/212>.

* Rubocop fixes

* Code review changes

* Test fixes

This patchset closes #648 and resolves #3271.

* Rubocop fix

* Revert reblogs defaulting in argument, fix tests

It turns out we needed this for the same reason we needed it in muting:
if nil gets passed in somehow (most usually by an API client not passing
any value), we need to detect and handle it.

We could specify a default in the parameter and then also catch nil, but
there's no great reason to duplicate the default value.
2017-11-28 15:00:35 +01:00
ThibG
b8efb5daed Fix handling of temporary failures in ProcessMentionsService (#5842)
* Add test for temporary account resolving failures in ProcessMentionsService

* Fix processing of mentions to already-known remote accounts on temporary failures
2017-11-28 15:00:22 +01:00
Yamagishi Kazutoshi
2b3b44ebbc Refactor KeyboardShortcuts component (#5835) 2017-11-28 14:32:27 +01:00
nullkal
1b57d4dd3a Fix account migration feature (#5837)
* Make removable account migration

* Fix error during update of account migration setting

* Add notice when update account migration setting
2017-11-28 14:31:23 +01:00
Marcin Mikołajczak
d937a59997 i18n: 🇵🇱 (#5841)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-28 19:30:18 +09:00
David Yip
7463d80ff4 Merge remote-tracking branch 'tootsuite/master' 2017-11-27 22:33:26 -06:00
David Yip
8bad6bdd00 Normalize config/locales/en.yml. 2017-11-27 22:32:31 -06:00
kibigo!
6a48efe16c Adds spoiler hotkey support to detailed statuses 2017-11-27 14:05:03 -08:00
Eugen Rochko
706e534455 Add UI for setting up account migration (#5832) 2017-11-27 22:47:06 +01:00
kibigo!
f3c3df62ab Implement status hotkeys + spoiler expanding 2017-11-27 13:30:01 -08:00
Joshua Wood
ff78c1177a Add Keyboard Shortcuts Legend (#5823)
* Add Keyboard Shortcuts Legend

Adds a "Keyboard Shortcuts" legend (displayed in the rightmost column)
which is toggled via a new "?" hotkey. When subsequently pressed from
the Keyboard Shortcuts legend, "?" will navigate back to the previous
location.

* Add hidden table headings.

Makes the headings available for accessibility but hides them visually.
2017-11-27 21:31:58 +01:00
Jenkins
63d47e04c4 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-27 20:17:12 +00:00
mayaeh
c6b7c77229 i18n: Update ja translation for consumable invites. (#5829)
* Add Japanese translation for consumable invites.

* Add Japanese translation.

* Update Japanese translation.
2017-11-27 20:23:14 +01:00
Sorin Davidoi
e20258a2e5 chore(yarn): Upgrade react-swipeable-views (smoother swiping) (#5830) 2017-11-27 20:22:47 +01:00
unarist
7fb850e987 Merge indexes for reblog on statuses table (#5831)
We added an index for `[account_id, reblog_of_id]`, but we already have a similar index for `reblog_of_id`. Those index will be bigger according to statuses count. For example, `reblog_of_id` index uses 800MB for 10GB statuses table.

So this patch swaps indexed columns like `[reblog_of_id, account_id]`, then it will covers both usage with single index.

Since those index creation may take a while, I've also disabled previous index creation.
2017-11-27 20:22:27 +01:00
David Yip
e77c3996a5 Merge remote-tracking branch 'origin/master' into gs-master 2017-11-27 11:29:04 -06:00
Yamagishi Kazutoshi
1c5b0e3334 Use account.username when display_name is empty (#5828) 2017-11-27 16:09:52 +01:00
Eugen Rochko
740f8a95a9 Add consumable invites (#5814)
* Add consumable invites

* Add UI for generating invite codes

* Add tests

* Display max uses and expiration in invites table, delete invite

* Remove unused column and redundant validator

- Default follows not used, probably bad idea
- InviteCodeValidator is redundant because RegistrationsController
  checks invite code validity

* Add admin setting to disable invites

* Add admin UI for invites, configurable role for invite creation

- Admin UI that lists everyone's invites, always available
- Admin setting min_invite_role to control who can invite people
- Non-admin invite UI only visible if users are allowed to

* Do not remove invites from database, expire them instantly
2017-11-27 16:07:59 +01:00
Jenkins
5de42665d7 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-27 05:17:11 +00:00
Alda Marteau-Hardi
0ea4478b68 Use account.display_name for og:title single toot pages (#5821) 2017-11-27 05:31:26 +01:00
Akihiko Odaki
fd87e5a53b Do not filter the status collection after muting and blocking (#5815)
Filtering the status collection wipes out even the profiles of muted and
blocked accounts. However, the behavior is inconsistent with the server-
side behavior.
2017-11-26 01:45:17 +01:00
SerCom_KC
57fe4102ea i18n: (zh-CN) Add translations for #5746 & #5750 (#5816) 2017-11-26 02:38:40 +09:00
Akihiko Odaki
bf7757cbbc Allow to open a modal for embedded photo (#5777) 2017-11-25 15:41:45 +01:00
Akihiko Odaki
1266c66f79 Rename ariaLabel property of Dropdown to title (#5813)
DropdownMenu has ariaLabel property, but it is actually applied to title
property of IconButton. Keep it consistent.
2017-11-25 15:41:08 +01:00
spla
d07983b56d Updated Catalan strings (#5801)
* Updated Catalan strings

* Update ca.yml

* Update ca.yml

* Update ca.yml

* Update ca.yml

* Update ca.yml

* Update ca.yml

* Update ca.yml

* Update simple_form.ca.yml

* Update simple_form.ca.yml

* Update simple_form.ca.yml

* bundle exec i18n-tasks

* Update ca.json

* Update simple_form.ca.yml
2017-11-25 23:22:59 +09:00
Jenkins
86f4f8e158 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-25 05:17:15 +00:00
Yamagishi Kazutoshi
662b8eefe8 Change navigation link for moderators (#5812) 2017-11-25 05:53:54 +01:00
Yamagishi Kazutoshi
520d147803 Add Japanese translations (#5810)
* yarn manage:translations

* Add Japanese translation for #5087

* Add Japanese translation for #5616

* Add Japanese translation for #5746

* Add Japanese translation for #5750
2017-11-25 03:39:57 +01:00
William Pitcock
32987004c9 status: preserve visibility attribute when reblogging (infoleak fix) (#5789)
this should fix *all* remaining visibility-related mastodon ostatus infoleaks.
thanks to @csaurus@gnusocial.de for pointing out the infoleak.
2017-11-25 01:36:08 +01:00
Eugen Rochko
31ac5f0e00 Display list column (#5750) 2017-11-25 00:35:37 +01:00
abcang
269a445c0b Fix unnecessary order (#5807) 2017-11-24 18:41:07 +01:00
sdukhovni
2b51b4094c Don't remove originals of boosted toots from timeline (#5479)
* Don't remove originals of boosted toots from timeline

* Remove unused argument to updateTimeline
2017-11-24 14:43:53 +01:00
ysksn
1104ac35d3 Add tests for Streamable (#5771) 2017-11-24 14:42:57 +01:00
abcang
a78f66c069 Add index of account and reblog to statuses (#5785) 2017-11-24 14:42:09 +01:00
ysksn
8c0e77d688 Add tests for AccountRelationshipsPresenter (#5805) 2017-11-24 14:41:04 +01:00
mayaeh
7a45d382ea i18n: Update ja translations (#5804)
* Add Japanese translations.

* Update Japanese translations.

* Fix japanese translation.
2017-11-24 13:14:31 +01:00
Akihiko Odaki
5a551b530a Do not require onClose property in NavigationBar (#5802)
NavigationBar can be used as mock as it is in OnboardingModal. In such a
case, onClose property is not required.
2017-11-24 13:13:17 +01:00
Jenkins
167fe2ab08 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-24 01:17:20 +00:00
Eugen Rochko
e84fecb7e9 Add logging of admin actions (#5757)
* Add logging of admin actions

* Update brakeman whitelist

* Log creates, updates and destroys with history of changes

* i18n: Update Polish translation (#5782)

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

* Split admin navigation into moderation and administration

* Redesign audit log page

* 🇵🇱 (#5795)

* Add color coding to audit log

* Change dismiss->resolve, log all outcomes of report as resolve

* Update terminology (e-mail blacklist) (#5796)

* Update terminology (e-mail blacklist)

imho looks better

* Update en.yml

* Fix code style issues

* i18n-tasks normalize
2017-11-24 02:05:53 +01:00
Jenkins
faad820458 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-23 21:17:14 +00:00
Devon Blandin
801eee0ff3 Disable Code Climate maintainability checks (#5798)
- Migrate `.codeclimate.yml` to version 2 structure
- Disable Code Climate [maintainability checks][]

https://docs.codeclimate.com/docs/advanced-configuration

[maintainability checks]: http://blog.codeclimate.com/blog/2017/10/12/10-point-technical-debt-assessment

This commit upgrades the Code Climate configuration file and disables
the new maintainability checks.

Once this PR is merged, we can experiment with the new checks via pull
requests (the PR will report new issues) or via the [Code Climate
CLI][cli], which [now also supports the new maintainability
checks][cli-support].

[cli]: https://github.com/codeclimate/codeclimate
[cli-support]: https://codeclimate.com/changelog/5a0e488824cfa902a300091c
2017-11-23 21:38:17 +01:00
Jenkins
41c71565c2 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-23 02:17:12 +00:00
SerCom_KC
bc4a726c24 i18n: (zh-CN) Add translations for #5746 & #5735 (#5764) 2017-11-23 02:49:21 +01:00
kibigo!
2585649b20 Styling fixes 2017-11-21 19:00:07 -08:00
kibigo!
12c0011fee Update statuses on hidden toggle (Fixes #227) 2017-11-21 18:27:34 -08:00
kibigo!
8812bab687 Minor fixes 2017-11-21 18:17:38 -08:00
Jenkins
2a5af9c10c Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-21 17:17:11 +00:00
Marcin Mikołajczak
3c83b7e06e i18n: Update Polish translation - front-end added (#231)
* i18n: Update Polish translation

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

* i18n: Update Polish translation

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-21 17:53:13 +01:00
mayaeh
fc2155019b Add Japanese translations. (#5769) 2017-11-21 17:11:33 +01:00
beatrix
b7a7eb84dd update code of conduct email address 2017-11-21 11:01:28 -05:00
Marcin Mikołajczak
1a07b83c69 i18n: Update Polish translation - back-end (#230)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-21 16:36:22 +01:00
Joan Montané
53b7b81b43 Update and fix Catalan translation (#5773)
* Update activerecord.ca.yml

* Update ca.yml

* Update devise.ca.yml

* Update doorkeeper.ca.yml

* Update simple_form.ca.yml

* fix syntax error for ca translation

* fix syntax errors in ca translations

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-11-21 23:17:28 +09:00
kibigo!
bdbbd06dad Finalized theme loading and stuff 2017-11-20 22:13:37 -08:00
Jenkins
8f3e5f6128 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-21 05:17:12 +00:00
ysksn
6f609dc4b4 Add tests for Remotable (#5768) 2017-11-21 13:59:36 +09:00
abcang
3dce6cbbd7 Fixed duplicating URL of photo type of oEmbed (#5763) 2017-11-20 20:45:54 +01:00
Jenkins
a36a2c1796 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-19 15:17:12 +00:00
abcang
2bcc81700c Fix NoMethodError at ActivityPub::FetchRemoteStatusService (#5753) 2017-11-19 15:33:15 +01:00
abcang
53e95c4efc Fix N+1 at notification (#5752) 2017-11-19 15:32:48 +01:00
David Yip
e45cb0837b Update .following_map examples with show-reblogs information. 2017-11-19 02:56:45 -06:00
David Yip
d083f7741a Merge remote-tracking branch 'tootsuite/master' 2017-11-19 01:30:29 -06:00
mayaeh
08deec4c84 i18n: Update ja translation for moderator badge. (#5731)
* Update Japanese translation for moderator badge.

* Follows English notation
2017-11-19 04:16:56 +01:00
ysksn
2590aac863 Add tests for AccountInteractions (#5751)
* Add tests for class methods of AccountInteractions

* Add tests for instance methods of AccountInteractions
2017-11-19 04:16:14 +01:00
Lynx Kotoura
3d1d3d9a20 Remove translations of default theme (#5741) 2017-11-19 04:15:50 +01:00
ysksn
8f638a2bf2 Add a test for Settings::Extend#settings (#5720) 2017-11-19 04:15:32 +01:00
ysksn
9d9b1aff1e Add tests for Status#title (#5718) 2017-11-19 04:15:17 +01:00
Yamagishi Kazutoshi
bfdcf76a64 Add “staff” badge to admin user pages (#5735)
* Allow staff badge to change from setting to hidden

* i18n: Update Polish translation
2017-11-19 04:13:57 +01:00
beatrix
bcda3f85ce Merge pull request #226 from glitch-soc/glitch-theme
Glitch/Vanilla themes
2017-11-18 20:32:17 -05:00
kibigo!
321fa41930 Merge remote-tracking branch 'cybre/theme_win95' into glitch-theme 2017-11-18 15:18:00 -08:00
kibigo!
a9ed857171 WIP static themeing 2017-11-18 15:12:52 -08:00
Marcin Mikołajczak
b380e9d2cb i18n: Update Polish translation (#5749) 2017-11-18 21:43:10 +01:00
kibigo!
92cc79be72 Enabled vanilla thmee 2017-11-18 11:06:04 -08:00
kibigo!
08a01dd037 Public pack fix 2017-11-18 11:05:39 -08:00
kibigo!
672ace5a20 Media styling 2017-11-18 11:05:23 -08:00
kibigo!
f51f7b0e06 Font styles are now packagified 2017-11-18 11:05:00 -08:00
Eugen Rochko
58cede4808 Profile redirect notes (#5746)
* Serialize moved accounts into REST and ActivityPub APIs

* Parse federated moved accounts from ActivityPub

* Add note about moved accounts to public profiles

* Add moved account message to web UI

* Fix code style issues
2017-11-18 19:39:02 +01:00
beatrix
dec960c828 Merge pull request #208 from yipdw/keyword-mute
Run keyword mutes on hashtags
2017-11-18 12:06:40 -05:00
Jenkins
eed50514be Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-18 06:17:14 +00:00
SerCom_KC
6be72a3ec6 Updating Chinese (Simplified) translations (#5725)
* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Change `静音` to `隐藏`

* i18n: (zh-CN) Add translations for #5087 & #5669

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Improve translations

* i18n: (zh-CN) Add missing translation for #5728
2017-11-18 14:35:11 +09:00
Baptiste Gelez
2864e5e077 Make it possible to bind streming service to 0.0.0.0 (#5744) 2017-11-18 04:44:19 +01:00
kibigo!
a618df7998 Sets default theme to glitch 2017-11-17 19:32:04 -08:00
kibigo!
fc2c8b50dd Removed glitch tests 2017-11-17 19:29:17 -08:00
kibigo!
e19fc6a9f8 Restore vanilla components 2017-11-17 19:29:16 -08:00
kibigo!
45c44989c8 Forking glitch theme 2017-11-17 19:29:16 -08:00
beatrix
5a9982b425 Merge pull request #223 from glitch-soc/glitchsoc/feature/configurable-status-size
Make character limit configurable (from tootsuite/mastodon#5697)
2017-11-17 19:02:40 -05:00
David Yip
284e2cde81 Merge pull request #224 from yipdw/merge-upstream
Merge upstream (tootsuite/mastodon#5703)
2017-11-17 17:58:13 -06:00
David Yip
130aa90d55 Update annotations on Follow, FollowRequest, and Mute.
Follow and FollowRequest had conflicts in their schema annotations, so I
ran latest migrations and let annotate_models fix them up.
2017-11-17 17:41:15 -06:00
David Yip
1ab12ba38e Merge remote-tracking branch 'origin/master' into merge-upstream 2017-11-17 17:40:00 -06:00
Eugen Rochko
24cafd73a2 Lists (#5703)
* Add structure for lists

* Add list timeline streaming API

* Add list APIs, bind list-account relation to follow relation

* Add API for adding/removing accounts from lists

* Add pagination to lists API

* Add pagination to list accounts API

* Adjust scopes for new APIs

- Creating and modifying lists merely requires "write" scope
- Fetching information about lists merely requires "read" scope

* Add test for wrong user context on list timeline

* Clean up tests
2017-11-18 00:16:48 +01:00
David Yip
6f8ccbfcdf Merge pull request #222 from yipdw/merge/tootsuite/master
Merge upstream (e28b33c.. 94c5a11)
2017-11-17 14:35:16 -06:00
David Yip
dc16d73bf5 Merge remote-tracking branch 'lambadalambda/feature/configurable-status-size' into glitchsoc/feature/configurable-status-size 2017-11-17 14:28:16 -06:00
beatrix
b006bb82af Merge pull request #221 from glitch-soc/local-only-policy
Update StatusPolicy to check current_account for local_only? toots.
2017-11-17 15:17:49 -05:00
Jenkins
94c5a11cda Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2017-11-17 20:17:12 +00:00
David Yip
45f18b8f49 Merge pull request #218 from glitch-soc/themed-prefetching
Themed preloading
2017-11-17 14:01:23 -06:00
Lynx Kotoura
4a2fc2d444 Increase contrast of Admin badges (#5740)
To comply WCAG 2.0 guidelines on contrast ratio.
2017-11-17 19:59:53 +01:00
Clworld
e1ebf36352 Fix regression #5467: fixes emoji copy (#5739) 2017-11-17 18:22:31 +01:00
Alex Coto
ae6dd08121 Change libinconv download URL to HTTPS (#5738) 2017-11-17 18:21:05 +01:00
David Yip
f6355f6ffb Update StatusPolicy to check current_account for local_only? toots.
StatusPolicy#account was renamed to StatusPolicy#current_account in
upstream.  This commit renames the local-only changes to match and
augments the #show? policy spec with what we expect for local-only
toots.
2017-11-17 09:07:21 -06:00
unarist
e28b33c89c Move "Permissions" row into main table in /admin/accounts/:id (#5734) 2017-11-17 15:18:20 +01:00
David Yip
2a386ad88d Add trailing comma in mapStateToProps
Consistency, eslint, etc.
2017-11-17 00:24:22 -06:00
David Yip
fbf76feb37 Merge pull request #219 from glitch-soc/no-spin
Remove 'spin' theme
2017-11-17 00:14:24 -06:00
David Yip
392945f9a3 Throw layout, isWide, navbarUnder back into mapStateToProps.
This gets wide view, auto layout, and navbar-under-mobile working again.
2017-11-17 00:11:01 -06:00
kibigo!
4dd19054d6 Removes spin theme 2017-11-16 21:54:12 -08:00
David Yip
c2a92dffc9 Add some examples for Glitch::KeywordMute::TagMatcher. #208. 2017-11-15 23:31:49 -06:00
David Yip
08652baab0 Replace =~ with #matches?. #208.
=~ made sense when we were passing it through to a regex, but we're no
longer doing that: TagMatcher looks at individual tags and returns a
value that *looks* like what you get out of #=~ but really isn't that
meaningful.  Probably a good idea to not subvert convention like this
and instead use a name with guessable intent.
2017-11-15 18:27:25 -06:00
David Yip
8fc54890e5 Prefix cache keys with the matcher type. #208.
We already know about one regex limitation, which is that they cannot
segment words in e.g. Japanese, Chinese, or Thai.  It may also end up
that regex matching is too slow compared to other methods.

However, the regex is an implementation detail.  We still want the
ability to switch between "occurs anywhere" and "match whole word", and
caching the matcher result is likely to still be important (since the
matcher itself won't change nearly as often as status ingress rate).
Therefore, we ought to be able to change the cache keys to reflect a
change of data structure.

(Old cache keys expire within minutes, so they shouldn't be too big of
an issue.  Old cache keys could also be explicitly removed by an
instance administrator.)
2017-11-15 18:12:31 -06:00
David Yip
cb4ef24ac9 Match keyword mute filter on hashtags. #208.
It is reasonable to expect someone to enter #foo to mute hashtag #foo.
However, tags are recorded on statuses without the preceding #.
To adjust for this, we build a separate tag matcher and use
Tag::HASHTAG_RE to extract a hashtag from the hashtag syntax.
2017-11-15 18:12:29 -06:00
David Yip
431503bae2 Also run the keyword matcher on a status' tags. #208. 2017-11-15 18:12:26 -06:00
Lain Iwakura
ca5440b93d Rename MAX_CHARS to MAX_TOOT_CHARS to be more specific. 2017-11-14 17:56:38 +01:00
Lain Iwakura
dae8916544 Code style fixes. 2017-11-14 16:54:04 +01:00
Lain Iwakura
d11b1a1aa7 Handle character limit in initial state in frontend. 2017-11-14 16:24:10 +01:00
Lain Iwakura
a4dcabc11b Return character limit in API and initial state. 2017-11-14 16:23:12 +01:00
Lain Iwakura
6cd192b9fb Make character limit configurable. 2017-11-14 16:22:34 +01:00
Andrew
c238444188 Updates and fixes to win95 theme 2017-11-14 01:22:48 -08:00
Andrew
2827f852c0 Theme: Windows 95 2017-10-26 23:22:48 -07:00
769 changed files with 37944 additions and 5858 deletions

View File

@@ -1,21 +1,36 @@
engines:
version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
brakeman:
enabled: true
bundler-audit:
enabled: true
duplication:
enabled: false
eslint:
enabled: true
rubocop:
enabled: true
scss-lint:
enabled: true
ratings:
paths:
- "**.rb"
- "**.js"
- "**.scss"
exclude_paths:
exclude_patterns:
- spec/
- vendor/asset

View File

@@ -134,3 +134,6 @@ STREAMING_CLUSTER_NUM=1
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# Maximum allowed character count
# MAX_TOOT_CHARS=500

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

View File

@@ -27,11 +27,14 @@ addons:
apt:
sources:
- trusty-media
- sourceline: deb https://dl.yarnpkg.com/debian/ stable main
key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
packages:
- ffmpeg
- libicu-dev
- libprotobuf-dev
- protobuf-compiler
- libicu-dev
- yarn
rvm:
- 2.3.4
@@ -42,7 +45,6 @@ services:
install:
- nvm install
- npm install -g yarn
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- yarn install

View File

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.1.0
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
ARG YARN_VERSION=1.3.2
ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@@ -48,7 +48,7 @@ RUN apk -U upgrade \
&& rm yarn.tar.gz \
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \

View File

@@ -49,7 +49,6 @@ gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4'

View File

@@ -24,11 +24,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.6)
active_model_serializers (0.10.7)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.5.4)
activejob (5.1.4)
activesupport (= 5.1.4)
@@ -83,15 +83,15 @@ GEM
capistrano-bundler (1.3.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.3.0)
capistrano-rails (1.3.1)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.2)
capistrano-rbenv (2.1.3)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.15.4)
capybara (2.16.1)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
@@ -113,7 +113,7 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
crass (1.0.3)
debug_inspector (0.0.3)
devise (4.3.0)
bcrypt (~> 3.0)
@@ -121,11 +121,11 @@ GEM
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.0)
activesupport
devise-two-factor (3.0.2)
activesupport (< 5.2)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties
railties (< 5.2)
rotp (~> 2.0)
diff-lcs (1.3)
docile (1.1.5)
@@ -184,7 +184,7 @@ GEM
http (~> 2.2)
nokogiri (~> 1.8)
oj (~> 3.0)
hamlit (2.8.4)
hamlit (2.8.5)
temple (>= 0.8.0)
thor
tilt
@@ -196,7 +196,7 @@ GEM
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.7)
highline (1.7.8)
highline (1.7.10)
hiredis (0.6.1)
hkdf (0.3.0)
htmlentities (4.3.4)
@@ -213,9 +213,9 @@ GEM
httplog (0.99.7)
colorize
rack
i18n (0.9.0)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.18)
i18n-tasks (0.9.19)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -236,8 +236,8 @@ GEM
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
jsonapi-renderer (0.2.0)
jwt (2.1.0)
kaminari (1.1.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1)
@@ -267,8 +267,8 @@ GEM
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mail (2.7.0)
mini_mime (>= 0.1.1)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.9.0)
@@ -279,7 +279,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_mime (0.1.4)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.10.3)
msgpack (1.1.0)
@@ -305,7 +305,7 @@ GEM
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.8.1)
ox (2.8.2)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -316,26 +316,24 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.12.0)
parallel_tests (2.17.0)
parallel_tests (2.19.0)
parallel
parser (2.4.0.0)
ast (~> 2.2)
parser (2.4.0.2)
ast (~> 2.3)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.8)
powerpack (0.1.1)
pry (0.11.2)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.0)
puma (3.10.0)
public_suffix (3.0.1)
puma (3.11.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (2.0.3)
rack-attack (5.0.1)
rack
@@ -344,7 +342,7 @@ GEM
rack
rack-proxy (0.6.2)
rack
rack-test (0.7.0)
rack-test (0.8.2)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
rails (5.1.4)
@@ -381,8 +379,11 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.2.1)
rdf (2.2.11)
rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rdf (2.2.12)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
@@ -395,8 +396,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4)
redis-namespace (1.6.0)
redis (>= 3.0.4)
redis-rack (2.0.3)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -421,7 +422,7 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-rails (3.7.1)
rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@@ -449,10 +450,14 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.25)
scss_lint (0.55.0)
sass (3.5.3)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sass (~> 3.5.3)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
@@ -486,7 +491,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.14.0)
sshkit (1.15.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-ruby (1.2.1)
@@ -514,7 +519,7 @@ GEM
uniform_notifier (1.10.0)
warden (1.2.7)
rack (>= 1.0)
webmock (3.1.0)
webmock (3.1.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
@@ -522,12 +527,12 @@ GEM
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
webpush (0.3.2)
webpush (0.3.3)
hkdf (~> 0.2)
jwt
jwt (~> 2.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
websocket-extensions (0.1.3)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -599,7 +604,6 @@ DEPENDENCIES
pry-rails (~> 0.3)
puma (~> 3.10)
pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
@@ -638,4 +642,4 @@ RUBY VERSION
ruby 2.4.2p198
BUNDLED WITH
1.15.4
1.16.0

View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true
class AboutController < ApplicationController
before_action :set_pack
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
@@ -21,6 +22,10 @@ class AboutController < ApplicationController
helper_method :new_user
def set_pack
use_pack action_name == 'show' ? 'about' : 'common'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -7,6 +7,7 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
use_pack 'public'
@pinned_statuses = []
if current_account && @account.blocking?(current_account)

View File

@@ -21,7 +21,7 @@ module Admin
def destroy
authorize @account_moderation_note, :destroy?
@account_moderation_note.destroy
@account_moderation_note.destroy!
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end

View File

@@ -32,18 +32,21 @@ module Admin
def memorialize
authorize @account, :memorialize?
@account.memorialize!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
log_action :disable, @account.user
redirect_to admin_account_path(@account.id)
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
class ActionLogsController < BaseController
def index
@action_logs = Admin::ActionLog.page(params[:page])
end
end
end

View File

@@ -3,9 +3,15 @@
module Admin
class BaseController < ApplicationController
include Authorization
before_action :require_staff!
include AccountableConcern
layout 'admin'
before_action :require_staff!
before_action :set_pack
def set_pack
use_pack 'admin'
end
end
end

View File

@@ -7,6 +7,7 @@ module Admin
def create
authorize @user, :confirm?
@user.confirm!
log_action :confirm, @user
redirect_to admin_accounts_path
end

View File

@@ -20,6 +20,7 @@ module Admin
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
log_action :create, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
@@ -30,6 +31,7 @@ module Admin
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
@@ -38,16 +40,19 @@ module Admin
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode)
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.update(image: @custom_emoji.image)
if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
@@ -59,12 +64,14 @@ module Admin
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end

View File

@@ -21,6 +21,7 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
@@ -34,6 +35,7 @@ module Admin
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
log_action :destroy, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end

View File

@@ -20,6 +20,7 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save
log_action :create, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
render :new
@@ -28,7 +29,8 @@ module Admin
def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end

View File

@@ -0,0 +1,47 @@
# frozen_string_literal: true
module Admin
class InvitesController < BaseController
def index
authorize :invite, :index?
@invites = filtered_invites.includes(user: :account).page(params[:page])
@invite = Invite.new
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to admin_invites_path
else
@invites = Invite.page(params[:page])
render :index
end
end
def destroy
@invite = Invite.find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to admin_invites_path
end
private
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
def filtered_invites
InviteFilter.new(filter_params).results
end
def filter_params
params.permit(:available, :expired)
end
end
end

View File

@@ -8,7 +8,7 @@ module Admin
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params)
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
@@ -16,13 +16,15 @@ module Admin
def update
authorize @status, :update?
@status.update(status_params)
@status.update!(status_params)
log_action :update, @status
redirect_to admin_report_path(@report)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end

View File

@@ -25,12 +25,17 @@ module Admin
def process_report
case params[:outcome].to_s
when 'resolve'
@report.update(action_taken_by_current_attributes)
@report.update!(action_taken_by_current_attributes)
log_action :resolve, @report
when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report
log_action :suspend, @report.target_account
resolve_all_target_account_reports
when 'silence'
@report.target_account.update(silenced: true)
@report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound

View File

@@ -7,6 +7,7 @@ module Admin
def create
authorize @user, :reset_password?
@user.send_reset_password_instructions
log_action :reset_password, @user
redirect_to admin_accounts_path
end

View File

@@ -7,12 +7,14 @@ module Admin
def promote
authorize @user, :promote?
@user.promote!
log_action :promote, @user
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
log_action :demote, @user
redirect_to admin_account_path(@user.account_id)
end

View File

@@ -13,14 +13,17 @@ module Admin
closed_registrations_message
open_deletion
timeline_preview
show_staff_badge
bootstrap_timeline_accounts
thumbnail
min_invite_role
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
show_staff_badge
).freeze
UPLOAD_SETTINGS = %w(

View File

@@ -6,13 +6,15 @@ module Admin
def create
authorize @account, :silence?
@account.update(silenced: true)
@account.update!(silenced: true)
log_action :silence, @account
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsilence?
@account.update(silenced: false)
@account.update!(silenced: false)
log_action :unsilence, @account
redirect_to admin_accounts_path
end

View File

@@ -26,7 +26,7 @@ module Admin
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params)
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
@@ -34,13 +34,15 @@ module Admin
def update
authorize @status, :update?
@status.update(status_params)
@status.update!(status_params)
log_action :update, @status
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end

View File

@@ -7,12 +7,14 @@ module Admin
def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id)
log_action :suspend, @account
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path
end

View File

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

View File

@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
AccountSearchService.new.call(
params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolving_search?,
current_account
current_account,
resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
)
end
def resolving_search?
params[:resolve] == 'true'
def truthy_param?(key)
params[key] == 'true'
end
end

View File

@@ -13,11 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
reblogs_arg = { reblogs: params[:reblogs] }
FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@@ -53,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id])
end
def relationships(options = {})
def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
end

View File

@@ -0,0 +1,97 @@
# frozen_string_literal: true
class Api::V1::Lists::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action -> { doorkeeper_authorize! :write }, except: [:show]
before_action :require_user!
before_action :set_list
after_action :insert_pagination_headers, only: :show
def show
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
def create
ApplicationRecord.transaction do
list_accounts.each do |account|
@list.accounts << account
end
end
render_empty
end
def destroy
ListAccount.where(list: @list, account_id: account_ids).destroy_all
render_empty
end
private
def set_list
@list = List.where(account: current_account).find(params[:list_id])
end
def load_accounts
if unlimited?
@list.accounts.all
else
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def list_accounts
Account.find(account_ids)
end
def account_ids
Array(resource_params[:account_ids])
end
def resource_params
params.permit(account_ids: [])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
if records_continue?
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
return if unlimited?
unless @accounts.empty?
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.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
def unlimited?
params[:limit] == '0'
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
class Api::V1::ListsController < Api::BaseController
LISTS_LIMIT = 50
before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
before_action :require_user!
before_action :set_list, except: [:index, :create]
after_action :insert_pagination_headers, only: :index
def index
@lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
render json: @lists, each_serializer: REST::ListSerializer
end
def show
render json: @list, serializer: REST::ListSerializer
end
def create
@list = List.create!(list_params.merge(account: current_account))
render json: @list, serializer: REST::ListSerializer
end
def update
@list.update!(list_params)
render json: @list, serializer: REST::ListSerializer
end
def destroy
@list.destroy!
render_empty
end
private
def set_list
@list = List.where(account: current_account).find(params[:id])
end
def list_params
params.permit(:title)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_lists_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @lists.empty?
api_v1_lists_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@lists.last.id
end
def pagination_since_id
@lists.first.id
end
def records_continue?
@lists.size == limit_param(LISTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end
def account_home_feed
Feed.new(:home, current_account)
HomeFeed.new(current_account)
end
def insert_pagination_headers

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::V1::Timelines::ListController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_list
before_action :set_statuses
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
end
private
def set_list
@list = List.where(account: current_account).find(params[:id])
end
def set_statuses
@statuses = cached_list_statuses
end
def cached_list_statuses
cache_collection list_statuses, Status
end
def list_statuses
list_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def list_feed
ListFeed.new(@list)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
def next_path
api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_list_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

@@ -12,8 +12,8 @@ class ApplicationController < ActionController::Base
helper_method :current_account
helper_method :current_session
helper_method :current_theme
helper_method :theme_data
helper_method :current_flavour
helper_method :current_skin
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
@@ -54,6 +54,73 @@ class ApplicationController < ActionController::Base
new_user_session_path
end
def pack(data, pack_name, skin = 'default')
return nil unless pack?(data, pack_name)
pack_data = {
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
flavour: data['name'],
pack: pack_name,
preload: nil,
skin: nil,
}
if data['pack'][pack_name].is_a?(Hash)
pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
if data['pack'][pack_name]['preload']
pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String)
pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array)
end
if skin != 'default' && data['skin'][skin]
pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
else # default skin
pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet']
end
end
pack_data
end
def pack?(data, pack_name)
if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name)
return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash)
end
false
end
def nil_pack(data, pack_name, skin = 'default')
{
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
flavour: data['name'],
pack: nil,
preload: nil,
skin: nil,
}
end
def resolve_pack(data, pack_name, skin = 'default')
result = pack(data, pack_name, skin)
unless result
if data['name'] && data.key?('fallback')
if data['fallback'].nil?
return nil_pack(data, pack_name, skin)
elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback'])
return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name, skin)
elsif data['fallback'].is_a?(Array)
data['fallback'].each do |fallback|
return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback)
end
end
return nil_pack(data, pack_name, skin)
end
return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name, skin) : nil_pack(data, pack_name, skin)
end
result
end
def use_pack(pack_name)
@core = resolve_pack(Themes.instance.core, pack_name)
@theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin)
end
protected
def forbidden
@@ -84,13 +151,14 @@ class ApplicationController < ActionController::Base
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
end
def current_theme
return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme
current_user.setting_theme
def current_flavour
return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour
current_user.setting_flavour
end
def theme_data
Themes.instance.get(current_theme)
def current_skin
return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
current_user.setting_skin
end
def cache_collection(raw, klass)

View File

@@ -2,10 +2,17 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'
before_action :set_pack
def show
super do |user|
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
end
end
private
def set_pack
use_pack 'auth'
end
end

View File

@@ -2,6 +2,7 @@
class Auth::PasswordsController < Devise::PasswordsController
before_action :check_validity_of_reset_password_token, only: :edit
before_action :set_pack
layout 'auth'
@@ -17,4 +18,8 @@ class Auth::PasswordsController < Devise::PasswordsController
def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present?
end
def set_pack
use_pack 'auth'
end
end

View File

@@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_pack
before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
@@ -16,13 +17,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil)
super(hash)
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.build_account if resource.account.nil?
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation)
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
end
end
@@ -35,11 +39,27 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def check_enabled_registrations
redirect_to root_path if single_user_mode? || !Setting.open_registrations
redirect_to root_path if single_user_mode? || !allowed_registrations?
end
def allowed_registrations?
Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
end
def invite_code
if params[:user]
params[:user][:invite_code]
else
params[:invite_code]
end
end
private
def set_pack
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :check_suspension, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
before_action :set_instance_presenter, only: [:new]
before_action :set_pack
def create
super do |resource|
@@ -85,6 +86,10 @@ class Auth::SessionsController < Devise::SessionsController
private
def set_pack
use_pack 'auth'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -4,6 +4,7 @@ class AuthorizeFollowsController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_pack
def show
@account = located_account || render(:error)
@@ -23,6 +24,10 @@ class AuthorizeFollowsController < ApplicationController
private
def set_pack
use_pack 'modal'
end
def follow_attempt
FollowService.new.call(current_account, acct_without_prefix)
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target)
Admin::ActionLog.create(account: current_account, action: action, target: target)
end
end

View File

@@ -7,7 +7,9 @@ class FollowerAccountsController < ApplicationController
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
respond_to do |format|
format.html
format.html do
use_pack 'public'
end
format.json do
render json: collection_presenter,

View File

@@ -7,7 +7,9 @@ class FollowingAccountsController < ApplicationController
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
respond_to do |format|
format.html
format.html do
use_pack 'public'
end
format.json do
render json: collection_presenter,

View File

@@ -2,6 +2,7 @@
class HomeController < ApplicationController
before_action :authenticate_user!
before_action :set_pack
before_action :set_initial_state_json
def index
@@ -37,6 +38,10 @@ class HomeController < ApplicationController
redirect_to(default_redirect_path)
end
def set_pack
use_pack 'home'
end
def set_initial_state_json
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
class InvitesController < ApplicationController
include Authorization
layout 'admin'
before_action :authenticate_user!
before_action :set_pack
def index
authorize :invite, :create?
@invites = Invite.where(user: current_user)
@invite = Invite.new(expires_in: 1.day.to_i)
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to invites_path
else
@invites = Invite.where(user: current_user)
render :index
end
end
def destroy
@invite = Invite.where(user: current_user).find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to invites_path
end
private
def set_pack
use_pack 'settings'
end
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
end

View File

@@ -4,6 +4,7 @@ class RemoteFollowController < ApplicationController
layout 'modal'
before_action :set_account
before_action :set_pack
before_action :gone, if: :suspended_account?
def new
@@ -31,6 +32,10 @@ class RemoteFollowController < ApplicationController
{ acct: session[:remote_follow] }
end
def set_pack
use_pack 'modal'
end
def set_account
@account = Account.find_local!(params[:account_username])
end

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
class Settings::ApplicationsController < ApplicationController
layout 'admin'
class Settings::ApplicationsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
before_action :prepare_scopes, only: [:create, :update]

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
class Settings::BaseController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_pack
def set_pack
use_pack 'settings'
end
end

View File

@@ -1,10 +1,8 @@
# frozen_string_literal: true
class Settings::DeletesController < ApplicationController
layout 'admin'
class Settings::DeletesController < Settings::BaseController
before_action :check_enabled_deletion
before_action :authenticate_user!
prepend_before_action :check_enabled_deletion
def show
@confirmation = Form::DeleteConfirmation.new

View File

@@ -1,10 +1,6 @@
# frozen_string_literal: true
class Settings::ExportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::ExportsController < Settings::BaseController
def show
@export = Export.new(current_account)
end

View File

@@ -2,11 +2,7 @@
require 'sidekiq-bulk'
class Settings::FollowerDomainsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::FollowerDomainsController < Settings::BaseController
def show
@account = current_account
@domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)

View File

@@ -1,9 +1,6 @@
# frozen_string_literal: true
class Settings::ImportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::ImportsController < Settings::BaseController
before_action :set_account
def show

View File

@@ -1,9 +1,6 @@
# frozen_string_literal: true
class Settings::KeywordMutesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::KeywordMutesController < Settings::BaseController
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
def index

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
class Settings::MigrationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show
@migration = Form::Migration.new(account: current_account.moved_to_account)
end
def update
@migration = Form::Migration.new(resource_params)
if @migration.valid? && migration_account_changed?
current_account.update!(moved_to_account: @migration.account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
else
render :show
end
end
private
def resource_params
params.require(:migration).permit(:acct)
end
def migration_account_changed?
current_account.moved_to_account_id != @migration.account&.id
end
end

View File

@@ -1,10 +1,6 @@
# frozen_string_literal: true
class Settings::NotificationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::NotificationsController < Settings::BaseController
def show; end
def update

View File

@@ -1,10 +1,6 @@
# frozen_string_literal: true
class Settings::PreferencesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class Settings::PreferencesController < Settings::BaseController
def show; end
def update
@@ -42,7 +38,8 @@ class Settings::PreferencesController < ApplicationController
:setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,
:setting_theme,
:setting_flavour,
:setting_skin,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
)

View File

@@ -1,11 +1,8 @@
# frozen_string_literal: true
class Settings::ProfilesController < ApplicationController
class Settings::ProfilesController < Settings::BaseController
include ObfuscateFilename
layout 'admin'
before_action :authenticate_user!
before_action :set_account
obfuscate_filename [:account, :avatar]

View File

@@ -1,5 +1,6 @@
# frozen_string_literal: true
# Intentionally does not inherit from BaseController
class Settings::SessionsController < ApplicationController
before_action :set_session, only: :destroy

View File

@@ -1,10 +1,7 @@
# frozen_string_literal: true
module Settings
class TwoFactorAuthenticationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
class TwoFactorAuthenticationsController < BaseController
before_action :verify_otp_required, only: [:create]
def show

View File

@@ -4,6 +4,7 @@ class SharesController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_pack
before_action :set_body_classes
def show
@@ -24,6 +25,10 @@ class SharesController < ApplicationController
}
end
def set_pack
use_pack 'share'
end
def set_body_classes
@body_classes = 'compose-standalone'
end

View File

@@ -14,6 +14,7 @@ class StatusesController < ApplicationController
def show
respond_to do |format|
format.html do
use_pack 'public'
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
@@ -37,6 +38,7 @@ class StatusesController < ApplicationController
end
def embed
use_pack 'embed'
response.headers['X-Frame-Options'] = 'ALLOWALL'
render 'stream_entries/embed', layout: 'embedded'
end

View File

@@ -14,6 +14,7 @@ class StreamEntriesController < ApplicationController
def show
respond_to do |format|
format.html do
use_pack 'public'
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
end

View File

@@ -9,6 +9,7 @@ class TagsController < ApplicationController
respond_to do |format|
format.html do
use_pack 'about'
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end

View File

@@ -6,12 +6,10 @@ module WellKnown
def show
@account = Account.find_local!(username_from_resource)
@canonical_account_uri = @account.to_webfinger_s
@magic_key = pem_to_magic_key(@account.keypair.public_key)
respond_to do |format|
format.any(:json, :html) do
render formats: :json, content_type: 'application/jrd+json'
render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
end
format.xml do
@@ -35,21 +33,6 @@ module WellKnown
WebfingerResource.new(resource_user).username
end
def pem_to_magic_key(public_key)
modulus, exponent = [public_key.n, public_key.e].map do |component|
result = []
until component.zero?
result << [component % 256].pack('C')
component >>= 8
end
result.reverse.join
end
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
end
def resource_param
params.require(:resource)
end

View File

@@ -0,0 +1,103 @@
# frozen_string_literal: true
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
end
end
def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
end
end
def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain')
elsif log.target_type == 'CustomEmoji' && log.action == :update
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
end
def log_extra_attributes(hash)
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
end
def log_change(val)
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
end
def icon_for_log(log)
case log.target_type
when 'Account', 'User'
'user'
when 'CustomEmoji'
'file'
when 'Report'
'flag'
when 'DomainBlock'
'lock'
when 'EmailDomainBlock'
'envelope'
when 'Status'
'pencil'
end
end
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize
'neutral'
when :demote, :silence, :disable, :suspend
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
else
''
end
end
private
def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
end
end

View File

@@ -3,8 +3,9 @@
module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params)
@@ -12,7 +13,7 @@ module Admin::FilterHelper
link_to text, new_url, class: filter_link_class(new_class)
end
def table_link_to(icon, text, path, options = {})
def table_link_to(icon, text, path, **options)
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
end

View File

@@ -5,7 +5,7 @@ module ApplicationHelper
current_page?(path) ? 'active' : ''
end
def active_link_to(label, path, options = {})
def active_link_to(label, path, **options)
link_to label, path, options.merge(class: active_nav_class(path))
end

View File

@@ -9,6 +9,24 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
if single_value.nil? || single_value.is_a?(String)
single_value
else
single_value['href']
end
end
def as_array(value)
value.is_a?(Array) ? value : [value]
end

View File

@@ -11,7 +11,7 @@ module RoutingHelper
end
end
def full_asset_url(source, options = {})
def full_asset_url(source, **options)
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s

View File

@@ -1,3 +1,5 @@
// This file will be loaded on admin pages, regardless of theme.
import { delegate } from 'rails-ujs';
function handleDeleteStatus(event) {

View File

@@ -0,0 +1,8 @@
// This file will be loaded on all pages, regardless of theme.
import { start } from 'rails-ujs';
import 'font-awesome/css/font-awesome.css';
require.context('../images/', true);
start();

View File

@@ -0,0 +1,23 @@
// This file will be loaded on embed pages, regardless of theme.
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
};
if (['interactive', 'complete'].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener('DOMContentLoaded', setEmbedHeight);
}
});

View File

@@ -0,0 +1,25 @@
// This file will be loaded on public pages, regardless of theme.
const { delegate } = require('rails-ujs');
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
if (button !== 0) {
return true;
}
window.location.href = target.href;
return false;
});
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0;
} else {
contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null;
}
return false;
});

View File

@@ -0,0 +1,43 @@
// This file will be loaded on settings pages, regardless of theme.
const { length } = require('stringz');
const { delegate } = require('rails-ujs');
import { processBio } from 'flavours/glitch/util/bio_metadata';
delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter');
if (nameCounter) {
nameCounter.textContent = 30 - length(target.value);
}
});
delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter');
if (noteCounter) {
const noteWithoutMetadata = processBio(target.value).text;
noteCounter.textContent = 500 - length(noteWithoutMetadata);
}
});
delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card.compact');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.style.backgroundImage = `url(${url})`;
});
delegate(document, '#user_setting_flavour, #user_setting_skin', 'change', ({ target }) => {
target.form.submit();
});

View File

@@ -0,0 +1,16 @@
# These packs will be loaded on every appropriate page, regardless of
# theme.
pack:
about:
admin: admin.js
auth:
common:
filename: common.js
stylesheet: true
embed: embed.js
error:
home:
modal:
public: public.js
settings: settings.js
share:

View File

@@ -0,0 +1,661 @@
import api, { getLinks } from 'flavours/glitch/util/api';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
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 FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL';
export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL';
export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(fetchAccountSuccess(response.data));
}).catch(error => {
dispatch(fetchAccountFail(id, error));
});
};
};
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,
id,
};
};
export function fetchAccountSuccess(account) {
return {
type: ACCOUNT_FETCH_SUCCESS,
account,
};
};
export function fetchAccountFail(id, error) {
return {
type: ACCOUNT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
};
export function followAccount(id, reblogs = true) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
dispatch(followAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error));
});
};
};
export function unfollowAccount(id) {
return (dispatch, getState) => {
dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(unfollowAccountFail(error));
});
};
};
export function followAccountRequest(id) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
id,
};
};
export function followAccountSuccess(relationship, alreadyFollowing) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
relationship,
alreadyFollowing,
};
};
export function followAccountFail(error) {
return {
type: ACCOUNT_FOLLOW_FAIL,
error,
};
};
export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
id,
};
};
export function unfollowAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
statuses,
};
};
export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
error,
};
};
export function blockAccount(id) {
return (dispatch, getState) => {
dispatch(blockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(blockAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(blockAccountFail(id, error));
});
};
};
export function unblockAccount(id) {
return (dispatch, getState) => {
dispatch(unblockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
dispatch(unblockAccountSuccess(response.data));
}).catch(error => {
dispatch(unblockAccountFail(id, error));
});
};
};
export function blockAccountRequest(id) {
return {
type: ACCOUNT_BLOCK_REQUEST,
id,
};
};
export function blockAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_BLOCK_SUCCESS,
relationship,
statuses,
};
};
export function blockAccountFail(error) {
return {
type: ACCOUNT_BLOCK_FAIL,
error,
};
};
export function unblockAccountRequest(id) {
return {
type: ACCOUNT_UNBLOCK_REQUEST,
id,
};
};
export function unblockAccountSuccess(relationship) {
return {
type: ACCOUNT_UNBLOCK_SUCCESS,
relationship,
};
};
export function unblockAccountFail(error) {
return {
type: ACCOUNT_UNBLOCK_FAIL,
error,
};
};
export function muteAccount(id, notifications) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(muteAccountFail(id, error));
});
};
};
export function unmuteAccount(id) {
return (dispatch, getState) => {
dispatch(unmuteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
dispatch(unmuteAccountSuccess(response.data));
}).catch(error => {
dispatch(unmuteAccountFail(id, error));
});
};
};
export function muteAccountRequest(id) {
return {
type: ACCOUNT_MUTE_REQUEST,
id,
};
};
export function muteAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_MUTE_SUCCESS,
relationship,
statuses,
};
};
export function muteAccountFail(error) {
return {
type: ACCOUNT_MUTE_FAIL,
error,
};
};
export function unmuteAccountRequest(id) {
return {
type: ACCOUNT_UNMUTE_REQUEST,
id,
};
};
export function unmuteAccountSuccess(relationship) {
return {
type: ACCOUNT_UNMUTE_SUCCESS,
relationship,
};
};
export function unmuteAccountFail(error) {
return {
type: ACCOUNT_UNMUTE_FAIL,
error,
};
};
export function fetchFollowers(id) {
return (dispatch, getState) => {
dispatch(fetchFollowersRequest(id));
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFollowersFail(id, error));
});
};
};
export function fetchFollowersRequest(id) {
return {
type: FOLLOWERS_FETCH_REQUEST,
id,
};
};
export function fetchFollowersSuccess(id, accounts, next) {
return {
type: FOLLOWERS_FETCH_SUCCESS,
id,
accounts,
next,
};
};
export function fetchFollowersFail(id, error) {
return {
type: FOLLOWERS_FETCH_FAIL,
id,
error,
};
};
export function expandFollowers(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'followers', id, 'next']);
if (url === null) {
return;
}
dispatch(expandFollowersRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(expandFollowersFail(id, error));
});
};
};
export function expandFollowersRequest(id) {
return {
type: FOLLOWERS_EXPAND_REQUEST,
id,
};
};
export function expandFollowersSuccess(id, accounts, next) {
return {
type: FOLLOWERS_EXPAND_SUCCESS,
id,
accounts,
next,
};
};
export function expandFollowersFail(id, error) {
return {
type: FOLLOWERS_EXPAND_FAIL,
id,
error,
};
};
export function fetchFollowing(id) {
return (dispatch, getState) => {
dispatch(fetchFollowingRequest(id));
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFollowingFail(id, error));
});
};
};
export function fetchFollowingRequest(id) {
return {
type: FOLLOWING_FETCH_REQUEST,
id,
};
};
export function fetchFollowingSuccess(id, accounts, next) {
return {
type: FOLLOWING_FETCH_SUCCESS,
id,
accounts,
next,
};
};
export function fetchFollowingFail(id, error) {
return {
type: FOLLOWING_FETCH_FAIL,
id,
error,
};
};
export function expandFollowing(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'following', id, 'next']);
if (url === null) {
return;
}
dispatch(expandFollowingRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(expandFollowingFail(id, error));
});
};
};
export function expandFollowingRequest(id) {
return {
type: FOLLOWING_EXPAND_REQUEST,
id,
};
};
export function expandFollowingSuccess(id, accounts, next) {
return {
type: FOLLOWING_EXPAND_SUCCESS,
id,
accounts,
next,
};
};
export function expandFollowingFail(id, error) {
return {
type: FOLLOWING_EXPAND_FAIL,
id,
error,
};
};
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
const loadedRelationships = getState().get('relationships');
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
if (newAccountIds.length === 0) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
};
};
export function fetchRelationshipsRequest(ids) {
return {
type: RELATIONSHIPS_FETCH_REQUEST,
ids,
skipLoading: true,
};
};
export function fetchRelationshipsSuccess(relationships) {
return {
type: RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
};
};
export function fetchRelationshipsFail(error) {
return {
type: RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
};
};
export function fetchFollowRequests() {
return (dispatch, getState) => {
dispatch(fetchFollowRequestsRequest());
api(getState).get('/api/v1/follow_requests').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
};
};
export function fetchFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_FETCH_REQUEST,
};
};
export function fetchFollowRequestsSuccess(accounts, next) {
return {
type: FOLLOW_REQUESTS_FETCH_SUCCESS,
accounts,
next,
};
};
export function fetchFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_FETCH_FAIL,
error,
};
};
export function expandFollowRequests() {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'follow_requests', 'next']);
if (url === null) {
return;
}
dispatch(expandFollowRequestsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(expandFollowRequestsFail(error)));
};
};
export function expandFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_EXPAND_REQUEST,
};
};
export function expandFollowRequestsSuccess(accounts, next) {
return {
type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
accounts,
next,
};
};
export function expandFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_EXPAND_FAIL,
error,
};
};
export function authorizeFollowRequest(id) {
return (dispatch, getState) => {
dispatch(authorizeFollowRequestRequest(id));
api(getState)
.post(`/api/v1/follow_requests/${id}/authorize`)
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
};
};
export function authorizeFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
id,
};
};
export function authorizeFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
id,
};
};
export function authorizeFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
id,
error,
};
};
export function rejectFollowRequest(id) {
return (dispatch, getState) => {
dispatch(rejectFollowRequestRequest(id));
api(getState)
.post(`/api/v1/follow_requests/${id}/reject`)
.then(() => dispatch(rejectFollowRequestSuccess(id)))
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
};
};
export function rejectFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_REJECT_REQUEST,
id,
};
};
export function rejectFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_REJECT_SUCCESS,
id,
};
};
export function rejectFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_REJECT_FAIL,
id,
error,
};
};

View File

@@ -0,0 +1,24 @@
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export function dismissAlert(alert) {
return {
type: ALERT_DISMISS,
alert,
};
};
export function clearAlert() {
return {
type: ALERT_CLEAR,
};
};
export function showAlert(title, message) {
return {
type: ALERT_SHOW,
title,
message,
};
};

View File

@@ -0,0 +1,82 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import { fetchRelationships } from './accounts';
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL';
export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
api(getState).get('/api/v1/blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchBlocksFail(error)));
};
};
export function fetchBlocksRequest() {
return {
type: BLOCKS_FETCH_REQUEST,
};
};
export function fetchBlocksSuccess(accounts, next) {
return {
type: BLOCKS_FETCH_SUCCESS,
accounts,
next,
};
};
export function fetchBlocksFail(error) {
return {
type: BLOCKS_FETCH_FAIL,
error,
};
};
export function expandBlocks() {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'blocks', 'next']);
if (url === null) {
return;
}
dispatch(expandBlocksRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandBlocksFail(error)));
};
};
export function expandBlocksRequest() {
return {
type: BLOCKS_EXPAND_REQUEST,
};
};
export function expandBlocksSuccess(accounts, next) {
return {
type: BLOCKS_EXPAND_SUCCESS,
accounts,
next,
};
};
export function expandBlocksFail(error) {
return {
type: BLOCKS_EXPAND_FAIL,
error,
};
};

View File

@@ -0,0 +1,25 @@
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
export function fetchBundleRequest(skipLoading) {
return {
type: BUNDLE_FETCH_REQUEST,
skipLoading,
};
}
export function fetchBundleSuccess(skipLoading) {
return {
type: BUNDLE_FETCH_SUCCESS,
skipLoading,
};
}
export function fetchBundleFail(error, skipLoading) {
return {
type: BUNDLE_FETCH_FAIL,
error,
skipLoading,
};
}

View File

@@ -0,0 +1,52 @@
import api from 'flavours/glitch/util/api';
export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST';
export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS';
export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL';
export function fetchStatusCard(id) {
return (dispatch, getState) => {
if (getState().getIn(['cards', id], null) !== null) {
return;
}
dispatch(fetchStatusCardRequest(id));
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
if (!response.data.url) {
return;
}
dispatch(fetchStatusCardSuccess(id, response.data));
}).catch(error => {
dispatch(fetchStatusCardFail(id, error));
});
};
};
export function fetchStatusCardRequest(id) {
return {
type: STATUS_CARD_FETCH_REQUEST,
id,
skipLoading: true,
};
};
export function fetchStatusCardSuccess(id, card) {
return {
type: STATUS_CARD_FETCH_SUCCESS,
id,
card,
skipLoading: true,
};
};
export function fetchStatusCardFail(id, error) {
return {
type: STATUS_CARD_FETCH_FAIL,
id,
error,
skipLoading: true,
skipAlert: true,
};
};

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

@@ -0,0 +1,398 @@
import api from 'flavours/glitch/util/api';
import { throttle } from 'lodash';
import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
import { useEmoji } from './emojis';
import {
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
refreshDirectTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
text: text,
};
};
export function replyCompose(status, router) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
status: status,
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
};
};
export function cancelReplyCompose() {
return {
type: COMPOSE_REPLY_CANCEL,
};
};
export function resetCompose() {
return {
type: COMPOSE_RESET,
};
};
export function mentionCompose(account, router) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
account: account,
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
};
};
export function submitCompose() {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
if (!status || !status.length) {
return;
}
dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
}
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
visibility: getState().getIn(['compose', 'privacy']),
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
const insertOrRefresh = (timelineId, refreshAction) => {
if (getState().getIn(['timelines', timelineId, 'online'])) {
dispatch(updateTimeline(timelineId, { ...response.data }));
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
dispatch(refreshAction());
}
};
insertOrRefresh('home', refreshHomeTimeline);
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
} else if (response.data.visibility === 'direct') {
insertOrRefresh('direct', refreshDirectTimeline);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
};
};
export function submitComposeRequest() {
return {
type: COMPOSE_SUBMIT_REQUEST,
};
};
export function submitComposeSuccess(status) {
return {
type: COMPOSE_SUBMIT_SUCCESS,
status: status,
};
};
export function submitComposeFail(error) {
return {
type: COMPOSE_SUBMIT_FAIL,
error: error,
};
};
export function doodleSet(options) {
return {
type: COMPOSE_DOODLE_SET,
options: options,
};
};
export function uploadCompose(files) {
return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
return;
}
dispatch(uploadComposeRequest());
let data = new FormData();
data.append('file', files[0]);
api(getState).post('/api/v1/media', data, {
onUploadProgress: function (e) {
dispatch(uploadComposeProgress(e.loaded, e.total));
},
}).then(function (response) {
dispatch(uploadComposeSuccess(response.data));
}).catch(function (error) {
dispatch(uploadComposeFail(error));
});
};
};
export function changeUploadCompose(id, description) {
return (dispatch, getState) => {
dispatch(changeUploadComposeRequest());
api(getState).put(`/api/v1/media/${id}`, { description }).then(response => {
dispatch(changeUploadComposeSuccess(response.data));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
});
};
};
export function changeUploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
skipLoading: true,
};
};
export function changeUploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
media: media,
skipLoading: true,
};
};
export function changeUploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error,
skipLoading: true,
};
};
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST,
skipLoading: true,
};
};
export function uploadComposeProgress(loaded, total) {
return {
type: COMPOSE_UPLOAD_PROGRESS,
loaded: loaded,
total: total,
};
};
export function uploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_SUCCESS,
media: media,
skipLoading: true,
};
};
export function uploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_FAIL,
error: error,
skipLoading: true,
};
};
export function undoUploadCompose(media_id) {
return {
type: COMPOSE_UPLOAD_UNDO,
media_id: media_id,
};
};
export function clearComposeSuggestions() {
return {
type: COMPOSE_SUGGESTIONS_CLEAR,
};
};
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
api(getState).get('/api/v1/accounts/search', {
params: {
q: token.slice(1),
resolve: false,
limit: 4,
},
}).then(response => {
dispatch(readyComposeSuggestionsAccounts(token, response.data));
});
}, 200, { leading: true, trailing: true });
const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
dispatch(readyComposeSuggestionsEmojis(token, results));
};
export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
if (token[0] === ':') {
fetchComposeSuggestionsEmojis(dispatch, getState, token);
} else {
fetchComposeSuggestionsAccounts(dispatch, getState, token);
}
};
};
export function readyComposeSuggestionsEmojis(token, emojis) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
emojis,
};
};
export function readyComposeSuggestionsAccounts(token, accounts) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
accounts,
};
};
export function selectComposeSuggestion(position, token, suggestion) {
return (dispatch, getState) => {
let completion, startPosition;
if (typeof suggestion === 'object' && suggestion.id) {
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
startPosition = position;
}
dispatch({
type: COMPOSE_SUGGESTION_SELECT,
position: startPosition,
token,
completion,
});
};
};
export function mountCompose() {
return {
type: COMPOSE_MOUNT,
};
};
export function unmountCompose() {
return {
type: COMPOSE_UNMOUNT,
};
};
export function toggleComposeAdvancedOption(option) {
return {
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
option: option,
};
}
export function changeComposeSensitivity() {
return {
type: COMPOSE_SENSITIVITY_CHANGE,
};
};
export function changeComposeSpoilerness() {
return {
type: COMPOSE_SPOILERNESS_CHANGE,
};
};
export function changeComposeSpoilerText(text) {
return {
type: COMPOSE_SPOILER_TEXT_CHANGE,
text,
};
};
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
};
export function insertEmojiCompose(position, emoji) {
return {
type: COMPOSE_EMOJI_INSERT,
position,
emoji,
};
};
export function changeComposing(value) {
return {
type: COMPOSE_COMPOSING_CHANGE,
value,
};
}

View File

@@ -0,0 +1,117 @@
import api, { getLinks } from 'flavours/glitch/util/api';
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
export function blockDomain(domain, accountId) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
dispatch(blockDomainSuccess(domain, accountId));
}).catch(err => {
dispatch(blockDomainFail(domain, err));
});
};
};
export function blockDomainRequest(domain) {
return {
type: DOMAIN_BLOCK_REQUEST,
domain,
};
};
export function blockDomainSuccess(domain, accountId) {
return {
type: DOMAIN_BLOCK_SUCCESS,
domain,
accountId,
};
};
export function blockDomainFail(domain, error) {
return {
type: DOMAIN_BLOCK_FAIL,
domain,
error,
};
};
export function unblockDomain(domain, accountId) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
dispatch(unblockDomainSuccess(domain, accountId));
}).catch(err => {
dispatch(unblockDomainFail(domain, err));
});
};
};
export function unblockDomainRequest(domain) {
return {
type: DOMAIN_UNBLOCK_REQUEST,
domain,
};
};
export function unblockDomainSuccess(domain, accountId) {
return {
type: DOMAIN_UNBLOCK_SUCCESS,
domain,
accountId,
};
};
export function unblockDomainFail(domain, error) {
return {
type: DOMAIN_UNBLOCK_FAIL,
domain,
error,
};
};
export function fetchDomainBlocks() {
return (dispatch, getState) => {
dispatch(fetchDomainBlocksRequest());
api(getState).get().then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchDomainBlocksFail(err));
});
};
};
export function fetchDomainBlocksRequest() {
return {
type: DOMAIN_BLOCKS_FETCH_REQUEST,
};
};
export function fetchDomainBlocksSuccess(domains, next) {
return {
type: DOMAIN_BLOCKS_FETCH_SUCCESS,
domains,
next,
};
};
export function fetchDomainBlocksFail(error) {
return {
type: DOMAIN_BLOCKS_FETCH_FAIL,
error,
};
};

View File

@@ -0,0 +1,14 @@
import { saveSettings } from './settings';
export const EMOJI_USE = 'EMOJI_USE';
export function useEmoji(emoji) {
return dispatch => {
dispatch({
type: EMOJI_USE,
emoji,
});
dispatch(saveSettings());
};
};

View File

@@ -0,0 +1,83 @@
import api, { getLinks } from 'flavours/glitch/util/api';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL';
export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST';
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
export function fetchFavouritedStatuses() {
return (dispatch, getState) => {
dispatch(fetchFavouritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
});
};
};
export function fetchFavouritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_FETCH_REQUEST,
};
};
export function fetchFavouritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchFavouritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_FETCH_FAIL,
error,
};
};
export function expandFavouritedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
if (url === null) {
return;
}
dispatch(expandFavouritedStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
});
};
};
export function expandFavouritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_EXPAND_REQUEST,
};
};
export function expandFavouritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
};
};
export function expandFavouritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_EXPAND_FAIL,
error,
};
};

View File

@@ -0,0 +1,17 @@
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
};
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
};

View File

@@ -0,0 +1,313 @@
import api from 'flavours/glitch/util/api';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
export const REBLOG_FAIL = 'REBLOG_FAIL';
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
export const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
export const UNPIN_REQUEST = 'UNPIN_REQUEST';
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
export const UNPIN_FAIL = 'UNPIN_FAIL';
export function reblog(status) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
// The reblog API method returns a new status wrapped around the original. In this case we are only
// interested in how the original is modified, hence passing it skipping the wrapper
dispatch(reblogSuccess(status, response.data.reblog));
}).catch(function (error) {
dispatch(reblogFail(status, error));
});
};
};
export function unreblog(status) {
return (dispatch, getState) => {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
dispatch(unreblogSuccess(status, response.data));
}).catch(error => {
dispatch(unreblogFail(status, error));
});
};
};
export function reblogRequest(status) {
return {
type: REBLOG_REQUEST,
status: status,
};
};
export function reblogSuccess(status, response) {
return {
type: REBLOG_SUCCESS,
status: status,
response: response,
};
};
export function reblogFail(status, error) {
return {
type: REBLOG_FAIL,
status: status,
error: error,
};
};
export function unreblogRequest(status) {
return {
type: UNREBLOG_REQUEST,
status: status,
};
};
export function unreblogSuccess(status, response) {
return {
type: UNREBLOG_SUCCESS,
status: status,
response: response,
};
};
export function unreblogFail(status, error) {
return {
type: UNREBLOG_FAIL,
status: status,
error: error,
};
};
export function favourite(status) {
return function (dispatch, getState) {
dispatch(favouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
dispatch(favouriteSuccess(status, response.data));
}).catch(function (error) {
dispatch(favouriteFail(status, error));
});
};
};
export function unfavourite(status) {
return (dispatch, getState) => {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
dispatch(unfavouriteSuccess(status, response.data));
}).catch(error => {
dispatch(unfavouriteFail(status, error));
});
};
};
export function favouriteRequest(status) {
return {
type: FAVOURITE_REQUEST,
status: status,
};
};
export function favouriteSuccess(status, response) {
return {
type: FAVOURITE_SUCCESS,
status: status,
response: response,
};
};
export function favouriteFail(status, error) {
return {
type: FAVOURITE_FAIL,
status: status,
error: error,
};
};
export function unfavouriteRequest(status) {
return {
type: UNFAVOURITE_REQUEST,
status: status,
};
};
export function unfavouriteSuccess(status, response) {
return {
type: UNFAVOURITE_SUCCESS,
status: status,
response: response,
};
};
export function unfavouriteFail(status, error) {
return {
type: UNFAVOURITE_FAIL,
status: status,
error: error,
};
};
export function fetchReblogs(id) {
return (dispatch, getState) => {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
dispatch(fetchReblogsSuccess(id, response.data));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
});
};
};
export function fetchReblogsRequest(id) {
return {
type: REBLOGS_FETCH_REQUEST,
id,
};
};
export function fetchReblogsSuccess(id, accounts) {
return {
type: REBLOGS_FETCH_SUCCESS,
id,
accounts,
};
};
export function fetchReblogsFail(id, error) {
return {
type: REBLOGS_FETCH_FAIL,
error,
};
};
export function fetchFavourites(id) {
return (dispatch, getState) => {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
dispatch(fetchFavouritesSuccess(id, response.data));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
};
};
export function fetchFavouritesRequest(id) {
return {
type: FAVOURITES_FETCH_REQUEST,
id,
};
};
export function fetchFavouritesSuccess(id, accounts) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
};
};
export function fetchFavouritesFail(id, error) {
return {
type: FAVOURITES_FETCH_FAIL,
error,
};
};
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
dispatch(pinSuccess(status, response.data));
}).catch(error => {
dispatch(pinFail(status, error));
});
};
};
export function pinRequest(status) {
return {
type: PIN_REQUEST,
status,
};
};
export function pinSuccess(status, response) {
return {
type: PIN_SUCCESS,
status,
response,
};
};
export function pinFail(status, error) {
return {
type: PIN_FAIL,
status,
error,
};
};
export function unpin (status) {
return (dispatch, getState) => {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
dispatch(unpinSuccess(status, response.data));
}).catch(error => {
dispatch(unpinFail(status, error));
});
};
};
export function unpinRequest(status) {
return {
type: UNPIN_REQUEST,
status,
};
};
export function unpinSuccess(status, response) {
return {
type: UNPIN_SUCCESS,
status,
response,
};
};
export function unpinFail(status, error) {
return {
type: UNPIN_FAIL,
status,
error,
};
};

View File

@@ -0,0 +1,24 @@
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
export function changeLocalSetting(key, value) {
return dispatch => {
dispatch({
type: LOCAL_SETTING_CHANGE,
key,
value,
});
dispatch(saveLocalSettings());
};
};
// __TODO :__
// Right now `saveLocalSettings()` doesn't keep track of which user
// is currently signed in, but it might be better to give each user
// their *own* local settings.
export function saveLocalSettings() {
return (_, getState) => {
const localSettings = getState().get('local_settings').toJS();
localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
};
};

View File

@@ -0,0 +1,16 @@
export const MODAL_OPEN = 'MODAL_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE';
export function openModal(type, props) {
return {
type: MODAL_OPEN,
modalType: type,
modalProps: props,
};
};
export function closeModal() {
return {
type: MODAL_CLOSE,
};
};

View File

@@ -0,0 +1,103 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import { fetchRelationships } from './accounts';
import { openModal } from 'flavours/glitch/actions/modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
api(getState).get('/api/v1/mutes').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchMutesFail(error)));
};
};
export function fetchMutesRequest() {
return {
type: MUTES_FETCH_REQUEST,
};
};
export function fetchMutesSuccess(accounts, next) {
return {
type: MUTES_FETCH_SUCCESS,
accounts,
next,
};
};
export function fetchMutesFail(error) {
return {
type: MUTES_FETCH_FAIL,
error,
};
};
export function expandMutes() {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'mutes', 'next']);
if (url === null) {
return;
}
dispatch(expandMutesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandMutesFail(error)));
};
};
export function expandMutesRequest() {
return {
type: MUTES_EXPAND_REQUEST,
};
};
export function expandMutesSuccess(accounts, next) {
return {
type: MUTES_EXPAND_SUCCESS,
accounts,
next,
};
};
export function expandMutesFail(error) {
return {
type: MUTES_EXPAND_FAIL,
error,
};
};
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal('MUTE'));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}

View File

@@ -0,0 +1,265 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import { List as ImmutableList } from 'immutable';
import IntlMessageFormat from 'intl-messageformat';
import { fetchRelationships } from './accounts';
import { defineMessages } from 'react-intl';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
// tracking the notif cleaning request
export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL';
export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE';
export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
// Unmark notifications (when the cleaning mode is left)
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
// Mark one for delete
export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE';
export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST';
export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS';
export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
const fetchRelatedRelationships = (dispatch, notifications) => {
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
if (accountIds > 0) {
dispatch(fetchRelationships(accountIds));
}
};
const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
html = html.replace(/<br \/>|<br>|\n/, ' ');
wrapper.innerHTML = html;
return wrapper.textContent;
};
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
account: notification.account,
status: notification.status,
meta: playSound ? { sound: 'boop' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener('click', () => {
window.focus();
notify.close();
});
}
};
};
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
export function refreshNotifications() {
return (dispatch, getState) => {
const params = {};
const ids = getState().getIn(['notifications', 'items']);
let skipLoading = false;
if (ids.size > 0) {
params.since_id = ids.first().get('id');
}
if (getState().getIn(['notifications', 'loaded'])) {
skipLoading = true;
}
params.exclude_types = excludeTypesFromSettings(getState());
dispatch(refreshNotificationsRequest(skipLoading));
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null));
fetchRelatedRelationships(dispatch, response.data);
}).catch(error => {
dispatch(refreshNotificationsFail(error, skipLoading));
});
};
};
export function refreshNotificationsRequest(skipLoading) {
return {
type: NOTIFICATIONS_REFRESH_REQUEST,
skipLoading,
};
};
export function refreshNotificationsSuccess(notifications, skipLoading, next) {
return {
type: NOTIFICATIONS_REFRESH_SUCCESS,
notifications,
accounts: notifications.map(item => item.account),
statuses: notifications.map(item => item.status).filter(status => !!status),
skipLoading,
next,
};
};
export function refreshNotificationsFail(error, skipLoading) {
return {
type: NOTIFICATIONS_REFRESH_FAIL,
error,
skipLoading,
};
};
export function expandNotifications() {
return (dispatch, getState) => {
const items = getState().getIn(['notifications', 'items'], ImmutableList());
if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
return;
}
const params = {
max_id: items.last().get('id'),
limit: 20,
exclude_types: excludeTypesFromSettings(getState()),
};
dispatch(expandNotificationsRequest());
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 => {
dispatch(expandNotificationsFail(error));
});
};
};
export function expandNotificationsRequest() {
return {
type: NOTIFICATIONS_EXPAND_REQUEST,
};
};
export function expandNotificationsSuccess(notifications, next) {
return {
type: NOTIFICATIONS_EXPAND_SUCCESS,
notifications,
accounts: notifications.map(item => item.account),
statuses: notifications.map(item => item.status).filter(status => !!status),
next,
};
};
export function expandNotificationsFail(error) {
return {
type: NOTIFICATIONS_EXPAND_FAIL,
error,
};
};
export function clearNotifications() {
return (dispatch, getState) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});
api(getState).post('/api/v1/notifications/clear');
};
};
export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,
top,
};
};
export function deleteMarkedNotifications() {
return (dispatch, getState) => {
dispatch(deleteMarkedNotificationsRequest());
let ids = [];
getState().getIn(['notifications', 'items']).forEach((n) => {
if (n.get('markedForDelete')) {
ids.push(n.get('id'));
}
});
if (ids.length === 0) {
return;
}
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
dispatch(deleteMarkedNotificationsSuccess());
}).catch(error => {
console.error(error);
dispatch(deleteMarkedNotificationsFail(error));
});
};
};
export function enterNotificationClearingMode(yes) {
return {
type: NOTIFICATIONS_ENTER_CLEARING_MODE,
yes: yes,
};
};
export function markAllNotifications(yes) {
return {
type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
yes: yes, // true, false or null. null = invert
};
};
export function deleteMarkedNotificationsRequest() {
return {
type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
};
};
export function deleteMarkedNotificationsFail() {
return {
type: NOTIFICATIONS_DELETE_MARKED_FAIL,
};
};
export function markNotificationForDelete(id, yes) {
return {
type: NOTIFICATION_MARK_FOR_DELETE,
id: id,
yes: yes,
};
};
export function deleteMarkedNotificationsSuccess() {
return {
type: NOTIFICATIONS_DELETE_MARKED_SUCCESS,
};
};

View File

@@ -0,0 +1,14 @@
import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';
export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
if (!alreadySeen) {
dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
};
};

View File

@@ -0,0 +1,40 @@
import api from 'flavours/glitch/util/api';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from 'flavours/glitch/util/initial_state';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
};
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
};
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
};

View File

@@ -0,0 +1,52 @@
import axios from 'axios';
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
export function setBrowserSupport (value) {
return {
type: SET_BROWSER_SUPPORT,
value,
};
}
export function setSubscription (subscription) {
return {
type: SET_SUBSCRIPTION,
subscription,
};
}
export function clearSubscription () {
return {
type: CLEAR_SUBSCRIPTION,
};
}
export function changeAlerts(key, value) {
return dispatch => {
dispatch({
type: ALERTS_CHANGE,
key,
value,
});
dispatch(saveSettings());
};
}
export function saveSettings() {
return (_, getState) => {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data: {
alerts,
},
});
};
}

View File

@@ -0,0 +1,80 @@
import api from 'flavours/glitch/util/api';
import { openModal, closeModal } from './modal';
export const REPORT_INIT = 'REPORT_INIT';
export const REPORT_CANCEL = 'REPORT_CANCEL';
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
export function initReport(account, status) {
return dispatch => {
dispatch({
type: REPORT_INIT,
account,
status,
});
dispatch(openModal('REPORT'));
};
};
export function cancelReport() {
return {
type: REPORT_CANCEL,
};
};
export function toggleStatusReport(statusId, checked) {
return {
type: REPORT_STATUS_TOGGLE,
statusId,
checked,
};
};
export function submitReport() {
return (dispatch, getState) => {
dispatch(submitReportRequest());
api(getState).post('/api/v1/reports', {
account_id: getState().getIn(['reports', 'new', 'account_id']),
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
comment: getState().getIn(['reports', 'new', 'comment']),
}).then(response => {
dispatch(closeModal());
dispatch(submitReportSuccess(response.data));
}).catch(error => dispatch(submitReportFail(error)));
};
};
export function submitReportRequest() {
return {
type: REPORT_SUBMIT_REQUEST,
};
};
export function submitReportSuccess(report) {
return {
type: REPORT_SUBMIT_SUCCESS,
report,
};
};
export function submitReportFail(error) {
return {
type: REPORT_SUBMIT_FAIL,
error,
};
};
export function changeReportComment(comment) {
return {
type: REPORT_COMMENT_CHANGE,
comment,
};
};

View File

@@ -0,0 +1,73 @@
import api from 'flavours/glitch/util/api';
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
export const SEARCH_SHOW = 'SEARCH_SHOW';
export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
value,
};
};
export function clearSearch() {
return {
type: SEARCH_CLEAR,
};
};
export function submitSearch() {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
if (value.length === 0) {
return;
}
dispatch(fetchSearchRequest());
api(getState).get('/api/v1/search', {
params: {
q: value,
resolve: true,
},
}).then(response => {
dispatch(fetchSearchSuccess(response.data));
}).catch(error => {
dispatch(fetchSearchFail(error));
});
};
};
export function fetchSearchRequest() {
return {
type: SEARCH_FETCH_REQUEST,
};
};
export function fetchSearchSuccess(results) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
accounts: results.accounts,
statuses: results.statuses,
};
};
export function fetchSearchFail(error) {
return {
type: SEARCH_FETCH_FAIL,
error,
};
};
export function showSearch() {
return {
type: SEARCH_SHOW,
};
};

View File

@@ -0,0 +1,31 @@
import axios from 'axios';
import { debounce } from 'lodash';
export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE';
export function changeSetting(key, value) {
return dispatch => {
dispatch({
type: SETTING_CHANGE,
key,
value,
});
dispatch(saveSettings());
};
};
const debouncedSave = debounce((dispatch, getState) => {
if (getState().getIn(['settings', 'saved'])) {
return;
}
const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS();
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
}, 5000, { trailing: true });
export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState);
};

View File

@@ -0,0 +1,217 @@
import api from 'flavours/glitch/util/api';
import { deleteFromTimelines } from './timelines';
import { fetchStatusCard } from './cards';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
id,
skipLoading,
};
};
export function fetchStatus(id) {
return (dispatch, getState) => {
const skipLoading = getState().getIn(['statuses', id], null) !== null;
dispatch(fetchContext(id));
dispatch(fetchStatusCard(id));
if (skipLoading) {
return;
}
dispatch(fetchStatusRequest(id, skipLoading));
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(fetchStatusSuccess(response.data, skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
});
};
};
export function fetchStatusSuccess(status, skipLoading) {
return {
type: STATUS_FETCH_SUCCESS,
status,
skipLoading,
};
};
export function fetchStatusFail(id, error, skipLoading) {
return {
type: STATUS_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: true,
};
};
export function deleteStatus(id) {
return (dispatch, getState) => {
dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
}).catch(error => {
dispatch(deleteStatusFail(id, error));
});
};
};
export function deleteStatusRequest(id) {
return {
type: STATUS_DELETE_REQUEST,
id: id,
};
};
export function deleteStatusSuccess(id) {
return {
type: STATUS_DELETE_SUCCESS,
id: id,
};
};
export function deleteStatusFail(id, error) {
return {
type: STATUS_DELETE_FAIL,
id: id,
error: error,
};
};
export function fetchContext(id) {
return (dispatch, getState) => {
dispatch(fetchContextRequest(id));
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => {
if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id));
}
dispatch(fetchContextFail(id, error));
});
};
};
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
};
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
};
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
};
export function muteStatus(id) {
return (dispatch, getState) => {
dispatch(muteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
dispatch(muteStatusSuccess(id));
}).catch(error => {
dispatch(muteStatusFail(id, error));
});
};
};
export function muteStatusRequest(id) {
return {
type: STATUS_MUTE_REQUEST,
id,
};
};
export function muteStatusSuccess(id) {
return {
type: STATUS_MUTE_SUCCESS,
id,
};
};
export function muteStatusFail(id, error) {
return {
type: STATUS_MUTE_FAIL,
id,
error,
};
};
export function unmuteStatus(id) {
return (dispatch, getState) => {
dispatch(unmuteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
dispatch(unmuteStatusSuccess(id));
}).catch(error => {
dispatch(unmuteStatusFail(id, error));
});
};
};
export function unmuteStatusRequest(id) {
return {
type: STATUS_UNMUTE_REQUEST,
id,
};
};
export function unmuteStatusSuccess(id) {
return {
type: STATUS_UNMUTE_SUCCESS,
id,
};
};
export function unmuteStatusFail(id, error) {
return {
type: STATUS_UNMUTE_FAIL,
id,
error,
};
};

View File

@@ -0,0 +1,17 @@
import { Iterable, fromJS } from 'immutable';
export const STORE_HYDRATE = 'STORE_HYDRATE';
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState =>
fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) {
const state = convertState(rawState);
return {
type: STORE_HYDRATE,
state,
};
};

View File

@@ -0,0 +1,54 @@
import { connectStream } from 'flavours/glitch/util/stream';
import {
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
connectTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, refreshNotifications } from './notifications';
import { getLocale } from 'mastodon/locales';
const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
return {
onConnect() {
dispatch(connectTimeline(timelineId));
},
onDisconnect() {
dispatch(disconnectTimeline(timelineId));
},
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
};
});
}
function refreshHomeTimelineAndNotification (dispatch) {
dispatch(refreshHomeTimeline());
dispatch(refreshNotifications());
}
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');

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