Compare commits

..

334 Commits

Author SHA1 Message Date
kibigo!
866e441df3 [WIP] Initial status work 2017-08-14 15:07:22 -07:00
Ondřej Hruška
4dc0ddc601 fix regression - collapse button not working 2017-08-07 22:46:24 +02:00
Ondřej Hruška
7a1ca8b0df Merge remote-tracking branch 'upstream/master' 2017-08-07 22:43:52 +02:00
Ondřej Hruška
b8791ae79b always put @handles on a new line. undo some needless changes from prev cmmt 2017-08-07 21:59:49 +02:00
Ondřej Hruška
b9a2ceca35 removed ellipsis from .display-name 2017-08-07 21:24:19 +02:00
Lynx Kotoura
a3e53bd442 Adjust tags and accounts page (#4534)
* Adjust tag and accounts page

* Remove units from 0px paddings
2017-08-07 20:33:06 +02:00
Sorin Davidoi
8eb6d171e6 feat: Cache status height to avoid expensive renders (#4439)
* feat: Cache status height to avoid expensive renders

* feat: Escape content and emojify in reducers

* fix(css): Remove backface-visibility: hidden from .scrollable

* fix(statuses): Avoid creating DOMParses inside a loop
2017-08-07 20:32:03 +02:00
Ondřej Hruška
5942347407 Refactor Avatar and AvatarOverlay to have 'account' as prop instead of src and staticSrc (#4526)
* Refactored Avatar and AvatarOverlay (DRY) to have 'account' as prop.
Also removed animate attribute from compose navigation bar, which should
have never been there. Added test for avatar overlay.

* fix broken tests

* god dammit another bug in tests! travis please let this pass

* formatting in avatar overlay
2017-08-07 19:44:55 +02:00
Yamagishi Kazutoshi
22db947225 Update dependencies for Ruby (#4543)
* Update twitter-text to version 1.14.7

* Update tilt to version 2.0.8

* Update statsd-instrument to version 2.1.4

* Update sidekiq to version 5.0.4

* Update sidekiq-scheduler to version 2.1.8

* Update sidekiq-unique-jobs to version 5.0.9

* Update redis-activesupport to version 5.0.3

* Update rails-settings-cached to version v0.6.6

* Update pkg-config to version 1.2.4

* Update parallel_tests to version 2.14.2

* Update jsonapi-renderer to version 0.1.3

* Update i18n-tasks to version 0.9.16

* Update httplog to version 0.99.7

* Update fabrication to version 2.16.2

* Update bootsnap to version 1.1.2

* Update aws-sigv4 to version 1.0.1

* Update aws-sdk-core to version 2.10.21

* Update hashdiff to version 0.3.5

* Update rails to version 5.1.3
2017-08-07 18:55:36 +02:00
Thomas Leister
5d408fd9aa [Docker] Add multicore support to "make" and "bundler" (#4544)
* Let make and bundler use multiple cores

* Adds -j option to bundle install instead of bundle config
2017-08-07 18:55:07 +02:00
m4sk1n
47579ec58c It makes no sense to try using invalid or expired link again (#4521)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-08-08 00:20:21 +09:00
m4sk1n
3363a05539 i18n: Update Polish translation (#4545) 2017-08-07 21:55:25 +09:00
Yamagishi Kazutoshi
87f10d476c Set false to animated options for thumbnail processor (#4547)
Resolve #3199

Fix the aspect ratio of animated GIF whose background is transparent.
2017-08-07 14:41:21 +02:00
kibigo!
70c5eccc12 Compatibility regex for user profiles 2017-08-06 15:10:06 -07:00
Eugen Rochko
41c3389d76 Bump to 1.5.1 2017-08-06 23:53:25 +02:00
Masoud Abkenar
e7a5a188ef i18n: update Persian translation (#4540) 2017-08-06 23:50:20 +02:00
Ondřej Hruška
eb7fc34708 Merge pull request #124 from glitch-soc/data-avatar-of
all checks have failed woooooo \*merges*
2017-08-06 21:49:26 +02:00
Ondřej Hruška
91836d577e Add data-avatar-of="@..." to all user avatars 2017-08-06 21:24:13 +02:00
Ondřej Hruška
7de0fa698d Updated glitch files to use the new Avatar class correctly 2017-08-06 21:23:59 +02:00
Ondřej Hruška
811d895f7b Merged upstream PR #4526 2017-08-06 21:23:36 +02:00
Komic
71384b2ef9 Fix #4531 (#4533)
wrong character, sorry about that~
2017-08-06 21:52:12 +09:00
Komic
d1d465347a fr.yml update (#4531)
Translation for https://github.com/tootsuite/mastodon/pull/4523
2017-08-06 17:36:10 +09:00
Daigo 3 Dango
5eba129b0f Translate 'Signed in as' into Japanese (#4530) 2017-08-06 03:14:11 +02:00
Quent-in
021a83ead4 l10n OC - Added #4523 "signed in as" (#4529)
Neutral form used.
2017-08-06 08:10:53 +09:00
MitarashiDango
5ee45fa571 fix columns_area.js (#4528) 2017-08-05 20:33:41 +02:00
Eugen Rochko
61a06eb328 Update goldfinger to 2.0.1, see tootsuite/goldfinger#5 (#4527) 2017-08-05 15:29:52 +02:00
Eugen Rochko
df605f0f8b Add "signed in as" header to some pages (#4523) 2017-08-05 04:24:58 +02:00
Quent-in
029786442a l10n update Occitan (#4522)
Salutation mailer + invalid reset link.
2017-08-05 01:31:27 +02:00
Ondřej Hruška
7b42d14f45 fix bug with data attrib for boost in home TL 2017-08-04 22:38:58 +02:00
Ondřej Hruška
f34f33c19e Add data- attributes to statuses for userstyle selectors (#117)
* Add data- attributes to statuses for userstyle selectors

* use const and template string, replace reblog->boosted and favourite->favourited

* more template strings because sorin-sama said so
2017-08-04 22:11:46 +02:00
Akihiko Odaki
9d1f8b9d6a Scroll columns area to right when children property is changed (#4517)
The feature to pin column could hide the rightmost column, which is
specified with children property of ColumnsArea.

The user is likely to see the column when the property changed, so scroll
the area in such cases.
2017-08-04 18:57:46 +02:00
Krzysztof Jurewicz
400616813e Fix some mistakes in Polish translation (#4495) 2017-08-04 22:43:28 +09:00
Eugen Rochko
724be2d5fe Ignore some locale keys that can but do not need to be translated (#4515) 2017-08-04 04:42:28 +02:00
Jeroen
76da330155 Dutch strings: typo (#4489) 2017-08-04 07:16:25 +09:00
Yamagishi Kazutoshi
ab60aa2266 Use GNU libiconv in Nokogiri (#4494)
System default libiconv of Alpine Linux only supports some charset (e.g. UTF-8).
Therefore, the preview card of the page which is not UTF-8 will be broken in the Docker environment.

Using GNU libiconv!
2017-08-03 18:04:36 +02:00
m4sk1n
0bbd5789b5 i18n: Update Polish translation (#4479)
* i18n: Update Polish translation

* Update Polish translation
2017-08-03 17:49:53 +02:00
Yamagishi Kazutoshi
fae71b653a Enable cache for babel-loader (#4505) 2017-08-03 17:46:49 +02:00
nullkal
dfcd2834f9 Redirect to PasswordController#new when reset_password_token is invalid (#4506) 2017-08-03 17:45:45 +02:00
MIYAGI Hikaru
09e86ef90b make number of comparison in emojify() fewer (#4500)
fix style

"©"削除処理をemojione_lightに移動
2017-08-02 21:05:17 +02:00
TheKinrar
9ba7d526a0 Don't normalize invalid domain names (#4499)
Fixes #4496
2017-08-02 14:54:33 +02:00
unarist
94e233e7b2 Fix column-back-button style for some browsers (#4484)
Use `text-align: unset` instead of `text-align: start` which Edge doesn't support for now.

Also remove default margin on Safari.
2017-08-02 13:09:37 +02:00
Sorin Davidoi
ac53736814 fix(status_list): Use correct keys for keyboard navigation (#4487) 2017-08-02 13:09:09 +02:00
Komic
8c0e78ae43 fr.json update (#4492) 2017-08-02 13:31:49 +09:00
asria-jp
26ab702304 Update Japanese Translation (authorize_follow) (#4481)
* Update Japanese Translation

* preserve "Unfortunately"("残念ながら")

preserve "Unfortunately"("残念ながら")
2017-08-02 11:49:25 +09:00
kibigo!
8b58153583 Fixed onClick column links 2017-08-01 13:46:52 -07:00
kibigo!
8150689b48 Merge upstream (#111) 2017-08-01 13:20:29 -07:00
Quent-in
7ef8482568 l10n #4457 update for Occitan language (#4483)
Previous and Next buttons
2017-08-02 00:12:30 +09:00
Eugen Rochko
559fd08845 Bump to 1.5.0 2017-08-01 15:12:07 +02:00
Jeroen
202942a76f Update Dutch (nl) strings (#4480) 2017-08-01 15:00:41 +02:00
ScienJus
c3e355388a Show SMTP_TLS in config sample (#4477) 2017-08-01 15:00:29 +02:00
Sorin Davidoi
d4c4820c03 fix(web_push_notifications): Send message with welcome notifications (#4471)
* fix(web_push_notifications): Send grouping title in welcome notification

* fix(web_push_notifications): Read message from last notification
2017-08-01 06:00:13 +02:00
Yamagishi Kazutoshi
e05606c8d0 Change logo URL for Heroku and Scalingo (#4476) 2017-08-01 05:59:11 +02:00
Yamagishi Kazutoshi
161f72cce3 Add libidn11 to Aptfile (#4475)
Fix builds that fail on Heroku.
2017-08-01 05:58:08 +02:00
Yamagishi Kazutoshi
8ccb3b96ab Re-add outline (#4474)
* Re-add outline

* respect default of web browser
2017-08-01 01:40:31 +02:00
Ondřej Hruška
e9ee249fd5 do not emojify tm, (R) and (C) (#4472) 2017-08-01 00:16:05 +02:00
Masoud Abkenar
4b6cd1dfdb i18n: update Persian translation (#4470)
* i18n: update Persian translation

* i18n: fix quotes
2017-07-31 19:40:57 +02:00
Yamagishi Kazutoshi
b9ec3b7e7c Load extra polyfills when isIntersecting is undefined (#4469) 2017-07-31 19:40:20 +02:00
Eugen Rochko
9b247c3d88 Bump to 1.5.0rc3 2017-07-31 15:28:36 +02:00
Eugen Rochko
c7cc806251 Simplify web UI character counter logic (#4463) 2017-07-31 15:19:48 +02:00
unarist
82b4cf4acb Fix button overflow on confirmation modal for mobile (#4465) 2017-07-31 15:19:30 +02:00
Yamagishi Kazutoshi
3e7a541e09 Change RuboCop rules to loose (#4464) 2017-07-31 15:19:13 +02:00
m4sk1n
93aafa8549 i18n: Update Polish translation (#4467) 2017-07-31 19:40:25 +09:00
Yamagishi Kazutoshi
bb85043f46 Disable sensitive button when with content warnings (#4460) 2017-07-31 05:06:56 +02:00
Eugen Rochko
e1fcad34a9 Fix length validator counting things that look like URIs like URLs (#4462)
URI.extract is too strong, not limited to URLs, matched real text.
Same issue was present in language detector.
2017-07-31 05:06:20 +02:00
unarist
155ba8fd3a Make salutation of email localizable (#4454) 2017-07-31 00:18:44 +02:00
Sorin Davidoi
e44f03bc71 Improve accessibility (part 7) (#4457)
* fix(media_modal): Keyboard navigation

* fix(column_back_button): Use native button

* fix(media_gallery): Keyboard navigation

* fix(status_content): Make CW content focusable
2017-07-31 00:18:15 +02:00
Gô Shoemake
b61e3daf98 Multiple frontend support (#110)
* Initial multiple frontend support

* Removed unnecessary require()

* Moved styles/images out of common
2017-07-30 19:28:21 +02:00
Ondřej Hruška
6ff084dbbb Improved notifications cleaning UI with set operations (#109)
* added notification cleaning drawer

* bugfix

* fully implemented set operations for notif cleaning

* i18n for notif cleaning drawer & improved logic slightly. Also added a confirm dialog

* - notif dismiss "overlay" now shoves the notif aside to avoid overlap
- added focus ring to header buttons
- removed notif overlay entirely from DOM if mode is disabled

* removed comment

* CSS tuning - inconsistent division lines fix
2017-07-30 12:36:28 -04:00
Yamagishi Kazutoshi
970297a138 Change to sensitive when adding content warning from web UI (#4456) 2017-07-30 16:27:57 +02:00
Yamagishi Kazutoshi
29abc9438c Remove outline from focused toot (#4448)
* Remove outline from focused toot

* change style
2017-07-30 16:27:51 +02:00
Yamagishi Kazutoshi
f91284d230 Do not create empty a element when there is no e-mail (#4455)
Empty a element is created when there is no business e-mail input.
2017-07-30 16:24:18 +02:00
unarist
feadf7553d Update Japanese translations (#4453) 2017-07-30 14:14:41 +02:00
Lynx Kotoura
ea33cdc30b Set contact address in about/more as mailto link (#4450)
* Set contact address as mailto link

To be able to copy long email address

* Set style for mailto link
2017-07-30 12:56:04 +02:00
masarakki
579e85f606 add-libpq-dev (#4445) 2017-07-30 01:05:27 +02:00
Damien Erambert
ea144ba302 Use a fainted text color for <hr> elements in the landing page (#4443)
* Use a fainted text color for <hr> elements in the landing page

* remove trailing whitespace
2017-07-29 21:20:52 +02:00
Sorin Davidoi
4f04981dde feat(tabs_bar): Avoid optimization for non-touch devices (#4444)
* fix(tabs_bar): Check if transition is necessary

* feat(tabs_bar): Only apply optimization for touch devices
2017-07-29 21:20:34 +02:00
Jeroen
990cea471e Dutch: Small fix (#4441) 2017-07-29 18:46:40 +02:00
Jeroen
0913351dcf Dutch: only one (new) string (#4440) 2017-07-29 23:54:27 +09:00
Lynx Kotoura
57a794d8eb Fix autocomplete option in haml files (#4438)
* Fix autocomplete in two_factor.html.haml

* Fix autocomplete in registrations edit.html.haml

* Fix autocomplete in passwords edit.html.haml

* Fix autocomplete in _registration.html.haml

* Fix autocomplete in new.html.haml

* Fix autocomplete in show.html.haml

* Add autocomplete option to sessions new.html.haml

* Add autocomplete option to 2FA new.html.haml

* Add autocomplete option to 2FA show.html.haml
2017-07-29 14:20:31 +02:00
unarist
a5e0cf2450 fix(status): Don't add tabIndex to wrapped status (#4437) 2017-07-29 14:20:05 +02:00
みたらしだんご
a46ba4a8f5 fix tabs_bar.js (#4436) 2017-07-29 14:19:49 +02:00
Sorin Davidoi
c71874b84c Improve accessibility (part 6) (#4435)
* fix(status_action_bar): Use aria-pressed for reblog and favourite button

* fix(column_back_button): Keyboard accessible

* fix(status_content): Make focusable and accessible

* fix(dropdown_menu): Use aria-expanded instead of aria-pressed

* fix(emoji_picker_dropdown): Use aria-expanded instead of aria-hidden

* feat(icon_button): Add aria-expanded

* fix(privacy_dropdown): Use aria-expanded instead of aria-hidden
2017-07-29 01:58:53 +02:00
David Yip
9aaf3218d2 Add commit_hash to instance presenter double (#107)
glitch-soc's about page grabs said value from InstancePresenter; the
double needs to emulate that.
2017-07-28 19:25:30 -04:00
Eugen Rochko
53b2b1b238 Count all URLs in text as 23 characters flat, do not count domain part of usernames (#4427)
* Count all URLs in text as 23 characters flat, do not count domain part of usernames

* Add new status text counting logic to web UI
2017-07-29 00:06:29 +02:00
Sorin Davidoi
634b71ed1d Accessability fixes (#4432)
* fix(modal_root): Read type from props, not from component

* fix(status_list): Do not user event.path
2017-07-28 22:55:19 +02:00
Sorin Davidoi
3d378ed0b4 fix(tabs_bar): Allow animation to end before navigating (#4429)
* fix(tabs_bar): Allow animation to end before navigating

* fix(tabs_bar): Do not use event.path
2017-07-28 22:55:13 +02:00
Sorin Davidoi
7e0c00a555 fix(mocha): Run all tests (#4433) 2017-07-28 22:53:16 +02:00
Eugen Rochko
f0bb2c6d1e Fix web push notifications "boost" icon not being loaded (regression from #4426) (#4431) 2017-07-28 20:26:54 +02:00
Eugen Rochko
13bb1ddc7f Use consistent icons for web push notifications, same as web UI (#4426) 2017-07-28 18:13:42 +02:00
Eugen Rochko
fdb65dcbee Bump to 1.5.0rc2 2017-07-28 17:22:41 +02:00
Eugen Rochko
4e2f2fab73 Fix guard clause in WebPushNotificationWorker (#4421) 2017-07-28 17:21:28 +02:00
Eugen Rochko
6e186b9c77 When PuSH subscribe attempts are exhausted, unsubscribe (#4422) 2017-07-28 17:21:14 +02:00
Quent-in
ff9d344d4c i18n update of OC version (#4425)
* Correction of misspellings

* Improve the OC version

* Improve of the OC version

* Improvement of OC version
2017-07-29 00:09:38 +09:00
Eugen Rochko
b3c44e95a9 Improve actions modal style (#4423) 2017-07-28 15:30:53 +02:00
Eugen Rochko
8c0dd33ce4 Fix current session not being displayed in sessions list (#4424) 2017-07-28 15:30:06 +02:00
Yamagishi Kazutoshi
12874eafa6 Unify webpackChunkName to lowercase (#4412) 2017-07-28 13:34:55 +02:00
Lynx Kotoura
afb593b44e Fix protruded infomation board section in about/more page (#4415)
* Fix protruded infomation board section

Set "flexwrap: wrap" for sections. Set upper and lower padding for each section, and adjust clearances around sections accordingly. Settings for viewport threshold 500px is no more needed.

* Fix mistake of reducing information-board padding

In according with this fix, additional padding setting  for maximum 840px width is no more needed.
2017-07-28 13:34:44 +02:00
Sorin Davidoi
296bfa23aa fix(actions-modal): Inconsistent margins (#4418) 2017-07-28 13:34:06 +02:00
Sorin Davidoi
534da4f24f Improve accessibility (part 5) (#4417)
* fix(status): Add tabIndex=0

* fix(status_list): Make keyboard navigable
2017-07-28 13:33:40 +02:00
Yamagishi Kazutoshi
62a9da62a6 Fix column header in landing page (regression from #4405) (#4416) 2017-07-28 13:32:41 +02:00
Yamagishi Kazutoshi
58eea59864 Fix broken PropTypes (#4413) 2017-07-28 13:32:05 +02:00
m4sk1n
c7de92e0df i18n: Update Polish translation (#4414)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-28 17:53:44 +09:00
Yamagishi Kazutoshi
c1633eeb0f Update webpack and related dependencies (without extract-text-webpack-plugin) (#4410) 2017-07-28 05:16:12 +02:00
Yamagishi Kazutoshi
f93f306053 Remove hash from chunk filename when dev env (#4411) 2017-07-28 05:14:01 +02:00
Sorin Davidoi
e67fc997dc feat(web_push_notifications): Group notifications (#4388) 2017-07-28 05:06:22 +02:00
Sorin Davidoi
3e01a7e677 fix(web_push_notification): Do not hard reload tab (#4380)
* fix(web_push_notification): Do not hard reload tab

* fix(web_push_notification_worker): Guard against null subscription

* refactor: Incorporate pull request feedback
2017-07-28 05:06:01 +02:00
Akihiko Odaki
0f92119ceb Cover Api::V1:FavouritesController more (#4409) 2017-07-28 04:37:44 +02:00
Sorin Davidoi
b7d47c2aef Improve accessibility (part 4) (#4408)
* fix(dropdown_menu): Keyboard navigation

* fix(icon_button): Add aria-pressed attribute

* fix(privacy_dropdown): Make accessible

* fix(emoji_picker_dropdown): Make accessible

* fix(icon_button): Support tabIndex

* fix(actions_modal): Remove icon from tab order

* fix(dropdown_menu): Add role=group

* fix(setting_toggle): Toggle via space key

* fix(dropdown_menu): Remove redundant handling of Space key

* fix(emoji_picker_dropdown): Remove redundant Space key handling

* fix(privacy_dropdown): Remove redundant Space key handling

* fix(status): Switch to article and add aria-posinset, aria-setsize

* fix(status_list): Use role=feed and pass more ARIA props to Status

* chore(eslint): jsx-a11y/role-supports-aria-props
2017-07-28 04:37:30 +02:00
Eugen Rochko
6270f9ce34 Fix #3189 - Share CSRF secret between main app and Sidekiq Web (#4383) 2017-07-28 04:36:42 +02:00
Satoshi KOJIMA
e54cc15cbd fix #4356 : place sw.js to assets/sw.js (#4357) 2017-07-28 01:55:52 +02:00
Ratmir Karabut
2654f3be82 Update Russian translation (#4407)
* Add Russian translation (ru)

* Fix a missing comma

* Fix the wording for better consistency

* Update Russian translation

* Arrange Russian setting alphabetically

* Fix syntax error

* Update Russian translation

* Fix formatting error

* Update Russian translation

* Update Russian translation

* Update ru.jsx

* Fix syntax error

* Remove two_factor_auth.warning (appears obsolete)

* Add missing strings in ru.yml

A lot of new strings translated, especially for the newly added admin section

* Fix translation consistency

* Update Russian translation

* Update Russian translation (pluralizations)

* Update Russian translation

* Update Russian translation

* Update Russian translation (pin)

* Update Russian translation (account deletion)

* Fix extra line

* Update Russian translation (sessions)

* Update Russian translation
2017-07-28 08:16:30 +09:00
Sorin Davidoi
9004151e34 feat: Web Share for detailed status and account (#4402)
* feat: Web Share for detailed status and account

* fix(account/action_bar): Move share under mention
2017-07-28 00:55:15 +02:00
Sorin Davidoi
6884dd79ba Improve accessibility (part 3) (#4405)
* fix(compose): Add aria-label for the navigation links

* fix(search): Add input label

* fix(navigation_bar): Link description

* fix(autosuggest_textarea): Add input label

* fix(compose_form): Add input label

* fix(upload_button): Add input label

* fix(account/header): Add link content

* fix(column_header): Use h1 tag

* fix(column_header): Labels move buttons moving column

* fix(settings_text): Add label to input

* fix(column_header): Remove role from h1

* fix(modal_root): Use role=dialog

* fix(modal_root): Focus restauration

* fix(modal_root): Apply inert to sibligs

* fix(column_header): Add role=button

* chore(eslint): Disable jsx-a11y/label-has-for
2017-07-28 00:54:48 +02:00
Sorin Davidoi
f9075577e4 fix(actions_modal): Warning about missing prop (#4406) 2017-07-27 23:01:50 +02:00
Sorin Davidoi
50d38d7605 fix(dropdown_menu): Open as modal on mobile (#4295)
* fix(dropdown_menu): Open as modal on mobile

* fix(dropdown_menu): Open modal on touch

* fix(dropdown_menu): Show status

* fix(dropdown_menu): Max dimensions and reduce padding

* chore(dropdown_menu): Test new functionality

* refactor: Use DropdownMenuContainer instead of DropdownMenu

* feat(privacy_dropdown): Open as modal on touch devices

* feat(modal_root): Do not load actions-modal async
2017-07-27 22:31:59 +02:00
Yamagishi Kazutoshi
aa803153e2 Remove Storybook (#4397) 2017-07-27 22:30:27 +02:00
Yamagishi Kazutoshi
f2233c3e25 Update intersection-observer to version 0.4.0 (#4398) 2017-07-27 16:20:48 +02:00
unarist
73890c3cac Fix timeline height on landing page for Safari (#4392)
`height: 100%` in `align-self: stretch` flexboxes doesn't work on Safari < 11.

https://bugs.webkit.org/show_bug.cgi?id=137730

This workaround uses flexbox instead of `height: 100%` to stretch height.
2017-07-27 15:49:56 +02:00
Sorin Davidoi
e1798d0eb0 fix(push_subscription): Use star icon for favourite action (#4396) 2017-07-27 15:46:33 +02:00
Akihiko Odaki
4f0b638cda Introduce access token fabricators (#4401) 2017-07-27 15:16:07 +02:00
Yamagishi Kazutoshi
bb96ba13cf Fallback to site_hostname when site_title is empty (#4394) 2017-07-27 15:13:49 +02:00
Daniel Hunsaker
5bf4838e2f [nanobox] Minor tweaks for 1.5 (#4395)
- Be less strict about the Ruby version, which resolves a build failure.
- Add libidn as a dependency (until Nanobox adds idn-ruby to the list of gems with a dependency on it).
- Remove redundant bundler commands (Nanobox's Ruby engine handles these things cleanly on its own, now).
2017-07-27 15:13:32 +02:00
Akihiko Odaki
bdf573d140 Remove redundant fabrication in the spec for Api::V1::FavouritesController (#4391) 2017-07-27 15:13:10 +02:00
Yamagishi Kazutoshi
97a48f237d Add default settings for user (#4393) 2017-07-27 15:12:51 +02:00
nullkal
6654c30033 Fix an error when a user tries to search nonexistent remote user (regression from #4275) (#4400) 2017-07-27 15:11:59 +02:00
Ondřej Hruška
f49339ca9c Fix multipoint shortcode bug (#4387)
* Fix multipoint shortcode bug

* Added testcase for composite emoji shortcode
2017-07-27 01:02:15 +02:00
Ondřej Hruška
cb69e35b3b Add visibility icon to Detailed status 2017-07-27 00:41:28 +02:00
Clworld
994d948c39 Add callback_url/acct information for Sidekiq PuSH workers Exception. (#4281)
* Add destination informations to exception on SubscribeWorker and DeliveryWorker.

* Simplify delivery error message.

* Prevent changing Exception type...

* fix typo.
2017-07-27 00:38:20 +02:00
Jeroen
f5e228ad2e Update Dutch strings (#4389) 2017-07-27 00:37:35 +02:00
Eugen Rochko
92cb451da8 Fix #4026 - Accept backup codes for disabling 2FA (#4382) 2017-07-26 23:36:33 +02:00
Eugen Rochko
55bee84c97 Fix infinite scroll fluidity (#4381) 2017-07-26 23:35:03 +02:00
Sorin Davidoi
a248be4fce fix(columns_area): Manually set tabs style when swiping (#4320) 2017-07-26 19:03:56 +02:00
Sorin Davidoi
8b43d6bf9c fix(web_push_notification_worker): Guard against deleted notifications (#4379) 2017-07-26 16:14:39 +02:00
Sorin Davidoi
b8adb4d7fa fix(column): Crash when heading is undefined (#4378) 2017-07-26 15:03:23 +02:00
mayaeh
4ba33f99fc Update Japanese translations (#4376)
* Update Japanese translations for remote profile, settings toggle button and web share button.

* Update Japanese translation for remote profile.
2017-07-26 21:57:51 +09:00
m4sk1n
7905739c2a s/PubSubHubbub/WebSub/g (#4372)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-26 13:47:41 +02:00
Sorin Davidoi
6a6a62f13f Improve accessibility (part 2) (#4377)
* fix(column_header): Invalid ARIA role

* fix(column): Remove hidden nodes from the DOM

* refactor(column_link): Remove unused property hideOnMobile

* fix(column_header): Use aria-pressed

* fix(column_header): Make collapsed content not focusable, add focusable property

* fix(column_loading): Make header non-focusable

* fix(column_settings): Use role to group the toggles
2017-07-26 13:46:53 +02:00
Lynx Kotoura
aa8fa71df6 Fix padding in hero container of landing page (#4373)
* Fix padding in hero container, landing page

Erase hero container padding to fit registration form to full width. By this setting, heading padding disappears, so I adjust heading padding.

* Specify attribute strictly for heading padding

To overwrite padding-bottom in superior settings.

* Make padding shorthand more concise
2017-07-26 13:45:49 +02:00
m4sk1n
7874c6d630 i18n: Update Polish translation (#4371) 2017-07-26 16:09:16 +09:00
Akihiko Odaki
7bf0afb1dc Add index favourites on account_id and id (#4360) 2017-07-26 03:35:25 +02:00
Sorin Davidoi
2f8bfb3d38 Improve accessibility (#4369)
* fix(compose): Use nav and remove redundant aria-label

* fix(tabs_tab): Use nav and add aria-label

* fix(app): Add aria-label for settings toggle button

* chore: Run yarn manage:translations
2017-07-26 02:01:27 +02:00
Ondřej Hruška
4115043dc7 Added external link icons to joinmastodon navbar buttons (#4368) 2017-07-26 02:00:36 +02:00
m4sk1n
7062cb764f i18n: Update Polish translation (#4367) 2017-07-25 23:56:04 +02:00
Ondřej Hruška
e82021e0e6 Merge pull request #97 from glitch-soc/LESS_UGLY_LANDING_PAGE_TEXT_CSS
Landing page CSS fixes
2017-07-25 23:32:59 +02:00
Ondřej Hruška
8925731c98 Improve landing page CSS and text contrast 2017-07-25 23:30:31 +02:00
Ondřej Hruška
4c233b4f3a Add .fa-external-link to instance list link 2017-07-25 23:30:13 +02:00
Ondřej Hruška
b7cf758fbe Fix JS errors and add back commit hash 2017-07-25 21:54:12 +02:00
Ondřej Hruška
7e5691804d Merge git://github.com/tootsuite/mastodon into tootsuite-master 2017-07-25 21:36:22 +02:00
Lynx Kotoura
9891ff80f9 Adjust mobile landing page (#4366)
* Adjust mobile landing page

Change mobile viewport threshold to 840px in consideration of padding. Fix loss of "container hero" padding in about/more under 675px.

* Fix indent
2017-07-25 21:31:56 +02:00
Sorin Davidoi
7232cdf7e8 feat(status_action_bar): Web share button (#4365) 2017-07-25 20:38:39 +02:00
Lynx Kotoura
9f97c8c750 Fix multiple-gif display in user page (#4364)
Divide media-items to equal width, and hide overlaps.
2017-07-25 19:12:57 +02:00
Clworld
edadc93757 Allow longer link text on landing page (reduce padding for Mobile) (#4363)
* Allow longer link text on landing page (reduce padding for Mobile)

* remove empty line
2017-07-25 16:20:26 +02:00
Akihiko Odaki
a6ea7e282f Merge queries in Api::V1::FavouritesController (#4359) 2017-07-25 16:01:03 +02:00
Yamagishi Kazutoshi
e5c0aa6493 Send short account URL to remote instance (regression from #3844) (#4355)
* Send short account URL to remote instance (regression from #3844)

Resolve #4347

* remove format: 'json'
2017-07-25 15:59:43 +02:00
Clworld
02744f29ef Update Japanese translations for landing page (for Mobile). (#4362) 2017-07-25 22:57:40 +09:00
m4sk1n
a31d24ee18 i18n: Update Polish translation (#4361) 2017-07-25 22:39:49 +09:00
Komic
6957c5b5c6 Small fr.json update (#4354)
* Small fr.json update

• Translation for #4342
• Fixed a couple of typos as well as a few weird phrasings

* insert final newline
2017-07-25 12:01:47 +09:00
Sorin Davidoi
696bcff6bf fix(status_list): Guard against missing ref (#4353) 2017-07-25 02:13:05 +02:00
Jeroen
f52ce92f2b Update and (almost) completing Dutch strings for 1.5 (#4351)
* Update and expanding Dutch strings for 1.5

Only privacy policy is not translated, but is included. Hopefully I have time to translate this another time (you can set your own privacy policy now anyway).

* Missing file

* A few changes cause of changes in another file

* Fix

* Update Dutch strings for 1.5

* nl

* fix

don't know how that ended up there

* Update nl.yml
2017-07-25 02:12:50 +02:00
Petko Bordjukov
c80046a77b Update Capistrano lock version in config/deploy.rb (#4352)
Deployment with Capistrano was failing due to a mismatch between the loaded
version of Capistrano and the one specified in the deployment
configuration. Bumping the version in config/deploy.rb fixes this issue.
2017-07-25 01:14:01 +02:00
Sorin Davidoi
ebf5a06084 fix(column): Use passive wheel listener (#4348)
* chore(yarn): Install detect-passive-events

* fix(column): Use passive listener
2017-07-25 01:05:51 +02:00
Sorin Davidoi
23e854cb91 fix(components/bundle): Exit early in load function (#4349) 2017-07-25 01:05:44 +02:00
Sorin Davidoi
de105d64d5 fix(getting_started): No scrollbar if not needed (#4350) 2017-07-25 01:05:35 +02:00
Eugen Rochko
07d93716aa Improve remote profile disclaimer (#4342)
* Improve remote profile disclaimer

* yarn run manage:translations
2017-07-24 20:05:29 +02:00
Eugen Rochko
88b5e0b703 Use the "m" version of the logo for favicons (#4345) 2017-07-24 19:54:59 +02:00
Sorin Davidoi
32fa312b2a fix(compose_from): Do not autofocus on mobile (#4344) 2017-07-24 19:54:39 +02:00
Eugen Rochko
1306d637a2 Fix oddities with public profile CSS (#4341)
* Fix oddities with public profile CSS

* Remove trailing whitespace
2017-07-24 17:56:50 +02:00
m4sk1n
462b3752e4 i18n: @2374d63 (pl) (#4340)
* i18n: @2374d63 (pl)

* i18n: fixed typo on Polish translation
2017-07-24 17:15:35 +02:00
Eugen Rochko
029f2c4545 Redesign /terms page (#4338)
* Redesign /terms page

* Fix tests

* Remove unused keys
2017-07-24 17:15:15 +02:00
Lynx Kotoura
b3e7beb7c5 Perfectly center mastodon logo on mobile landing page (#4339)
Setting only padding-left made the logo left-shifted on mobile page. Margin-right was old setting for adjusting clearance between old logo and letter "Mastodon".
2017-07-24 16:52:47 +02:00
Eugen Rochko
a549d1ae6b Bump to 1.5.0rc1 (#4318) 2017-07-24 16:21:08 +02:00
Eugen Rochko
467456f7a1 Move clean up of unconfirmed users to sidekiq-scheduler (#4336)
* Move clean up of unconfirmed users to sidekiq-scheduler

* mastodon:daily is now deprecated
2017-07-24 16:17:55 +02:00
Eugen Rochko
2374d63536 Redesign extended information page (#4322)
* Redesign extended information page

* Remove unused keys

* Add $ui-base-lighter-color variable to SCSS
2017-07-24 15:09:08 +02:00
Yamagishi Kazutoshi
117eb3b2bc Change defaultMessage for spoiler placeholder (#4329) 2017-07-24 14:49:06 +02:00
Eugen Rochko
de985a30bc Run i18n-tasks normalize (#4335) 2017-07-24 14:30:10 +02:00
sdukhovni
06d905f415 Allow domain blocks that only reject media without silencing or suspending (#4325)
* Allow domain blocks to reject media without silencing or suspending

* Fix typo

* Hide 'Reject media' button when superfluous, instead of disabling it

* Properly hide 'reject media' checkbox on page load if needed

This may happen when resubmitting the domain block form after an error.

* Don't ask whether undoing a media-only block should be retroactive

* Rename :media_only block to :noop

* Display :noop block as None in frontend, allow blocks that do nothing

* Remove 'coding' line auto-added by emacs
2017-07-24 14:26:55 +02:00
Andrew Zyabin
0ad41be0f3 Update Russian translation for #4313 (#4334) 2017-07-24 13:37:05 +02:00
masarakki
d6f5dbff3e add-dependent-package (#4330) 2017-07-24 10:51:38 +02:00
Yamagishi Kazutoshi
1e665a0bf4 Add space to toot paragraph in landing page (#4331) 2017-07-24 10:43:59 +02:00
Yamagishi Kazutoshi
ef16089c6d Redirect user profile page to short URL when HTML format (#4328) 2017-07-24 08:46:29 +02:00
m4sk1n
4b4ea1f929 i18n: Update Polish translation (#4327)
* i18n: Update Polish translation

* i18n: Update Polish translation
2017-07-23 23:57:13 +02:00
Yamagishi Kazutoshi
45af29912f Change about_mastodon to about_mastodon_html for all locales (#4326) 2017-07-23 23:32:53 +02:00
Sorin Davidoi
9075c90c46 feat: Enable push subscription for mobile devices by default (#4321) 2017-07-23 23:27:23 +02:00
Yamagishi Kazutoshi
63a2566007 Fix manifest description (regression from #4122) (#4324) 2017-07-23 23:04:44 +02:00
Yamagishi Kazutoshi
43cad817e8 Update Japanese translations for #4313 and #4315 (#4323)
* Update Japanese translations for #4313

* Update Japanese translations for #4315
2017-07-23 23:04:25 +02:00
Eugen Rochko
ed4c754fff Add background to media modal (#4317) 2017-07-23 20:28:31 +02:00
Eugen Rochko
1e0c7a0afc Add loading indicator animation (#4316) 2017-07-23 20:28:18 +02:00
Eugen Rochko
3a3b556065 Improve noscript message and style (#4315) 2017-07-23 20:28:06 +02:00
Sylvhem
9244f6b628 Change "Content Warning" to "Write your warning here" (#4313)
Change the placeholder used in the content warning field from "Content Warning" to "Write your warning here". This change should made it easier to understand what the field is about.

Change le message de substitution utilisé dans le champ d’avertissement de « Avertissement » à « Écrivez ici votre avertissement ». Ce changement devrait rendre plus évidente la fonction du champ.
2017-07-23 16:57:32 +02:00
Sorin Davidoi
ff26b72333 fix(getting_started): Scroll on mobile (#4312) 2017-07-23 14:50:26 +02:00
m4sk1n
6803935c4d i18n: Update Polish translation (#4311) 2017-07-23 14:50:10 +02:00
Sorin Davidoi
3757546f1b fix(columns_area): Show correct title for preview columns (#4310) 2017-07-23 14:03:35 +02:00
Quent-in
a677ac8384 i10n Update for Occitan (#4301)
* Update to translate new strings

* Update

* Update

* Indend added

* Update oc.yml
2017-07-23 12:46:40 +02:00
Yamagishi Kazutoshi
bdbfb10cff Fix logo URL (#4309) 2017-07-23 12:46:10 +02:00
Yamagishi Kazutoshi
4d661e1183 Change logo.png to logo.svg for web UI (#4307) 2017-07-23 11:09:42 +02:00
Eugen Rochko
dd28b557ae Update README header with logo 2017-07-23 02:46:10 +02:00
Eugen Rochko
0e0f18ce7c New logo (#4306)
* Add new logo

* Remove unused images
2017-07-23 02:40:39 +02:00
Eugen Rochko
7964bfccdb Add rake task mastodon:feeds:build to regenerate all active users' feeds (#4303) 2017-07-23 01:15:04 +02:00
Eugen Rochko
3c515f2cd2 Run yarn run manage:translations and i18n-tasks normalize (#4302) 2017-07-23 01:14:57 +02:00
Ondřej Hruška
852acbd738 amend 3ba7c1e7 to actually do what I wanted to do *facepalm* 2017-07-22 22:32:03 +02:00
Ondřej Hruška
db73ac92d7 Changed landing page cloud animation to be more relaxed and smoother (#4304) 2017-07-22 22:27:06 +02:00
Ondřej Hruška
6913426e48 Merge pull request #92 from tootsuite/master
tiny upstream merge
2017-07-22 22:20:17 +02:00
Ondřej Hruška
3ba7c1e725 Textarea does not auto-expand on mobile anymore (autofocus only on desktop) 2017-07-22 21:48:56 +02:00
Ondřej Hruška
9b74a12045 Adjust margins for mobile with navbar-under 2017-07-22 20:41:21 +02:00
Damien Erambert
4cd82d442e Update French locales (settings, ToS, & co) (#4261)
* add fr-FR locales to the landing page

* moar french locales

* terms of service/privacy policy

* remove un-used locales in the client (yarn manage:translations)

* update french locales in the client

* remove duplicate locales in fr.yml

* fix typos per PR comments in fr.yml

* put back default messages

* translate untranslated keys on the client

* add "push" after notifications

* correctly ident ToS in fr.yml
2017-07-22 20:28:40 +02:00
Ondřej Hruška
74a0cc6a11 Added settings toggle to move the navbar at the bottom in mobile view (#93) 2017-07-22 19:51:34 +02:00
Eugen Rochko
311871eefc Turn clouds on landing page into SVGs, use SCSS variables to color them (#4299) 2017-07-22 16:36:54 +02:00
Eugen Rochko
a929f7e6ac Fix #4172 - Mutes now block notifications (#4300) 2017-07-22 16:36:36 +02:00
Sylvhem
cf51e07bde Mise à jour de la traduction de la page d’accueil (#4294)
* Mise à jour de la traduction de la page d’accueil

Met à jour les traductions et ajoute les chaînes manquantes suite à la refonte de la page d’accueil.

Update translations and add the strings introduced by the landing page's redesign.

* Correction d’erreurs

Corrige l’indentation de certaines clefs et supprime admin.settings.setting qui n’existe plus dans le projet original.

Fix some indentations and delete admin.settings.setting, which have been removed upstream.
2017-07-22 14:11:46 +02:00
beatrix-bitrot
984d2d4cb6 Merge that good fresh upstream shit 2017-07-22 01:16:53 +00:00
Eugen Rochko
8d6c3cd48a Fix sessions being replaced needlessly (#4292) 2017-07-22 01:09:10 +02:00
Ondřej Hruška
0244019ca1 Fixed horrible outline around notif clearing checkbox & moved the overlay to a more sr-friendly place 2017-07-21 21:12:43 +02:00
Ondřej Hruška
604654ccb4 New notification cleaning mode (#89)
This PR adds a new notification cleaning mode, super perfectly tuned for accessibility, and removes the previous notification cleaning functionality as it's now redundant.

* w.i.p. notif clearing mode

* Better CSS for selected notification and shorter text if Stretch is off

* wip for rebase ~

* all working in notif clearing mode, except the actual removal

* bulk delete route for piggo

* cleaning + refactor. endpoint gives 422 for some reason

* formatting

* use the right route

* fix broken destroy_multiple

* load more notifs after succ cleaning

* satisfy eslint

* Removed CSS for the old notif delete button

* Tabindex=0 is mandatory

In order to make it possible to tab to this element you must have tab index = 0. Removing this violates WCAG and makes it impossible to use the interface without good eyesight and a mouse. So nobody with certain mobility impairments, vision impairments, or brain injuries would be able to use this feature if you don't have tabindex=0

* Corrected aria-label

Previous label implied a different behavior from what actually happens

* aria role localization & made the overlay behave like a checkbox

* checkboxes css and better contrast

* color tuning for the notif overlay

* fanceh checkboxes etc and nice backgrounds

* SHUT UP TRAVIS
2017-07-21 20:33:16 +02:00
Sorin Davidoi
3817704806 feat(compose): Change media query for compact mode to max-height: 400px (#4291) 2017-07-21 14:22:29 +02:00
Yamagishi Kazutoshi
d4c6bf770d Run bundle (regression from #4284) (#4290) 2017-07-21 14:22:05 +02:00
Eugen Rochko
399f9f4a4e Update Goldfinger gem to 2.0 (#4286)
- No masking of HTTP::Error and OpenSSL::SSL::SSLError
- No longer accepts non-HTTPS WebFinger endpoints
2017-07-21 13:40:48 +02:00
Yamagishi Kazutoshi
f2390e2803 Dynamicaly change avatar and header bg from account setting (#4289) 2017-07-21 12:47:16 +02:00
Eugen Rochko
dbaa6a0e13 Fix intermittent test failures due to accidentally reused class instance between threads (#4287) 2017-07-21 12:45:13 +02:00
Eugen Rochko
7bf7ed6123 Fix #3220 - Show avatar/header on edit profile page (#4288) 2017-07-21 05:28:51 +02:00
Eugen Rochko
a390abdefb Use the same emoji data on the frontend and backend (#4284)
* Use the same emoji data on the frontend and backend

* Move emoji.json to repository, add tests

This way you don't need to install node dependencies if you only
want to run Ruby code
2017-07-21 04:27:40 +02:00
Sorin Davidoi
c1bc5e14eb feat(compose): More space on mobile devices (#4282)
* feat(compose): More space on mobile devices

* feat(compose): Hide navigation when typing on mobile devices

* fix(compose): Make animation faster

* fix(navigation_bar): Remove hardcoded title

* fix(compose): Prevent accidental bluring

* fix(compose): Increase max-height to 600px
2017-07-21 01:38:24 +02:00
beatrix
0efd7e7406 disable about page timeline 2017-07-20 17:26:00 -04:00
Eugen Rochko
4b911fea03 Add button to re-subscribe to all accounts from a domain (#4285) 2017-07-20 23:07:13 +02:00
beatrix
e7edb4d1ee Merge pull request #87 from tootsuite/master
merge upstream
2017-07-20 11:24:32 -04:00
kibigo!
d235224692 Restructured local settings internals 2017-07-19 20:41:28 -07:00
Eugen Rochko
1fcdaafa6f Fix webfinger retries (#4275)
* Do not raise unretryable exceptions in ResolveRemoteAccountService

* Removed fatal exceptions from ResolveRemoteAccountService

Exceptions that cannot be retried should not be raised. New exception
class for those that can be retried (Mastodon::UnexpectedResponseError)
2017-07-20 01:59:07 +02:00
Sorin Davidoi
f24b81e27f feat(css): Remove border radius from WebKit's scrollbars (#4278) 2017-07-20 00:20:54 +02:00
Sorin Davidoi
e01966f7b8 fix(status_list): Update scroll position after toots are appended (#4277) 2017-07-19 22:38:26 +02:00
Sorin Davidoi
dcb9497148 fix(push_subscription): Guard against malformed favourite notification (#4271) 2017-07-19 22:37:52 +02:00
unarist
4f2513337f Update scroll top state on column mounting (#4276) 2017-07-19 20:26:51 +02:00
Sorin Davidoi
015269914e fix: Handle errors without response (#4274) 2017-07-19 19:38:50 +02:00
Eugen Rochko
bbdcfd6baf Add ActivityPub attributes to accounts (#4273) 2017-07-19 17:06:46 +02:00
Akihiko Odaki
f0d6550f16 Fix logging in ProcessFeedService (#4270) 2017-07-19 16:02:03 +02:00
Eugen Rochko
8400bee3b1 Refactor ResolveRemoteAccountService (#4258)
* Refactor ResolveRemoteAccountService

* Remove trailing whitespace

* Use redis locks around critical ResolveRemoteAccountService code

* Add test for race condition of lock
2017-07-19 14:44:04 +02:00
Cygnan
bc1f9dc24b Fix a typo in public.js (double semicolons) (#4269)
Signed-off-by: Cygnan <mail@cygnan.com>
2017-07-19 13:35:22 +02:00
m4sk1n
cdc349a2d1 i18n: Update Polish translation (#4268) 2017-07-19 12:25:06 +02:00
Eugen Rochko
c2c93f8cd6 Fix deprecation warning for non-pipeline assets (#4267) 2017-07-19 12:00:05 +02:00
m4sk1n
9fc082ea81 i18n: @716f4cb (pl) (#4266)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-19 10:49:36 +02:00
Damien Erambert
4c7a9adb98 fix missing closing <p> tag in ToS in en.yml (#4262) 2017-07-19 09:43:46 +02:00
m4sk1n
030e5cec58 Fix missing closing <p> tag in ToS in .yml files (#4264)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-19 09:43:10 +02:00
Yamagishi Kazutoshi
716f4cb11c Add feature to revoke sessions (#4259) 2017-07-19 04:59:04 +02:00
Eugen Rochko
a5a07da892 Correct OStatus inflection (Ostatus -> OStatus) (#4255) 2017-07-19 01:37:26 +02:00
Sorin Davidoi
72108b20e2 feat(emoji): Add back title attribute (#4253) 2017-07-18 22:49:24 +02:00
kibigo!
0a678cf377 Fix for stylesheet split 2017-07-18 11:28:52 -07:00
Surinna Curtis
7a77f7b3bb Add sourceRoot/includePaths to loaders
Use the settings modal as an example/testcase
2017-07-18 11:27:48 -07:00
lindwurm
767117f9b0 Update Japanese translation (chase #4249) (#4251)
Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-07-18 20:03:18 +02:00
m4sk1n
fb7f06a752 i18n: Update Polish translation (#4252)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-18 20:03:06 +02:00
Ondřej Hruška
df74e26baf Merge branch 'tootsuite-master' 2017-07-18 18:59:00 +02:00
Ondřej Hruška
d69fa9e1f4 Merge changes from upstream with the CSS reload fix 2017-07-18 18:58:47 +02:00
lindwurm
0b4006fc47 Add Japanese translation for #4246 (#4249)
Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-07-18 18:43:19 +02:00
lindwurm
0ccd47f413 Update Japanese translations for new landing page (#4159)
Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-07-18 18:42:59 +02:00
Ondřej Hruška
02f896c12e Fix broken CSS reload due to webpack upgrade (#4250) 2017-07-18 18:42:45 +02:00
unarist
bb4c3831b2 Add Japanese translation for #4199 (#4247) 2017-07-18 17:23:35 +02:00
Yamagishi Kazutoshi
3267e4a785 Add unfollow modal (optional) (#4246)
* Add unfollow modal

* unfollowing someone

* remove unnecessary prop
2017-07-18 17:14:43 +02:00
Akihiko Odaki
89b988cab5 Introduce Ostatus name space (#4164)
* Wrap methods of ProcessFeedService::ProcessEntry in classes

This is a change same with 425acecfdb, except
that it has the following changes:

* Revert irrelevant change in find_or_create_conversation
* Fix error handling for RemoteActivity

* Introduce Ostatus name space
2017-07-18 16:39:47 +02:00
abcang
4d42a38954 Improve admin page (#4121)
* Improve admin page

* Fix test

* Add spec

* Improve select style
2017-07-18 16:38:22 +02:00
Sorin Davidoi
8387b3928e fix(push-subscriptions): Refactor how Sidekiq jobs are handled (#4226) 2017-07-18 16:25:40 +02:00
abcang
afa52e4d63 Fixed issue that the NSFW image is not hidden on detail page (#4244) 2017-07-18 15:24:57 +02:00
Yamagishi Kazutoshi
8949aad030 Add empty alt attribute to img elements in landing page (#4243) 2017-07-18 15:22:09 +02:00
Yamagishi Kazutoshi
c0c7af2194 Download npm and yarn from Alpine packages repository (#4242) 2017-07-18 15:21:30 +02:00
Yamagishi Kazutoshi
f5382ec085 Exclude self toots from regular expression filter (#4245)
* Exclude self toots from regular expression filter

* refactor
2017-07-18 15:20:38 +02:00
Yamagishi Kazutoshi
407073d7a2 Move icu-dev to build-dependencies (#4240) 2017-07-18 15:19:49 +02:00
Yamagishi Kazutoshi
7f4375822a Install libidn11-dev in Vagrant (#4238) 2017-07-18 00:31:43 +02:00
Sorin Davidoi
719ab720a7 feat(push-notifications): Open link in current tab if possible (#4228)
* fix(push-notification): Open link in current tab if possible

* feat(sw): Skip waiting and claim clients
2017-07-18 00:19:17 +02:00
Akihiko Odaki
b11ac88692 Require any modules after loading polyfill in entry points (#4231)
app/javascript/mastodon/main.js delayed the execution of modules,
but other entry points didn't. That leads to failure in executing
modules, which requires those polyfills.

Strictly enforce the rule to require any modules after loading
polyfill in entry points.
2017-07-18 00:19:02 +02:00
Ondřej Hruška
c727eae441 Updated from tootsuite 2017-07-17 20:03:57 +02:00
Lynx Kotoura
681c33d1f4 Quit overwriting style of extended-video-player (#4237)
Though size of extended-video-player is already fixed to 80vw*80vh in components.scss, player size was also set to original video size in extended_video_player.js. Video size is fixed to 80vw*80vh, so video player's size must also be fixed to 80vw*80vh.
2017-07-17 19:05:29 +02:00
Akihiko Odaki
7f35947d8e Improve a query in WebPushNotificationWorker (#4234) 2017-07-17 11:03:48 +02:00
Akihiko Odaki
68941d4dfa Use update method to update session in WebPushNotificationWorker (#4235) 2017-07-17 11:03:42 +02:00
Sorin Davidoi
1d2616b79b fix(emojis): Handle multipoint emojis (e.g. country flags) (#4221) 2017-07-17 10:57:45 +02:00
Eugen Rochko
d4b097a88c Add missing ActivityPub representations (#4230)
- Follow, undo follow
- Accept follow, reject follow
- Like, undo like
- Block, undo block
- Delete (note)
- Update (actor)
2017-07-17 10:57:04 +02:00
m4sk1n
902c5cf7ca i18n: update (pl) (#4233)
* i18n: update (pl)

* Update pl.yml
2017-07-17 10:24:53 +02:00
Akihiko Odaki
b15f790221 Require images in common.js (#4232) 2017-07-17 10:23:28 +02:00
Eugen Rochko
a47c2e8890 Add public key declaration to actors (#4215) 2017-07-17 02:37:27 +02:00
kibigo!
d0aad1ac85 Documentation and cleanup 2017-07-16 17:13:16 -07:00
kibigo!
21b04af524 Fixes lack of <tbody> in header metadata table 2017-07-16 16:49:12 -07:00
Eugen Rochko
a3202fd51e Add SMTP_TLS variable for configuring TLS when server uses non-standard port (#4227) 2017-07-16 23:48:35 +02:00
unarist
1cceefce33 Check table existence on prepare_for_foreign_keys (#4225)
* Check table existence on prepare_for_foreign_keys

* Remove trailing whitespace
2017-07-16 20:01:21 +02:00
unarist
033f970af3 Don't mention default post privacy on hints for locked accounts (#4222)
"defaults post privacy to followers-only" only means...

* default value of `visibility` param on post API
* default value for web UI privacy setting (i.e. it will be overridden if they once updated)

...so, many users won't see an effect of it.
2017-07-16 14:02:03 +02:00
Lynx Kotoura
d1c3e35d3f Fix style settings for images in media modal (#4220)
* Shrink too wide single image modal

Fix too wide react-swipeable-view-container

Fix 0067f80 shrinking all react-swipeable-view-container

Change to apply max-width of react-swipeable-view-container only under media-modal.

Fix b30b03b just a typo

* Centering contents in image-loader

Centering small img, canvas, and video in image-loader.
2017-07-16 11:52:11 +02:00
Eugen Rochko
a6328fc1b1 Minor ActivityPub JSON fixes (#4214)
- Objects must have attributedTo instead of actor
- The current attribute belongs to CollectionPage, not Collection
2017-07-16 10:28:55 +02:00
Yamagishi Kazutoshi
35b868eeca Install libidn with Docker (#4219) 2017-07-16 04:40:56 +02:00
Ondřej Hruška
3ea02314b9 split added glitch locales from vanilla (#82)
* Locale script now accepts overrides and new keys from glitch/locales

* Revert glitchsoc changes to mastodon/locales to prevent future merge conflicts
2017-07-16 01:15:25 +02:00
kibigo!
4715161a93 FIXED STUFF FROM THE MERGE SORRY ;_; 2017-07-15 15:42:39 -07:00
kibigo!
144db8ea1d poke and visible docs url 2017-07-15 15:34:46 -07:00
kibigo!
bc4202d00b Ported updates from #64 2017-07-15 15:10:06 -07:00
kibigo!
09cfc079b0 Merge upstream (#81) 2017-07-15 14:33:15 -07:00
unarist
695439775e Fix column swiping (#4211)
This fixes broken behavior and enable animation only on swiping.
2017-07-15 17:25:04 +02:00
ThibG
05cd37097c Optimize uri normalization (#4212)
* Add dependency on idn-ruby to speed up URI normalization

* Use normalized_host instead of normalize.host when applicable

When we are only interested in the normalized host, calling normalized_host
avoids normalizing the other components of the URI as well as creating a
new object
2017-07-15 17:24:35 +02:00
Ondřej Hruška
08d021916d Fixed issue #72 - bad css in report dialog 2017-07-15 16:45:39 +02:00
Ondřej Hruška
99f24ab0c7 Raise search results count to 10 for test
reference: https://mastodon.xyz/users/lx/updates/278054
2017-07-15 15:42:24 +02:00
Ondřej Hruška
3a526e2369 Fix broken letterboxing in media previews 2017-07-15 15:38:18 +02:00
Yamagishi Kazutoshi
bd915d9398 Move babel-plugin-preval to dependencies (#4209) 2017-07-15 12:47:43 +02:00
Eugen Rochko
8c45cd0e36 Improve ActivityPub representations (#3844)
* Improve webfinger templates and make tests more flexible

* Clean up AS2 representation of actor

* Refactor outbox

* Create activities representation

* Add representations of followers/following collections, do not redirect /users/:username route if format is empty

* Remove unused translations

* ActivityPub endpoint for single statuses, add ActivityPub::TagManager for better
URL/URI generation

* Add ActivityPub::TagManager#to

* Represent all attachments as Document instead of Image/Video specifically
(Because for remote ones we may not know for sure)

Add mentions and hashtags representation to AP notes

* Add AP-resolvable hashtag URIs

* Use ActiveModelSerializers for ActivityPub

* Clean up unused translations

* Separate route for object and activity

* Adjust cc/to matrices

* Add to/cc to activities, ensure announce activity embeds target status and
not the wrapper status, add "id" to all collections
2017-07-15 03:01:39 +02:00
Eugen Rochko
3fbf1bf35a Fix #3773 - Pin favourites column (#4201) 2017-07-15 00:49:34 +02:00
Eugen Rochko
cd9b2ab2f7 Fix #2672 - Connect signed PuSH subscription requests to instance domain (#4205)
* Fix #2672 - Connect signed PuSH subscription requests to instance domain

Resolves #2739

* Fix return of locate_subscription

* Fix tests
2017-07-14 23:01:20 +02:00
Eugen Rochko
de397f3bc1 Fix subsequent replies to unresolved status not being filtered from home (#4190)
Resolves #4177 - smaller changeset
2017-07-14 22:31:38 +02:00
Sorin Davidoi
72bd73f605 Small style fixes (#4206)
* fix(components/media_modal): Center horizontally in Firefox

* fix(components/status_list): Do not remove load more button
2017-07-14 22:31:25 +02:00
unarist
1896a154f5 Fix response of unreblog/unfavourite APIs (#4204)
Both APIs process asynchronously, so reblogged/favourited fields in the response should be set to `false` manually.
2017-07-14 20:44:53 +02:00
Eugen Rochko
1618b68bfa HTTP signatures (#4146)
* Add Request class with HTTP signature generator

Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06

* Add HTTP signature verification concern

* Add test for SignatureVerification concern

* Add basic test for Request class

* Make PuSH subscribe/unsubscribe requests use new Request class

Accidentally fix lease_seconds not being set and sent properly, and
change the new minimum subscription duration to 1 day

* Make all PuSH workers use new Request class

* Make Salmon sender use new Request class

* Make FetchLinkService use new Request class

* Make FetchAtomService use the new Request class

* Make Remotable use the new Request class

* Make ResolveRemoteAccountService use the new Request class

* Add more tests

* Allow +-30 seconds window for signed request to remain valid

* Disable time window validation for signed requests, restore 7 days
as PuSH subscription duration (which was previous default due to a bug)
2017-07-14 20:41:49 +02:00
Sorin Davidoi
c1f201c49a 🎄🔨 Force tree shake emojione (#4202)
* chore(yarn): Install babel-plugin-preval as development dependency

* feat(babel): Add preval as a plugin

* feat(emojione_light): Prevaled module what tree-shaked emojione

* refactor(emoji): Use emojione_light

* feat: Preload emojione_picker bundle

* fix(emojione_light): Do not use Object.entries

* fix(emojify): Update tests

* chore(emojione_light): Remove silly ascii art
2017-07-14 20:30:12 +02:00
unarist
8d224ad23b Follow renaming of microformats2 gem (#4203) 2017-07-14 19:57:49 +02:00
Eugen Rochko
e2685ccc81 Fix #4149, fix #1199 - Store emojis as unicode (#4189)
- Use unicode when selecting emoji through picker
- Convert shortcodes to unicode when storing text input server-side
- Do not convert shortcodes in JS anymore
2017-07-14 19:47:53 +02:00
Gô Shoemake
51e3ac2534 Added link to docs website 2017-07-14 09:42:50 -07:00
Ondřej Hruška
75aafc932e Added buttons and menu items to dismiss individual notifications (#76)
* Added DELETE verb for notifications

* Added notification dismiss button to status dropdown

* Added reveal-on-hover notif dismiss button, added FollowNotification component
2017-07-14 11:03:43 -04:00
Eugen Rochko
c42092ba7a Add option to opt out of search engines on public profile/status pages (#4199) 2017-07-14 16:41:02 +02:00
m4sk1n
999170d898 i18n: fediwers › fediwersum (pl) (#4198)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-14 14:41:13 +02:00
Albert ARIBAUD
37430a3401 fr: replace "silencer" with "masquer" throughout (#4197)
* fr.json: replace "silencer" with "masquer"

"Silencer" as a verb does not exist in French. A good and valid replacement is "masquer".

* FR translation: use "masquer" for "silence"
2017-07-14 13:55:40 +02:00
Yamagishi Kazutoshi
0fa9dd8527 Add Rake task for generate VAPID key (#4195)
* Add Rake task for generate VAPID key

* edit config/initializers/vapid.rb
2017-07-14 12:13:43 +02:00
Albert ARIBAUD
489d162477 fr.json: replace "silencer" with "masquer" (#4196)
"Silencer" as a verb does not exist in French. A good and valid replacement is "masquer".
2017-07-14 12:12:16 +02:00
Yamagishi Kazutoshi
9008ab3407 Do not load unnecessary script files (#4193) 2017-07-14 11:08:56 +02:00
Yamagishi Kazutoshi
87b96f8d33 Add Japanese translations for #3243 (#4192) 2017-07-14 11:03:01 +02:00
masarakki
a49be27145 add validation to tag name (#4194) 2017-07-14 11:02:49 +02:00
Eugen Rochko
27b2355738 Fix #3904 - Adjustable time period for mastodon:media:remove_remote via NUM_DAYS (#4191) 2017-07-14 03:51:17 +02:00
unarist
eeb5923e89 Add object-fit polyfill for Edge (#4182) 2017-07-14 01:59:34 +02:00
Sorin Davidoi
a9067167bb Improve swiping (#4188)
* feat(components/columns_area): Toggle animation settings

* fix(components/media_modal): Center non-visible views

* fix(components/media_modal): Check for null

* refactor(columns_area): Better logic
2017-07-14 00:49:01 +02:00
Sorin Davidoi
a9a0c854e1 fix(components/media_modal): Style issues (#4187) 2017-07-13 22:18:18 +02:00
Sorin Davidoi
0c7c188c45 Web Push Notifications (#3243)
* feat: Register push subscription

* feat: Notify when mentioned

* feat: Boost, favourite, reply, follow, follow request

* feat: Notification interaction

* feat: Handle change of public key

* feat: Unsubscribe if things go wrong

* feat: Do not send normal notifications if push is enabled

* feat: Focus client if open

* refactor: Move push logic to WebPushSubscription

* feat: Better title and body

* feat: Localize messages

* chore: Fix lint errors

* feat: Settings

* refactor: Lazy load

* fix: Check if push settings exist

* feat: Device-based preferences

* refactor: Simplify logic

* refactor: Pull request feedback

* refactor: Pull request feedback

* refactor: Create /api/web/push_subscriptions endpoint

* feat: Spec PushSubscriptionController

* refactor: WebPushSubscription => Web::PushSubscription

* feat: Spec Web::PushSubscription

* feat: Display first media attachment

* feat: Support direction

* fix: Stuff broken while rebasing

* refactor: Integration with session activations

* refactor: Cleanup

* refactor: Simplify implementation

* feat: Set VAPID keys via environment

* chore: Comments

* fix: Crash when no alerts

* fix: Set VAPID keys in testing environment

* fix: Follow link

* feat: Notification actions

* fix: Delete previous subscription

* chore: Temporary logs

* refactor: Move migration to a later date

* fix: Fetch the correct session activation and misc bugs

* refactor: Move migration to a later date

* fix: Remove follow request (no notifications)

* feat: Send administrator contact to push service

* feat: Set time-to-live

* fix: Do not show sensitive images

* fix: Reducer crash in error handling

* feat: Add badge

* chore: Fix lint error

* fix: Checkbox label overlap

* fix: Check for payload support

* fix: Rename action "type" (crash in latest Chrome)

* feat: Action to expand notification

* fix: Lint errors

* fix: Unescape notification body

* fix: Do not allow boosting if the status is hidden

* feat: Add VAPID keys to the production sample environment

* fix: Strip HTML tags from status

* refactor: Better error messages

* refactor: Handle browser not implementing the VAPID protocol (Samsung Internet)

* fix: Error when target_status is nil

* fix: Handle lack of image

* fix: Delete reference to invalid subscriptions

* feat: Better error handling

* fix: Unescape HTML characters after tags are striped

* refactor: Simpify code

* fix: Modify to work with #4091

* Sort strings alphabetically

* i18n: Updated Polish translation

it annoys me that it's not fully localized :P

* refactor: Use current_session in PushSubscriptionController

* fix: Rebase mistake

* fix: Set cacheName to mastodon

* refactor: Pull request feedback

* refactor: Remove logging statements

* chore(yarn): Fix conflicts with master

* chore(yarn): Copy latest from master

* chore(yarn): Readd offline-plugin

* refactor: Use save! and update!

* refactor: Send notifications async

* fix: Allow retry when push fails

* fix: Save track for failed pushes

* fix: Minify sw.js

* fix: Remove account_id from fabricator
2017-07-13 22:15:32 +02:00
unarist
c2753fdfb4 Make tag search case insensitive again (#4184) 2017-07-13 19:31:33 +02:00
unarist
c29c20ab3c Add background color for spoiler input, like toot textarea (#4181) 2017-07-13 14:49:57 +02:00
kibigo!
6ce806f913 Fixed faulty import on notifs 2017-07-13 03:36:12 -07:00
kibigo!
35fda84ba8 Documentation pt. I 2017-07-13 03:26:08 -07:00
kibigo!
5770d461b2 Moved glitch containers and commented unused files 2017-07-13 02:40:16 -07:00
kibigo!
2e0645c26c Updated readme and contrib docs 2017-07-12 23:55:55 -07:00
Eugen Rochko
880a5eb25c Fix boolean columns sometimes having a null value (#4162)
* Fix boolean columns sometimes having a null value

* Fix wrong value being set instead of null
2017-07-13 03:12:25 +02:00
Eugen Rochko
e48d3bfd01 Fix #1010 - When spoiler text is set, enforce sensitivity too (#4176) 2017-07-13 01:47:23 +02:00
unarist
5abb3d8150 Rerender modal on property changes (#4175)
Render function for BundleContainer must not be methods.
React doesn't know dependency of the method, so they won't rerender on property updates.

In this case, when you close modal and open another modal immediately,
old modal will be open instead of new one.
2017-07-12 20:51:44 +02:00
Ondřej Hruška
66b1174d25 Fix CW auto-expanding if collapsed toots are disabled 2017-07-12 19:52:36 +02:00
Yamagishi Kazutoshi
c45a75ad34 Update dependencies for Node.js (2017-07-12) (#4167)
* Update @storybook/addon-actions to version 3.1.8

* Update @storybook/react to version 3.1.8

* Update autoprefixer to version 7.1.2

* Update babel-loader to version 7.1.1

* Update babel-preset-env to version 1.6.0

* Update chai to version 4.1.0

* Update js-yaml to version 3.9.0

* Update jsdom to version 11.1.0

* Update postcss-smart-import to version 0.7.5

* Update sinon to version 2.3.7

* Update substring-trie to version 1.0.1

* Update webpack to version 3.2.0

* Update webpack-manifest-plugin to version 1.1.2

* yarn upgrade

* Upgrade extract-text-webpack-plugin to version 3.0.0

* Upgrade intl-relativeformat to version 2.0.0
2017-07-12 15:55:17 +02:00
m4sk1n
3567ac3d3e i18n: @e19eefe, @056b5ed + consistency improvement (pl) (#4171)
* i18n: @e19eefe (pl)

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

* i18n: @056b5ed (pl)

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

* i18n: Improve consistency (pl)

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-07-12 15:53:50 +02:00
Yamagishi Kazutoshi
43f868de3d Add Japanese translations for #4163 and #4129 (#4166)
* Add Japanese translations for #4163

* Add Japanese translations for #4129

* top page -> front page
2017-07-12 14:50:07 +02:00
Yamagishi Kazutoshi
f41590912d Fix broken terms for Japanese (#4170) 2017-07-12 14:49:17 +02:00
kibigo!
183f993b01 Linting fixes 2017-07-12 02:36:40 -07:00
Surinna Curtis
e53fbb4a09 local-only/compose advanced options tweaks.
Squashed commit of the following:

commit b9877e37f72fdd8134936e1014033b07cb6c3671
Author: Surinna Curtis <ekiru.0@gmail.com>
Date:   Wed Jul 12 00:50:10 2017 -0500

    account for the eye in the chars left count for local-only toots

commit 56ebfa96542e16daa1986cc45e07974801ee12dc
Author: Surinna Curtis <ekiru.0@gmail.com>
Date:   Wed Jul 12 00:21:02 2017 -0500

    factor out an AdvancedOptionsToggle to avoid unnecessary re-renders

commit 04cec44ab8744e4e0f52da488c9ec24b1b1422ef
Author: Surinna Curtis <ekiru.0@gmail.com>
Date:   Wed Jul 12 00:20:24 2017 -0500

    s/changeComposeAdvancedOption/toggleComposeAdvancedOption/g

commit af5815dee750d1aa8b797a9305e5ab3ce6774e3f
Author: Surinna Curtis <ekiru.0@gmail.com>
Date:   Tue Jul 11 23:55:19 2017 -0500

    clicking anywhere on the whole advanced option toggles
2017-07-12 02:14:17 -07:00
610 changed files with 14887 additions and 7129 deletions

View File

@@ -22,7 +22,8 @@
{
"messagesDir": "./build/messages"
}
]
],
"preval"
],
"env": {
"development": {

View File

@@ -4,7 +4,6 @@ public/system
public/assets
public/packs
node_modules
storybook
neo4j
vendor/bundle
.DS_Store

View File

@@ -31,6 +31,17 @@ PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=
# VAPID keys (used for push notifications
# You can generate the keys using the following command (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=
# Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
@@ -58,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system

View File

@@ -112,7 +112,7 @@ rules:
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-has-alt: warn
jsx-a11y/img-redundant-alt: warn
jsx-a11y/label-has-for: warn
jsx-a11y/label-has-for: off
jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
@@ -121,6 +121,6 @@ rules:
jsx-a11y/onclick-has-focus: warn
jsx-a11y/onclick-has-role: warn
jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: warn
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn

View File

@@ -14,7 +14,6 @@ node_modules/
public/assets/
public/system/
spec/
storybook/
tmp/
.vagrant/
vendor/bundle/

View File

@@ -6,3 +6,4 @@ plugins:
- last 2 versions
- IE >= 11
- iOS >= 9
postcss-object-fit-images: {}

View File

@@ -27,6 +27,7 @@ Metrics/AbcSize:
Max: 100
Metrics/BlockLength:
Max: 35
Exclude:
- 'lib/tasks/**/*'
@@ -35,10 +36,10 @@ Metrics/BlockNesting:
Metrics/ClassLength:
CountComments: false
Max: 200
Max: 300
Metrics/CyclomaticComplexity:
Max: 15
Max: 25
Metrics/LineLength:
AllowURI: true
@@ -53,11 +54,11 @@ Metrics/ModuleLength:
Max: 200
Metrics/ParameterLists:
Max: 4
Max: 5
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 10
Max: 20
Rails:
Enabled: true

View File

@@ -2,4 +2,3 @@ node_modules/
.cache/
docs/
spec/
storybook/

View File

@@ -6,6 +6,7 @@ cache:
- node_modules
- public/assets
- public/packs-test
- tmp/cache/babel-loader
dist: trusty
sudo: required

View File

@@ -1,6 +1,9 @@
protobuf-compiler
libprotobuf-dev
ffmpeg
libicu-dev
libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
libicu-dev
protobuf-compiler

View File

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

View File

@@ -7,16 +7,21 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
EXPOSE 3000 4000
WORKDIR /mastodon
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
&& echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \
&& apk -U upgrade \
&& apk add -t build-dependencies \
build-base \
libxml2-dev \
libxslt-dev \
icu-dev \
libidn-dev \
libtool \
postgresql-dev \
protobuf-dev \
python \
@@ -25,23 +30,34 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
ffmpeg \
file \
git \
icu-dev \
icu-libs \
imagemagick@edge \
libidn \
libpq \
libxml2 \
libxslt \
nodejs-npm@edge \
nodejs@edge \
protobuf \
su-exec \
tini \
&& npm install -g npm@3 && npm install -g yarn \
yarn@edge \
&& update-ca-certificates \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& mkdir -p /tmp/src \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
&& ./configure --prefix=/usr/local \
&& make -j$(getconf _NPROCESSORS_ONLN)\
&& make install \
&& libtool --finish /usr/local/lib \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN bundle install --deployment --without test development \
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn --ignore-optional --pure-lockfile
COPY . /mastodon

View File

@@ -28,13 +28,14 @@ gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'
gem 'fast_blank', '~> 1.0'
gem 'goldfinger', '~> 1.2'
gem 'goldfinger', '~> 2.0'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.0'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
@@ -50,6 +51,7 @@ gem 'rack-timeout', '~> 0.4'
gem 'rails-i18n', '~> 5.0'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'sanitize', '~> 4.4'
@@ -64,6 +66,7 @@ gem 'statsd-instrument', '~> 2.1'
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0'
gem 'webpush'
group :development, :test do
gem 'fabrication', '~> 2.16'
@@ -77,7 +80,7 @@ group :test do
gem 'capybara', '~> 2.14'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'microformats2', '~> 3.0'
gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false

View File

@@ -1,25 +1,25 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.2)
actionpack (= 5.1.2)
actioncable (5.1.3)
actionpack (= 5.1.3)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
actionmailer (5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.2)
actionview (= 5.1.2)
activesupport (= 5.1.2)
actionpack (5.1.3)
actionview (= 5.1.3)
activesupport (= 5.1.3)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.2)
activesupport (= 5.1.2)
actionview (5.1.3)
activesupport (= 5.1.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -30,16 +30,16 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4)
activejob (5.1.2)
activesupport (= 5.1.2)
activejob (5.1.3)
activesupport (= 5.1.3)
globalid (>= 0.3.6)
activemodel (5.1.2)
activesupport (= 5.1.2)
activerecord (5.1.2)
activemodel (= 5.1.2)
activesupport (= 5.1.2)
activemodel (5.1.3)
activesupport (= 5.1.3)
activerecord (5.1.3)
activemodel (= 5.1.3)
activesupport (= 5.1.3)
arel (~> 8.0)
activesupport (5.1.2)
activesupport (5.1.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
@@ -57,14 +57,14 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.6)
aws-sdk-resources (= 2.10.6)
aws-sdk-core (2.10.6)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.6)
aws-sdk-core (= 2.10.6)
aws-sigv4 (1.0.0)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
bcrypt (3.1.11)
better_errors (2.1.1)
coderay (>= 1.0.0)
@@ -72,7 +72,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.1.1)
bootsnap (1.1.2)
msgpack (~> 1.0)
brakeman (3.6.2)
browser (2.4.0)
@@ -155,7 +155,7 @@ GEM
et-orbi (1.0.5)
tzinfo
execjs (2.7.0)
fabrication (2.16.1)
fabrication (2.16.2)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
@@ -165,10 +165,11 @@ GEM
ruby-progressbar (~> 1.4)
globalid (0.4.0)
activesupport (>= 4.2.0)
goldfinger (1.2.0)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
goldfinger (2.0.1)
addressable (~> 2.5)
http (~> 2.2)
nokogiri (~> 1.8)
oj (~> 3.0)
hamlit (2.8.4)
temple (>= 0.8.0)
thor
@@ -178,9 +179,10 @@ GEM
activesupport (>= 4.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
hashdiff (0.3.4)
hashdiff (0.3.5)
highline (1.7.8)
hiredis (0.6.1)
hkdf (0.3.0)
htmlentities (4.3.4)
http (2.2.2)
addressable (~> 2.3)
@@ -192,11 +194,11 @@ GEM
http-form_data (1.0.3)
http_accept_language (2.1.1)
http_parser.rb (0.6.0)
httplog (0.99.4)
httplog (0.99.7)
colorize
rack
i18n (0.8.4)
i18n-tasks (0.9.15)
i18n (0.8.6)
i18n-tasks (0.9.16)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -206,9 +208,11 @@ GEM
parser (>= 2.2.3.0)
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
idn-ruby (0.1.0)
jmespath (1.3.1)
json (2.1.0)
jsonapi-renderer (0.1.2)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
@@ -238,8 +242,10 @@ GEM
nokogiri (>= 1.5.9)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.8.2)
microformats2 (3.1.0)
microformats (4.0.7)
json
nokogiri
mime-types (3.1)
@@ -247,7 +253,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_portile2 (2.2.0)
minitest (5.10.2)
minitest (5.10.3)
msgpack (1.1.0)
multi_json (1.12.1)
net-scp (1.2.1)
@@ -258,7 +264,7 @@ GEM
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
oj (3.2.0)
oj (3.3.4)
openssl (2.0.4)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
@@ -277,14 +283,14 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.11.2)
parallel_tests (2.14.1)
parallel_tests (2.14.2)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.3)
pkg-config (1.2.4)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -307,17 +313,17 @@ GEM
rack-test (0.6.3)
rack (>= 1.0)
rack-timeout (0.4.2)
rails (5.1.2)
actioncable (= 5.1.2)
actionmailer (= 5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
activemodel (= 5.1.2)
activerecord (= 5.1.2)
activesupport (= 5.1.2)
bundler (>= 1.3.0, < 2.0)
railties (= 5.1.2)
rails (5.1.3)
actioncable (= 5.1.3)
actionmailer (= 5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
activemodel (= 5.1.3)
activerecord (= 5.1.3)
activesupport (= 5.1.3)
bundler (>= 1.3.0)
railties (= 5.1.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -331,11 +337,11 @@ GEM
rails-i18n (5.0.4)
i18n (~> 0.7)
railties (~> 5.0)
rails-settings-cached (0.6.5)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.1.2)
actionpack (= 5.1.2)
activesupport (= 5.1.2)
railties (5.1.3)
actionpack (= 5.1.3)
activesupport (= 5.1.3)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -347,7 +353,7 @@ GEM
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.2)
redis-activesupport (5.0.3)
activesupport (>= 3, < 6)
redis-store (~> 1.3.0)
redis-namespace (1.5.3)
@@ -407,7 +413,7 @@ GEM
scss_lint (0.54.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sidekiq (5.0.3)
sidekiq (5.0.4)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
@@ -415,12 +421,12 @@ GEM
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.7)
sidekiq-scheduler (2.1.8)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.8)
sidekiq-unique-jobs (5.0.9)
sidekiq (>= 4.0, <= 6.0)
thor (~> 0)
simple-navigation (4.0.5)
@@ -444,15 +450,15 @@ GEM
sshkit (1.13.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-instrument (2.1.2)
statsd-instrument (2.1.4)
temple (0.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4)
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.7)
twitter-text (1.14.6)
tilt (2.0.8)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
@@ -475,6 +481,9 @@ GEM
activesupport (>= 4.2)
multi_json (~> 1.2)
railties (>= 4.2)
webpush (0.3.2)
hkdf (~> 0.2)
jwt
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -513,7 +522,7 @@ DEPENDENCIES
faker (~> 1.7)
fast_blank (~> 1.0)
fuubar (~> 2.2)
goldfinger (~> 1.2)
goldfinger (~> 2.0)
hamlit-rails (~> 0.2)
hiredis (~> 0.6)
htmlentities (~> 4.3)
@@ -521,12 +530,14 @@ DEPENDENCIES
http_accept_language (~> 2.1)
httplog (~> 0.99)
i18n-tasks (~> 0.9)
idn-ruby
kaminari (~> 1.0)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
link_header (~> 0.0)
lograge (~> 0.5)
microformats2 (~> 3.0)
mario-redis-lock (~> 1.2)
microformats (~> 4.0)
mime-types (~> 3.1)
nokogiri (~> 1.7)
oj (~> 3.0)
@@ -573,9 +584,10 @@ DEPENDENCIES
uglifier (~> 3.2)
webmock (~> 3.0)
webpacker (~> 2.0)
webpush
RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH
1.15.1
1.15.3

View File

@@ -1,7 +1,10 @@
Mastodon Glitch Edition
========
Now with automated deploys!
# Mastodon Glitch Edition #
> Now with automated deploys!
[![Build Status](https://travis-ci.org/glitch-soc/mastodon.svg?branch=master)](https://travis-ci.org/glitch-soc/mastodon)
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).

3
Vagrantfile vendored
View File

@@ -35,9 +35,10 @@ sudo apt-get install \
postgresql-contrib \
protobuf-compiler \
yarn \
libicu-dev \
libidn11-dev \
libprotobuf-dev \
libreadline-dev \
libicu-dev \
-y
# Install rvm

View File

@@ -2,7 +2,7 @@
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
"logo": "https://github.com/tootsuite.png",
"env": {
"HEROKU": {
"description": "Leave this as true",

View File

@@ -2,6 +2,7 @@
class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
def show
respond_to do |format|
@@ -12,10 +13,12 @@ class AccountsController < ApplicationController
format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
end
format.activitystreams2
format.json do
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController
before_action :set_account
def show
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter
ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account),
type: :ordered,
size: @account.statuses_count,
items: @statuses
)
end
end

View File

@@ -6,15 +6,26 @@ module Admin
@instances = ordered_instances
end
def resubscribe
params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path
end
private
def paginated_instances
Account.remote.by_domain_accounts.page(params[:page])
end
helper_method :paginated_instances
def ordered_instances
paginated_instances.map { |account| Instance.new(account) }
end
def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain])
end
end
end

View File

@@ -5,7 +5,14 @@ module Admin
include Authorization
before_action :set_report
before_action :set_status
before_action :set_status, only: [:update, :destroy]
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
end
def update
@status.update(status_params)
@@ -15,7 +22,7 @@ module Admin
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
redirect_to admin_report_path(@report)
render json: @status
end
private
@@ -24,6 +31,10 @@ module Admin
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: [])
end
def set_report
@report = Report.find(params[:report_id])
end

View File

@@ -8,7 +8,9 @@ module Admin
@reports = filtered_reports.page(params[:page])
end
def show; end
def show
@form = Form::StatusBatch.new
end
def update
process_report

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
module Admin
class StatusesController < BaseController
include Authorization
helper_method :current_params
before_action :set_account
before_action :set_status, only: [:update, :destroy]
PAR_PAGE = 20
def index
@statuses = @account.statuses
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PAR_PAGE)
@form = Form::StatusBatch.new
end
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def update
@status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
render json: @status
end
private
def status_params
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: [])
end
def set_status
@status = @account.statuses.find(params[:id])
end
def set_account
@account = Account.find(params[:account_id])
end
def current_params
page = (params[:page] || 1).to_i
{
media: params[:media],
page: page > 1 && page,
}.select { |_, value| value.present? }
end
end
end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
class Api::ActivityPub::ActivitiesController < Api::BaseController
include Authorization
# before_action :set_follow, only: [:show_follow]
before_action :set_status, only: [:show_status]
respond_to :activitystreams2
# Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity.
def show_status
authorize @status, :show?
if @status.reblog?
render :show_status_announce
else
render :show_status_create
end
end
private
def set_status
@status = Status.find(params[:id])
end
end

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
class Api::ActivityPub::NotesController < Api::BaseController
include Authorization
before_action :set_status
respond_to :activitystreams2
def show
authorize @status, :show?
end
private
def set_status
@status = Status.find(params[:id])
end
end

View File

@@ -1,69 +0,0 @@
# frozen_string_literal: true
class Api::ActivityPub::OutboxController < Api::BaseController
before_action :set_account
respond_to :activitystreams2
def show
if params[:max_id] || params[:since_id]
show_outbox_page
else
show_base_outbox
end
end
private
def show_base_outbox
@statuses = Status.as_outbox_timeline(@account)
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_first_last_page(@statuses)
render :show
end
def show_outbox_page
all_statuses = Status.as_outbox_timeline(@account)
@statuses = all_statuses.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
all_statuses = cache_collection(all_statuses)
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_first_last_page(all_statuses)
@next_page_url = api_activitypub_outbox_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
@prev_page_url = api_activitypub_outbox_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
@paginated = @next_page_url || @prev_page_url
@part_of_url = api_activitypub_outbox_url
set_pagination_headers(@next_page_url, @prev_page_url)
render :show_page
end
def cache_collection(raw)
super(raw, Status)
end
def set_account
@account = Account.find(params[:id])
end
def set_first_last_page(statuses) # rubocop:disable Style/AccessorMethodName
return if statuses.empty?
@first_page_url = api_activitypub_outbox_url(max_id: statuses.first.id + 1)
@last_page_url = api_activitypub_outbox_url(since_id: statuses.last.id - 1)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
end

View File

@@ -17,11 +17,7 @@ class Api::BaseController < ApplicationController
render json: { error: 'Record not found' }, status: 404
end
rescue_from Goldfinger::Error do
render json: { error: 'Remote account could not be resolved' }, status: 422
end
rescue_from HTTP::Error do
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote data could not be fetched' }, status: 503
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
@@ -11,7 +13,7 @@ class Api::PushController < Api::BaseController
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds)
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
@@ -57,6 +59,10 @@ class Api::PushController < Api::BaseController
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end

View File

@@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 86_400).to_i.seconds
(params['hub.lease_seconds'] || 1.day).to_i.seconds
end
def set_account

View File

@@ -20,9 +20,7 @@ class Api::V1::FavouritesController < Api::BaseController
def cached_favourites
cache_collection(
Status.where(
id: results.map(&:status_id)
),
Status.reorder(nil).joins(:favourites).merge(results),
Status
)
end

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
render json: @status, serializer: REST::StatusSerializer
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, favourites_map: @favourites_map)
end
private

View File

@@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
authorize status_for_destroy, :unreblog?
RemovalWorker.perform_async(status_for_destroy.id)
render json: @status, serializer: REST::StatusSerializer
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
end
private

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::BaseController
respond_to :json
before_action :require_user!
def create
params.require(:subscription).require(:endpoint)
params.require(:subscription).require(:keys).require([:auth, :p256dh])
active_session = current_session
unless active_session.web_push_subscription.nil?
active_session.web_push_subscription.destroy!
active_session.update!(web_push_subscription: nil)
end
# Mobile devices do not support regular notifications, so we enable push notifications by default
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
data = {
alerts: {
follow: alerts_enabled,
favourite: alerts_enabled,
reblog: alerts_enabled,
mention: alerts_enabled,
},
}
web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh],
key_auth: params[:subscription][:keys][:auth],
data: data
)
active_session.update!(web_push_subscription: web_subscription)
render json: web_subscription.as_payload
end
def update
params.require([:id, :data])
web_subscription = ::Web::PushSubscription.find(params[:id])
web_subscription.update!(data: params[:data])
render json: web_subscription.as_payload
end
end

View File

@@ -43,6 +43,10 @@ class ApplicationController < ActionController::Base
forbidden if current_user.account.suspended?
end
def after_sign_out_path_for(_resource_or_scope)
new_user_session_path
end
protected
def forbidden

View File

@@ -1,5 +1,20 @@
# frozen_string_literal: true
class Auth::PasswordsController < Devise::PasswordsController
before_action :check_validity_of_reset_password_token, only: :edit
layout 'auth'
private
def check_validity_of_reset_password_token
unless reset_password_token_is_valid?
flash[:error] = I18n.t('auth.invalid_reset_password_token')
redirect_to new_password_path(resource_name)
end
end
def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present?
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class AuthorizeFollowsController < ApplicationController
layout 'public'
layout 'modal'
before_action :authenticate_user!

View File

@@ -0,0 +1,87 @@
# frozen_string_literal: true
# Implemented according to HTTP signatures (Draft 6)
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
module SignatureVerification
extend ActiveSupport::Concern
def signed_request?
request.headers['Signature'].present?
end
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
unless signed_request?
@signed_request_account = nil
return
end
raw_signature = request.headers['Signature']
signature_params = {}
raw_signature.split(',').each do |part|
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
next if parsed_parts.nil? || parsed_parts.size != 3
signature_params[parsed_parts[1]] = parsed_parts[2]
end
if incompatible_signature?(signature_params)
@signed_request_account = nil
return
end
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
if account.nil?
@signed_request_account = nil
return
end
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(signature_params['headers'])
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
else
@signed_request_account = nil
end
end
private
def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank?
signed_headers.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
end.join("\n")
end
def matches_time_window?
begin
time_sent = DateTime.httpdate(request.headers['Date'])
rescue ArgumentError
return false
end
(Time.now.utc - time_sent).abs <= 30
end
def to_header_name(name)
name.split(/-/).map(&:capitalize).join('-')
end
def incompatible_signature?(signature_params)
signature_params['keyId'].blank? ||
signature_params['signature'].blank? ||
signature_params['algorithm'].blank? ||
signature_params['algorithm'] != 'rsa-sha256' ||
!signature_params['keyId'].start_with?('acct:')
end
end

View File

@@ -5,5 +5,24 @@ class FollowerAccountsController < ApplicationController
def index
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account),
type: :ordered,
size: @account.followers_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
)
end
end

View File

@@ -5,5 +5,24 @@ class FollowingAccountsController < ApplicationController
def index
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account),
type: :ordered,
size: @account.following_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
)
end
end

View File

@@ -6,6 +6,7 @@ class HomeController < ApplicationController
def index
@body_classes = 'app-body'
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
end
private
@@ -22,6 +23,7 @@ class HomeController < ApplicationController
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class RemoteFollowController < ApplicationController
layout 'public'
layout 'modal'
before_action :set_account
before_action :gone, if: :suspended_account?

View File

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

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Settings::SessionsController < ApplicationController
before_action :set_session, only: :destroy
def destroy
@session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success')
redirect_to edit_user_registration_path
end
private
def set_session
@session = current_user.session_activations.find(params[:id])
end
end

View File

@@ -18,7 +18,7 @@ module Settings
end
def destroy
if current_user.validate_and_consume_otp!(confirmation_params[:code])
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
@@ -38,5 +38,10 @@ module Settings
def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
end
end
end

View File

@@ -11,10 +11,22 @@ class StatusesController < ApplicationController
before_action :check_account_suspension
def show
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
respond_to do |format|
format.html do
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
render 'stream_entries/show'
render 'stream_entries/show'
end
format.json do
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end
end
def activity
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
private

View File

@@ -2,6 +2,7 @@
class StreamEntriesController < ApplicationController
include Authorization
include SignatureVerification
layout 'public'
@@ -18,7 +19,7 @@ class StreamEntriesController < ApplicationController
end
format.atom do
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end
end
end

View File

@@ -5,7 +5,26 @@ class TagsController < ApplicationController
def show
@tag = Tag.find_by!(name: params[:id].downcase)
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = cache_collection(@statuses, Status)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag),
type: :ordered,
size: @tag.statuses.count,
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end
end

View File

@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Activitystreams2BuilderHelper
# Gets a usable name for an account, using display name or username.
def account_name(account)
account.display_name.presence || account.username
end
end

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
module EmojiHelper
def emojify(text)
return text if text.blank?
text.gsub(emoji_pattern) do |match|
emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
if emoji
emoji
else
match
end
end
end
def emoji_pattern
@emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
(#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
(?=[^[:alnum:]:]|$)/x
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
module HttpHelper
def http_client(options = {})
timeout = { write: 10, connect: 10, read: 10 }.merge(options)
HTTP.headers(user_agent: user_agent)
.timeout(:per_operation, timeout)
.follow
end
private
def user_agent
@user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +http://#{Rails.configuration.x.local_domain}/)"
end
end

View File

@@ -2,7 +2,7 @@
module InstanceHelper
def site_title
Setting.site_title.to_s
Setting.site_title.presence || site_hostname
end
def site_hostname

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
// <CommonAvatar>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/avatar
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonAvatar extends React.PureComponent {
// Props and state.
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
circular: PropTypes.bool,
className: PropTypes.string,
comrade: ImmutablePropTypes.map,
}
state = {
hovering: false,
}
// Starts or stops animation on hover.
handleMouseEnter = () => {
if (this.props.animate) return;
this.setState({ hovering: true });
}
handleMouseLeave = () => {
if (this.props.animate) return;
this.setState({ hovering: false });
}
// Renders the component.
render () {
const {
handleMouseEnter,
handleMouseLeave,
} = this;
const {
account,
animate,
circular,
className,
comrade,
...others
} = this.props;
const { hovering } = this.state;
const computedClass = classNames('glitch', 'glitch__common__avatar', {
_circular: circular,
}, className);
// We store the image srcs here for later.
const src = account.get('avatar');
const staticSrc = account.get('avatar_static');
const comradeSrc = comrade ? comrade.get('avatar') : null;
const comradeStaticSrc = comrade ? comrade.get('avatar_static') : null;
// Avatars are a straightforward div with image(s) inside.
return comrade ? (
<div
className={computedClass}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...others}
>
<img
className='avatar\main'
src={hovering || animate ? src : staticSrc}
alt=''
/>
<img
className='avatar\comrade'
src={hovering || animate ? comradeSrc : comradeStaticSrc}
alt=''
/>
</div>
) : (
<div
className={computedClass}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...others}
>
<img
className='avatar\solo'
src={hovering || animate ? src : staticSrc}
alt=''
/>
</div>
);
}
}

View File

@@ -0,0 +1,41 @@
@import 'variables';
.glitch.glitch__common__avatar {
display: inline-block;
position: relative;
& > img {
display: block;
position: static;
margin: 0;
border-radius: $ui-avatar-border-size;
width: 100%;
height: 100%;
&.avatar\\comrade {
position: absolute;
right: 0;
bottom: 0;
width: 50%;
height: 50%;
}
&.avatar\\main {
margin: 0 30% 30% 0;
width: 70%;
height: 70%;
}
}
&._circular {
& > img {
transition: border-radius ($glitch-animation-speed * .3s);
}
&:not(:hover) {
& > img {
border-radius: 50%;
}
}
}
}

View File

@@ -0,0 +1,146 @@
// <CommonButton>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/button
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
// Our imports.
import CommonLink from 'glitch/components/common/link';
import CommonIcon from 'glitch/components/common/icon';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonButton extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
animate: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
href: PropTypes.string,
icon: PropTypes.string,
onClick: PropTypes.func,
showTitle: PropTypes.bool,
title: PropTypes.string,
}
state = {
loaded: false,
}
// The `loaded` state property activates our animations. We wait
// until an activation change in order to prevent unsightly
// animations when the component first mounts.
componentWillReceiveProps (nextProps) {
const { active } = this.props;
// The double "not"s here cast both arguments to booleans.
if (!nextProps.active !== !active) this.setState({ loaded: true });
}
handleClick = (e) => {
const { onClick } = this.props;
if (!onClick) return;
onClick(e);
e.preventDefault();
}
// Rendering the component.
render () {
const { handleClick } = this;
const {
active,
animate,
children,
className,
disabled,
href,
icon,
onClick,
showTitle,
title,
...others
} = this.props;
const { loaded } = this.state;
const computedClass = classNames('glitch', 'glitch__common__button', className, {
_active: active && !href, // Links can't be active
_animated: animate && loaded,
_disabled: disabled,
_link: href,
_star: icon === 'star',
'_with-text': children || title && showTitle,
});
let conditionalProps = {};
// If href is provided, we render a link.
if (href) {
if (!disabled && href) conditionalProps.href = href;
if (title && !showTitle) {
if (!children) conditionalProps.title = title;
else conditionalProps['aria-label'] = title;
}
if (onClick) {
if (!disabled) conditionalProps.onClick = handleClick;
else conditionalProps['aria-disabled'] = true;
conditionalProps.role = 'button';
conditionalProps.tabIndex = 0;
}
return (
<CommonLink
className={computedClass}
{...conditionalProps}
{...others}
>
{children}
{title && showTitle ? <span className='button\title'>{title}</span> : null}
<CommonIcon name={icon} className='button\icon' />
</CommonLink>
);
// Otherwise, we render a button.
} else {
if (active !== void 0) conditionalProps['aria-pressed'] = active;
if (title && !showTitle) {
if (!children) conditionalProps.title = title;
else conditionalProps['aria-label'] = title;
}
if (onClick && !disabled) {
conditionalProps.onClick = handleClick;
}
return (
<button
className={computedClass}
{...conditionalProps}
disabled={disabled}
{...others}
tabIndex='0'
type='button'
>
{children}
{title && showTitle ? <span className='button\title'>{title}</span> : null}
<CommonIcon name={icon} className='button\icon' />
</button>
);
}
};
}

View File

@@ -0,0 +1,134 @@
@import 'variables';
.glitch.glitch__common__button {
display: inline-block;
border: none;
padding: 0;
color: $ui-base-lighter-color;
background: transparent;
outline: thin transparent dotted;
font-size: inherit;
text-decoration: none;
cursor: pointer;
transition: color ($glitch-animation-speed * .15s) ease-in, outline-color ($glitch-animation-speed * .3s) ease-in-out;
&._animated .button\\icon {
animation-name: glitch__common__button__deactivate;
animation-duration: .9s;
animation-timing-function: ease-in-out;
@keyframes glitch__common__button__deactivate {
from {
transform: rotate(360deg);
}
57% {
transform: rotate(-60deg);
}
86% {
transform: rotate(30deg);
}
to {
transform: rotate(0deg);
}
}
}
&._active {
.button\\icon {
color: $ui-highlight-color;
}
&._animated .button\\icon {
animation-name: glitch__common__button__activate;
@keyframes glitch__common__button__activate {
from {
transform: rotate(0deg);
}
57% {
transform: rotate(420deg); // Blazin' 😎
}
86% {
transform: rotate(330deg);
}
to {
transform: rotate(360deg);
}
}
}
/*
The special `._star` class is given to buttons which have a star
icon (see JS). When they are active, we give them a gold star ⭐️.
*/
&._star .button\\icon {
color: $gold-star;
}
}
/*
For links, we consider them disabled if they don't have an `href`
attribute (see JS).
*/
&._disabled {
opacity: $glitch-disabled-opacity;
cursor: default;
}
/*
This is confusing becuase of the names, but the `color .3 ease-out`
transition is actually used when easing *in* to a hovering/active/
focusing state, and the default transition is used when leaving. Our
buttons are a little slower to glow than they are to fade.
*/
&:active,
&:focus,
&:hover {
color: $glitch-lighter-color;
transition: color ($glitch-animation-speed * .3s) ease-out, outline-color ($glitch-animation-speed * .15s) ease-in-out;
}
&:focus {
outline-color: currentColor;
}
/*
Buttons with text have a number of different styling rules and an
overall different appearance.
*/
&._with-text {
display: inline-block;
border: none;
border-radius: .35em;
padding: 0 .5em;
color: $glitch-texture-color;
background: $ui-base-lighter-color;
font-size: .75em;
font-weight: inherit;
text-transform: uppercase;
line-height: 1.6;
cursor: pointer;
vertical-align: baseline;
transition: background-color ($glitch-animation-speed * .15s) ease-in, outline-color ($glitch-animation-speed * .3s) ease-in-out;
.button\\icon {
display: inline-block;
font-size: 1.25em;
vertical-align: -.1em;
}
& > *:not(:first-child) {
margin: 0 0 0 .4em;
border-left: 1px solid currentColor;
padding: 0 0 0 .3em;
}
&:active,
&:hover,
&:focus {
color: $glitch-texture-color;
background: $glitch-lighter-color;
transition: background-color ($glitch-animation-speed * .3s) ease-out, outline-color ($glitch-animation-speed * .15s) ease-in-out;
}
}
}

View File

@@ -0,0 +1,59 @@
// <CommonIcon>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/icon
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
const CommonIcon = ({
className,
name,
proportional,
title,
...others
}) => name ? (
<span
className={classNames('glitch', 'glitch__common__icon', className)}
{...others}
>
<span
aria-hidden
className={`fa ${proportional ? '' : 'fa-fw'} fa-${name} icon\fa`}
{...(title ? { title } : {})}
/>
{title ? (
<span className='_for-screenreader'>{title}</span>
) : null}
</span>
) : null;
// Props.
CommonIcon.propTypes = {
className: PropTypes.string,
name: PropTypes.string,
proportional: PropTypes.bool,
title: PropTypes.string,
};
// Export.
export default CommonIcon;

View File

@@ -0,0 +1,14 @@
@import 'variables';
.glitch.glitch__common__icon {
display: inline-block;
._for-screenreader {
position: absolute;
margin: -1px -1px;
width: 1px;
height: 1px;
clip: rect(0, 0, 0, 0);
overflow: hidden;
}
}

View File

@@ -0,0 +1,74 @@
// <CommonLink>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/link
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonLink extends React.PureComponent {
// Props.
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
destination: PropTypes.string,
history: PropTypes.object,
href: PropTypes.string,
};
// We only reroute the link if it is an unadorned click, we have
// access to the router, and there is somewhere to reroute it *to*.
handleClick = (e) => {
const { destination, history } = this.props;
if (!history || !destination || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
history.push(destination);
e.preventDefault();
}
// Rendering.
render () {
const { handleClick } = this;
const { children, className, destination, history, href, ...others } = this.props;
const computedClass = classNames('glitch', 'glitch__common__link', className);
const conditionalProps = {};
if (href) {
conditionalProps.href = href;
conditionalProps.onClick = handleClick;
} else if (destination) {
conditionalProps.onClick = handleClick;
conditionalProps.role = 'link';
conditionalProps.tabIndex = 0;
} else conditionalProps.role = 'presentation';
return (
<a
className={computedClass}
{...conditionalProps}
{...others}
rel='noopener'
target='_blank'
>{children}</a>
);
}
}

View File

@@ -0,0 +1,11 @@
@import 'variables';
/*
Most link styling happens elsewhere but we disable text-decoration
here.
*/
.glitch.glitch__common__link {
display: inline;
color: $ui-secondary-color;
text-decoration: none;
}

View File

@@ -0,0 +1,49 @@
// <CommonSeparator>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/separator
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
const CommonSeparator = ({
className,
visible,
...others
}) => visible ? (
<span
className={
classNames('glitch', 'glitch__common__separator', className)
}
{...others}
role='separator'
/> // Contents provided via CSS.
) : null;
// Props.
CommonSeparator.propTypes = {
className: PropTypes.string,
visible: PropTypes.bool,
};
// Export.
export default CommonSeparator;

View File

@@ -0,0 +1,15 @@
@import 'variables';
/*
The default contents for a separator is an interpunct, surrounded by
spaces. However, this can be changed using CSS selectors.
*/
.glitch.glitch__common__separator {
display: inline-block;
&::after {
display: inline-block;
padding: 0 .3em;
content: "·";
}
}

View File

@@ -1,112 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../mastodon/components/icon_button';
const messages = defineMessages({
local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' },
advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
@injectIntl
export default class ComposeAdvancedOptions extends React.PureComponent {
static propTypes = {
values: ImmutablePropTypes.contains({
do_not_federate: PropTypes.bool.isRequired,
}).isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
onToggleDropdown = () => {
this.setState({ open: !this.state.open });
};
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
}
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
state = {
open: false,
};
handleClick = (e) => {
const option = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.props.onChange(option);
}
toggleHandler(option) {
return () => this.props.onChange(option);
}
setRef = (c) => {
this.node = c;
}
render () {
const { open } = this.state;
const { intl, values } = this.props;
const options = [
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' },
];
const anyEnabled = values.some((enabled) => enabled);
const optionElems = options.map((option) => {
const active = values.get(option.key);
return (
<div role='button' className='advanced-options-dropdown__option' key={option.key} >
<div className='advanced-options-dropdown__option__toggle'>
<Toggle checked={active} onChange={this.toggleHandler(option.key)} />
</div>
<div className='advanced-options-dropdown__option__content'>
<strong>{intl.formatMessage(option.shortText)}</strong>
{intl.formatMessage(option.longText)}
</div>
</div>
);
});
return (<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className='advanced-options-dropdown__value'
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='ellipsis-h' active={open || anyEnabled}
size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
// <ListConversationContainer>
// =================
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/list/conversation/container
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import { connect } from 'react-redux';
// Mastodon imports.
import { fetchContext } from 'mastodon/actions/statuses';
// Our imports.
import ListConversation from '.';
// * * * * * * * //
// State mapping
// -------------
const mapStateToProps = (state, { id }) => {
return {
ancestors : state.getIn(['contexts', 'ancestors', id]),
descendants : state.getIn(['contexts', 'descendants', id]),
};
};
// * * * * * * * //
// Dispatch mapping
// ----------------
const mapDispatchToProps = (dispatch) => ({
fetch (id) {
dispatch(fetchContext(id));
},
});
// * * * * * * * //
// Connecting
// ----------
export default connect(mapStateToProps, mapDispatchToProps)(ListConversation);

View File

@@ -0,0 +1,80 @@
// <ListConversation>
// ====================
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/list/conversation
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import React from 'react';
import PropTypes from 'prop-types';
import ScrollContainer from 'react-router-scroll';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Our imports.
import StatusContainer from 'glitch/components/status/container';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class ListConversation extends ImmutablePureComponent {
// Props.
static propTypes = {
id: PropTypes.number.isRequired,
ancestors: ImmutablePropTypes.list,
descendants: ImmutablePropTypes.list,
fetch: PropTypes.func.isRequired,
}
// If this is a detailed status, we should fetch its contents and
// context upon mounting.
componentWillMount () {
const { id, fetch } = this.props;
fetch(id);
}
// Similarly, if the component receives new props, we need to fetch
// the new status.
componentWillReceiveProps (nextProps) {
const { id, fetch } = this.props;
if (nextProps.id !== id) fetch(nextProps.id);
}
// We just render our status inside a column with its
// ancestors and decendants.
render () {
const { id, ancestors, descendants } = this.props;
return (
<ScrollContainer scrollKey='thread'>
<div className='glitch glitch__list__conversation scrollable'>
{ancestors && ancestors.size > 0 ? (
ancestors.map(
ancestor => <StatusContainer key={ancestor} id={ancestor} route />
)
) : null}
<StatusContainer key={id} id={id} detailed route />
{descendants && descendants.size > 0 ? (
descendants.map(
descendant => <StatusContainer key={descendant} id={descendant} route />
)
) : null}
</div>
</ScrollContainer>
);
}
};

View File

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

View File

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

View File

@@ -0,0 +1,209 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import IntersectionObserverWrapper from 'mastodon/features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import { defineMessages, injectIntl } from 'react-intl';
import StatusContainer from 'glitch/components/status/container';
import CommonButton from 'glitch/components/common/button';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
@injectIntl
export default class ListStatuses extends ImmutablePureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
onScrollToBottom: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
};
static defaultProps = {
trackScroll: true,
};
state = {
currentDetail: null,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 150, {
trailing: true,
});
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
}
componentDidUpdate (prevProps) {
// Reset the scroll position when a new toot comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
}
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
});
}
detachIntersectionObserver () {
this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
}
setRef = (c) => {
this.node = c;
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
handleSetDetail = (id) => {
this.setState({ currentDetail : id });
}
render () {
const {
handleKeyDown,
handleLoadMore,
handleSetDetail,
intersectionObserverWrapper,
setRef,
} = this;
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, intl } = this.props;
const { currentDetail } = this.state;
const loadMore = (
<CommonButton
className='load-more'
disabled={isLoading || statusIds.size > 0 && hasMore}
onClick={handleLoadMore}
showTitle
title={intl.formatMessage(messages.load_more)}
/>
);
let scrollableArea = null;
if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={setRef}>
<div role='feed' className='status-list' onKeyDown={handleKeyDown}>
{prepend}
{statusIds.map((statusId, index) => (
<StatusContainer
key={statusId}
id={statusId}
index={index}
listLength={statusIds.size}
detailed={currentDetail === statusId}
setDetail={handleSetDetail}
intersectionObserverWrapper={intersectionObserverWrapper}
/>
))}
{loadMore}
</div>
</div>
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={setRef}>
{emptyMessage}
</div>
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
// Package imports //
// Package imports
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
// Our imports //
import SettingsItem from './item';
// Our imports
import LocalSettingsPageItem from './item';
// Stylesheet imports
import './style';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
@@ -14,27 +19,21 @@ const messages = defineMessages({
});
@injectIntl
export default class Settings extends React.PureComponent {
export default class LocalSettingsPage extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
toggleSetting: PropTypes.func.isRequired,
changeSetting: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
index : PropTypes.number,
intl : PropTypes.object.isRequired,
onChange : PropTypes.func.isRequired,
settings : ImmutablePropTypes.map.isRequired,
};
state = {
currentIndex: 0,
};
General = () => {
const { intl } = this.props;
return (
<div>
pages = [
({ intl, onChange, settings }) => (
<div className='glitch local-settings__page general'>
<h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
<SettingsItem
settings={this.props.settings}
<LocalSettingsPageItem
settings={settings}
item={['layout']}
id='mastodon-settings--layout'
options={[
@@ -42,180 +41,143 @@ export default class Settings extends React.PureComponent {
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
{ value: 'single', message: intl.formatMessage(messages.layout_mobile) },
]}
onChange={this.props.changeSetting}
onChange={onChange}
>
<FormattedMessage id='settings.layout' defaultMessage='Layout:' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['stretch']}
id='mastodon-settings--stretch'
onChange={this.props.toggleSetting}
onChange={onChange}
>
<FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
</SettingsItem>
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['navbar_under']}
id='mastodon-settings--navbar_under'
onChange={onChange}
>
<FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
</LocalSettingsPageItem>
</div>
);
}
CollapsedStatuses = () => {
return (
<div>
),
({ onChange, settings }) => (
<div className='glitch local-settings__page collapsed'>
<h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
<SettingsItem
settings={this.props.settings}
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'enabled']}
id='mastodon-settings--collapsed-enabled'
onChange={this.props.toggleSetting}
onChange={onChange}
>
<FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
</SettingsItem>
</LocalSettingsPageItem>
<section>
<h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
<SettingsItem
settings={this.props.settings}
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'all']}
id='mastodon-settings--collapsed-auto-all'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'notifications']}
id='mastodon-settings--collapsed-auto-notifications'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'lengthy']}
id='mastodon-settings--collapsed-auto-lengthy'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'replies']}
id='mastodon-settings--collapsed-auto-replies'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'media']}
id='mastodon-settings--collapsed-auto-media'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
</SettingsItem>
</LocalSettingsPageItem>
</section>
<section>
<h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
<SettingsItem
settings={this.props.settings}
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'backgrounds', 'user_backgrounds']}
id='mastodon-settings--collapsed-user-backgrouns'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'backgrounds', 'preview_images']}
id='mastodon-settings--collapsed-preview-images'
onChange={this.props.toggleSetting}
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
</SettingsItem>
</LocalSettingsPageItem>
</section>
</div>
);
}
Media = () => {
return (
<div>
),
({ onChange, settings }) => (
<div className='glitch local-settings__page media'>
<h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1>
<SettingsItem
settings={this.props.settings}
<LocalSettingsPageItem
settings={settings}
item={['media', 'letterbox']}
id='mastodon-settings--media-letterbox'
onChange={this.props.toggleSetting}
onChange={onChange}
>
<FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['media', 'fullwidth']}
id='mastodon-settings--media-fullwidth'
onChange={this.props.toggleSetting}
onChange={onChange}
>
<FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' />
</SettingsItem>
</LocalSettingsPageItem>
</div>
);
}
navigateTo = (e) =>
this.setState({ currentIndex: +e.currentTarget.getAttribute('data-mastodon-navigation_index') });
),
];
render () {
const { pages } = this;
const { index, intl, onChange, settings } = this.props;
const CurrentPage = pages[index] || pages[0];
const { General, CollapsedStatuses, Media, navigateTo } = this;
const { onClose } = this.props;
const { currentIndex } = this.state;
return (
<div className='modal-root__modal settings-modal'>
<nav className='settings-modal__navigation'>
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='0' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 0 ? ' active' : ''}`}>
<FormattedMessage id='settings.general' defaultMessage='General' />
</a>
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='1' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 1 ? ' active' : ''}`}>
<FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' />
</a>
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='2' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 2 ? ' active' : ''}`}>
<FormattedMessage id='settings.media' defaultMessage='Media' />
</a>
<a href='/settings/preferences' className='settings-modal__navigation-item'>
<i className='fa fa-fw fa-cog' /> <FormattedMessage id='settings.preferences' defaultMessage='User preferences' />
</a>
<a onClick={onClose} role='button' tabIndex='0' className='settings-modal__navigation-close'>
<FormattedMessage id='settings.close' defaultMessage='Close' />
</a>
</nav>
<div className='settings-modal__content'>
{
[
<General />,
<CollapsedStatuses />,
<Media />,
][currentIndex] || <General />
}
</div>
</div>
);
return <CurrentPage intl={intl} onChange={onChange} settings={settings} />;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
/*
`<NotificationContainer>`
=========================
This container connects `<Notification>`s to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Mastodon imports //
import { makeGetNotification } from '../../../mastodon/selectors';
// Our imports //
import Notification from '.';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. We wrap this in `makeMapStateToProps()` so that
we only have to call `makeGetNotification()` once instead of every
time.
*/
const makeMapStateToProps = () => {
const getNotification = makeGetNotification();
const mapStateToProps = (state, props) => ({
notification: getNotification(state, props.notification, props.accountId),
settings: state.get('local_settings'),
notifCleaning: state.getIn(['notifications', 'cleaningMode']),
});
return mapStateToProps;
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
export default connect(makeMapStateToProps)(Notification);

View File

@@ -0,0 +1,124 @@
/*
`<NotificationFollow>`
======================
This component renders a follow notification.
__Props:__
- __`id` (`PropTypes.number.isRequired`) :__
This is the id of the notification.
- __`onDeleteNotification` (`PropTypes.func.isRequired`) :__
The function to call when a notification should be
dismissed/deleted.
- __`account` (`PropTypes.object.isRequired`) :__
The account associated with the follow notification, ie the account
which followed the user.
- __`intl` (`PropTypes.object.isRequired`) :__
Our internationalization object, inserted by `@injectIntl`.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import emojify from '../../../mastodon/emoji';
import Permalink from '../../../mastodon/components/permalink';
import AccountContainer from '../../../mastodon/containers/account_container';
// Our imports //
import NotificationOverlayContainer from '../notification/overlay/container';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Implementation:
---------------
*/
export default class NotificationFollow extends ImmutablePureComponent {
static propTypes = {
id : PropTypes.number.isRequired,
account : ImmutablePropTypes.map.isRequired,
notification : ImmutablePropTypes.map.isRequired,
};
/*
### `render()`
This actually renders the component.
*/
render () {
const { account, notification } = this.props;
/*
`link` is a container for the account's `displayName`, which links to
the account timeline using a `<Permalink>`.
*/
const displayName = account.get('display_name') || account.get('username');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const link = (
<Permalink
className='notification__display-name'
href={account.get('url')}
title={account.get('acct')}
to={`/accounts/${account.get('id')}`}
dangerouslySetInnerHTML={displayNameHTML}
/>
);
/*
We can now render our component.
*/
return (
<div className='notification notification-follow'>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={{ name: link }}
/>
</div>
<AccountContainer id={account.get('id')} withNote={false} />
<NotificationOverlayContainer notification={notification} />
</div>
);
}
}

View File

@@ -1,17 +1,14 @@
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import AccountContainer from '../../../mastodon/containers/account_container';
import Permalink from '../../../mastodon/components/permalink';
import emojify from '../../../mastodon/emoji';
// Our imports //
import StatusContainer from '../../containers/status';
import StatusContainer from '../status/container';
import NotificationFollow from './follow';
import NotificationOverlayContainer from './overlay/container';
export default class Notification extends ImmutablePureComponent {
@@ -21,22 +18,12 @@ export default class Notification extends ImmutablePureComponent {
};
renderFollow (notification) {
const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
return (
<div className='notification notification-follow'>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</div>
<AccountContainer id={account.get('id')} withNote={false} />
</div>
<NotificationFollow
id={notification.get('id')}
account={notification.get('account')}
notification={notification}
/>
);
}
@@ -44,6 +31,7 @@ export default class Notification extends ImmutablePureComponent {
return (
<StatusContainer
id={notification.get('status')}
notification={notification}
withDismiss
/>
);
@@ -56,6 +44,7 @@ export default class Notification extends ImmutablePureComponent {
account={notification.get('account')}
prepend='favourite'
muted
notification={notification}
withDismiss
/>
);
@@ -68,6 +57,7 @@ export default class Notification extends ImmutablePureComponent {
account={notification.get('account')}
prepend='reblog'
muted
notification={notification}
withDismiss
/>
);
@@ -76,18 +66,25 @@ export default class Notification extends ImmutablePureComponent {
render () {
const { notification } = this.props;
switch(notification.get('type')) {
case 'follow':
return this.renderFollow(notification);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification);
case 'reblog':
return this.renderReblog(notification);
}
return null;
return (
<div class='status'>
{(() => {
switch (notification.get('type')) {
case 'follow':
return this.renderFollow(notification);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification);
case 'reblog':
return this.renderReblog(notification);
default:
return null;
}
})()}
<NotificationOverlayContainer notification={notification} />
</div>
);
}
}

View File

@@ -0,0 +1,39 @@
// <NotificationOverlayContainer>
// ==============================
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/notification/overlay/container
// * * * * * * * //
// Imports
// -------
// Package imports.
import { connect } from 'react-redux';
// Mastodon imports.
import { markNotificationForDelete } from 'mastodon/actions/notifications';
// Our imports.
import NotificationOverlay from './notification_overlay';
// State mapping
// -------------
const mapStateToProps = state => ({
show: state.getIn(['notifications', 'cleaningMode']),
});
// Dispatch mapping
// ----------------
const mapDispatchToProps = dispatch => ({
onMarkForDelete(id, yes) {
dispatch(markNotificationForDelete(id, yes));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);

View File

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

View File

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

View File

@@ -0,0 +1,268 @@
// <StatusActionBar>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/status/action_bar
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages } from 'react-intl';
// Mastodon imports.
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
// Our imports.
import CommonButton from 'glitch/components/common/button';
// Stylesheet imports.
import './style';
// * * * * * * * //
// Initial setup
// -------------
// Holds our localization messages.
const messages = defineMessages({
delete:
{ id: 'status.delete', defaultMessage: 'Delete' },
mention:
{ id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute:
{ id: 'account.mute', defaultMessage: 'Mute @{name}' },
block:
{ id: 'account.block', defaultMessage: 'Block @{name}' },
reply:
{ id: 'status.reply', defaultMessage: 'Reply' },
replyAll:
{ id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog:
{ id: 'status.reblog', defaultMessage: 'Boost' },
cannot_reblog:
{ id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite:
{ id: 'status.favourite', defaultMessage: 'Favourite' },
open:
{ id: 'status.open', defaultMessage: 'Expand this status' },
report:
{ id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation:
{ id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation:
{ id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
share:
{ id: 'status.share', defaultMessage: 'Share' },
more:
{ id: 'status.more', defaultMessage: 'More' },
});
// * * * * * * * //
// The component
// -------------
export default class StatusActionBar extends ImmutablePureComponent {
// Props.
static propTypes = {
detailed: PropTypes.bool,
handler: PropTypes.objectOf(PropTypes.func).isRequired,
history: PropTypes.object,
intl: PropTypes.object.isRequired,
me: PropTypes.number,
status: ImmutablePropTypes.map.isRequired,
};
// These handle all of our actions.
handleReplyClick = () => {
const { handler, history, status } = this.props;
handler.reply(status, { history }); // hack
}
handleFavouriteClick = () => {
const { handler, status } = this.props;
handler.favourite(status);
}
handleReblogClick = (e) => {
const { handler, status } = this.props;
handler.reblog(status, e.shiftKey);
}
handleDeleteClick = () => {
const { handler, status } = this.props;
handler.delete(status);
}
handleMentionClick = () => {
const { handler, history, status } = this.props;
handler.mention(status.get('account'), { history }); // hack
}
handleMuteClick = () => {
const { handler, status } = this.props;
handler.mute(status.get('account'));
}
handleBlockClick = () => {
const { handler, status } = this.props;
handler.block(status.get('account'));
}
handleOpen = () => {
const { history, status } = this.props;
history.push(`/statuses/${status.get('id')}`);
}
handleReport = () => {
const { handler, status } = this.props;
handler.report(status);
}
handleShare = () => {
const { status } = this.props;
navigator.share({
text: status.get('search_index'),
url: status.get('url'),
});
}
handleConversationMuteClick = () => {
const { handler, status } = this.props;
handler.muteConversation(status);
}
// Renders our component.
render () {
const {
handleBlockClick,
handleConversationMuteClick,
handleDeleteClick,
handleFavouriteClick,
handleMentionClick,
handleMuteClick,
handleOpen,
handleReblogClick,
handleReplyClick,
handleReport,
handleShare,
} = this;
const { detailed, intl, me, status } = this.props;
const account = status.get('account');
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const reblogTitle = reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog);
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
let menu = [];
let replyIcon;
let replyTitle;
// This builds our menu.
if (!detailed) {
menu.push({
text: intl.formatMessage(messages.open),
action: handleOpen,
});
menu.push(null);
}
menu.push({
text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: handleConversationMuteClick,
});
menu.push(null);
if (account.get('id') === me) {
menu.push({
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
});
} else {
menu.push({
text: intl.formatMessage(messages.mention, {
name: account.get('username'),
}),
action: handleMentionClick,
});
menu.push(null);
menu.push({
text: intl.formatMessage(messages.mute, {
name: account.get('username'),
}),
action: handleMuteClick,
});
menu.push({
text: intl.formatMessage(messages.block, {
name: account.get('username'),
}),
action: handleBlockClick,
});
menu.push({
text: intl.formatMessage(messages.report, {
name: account.get('username'),
}),
action: handleReport,
});
}
// This selects our reply icon.
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
// Now we can render the component.
return (
<div className='glitch glitch__status__action-bar'>
<CommonButton
className='action-bar\button'
disabled={anonymousAccess}
title={replyTitle}
icon={replyIcon}
onClick={handleReplyClick}
/>
<CommonButton
className='action-bar\button'
disabled={anonymousAccess || reblogDisabled}
active={status.get('reblogged')}
title={reblogTitle}
icon='retweet'
onClick={handleReblogClick}
/>
<CommonButton
className='action-bar\button'
disabled={anonymousAccess}
animate
active={status.get('favourited')}
title={intl.formatMessage(messages.favourite)}
icon='star'
onClick={handleFavouriteClick}
/>
{
'share' in navigator ? (
<CommonButton
className='action-bar\button'
disabled={status.get('visibility') !== 'public'}
title={intl.formatMessage(messages.share)}
icon='share-alt'
onClick={handleShare}
/>
) : null
}
<div className='action-bar\button'>
<DropdownMenuContainer
items={menu}
disabled={anonymousAccess}
icon='ellipsis-h'
size={18}
direction='right'
aria-label={intl.formatMessage(messages.more)}
/>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,28 @@
@import 'variables';
.glitch.glitch__status__action-bar {
display: block;
height: 1.25em;
font-size: 1.25em;
line-height: 1;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
// Dropdown style override for centering on the icon
.dropdown--active {
position: relative;
.dropdown__content.dropdown__right {
left: calc(50% + 3px);
right: initial;
transform: translate(-50%, 0);
top: 22px;
}
&::after {
right: 1px;
bottom: -2px;
}
}
}

View File

@@ -0,0 +1,212 @@
// <StatusContainer>
// =================
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/status/container
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import React from 'react';
import {
defineMessages,
injectIntl,
FormattedMessage,
} from 'react-intl';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { createStructuredSelector } from 'reselect';
// Mastodon imports.
import { blockAccount, muteAccount } from 'mastodon/actions/accounts';
import {
replyCompose,
mentionCompose,
} from 'mastodon/actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
} from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
import { initReport } from 'mastodon/actions/reports';
import {
muteStatus,
unmuteStatus,
deleteStatus,
} from 'mastodon/actions/statuses';
import { fetchStatusCard } from 'mastodon/actions/cards';
// Our imports.
import Status from '.';
import makeStatusSelector from 'glitch/selectors/status';
// * * * * * * * //
// Initial setup
// -------------
// Localization messages.
const messages = defineMessages({
blockConfirm : {
id : 'confirmations.block.confirm',
defaultMessage : 'Block',
},
deleteConfirm : {
id : 'confirmations.delete.confirm',
defaultMessage : 'Delete',
},
deleteMessage : {
id : 'confirmations.delete.message',
defaultMessage : 'Are you sure you want to delete this status?',
},
muteConfirm : {
id : 'confirmations.mute.confirm',
defaultMessage : 'Mute',
},
});
// * * * * * * * //
// State mapping
// -------------
// We wrap our `mapStateToProps()` function in a
// `makeMapStateToProps()` to give us a closure and preserve
// `makeGetStatus()`'s value.
const makeMapStateToProps = () => {
const statusSelector = makeStatusSelector();
// State mapping.
return (state, ownProps) => {
let status = statusSelector(state, ownProps.id);
let reblogStatus = status.get('reblog', null);
let comrade = undefined;
let prepend = undefined;
// Processes reblogs and generates their prepend.
if (reblogStatus !== null && typeof reblogStatus === 'object') {
comrade = status.get('account');
status = reblogStatus;
prepend = 'reblogged';
}
// This is what we pass to <Status>.
return {
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
comrade: comrade || ownProps.comrade,
deleteModal: state.getIn(['meta', 'delete_modal']),
me: state.getIn(['meta', 'me']),
prepend: prepend || ownProps.prepend,
reblogModal: state.getIn(['meta', 'boost_modal']),
settings: state.get('local_settings'),
status: status,
};
};
};
// * * * * * * * //
// Dispatch mapping
// ----------------
const makeMapDispatchToProps = (dispatch) => {
const dispatchSelector = createStructuredSelector({
handler: ({ intl }) => ({
block (account) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
}));
},
delete (status) {
if (!this.deleteModal) { // TODO: THIS IS BORKN (this refers to handler)
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'))),
}));
}
},
favourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
},
fetchCard (status) {
dispatch(fetchStatusCard(status.get('id')));
},
mention (account, router) {
dispatch(mentionCompose(account, router));
},
modalReblog (status) {
dispatch(reblog(status));
},
mute (account) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id'))),
}));
},
muteConversation (status) {
if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id')));
} else {
dispatch(muteStatus(status.get('id')));
}
},
openMedia (media, index) {
dispatch(openModal('MEDIA', { media, index }));
},
openVideo (media, time) {
dispatch(openModal('VIDEO', { media, time }));
},
reblog (status, withShift) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
if (withShift || !this.reblogModal) { // TODO: THIS IS BORKN (this refers to handler)
this.modalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.modalReblog }));
}
}
},
reply (status, router) {
dispatch(replyCompose(status, router));
},
report (status) {
dispatch(initReport(status.get('account'), status));
},
}),
});
return (_, ownProps) => dispatchSelector(ownProps);
};
// * * * * * * * //
// Connecting
// ----------
// `connect` will only update when its resultant props change. So
// `withRouter` won't get called unless an update is already planned.
// This is intended behaviour because we only care about the (mutable)
// `history` object.
export default injectIntl(
connect(makeMapStateToProps, makeMapDispatchToProps)(
withRouter(Status)
)
);

View File

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

View File

@@ -0,0 +1,190 @@
// <StatusContentCard>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/status/content/card
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import punycode from 'punycode';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports.
import emojify from 'mastodon/emoji';
// Our imports.
import CommonLink from 'glitch/components/common/link';
import CommonSeparator from 'glitch/components/common/separator';
// Stylesheet imports.
import './style';
// * * * * * * * //
// Initial setup
// -------------
// Reliably gets the hostname from a URL.
const getHostname = url => {
const parser = document.createElement('a');
parser.href = url;
return parser.hostname;
};
// * * * * * * * //
// The component
// -------------
export default class Card extends ImmutablePureComponent {
// Props.
static propTypes = {
card: ImmutablePropTypes.map.isRequired,
fullwidth: PropTypes.bool,
letterbox: PropTypes.bool,
}
// Rendering.
render () {
const { card, fullwidth, letterbox } = this.props;
let media = null;
let text = null;
let author = null;
let provider = null;
let caption = null;
// This gets all of our card properties.
const authorName = card.get('author_name');
const authorUrl = card.get('author_url');
const description = card.get('description');
const html = card.get('html');
const image = card.get('image');
const providerName = card.get('provider_name');
const providerUrl = card.get('provider_url');
const title = card.get('title');
const type = card.get('type');
const url = card.get('url');
// Sets our class.
const computedClass = classNames('glitch', 'glitch__status__content__card', type, {
_fullwidth: fullwidth,
_letterbox: letterbox,
});
// A card is required to render.
if (!card) return null;
// This generates our card media (image or video).
switch(type) {
case 'photo':
media = (
<CommonLink
className='card\media card\photo'
href={url}
>
<img
alt={title}
src={image}
/>
</CommonLink>
);
break;
case 'video':
media = (
<div
className='card\media card\video'
dangerouslySetInnerHTML={{ __html: html }}
/>
);
break;
}
// If we have at least a title or a description, then we can
// render some textual contents.
if (title || description) {
text = (
<CommonLink
className='card\description'
href={url}
>
{type === 'link' && image ? (
<div className='card\thumbnail'>
<img
alt=''
className='card\image'
src={image}
/>
</div>
) : null}
{title ? (
<h1 className='card\title'>{title}</h1>
) : null}
{emojify(description)}
</CommonLink>
);
}
// This creates links or spans (depending on whether a URL was
// provided) for the card author and provider.
if (authorUrl) {
author = (
<CommonLink
className='card\author card\link'
href={authorUrl}
>
{authorName ? authorName : punycode.toUnicode(getHostname(authorUrl))}
</CommonLink>
);
} else if (authorName) {
author = <span className='card\author'>{authorName}</span>;
}
if (providerUrl) {
provider = (
<CommonLink
className='card\provider card\link'
href={providerUrl}
>
{providerName ? providerName : punycode.toUnicode(getHostname(providerUrl))}
</CommonLink>
);
} else if (providerName) {
provider = <span className='card\provider'>{providerName}</span>;
}
// If we have either the author or the provider, then we can
// render an attachment.
if (author || provider) {
caption = (
<figcaption className='card\caption'>
{author}
<CommonSeparator
className='card\separator'
visible={author && provider}
/>
{provider}
</figcaption>
);
}
// Putting the pieces together and returning.
return (
<figure className={computedClass}>
{media}
{text}
{caption}
</figure>
);
}
}

View File

@@ -0,0 +1,123 @@
@import 'variables';
.glitch.glitch__content__card {
display: block;
border: thin $glitch-texture-color solid;
border-radius: .35em;
background: $glitch-darker-color;
.card\\caption {
color: $ui-primary-color;
background: $glitch-texture-color;
font-size: (1.25em / 1.35); // approx. .925em
.card\\link { // caption links
color: inherit;
&:hover {
text-decoration: underline;
}
}
}
.card\\media {
display: block;
position: relative;
width: 100%;
height: 13.5em;
/*
Our fallback styles letterbox the media, but we'll expand it to
fill the container if supported. This won't do anything for
`<iframe>`s, but we'll just have to trust them to manage their
own content.
*/
& > * {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: auto;
max-width: 100%;
height: auto;
max-height: 100%;
@supports (object-fit: cover) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.card\\description {
color: $ui-secondary-color;
background: $ui-base-color;
.card\\thumbnail {
position: relative;
float: left;
width: 6.75em;
height: 100%;
background: $glitch-darker-color;
& > img {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: auto;
max-width: 100%;
height: auto;
max-height: 100%;
@supports (object-fit: cover) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
/*
We have to divide the bottom margin of titles by their font-size to
get them to match what we use elsewhere.
*/
.card\\title {
margin-bottom: (.75em * 1.35 / 1.5);
font-size: 1.5em;
line-height: 1.125; // = 1.35 * (1.25 / 1.5)
}
}
&._fullwidth {
margin-left: -.75em;
width: calc(100% + 1.5em);
}
/*
If `letterbox` is specified, then we don't need object-fit (since
we essentially just do a scale-down).
*/
&._letterbox {
.card\\description .card\\thumbnail {
& > img {
width: auto;
height: auto;
object-fit: fill;
}
}
.card\\media {
& > * {
width: auto;
height: auto;
object-fit: fill;
}
}
}
}

View File

@@ -0,0 +1,191 @@
// <StatusContentGallery>
// ======================
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/status/content/gallery
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports:
// --------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage } from 'react-intl';
// Our imports.
import StatusContentGalleryItem from './item';
import StatusContentGalleryPlayer from './player';
import CommonButton from 'glitch/components/common/button';
// Stylesheet imports.
import './style';
// * * * * * * * //
// Initial setup
// -------------
// Holds our localization messages.
const messages = defineMessages({
hide: { id: 'media_gallery.hide_media', defaultMessage: 'Hide media' },
});
// * * * * * * * //
// The component
// -------------
export default class StatusContentGallery extends ImmutablePureComponent {
// Props and state.
static propTypes = {
attachments: ImmutablePropTypes.list.isRequired,
autoPlayGif: PropTypes.bool,
fullwidth: PropTypes.bool,
height: PropTypes.number.isRequired,
intl: PropTypes.object.isRequired,
letterbox: PropTypes.bool,
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
sensitive: PropTypes.bool,
standalone: PropTypes.bool,
};
state = {
visible: !this.props.sensitive,
};
// Handles media clicks.
handleMediaClick = index => {
const { attachments, onOpenMedia, standalone } = this.props;
if (standalone) return;
onOpenMedia(attachments, index);
}
// Handles showing and hiding.
handleToggle = () => {
this.setState({ visible: !this.state.visible });
}
// Handles video clicks.
handleVideoClick = time => {
const { attachments, onOpenVideo, standalone } = this.props;
if (standalone) return;
onOpenVideo(attachments.get(0), time);
}
// Renders.
render () {
const { handleMediaClick, handleToggle, handleVideoClick } = this;
const {
attachments,
autoPlayGif,
fullwidth,
intl,
letterbox,
sensitive,
} = this.props;
const { visible } = this.state;
const computedClass = classNames('glitch', 'glitch__status__content__gallery', {
_fullwidth: fullwidth,
});
const useableAttachments = attachments.take(4);
let button;
let children;
let size;
// This handles hidden media
if (!this.state.visible) {
button = (
<CommonButton
active
className='gallery\sensitive gallery\curtain'
title={intl.formatMessage(messages.hide)}
onClick={handleToggle}
>
<span className='gallery\message'>
<strong className='gallery\warning'>
{sensitive ? (
<FormattedMessage
id='status.sensitive_warning'
defaultMessage='Sensitive content'
/>
) : (
<FormattedMessage
id='status.media_hidden'
defaultMessage='Media hidden'
/>
)}
</strong>
<FormattedMessage
defaultMessage='Click to view'
id='status.sensitive_toggle'
/>
</span>
</CommonButton>
); // No children with hidden media
// If our media is visible, then we render it alongside the
// "eyeball" button.
} else {
button = (
<CommonButton
className='gallery\sensitive gallery\button'
icon={visible ? 'eye' : 'eye-slash'}
title={intl.formatMessage(messages.hide)}
onClick={handleToggle}
/>
);
// If our first item is a video, we render a player. Otherwise,
// we render our images.
if (attachments.getIn([0, 'type']) === 'video') {
size = 1;
children = (
<StatusContentGalleryPlayer
attachment={attachments.get(0)}
autoPlayGif={autoPlayGif}
intl={intl}
letterbox={letterbox}
onClick={handleVideoClick}
/>
);
} else {
size = useableAttachments.size;
children = useableAttachments.map(
(attachment, index) => (
<StatusContentGalleryItem
attachment={attachment}
autoPlayGif={autoPlayGif}
gallerySize={size}
index={index}
intl={intl}
key={attachment.get('id')}
letterbox={letterbox}
onClick={handleMediaClick}
/>
)
);
}
}
// Renders the gallery.
return (
<div
className={computedClass}
style={{ height: `${this.props.height}px` }}
>
{button}
{children}
</div>
);
}
}

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