Compare commits

...

176 Commits

Author SHA1 Message Date
Eugen Rochko
7406404fa3 Hotfix remote status formatting (#2543) 2017-04-27 23:37:22 +02:00
Eduardo Elias
010e4f2879 Language: Missing keys for pt-BR (#2540) 2017-04-27 22:32:38 +02:00
unarist
ebbbcfef1c Update Japanese translation (and English translation file) for JavaScript (#2538)
* Update English translation file for JavaScript

* Update Japanese translation for JavaScript
2017-04-27 20:41:25 +02:00
yhirano
3d776de2cb Change permission from 0755 to 0644 (#2536)
* chmod -x assets.rb

* chmod -x assets/fonts

* raname extname from jpeg to jpg
2017-04-27 19:29:41 +02:00
Yamagishi Kazutoshi
831ff60698 Fix broken oEmbed provider (#2537) 2017-04-27 19:29:11 +02:00
Yamagishi Kazutoshi
c25426ca47 Fix Doorkeeper error (#2534)
* Fix Doorkeeper error

* use Doorkeeper::OAuth::Scopes.from_string
2017-04-27 18:04:28 +02:00
jeroenpraat
09b93aaf85 Another (less urgent) Dutch string update (#2533)
* Another (less urgent) update

* Another (less urgent) update

* Dutch strings: consistency:  let's use 'voorkeuren' as a subset of 'instellingen'
2017-04-27 17:54:34 +02:00
Eugen Rochko
d4fedf84e0 Cache Status#ancestors recursive query results (#2527) 2017-04-27 17:07:15 +02:00
Eugen Rochko
2af4f3c4e2 Improve shared status verification (#2525)
* Instead of parsing shared status contents verbatim, make roundtrip
to purported original URL. Confirm that the "original" URL is from the
same domain as the author it claims to be from.

* Fix obvious typo, add comment

* Use URI look-up first

* Add test, update Goldfinger dependency to make less useless HTTP requests per Webfinger lookup
2017-04-27 17:06:47 +02:00
jeroenpraat
b8e7eee837 Last minute Dutch update and fix (after checking on live 1.3RC) (#2529)
* Update and fix

* Update and fix Dutch strings
2017-04-27 17:00:03 +02:00
Eugen Rochko
b48c9013aa Bump version, improve how version is stored for better commit history (#2526) 2017-04-27 15:22:19 +02:00
Matt Jankowski
b48f2cbc8b Catch error when server decryption fails on 2FA (#2512) 2017-04-27 15:18:21 +02:00
Ashley
1736badf28 Added '(dev)' to the title in development mode (#2524)
* Added different title for dev

* Changed indentation
2017-04-27 15:17:55 +02:00
Eugen Rochko
88725d6ce8 OEmbed support for PreviewCard (#2337)
* OEmbed support for PreviewCard

* Improve ProviderDiscovery code failure treatment

* Do not crawl links if there is a content warning, since those
don't display a link card anyway

* Reset db schema

* Fresh migrate

* Fix rubocop style issues
Fix #1681 - return existing access token when applicable instead of creating new

* Fix test

* Extract http client to helper

* Improve oembed controller
2017-04-27 14:42:22 +02:00
Yamagishi Kazutoshi
be0a01145b Sync a locale files for JavaScript (#2513)
There are multiple messages missing in en.jsx.
It is very troublesome when translating other languages.
2017-04-27 12:03:28 +02:00
Tomohiro Suwa
1f805a6377 Don't need to include ActiveModel::Validations (#2521) 2017-04-27 12:02:55 +02:00
Evan Minto
e595ccb294 Add ActivityPub Actor id to WebFinger JSON. (#2522) 2017-04-27 12:02:41 +02:00
Lindsey Bieda
43a29a9d0f Add aria-label to icon driven dropdown menus (#2516) 2017-04-27 12:01:36 +02:00
Kaylee
0dd71af5fa Copy over "Boost" hover text fix to detailed view. (#2508)
from #1754, should close out #1394
2017-04-27 01:53:55 +02:00
Matt Jankowski
0618f09939 Add spec coverage and refactor authorize_follows controller (#2505) 2017-04-27 00:19:53 +02:00
Koala Yeung
a23eaf720e Update Russian translation (rebased) (#2503)
* 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

* Update Russian translation

* Fix merge conflicts

Redid Works by @rkarabut
2017-04-26 22:33:54 +02:00
Koala Yeung
9d3fc1281d travis: Add yarn and nodejs caching (#2504)
Also cache node_module and yarn cache in Travis test process.
Improves speed for Travis tests.
2017-04-26 22:32:23 +02:00
Matt Jankowski
8857cabca4 Domain block service cleanup (#2490)
* Add coverage for domain block service with silence

* Get rid of warning about find_each and order

* Move domain_block to attr_reader

* Move optional clear_media into silence_accounts method

* Use blocked_domain method to reduce passed vars

* Extract blocked_domain_accounts method to find accounts on the domain

* Extract media_from_blocked_domain method to find relevant attachments

* Separate destruction of account images and account attachments
2017-04-26 20:09:01 +02:00
Matt Jankowski
affd75936e Add spec for Status.as_home_timeline (#2451) 2017-04-26 20:08:30 +02:00
Matt Jankowski
3e78b7cc3a Updated renamed i18n keys in de translation (#2500) 2017-04-26 19:33:10 +02:00
Matthieu Paret
647a148d4d fix indentation in translation file (#2498) 2017-04-26 18:48:43 +02:00
yhirano
bb777c24ff add task to report code statistics (#2489) 2017-04-26 18:47:22 +02:00
Benedikt Geißler
0576daf5f9 Update German translation (#2485) 2017-04-26 15:46:14 +02:00
Quent-in
c442cade78 Update of simple_forum OC (#2486)
Now perfectly similar to the English one.
2017-04-26 15:45:58 +02:00
Yamagishi Kazutoshi
6004b143a8 Update Japanese translation (#2487)
follow up #1309 & #2279
2017-04-26 15:45:47 +02:00
Lindsey Bieda
25cbb8454c Red favicon for dev (#2470)
* Set a dev specific favicon

* Consistent spacing

* Add trailing slash for consistency

* Update to remove interpolation
2017-04-26 15:45:27 +02:00
Tomohiro Suwa
416c9675fc Don't toot request with only blank characters (#2154)
* Don't toot request with only blank characters

* Enable toot button if no text
2017-04-26 15:18:09 +02:00
Yamagishi Kazutoshi
6a1e287053 Fix undefined variable in OnboardingModal (#2482) 2017-04-26 15:15:47 +02:00
Koala Yeung
5bda32e460 Update ar.jsx (#2469)
Adding more missing strings.
2017-04-26 14:55:14 +02:00
anon
6137268e79 Update Japanese translation for activitypub and authorized followers (#2471)
* Update Japanese translation for activitypub and authorized followers

* Fixed Japanese translation by review
2017-04-26 14:54:35 +02:00
Wonderfall
234e931db2 fix onboarding modal (#2477) 2017-04-26 14:54:12 +02:00
Eugen Rochko
fdcb55a0a6 Fix various regressions (#2466) 2017-04-26 04:40:27 +02:00
Eugen Rochko
d97a0525aa Fix #2261 - noopener typo (#2463) 2017-04-26 03:55:51 +02:00
Francis Chong
193dddb433 Add media dimensions (#2448)
* Fixes #1985

- add migration AddMediaAttachmentMeta, which add meta field to media_attachments
- before saving attachment, set file meta if needed
- add meta in api

* add spec

* align the “size” format for image and video

* fix code climate

* fixes media_attachment_spec.rb
2017-04-26 03:48:12 +02:00
Keiji, Yoshimi
8fe36654ef attach_media should be in transaction of creation status (#2426) 2017-04-26 03:47:44 +02:00
Yamagishi Kazutoshi
8ee1af9530 Move a locale files for ActiveRecord (#2461)
ref #2162 and fix #2460
2017-04-26 02:16:30 +02:00
Michael Deeb
e4c294432f Added the repository field and a script for after npm version (#2459) 2017-04-26 01:51:51 +02:00
JP DeVries
3ebaeccec9 Toolbar icons accessible names (#1424)
Closes tootsuite/mastodon#1349

This is my first PR and I’m only checking in the source JSX file.
Please let me know if it should be checked in after being built also.
2017-04-26 01:44:50 +02:00
Guillaume Lo Re
7177e37b99 Stricter whitelist rules (#2213)
* Stricter whitelist rules

* Linting

* Added spec for blacklisting

* Test subdomain blacklist on domain whitelist

* No need to split

* Change spec name
2017-04-26 01:22:51 +02:00
Amelia
fbc5099402 Make the getting started ui more intuitive (#1309) 2017-04-26 01:21:05 +02:00
Quent-in
45b379abac Missing occitan locale file (#2446)
Based on the device.en file
2017-04-25 23:26:53 +02:00
Mingye Wang
6ae975996e Refine CW placeholder in Chinese jsx files (#2447) 2017-04-25 23:26:46 +02:00
Mingye Wang
5d26c70a9c Complete proofreading and addition for zh-CN YML translations (#2445)
* proofread & update zh-CN setting form locale

* Pull in new devise zh-CN translations from their wiki

https://github.com/plataformatec/devise/wiki/I18n,
more precisely https://gist.github.com/Artoria2e5/6d2a06ca9e92a417afc627b5000f4690

* pull in zh-CN translation for doorkeeper

... from https://github.com/doorkeeper-gem/doorkeeper-i18n/pull/31/
2017-04-25 23:26:33 +02:00
esetomo
322cbf83c8 fix typo on daily tasks logging (#2450) 2017-04-25 20:35:20 +02:00
Mingye Wang
fc99d11703 Convert to "container-based" travis build (#2449)
* Convert to "container-based" travis build

Since all ppa sources used are currently whitelisted by Travis, why don't we convert to a "sudo-less" structure that promises a shorter build start time?

* Explicitly disable sudo in Travis
2017-04-25 20:14:34 +02:00
Ash Furrow
48652cb41e Fixes string length issue for multibyte characters. (#2443) 2017-04-25 17:37:51 +02:00
Ash Furrow
d4f7f11c3c Adds logging for daily tasks (#2438)
* Adds logging for daily tasks.

* Fixes Code Climate issues in #2438.
2017-04-25 16:44:48 +02:00
Stephen Burgess
1ce951d0be Adjust status action bar icons (#2424)
Set status action bar to have display flex and center align items so dropdown icon appears center aligned with other icons. Make styles alpha organized.

Add hover state to boost icon - lighten color on hover.
2017-04-25 15:16:09 +02:00
Hiroe Jun
52c119052a Refactor seed.rb (#2430) 2017-04-25 15:15:37 +02:00
alpaca-tc
9317ec8eb1 Localize with i18n for Devise::FailureApp (#2309)
This PR fixes I18n.locale for rake middlewares. Mastodon uses Devise that depends on Warden.
Warden::Manager can be found in rake middleware. It is outside of the controller.

In the case of authentication failed, warden calls throw(:warden). At the time Warden::Manager
delegates request to failure_app to generate response and flash[:alert] after catching it.
Unfortunately, I18n.locale is already reset then because I18n.with_locale is enabled only
inside the controller. If we used I18n.locale=, Devise::FailureApp could get the current locale.
2017-04-25 15:06:41 +02:00
STJrInuyasha
1b9447853b Login link for single user instances (#2393)
* Login link for single user instances

* Flip the if statement around
Fixes AuthorizationController tests
2017-04-25 15:06:24 +02:00
Evan Minto
122d59ac41 Change ActivityPub paging to match spec. Clean up ActivityPub outbox changes. (#2410)
* Change ActivityPub paging to match spec. Clean up ActivityPub outbox changes.

* Fix code style and test failures for OutboxController.

* Attempt to fix CI errors.
2017-04-25 15:06:06 +02:00
Eugen Rochko
8b5179d006 Fix #2402 - Add Idempotency-Key header to PostStatusService that prevents (#2419)
duplicates. Web UI regenerates UUID for that header every time the compose
form is changed or successfully submitted

Also, fix Farsi i18n overwriting the English one
2017-04-25 15:04:49 +02:00
Yamagishi Kazutoshi
3ea5b948a4 Decodes URL containing IDN (#2436) 2017-04-25 15:03:51 +02:00
Matt Jankowski
164bad171f Fix Persian translation (#2428) (#2432)
Settings page contained labels in Persian, when I was using English.
2017-04-25 05:17:52 -04:00
Ash Furrow
acc691851d Username above toot box UI changes (#2418)
* Makes username above toot box into a link.

* Prepends username above toot box with an @.
2017-04-25 04:45:27 +02:00
178inaba
d2159deaf2 Optimize account search (#2421) 2017-04-25 04:44:43 +02:00
Eugen
17c591ffba Punycode URI normalization (#2370)
* Fix #2119 - Whenever about to send a HTTP request, normalize the URI

* Add test for IDN request in FetchLinkCardService

* Perform IDN normalization on domains before they are stored in the DB
2017-04-25 02:47:31 +02:00
Masoud Abkenar
bb04a9be52 Persian translation (#2405)
* Persian translation

* adding :fa to make the site use the rtl stylesheet

based on [this comment](https://github.com/tootsuite/mastodon/pull/2405#issuecomment-296721105)

* fix missing quotes

* add 'fa' locale
2017-04-24 22:39:15 +02:00
Eugen
338df98ddf Add more FeedManager#filter? tests (#2413) 2017-04-24 22:37:24 +02:00
Eugen
da022e1e4f Run i18n-tasks normalize (#2412) 2017-04-24 20:52:55 +02:00
Stephen Burgess
f098f55cab fix(styles): Spoiler trigger zindex, close modal button position (#2406)
#2338
2017-04-24 20:20:16 +02:00
Shin Adachi
bc955eaf61 add patch method support to CORS (#2408) 2017-04-24 20:19:51 +02:00
Stephen Burgess
2d99c962df Only show upload modal when drag event contains files (#2409)
* fix(upload): Only show upload modal when drag even contains files

* fix(firefox): Close drag window ondragend also

Do not only end drag styles on drag leave, but also on drag end. Fixes firefox bug. #687

* fix(drag-modal): Remove drag modal trigger cruft

* fix(upload-modal): Allow close with escape button
2017-04-24 20:19:33 +02:00
Eugen
e59f5c8e13 Add an RTL stylesheet for Arabic i18n (#2378) 2017-04-24 18:03:53 +02:00
Yamagishi Kazutoshi
b073b092c9 Force wrap long email address (#2395) 2017-04-24 17:31:01 +02:00
Matt Jankowski
092fdc89fa Fix flashes partial render error for controllers which don't inherit from application controller (#2400)
* Add failing spec for oauth/authorized_applications controller

* Use explicit reference to flashes partial from admin layout

Because some of the controllers which use the admin layout do not inherit from
application controller, this partial is not in their view path.
2017-04-24 17:30:30 +02:00
alpaca-tc
d000a0b58a Add index to statuses_tags#status_id (#2394) 2017-04-24 15:10:14 +02:00
Akihiko Odaki
cf845fed38 Hide some components rather than unmounting (#2271)
Hide some components rather than unmounting them to allow to show again
quickly and keep the view state such as the scrolled offset.
2017-04-24 04:49:08 +02:00
Yamagishi Kazutoshi
72c984e105 Fix background-size being overwritten (#2382) 2017-04-24 04:10:32 +02:00
Eduardo Elias
3d3707a077 Language: Missing keys for pt and pt-BR (#2355) 2017-04-24 02:49:29 +02:00
Matt Jankowski
a0dd90a397 Return force_ssl to the controller (#2380) 2017-04-24 02:44:05 +02:00
Nolan Lawson
1e2a5dded7 Simplify browserify+babelify config (#2377) 2017-04-24 02:19:10 +02:00
Yamagishi Kazutoshi
549ce78cf5 Improve Japanese translation (#2376)
ref #2359

- https://github.com/tootsuite/mastodon/pull/2359#discussion_r112838771
- https://github.com/tootsuite/mastodon/pull/2359#discussion_r112838772
- https://github.com/tootsuite/mastodon/pull/2359#discussion_r112838812
- https://github.com/tootsuite/mastodon/pull/2359#discussion_r112838823
- https://github.com/tootsuite/mastodon/pull/2359#discussion_r112838829
2017-04-24 02:18:41 +02:00
Effy Elden
1801a36414 Report processing improvements (#2349)
* Expose media attachments on reported statuses directly

* Comment out unused bulk report checkbox. Add title to report comment for viewing full comment. Add 'contents' column, with icons and numerical indicators to show the number of referenced statuses and media attachments in the report

* Link account name on authorize_follow card back to account

* Add localisation string for report_contents

* Show new admin accounts card partial on report view. Apply simple_format to report comment so newlines are preserved.

* Add new admin accounts card partial, for display quick useful admin stats (e.g. report history, moderation status).

* Fix localized variable
2017-04-24 00:44:37 +02:00
Eugen
501514960a Followers-only post federation (#2111)
* Make private toots get PuSHed to subscription URLs that belong to domains where you have approved followers

* Authorized followers controller, stub for bulk action

* Soft block in the background

* Add simple test for new controller

* Rename Settings::FollowersController to Settings::FollowerDomainsController, paginate results,
rename "private" post setting to "followers-only", fix pagination style, improve post privacy
preferences style, improve warning style

* Extract compose form warnings into own container, show warning when posting to followers-only with unlocked account
2017-04-24 00:38:37 +02:00
Ash Furrow
ef5937da1f Fixes problem with showing non-local accounts. (#2371) 2017-04-23 22:15:10 +02:00
ymmtmdk
072c6f1527 Add link for domain in land strip (#2306)
* Add link for domain in land strip

* Use root_path

* Avoid repeating code
2017-04-23 21:45:03 +02:00
Yamagishi Kazutoshi
55e32fe579 Update Japanese translation (#2360)
for #2181
2017-04-23 21:34:48 +02:00
Stephen Burgess
7ee8e50b9c fix(style): Fix styles after classname refactor (#2368)
Float detailed status display avatar left. Only apply display block to display names in status info, not in detailed status. Thanks to @nightpool for finding those. Make star icon in notification show up as gold. Add anchor selector back to status__content__spoiler-link in order to override default anchor style elsewhere.
2017-04-23 21:34:33 +02:00
Stephen Burgess
995f0ad51c feat(cw-button): Add aria controls to CW trigger (#2303)
Add an ID to the CW spoiler input field to give aria-controls a handle on it. Pass that id to the CW trigger button. Modify text icon button component to accept aria controls id value. Add aria-expanded value to text icon button to indicate when it is expanded.
2017-04-23 20:33:44 +02:00
Matt Jankowski
948dd26931 Fix deprecation warnings on activitypub controller specs (#2357) 2017-04-23 19:10:04 +02:00
Matt Jankowski
55e1e12b7d Ensure i18n matches options (#2358)
* Add failing spec showing that human_locales does not match what i18n knows about

* Add missing `ar` key for arabic to human locales

* Remove duplicate `id` key from available locales

* Sort keys in human locales list

* Add spec for human_locale helper
2017-04-23 19:09:52 +02:00
Yamagishi Kazutoshi
6e27e08cb4 Add Japanese translation to EmojiPicker component (#2359) 2017-04-23 19:09:34 +02:00
Yamagishi Kazutoshi
d670f72830 Show multibyte URI preview card (#2363) 2017-04-23 19:08:59 +02:00
Matt Jankowski
57cd6546c3 Make HSTS enable optional with force_ssl (#2364) 2017-04-23 19:08:32 +02:00
178inaba
1244630ab4 Add simple_format to simplified_format (#2198) 2017-04-23 14:23:27 +02:00
Effy Elden
a9a4710fe1 Alter Vagrantfile to use RVM for Ruby installation (#2346) 2017-04-23 14:21:20 +02:00
Effy Elden
cc83ee60fb Export environment variables from .env.vagrant before running rails commands in Vagrant provisioning (#2343) 2017-04-23 14:21:09 +02:00
ThibG
89dc29affb Allow webfinger controller to reply to user@WEB_DOMAIN in addition to user@LOCAL_DOMAIN (#2351)
This provides a hotfix for outbound salmon requests to other Mastodon instances
as they currently will try to resovle user@WEB_DOMAIN instead of user@LOCAL_DOMAIN
(see #2012 and #20312).

Furthermore, this should ease transition from users switching from
LOCAL_DOMAIN = WEB_DOMAIN to another LOCAL_DOMAIN when WEB_DOMAIN does not change.
2017-04-23 14:19:54 +02:00
Ashley
85e09518b9 Halved padding for emoji (#2344) 2017-04-23 14:19:25 +02:00
Stephen Burgess
663f090c45 fix(style): Fix regressions introduced by #2338 (#2345)
Put Column Slim Back Button classnames in order so it is positioned right and top. Add icon button style back to privacy dropdown and upload buttons so they are positioned correctly and do not wiggle. Remove abstracted style from CSS. Add missed class for the upload form thumbnail. Add the missed styles for upload form thumnail. Alphabetize styles for column-back-button. Fix fontSize typo for media-spoiler-video-play-icon.
2017-04-23 14:18:58 +02:00
Joël Quenneville
1cf9e14a41 Test embedded_view related code in a helper (#2282)
The two methods `StreamEntriesHelper#stream_link_target` and
`StreamEntriesHelper#acct` are based on checking whether we are running
in an embedded view.

This adds some test helper code to make the testing easier. We extracted
some "magic strings" to constants to lower the coupling in the specs.
2017-04-23 06:05:52 +02:00
Joël Quenneville
0c2fe22bc1 Add test coverage to CSS class generation (#2285)
The code that generates CSS is based on a lot of boolean conditions.
The possible combinations of these grows exponentially as we add more
conditions.

Since most of the code is conditional on a single boolean, we tested the
following:

1. All `false`
2. All `true`
3. Each individual flag set to `true`

The methods tested are:

* `StreamEntriesHelper#style_classes`
* `StreamEntriesHelper#microformats_classes`
* `StreamEntriesHelper#microformats_h_class`
2017-04-23 06:04:32 +02:00
Koala Yeung
6f0b3b069f Adding arabic string localization (rebased) (#1895)
* Create ar.jsx

* Preparing file for translation.

* Create ar.yml

* Preparing file for translation.

* Create simple_form.ar.yml

* Preparing file for translation.

* Create devise.ar.yml

* Preparing file for translation.

* Create doorkeeper.ar.yml

* Preparing file for translation.

* Update index.jsx

Preparing file for translation.

* Update mastodon.jsx

* Preparing file to translation.
* Correction thanks to @Taekiro
  https://github.com/tootsuite/mastodon/pull/1798#pullrequestreview-32924922

* Update settings_helper.rb

Preparing file to translation.

* Update application.rb

* Preparing file to translation.

* Update simple_form.ar.yml

* Update doorkeeper.ar.yml

* Update devise.ar.yml

* Update ar.yml

* Fix i18n-tasks unused issue (ar)

Remove unused translation strings

* Update application.rb

* Update ar.jsx

* Fix various issues with Arabic translation

* Fix duplication caused in merge
* Fix syntax error in devise.ar.yml
* Fix import error Duplicated incorrect import in mastodon.jsx
* Fix duplication error in locales/index.jsx

* Update ar.yml

* Update ar.jsx

* Update ar.yml

* Update ar.jsx

* Fix ar translation
2017-04-23 05:58:47 +02:00
Ashley
9cd20a7062 Tooltip for boosting depends on toot visibility (#1394) (#1754)
* Made tooltip for boosting depend on visibility

* Removed spaces that were added in merge conflict resolution

* Try again :P

* Added missing bracket

* Changed 'toot' to 'status'
2017-04-23 05:36:40 +02:00
Yamagishi Kazutoshi
219fb317ee Remove unnecessary variable in ColumnLink component (#2341)
Fix bug in cca41ea544 .
2017-04-23 05:21:38 +02:00
Evan Minto
66fd8e7821 ActivityPub: Add basic, read-only support for Outboxes, Notes, and Create/Announce Activities (#2197)
* Clean up collapsible components

* Expose user Outboxes and AS2 representations of statuses

* Save work thus far.

* Fix bad merge.

* Save my work

* Clean up pagination.

* First test working.

* Add tests.

* Add Forbidden error template.

* Revert yarn.lock changes.

* Fix code style deviations and use localized instead of hardcoded English text.
2017-04-23 05:21:10 +02:00
Yamagishi Kazutoshi
83e3538181 Update CA certificates in Dockerfile (#2339) 2017-04-23 04:59:56 +02:00
Ash Furrow
723f25a999 Admin UI for confirming users (#2245)
* Shows confirmed status in list.

* Adds ability to confirm users in admin UI.

* Added new english translations.

* Addresses feedback from #2245.

* More feedback.
2017-04-23 04:43:42 +02:00
Eugen
59b1de0bcf Add a confirmation modal: (#2279)
- Deleting a toot
- Muting, blocking someone
- Clearing notifications

Remove source map generation from development environment, as it is a huge
performance sink hole with little gains
2017-04-23 04:39:50 +02:00
Akihiko Odaki
df46864b39 Alter Japanese translations for CW and NSFW (#2305)
The previous translation for NSFW, "不適切" means "impertinent". This change
suggests "閲覧注意", which means the content requires precaution, instead.

However, "閲覧注意" was also a translation for CW. CW is now translated as
"警告", which simply means "warnings".
2017-04-23 04:31:40 +02:00
Stephen Burgess
cca41ea544 Replace inline styles with CSS classes (BEM) (#2338)
* fix(classnames): Status icon style classnames

Take out inline css and put into classnames for the following components: account, avatar, icon button, status action bar, notification.

* fix(status): Move styles from inline to classes for statuses

Move styles to classnames in components.scss for the following components:

display name
media gallery
status
status content
video player

* fix(classnames): Add classnames to rest of components

Take out inline styles and apply them to classnames in the sass for the following components:

button
column back button
slim column back button
collapsable column
dropdown menu
loading indicator
status list

* fix(classnames): Remove all non-dynamic inline styles

Components affected:

autosuggested
permalink
action bar
header
character counter
compose form
emoji dropdown
privacy dropdown
reply indicator
upload form
account auth
followers
getting started
column settings
mutes
settings
reblogs
status checkbox
report
action bar
status
card
boost modal
media modal
video modal

* fix(permalink): Do not lose classname

* fix(tests): Add space back in display name

* fix(status__wrapper): Remove duplicate css name

Remove incorrect style attribute. Remove style attribute all together. Cursor defaults to "auto" when not specified as 'default'.

* fix(nl): do not lose translations
2017-04-23 04:26:55 +02:00
Matt Jankowski
532bec6e56 Clean up flash display in views (#2336)
* Add flashes partial to DRY up admin/auth layouts

* Further consolidate flash messages
2017-04-23 04:22:34 +02:00
Matt Jankowski
ee82d8a876 Move force_ssl check to production config (#2165)
The force_ssl method from controllers does not add all of the options that the
sitewide configuration in a config block does. For example, HSTS enforcement is
not added by the controller method, but is added by this style.
2017-04-23 04:22:22 +02:00
mecab
1646ca75f0 Fix character images' sizing to make it robust (#2327) 2017-04-22 22:28:54 +02:00
jeroenpraat
f876665264 Translation emojis and sorted on alphabet (#2328) 2017-04-22 20:22:46 +02:00
ばん
5817bae2da fix can toot whitespace (#2218) 2017-04-22 19:48:55 +02:00
Stephen Burgess
4cfc155560 Improve aria support - Columns and Navigation Drawer Icons (#2299)
* feat(aria): Add aria-labels to underlabelled tab nav items

The drawer tabs which control primary navigation are only labelled by a title which is not available to many screenreaders. Add an aria-label attribute to each link to improve readability with screenreaders. Organize link attributes so link target is first followed by classname.
Issue #1349

* feat(aria): Replace abstract aria role of section with region

Abstract aria roles such as section should not be used in content. Use non-abstract 'region' aria role instead. That role expects an aria-labelledby attribute with an id. Pass an ID to the column header. Remove the aria-label attribute on the ColumnHeader because the same value is output in plaintext as its child.
Issue #1349

* fix(aria): Remove aria-controls attribute until solution is found

Columns do not have wrappers, so these icons can't point to a column wrapper which it controls. Instead these icons function as triggers to show or hide individual columns.
#1349

* fix(typo): Remove type of aria-labelledby instead of aria-label
2017-04-22 17:30:35 +02:00
Wesley Ellis
a1174a6d7e Add og:url tags (#2298)
* add og:url to stream_item view

* add og:url and text to about/show

* add og:url to profile, followers and follows pages
2017-04-22 17:29:46 +02:00
Akihiko Odaki
5357329454 Fix TabsBar update (#2300)
This change allows to update TabsBar according to route changes.
2017-04-22 17:29:02 +02:00
Ashley
7fea36d155 Added strings for emoji category localization (#2302) 2017-04-22 17:28:36 +02:00
Keiji, Yoshimi
2bf7e81ed5 update emojione-picker for fixed warnings. (#2324) 2017-04-22 17:28:02 +02:00
Quent-in
ca8ae21b52 missing a var (#2320)
oc variable is missing on the second liste
2017-04-22 15:12:09 +02:00
masarakki
d0ec4fb828 refactor-cores (#2308) 2017-04-22 15:11:56 +02:00
unarist
4428cf6f07 Fix Japanese translation of privacy disclaimer (#2315) 2017-04-22 15:10:14 +02:00
ThibG
fe43991d02 Add author/actor URI to the list of webfinger aliases (#2319)
This fixes outbound salmon requests to remote GNU Social instances
2017-04-22 15:09:49 +02:00
codl
8565ba68f7 strip leading & trailing spaces from remote follow acct (#997) (#2321) 2017-04-22 15:08:30 +02:00
Matt Jankowski
629d35e6f5 [WIP] Html lang on statuses (#2297)
* Add html lang attributes around statuses

* Remove urls from language detection
2017-04-22 04:26:25 +02:00
Matt Jankowski
67dea31b0f 2FA controller cleanup (#2296)
* Add spec coverage for settings/two_factor_auth area

* extract setup method for qr code

* Move otp required check to before action

* Merge method only used once

* Remove duplicate view

* Consolidate creation of @codes for backup

* Move settings/2fq#recovery_codes to settings/recovery_codes#create

* Rename settings/two_factor_auth#disable to #destroy

* Add coverage for the otp required path on 2fa#show

* Clean up the recovery codes list styles

* Move settings/two_factor_auth to settings/two_factor_authentication

* Reorganize the settings two factor auth area

Updated to use a flow like:

- settings/two_factor_authentication goes to a #show view which has a button
  either enable or disable 2fa on the account
- the disable button turns off the otp requirement for the user
- the enable button cycles the user secret and redirects to a confirmation page
- the confirmation page is a #new view which shows the QR code for user
- that page posts to #create which verifies the code, and creates the recovery
  codes
- that create action shares a view with a recovery codes controller which can be
  used separately to reset codes if needed
2017-04-22 04:23:17 +02:00
Ashley
6af21daac9 Allowed the emoji picker 'Search...' string to be localized (#2294) 2017-04-22 03:36:33 +02:00
Eugen
9d3be5579a Improve bio compatibility (#2278)
* Fix #1057 (close #1819) - Move HTML-formatted bio from <poco:note /> to <summary type="html" />

* Ensure <poco:note /> is plaintext for remote accounts, also, by stripping out HTML
2017-04-22 03:33:24 +02:00
Ashley
05ac28f3e4 Added API for single notification dismissal (#2251)
* Added API backend for notification dismissal

* Added render statement

* Changed statement
2017-04-22 02:30:35 +02:00
Akihiko Odaki
0f852c6f74 Do not set port for puma if it is bound to unix socket (#2289)
Setting port after binding a unix socket puma listen to both of an unix
socket and TCP/IP, which is not a desired behavior.
2017-04-22 01:22:20 +02:00
Joël Quenneville
974ac467de Add tests for StreamEntriesHelper#rtl? (#2286)
We used some random Arabic characters to test that the various RTL
conditions got triggered.
2017-04-22 00:13:37 +02:00
Naouak
af7d02da5d Fix issue with line return in file (#1947)
Fix #1940
2017-04-21 20:07:47 +02:00
Rachel H
5abd543766 Put space before image (#1735) 2017-04-21 20:06:25 +02:00
Yamagishi Kazutoshi
1948f9e767 Remove deprecated features at React v15.5 (#1905)
* Remove deprecated features at React v15.5

- [x] React.PropTypes
- [x] react-addons-pure-render-mixin
- [x] react-addons-test-utils

* Uncommented out & Add browserify_rails options

* re-add react-addons-shallow

* Fix syntax error from resolve conflicts

* follow up 59a77923b3
2017-04-21 20:05:35 +02:00
Eduardo Elias
27ea2a88c1 Language: Missing keys for pt and pt-BR (#2277) 2017-04-21 19:53:47 +02:00
Akihiko Odaki
16cd648181 Allow to listen Unix socket (#2085)
* Allow puma to listen Unix socket

* streaming: Show the whole listening address instead of the port

Port is not always appropriate (e.g. Unix socket)

* streaming: Close server before exiting

This change especially allows to remove Unix socket before exiting.
2017-04-21 19:24:31 +02:00
Yamagishi Kazutoshi
53b21ac1cd Add Japanese translation for display name and bio counter (#2275)
* Add Japanese translation for display name and bio counter

* Remove dep key
2017-04-21 19:23:20 +02:00
Eduardo Elias
e4550811b2 Language: Update missing key for pt and pt-BR (#2276) 2017-04-21 19:23:02 +02:00
Eduardo Elias
704846a258 Fix: missing : for language (#2274) 2017-04-21 19:18:16 +02:00
Koala Yeung
935aecdc32 Add Indonesian (Bahasa Indonesia / id) translation (#2200)
* add Indonesian (Bahasa Indonesia) translation

add id.jsx
add translations on email views
add devise.id.yml
add doorkeeper.id.yml
add id.yml
add simple_form.id.yml

update id locale on mastodon.jsx, index.jsx, settings_helper.rb, and
application.rb

* add Indonesian (Bahasa Indonesia) translation

add id.jsx
add translations on email views
add devise.id.yml
add doorkeeper.id.yml
add id.yml
add simple_form.id.yml

update id locale on mastodon.jsx, index.jsx, settings_helper.rb, and
application.rb

* fix InvalidLocaleData on a string

* fix InvalidLocaleData on a string

* Fix language keys in Indonesian ruby locale (id)

Use "id" instead of "en" as object keys.

* Removed obsoleted translation (id)
2017-04-21 19:16:26 +02:00
Koala Yeung
1714f08d75 Add Ido (io) translation (#2202)
* Add Ido (io) translation

* Fix mastodon.jsx for io reference

There is no react-intl support for io. Removed the references.
2017-04-21 19:15:25 +02:00
Matt Jankowski
452dc6b5fe Use local_time gem for timezone replacement (#2174)
For the 'time ago' value of stream entry simple statuses and the timestamp value
of detailed statuses - output their UTC value and use the browser timezone to
convert locally.
2017-04-21 18:19:59 +02:00
Mingye Wang
5c9aa2b732 Fix mangling of ##tag matches (#2194) (#2247)
This commit fixes hashtag_html so it correctly handles matches with multiple hash-signs.

Bug located by @over9001, initial fix suggested by @nightpool.
2017-04-21 18:18:58 +02:00
Ash Furrow
78af88e1f4 Hides superluous details on small screens (#2175)
* Hides superluous details on small screans.

* Addressed feedback from #2175.
2017-04-21 18:17:55 +02:00
Sebastian Morr
74c474a652 Display remaining characters when editing display name and bio (#2219) 2017-04-21 18:17:21 +02:00
Takayoshi Nishida
5e33ad29d4 Fix #2195 - Set locale to error pages (#2255)
* Fix #2195 - Set locale to error pages

* Fix #2195 - Cut duplicate process into one method
2017-04-21 18:11:20 +02:00
Ram Lmn
27a99b19e8 Use local fonts and stop scrollable elements from repainting (#2266) 2017-04-21 18:09:05 +02:00
Matt Jankowski
ee0c897bba User settings mutation (#2270)
* Add user spec for settings, highlight global default mutation issue

* Fix mutation issue caused by settings/preferences spec
2017-04-21 18:07:17 +02:00
Yamagishi Kazutoshi
ceecf96208 Add crossorigin="anonymous" to about (#2272) 2017-04-21 18:06:43 +02:00
ik-fib
47a3036ea6 Add Mastodon version to user agent (#2253) 2017-04-21 17:26:23 +02:00
jeroenpraat
d2dc31a74a Ruby time format fixed (#2259) 2017-04-21 16:47:15 +02:00
Yamagishi Kazutoshi
60a9b938b4 Upgrade jQuery to v2.x (#2246)
Fix #2199 and minifying assets file.

jQuery v1.x is supports legacy browsers.
But, Mastodon does not support legacy browsers.
I think jQuery v1.x is unnecessary.
2017-04-21 16:46:32 +02:00
178inaba
dd517b9a55 Fix a bug where the hashtag could not be specified with the streaming API (#2258) 2017-04-21 16:45:14 +02:00
Jérémy Benoist
b3329c362e Open authorized application in a new window (#2250)
Like it's already done when you click on an application from a toot, it's opening in a new window.
0254ee9795/app/views/stream_entries/_detailed_status.html.haml (L41)
2017-04-21 16:44:14 +02:00
Mingye Wang
b00f4a0cf3 Use new ESLint filename (.json) (#2248)
* Use new ESLint filename (.json)

Codeclimate's ESLint 3 is complaining about the old filename. Let me try changing it.

* point eslint to new rc

(squash this)
2017-04-21 16:43:57 +02:00
Ash Furrow
a0ed88a99b Adds version to about/more and API (#2181)
* Adds version.

* Cleans up code.

* Removes standalone endpoint and adds version to instance endpoint.

* Addresses feedback from #2181.
2017-04-21 03:30:59 +02:00
May Kittens Devour Your Soul
25d2853db2 Croatian language updates & fixes (#2203)
* Update hr.jsx

* Update and rename simple.form.hr.yml to simple_form.hr.yml

fixed file name

* Update hr.yml

* Update hr.yml
2017-04-21 03:30:15 +02:00
Quent-in
1930051bde Adjustment of oc.jsx (#2214) 2017-04-21 03:30:01 +02:00
Ash Furrow
6c34eafe02 Enables the Polish translation. (#2235) 2017-04-21 03:29:11 +02:00
jeroenpraat
bbff431e3a Updated Dutch strings and added Dutch onboarding modal strings (#2243)
* A bit of more explanation

* Update nl.yml

* That's more elegant

* More consistent and adding missing translation

* Update

* Update

* Added Dutch onboarding modal strings
2017-04-21 03:29:00 +02:00
Mingye Wang
3660a321f0 zh*: minor jsx whitespace fix (#2231) 2017-04-21 03:28:45 +02:00
Yeechan Lu
e06f307c2d Fix Chinese (China) translation availability (#2230) 2017-04-21 03:28:38 +02:00
Mingye Wang
3b342be2f2 Initial overhaul at zh-CN YML translations (#2242)
This commit provides a heavy proofreading of zh-CN translations, and
mainly draws from the zh-HK source when a translation is found too
incoherent. Translating directly from en is used when zh-HK translation
is not available.

This commit also completes the tweet-toot (choo-choo, actually) for
zh-cn introduced in #2044. Some minor copyediting, as well as
additional en translations, has been fed back into zh-hk text.
2017-04-21 03:27:45 +02:00
Matt Jankowski
2dda356e3f Clean up settings/preferences controller (#2237)
* Add missing fields group on preferences page

* Clean up settings/preferences controller

* Extract a UserSettingsDecorator
2017-04-21 03:26:52 +02:00
Andrey Blinov
972f6bc861 Fix Textarea height should be reset upon sending toot #2189 (#2236) 2017-04-20 23:53:54 +02:00
Shel R
d2514445e1 Repair most egregious mistakes in onboarding modal (#1964)
* Repair most egregious mistakes

The deployed changes broke the pedagogy and curriculum built into the design of the onboarding modals, which had been reviewed and approved by various other contributors. While the changed layouts of the pages make some of the original decisions impossible to re-implement without substantial work, these changes should at least make the modals not outright terrible. 

The empty confusing description of Mastodon was replaced with an actual basic explanation of federation. The language was modified to be friendly and conversational again. Grammatical errors were fixed. The explanations of local and federated timelines were re-ordered to be in the direct that the menus go, top to bottom, and the sentence was re-added which explains that they are what "public timelines" refers to (since this phrasing is used elsewhere in the UI) and suggests the user use them to discover who to follow. A figure of the profile picture that lives above the compose box was padded to page two to serve as a landmark for less technically literate users to easily find the compose box and understand that the next page is moving upwards, so that they can find the search bar. 

Ideally, the elephant artwork should be either removed or replaced with something less depressing.  Room should be made to point users at the notifications column's settings icon because "how do I turn off the notification noise" is one of the most asked questions of new users and those icons are not easily noticeable. This last part may be made obsolete if the settings UI is later re-worked.

* Change copy

This commit addresses copy change suggestions made in the PR discussion

* Add periods to ends of sentences missing periods
* Made "Home" "Federated" "Local" and "Notifications" lowercase
* Separate explanation of "connecting" and the word "instance" into two sentences, to make the sentence less dense.
2017-04-20 18:20:40 +02:00
Alda Marteau-Hardi
daa46f14c4 French translation cleanup (#2223)
* Alphabetically order translation keys

* Fix some missing gender neutral translations in french

* Add a comment to remind contributors to order translations by key
2017-04-20 17:19:05 +02:00
Matt Jankowski
1ada494bb2 Admin settings controller refactor, add specs, cleanup (#2225)
* Add render_views for admin/settings spec

* Add coverage for admin/settings#update

* Add coverage for admin/settings typecasting open_registrations setting

* Simplify how admin/settings finds the value for updating

* Rely on activerecord to not update a value that hasnt changed

* Add coverage for non-existent setting

* Use a constant for boolean settings
2017-04-20 17:18:09 +02:00
Keiji, Yoshimi
4df26b2621 it may be mentions_status_id_index is duplicated of index_mentions_on_status_id index. (#2224) 2017-04-20 17:13:06 +02:00
Christoph Witzany
556f68ab15 Make Docker image buildable on ARM (#2065)
* after yarn upgrade

* after manually removing node-gyp as top level dependency
2017-04-20 13:29:38 +02:00
Yamagishi Kazutoshi
fdb2689a14 Fix PropTypes warning in OnboardingModal (#2191) 2017-04-20 13:22:54 +02:00
Matt Jankowski
598d3defd5 Use default locale for html lang in layouts (#2212) 2017-04-20 13:09:31 +02:00
Matt Jankowski
7f8044d913 Link to profile/feed/salmon from admin/accounts show page (#2186) 2017-04-20 04:00:02 +02:00
Ash Furrow
66b39ccaed Removes timestamp from URLs. (#2185) 2017-04-20 03:54:24 +02:00
Eugen
cae2a26ee3 Fix #2120 - Use Status#as_tag_timeline on public hashtag page (#2182)
* Fix #2120 - Use Status#as_tag_timeline on public hashtag page

* Update tags_controller.rb
2017-04-20 03:54:02 +02:00
Miguel Jacq
b79ba3db8a Send nil for SMTP_LOGIN or SMTP_PASSWORD if it was left blank in the envfile, otherwise Ruby still attempts auth even if SMTP_AUTH_METHOD is none (#2180) 2017-04-20 03:12:14 +02:00
Marcin Cieślak
59a77923b3 Add static gif for upload testing (#2168) 2017-04-19 23:47:50 +02:00
402 changed files with 10984 additions and 3263 deletions

View File

@@ -1,7 +1,11 @@
language: ruby
cache: bundler
cache:
bundler: true
yarn: true
directories:
- node_modules
dist: trusty
sudo: required
sudo: false
notifications:
email: false
@@ -14,6 +18,13 @@ env:
- CXX=g++-4.8
addons:
postgresql: 9.4
apt:
sources:
- ubuntu-toolchain-r-test
- trusty-media
packages:
- g++-4.8
- ffmpeg
rvm:
- 2.3.4
@@ -24,11 +35,6 @@ services:
bundler_args: --without development production --retry=3 --jobs=3
before_install:
- sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
- sudo add-apt-repository -y ppa:mc3man/trusty-media
- sudo apt-get -qq update
- sudo apt-get -qq install g++-4.8 ffmpeg
install:
- nvm install
- npm install -g yarn

View File

@@ -17,6 +17,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
postgresql-dev \
libxml2-dev \
libxslt-dev \
python \
build-base" \
&& apk -U upgrade && apk add \
$BUILD_DEPS \
@@ -28,11 +29,13 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
ffmpeg \
file \
imagemagick@edge \
ca-certificates \
&& npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \
&& yarn --ignore-optional \
&& yarn cache clean \
&& npm -g cache clean \
&& update-ca-certificates \
&& apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/*

View File

@@ -35,6 +35,7 @@ gem 'http_accept_language'
gem 'httplog'
gem 'kaminari'
gem 'link_header'
gem 'local_time'
gem 'nokogiri'
gem 'oj'
gem 'ostatus2', '~> 1.1'
@@ -48,6 +49,7 @@ gem 'rails-settings-cached'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
gem 'rqrcode'
gem 'ruby-oembed', require: 'oembed'
gem 'sanitize'
gem 'sidekiq'
gem 'sidekiq-unique-jobs'
gem 'simple-navigation'

View File

@@ -111,11 +111,19 @@ GEM
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1)
coffee-rails (4.2.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.2.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
debug_inspector (0.0.2)
devise (4.2.1)
bcrypt (~> 3.0)
@@ -157,7 +165,7 @@ GEM
ruby-progressbar (~> 1.4)
globalid (0.3.7)
activesupport (>= 4.1.0)
goldfinger (1.1.2)
goldfinger (1.2.0)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
@@ -224,6 +232,8 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
local_time (1.0.3)
coffee-rails
lograge (0.4.1)
actionpack (>= 4, < 5.1)
activesupport (>= 4, < 5.1)
@@ -249,6 +259,8 @@ GEM
nio4r (2.0.0)
nokogiri (1.7.1)
mini_portile2 (~> 2.1.0)
nokogumbo (1.4.10)
nokogiri
oj (2.18.5)
openssl (2.0.3)
orm_adapter (0.5.0)
@@ -389,6 +401,10 @@ GEM
ruby-oembed (0.12.0)
ruby-progressbar (1.8.1)
safe_yaml (1.0.4)
sanitize (4.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.23)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
@@ -443,7 +459,7 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.3)
unf_ext (0.0.7.4)
unicode-display_width (1.1.3)
uniform_notifier (1.10.0)
warden (1.2.7)
@@ -500,6 +516,7 @@ DEPENDENCIES
letter_opener
letter_opener_web
link_header
local_time
lograge
microformats2
nokogiri
@@ -530,6 +547,7 @@ DEPENDENCIES
rspec-sidekiq
rubocop
ruby-oembed
sanitize
sass-rails (~> 5.0)
sidekiq
sidekiq-unique-jobs

21
Vagrantfile vendored
View File

@@ -35,22 +35,12 @@ sudo apt-get install \
libreadline-dev \
-y
# Install rbenv
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
# Install rvm
cd /vagrant
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
rbenv install $(cat .ruby-version)
rbenv global $(cat .ruby-version)
read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm
# Configure database
sudo -u postgres createuser -U postgres vagrant -s
@@ -62,6 +52,7 @@ bundle install
yarn install
# Build Mastodon
export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup
bundle exec rails assets:precompile

View File

View File

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

View File

View File

View File

0
app/assets/fonts/roboto/roboto-bold-webfont.eot Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-bold-webfont.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

0
app/assets/fonts/roboto/roboto-bold-webfont.ttf Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-bold-webfont.woff Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-bold-webfont.woff2 Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-italic-webfont.eot Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-italic-webfont.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

0
app/assets/fonts/roboto/roboto-italic-webfont.ttf Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-italic-webfont.woff Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-italic-webfont.woff2 Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-medium-webfont.eot Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-medium-webfont.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

0
app/assets/fonts/roboto/roboto-medium-webfont.ttf Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-medium-webfont.woff Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-medium-webfont.woff2 Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-regular-webfont.eot Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-regular-webfont.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

0
app/assets/fonts/roboto/roboto-regular-webfont.ttf Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-regular-webfont.woff Executable file → Normal file
View File

0
app/assets/fonts/roboto/roboto-regular-webfont.woff2 Executable file → Normal file
View File

View File

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -10,6 +10,6 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery2
//= require jquery_ujs
//= require components

View File

@@ -1,7 +1,8 @@
//= require jquery
//= require jquery2
//= require jquery_ujs
//= require extras
//= require best_in_place
//= require local_time
$(function () {
$(".best_in_place").best_in_place();

View File

@@ -13,7 +13,7 @@ export function fetchStatusCard(id) {
dispatch(fetchStatusCardRequest(id));
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
if (!response.data.url || !response.data.title || !response.data.description) {
if (!response.data.url) {
return;
}

View File

@@ -85,6 +85,10 @@ export function submitCompose() {
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
visibility: getState().getIn(['compose', 'privacy'])
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey'])
}
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));

View File

@@ -1,5 +1,5 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import DisplayName from './display_name';
import Permalink from './permalink';
@@ -10,39 +10,30 @@ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }
});
const buttonsStyle = {
padding: '10px',
height: '18px'
};
class Account extends React.PureComponent {
const Account = React.createClass({
propTypes: {
account: ImmutablePropTypes.map.isRequired,
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
onBlock: React.PropTypes.func.isRequired,
onMute: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleFollow = this.handleFollow.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleMute = this.handleMute.bind(this);
}
handleFollow () {
this.props.onFollow(this.props.account);
},
}
handleBlock () {
this.props.onBlock(this.props.account);
},
}
handleMute () {
this.props.onMute(this.props.account);
},
}
render () {
const { account, me, intl } = this.props;
@@ -72,13 +63,13 @@ const Account = React.createClass({
return (
<div className='account'>
<div style={{ display: 'flex' }}>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
<div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
<DisplayName account={account} />
</Permalink>
<div style={buttonsStyle}>
<div className='account__relationship'>
{buttons}
</div>
</div>
@@ -86,6 +77,15 @@ const Account = React.createClass({
);
}
});
}
Account.propTypes = {
account: ImmutablePropTypes.map.isRequired,
me: PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
}
export default injectIntl(Account);

View File

@@ -1,14 +1,8 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
const AttachmentList = React.createClass({
propTypes: {
media: ImmutablePropTypes.list.isRequired
},
mixins: [PureRenderMixin],
class AttachmentList extends React.PureComponent {
render () {
const { media } = this.props;
@@ -29,6 +23,10 @@ const AttachmentList = React.createClass({
</div>
);
}
});
}
AttachmentList.propTypes = {
media: ImmutablePropTypes.list.isRequired
};
export default AttachmentList;

View File

@@ -1,5 +1,6 @@
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
const textAtCursorMatchesToken = (str, caretPosition) => {
@@ -27,30 +28,23 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
}
};
const AutosuggestTextarea = React.createClass({
class AutosuggestTextarea extends React.Component {
propTypes: {
value: React.PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: React.PropTypes.bool,
placeholder: React.PropTypes.string,
onSuggestionSelected: React.PropTypes.func.isRequired,
onSuggestionsClearRequested: React.PropTypes.func.isRequired,
onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
onChange: React.PropTypes.func.isRequired,
onKeyUp: React.PropTypes.func,
onKeyDown: React.PropTypes.func,
onPaste: React.PropTypes.func.isRequired,
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
suggestionsHidden: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0
};
},
this.onChange = this.onChange.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onSuggestionClick = this.onSuggestionClick.bind(this);
this.setTextarea = this.setTextarea.bind(this);
this.onPaste = this.onPaste.bind(this);
}
onChange (e) {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
@@ -68,7 +62,7 @@ const AutosuggestTextarea = React.createClass({
e.target.style.height = `${e.target.scrollHeight}px`;
this.props.onChange(e);
},
}
onKeyDown (e) {
const { suggestions, disabled } = this.props;
@@ -118,7 +112,7 @@ const AutosuggestTextarea = React.createClass({
}
this.props.onKeyDown(e);
},
}
onBlur () {
// If we hide the suggestions immediately, then this will prevent the
@@ -128,36 +122,35 @@ const AutosuggestTextarea = React.createClass({
setTimeout(() => {
this.setState({ suggestionsHidden: true });
}, 100);
},
}
onSuggestionClick (suggestion, e) {
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus();
},
}
componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
this.setState({ suggestionsHidden: false });
}
},
}
setTextarea (c) {
this.textarea = c;
},
}
onPaste (e) {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files)
e.preventDefault();
}
},
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
const { suggestionsHidden, selectedSuggestion } = this.state;
const className = 'autosuggest-textarea__textarea';
const style = { direction: 'ltr' };
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
@@ -167,7 +160,7 @@ const AutosuggestTextarea = React.createClass({
<div className='autosuggest-textarea'>
<textarea
ref={this.setTextarea}
className={className}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={true}
@@ -196,6 +189,20 @@ const AutosuggestTextarea = React.createClass({
);
}
});
};
AutosuggestTextarea.propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
};
export default AutosuggestTextarea;

View File

@@ -1,36 +1,23 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
const Avatar = React.createClass({
class Avatar extends React.PureComponent {
propTypes: {
src: React.PropTypes.string.isRequired,
staticSrc: React.PropTypes.string,
size: React.PropTypes.number.isRequired,
style: React.PropTypes.object,
animate: React.PropTypes.bool
},
getDefaultProps () {
return {
animate: false
};
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
hovering: false
};
},
mixins: [PureRenderMixin],
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
}
handleMouseEnter () {
this.setState({ hovering: true });
},
}
handleMouseLeave () {
this.setState({ hovering: false });
},
}
render () {
const { src, size, staticSrc, animate } = this.props;
@@ -51,7 +38,7 @@ const Avatar = React.createClass({
return (
<div
className='avatar'
className='account__avatar'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
@@ -59,6 +46,18 @@ const Avatar = React.createClass({
);
}
});
}
Avatar.propTypes = {
src: PropTypes.string.isRequired,
staticSrc: PropTypes.string,
size: PropTypes.number.isRequired,
style: PropTypes.object,
animate: PropTypes.bool
};
Avatar.defaultProps = {
animate: false
};
export default Avatar;

View File

@@ -1,53 +1,25 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
const Button = React.createClass({
class Button extends React.PureComponent {
propTypes: {
text: React.PropTypes.node,
onClick: React.PropTypes.func,
disabled: React.PropTypes.bool,
block: React.PropTypes.bool,
secondary: React.PropTypes.bool,
size: React.PropTypes.number,
style: React.PropTypes.object,
children: React.PropTypes.node
},
getDefaultProps () {
return {
size: 36
};
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
if (!this.props.disabled) {
this.props.onClick();
}
},
}
render () {
const style = {
fontFamily: 'inherit',
display: this.props.block ? 'block' : 'inline-block',
width: this.props.block ? '100%' : 'auto',
position: 'relative',
boxSizing: 'border-box',
textAlign: 'center',
border: '10px none',
fontSize: '14px',
fontWeight: '500',
letterSpacing: '0',
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
cursor: 'pointer',
lineHeight: `${this.props.size}px`,
borderRadius: '4px',
textDecoration: 'none',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden'
lineHeight: `${this.props.size}px`
};
return (
@@ -57,6 +29,21 @@ const Button = React.createClass({
);
}
});
}
Button.propTypes = {
text: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
size: PropTypes.number,
style: PropTypes.object,
children: PropTypes.node
};
Button.defaultProps = {
size: 36
};
export default Button;

View File

@@ -1,4 +1,5 @@
import { Motion, spring } from 'react-motion';
import PropTypes from 'prop-types';
const Collapsable = ({ fullHeight, isVisible, children }) => (
<Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
@@ -11,9 +12,9 @@ const Collapsable = ({ fullHeight, isVisible, children }) => (
);
Collapsable.propTypes = {
fullHeight: React.PropTypes.number.isRequired,
isVisible: React.PropTypes.bool.isRequired,
children: React.PropTypes.node.isRequired
fullHeight: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired
};
export default Collapsable;

View File

@@ -1,33 +1,31 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const iconStyle = {
display: 'inline-block',
marginRight: '5px'
};
class ColumnBackButton extends React.PureComponent {
const ColumnBackButton = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
if (window.history && window.history.length === 1) this.context.router.push("/");
else this.context.router.goBack();
},
}
render () {
return (
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<i className='fa fa-fw fa-chevron-left column-back-button__icon'/>
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
);
}
});
};
ColumnBackButton.contextTypes = {
router: PropTypes.object
};
export default ColumnBackButton;

View File

@@ -1,44 +1,31 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const outerStyle = {
position: 'absolute',
right: '0',
top: '-48px',
padding: '15px',
fontSize: '16px',
flex: '0 0 auto',
cursor: 'pointer'
};
class ColumnBackButtonSlim extends React.PureComponent {
const iconStyle = {
display: 'inline-block',
marginRight: '5px'
};
const ColumnBackButtonSlim = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
this.context.router.push('/');
},
}
render () {
return (
<div style={{ position: 'relative' }}>
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>
);
}
}
});
ColumnBackButtonSlim.contextTypes = {
router: PropTypes.object
};
export default ColumnBackButtonSlim;

View File

@@ -1,33 +1,16 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { Motion, spring } from 'react-motion';
import PropTypes from 'prop-types';
const iconStyle = {
fontSize: '16px',
padding: '15px',
position: 'absolute',
right: '0',
top: '-48px',
cursor: 'pointer',
zIndex: '3'
};
class ColumnCollapsable extends React.PureComponent {
const ColumnCollapsable = React.createClass({
propTypes: {
icon: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
fullHeight: React.PropTypes.number.isRequired,
children: React.PropTypes.node,
onCollapse: React.PropTypes.func
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
collapsed: true
};
},
mixins: [PureRenderMixin],
this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this);
}
handleToggleCollapsed () {
const currentState = this.state.collapsed;
@@ -37,7 +20,7 @@ const ColumnCollapsable = React.createClass({
if (!currentState && this.props.onCollapse) {
this.props.onCollapse();
}
},
}
render () {
const { icon, title, fullHeight, children } = this.props;
@@ -45,8 +28,8 @@ const ColumnCollapsable = React.createClass({
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
return (
<div style={{ position: 'relative' }}>
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
<div className='column-collapsable'>
<div role='button' tabIndex='0' title={`${title}`} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
<i className={`fa fa-${icon}`} />
</div>
@@ -60,6 +43,14 @@ const ColumnCollapsable = React.createClass({
</div>
);
}
});
}
ColumnCollapsable.propTypes = {
icon: PropTypes.string.isRequired,
title: PropTypes.string,
fullHeight: PropTypes.number.isRequired,
children: PropTypes.node,
onCollapse: PropTypes.func
};
export default ColumnCollapsable;

View File

@@ -1,27 +1,24 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../emoji';
const DisplayName = React.createClass({
propTypes: {
account: ImmutablePropTypes.map.isRequired
},
mixins: [PureRenderMixin],
class DisplayName extends React.PureComponent {
render () {
const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<span style={{ display: 'block', maxWidth: '100%', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }} className='display-name'>
<strong style={{ fontWeight: '500' }} dangerouslySetInnerHTML={displayNameHTML} /> <span style={{ fontSize: '14px' }}>@{this.props.account.get('acct')}</span>
<span className='display-name'>
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
</span>
);
}
});
};
DisplayName.propTypes = {
account: ImmutablePropTypes.map.isRequired
}
export default DisplayName;

View File

@@ -1,26 +1,20 @@
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
const DropdownMenu = React.createClass({
class DropdownMenu extends React.PureComponent {
propTypes: {
icon: React.PropTypes.string.isRequired,
items: React.PropTypes.array.isRequired,
size: React.PropTypes.number.isRequired,
direction: React.PropTypes.string
},
getDefaultProps () {
return {
constructor (props, context) {
super(props, context);
this.state = {
direction: 'left'
};
},
mixins: [PureRenderMixin],
this.setRef = this.setRef.bind(this);
this.renderItem = this.renderItem.bind(this);
}
setRef (c) {
this.dropdown = c;
},
}
handleClick (i, e) {
const { action } = this.props.items[i];
@@ -30,36 +24,36 @@ const DropdownMenu = React.createClass({
action();
this.dropdown.hide();
}
},
}
renderItem (item, i) {
if (item === null) {
return <li key={i} className='dropdown__sep' />;
return <li key={ 'sep' + i } className='dropdown__sep' />;
}
const { text, action, href = '#' } = item;
return (
<li key={i}>
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)}>
<li className='dropdown__content-list-item' key={ text + i }>
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'>
{text}
</a>
</li>
);
},
}
render () {
const { icon, items, size, direction } = this.props;
const { icon, items, size, direction, ariaLabel } = this.props;
const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right";
return (
<Dropdown ref={this.setRef}>
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
<i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
<i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} />
</DropdownTrigger>
<DropdownContent className={directionClass} style={{ lineHeight: '18px', textAlign: 'left' }}>
<ul>
<DropdownContent className={directionClass}>
<ul className='dropdown__content-list'>
{items.map(this.renderItem)}
</ul>
</DropdownContent>
@@ -67,6 +61,18 @@ const DropdownMenu = React.createClass({
);
}
});
}
DropdownMenu.propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
direction: PropTypes.string,
ariaLabel: PropTypes.string
};
DropdownMenu.defaultProps = {
ariaLabel: "Menu"
};
export default DropdownMenu;

View File

@@ -1,33 +1,30 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
const ExtendedVideoPlayer = React.createClass({
class ExtendedVideoPlayer extends React.PureComponent {
propTypes: {
src: React.PropTypes.string.isRequired,
time: React.PropTypes.number,
controls: React.PropTypes.bool.isRequired,
muted: React.PropTypes.bool.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleLoadedData = this.handleLoadedData.bind(this);
this.setRef = this.setRef.bind(this);
}
handleLoadedData () {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
},
}
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
},
}
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
},
}
setRef (c) {
this.video = c;
},
}
render () {
return (
@@ -42,8 +39,15 @@ const ExtendedVideoPlayer = React.createClass({
/>
</div>
);
},
}
});
}
ExtendedVideoPlayer.propTypes = {
src: PropTypes.string.isRequired,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired
};
export default ExtendedVideoPlayer;

View File

@@ -1,33 +1,12 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { Motion, spring } from 'react-motion';
import PropTypes from 'prop-types';
const IconButton = React.createClass({
class IconButton extends React.PureComponent {
propTypes: {
title: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func,
size: React.PropTypes.number,
active: React.PropTypes.bool,
style: React.PropTypes.object,
activeStyle: React.PropTypes.object,
disabled: React.PropTypes.bool,
inverted: React.PropTypes.bool,
animate: React.PropTypes.bool,
overlay: React.PropTypes.bool
},
getDefaultProps () {
return {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false
};
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
e.preventDefault();
@@ -35,7 +14,7 @@ const IconButton = React.createClass({
if (!this.props.disabled) {
this.props.onClick(e);
}
},
}
render () {
let style = {
@@ -68,6 +47,10 @@ const IconButton = React.createClass({
classes.push('overlayed');
}
if (this.props.className) {
classes.push(this.props.className)
}
return (
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) =>
@@ -84,6 +67,29 @@ const IconButton = React.createClass({
);
}
});
}
IconButton.propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool
};
IconButton.defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false
};
export default IconButton;

View File

@@ -1,4 +1,5 @@
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const LoadMore = ({ onClick }) => (
<a href="#" className='load-more' role='button' onClick={onClick}>
@@ -7,7 +8,7 @@ const LoadMore = ({ onClick }) => (
);
LoadMore.propTypes = {
onClick: React.PropTypes.func
onClick: PropTypes.func
};
export default LoadMore;

View File

@@ -1,14 +1,7 @@
import { FormattedMessage } from 'react-intl';
const style = {
textAlign: 'center',
fontSize: '16px',
fontWeight: '500',
paddingTop: '120px'
};
const LoadingIndicator = () => (
<div className='loading-indicator' style={style}>
<div className='loading-indicator'>
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
</div>
);

View File

@@ -1,5 +1,5 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
@@ -8,81 +8,11 @@ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
});
const outerStyle = {
marginTop: '8px',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
position: 'relative'
};
const spoilerStyle = {
textAlign: 'center',
height: '100%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
};
const spoilerSpanStyle = {
display: 'block',
fontSize: '14px',
};
const spoilerSubSpanStyle = {
display: 'block',
fontSize: '11px',
fontWeight: '500'
};
const spoilerButtonStyle = {
position: 'absolute',
top: '4px',
left: '4px',
zIndex: '100'
};
const itemStyle = {
boxSizing: 'border-box',
position: 'relative',
float: 'left',
border: 'none',
display: 'block'
};
const thumbStyle = {
display: 'block',
width: '100%',
height: '100%',
textDecoration: 'none',
backgroundSize: 'cover',
cursor: 'zoom-in'
};
const gifvThumbStyle = {
position: 'relative',
zIndex: '1',
width: '100%',
height: '100%',
objectFit: 'cover',
top: '50%',
transform: 'translateY(-50%)',
cursor: 'zoom-in'
};
const Item = React.createClass({
propTypes: {
attachment: ImmutablePropTypes.map.isRequired,
index: React.PropTypes.number.isRequired,
size: React.PropTypes.number.isRequired,
onClick: React.PropTypes.func.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
},
mixins: [PureRenderMixin],
class Item extends React.PureComponent {
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
const { index, onClick } = this.props;
@@ -93,7 +23,7 @@ const Item = React.createClass({
}
e.stopPropagation();
},
}
render () {
const { attachment, index, size } = this.props;
@@ -152,24 +82,26 @@ const Item = React.createClass({
if (attachment.get('type') === 'image') {
thumbnail = (
<a
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || attachment.get('url')}
onClick={this.handleClick}
target='_blank'
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }}
/>
);
} else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif;
thumbnail = (
<div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }} className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
<div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
<video
className='media-gallery__item-gifv-thumbnail'
role='application'
src={attachment.get('url')}
onClick={this.handleClick}
autoPlay={autoPlay}
loop={true}
muted={true}
style={gifvThumbStyle}
/>
<span className='media-gallery__gifv__label'>GIF</span>
@@ -178,40 +110,40 @@ const Item = React.createClass({
}
return (
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
{thumbnail}
</div>
);
}
});
}
const MediaGallery = React.createClass({
Item.propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool.isRequired
};
getInitialState () {
return {
visible: !this.props.sensitive
class MediaGallery extends React.PureComponent {
constructor (props, context) {
super(props, context);
this.state = {
visible: !props.sensitive
};
},
propTypes: {
sensitive: React.PropTypes.bool,
media: ImmutablePropTypes.list.isRequired,
height: React.PropTypes.number.isRequired,
onOpenMedia: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
},
mixins: [PureRenderMixin],
this.handleOpen = this.handleOpen.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleOpen (e) {
this.setState({ visible: !this.state.visible });
},
}
handleClick (index) {
this.props.onOpenMedia(this.props.media, index);
},
}
render () {
const { media, intl, sensitive } = this.props;
@@ -228,9 +160,9 @@ const MediaGallery = React.createClass({
}
children = (
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<span style={spoilerSpanStyle}>{warning}</span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
<span className='media-spoiler__warning'>{warning}</span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else {
@@ -239,8 +171,8 @@ const MediaGallery = React.createClass({
}
return (
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
<div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
<div className='media-gallery' style={{ height: `${this.props.height}px` }}>
<div className='spoiler-button' style={{ display: !this.state.visible ? 'none' : 'block' }}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
</div>
@@ -249,6 +181,15 @@ const MediaGallery = React.createClass({
);
}
});
}
MediaGallery.propTypes = {
sensitive: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired,
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired
};
export default injectIntl(MediaGallery);

View File

@@ -1,28 +1,36 @@
const Permalink = React.createClass({
import PropTypes from 'prop-types';
contextTypes: {
router: React.PropTypes.object
},
class Permalink extends React.Component {
propTypes: {
href: React.PropTypes.string.isRequired,
to: React.PropTypes.string.isRequired,
children: React.PropTypes.node
},
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
if (e.button === 0) {
e.preventDefault();
this.context.router.push(this.props.to);
}
},
render () {
const { href, children, ...other } = this.props;
return <a href={href} onClick={this.handleClick} {...other}>{children}</a>;
}
});
render () {
const { href, children, className, ...other } = this.props;
return <a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>{children}</a>;
}
}
Permalink.contextTypes = {
router: PropTypes.object
};
Permalink.propTypes = {
className: PropTypes.string,
href: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
children: PropTypes.node
};
export default Permalink;

View File

@@ -1,4 +1,5 @@
import { injectIntl, FormattedRelative } from 'react-intl';
import PropTypes from 'prop-types';
const RelativeTimestamp = ({ intl, timestamp }) => {
const date = new Date(timestamp);
@@ -11,8 +12,8 @@ const RelativeTimestamp = ({ intl, timestamp }) => {
};
RelativeTimestamp.propTypes = {
intl: React.PropTypes.object.isRequired,
timestamp: React.PropTypes.string.isRequired
intl: PropTypes.object.isRequired,
timestamp: PropTypes.string.isRequired
};
export default injectIntl(RelativeTimestamp);

View File

@@ -1,7 +1,7 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import RelativeTimestamp from './relative_timestamp';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import DisplayName from './display_name';
import MediaGallery from './media_gallery';
import VideoPlayer from './video_player';
@@ -12,41 +12,25 @@ import { FormattedMessage } from 'react-intl';
import emojify from '../emoji';
import escapeTextContentForBrowser from 'escape-html';
const Status = React.createClass({
class Status extends React.PureComponent {
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map,
wrapped: React.PropTypes.bool,
onReply: React.PropTypes.func,
onFavourite: React.PropTypes.func,
onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func,
onOpenMedia: React.PropTypes.func,
onOpenVideo: React.PropTypes.func,
onBlock: React.PropTypes.func,
me: React.PropTypes.number,
boostModal: React.PropTypes.bool,
autoPlayGif: React.PropTypes.bool,
muted: React.PropTypes.bool
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
this.handleAccountClick = this.handleAccountClick.bind(this);
}
handleClick () {
const { status } = this.props;
this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
},
}
handleAccountClick (id, e) {
if (e.button === 0) {
e.preventDefault();
this.context.router.push(`/accounts/${id}`);
}
},
}
render () {
let media = '';
@@ -66,10 +50,10 @@ const Status = React.createClass({
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<div style={{ cursor: 'default' }}>
<div className='status__wrapper'>
<div className='status__prepend'>
<div style={{ position: 'absolute', 'left': '-26px'}}><i className='fa fa-fw fa-retweet' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} reblogged' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
</div>
<Status {...other} wrapped={true} status={status.get('reblog')} />
@@ -89,13 +73,13 @@ const Status = React.createClass({
return (
<div className={this.props.muted ? 'status muted' : 'status'}>
<div style={{ fontSize: '15px' }}>
<div style={{ float: 'right', fontSize: '14px' }}>
<div className='status__info'>
<div className='status__info-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
</div>
@@ -112,6 +96,26 @@ const Status = React.createClass({
);
}
});
}
Status.contextTypes = {
router: PropTypes.object
};
Status.propTypes = {
status: ImmutablePropTypes.map,
wrapped: PropTypes.bool,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
me: PropTypes.number,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool
};
export default Status;

View File

@@ -1,5 +1,5 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import { defineMessages, injectIntl } from 'react-intl';
@@ -11,73 +11,68 @@ const messages = defineMessages({
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: 'Reblog' },
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}' }
});
const StatusActionBar = React.createClass({
class StatusActionBar extends React.PureComponent {
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map.isRequired,
onReply: React.PropTypes.func,
onFavourite: React.PropTypes.func,
onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func,
onMention: React.PropTypes.func,
onMute: React.PropTypes.func,
onBlock: React.PropTypes.func,
onReport: React.PropTypes.func,
me: React.PropTypes.number.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleReplyClick = this.handleReplyClick.bind(this);
this.handleFavouriteClick = this.handleFavouriteClick.bind(this);
this.handleReblogClick = this.handleReblogClick.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
this.handleMentionClick = this.handleMentionClick.bind(this);
this.handleMuteClick = this.handleMuteClick.bind(this);
this.handleBlockClick = this.handleBlockClick.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleReport = this.handleReport.bind(this);
}
handleReplyClick () {
this.props.onReply(this.props.status, this.context.router);
},
}
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);
},
}
handleMuteClick () {
this.props.onMute(this.props.status.get('account'));
},
}
handleBlockClick () {
this.props.onBlock(this.props.status.get('account'));
},
}
handleOpen () {
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
},
}
handleReport () {
this.props.onReport(this.props.status);
this.context.router.push('/report');
},
}
render () {
const { status, me, intl } = this.props;
const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
let menu = [];
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
@@ -107,18 +102,36 @@ const StatusActionBar = React.createClass({
}
return (
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
<div className='status__action-bar'>
<div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div>
<div style={{ width: '18px', height: '18px', float: 'left' }}>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" />
<div className='status__action-bar-dropdown'>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/>
</div>
</div>
);
}
});
}
StatusActionBar.contextTypes = {
router: PropTypes.object
};
StatusActionBar.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,
me: PropTypes.number.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(StatusActionBar);

View File

@@ -1,29 +1,24 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import escapeTextContentForBrowser from 'escape-html';
import PropTypes from 'prop-types';
import emojify from '../emoji';
import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
const StatusContent = React.createClass({
class StatusContent extends React.PureComponent {
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map.isRequired,
onClick: React.PropTypes.func
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
hidden: true
};
},
mixins: [PureRenderMixin],
this.onMentionClick = this.onMentionClick.bind(this);
this.onHashtagClick = this.onHashtagClick.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this)
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleSpoilerClick = this.handleSpoilerClick.bind(this);
};
componentDidMount () {
const node = ReactDOM.findDOMNode(this);
@@ -47,14 +42,14 @@ const StatusContent = React.createClass({
link.setAttribute('title', link.href);
}
}
},
}
onMentionClick (mention, e) {
if (e.button === 0) {
e.preventDefault();
this.context.router.push(`/accounts/${mention.get('id')}`);
}
},
}
onHashtagClick (hashtag, e) {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
@@ -63,11 +58,11 @@ const StatusContent = React.createClass({
e.preventDefault();
this.context.router.push(`/timelines/tag/${hashtag}`);
}
},
}
handleMouseDown (e) {
this.startXY = [e.clientX, e.clientY];
},
}
handleMouseUp (e) {
const [ startX, startY ] = this.startXY;
@@ -82,12 +77,12 @@ const StatusContent = React.createClass({
}
this.startXY = null;
},
}
handleSpoilerClick (e) {
e.preventDefault();
this.setState({ hidden: !this.state.hidden });
},
}
render () {
const { status } = this.props;
@@ -117,7 +112,7 @@ const StatusContent = React.createClass({
}
return (
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
</p>
@@ -131,7 +126,7 @@ const StatusContent = React.createClass({
return (
<div
className='status__content'
style={{ cursor: 'pointer', ...directionStyle }}
style={{ ...directionStyle }}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
@@ -140,14 +135,23 @@ const StatusContent = React.createClass({
} else {
return (
<div
className='status__content'
className='status__content status__content--no-action'
style={{ ...directionStyle }}
dangerouslySetInnerHTML={content}
/>
);
}
},
}
});
}
StatusContent.contextTypes = {
router: PropTypes.object
};
StatusContent.propTypes = {
status: ImmutablePropTypes.map.isRequired,
onClick: PropTypes.func
};
export default StatusContent;

View File

@@ -1,32 +1,18 @@
import Status from './status';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
const StatusList = React.createClass({
class StatusList extends React.PureComponent {
propTypes: {
statusIds: ImmutablePropTypes.list.isRequired,
onScrollToBottom: React.PropTypes.func,
onScrollToTop: React.PropTypes.func,
onScroll: React.PropTypes.func,
trackScroll: React.PropTypes.bool,
isLoading: React.PropTypes.bool,
isUnread: React.PropTypes.bool,
hasMore: React.PropTypes.bool,
prepend: React.PropTypes.node,
emptyMessage: React.PropTypes.node
},
getDefaultProps () {
return {
trackScroll: true
};
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
this.setRef = this.setRef.bind(this);
this.handleLoadMore = this.handleLoadMore.bind(this);
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -40,41 +26,41 @@ const StatusList = React.createClass({
} else if (this.props.onScroll) {
this.props.onScroll();
}
},
}
componentDidMount () {
this.attachScrollListener();
},
}
componentDidUpdate (prevProps) {
if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) {
this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
}
},
}
componentWillUnmount () {
this.detachScrollListener();
},
}
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();
},
}
render () {
const { statusIds, onScrollToBottom, trackScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
let loadMore = '';
let scrollableArea = '';
@@ -93,7 +79,7 @@ const StatusList = React.createClass({
<div className='scrollable' ref={this.setRef}>
{unread}
<div>
<div className='status-list'>
{prepend}
{statusIds.map((statusId) => {
@@ -112,17 +98,31 @@ const StatusList = React.createClass({
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey='status-list'>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
}
});
}
StatusList.propTypes = {
scrollKey: PropTypes.string.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
onScrollToBottom: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node
};
StatusList.defaultProps = {
trackScroll: true
};
export default StatusList;

View File

@@ -1,5 +1,5 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
@@ -11,100 +11,31 @@ const messages = defineMessages({
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
});
const videoStyle = {
position: 'relative',
zIndex: '1',
width: '100%',
height: '100%',
objectFit: 'cover',
top: '50%',
transform: 'translateY(-50%)'
};
class VideoPlayer extends React.PureComponent {
const muteStyle = {
position: 'absolute',
top: '4px',
right: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
opacity: '0.8',
zIndex: '5'
};
const coverStyle = {
marginTop: '8px',
textAlign: 'center',
height: '100%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
position: 'relative'
};
const spoilerSpanStyle = {
display: 'block',
fontSize: '14px'
};
const spoilerSubSpanStyle = {
display: 'block',
fontSize: '11px',
fontWeight: '500'
};
const spoilerButtonStyle = {
position: 'absolute',
top: '4px',
left: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
zIndex: '100'
};
const expandButtonStyle = {
position: 'absolute',
bottom: '4px',
right: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
zIndex: '100'
};
const VideoPlayer = React.createClass({
propTypes: {
media: ImmutablePropTypes.map.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
sensitive: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired,
autoplay: React.PropTypes.bool,
onOpenVideo: React.PropTypes.func.isRequired
},
getDefaultProps () {
return {
width: 239,
height: 110
};
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
visible: !this.props.sensitive,
preview: true,
muted: true,
hasAudio: true,
videoError: false
};
},
mixins: [PureRenderMixin],
this.handleClick = this.handleClick.bind(this);
this.handleVideoClick = this.handleVideoClick.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleVisibility = this.handleVisibility.bind(this);
this.handleExpand = this.handleExpand.bind(this);
this.setRef = this.setRef.bind(this);
this.handleLoadedData = this.handleLoadedData.bind(this);
this.handleVideoError = this.handleVideoError.bind(this);
}
handleClick () {
this.setState({ muted: !this.state.muted });
},
}
handleVideoClick (e) {
e.stopPropagation();
@@ -116,37 +47,37 @@ const VideoPlayer = React.createClass({
} else {
node.pause();
}
},
}
handleOpen () {
this.setState({ preview: !this.state.preview });
},
}
handleVisibility () {
this.setState({
visible: !this.state.visible,
preview: true
});
},
}
handleExpand () {
this.video.pause();
this.props.onOpenVideo(this.props.media, this.video.currentTime);
},
}
setRef (c) {
this.video = c;
},
}
handleLoadedData () {
if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
this.setState({ hasAudio: false });
}
},
}
handleVideoError () {
this.setState({ videoError: true });
},
}
componentDidMount () {
if (!this.video) {
@@ -155,7 +86,7 @@ const VideoPlayer = React.createClass({
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
}
componentDidUpdate () {
if (!this.video) {
@@ -164,7 +95,7 @@ const VideoPlayer = React.createClass({
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
}
componentWillUnmount () {
if (!this.video) {
@@ -173,19 +104,19 @@ const VideoPlayer = React.createClass({
this.video.removeEventListener('loadeddata', this.handleLoadedData);
this.video.removeEventListener('error', this.handleVideoError);
},
}
render () {
const { media, intl, width, height, sensitive, autoplay } = this.props;
let spoilerButton = (
<div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
<div className='status__video-player-spoiler' style={{ display: !this.state.visible ? 'none' : 'block' }} >
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
</div>
);
let expandButton = (
<div style={expandButtonStyle} >
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
@@ -194,7 +125,7 @@ const VideoPlayer = React.createClass({
if (this.state.hasAudio) {
muteButton = (
<div style={muteStyle}>
<div className='status__video-player-mute'>
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
</div>
);
@@ -203,18 +134,18 @@ const VideoPlayer = React.createClass({
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
}
@@ -222,31 +153,46 @@ const VideoPlayer = React.createClass({
if (this.state.preview && !autoplay) {
return (
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
<div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center` }} onClick={this.handleOpen}>
{spoilerButton}
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
</div>
);
}
if (this.state.videoError) {
return (
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
<div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
<span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
</div>
);
}
return (
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
<div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}>
{spoilerButton}
{muteButton}
{expandButton}
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
<video className='status__video-player-video' role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} onClick={this.handleVideoClick} />
</div>
);
}
});
}
VideoPlayer.propTypes = {
media: ImmutablePropTypes.map.isRequired,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
intl: PropTypes.object.isRequired,
autoplay: PropTypes.bool,
onOpenVideo: PropTypes.func.isRequired
};
VideoPlayer.defaultProps = {
width: 239,
height: 110
};
export default injectIntl(VideoPlayer);

View File

@@ -1,4 +1,5 @@
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import {
refreshTimelineSuccess,
@@ -41,10 +42,12 @@ import Blocks from '../features/blocks';
import Mutes from '../features/mutes';
import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import ar from 'react-intl/locale-data/ar';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
import eo from 'react-intl/locale-data/eo';
import es from 'react-intl/locale-data/es';
import fa from 'react-intl/locale-data/fa';
import fi from 'react-intl/locale-data/fi';
import fr from 'react-intl/locale-data/fr';
import hu from 'react-intl/locale-data/hu';
@@ -57,7 +60,9 @@ import ru from 'react-intl/locale-data/ru';
import uk from 'react-intl/locale-data/uk';
import zh from 'react-intl/locale-data/zh';
import bg from 'react-intl/locale-data/bg';
import id from 'react-intl/locale-data/id';
import { localeData as zh_hk } from '../locales/zh-hk';
import { localeData as zh_cn } from '../locales/zh-cn';
import pt_br from '../locales/pt-br';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
@@ -73,9 +78,11 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
addLocaleData([
...en,
...ar,
...de,
...eo,
...es,
...fa,
...fi,
...fr,
...hu,
@@ -89,14 +96,131 @@ addLocaleData([
...uk,
...zh,
...zh_hk,
...zh_cn,
...bg,
...id,
]);
const Mastodon = React.createClass({
const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0];
propTypes: {
locale: React.PropTypes.string.isRequired
},
const hiddenColumnContainerStyle = {
position: 'absolute',
left: '0',
top: '0',
visibility: 'hidden'
};
class Container extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
renderedPersistents: [],
unrenderedPersistents: [],
};
}
componentWillMount () {
this.unlistenHistory = null;
this.setState(() => {
return {
mountImpersistent: false,
renderedPersistents: [],
unrenderedPersistents: [
{pathname: '/timelines/home', component: HomeTimeline},
{pathname: '/timelines/public', component: PublicTimeline},
{pathname: '/timelines/public/local', component: CommunityTimeline},
{pathname: '/notifications', component: Notifications},
{pathname: '/favourites', component: FavouritedStatuses}
],
};
}, () => {
if (this.unlistenHistory) {
return;
}
this.unlistenHistory = browserHistory.listen(location => {
const pathname = location.pathname.replace(/\/$/, '').toLowerCase();
this.setState(oldState => {
let persistentMatched = false;
const newState = {
renderedPersistents: oldState.renderedPersistents.map(persistent => {
const givenMatched = persistent.pathname === pathname;
if (givenMatched) {
persistentMatched = true;
}
return {
hidden: !givenMatched,
pathname: persistent.pathname,
component: persistent.component
};
}),
};
if (!persistentMatched) {
newState.unrenderedPersistents = [];
oldState.unrenderedPersistents.forEach(persistent => {
if (persistent.pathname === pathname) {
persistentMatched = true;
newState.renderedPersistents.push({
hidden: false,
pathname: persistent.pathname,
component: persistent.component
});
} else {
newState.unrenderedPersistents.push(persistent);
}
});
}
newState.mountImpersistent = !persistentMatched;
return newState;
});
});
});
}
componentWillUnmount () {
if (this.unlistenHistory) {
this.unlistenHistory();
}
this.unlistenHistory = "done";
}
render () {
// Hide some components rather than unmounting them to allow to show again
// quickly and keep the view state such as the scrolled offset.
const persistentsView = this.state.renderedPersistents.map((persistent) =>
<div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}>
<persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} />
</div>
);
return (
<UI>
{this.state.mountImpersistent && this.props.children}
{persistentsView}
</UI>
);
}
}
Container.propTypes = {
children: PropTypes.node,
};
class Mastodon extends React.Component {
componentDidMount() {
const { locale } = this.props;
@@ -141,14 +265,14 @@ const Mastodon = React.createClass({
}
store.dispatch(showOnboardingOnce());
},
}
componentWillUnmount () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
},
}
render () {
const { locale } = this.props;
@@ -157,18 +281,12 @@ const Mastodon = React.createClass({
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
<Provider store={store}>
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
<Route path='/' component={UI}>
<Route path='/' component={Container}>
<IndexRedirect to="/getting-started" />
<Route path='getting-started' component={GettingStarted} />
<Route path='timelines/home' component={HomeTimeline} />
<Route path='timelines/public' component={PublicTimeline} />
<Route path='timelines/public/local' component={CommunityTimeline} />
<Route path='timelines/tag/:id' component={HashtagTimeline} />
<Route path='notifications' component={Notifications} />
<Route path='favourites' component={FavouritedStatuses} />
<Route path='statuses/new' component={Compose} />
<Route path='statuses/:statusId' component={Status} />
<Route path='statuses/:statusId/reblogs' component={Reblogs} />
@@ -191,6 +309,10 @@ const Mastodon = React.createClass({
);
}
});
}
Mastodon.propTypes = {
locale: PropTypes.string.isRequired
};
export default Mastodon;

View File

@@ -20,6 +20,14 @@ import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
@@ -34,7 +42,7 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
dispatch(replyCompose(status, router));
@@ -65,7 +73,11 @@ const mapDispatchToProps = (dispatch) => ({
},
onDelete (status) {
dispatch(deleteStatus(status.get('id')));
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id')))
}));
},
onMention (account, router) {
@@ -81,7 +93,11 @@ const mapDispatchToProps = (dispatch) => ({
},
onBlock (account) {
dispatch(blockAccount(account.get('id')));
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id')))
}));
},
onReport (status) {
@@ -89,9 +105,13 @@ const mapDispatchToProps = (dispatch) => ({
},
onMute (account) {
dispatch(muteAccount(account.get('id')));
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')))
}));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(Status);
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@@ -1,5 +1,5 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import DropdownMenu from '../../../components/dropdown_menu';
import { Link } from 'react-router';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
@@ -17,31 +17,7 @@ const messages = defineMessages({
disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }
});
const outerDropdownStyle = {
padding: '10px',
flex: '1 1 auto'
};
const outerLinksStyle = {
flex: '1 1 auto',
display: 'flex',
lineHeight: '18px'
};
const ActionBar = React.createClass({
propTypes: {
account: ImmutablePropTypes.map.isRequired,
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func,
onBlock: React.PropTypes.func.isRequired,
onMention: React.PropTypes.func.isRequired,
onReport: React.PropTypes.func.isRequired,
onMute: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
class ActionBar extends React.PureComponent {
render () {
const { account, me, intl } = this.props;
@@ -76,11 +52,11 @@ const ActionBar = React.createClass({
return (
<div className='account__action-bar'>
<div style={outerDropdownStyle}>
<div className='account__action-bar-dropdown'>
<DropdownMenu items={menu} icon='bars' size={24} direction="right" />
</div>
<div style={outerLinksStyle}>
<div className='account__action-bar-links'>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
<span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
<strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong>
@@ -100,6 +76,17 @@ const ActionBar = React.createClass({
);
}
});
}
ActionBar.propTypes = {
account: ImmutablePropTypes.map.isRequired,
me: PropTypes.number.isRequired,
onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(ActionBar);

View File

@@ -1,5 +1,5 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -21,30 +21,28 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const Avatar = React.createClass({
class Avatar extends React.PureComponent {
propTypes: {
account: ImmutablePropTypes.map.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
},
constructor (props, context) {
super(props, context);
getInitialState () {
return {
this.state = {
isHovered: false
};
},
mixins: [PureRenderMixin],
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseOut = this.handleMouseOut.bind(this);
}
handleMouseOver () {
if (this.state.isHovered) return;
this.setState({ isHovered: true });
},
}
handleMouseOut () {
if (!this.state.isHovered) return;
this.setState({ isHovered: false });
},
}
render () {
const { account, autoPlayGif } = this.props;
@@ -58,7 +56,7 @@ const Avatar = React.createClass({
className='account__header__avatar'
target='_blank'
rel='noopener'
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden', backgroundSize: '90px 90px', backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
@@ -69,19 +67,14 @@ const Avatar = React.createClass({
);
}
});
}
const Header = React.createClass({
Avatar.propTypes = {
account: ImmutablePropTypes.map.isRequired,
autoPlayGif: PropTypes.bool.isRequired
};
propTypes: {
account: ImmutablePropTypes.map,
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
},
mixins: [PureRenderMixin],
class Header extends React.Component {
render () {
const { account, me, intl } = this.props;
@@ -142,6 +135,14 @@ const Header = React.createClass({
);
}
});
}
Header.propTypes = {
account: ImmutablePropTypes.map,
me: PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired
};
export default connect(makeMapStateToProps)(injectIntl(Header));

View File

@@ -1,46 +1,40 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from '../../account/components/header';
import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator';
const Header = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
class Header extends React.PureComponent {
propTypes: {
account: ImmutablePropTypes.map,
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
onBlock: React.PropTypes.func.isRequired,
onMention: React.PropTypes.func.isRequired,
onReport: React.PropTypes.func.isRequired,
onMute: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleFollow = this.handleFollow.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleMention = this.handleMention.bind(this);
this.handleReport = this.handleReport.bind(this);
this.handleMute = this.handleMute.bind(this);
}
handleFollow () {
this.props.onFollow(this.props.account);
},
}
handleBlock () {
this.props.onBlock(this.props.account);
},
}
handleMention () {
this.props.onMention(this.props.account, this.context.router);
},
}
handleReport () {
this.props.onReport(this.props.account);
this.context.router.push('/report');
},
}
handleMute() {
this.props.onMute(this.props.account);
},
}
render () {
const { account, me } = this.props;
@@ -50,7 +44,7 @@ const Header = React.createClass({
}
return (
<div>
<div className='account-timeline__header'>
<InnerHeader
account={account}
me={me}
@@ -68,6 +62,20 @@ const Header = React.createClass({
</div>
);
}
});
}
Header.propTypes = {
account: ImmutablePropTypes.map,
me: PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired
};
Header.contextTypes = {
router: PropTypes.object
};
export default Header;

View File

@@ -11,6 +11,13 @@ import {
} from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@@ -23,7 +30,7 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following'])) {
dispatch(unfollowAccount(account.get('id')));
@@ -36,7 +43,11 @@ const mapDispatchToProps = dispatch => ({
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
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')))
}));
}
},
@@ -52,9 +63,13 @@ const mapDispatchToProps = dispatch => ({
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(muteAccount(account.get('id')));
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')))
}));
}
}
});
export default connect(makeMapStateToProps, mapDispatchToProps)(Header);
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import {
fetchAccount,
fetchAccountTimeline,
@@ -20,36 +20,30 @@ const mapStateToProps = (state, props) => ({
me: state.getIn(['meta', 'me'])
});
const AccountTimeline = React.createClass({
class AccountTimeline extends React.PureComponent {
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
isLoading: React.PropTypes.bool,
hasMore: React.PropTypes.bool,
me: React.PropTypes.number.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScrollToBottom = this.handleScrollToBottom.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId)));
},
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
this.props.dispatch(fetchAccountTimeline(Number(nextProps.params.accountId)));
}
},
}
handleScrollToBottom () {
if (!this.props.isLoading && this.props.hasMore) {
this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId)));
}
},
}
render () {
const { statusIds, isLoading, hasMore, me } = this.props;
@@ -68,6 +62,7 @@ const AccountTimeline = React.createClass({
<StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
scrollKey='account_timeline'
statusIds={statusIds}
isLoading={isLoading}
hasMore={hasMore}
@@ -78,6 +73,15 @@ const AccountTimeline = React.createClass({
);
}
});
}
AccountTimeline.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
me: PropTypes.number.isRequired
};
export default connect(mapStateToProps)(AccountTimeline);

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
import Column from '../ui/components/column';
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items'])
});
const Blocks = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: React.PropTypes.object.isRequired
},
class Blocks extends React.PureComponent {
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchBlocks());
},
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -37,7 +34,7 @@ const Blocks = React.createClass({
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandBlocks());
}
},
}
render () {
const { intl, accountIds } = this.props;
@@ -63,6 +60,13 @@ const Blocks = React.createClass({
</Column>
);
}
});
}
Blocks.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired
};
export default connect(mapStateToProps)(injectIntl(Blocks));

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../ui/components/column';
import {
@@ -25,17 +25,7 @@ const mapStateToProps = state => ({
let subscription;
const CommunityTimeline = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
streamingAPIBaseURL: React.PropTypes.string.isRequired,
accessToken: React.PropTypes.string.isRequired,
hasUnread: React.PropTypes.bool
},
mixins: [PureRenderMixin],
class CommunityTimeline extends React.PureComponent {
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
@@ -72,14 +62,14 @@ const CommunityTimeline = React.createClass({
}
});
},
}
componentWillUnmount () {
// if (typeof subscription !== 'undefined') {
// subscription.close();
// subscription = null;
// }
},
}
render () {
const { intl, hasUnread } = this.props;
@@ -87,11 +77,19 @@ const CommunityTimeline = React.createClass({
return (
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
<ColumnBackButtonSlim />
<StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
<StatusListContainer {...this.props} scrollKey='community_timeline' type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
</Column>
);
},
}
});
}
CommunityTimeline.propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool
};
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));

View File

@@ -3,8 +3,8 @@ import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
const AutosuggestAccount = ({ account }) => (
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
<div className='autosuggest-account'>
<div className='autosuggest-account-icon'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
<DisplayName account={account} />
</div>
);

View File

@@ -3,7 +3,7 @@ import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
const AutosuggestStatus = ({ status }) => (
<div style={{ overflow: 'hidden' }} className='autosuggest-status'>
<div className='autosuggest-status'>
<FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} />
</div>
);

View File

@@ -1,27 +1,26 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import { length } from 'stringz';
const CharacterCounter = React.createClass({
propTypes: {
text: React.PropTypes.string.isRequired,
max: React.PropTypes.number.isRequired
},
mixins: [PureRenderMixin],
class CharacterCounter extends React.PureComponent {
checkRemainingText (diff) {
if (diff <= 0) {
return <span style={{ fontSize: '16px', cursor: 'default', color: '#ff5050' }}>{diff}</span>;
return <span className='character-counter character-counter--over'>{diff}</span>;
}
return <span style={{ fontSize: '16px', cursor: 'default' }}>{diff}</span>;
},
return <span className='character-counter'>{diff}</span>;
}
render () {
const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length;
const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff);
}
});
}
CharacterCounter.propTypes = {
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired
}
export default CharacterCounter;

View File

@@ -1,7 +1,7 @@
import CharacterCounter from './character_counter';
import Button from '../../../components/button';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { debounce } from 'react-decoration';
@@ -15,6 +15,7 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
import EmojiPickerDropdown from './emoji_picker_dropdown';
import UploadFormContainer from '../containers/upload_form_container';
import TextIconButton from './text_icon_button';
import WarningContainer from '../containers/warning_container';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -22,66 +23,53 @@ const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
});
const ComposeForm = React.createClass({
class ComposeForm extends React.PureComponent {
propTypes: {
intl: React.PropTypes.object.isRequired,
text: React.PropTypes.string.isRequired,
suggestion_token: React.PropTypes.string,
suggestions: ImmutablePropTypes.list,
spoiler: React.PropTypes.bool,
privacy: React.PropTypes.string,
spoiler_text: React.PropTypes.string,
focusDate: React.PropTypes.instanceOf(Date),
preselectDate: React.PropTypes.instanceOf(Date),
is_submitting: React.PropTypes.bool,
is_uploading: React.PropTypes.bool,
me: React.PropTypes.number,
needsPrivacyWarning: React.PropTypes.bool,
mentionedDomains: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired,
onClearSuggestions: React.PropTypes.func.isRequired,
onFetchSuggestions: React.PropTypes.func.isRequired,
onSuggestionSelected: React.PropTypes.func.isRequired,
onChangeSpoilerText: React.PropTypes.func.isRequired,
onPaste: React.PropTypes.func.isRequired,
onPickEmoji: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this);
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this);
this.onSuggestionSelected = this.onSuggestionSelected.bind(this);
this.handleChangeSpoilerText = this.handleChangeSpoilerText.bind(this);
this.setAutosuggestTextarea = this.setAutosuggestTextarea.bind(this);
this.handleEmojiPick = this.handleEmojiPick.bind(this);
}
handleChange (e) {
this.props.onChange(e.target.value);
},
}
handleKeyDown (e) {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.props.onSubmit();
}
},
}
handleSubmit () {
this.autosuggestTextarea.textarea.style.height = "auto";
this.props.onSubmit();
},
}
onSuggestionsClearRequested () {
this.props.onClearSuggestions();
},
}
@debounce(500)
onSuggestionsFetchRequested (token) {
this.props.onFetchSuggestions(token);
},
}
onSuggestionSelected (tokenStart, token, value) {
this._restoreCaret = null;
this.props.onSuggestionSelected(tokenStart, token, value);
},
}
handleChangeSpoilerText (e) {
this.props.onChangeSpoilerText(e.target.value);
},
}
componentWillReceiveProps (nextProps) {
// If this is the update where we've finished uploading,
@@ -89,7 +77,7 @@ const ComposeForm = React.createClass({
if (!nextProps.is_uploading && this.props.is_uploading) {
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
}
},
}
componentDidUpdate (prevProps) {
// This statement does several things:
@@ -116,58 +104,45 @@ const ComposeForm = React.createClass({
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus();
}
},
}
setAutosuggestTextarea (c) {
this.autosuggestTextarea = c;
},
}
handleEmojiPick (data) {
const position = this.autosuggestTextarea.textarea.selectionStart;
this._restoreCaret = position + data.shortname.length + 1;
this.props.onPickEmoji(position, data);
},
}
render () {
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
const { intl, onPaste } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, this.props.text].join('');
let publishText = '';
let privacyWarning = '';
let reply_to_other = false;
if (needsPrivacyWarning) {
privacyWarning = (
<div className='compose-form__warning'>
<FormattedMessage
id='compose_form.privacy_disclaimer'
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
/>
</div>
);
}
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== 'unlisted' ? '!' : '');
}
return (
<div style={{ padding: '10px' }}>
<div className='compose-form'>
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className="spoiler-input">
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" />
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" id='cw-spoiler-input'/>
</div>
</Collapsable>
{privacyWarning}
<WarningContainer />
<ReplyIndicatorContainer />
<div style={{ position: 'relative' }}>
<div className='compose-form__autosuggest-wrapper'>
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
@@ -189,7 +164,7 @@ const ComposeForm = React.createClass({
<UploadFormContainer />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PrivacyDropdownContainer />
@@ -197,15 +172,38 @@ const ComposeForm = React.createClass({
<SpoilerButtonContainer />
</div>
<div style={{ display: 'flex', minWidth: 0 }}>
<div style={{ paddingTop: '10px', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={text} /></div>
<div style={{ paddingTop: '10px', overflow: 'hidden' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500} block /></div>
<div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500 || (text.length !==0 && text.trim().length === 0)} block /></div>
</div>
</div>
</div>
);
}
});
}
ComposeForm.propTypes = {
intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired,
suggestion_token: PropTypes.string,
suggestions: ImmutablePropTypes.list,
spoiler: PropTypes.bool,
privacy: PropTypes.string,
spoiler_text: PropTypes.string,
focusDate: PropTypes.instanceOf(Date),
preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool,
me: PropTypes.number,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
onPickEmoji: PropTypes.func.isRequired
};
export default injectIntl(ComposeForm);

View File

@@ -1,10 +1,19 @@
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import EmojiPicker from 'emojione-picker';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
people: { id: 'emoji_button.people', defaultMessage: 'People' },
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }
});
const settings = {
@@ -13,46 +22,93 @@ const settings = {
imagePathPNG: '/emoji/'
};
const style = {
const dropdownStyle = {
position: 'absolute',
right: '5px',
top: '5px'
};
const EmojiPickerDropdown = React.createClass({
const dropdownTriggerStyle = {
display: 'block',
fontSize: '24px',
lineHeight: '24px',
marginLeft: '2px',
width: '24px'
}
propTypes: {
intl: React.PropTypes.object.isRequired,
onPickEmoji: React.PropTypes.func.isRequired
},
class EmojiPickerDropdown extends React.PureComponent {
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.setRef = this.setRef.bind(this);
this.handleChange = this.handleChange.bind(this);
}
setRef (c) {
this.dropdown = c;
},
}
handleChange (data) {
this.dropdown.hide();
this.props.onPickEmoji(data);
},
}
render () {
const { intl } = this.props;
const categories = {
people: {
title: intl.formatMessage(messages.people),
emoji: 'smile',
},
nature: {
title: intl.formatMessage(messages.nature),
emoji: 'hamster',
},
food: {
title: intl.formatMessage(messages.food),
emoji: 'pizza',
},
activity: {
title: intl.formatMessage(messages.activity),
emoji: 'soccer',
},
travel: {
title: intl.formatMessage(messages.travel),
emoji: 'earth_americas',
},
objects: {
title: intl.formatMessage(messages.objects),
emoji: 'bulb',
},
symbols: {
title: intl.formatMessage(messages.symbols),
emoji: 'clock9',
},
flags: {
title: intl.formatMessage(messages.flags),
emoji: 'flag_gb',
}
}
return (
<Dropdown ref={this.setRef} style={style}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
<Dropdown ref={this.setRef} style={dropdownStyle}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={dropdownTriggerStyle}>
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
</DropdownTrigger>
<DropdownContent className='dropdown__left light'>
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search={true} />
</DropdownContent>
</Dropdown>
);
}
});
}
EmojiPickerDropdown.propTypes = {
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired
};
export default injectIntl(EmojiPickerDropdown);

View File

@@ -1,4 +1,3 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
@@ -7,26 +6,27 @@ import Permalink from '../../../components/permalink';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
const NavigationBar = React.createClass({
propTypes: {
account: ImmutablePropTypes.map.isRequired
},
mixins: [PureRenderMixin],
class NavigationBar extends React.PureComponent {
render () {
return (
<div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
<a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
<div className='navigation-bar__profile'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
</Permalink>
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
</div>
</div>
);
}
});
}
NavigationBar.propTypes = {
account: ImmutablePropTypes.map.isRequired
};
export default NavigationBar;

View File

@@ -1,4 +1,4 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
@@ -7,7 +7,7 @@ const messages = defineMessages({
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
@@ -15,55 +15,52 @@ const messages = defineMessages({
});
const iconStyle = {
lineHeight: '27px',
height: null
};
height: null,
lineHeight: '27px'
}
const PrivacyDropdown = React.createClass({
class PrivacyDropdown extends React.PureComponent {
propTypes: {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
getInitialState () {
return {
constructor (props, context) {
super(props, context);
this.state = {
open: false
};
},
mixins: [PureRenderMixin],
this.handleToggle = this.handleToggle.bind(this);
this.handleClick = this.handleClick.bind(this);
this.onGlobalClick = this.onGlobalClick.bind(this);
this.setRef = this.setRef.bind(this);
}
handleToggle () {
this.setState({ open: !this.state.open });
},
}
handleClick (value, e) {
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
},
}
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);
},
}
setRef (c) {
this.node = c;
},
}
render () {
const { value, onChange, intl } = this.props;
@@ -80,7 +77,7 @@ const PrivacyDropdown = React.createClass({
return (
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div>
<div className='privacy-dropdown__dropdown'>
{options.map(item =>
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
@@ -96,6 +93,12 @@ const PrivacyDropdown = React.createClass({
);
}
});
}
PrivacyDropdown.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(PrivacyDropdown);

View File

@@ -1,5 +1,5 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
@@ -10,30 +10,24 @@ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }
});
const ReplyIndicator = React.createClass({
class ReplyIndicator extends React.PureComponent {
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map,
onCancel: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
this.handleAccountClick = this.handleAccountClick.bind(this);
}
handleClick () {
this.props.onCancel();
},
}
handleAccountClick (e) {
if (e.button === 0) {
e.preventDefault();
this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
},
}
render () {
const { status, intl } = this.props;
@@ -46,11 +40,11 @@ const ReplyIndicator = React.createClass({
return (
<div className='reply-indicator'>
<div style={{ overflow: 'hidden', marginBottom: '5px' }}>
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
<div className='reply-indicator__header'>
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
<div className='reply-indicator__display-avatar'><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
<DisplayName account={status.get('account')} />
</a>
</div>
@@ -60,6 +54,16 @@ const ReplyIndicator = React.createClass({
);
}
});
}
ReplyIndicator.contextTypes = {
router: PropTypes.object
};
ReplyIndicator.propTypes = {
status: ImmutablePropTypes.map,
onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(ReplyIndicator);

View File

@@ -1,48 +1,47 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
});
const Search = React.createClass({
class Search extends React.PureComponent {
propTypes: {
value: React.PropTypes.string.isRequired,
submitted: React.PropTypes.bool,
onChange: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired,
onClear: React.PropTypes.func.isRequired,
onShow: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleClear = this.handleClear.bind(this);
}
handleChange (e) {
this.props.onChange(e.target.value);
},
}
handleClear (e) {
e.preventDefault();
this.props.onClear();
},
if (this.props.value.length > 0 || this.props.submitted) {
this.props.onClear();
}
}
handleKeyDown (e) {
if (e.key === 'Enter') {
e.preventDefault();
this.props.onSubmit();
}
},
}
noop () {
},
}
handleFocus () {
this.props.onShow();
},
}
render () {
const { intl, value, submitted } = this.props;
@@ -60,14 +59,24 @@ const Search = React.createClass({
onFocus={this.handleFocus}
/>
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
<i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
</div>
</div>
);
}
});
}
Search.propTypes = {
value: PropTypes.string.isRequired,
submitted: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(Search);

View File

@@ -1,17 +1,10 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import { Link } from 'react-router';
const SearchResults = React.createClass({
propTypes: {
results: ImmutablePropTypes.map.isRequired
},
mixins: [PureRenderMixin],
class SearchResults extends React.PureComponent {
render () {
const { results } = this.props;
@@ -63,6 +56,10 @@ const SearchResults = React.createClass({
);
}
});
}
SearchResults.propTypes = {
results: ImmutablePropTypes.map.isRequired
};
export default SearchResults;

View File

@@ -1,31 +1,35 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
const TextIconButton = React.createClass({
class TextIconButton extends React.PureComponent {
propTypes: {
label: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
active: React.PropTypes.bool,
onClick: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick (e) {
e.preventDefault();
this.props.onClick();
},
}
render () {
const { label, title, active } = this.props;
const { label, title, active, ariaControls } = this.props;
return (
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} onClick={this.handleClick}>
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>
{label}
</button>
);
}
});
}
TextIconButton.propTypes = {
label: PropTypes.string.isRequired,
title: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
ariaControls: PropTypes.string
};
export default TextIconButton;

View File

@@ -1,53 +1,60 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media' }
});
const iconStyle = {
lineHeight: '27px',
height: null
};
height: null,
lineHeight: '27px'
}
const UploadButton = React.createClass({
class UploadButton extends React.PureComponent {
propTypes: {
disabled: React.PropTypes.bool,
onSelectFile: React.PropTypes.func.isRequired,
style: React.PropTypes.object,
resetFileKey: React.PropTypes.number,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
this.setRef = this.setRef.bind(this);
}
handleChange (e) {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
},
}
handleClick () {
this.fileElement.click();
},
}
setRef (c) {
this.fileElement = c;
},
}
render () {
const { intl, resetFileKey, disabled } = this.props;
return (
<div style={this.props.style}>
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} style={iconStyle} size={18} inverted />
<div className='compose-form__upload-button'>
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle}/>
<input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
</div>
);
}
});
}
UploadButton.propTypes = {
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
style: PropTypes.object,
resetFileKey: PropTypes.number,
intl: PropTypes.object.isRequired
};
export default injectIntl(UploadButton);

View File

@@ -1,5 +1,5 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import UploadProgressContainer from '../containers/upload_progress_container';
@@ -9,24 +9,16 @@ const messages = defineMessages({
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }
});
const UploadForm = React.createClass({
propTypes: {
media: ImmutablePropTypes.list.isRequired,
onRemoveFile: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
class UploadForm extends React.PureComponent {
render () {
const { intl, media } = this.props;
const uploads = media.map(attachment =>
<div key={attachment.get('id')} style={{ margin: '5px', flex: '1 1 0' }}>
<div className='compose-form__upload' key={attachment.get('id')}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) =>
<div style={{ transform: `translateZ(0) scale(${scale})`, width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, background: `url(${attachment.get('preview_url')}) no-repeat center` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
</div>
}
@@ -35,13 +27,19 @@ const UploadForm = React.createClass({
);
return (
<div style={{ overflow: 'hidden' }}>
<div className='compose-form__upload-wrapper'>
<UploadProgressContainer />
<div style={{ display: 'flex', padding: '5px' }}>{uploads}</div>
<div className='compose-form__uploads-wrapper'>{uploads}</div>
</div>
);
}
});
}
UploadForm.propTypes = {
media: ImmutablePropTypes.list.isRequired,
onRemoveFile: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(UploadForm);

View File

@@ -1,15 +1,8 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import { Motion, spring } from 'react-motion';
import { FormattedMessage } from 'react-intl';
const UploadProgress = React.createClass({
propTypes: {
active: React.PropTypes.bool,
progress: React.PropTypes.number
},
mixins: [PureRenderMixin],
class UploadProgress extends React.PureComponent {
render () {
const { active, progress } = this.props;
@@ -20,11 +13,11 @@ const UploadProgress = React.createClass({
return (
<div className='upload-progress'>
<div>
<div className='upload-progress__icon'>
<i className='fa fa-upload' />
</div>
<div style={{ flex: '1 1 auto' }}>
<div className='upload-progress__message'>
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
<div className='upload-progress__backdrop'>
@@ -39,6 +32,11 @@ const UploadProgress = React.createClass({
);
}
});
}
UploadProgress.propTypes = {
active: PropTypes.bool,
progress: PropTypes.number
};
export default UploadProgress;

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
class Warning extends React.PureComponent {
constructor (props) {
super(props);
}
render () {
const { message } = this.props;
return (
<div className='compose-form__warning'>
{message}
</div>
);
}
}
Warning.propTypes = {
message: PropTypes.node.isRequired
};
export default Warning;

View File

@@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form';
import { uploadCompose } from '../../../actions/compose';
import { createSelector } from 'reselect';
import {
changeCompose,
submitCompose,
@@ -12,33 +11,20 @@ import {
insertEmojiCompose
} from '../../../actions/compose';
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']),
suggestion_token: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoiler_text: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
me: state.getIn(['compose', 'me'])
});
const mapStateToProps = (state, props) => {
const mentionedUsernames = getMentionedUsernames(state);
const mentionedUsernamesWithDomains = getMentionedDomains(state);
return {
text: state.getIn(['compose', 'text']),
suggestion_token: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoiler_text: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
me: state.getIn(['compose', 'me']),
needsPrivacyWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
mentionedDomains: mentionedUsernamesWithDomains
};
};
const mapDispatchToProps = (dispatch) => ({
onChange (text) {

View File

@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import TextIconButton from '../components/text_icon_button';
import { changeComposeSensitivity } from '../../../actions/compose';
import { Motion, spring } from 'react-motion';
@@ -21,14 +22,7 @@ const mapDispatchToProps = dispatch => ({
});
const SensitiveButton = React.createClass({
propTypes: {
visible: React.PropTypes.bool,
active: React.PropTypes.bool,
onClick: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
class SensitiveButton extends React.PureComponent {
render () {
const { visible, active, onClick, intl } = this.props;
@@ -44,6 +38,13 @@ const SensitiveButton = React.createClass({
);
}
});
}
SensitiveButton.propTypes = {
visible: PropTypes.bool,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));

View File

@@ -4,13 +4,14 @@ import { changeComposeSpoilerness } from '../../../actions/compose';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind content warning' }
title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind warning' }
});
const mapStateToProps = (state, { intl }) => ({
label: 'CW',
title: intl.formatMessage(messages.title),
active: state.getIn(['compose', 'spoiler'])
active: state.getIn(['compose', 'spoiler']),
ariaControls: 'cw-spoiler-input'
});
const mapDispatchToProps = dispatch => ({

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import Warning from '../components/warning';
import { createSelector } from 'reselect';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
});
const mapStateToProps = state => {
const mentionedUsernames = getMentionedUsernames(state);
const mentionedUsernamesWithDomains = getMentionedDomains(state);
return {
needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
mentionedDomains: mentionedUsernamesWithDomains,
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked'])
};
};
const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => {
if (needsLockWarning) {
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
} else if (needsLeakWarning) {
return (
<Warning
message={<FormattedMessage
id='compose_form.privacy_disclaimer'
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.'
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
/>}
/>
);
}
return null;
};
WarningWrapper.propTypes = {
needsLeakWarning: PropTypes.bool,
needsLockWarning: PropTypes.bool,
mentionedDomains: PropTypes.array.isRequired,
};
export default connect(mapStateToProps)(WarningWrapper);

View File

@@ -1,7 +1,7 @@
import ComposeFormContainer from './containers/compose_form_container';
import UploadFormContainer from './containers/upload_form_container';
import NavigationContainer from './containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { mountCompose, unmountCompose } from '../../actions/compose';
import { Link } from 'react-router';
@@ -22,24 +22,15 @@ const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
});
const Compose = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
withHeader: React.PropTypes.bool,
showSearch: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
class Compose extends React.PureComponent {
componentDidMount () {
this.props.dispatch(mountCompose());
},
}
componentWillUnmount () {
this.props.dispatch(unmountCompose());
},
}
render () {
const { withHeader, showSearch, intl } = this.props;
@@ -49,11 +40,11 @@ const Compose = React.createClass({
if (withHeader) {
header = (
<div className='drawer__header'>
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role="img" aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role="img" aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role="img" aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role="img" aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role="img" aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
</div>
);
}
@@ -82,6 +73,13 @@ const Compose = React.createClass({
);
}
});
}
Compose.propTypes = {
dispatch: PropTypes.func.isRequired,
withHeader: PropTypes.bool,
showSearch: PropTypes.bool,
intl: PropTypes.object.isRequired
};
export default connect(mapStateToProps)(injectIntl(Compose));

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
@@ -18,26 +18,20 @@ const mapStateToProps = state => ({
me: state.getIn(['meta', 'me'])
});
const Favourites = React.createClass({
class Favourites extends React.PureComponent {
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
loaded: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired,
me: React.PropTypes.number.isRequired
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScrollToBottom = this.handleScrollToBottom.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
},
}
handleScrollToBottom () {
this.props.dispatch(expandFavouritedStatuses());
},
}
render () {
const { statusIds, loaded, intl, me } = this.props;
@@ -53,11 +47,20 @@ const Favourites = React.createClass({
return (
<Column icon='star' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
<StatusList {...this.props} onScrollToBottom={this.handleScrollToBottom} />
</Column>
);
}
});
}
Favourites.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
loaded: PropTypes.bool,
intl: PropTypes.object.isRequired,
me: PropTypes.number.isRequired
};
export default connect(mapStateToProps)(injectIntl(Favourites));

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
@@ -12,25 +12,17 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)])
});
const Favourites = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
},
mixins: [PureRenderMixin],
class Favourites extends React.PureComponent {
componentWillMount () {
this.props.dispatch(fetchFavourites(Number(this.props.params.statusId)));
},
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId)));
}
},
}
render () {
const { accountIds } = this.props;
@@ -56,6 +48,12 @@ const Favourites = React.createClass({
);
}
});
}
Favourites.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
};
export default connect(mapStateToProps)(Favourites);

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from '../../../components/permalink';
import Avatar from '../../../components/avatar';
@@ -11,38 +12,23 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }
});
const outerStyle = {
padding: '14px 10px'
};
const panelStyle = {
display: 'flex',
flexDirection: 'row',
padding: '10px 0'
};
const btnStyle = {
flex: '1 1 auto',
textAlign: 'center'
};
const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
const content = { __html: emojify(account.get('note')) };
return (
<div>
<div style={outerStyle}>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
<div className='account-authorize__wrapper'>
<div className='account-authorize'>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'>
<div className='account-authorize__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
<DisplayName account={account} />
</Permalink>
<div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
<div className='account__header__content' dangerouslySetInnerHTML={content} />
</div>
<div className='account--panel' style={panelStyle}>
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
<div className='account--panel'>
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
</div>
</div>
)
@@ -50,9 +36,9 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
AccountAuthorize.propTypes = {
account: ImmutablePropTypes.map.isRequired,
onAuthorize: React.PropTypes.func.isRequired,
onReject: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(AccountAuthorize);

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items'])
});
const FollowRequests = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: React.PropTypes.object.isRequired
},
class FollowRequests extends React.PureComponent {
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchFollowRequests());
},
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -37,7 +34,7 @@ const FollowRequests = React.createClass({
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandFollowRequests());
}
},
}
render () {
const { intl, accountIds } = this.props;
@@ -63,6 +60,13 @@ const FollowRequests = React.createClass({
</Column>
);
}
});
}
FollowRequests.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired
};
export default connect(mapStateToProps)(injectIntl(FollowRequests));

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import {
@@ -18,27 +18,25 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items'])
});
const Followers = React.createClass({
class Followers extends React.PureComponent {
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
this.handleLoadMore = this.handleLoadMore.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
this.props.dispatch(fetchFollowers(Number(this.props.params.accountId)));
},
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId)));
}
},
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -46,12 +44,12 @@ const Followers = React.createClass({
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
}
},
}
handleLoadMore (e) {
e.preventDefault();
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
},
}
render () {
const { accountIds } = this.props;
@@ -70,7 +68,7 @@ const Followers = React.createClass({
<ScrollContainer scrollKey='followers'>
<div className='scrollable' onScroll={this.handleScroll}>
<div>
<div className='followers'>
<HeaderContainer accountId={this.props.params.accountId} />
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
<LoadMore onClick={this.handleLoadMore} />
@@ -81,6 +79,12 @@ const Followers = React.createClass({
);
}
});
}
Followers.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
};
export default connect(mapStateToProps)(Followers);

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import {
@@ -18,27 +18,25 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items'])
});
const Following = React.createClass({
class Following extends React.PureComponent {
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
},
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
this.handleLoadMore = this.handleLoadMore.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
this.props.dispatch(fetchFollowing(Number(this.props.params.accountId)));
},
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId)));
}
},
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -46,12 +44,12 @@ const Following = React.createClass({
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
}
},
}
handleLoadMore (e) {
e.preventDefault();
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
},
}
render () {
const { accountIds } = this.props;
@@ -70,7 +68,7 @@ const Following = React.createClass({
<ScrollContainer scrollKey='following'>
<div className='scrollable' onScroll={this.handleScroll}>
<div>
<div className='following'>
<HeaderContainer accountId={this.props.params.accountId} />
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
<LoadMore onClick={this.handleLoadMore} />
@@ -81,6 +79,12 @@ const Following = React.createClass({
);
}
});
}
Following.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list
};
export default connect(mapStateToProps)(Following);

View File

@@ -1,13 +1,17 @@
import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { Link } from 'react-router';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation'},
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings'},
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@@ -30,16 +34,18 @@ const GettingStarted = ({ intl, me }) => {
}
return (
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
<div style={{ position: 'relative' }}>
<ColumnLink icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
<ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile={true}>
<div className='getting-started__wrapper'>
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)}/>
<ColumnLink icon='users' hideOnMobile={true} text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
<ColumnLink icon='globe' hideOnMobile={true} text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
{followRequests}
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)}/>
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>
@@ -53,7 +59,7 @@ const GettingStarted = ({ intl, me }) => {
};
GettingStarted.propTypes = {
intl: React.PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
me: ImmutablePropTypes.map.isRequired
};

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../ui/components/column';
import {
@@ -17,17 +17,7 @@ const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token'])
});
const HashtagTimeline = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
streamingAPIBaseURL: React.PropTypes.string.isRequired,
accessToken: React.PropTypes.string.isRequired,
hasUnread: React.PropTypes.bool
},
mixins: [PureRenderMixin],
class HashtagTimeline extends React.PureComponent {
_subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props;
@@ -46,14 +36,14 @@ const HashtagTimeline = React.createClass({
}
});
},
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
},
}
componentDidMount () {
const { dispatch } = this.props;
@@ -61,7 +51,7 @@ const HashtagTimeline = React.createClass({
dispatch(refreshTimeline('tag', id));
this._subscribe(dispatch, id);
},
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) {
@@ -69,11 +59,11 @@ const HashtagTimeline = React.createClass({
this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.params.id);
}
},
}
componentWillUnmount () {
this._unsubscribe();
},
}
render () {
const { id, hasUnread } = this.props.params;
@@ -81,11 +71,19 @@ const HashtagTimeline = React.createClass({
return (
<Column icon='hashtag' active={hasUnread} heading={id}>
<ColumnBackButtonSlim />
<StatusListContainer type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} />
<StatusListContainer scrollKey='hashtag_timeline' type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} />
</Column>
);
},
}
});
}
HashtagTimeline.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool
};
export default connect(mapStateToProps)(HashtagTimeline);

View File

@@ -1,4 +1,4 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable';
@@ -10,51 +10,27 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
});
const outerStyle = {
padding: '15px'
};
const sectionStyle = {
cursor: 'default',
display: 'block',
fontWeight: '500',
marginBottom: '10px'
};
const rowStyle = {
};
const ColumnSettings = React.createClass({
propTypes: {
settings: ImmutablePropTypes.map.isRequired,
onChange: React.PropTypes.func.isRequired,
onSave: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
class ColumnSettings extends React.PureComponent {
render () {
const { settings, onChange, onSave, intl } = this.props;
return (
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
<div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div className='column-settings__outer'>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div style={rowStyle}>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
</div>
<div style={rowStyle}>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
</div>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<div style={rowStyle}>
<div className='column-settings__row'>
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div>
</div>
@@ -62,6 +38,13 @@ const ColumnSettings = React.createClass({
);
}
});
}
ColumnSettings.propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
}
export default injectIntl(ColumnSettings);

View File

@@ -1,33 +1,22 @@
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
const style = {
display: 'block',
fontFamily: 'inherit',
marginBottom: '10px',
padding: '7px 0',
boxSizing: 'border-box',
width: '100%'
};
class SettingText extends React.PureComponent {
const SettingText = React.createClass({
propTypes: {
settings: ImmutablePropTypes.map.isRequired,
settingKey: React.PropTypes.array.isRequired,
label: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired
},
constructor (props, context) {
super(props, context);
this.handleChange = this.handleChange.bind(this);
}
handleChange (e) {
this.props.onChange(this.props.settingKey, e.target.value)
},
}
render () {
const { settings, settingKey, label } = this.props;
return (
<input
style={style}
className='setting-text'
value={settings.getIn(settingKey)}
onChange={this.handleChange}
@@ -36,6 +25,13 @@ const SettingText = React.createClass({
);
}
});
}
SettingText.propTypes = {
settings: ImmutablePropTypes.map.isRequired,
settingKey: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
};
export default SettingText;

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../ui/components/column';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -14,14 +14,7 @@ const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0
});
const HomeTimeline = React.createClass({
propTypes: {
intl: React.PropTypes.object.isRequired,
hasUnread: React.PropTypes.bool
},
mixins: [PureRenderMixin],
class HomeTimeline extends React.PureComponent {
render () {
const { intl, hasUnread } = this.props;
@@ -29,11 +22,16 @@ const HomeTimeline = React.createClass({
return (
<Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}>
<ColumnSettingsContainer />
<StatusListContainer {...this.props} type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
<StatusListContainer {...this.props} scrollKey='home_timeline' type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
</Column>
);
},
}
});
}
HomeTimeline.propTypes = {
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool
};
export default connect(mapStateToProps)(injectIntl(HomeTimeline));

View File

@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
});
const Mutes = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: React.PropTypes.object.isRequired
},
class Mutes extends React.PureComponent {
mixins: [PureRenderMixin],
constructor (props, context) {
super(props, context);
this.handleScroll = this.handleScroll.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchMutes());
},
}
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -37,7 +34,7 @@ const Mutes = React.createClass({
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandMutes());
}
},
}
render () {
const { intl, accountIds } = this.props;
@@ -54,7 +51,7 @@ const Mutes = React.createClass({
<Column icon='volume-off' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollContainer scrollKey='mutes'>
<div className='scrollable' onScroll={this.handleScroll}>
<div className='scrollable mutes' onScroll={this.handleScroll}>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />
)}
@@ -63,6 +60,14 @@ const Mutes = React.createClass({
</Column>
);
}
});
}
Mutes.propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired
};
export default connect(mapStateToProps)(injectIntl(Mutes));

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