Compare commits

...

371 Commits

Author SHA1 Message Date
Lynx Kotoura
4c14ff659b Oauth code in input form and add description message (#4986)
* Oauth code in a input form and add description

* New authcode description
2017-09-17 15:23:44 +02:00
unarist
dd6f9a1b82 Validate uri presence for remote status (#4985) 2017-09-17 15:21:57 +02:00
unarist
3f07f1b2b1 Raise an error on getting activity uri for remote status (#4984)
We had returned `nil` for that case, but this raises an error instead, as a wrong usage of the method.
This method is currently only used in ActivitySerializer.
2017-09-17 13:51:34 +02:00
abcang
44245926f1 Fix cancellation of scroll to the right (#4978) 2017-09-17 11:59:23 +02:00
Patrick Figel
8811778b55 Randomize sidekiq-scheduler cron schedule (#4980)
SubscriptionsScheduler in particular causes high load across the
entire fediverse at 5 AM UTC every day. Randomizing cron schedules
and/or adding a random delay is considered best practice to avoid
this issue.
2017-09-17 11:58:20 +02:00
unarist
1eab53ee10 Fix an error when actor json couldn't be fetched in ResolveRemoteAccountService (#4979)
* Fix an error when actor json couldn't be fetched in ResolveRemoteAccountService

* Add specs
2017-09-17 11:54:23 +02:00
BruWalfas
7be3131240 So Spanish. Much changes. Wow. (#4976) 2017-09-17 18:16:43 +09:00
Akihiko Odaki
198a9a4fa4 Remove local_only scope in Status (#4977) 2017-09-17 05:29:43 +02:00
unarist
ec36df97c4 Escape URL parts on formatting local status (#4975) 2017-09-16 21:33:52 +02:00
Mingye Wang
c8969dca35 Minor Chinese check & jsx addition (#4973)
* zh-*: transition from "like" back to "fav"

This commit reverts the translation for the yellow-star "fav" button
back to "fav" in Chinese. Some ambuiguity between "like" and "fav" is
deliberately used in zh-TW/HK by using the existing phrase "最爱"
(favorite (adj.), lit. love-most) instead of "收藏" (favourite (v.),
"collect") in some instances.

Fixes #3511.

* zh-*: apply suggestions for PR #4557

* zh-cn: de-monetize ya account

In Chinese two separate characters, 账 and 帐, can be used to spell the
word for account (账/帐户). However, the one with a 贝 on the left is
evolved from the latter specifically for monetary purposes. Since
people usually can't figure out which one to use, it might be a good
idea to use the original not-so-money one.

* zh-*: complete jsx translation
2017-09-16 18:48:38 +02:00
Lynx Kotoura
1e3b1d7211 Adjust landing pages 2 (#4967)
* Adjust landing pages 2

Fix styles of terms page
Remove action buttons from timeline in about page
Adjust styles of short description
Adjust form inputs
Set autocomplete off for username and email box in registration form. Remove line breakings.

* Revert removing action buttons
2017-09-16 18:39:11 +02:00
unarist
0698c610a6 Fix an error in ReplyDistributionWorker when replied status was deleted (#4974)
Reply distribution is proceed by Sidekiq, so replied status may be deleted before this.
2017-09-16 18:18:00 +02:00
ふぁぼ原
629fae8b3b correct URL pattern used in text length counter in WebUI (#4968) 2017-09-16 15:01:15 +02:00
unarist
c30e6433de Fix AP serialization error when thread is missing (#4970)
`Status#reply?` may returns true even if the thread is missing.
e.g. the replied status was deleted or couldn't be fetched.

Then it raises NoMethodError on various AP json serialization.

This issue won't happen on Atom serialization because it checks thread
existence using `StreamEntry#threaded?` instead.
2017-09-16 15:00:36 +02:00
Naoki Kosaka
cea5597722 Fix hasSize condition in secSet and sizes. (#4969) 2017-09-16 15:00:01 +02:00
Akihiko Odaki
48d77ea1eb Fix filterable_languages method of SettingsHelper (#4966) 2017-09-16 14:59:41 +02:00
Eugen Rochko
efec507230 Bump to 1.6.1 2017-09-16 03:08:29 +02:00
Eugen Rochko
54edb4b853 When accessing uncached media attachment, redownload it (#4955)
* When accessing uncached media attachment, redownload it

* Prevent re-download of rejected media
2017-09-16 03:01:45 +02:00
unarist
6c81f9d6e5 Fix invisible load more button (#4962)
* Fix behavior while the button is invisible
e.g. pointer cursor, couldn't open contextmenu
* Avoid rendering the button to remove blank space if no more items are available or no items are rendered
2017-09-16 00:32:43 +02:00
Eugen Rochko
472df24579 When web UI URL used while logged out, redirect to static page (#4954) 2017-09-15 00:57:08 +02:00
Eugen Rochko
0d1215e82f Remove redundant width/height values from SVGs to fix Safari bug (#4956) 2017-09-15 00:51:00 +02:00
Anna e só
e77cc032c2 l10n: PT-BR translation updated (#4953)
* devise.pt-BR.yml now fully translated

* pt-BR.json now fully translated

* pt-BR.yml partially translated; 46 lines left

* pt-BR.yml now fully translated

* simple_form.pt-BR.yml fully translated

* doorkeeper.pt-BR.yml now fully translated

* E-mail instructions on app/views/user_mailer added and fully translated

* PT-BR translation for #4871

* Deleted an unwanted caracter on pt-BR.yml

* Fixing typos on pt-BR.yml

* Added translation for Pinned toots tab on pt-BR.json

* Added missing translation for navigation_bar.pins

* Fixed spelling on pt-BR.yml

* Update pt-BR.json
2017-09-15 00:02:38 +02:00
sdukhovni
67559361e8 Add scheduled worker to purge old user IPs (#4951)
* Add scheduled worker to purge old user IPs

* Use ruby 1.9 hash syntax
2017-09-14 22:26:38 +02:00
ThibG
4a73615193 Fix race condition when receiving an ActivityPub Create multiple times (#4930)
* Fix race condition when receiving an ActivityPub Create multiple times

* Use a RedisLock to avoid concurrent processing of a same Create activity
2017-09-14 22:26:22 +02:00
Yamagishi Kazutoshi
bdcc9e2ceb Add missing Japanese translations (#4947) 2017-09-14 18:03:34 +02:00
ふぁぼ原
3816943e6b Enable to recognize most kinds of characters as URL paths (#4941) 2017-09-14 18:03:20 +02:00
Masoud Abkenar
b39d512ade l10n: update Persian translation (#4946) 2017-09-15 00:13:38 +09:00
Eugen Rochko
04046a4983 Fix #4908 - Do not keep remote file names, generate random (#4934) 2017-09-14 16:41:59 +02:00
unarist
a4c500176b Include requested URL into the message on network errors (#4945) 2017-09-14 16:12:50 +02:00
abcang
1aad015bbb Revert unique retry job (#4937)
* Revert "Enable UniqueRetryJobMiddleware even when called from sidekiq worker (#4836)"

This reverts commit 6859d4c028.

* Revert "Do not execute the job with the same arguments as the retry job (#4814)"

This reverts commit be7ffa2d75.
2017-09-14 15:12:43 +02:00
m4sk1n
94fba44eec i18n: Update Polish translation (#4942)
* i18n: Update Polish translation

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

* i18n: Update Polish translation

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

* Update pl.yml
2017-09-14 21:58:48 +09:00
Jeroen
721460a59b Another Dutch language update (#4944)
* Update nl.json

* Update nl.yml

* Update nl.json
2017-09-14 21:52:47 +09:00
Naf
45b595cdca Add Japanese translate for #4911 (#4943) 2017-09-14 18:20:04 +09:00
Quent-in
aad3df6afc l10n update OC/FR video redesign (#4938)
* l10n update for Redesign video player (#4911)

* Update videp

* Update

I hope this time format works well.

* One missing string

* Update time format

I'd like the complete name of the month in the Long format and the short one in the short format.
I hope it works now
2017-09-14 16:32:14 +09:00
N氏
1023f52eaa Add Japanese translate for #4913 (#4936) 2017-09-14 12:25:18 +09:00
Eugen Rochko
596dab06e9 Support OpenGraph video embeds (#4897)
* Support OpenGraph video embeds

It's not really OpenGraph, it's twitter:player property, but it's
not OEmbed so that fits. For example, this allows Twitch clips to
be displayed as embeds.

Also, fixes glitch-soc/mastodon#135

* Fix invalid OpenGraph cards being saved through attaching and
revisit URLs after 14 days
2017-09-14 04:11:36 +02:00
Eugen Rochko
4f0597d579 Give video player fluid max-width (#4935) 2017-09-14 04:03:00 +02:00
Eugen Rochko
2bbf987a0a Redesign video player (#4911)
* Redesign video player

* Use new video player on static public pages too

* Use media gallery component on static public pages too

* Pause video when hiding it

* Full-screen sizing on WebKit

* Add aria labels to video player buttons

* Display link card on public status page

* Fix fullscreen from modal sizing issue

* Remove contain: strict property to fix fullscreen from columns
2017-09-14 03:39:10 +02:00
ThibG
af00220d79 Fix refollowing (#4931)
* Make RefollowWorker ActivityPub-only to avoid potential identifier mismatches

* Don't call RefollowWorker on new accounts
2017-09-14 00:05:25 +02:00
Eugen Rochko
9239e4ce4d Uploads for admin site settings (#4913)
* Improve OpenGraph tags for about pages

* Add thumbnail admin setting

* Fix error

* Fix up
2017-09-14 00:04:30 +02:00
m4sk1n
06f26e09b4 i18n: Update Polish translation (#4929)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-13 20:57:33 +02:00
Jeroen
331263270b Updating Dutch translation (#4927)
* Update doorkeeper.nl.yml

* Update nl.yml

* Update simple_form.nl.yml

* Update nl.json

* Update en.json

* Update en.json

* Update nl.json
2017-09-13 23:12:29 +09:00
MitarashiDango
283a5ec1a4 fix share intent. (#4926) 2017-09-13 15:20:03 +02:00
ThibG
550ff677da Fix ActivityPub handling of replies with WEB_DOMAIN (#4895) (#4904)
* Fix ActivityPub handling of replies when LOCAL_DOMAIN ≠ WEB_DOMAIN (#4895)

For all intents and purposes, `local_url?` is used to check if an URL refers
to the Web UI or the various API endpoints of the local instances. Those things
reside on `WEB_DOMAIN` and not `LOCAL_DOMAIN`.

* Change local_url? spec, as all URLs handled by Mastodon are based on WEB_DOMAIN
2017-09-13 14:22:16 +02:00
nullkal
da77f65c46 Add instance search feature (#4925) 2017-09-13 12:30:07 +02:00
nullkal
9e2ff3ef71 Make instance names in into links to user list in the instance (#4924) 2017-09-13 11:34:07 +02:00
Eugen Rochko
b9d241c6f5 Fix #4917 - Add missing suspend checks (#4921) 2017-09-13 11:05:02 +02:00
Eugen Rochko
56af04dbb4 Fix #4918 - Limit pinned toots to 5 (#4923) 2017-09-13 11:04:32 +02:00
abcang
60944d5dca Fix height cache (#4909) 2017-09-13 10:24:33 +02:00
Daigo 3 Dango
081f907f90 Specify libicu explicitly in Aptfile (#4920)
It seems libicu-dev no longer installs libicu55 needed by charlock_holmes.
2017-09-13 09:30:13 +02:00
ThibG
f29918e707 [WiP] Whenever a remote keypair changes, unfollow them and re-subscribe to … (#4907)
* Whenever a remote keypair changes, unfollow them and re-subscribe to them

In Mastodon (it could be different for other OStatus or AP-enabled software),
a keypair change is indicative of whole user (or instance) data loss. In this
situation, the “new” user might be different, and almost certainly has an empty
followers list. In this case, Mastodon instances will disagree on follower
lists, leading to unreliable delivery and “shadow followers”, that is users
believed by a remote instance to be followers, without the affected user
knowing.

Drawbacks of this change are:
1. If an user legitimately changes public key for some reason without losing
   data (not possible in Mastodon at the moment), they will have their remote
   followers unsubscribed/re-subscribed needlessly.
2. Depending of the number of remote followers, this may generate quite some
   traffic.
3. If the user change is an attempt at usurpation, the remote followers will
   unknowingly follow the usurper. Note that this is *not* a change of
   behavior, Mastodon already behaves like that, although delivery might be
   unreliable, and the usurper would not have known the former user's
   followers.

* Rename ResubscribeWorker to RefollowWorker

* Process followers in batches
2017-09-12 23:10:40 +02:00
unarist
af10c9fbff Add section for protocol specific information on the admin page (#4910)
This PR adds section for protocol specific information, then always show
both of OStatus and ActivityPub. Specifically, this will help admins to
check PuSH subscription status and unsubscribe manually, even `protocol`
has been changed.

This also includes below changes:

* Add `overflow: hidden` to prevent float leaking
* Add missing fields for ActivityPub
2017-09-12 23:06:10 +02:00
Eugen Rochko
8f8e677630 Clean up and improve generated OpenGraph tags (#4901)
- Return all images as og:image
- Return videos as og:image (preview) and og:video
- Return profile:username on profiles
2017-09-12 05:39:38 +02:00
Eugen Rochko
4931eac280 Fix nil error for old toots that don't have a conversation (#4900) 2017-09-12 00:57:18 +02:00
Eugen Rochko
881856553e Fix #4894 - Merge context hash into final JSON hash after key transform (#4898) 2017-09-12 00:16:18 +02:00
Eugen Rochko
0a6b098668 Fix count numbers from ActivityPub not being saved (#4899)
They are marked as read-only by Rails, but we know what we are doing,
so we are un-marking them as such.

The mastodon:maintenance:update_counter_caches task is not really
supposed to be run anymore (it was a one-time thing during an upgrade)
however, just in case, I have modified it to not touch ActivityPub
accounts.

Also, no point writing to logger from these rake tasks, since they
are not to be run from cron. Better to give stdout feedback.
2017-09-12 00:16:03 +02:00
Eugen Rochko
0ef9d45d05 Fix error when following locked accounts (#4896) 2017-09-11 23:50:37 +02:00
Grey Baker
a6a206ef85 Bump puma from 3.9.1 to 3.10.0 (#4879)
Bumps [puma](https://github.com/puma/puma) from 3.9.1 to 3.10.0.
- [Release notes](https://github.com/puma/puma/releases/tag/v3.10.0)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v3.9.1...v3.10.0)
2017-09-11 23:43:19 +02:00
Grey Baker
bbff144004 Bump rails from 5.1.3 to 5.1.4 (#4875)
Bumps [rails](https://github.com/rails/rails) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/rails/rails/compare/v5.1.3...v5.1.4)
2017-09-11 23:03:14 +02:00
unarist
47d48fed8d Reset preview image if avatar/header image selection was cancelled (#4893) 2017-09-11 16:19:54 +02:00
Patrick Figel
3018043fc2 Add OpenStack Keystone V3 support (#4889)
Keystone V2 is deprecated in favour of V3. This adds the necessary
connection parameters for establishing a V3 connection. Connections
to V2 endpoints are still possible and the configuration should
remain compatible.

This also introduces a SWIFT_REGION variable for multi-region
OpenStack environments and a SWIFT_CACHE_TTL that controls how long
tokens and other meta-data is cached for. Caching tokens avoids
rate-limiting errors that would result in media uploads becoming
unavailable during high load or when using tasks like
media:remove_remote. fog-openstack only supports token caching for
V3 endpoints, so a recommendation for using V3 was added.
2017-09-11 15:11:13 +02:00
Anna e só
c2bee07dbc l10n: Full PT-BR translation (#4882)
* devise.pt-BR.yml now fully translated

* pt-BR.json now fully translated

* pt-BR.yml partially translated; 46 lines left

* pt-BR.yml now fully translated

* simple_form.pt-BR.yml fully translated

* doorkeeper.pt-BR.yml now fully translated

* E-mail instructions on app/views/user_mailer added and fully translated

* PT-BR translation for #4871

* Deleted an unwanted caracter on pt-BR.yml

* Fixing typos on pt-BR.yml

* Added translation for Pinned toots tab on pt-BR.json

* Added missing translation for navigation_bar.pins
2017-09-11 08:40:29 +09:00
Masoud Abkenar
a345479de2 l10n: update Persian translation (#4880)
* l10n: update Persian translation

* l10n: fix missing Persian translation
2017-09-11 08:35:27 +09:00
m4sk1n
08f00df94b i18n: Update Polish translation (#4881) 2017-09-11 00:25:39 +09:00
Eugen Rochko
ab71cf4593 Bump to 1.6.0 2017-09-10 15:10:03 +02:00
Eugen Rochko
c450ddb613 Fix POST /api/v1/follows error when already following (#4878) 2017-09-10 15:09:06 +02:00
yoshipc
15b886a6f0 Fix Japanese translation (#4876)
I translated the additional text ( added by #4871)
2017-09-10 20:34:15 +09:00
Eugen Rochko
4819e2913d Bump version to 1.6.0rc5 2017-09-10 10:26:51 +02:00
Eugen Rochko
72e662bb0d Hide modal loading screen for media/video/boost/confirm/actions modals (#4873) 2017-09-10 10:26:01 +02:00
Eugen Rochko
7d7844a47f Default follows for new users (#4871)
When a new user confirms their e-mail, bootstrap their home timeline
by automatically following a set of accounts. By default, all local
admin accounts (that are unlocked). Can be customized by new admin
setting (comma-separated usernames, local and unlocked only)
2017-09-10 09:58:38 +02:00
Eugen Rochko
f2cbfb2eb3 Fix dimensions of loading component for compose drawer (#4872) 2017-09-10 08:48:11 +02:00
Yamagishi Kazutoshi
3f333a8d31 Set fallback address when empty notification address (#4868) 2017-09-09 21:31:48 +02:00
Quent-in
bc077018b8 i10n minors changes for 1.6 (#4867)
* wrong preposition + typo

* wrong preposition + typo

* Typo

* Typo

* minor changes

* minor changes
2017-09-09 18:22:56 +02:00
Eugen Rochko
90712d4293 Fix errors preventing UnsubscribeService from working (#4866) 2017-09-09 17:36:27 +02:00
Eugen Rochko
6867681c7c Add script to make embedded iframes autosize (#4853) 2017-09-09 16:23:44 +02:00
Eugen Rochko
bdc8b4fd91 Disable mouse-based pause from #4859 (#4865)
It wasn't working ideally and introduced some annoying false positivies
2017-09-09 15:09:50 +02:00
Eugen Rochko
2ff7146b6d Bump version to 1.6.0rc4 2017-09-09 14:53:49 +02:00
unarist
c7908e2d09 Fix scroll behavior and others on paused timeline (#4864)
Resolved:

* Lot of redundant renders while mouse moving
* Scroll jumping when timeline loaded
* Scroll position isn't kept when statuses below the scrollTop was deleted then new status arrived

Unresolved:

* Scroll position isn't kept when statuses over the scrollTop was deleted then new status arrived
-> It needs to know which statuses are over the scrollTop
* New status indicator should be active when new statuses arrived while mouse moved recently
-> It needs a) update indicator in ScrollableList, or b) set scrollTop status while mouse moving
2017-09-09 14:16:11 +02:00
Yamagishi Kazutoshi
c9d04f1c39 Fix second report (regression from 3b81baaaaf) (#4863) 2017-09-09 13:42:48 +02:00
Eugen Rochko
9e15eeec63 Add missing reject_media check before avatar download via ActivityPub (#4862) 2017-09-09 13:41:45 +02:00
Lynx Kotoura
3c45d3963a Scrollable tables in settings pages (#4857)
* Scrollable tables in settings pages

* Add space before curly brace
2017-09-09 02:26:58 +02:00
Eugen Rochko
baa8b82179 Fix #1004 - Temporarily pause timeline if there's been recent mouse movement (#4859) 2017-09-09 02:26:41 +02:00
Eugen Rochko
4b460bc571 Fix #4852 - Check if already requested from FollowService (#4855) 2017-09-09 02:02:44 +02:00
Eugen Rochko
7ca173be47 Fix #4850 - When visibility missing from API call to toot, fallback to user preference (#4861) 2017-09-09 02:02:29 +02:00
unarist
1ae5d49a71 Refresh timeline after toot while the timeline is disconnected (#4858)
To reflect status posting immediately, we've inserted the status into timelines directly. However, status insertion changes "latest status", and it means next timeline refresh only fetches statuses since the inserted status. This behavior is very bad for disconnected timeline and mobile views.

After this patch, it refreshes timeline for disconnected timelines, instead of direct insertion.
2017-09-08 21:43:34 +02:00
unarist
a12572e074 Handle stream_entry URL correctly in ActivityPub (#4854)
In before, the method uses stream_entry id as status id, so replied status was wrongly selected.

This PR uses StatusFinder which was introduced with `Api::Web::EmbedsController`.
2017-09-08 18:20:03 +02:00
Quent-in
dabc309ca3 i10n update OC and FR (#4849)
* Missing "navigation_bar.pins"

* Missing "navigation_bar.pins"
2017-09-08 13:55:47 +02:00
Eugen Rochko
1caf11ddcc Fix language filter codes (#4841)
* Fix language filter codes

CLD3 returns BCP-47 language identifier, filter settings expect
identifiers in the ISO 639-1 format. Convert between formats,
and exclude duplicate languages from filter choices (zh-CN->zh)

* Fix zh name
2017-09-08 12:32:22 +02:00
Eugen Rochko
95f018a3d4 "Mute conversation" option on all own toots, not just in notifications (#4844)
That way you can mute notifications for a toot before you get replies
to it or boosts or favourites
2017-09-08 12:00:30 +02:00
Eugen Rochko
a4caa7eb62 Fetch statuses/following/followers numbers from ActivityPub collections (#4840) 2017-09-08 12:00:17 +02:00
m4sk1n
7c2d84910c i18n: Update Polish translation (#4845)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-08 05:51:48 +09:00
Quent-in
b00cc4b9bd i10n OC / FR update Pinned toots (#4842)
* Added column.pins

New strings

* Added column.pins

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* Update password_change.oc.html.erb

* Update password_change.oc.text.erb

* Update reset_password_instructions.oc.html.erb

* Update reset_password_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb
2017-09-07 22:07:03 +02:00
Eugen Rochko
dd6ede554f Fix #4834 - Adjust Status#local and Status#remote scopes (#4839) 2017-09-07 20:18:34 +02:00
abcang
6859d4c028 Enable UniqueRetryJobMiddleware even when called from sidekiq worker (#4836) 2017-09-07 16:44:14 +02:00
PFM
7d853b514a Use <button> instead of <div role="button"> (#4835) 2017-09-07 16:18:41 +02:00
voidSatisfaction
85c7c42098 Add Pinned toot column (#4817)
* Add Pinned_toot_section

* Fix add frozen_string_literal

* Fix delete no need controller and tests

* Fix replace query strings to axios params

* Fix change value to accountId and disabling more button
2017-09-07 09:58:11 +02:00
voidSatisfaction
8185f98872 Feat add validation for report comment: characters under 1000 valid (#4833) 2017-09-07 09:55:42 +02:00
Joseph Mingrone
5264496240 Use casecmp() instead of casecmp?() for now (#4832)
* Use casecmp() instead of casecmp?() for now

casecmp?() is only available in ruby 2.4.0.  Users running earlier ruby versions
would see errors, e.g., running
RAILS_ENV=production rails mastodon:maintenance:remove_deprecated_preview_cards.

* Correctly check whether casecmp() returns 0
2017-09-07 03:55:06 +02:00
Quent-in
be75b13d68 i10n update OC and FR files (#4824)
* Onboarding: corrections

Some missing letters and spaces or better wording

* Embed

Translated as Intégrer in FR / Embarcar in OC
2017-09-07 08:55:03 +09:00
Olivier Humbert
9417c9bb8f Update fr.json (#4830)
typo
2017-09-06 22:32:49 +02:00
Eugen Rochko
11bddd31ce Fix locking migration on statuses table. Nullable column and NO default value (#4825) 2017-09-06 20:57:52 +02:00
Eugen Rochko
dd5cb5085c Bump version to 1.6.0rc2 2017-09-06 19:02:03 +02:00
Eugen Rochko
e7adbf572a Switch to static URIs, new URI format in both protocols for new statuses (#4815)
* Decouple Status#local? from uri being nil

* Replace on-the-fly URI generation with stored URIs

- Generate URI in after_save hook for local statuses
- Use static value in TagManager when available, fallback to tag format
- Make TagManager use ActivityPub::TagManager to understand new format
- Adjust tests

* Use other heuristic for locality of old statuses, do not perform long query

* Exclude tombstone stream entries from Atom feed

* Prevent nil statuses from landing in Pubsubhubbub::DistributionWorker

* Fix URI not being saved (#4818)

* Add more specs for Status

* Save generated uri immediately

and also fix method order to minimize diff.

* Fix alternate HTML URL in Atom

* Fix tests

* Remove not-null constraint from statuses migration to speed it up
2017-09-06 19:01:28 +02:00
voidSatisfaction
13ffa3c59e Add Smartphone screen favourite back button and adjust styles (#4813)
* Feat add get-back button on favourite columnHeader

* Style adjust nice looking get-back button

* Fix delete media query and add padding right

* fix: restore padding and add lastchild style for back-button
2017-09-06 17:32:15 +02:00
Masoud Abkenar
aec5097d44 i18n: update Persian translation (#4822) 2017-09-06 17:31:54 +02:00
Clworld
1646f622a5 fix scroll position (#4821) 2017-09-06 17:29:56 +02:00
Yamagishi Kazutoshi
e0cda4a851 Update react-intl to version 2.4.0 (#4820) 2017-09-06 16:25:19 +02:00
PFM
d8d2a54741 fix text position of NSFW for video file (#4819) 2017-09-06 13:55:47 +02:00
Yamagishi Kazutoshi
fa21d004c7 Add environment sample for OpenStack Swift (#4816) 2017-09-06 12:13:00 +02:00
Adam Thurlow
6994664a13 swift-enable the paperclip! 📎 (#2322) 2017-09-05 23:17:06 +02:00
abcang
be7ffa2d75 Do not execute the job with the same arguments as the retry job (#4814) 2017-09-05 20:56:20 +02:00
Eugen Rochko
e821c00e74 Fix mentions in direct statuses not being delivered via AP (#4806) 2017-09-05 20:55:25 +02:00
Eugen Rochko
9b994c4aee Fix #4794 - Fake instant follow in API response when account is believed unlocked (#4799) 2017-09-05 17:48:13 +02:00
Lynx Kotoura
4c3dd0b254 Adjust status embeds (#4808)
* Adjust status embeds

Adjust styles of embed code. Adjust styles of embed pages. Fix overflow of embed-modal.

* Remove trailing whitespace

* Using width from the variable
2017-09-05 12:31:24 +02:00
takayamaki
672df4ecc0 add index_notifications_on_id_and_account_id_and_activity_type on notifications table (#4750) 2017-09-05 12:30:01 +02:00
Treyssat-Vincent Nino
aefb4719bc comment correction (#4812) 2017-09-05 12:13:25 +02:00
voidSatisfaction
4d67bf18fe Translation korean added (#4802) 2017-09-04 20:40:01 +02:00
ButterflyOfFire
f09a250a7c Update ar.yml (#4810)
Some little changes to "ar" locale
2017-09-04 19:54:12 +02:00
Eugen Rochko
9b50a9dd83 Fix some ActivityPub JSON bugs (#4796)
- Fix assumption that `url` is always a string. Handle it if it's an
  array of strings, array of objects, object, or string, both for
  accounts and for objects
- `sharedInbox` is actually supposed to be under `endpoints`, handle
  both cases and adjust the serializer
2017-09-04 18:26:33 +02:00
nullkal
2293466edd Show pinned statuses only in the top of the profile page (#4803)
* Show pinned statuses only in the top of the profile page

* Refactor AccountsController#show_pinned_statuses?
2017-09-04 12:53:18 +02:00
voidSatisfaction
b6f3869f8d Fix streaming url to lowercase (#4804) 2017-09-04 12:52:06 +02:00
Eugen Rochko
09cffaaf04 Fix #4551 - Use correct syntax for content preloading (#4798) 2017-09-04 02:14:12 +02:00
abcang
334a633c2a Fix a problem that notification column goes to top (#4792) 2017-09-03 20:31:51 +02:00
Eugen Rochko
8b12e3cc7f Fix short number locales (#4790)
Overwrite values from rails-i18n by manually overwriting in every locale.
We want numbers like 1.5K in every language
2017-09-03 22:17:34 +09:00
Andreas Drop
d3f46a77c3 Make german translation more gender neutral #4755 (#4789) 2017-09-03 15:17:24 +02:00
Akihiko Odaki
a789315361 Use next instead of return in task (#4787) 2017-09-03 12:39:39 +02:00
May Kittens Devour Your Soul
579c7a88e0 Croatian translation - updated (#4183)
* Update hr.json

* Update hr.json
2017-09-03 18:10:53 +09:00
Quent-in
8538170c2d l10n Occitan update for Embed, cancel follow request, ... (#4788)
* Update: some missing strings

* Updates missing strings

* New string

* Update oc.json

* Update oc.yml

* Update oc.json
2017-09-03 18:08:37 +09:00
Akihiko Odaki
249bdc169c Explicitly define attached file of DeprecatedPreviewCard (#4786)
The path template of the attached files must explicitly be defined because
it is contradicting to the name of the class.
2017-09-03 07:58:58 +02:00
Eugen Rochko
9dd8dff683 Bump version to 1.6.0rc1 (#4768) 2017-09-03 01:12:05 +02:00
Eugen Rochko
a187dcefa1 Instantly upgrade account to ActivityPub if we receive ActivityPub payload (#4766) 2017-09-03 01:11:23 +02:00
Eugen Rochko
5d170587e3 Improve client-side German i18n (#4785) 2017-09-02 23:21:48 +02:00
Eugen Rochko
37fdddd927 Rename "locked" to "manuallyApprovesFollowers" in ActivityPub (#4779)
See: <https://www.w3.org/wiki/Activity_Streams_extensions#as:manuallyApprovesFollowers>
2017-09-02 23:13:35 +02:00
abcang
6ec1aa372d Validate data of Imports (#4782) 2017-09-02 20:45:42 +02:00
m4sk1n
2c3544eedd i18n: Improve Polish translation (#4783)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-02 20:45:20 +02:00
Eugen Rochko
d3b6746173 Make "unfollow" undo pending outgoing follow request too (#4781)
* Make "unfollow" undo pending outgoing follow request too

* Add cancel button to web UI when awaiting follow request approval

* Make the hourglass button do the cancelling
2017-09-02 20:44:41 +02:00
PFM
2a5d1d5a1b fix text position of NSFW in Safari (Mac/iPhone) (#4570) 2017-09-02 18:52:56 +02:00
unarist
6a4e2db661 Raise an error for remote url in StatusFinder (#4776)
* Raise an error for remote url in StatusFinder

Previous implementation had allowed remote url with status id which also exists on local.

Then that bug leads /api/web/embed to return wrong embed url.

* Fix oembed_controller_spec
2017-09-02 17:42:47 +02:00
unarist
bfa7f9ebf2 Fix WebPush (regression from #4524) (#4778)
* Fix NoMethodError in Web::PushSubscription

```
undefined method `site_contact_email' for #<Class:0x00005976d13c40>

/usr/local/bundle/gems/activerecord-5.1.3/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
/usr/local/bundle/gems/attr_encrypted-3.0.3/lib/attr_encrypted.rb:295:in `method_missing'
/usr/local/bundle/gems/attr_encrypted-3.0.3/lib/attr_encrypted/adapters/active_record.rb:129:in `method_missing_with_attr_encrypted'
/mastodon/app/models/web/push_subscription.rb:53:in `push_payload'
```

* Specify serializer in Web::NotificationSerializer
2017-09-02 16:50:42 +02:00
Surinna Curtis
8cc1ed3c55 Don't unconditionally call preventDefault and stopPropagation on all keyup events (#4777)
* UploadArea should only preventDefault for Escape

This will make accessibility for some things less effortful, since we won't have to define a prior event handler to do whatever should be happening by default.

* Remove workaround for fixed bug in SettingToggle

SettingToggle was toggling itself in response to keydown of space, and then the keyup was doing it again
2017-09-02 16:27:16 +02:00
m4sk1n
5e1e466da0 i18n: Update Polish translation (#4775)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-02 16:25:54 +02:00
Lynx Kotoura
cfe39fb58d Adjust settings pages (#4765)
* views: Adjust heading positions

* Adjust settings pages

Adjust label. Adjust tables. Adjust admin/reports/* pages. Fix 2FA QR code style for narrow devices. Widen several pages. Increase contrast.

* Remove trailing whitespace
2017-09-02 15:49:28 +02:00
Yamagishi Kazutoshi
a0294c8880 Add Japanese translate for #4561 (#4771) 2017-09-02 14:02:29 +02:00
Yamagishi Kazutoshi
ba8fb2fd0f Add text color style for noscript link (#4772) 2017-09-02 14:02:15 +02:00
Yamagishi Kazutoshi
6fd2e8c3c5 Fix profile page when use system's font (#4774) 2017-09-02 14:01:59 +02:00
Yamagishi Kazutoshi
15963a15c6 Disable embed modal when private status (#4773)
* Disable embed modal when private status

* Remove `reblogDisabled`
2017-09-02 14:01:44 +02:00
Eugen Rochko
1b5806b744 Define missing JSON-LD properties (#4767)
Using _: property names is discouraged, as in the future,
canonicalization may throw an error when encountering that instead
of discarding it silently like it does now.

We are defining some ActivityStreams properties which we expect
to land in ActivityStreams eventually, to ensure that future versions
of Mastodon will remain compatible with this even once that happens.
Those would be `locked`, `sensitive` and `Hashtag`

We are defining a custom context inline for some properties which we
do not expect to land in any other context. `atomUri`, `inReplyToAtomUri`
and `conversation` are part of the custom defined OStatus context.
2017-09-02 14:01:23 +02:00
Eugen Rochko
1b1e025b41 Use updated ActivityStreams context (added: sharedInbox) (#4764) 2017-09-02 14:00:58 +02:00
mayaeh
ab9f1b6e50 Add japanese translations for embed modal feature. (#4770) 2017-09-02 14:48:51 +09:00
Yamagishi Kazutoshi
b767eb7ff8 Add RoutingHelper (#4769) 2017-09-02 03:03:20 +02:00
m4sk1n
0b32338e3f Add link to 'noscript' message (#4561)
* Add link to 'noscript' message

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

* remove indent
2017-09-02 01:52:28 +02:00
Eugen Rochko
e482595a5d Add ActivityPub handler for Delete->Actor activities (#4761) 2017-09-01 21:54:42 +02:00
Eugen Rochko
9c04fadec9 Finish up embed modal feature (#4759)
* Add embed button to dropdowns of in-timeline statuses

* yarn run manage:translations
2017-09-01 21:30:13 +02:00
Eugen Rochko
390bfec6da Avoid sending some ActivityPub payloads if the receiver will get them through distribution (#4739) 2017-09-01 21:26:01 +02:00
Eugen Rochko
c2980d5b17 Do not rely on activity arriving exactly once after delete arrived (#4754) 2017-09-01 21:12:59 +02:00
Cygnan
a75aa62f5b Adjust padding on the public profile page (#4757)
* Fix a style issue on the public profile page for some mobile browsers

Signed-off-by: Cygnan <email@cygnan.com>

* Set padding-bottom to 20px

Signed-off-by: Cygnan <email@cygnan.com>
2017-09-01 21:01:23 +02:00
unarist
8fd8f81ae7 Deduplicate with local status on Create activity (#4763) 2017-09-01 21:00:43 +02:00
Eugen Rochko
921cf3e9c8 Fix NoMethodError (#4762) 2017-09-01 20:50:42 +02:00
Eugen Rochko
7dc5035031 Make PreviewCard records reuseable between statuses (#4642)
* Make PreviewCard records reuseable between statuses

**Warning!** Migration truncates preview_cards tablec

* Allow a wider thumbnail for link preview, display it in horizontal layout (#4648)

* Delete preview cards files before truncating

* Rename old table instead of truncating it

* Add mastodon:maintenance:remove_deprecated_preview_cards

* Ignore deprecated_preview_cards in schema definition

* Fix null behaviour
2017-09-01 16:20:16 +02:00
Yamagishi Kazutoshi
2305f7c391 Use system's default font on non web UI pages (#4553)
* Use system's default font on non web UI pages

* Remove import for Redirect
2017-09-01 16:13:31 +02:00
Eugen Rochko
ff7d02b236 Make first use less overwhelming with browser permissions (#4760)
- Ask for desktop notifications after 1 minute of use instead of
  instantly
- Ask for protocol handler permission after 5 minutes of use
  instead of instantly
2017-09-01 16:07:08 +02:00
Damien Erambert
1a0df58878 Update FR locales (#4714)
* Make the fr locales up-to-date with the last changes (new profile view, applications)

* Use the same wording for toots in fr.yml and fr.json

* Translate the pin related strings

* Translate pin-related locales on the front-end

* Add missing locales in doorkeeper.fr.yml and remove un-used ones

* Change "posts" back to "status" in the /about/more page in fr.yml

* Fix typos for "status" in fr.yml

* fix typo for "status" in fr.json

* Remove duplicate string

* Non-breaking space before punctuation

* 'Better' translation for "unpin"

* Put back 'pouet' where it was already

* Fix

* Fix
2017-09-01 14:09:01 +02:00
Eugen Rochko
74437c6bff Refactor Web::PushSubscription, remove welcome message (#4524)
* Refactor Web::PushSubscription, remove welcome message

* Add missing helper

* Use locale of the receiver on push notifications (#4519)

* Remove unused translations

* Fix dir on notifications
2017-09-01 13:35:23 +02:00
unarist
504737e860 Convert OStatus tag to ActivityPub id on in_reply_to resolution (#4756) 2017-09-01 13:34:04 +02:00
unarist
af2d22f88c Fallback from perform_via_activitypub on private posts (#4758)
Currently, private / direct posts via OStatus from AP compatible instance will be dropped due to failing to fetch AP version.

So this fallbacks to OStatus handling:

* when failed to fetch ActivityPub version
* when status is neither :public nor :unlisted
2017-09-01 12:33:02 +02:00
Eugen Rochko
9a5ae09620 Remove identity context from output of LinkedDataSignature (#4753) 2017-08-31 21:32:09 +02:00
unarist
f7937d903c Don't process ActivityPub payload if signature is invalid (#4752)
* Don't process ActivityPub payload if signature is invalid

* Fix style issue
2017-08-31 17:18:49 +02:00
James
6b2be5dbfb Guarantee Subscription service first account has proper URL details (#4732)
* Guarantee Subscription service first account has proper URL details

Subscription Service potentially could break if the first user suspended
themselves, creating a situation where the urls that populate throughout
subscription service's PuSH request would cause the remote API to throw 503 errors.

Guaranteeing that the first account picked is not suspended prevents this problem.

* Fix style issue
2017-08-31 15:44:00 +02:00
Akihiko Odaki
69957ed10a Fix the usages of Detect Passive Events (#4749) 2017-08-31 11:20:54 +02:00
Eugen Rochko
d1a78eba15 Embed modal (#4748)
* Embed modal

* Proxy OEmbed requests from web UI
2017-08-31 03:38:35 +02:00
Eugen Rochko
2db9ccaf3e Add sharedInbox to actors (#4737) 2017-08-31 00:02:59 +02:00
Wonderfall
cecf204bbb Update to Alpine 3.6 (#4747) 2017-08-30 23:52:19 +02:00
MitarashiDango
fec13735a7 error fixed (when loading pages in single column mode.) (#4746) 2017-08-30 17:30:25 +02:00
Eugen Rochko
7b8f262840 Forward ActivityPub creates that reply to local statuses (#4709)
* Forward ActivityPub creates that reply to local statuses

* Fix test

* Fix wrong signers
2017-08-30 15:37:02 +02:00
Yamagishi Kazutoshi
3f51a22d3b Add close tag of iframe for oEmbed response (#4745)
* Add close tag of iframe for oEmbed response

* add comma
2017-08-30 14:03:17 +02:00
nullkal
39e7a763ff Use request.remote_ip instead of request.ip (#4744) 2017-08-30 10:24:30 +02:00
Eugen Rochko
e95bdec7c5 Update status embeds (#4742)
- Use statuses controller for embeds instead of stream entries controller
- Prefer /@:username/:id/embed URL for embeds
- Use /@:username as author_url in OEmbed
- Add follow link to embeds which opens web intent in new window
- Use redis cache in development
- Cache entire embed
2017-08-30 10:23:43 +02:00
Eugen Rochko
fcca31350d Remove unneccesary indices (#4738)
We only look up status_pins by account_id, or account_id and status_id,
never by status_id
2017-08-30 05:04:20 +02:00
Eugen Rochko
ee72a39641 Update bundler-audit and brakeman (#4740) 2017-08-30 03:30:13 +02:00
abcang
f59ed3a4fa Scroll smoothly to the right (#4735) 2017-08-29 17:06:19 +02:00
MitarashiDango
7be620775e fix error when single columns mode. (#4734) 2017-08-29 16:11:28 +02:00
Eugen Rochko
4c76402ba1 Serialize ActivityPub alternate link into OStatus deletes, handle it (#4730)
Requires moving Atom rendering from DistributionWorker (where
`stream_entry.status` is already nil) to inline (where
`stream_entry.status.destroyed?` is true) and distributing that.

Unfortunately, such XML renderings can no longer be easily chained
together into one payload of n items.
2017-08-29 16:11:05 +02:00
Akihiko Odaki
9958eba356 Do not scroll the columns area due to redirection (#4541)
Commit 9d1f8b9d6a scrolls the columns area
when the route changes since the user is likely to want to see the
rightmost column in such cases.

However, redirection is automatic and does not indicate users' intension.
Do not scroll the columns area due to one.
2017-08-29 14:16:21 +02:00
abcang
0827c09c44 Generalized the infinite scrollable list (#4697) 2017-08-28 22:23:44 +02:00
unarist
938cd2875b Fix Delete activity handling when the status has been reblogged (#4729) 2017-08-28 22:08:11 +02:00
unarist
7876aed134 Fix deletion of status which has been reblogged (#4728) 2017-08-28 21:38:59 +02:00
abcang
ce9a5f358e rescue HTTP::ConnectionError in RemoteFollowController#create (#4726) 2017-08-28 19:12:09 +02:00
Lynx Kotoura
8f527bd588 Add japanese translations for shorten display of large numbers (#4722) 2017-08-28 08:16:49 +09:00
Lynx Kotoura
07994eed00 Adjust "signed in as" pages (#4720)
* Adjust "signed in as" pages


Fix min-width


Set width of .account-header .name

To apply text-overflow and overflow settings
Set overflow for detailed-status__display-name

* Remove trailing whitespace
2017-08-28 00:01:07 +02:00
Lynx Kotoura
bab9afaa09 Adjust public profile pages (#4713)
* Adjust account-grid in public profiles

Full-width card on mobile UI. Set break-word for long name and ID. Fix margin.

* Reduce padding-bottom of public profiles

* Revive next prev buttons in mobile public profiles

In followers followees pages.

* Revert break-word for username

* Fix overflow of display_name

Need re-setting text-overflow and overflow in display: block;
2017-08-27 23:59:51 +02:00
Eugen Rochko
15093f9113 Shorten display of large numbers on public profiles (#4711) 2017-08-27 17:04:45 +02:00
mayaeh
f92d991e52 Add japanese translations for Pinned statuses based on pawoo. (#4717)
Add japanese translations for pin_errors.
2017-08-28 00:03:27 +09:00
Eugen Rochko
26402ee2cb Adjust RTL styles (#4712) 2017-08-27 13:35:18 +02:00
unarist
f095a9f8a5 Allow Symbol keyed Hash in LinkedDataSignature (#4715)
SerializarbleResource#as_json serializes to Symbol keyed Hash, but current
implementation of LinkedDataSignature expects String keyed Hash.

So it generates broken payload.
2017-08-27 13:35:01 +02:00
Eugen Rochko
0d5d11eeff Add _:inReplyToAtomUri to ActivityPub (#4702) 2017-08-26 19:55:10 +02:00
Eugen Rochko
0397c58b61 Forward ActivityPub deletes to followers of rebloggers (#4706) 2017-08-26 18:52:53 +02:00
Eugen Rochko
884b085f53 Use Tombstone and _:atomUri in Delete activities as fallback (#4704) 2017-08-26 16:10:35 +02:00
Eugen Rochko
2a2698e450 Add ActivityPub serializer for Undo of Announce (#4703) 2017-08-26 15:32:40 +02:00
Lynx Kotoura
8ecfdd8795 Set margin between character-counter and compose-form__buttons (#4698)
For some languages publish translation is long.
2017-08-26 14:23:20 +02:00
Eugen Rochko
00840f4f2e Add handling of Linked Data Signatures in payloads (#4687)
* Add handling of Linked Data Signatures in payloads

* Add a way to sign JSON, fix canonicalization of signature options

* Fix signatureValue encoding, send out signed JSON when distributing

* Add missing security context
2017-08-26 13:47:38 +02:00
Anna e só
1cebfed23e Added new translations of error messages, block and mute domains and users, privacy disclaimers, etc (#4700)
* Added new translations of error messages, block and mute domains and users

* Added new translations of error messages, block and mute domains and users
2017-08-26 20:45:35 +09:00
masarakki
649a20ab46 authorize-follow-requests-after-unlocking (#4658) 2017-08-26 12:40:03 +02:00
Yamagishi Kazutoshi
3ac7b353f8 Fix missing at-sign (regression from #4688) (#4705) 2017-08-26 12:39:26 +02:00
Lynx Kotoura
21bb4a6c3b Fix ar.json (#4699)
Remove ! from compose_form.publish
2017-08-25 20:02:44 +02:00
nullkal
c2af138113 Allow multiple pinned statuses to be shown and make them be ordered b… (#4690)
* Allow multiple pinned statuses to be shown and make them be ordered by pinned date

* Set timestamps NOT NULL

* Make single-line pinned_statuses

* Spec for pinned_statuses

* Remove redundant empty line
2017-08-25 18:50:52 +02:00
unarist
fb8aa2b3ba Apply user timezone for the title attribute of .time-ago (#4693) 2017-08-25 17:21:16 +02:00
Yamagishi Kazutoshi
00f9f16f94 Change timezone of the datetime to what browser specifies (#4688) 2017-08-25 17:21:00 +02:00
Lynx Kotoura
18f69fb964 Adjust styles of landing pages. (#4682)
* Adjust about.scss

* Delete trailing whitespace.
2017-08-25 17:19:35 +02:00
Quent-in
04c3fb2189 i18n Updated strings (#4675 - pinned toot) (#4695)
* Added string for pinned toots

* Pinned toot #4675 + missing string

Somehow I deleted it "enabled_success"

* update after advice
2017-08-25 23:04:52 +09:00
Yamagishi Kazutoshi
7c03e59338 Update addressable to version 2.5.2 (#4686) 2017-08-25 14:17:08 +02:00
Yamagishi Kazutoshi
b88635202f Add label for application scopes (#4691)
* Add label for application scopes

* hint
2017-08-25 13:03:26 +02:00
m4sk1n
409051c22c i18n: Update Polish translation #4675 (#4692)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-08-25 17:58:31 +09:00
Eugen Rochko
9caa90025f Pinned statuses (#4675)
* Pinned statuses

* yarn manage:translations
2017-08-25 01:41:18 +02:00
Ratmir Karabut
c5157ef07b Update Russian translation (#4685)
* Add Russian translation (ru)

* Fix a missing comma

* Fix the wording for better consistency

* Update Russian translation

* Arrange Russian setting alphabetically

* Fix syntax error

* Update Russian translation

* Fix formatting error

* Update Russian translation

* Update Russian translation

* Update ru.jsx

* Fix syntax error

* Remove two_factor_auth.warning (appears obsolete)

* Add missing strings in ru.yml

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

* Fix translation consistency

* Update Russian translation

* Update Russian translation (pluralizations)

* Update Russian translation

* Update Russian translation

* Update Russian translation (pin)

* Update Russian translation (account deletion)

* Fix extra line

* Update Russian translation (sessions)

* Update Russian translation

* Update Russian translation

* Fix merge conflicts (revert)
2017-08-25 07:11:06 +09:00
Damien Erambert
f72ed21cd6 Don't load Roboto webfont when system font is used in the app (#4591)
* Don't load Roboto webfont when system font is used in the app

* remove trailing whitespace
2017-08-24 19:28:49 +02:00
Yamagishi Kazutoshi
da172a8b1b Disable babel-loader cache when development environment (#4684) 2017-08-24 19:27:52 +02:00
Eugen Rochko
cf615abbf9 Add configuration to disable private status federation over PuSH (#4582) 2017-08-24 17:51:32 +02:00
unarist
b01a19fe39 Fetch reblogs as Announce activity instead of Note object (#4672)
* Process Create / Announce activity in FetchRemoteStatusService

* Use activity URL in ActivityPub for reblogs

* Redirect to the original status on StatusesController#show
2017-08-24 16:21:42 +02:00
Eugen Rochko
c66fe2aeba Minor performance improvement for test suite (#4678) 2017-08-24 13:31:55 +02:00
Yamagishi Kazutoshi
fbe1115114 Remove eslint-disable comments (#4681)
Do not reject console.error and console.warn with ESLint rules.
2017-08-24 12:15:36 +02:00
Quent-in
e4c761f902 l18n update OC new strings (#4664) (#4680)
* New strings

* Update

Thin non breaking spaces

* Update

Thin non breaking spaces

* Update

Thin non breaking spaces
2017-08-24 16:16:32 +09:00
HIKARU KOBORI
2c6a85832c Fix typo in admin/status_controller.rb (#4679) 2017-08-24 04:03:52 +02:00
m4sk1n
829e2e8c5d Update Polish translation (#4674)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-08-23 17:45:29 +02:00
m4sk1n
8a716c9e96 Introduce CODEOWNERS file (#4670)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-08-23 15:21:00 +02:00
nullkal
80393a23d0 Use checkboxes for application scope setting (#4671) 2017-08-23 15:16:20 +02:00
Yamagishi Kazutoshi
8d23667536 Add Japanese translations for #2758, #4506, #4521, #4600 and #4664 (#4665)
* Add Japanese translations for #2758, #4506, #4521, #4600 and #4664

* Do not translate Inbox URL and Outbox URL

* Remove "あなたの"

* Remove "あなたの"
2017-08-23 14:14:22 +02:00
unarist
9846806cb5 Fix Japanese translation (#4669) 2017-08-23 20:07:29 +09:00
unarist
760cfe328f Fix accessing to XML attribute in FetchAtomService (#4668) 2017-08-23 12:25:57 +02:00
Eugen Rochko
c1b086a538 Fix up the applications area (#4664)
- Section it into "Development" area
- Improve UI of application form, index, and details
2017-08-23 00:59:35 +02:00
Daigo 3 Dango
696c2c6f2f Add Mastodon::Source.url (#4643)
* Add Mastodon::Source.url

* Update spec

* Refactor

Move things frmo Mastodon::Source to Mastodon::Version
2017-08-22 22:54:19 +02:00
unarist
5927b43c0f Ignore empty response in ActivityPub::FetchRemoteStatusService (#4661)
* Ignore empty response in ActivityPub::FetchRemoteStatusService

This fixes `NoMethodError: undefined method `[]' for nil:NilClass` error.

* Check json.nil? in JsonLdHelper#supported_context?
2017-08-22 20:00:49 +02:00
Colin Mitchell
871c0d251a Application prefs section (#2758)
* Add code for creating/managing apps to settings section

* Add specs for app changes

* Fix controller spec

* Fix view file I pasted over by mistake

* Add locale strings. Add 'my apps' to nav

* Add Client ID/Secret to App page. Add some visual separation

* Fix rubocop warnings

* Fix embarrassing typo

I lost an `end` statement while fixing a merge conflict.

* Add code for creating/managing apps to settings section

- Add specs for app changes
- Add locale strings. Add 'my apps' to nav
- Add Client ID/Secret to App page. Add some visual separation
- Fix some bugs/warnings

* Update to match code standards

* Trigger notification

* Add warning about not sharing API secrets

* Tweak spec a bit

* Cleanup fixture creation by using let!

* Remove unused key

* Add foreign key for application<->user
2017-08-22 18:33:57 +02:00
Yamagishi Kazutoshi
11a7507318 Add delete account link for French (#4659) 2017-08-22 18:31:42 +02:00
unarist
d63de55ef8 Fix bugs which OStatus accounts may detected as ActivityPub ready (#4662)
* Fallback to OStatus in FetchAtomService

* Skip activity+json link if that activity is Person without inbox
* If unsupported activity was detected and all other URLs failed, retry with ActivityPub-less Accept header

* Allow mention to OStatus account in ActivityPub

* Don't update profile with inbox-less Person object
2017-08-22 18:30:15 +02:00
Eugen Rochko
72bb3e03fd Support more variations of ActivityPub keyId in signature (#4630)
- Tries to avoid performing HTTP request if the keyId is an actor URI
- Likewise if the URI is a fragment URI on top of actor URI
- Resolves public key, returns owner if the owner links back to the key
2017-08-21 22:57:34 +02:00
Eugen Rochko
f391a4673a Periodically remove expired PuSH subscribers (#4654) 2017-08-21 22:56:33 +02:00
Lynx Kotoura
143b77e10d Increase contrast in landing pages (#4567)
* Increase contrast in about and about/more page

* Lighten em color in landing pages

* Increase contrast in landing pages


Fix about.scss
2017-08-21 21:59:03 +02:00
Eugen Rochko
4cbb638604 Fix visual line-break glitch with .invisible parts of links (#4655) 2017-08-21 17:59:34 +02:00
Eugen Rochko
3534e115e5 Do not try to re-subscribe to unsubscribed accounts (#4653) 2017-08-21 17:32:41 +02:00
abcang
ea958cae7f Refactoring streaming connections (#4645) 2017-08-21 15:04:34 +02:00
Yamagishi Kazutoshi
10e9a9a3f9 Use URI.join even when S3 enabled (#4652) 2017-08-21 12:42:16 +02:00
Eugen Rochko
6e9eda5331 ActivityPub migration procedure (#4617)
* ActivityPub migration procedure

Once one account is detected as going from OStatus to ActivityPub,
invalidate WebFinger cache for other accounts from the same domain

* Unsubscribe from PuSH updates once we receive an ActivityPub payload

* Re-subscribe to PuSH unless already unsubscribed, regardless of protocol
2017-08-21 01:14:40 +02:00
m4sk1n
4c23544714 i18n: Minor changes in Polish translation (#4649)
* i18n: Minor changes in Polish translation

* i18n: pl
2017-08-21 07:57:28 +09:00
Eugen Rochko
74e5078795 Fix #4637 - Re-add missing doorkeeper_authorize for /api/v1/verify_credentials (#4650) 2017-08-21 00:41:08 +02:00
Yamagishi Kazutoshi
110227ac5e Remove status from favorites list when unfavorited (#4597) 2017-08-20 23:23:05 +02:00
unarist
f26758dc01 Fix .information-board style for Safari (#4602)
flex-basis: 0 allows make flexbox smaller than its contents on Safari <10.

https://github.com/philipwalton/flexbugs#1-minimum-content-sizing-of-flex-items-not-honored
2017-08-20 20:45:44 +02:00
abcang
23792f5a7c Fix hasUnread on HashtagTimeline (#4644) 2017-08-20 17:12:06 +02:00
Eugen Rochko
fe5b66aa08 Handle duplicate ActivityPub activities (#4639)
* Handle duplicate ActivityPub activities

Only perform side-effects when record processed for the first time

* Fast-forward repeat follow requests
2017-08-20 16:53:47 +02:00
Quent-in
93d4192a67 l10n update OC : Redesign public profiles (#4608) (#4646)
New strings added to be shown on the new profile page
2017-08-20 21:49:12 +09:00
takayamaki
d5acf4275f Improve about ja translation standalone.public_title (#4641) 2017-08-20 20:27:14 +09:00
Eugen Rochko
412ea87306 Improve ActivityPub/OStatus compatibility (#4632)
*Note: OStatus URIs are invalid for ActivityPub. But we have them for
as long as we want to keep old OStatus-sourced content and as long as
we remain OStatus-compatible.*

- In Announce handling, if object URI is not a URL, fallback to object URL
- Do not use specialized ThreadResolveWorker, rely on generalized handling
- When serializing notes, if parent's URI is not a URL, use parent's URL
2017-08-19 18:44:48 +02:00
Eugen Rochko
774b8661bc Revert #4616 (#4638) 2017-08-19 02:23:47 +02:00
Eugen Rochko
c7d2619ab1 Parse OStatus tag URIs in ActivityPub handlers when those are local (#4631) 2017-08-18 11:24:44 +02:00
Yamagishi Kazutoshi
2edfdab6e6 Don't send Link header when don't know prev and next links (#4633) 2017-08-18 10:42:59 +02:00
Eugen Rochko
4edf9d849f Make ActivityPub::TagManager#local_uri? recognize local URIs with ports (#4628) 2017-08-18 03:21:59 +02:00
Eugen Rochko
10489b4e4a If url attribute not present in Note, fallback to id attribute (#4629) 2017-08-18 02:29:12 +02:00
Eugen Rochko
40c45f5dd9 Put ActivityPub alternate link into Atom, prefer it when processing Atom (#4623) 2017-08-18 01:03:18 +02:00
nightpool
efec02f153 use existing inflections instead of custom helper (#4624)
* use existing inflections instead of custom helper

* use ActiveSupport versions
2017-08-17 23:20:50 +02:00
Eugen Rochko
116b8a6363 Fix #4607 - Accept/reject activities use FollowRequest, which has inverse relations (#4616) 2017-08-17 22:15:37 +02:00
Eugen Rochko
ad892dbc0c Add _:atomUri property for deduplicating OStatus/ActivityPub legacy records (#4593) 2017-08-17 21:35:00 +02:00
nullkal
075d6a1e13 Show what protocol is used for accounts in admin/accounts#index (#4622)
* Show what protocol used for in admin/accounts#index

* Add frozen_string_literal
2017-08-17 17:52:40 +02:00
nullkal
54a04e3658 Update charlock_holmes to 0.7.5 (#4620) 2017-08-17 14:46:53 +02:00
Naoki Kosaka
462c30e26c Update Japanese Translation. (Redesign public profiles) (#4612) 2017-08-17 06:19:37 +09:00
m4sk1n
2a04bdc87a i18n: Update Polish translation (#4613)
* i18n: Update Polish translation

* Update pl.json
2017-08-16 22:14:23 +02:00
Eugen Rochko
ca7ea1aba9 Redesign public profiles (#4608)
* Redesign public profiles

* Responsive design

* Change public profile status filtering defaults and add options

- No longer displays private/direct toots even if you are permitted access
- By default omits replies
- "With replies" option
- "Media only" option

* Redesign account grid cards

* Fix style issues
2017-08-16 17:12:58 +02:00
Clworld
f814661fca Make share intent modal to make "signed in as" shown. (#4611)
* Make share intent modal to make "signed in as" shown.

* fix glitch on mobile.
2017-08-16 16:48:44 +02:00
Quent-in
e33c28a6d8 Update ActivityPub (#4600) (#4609)
Update: new string + more translations for the time in words
2017-08-16 17:21:34 +09:00
abcang
e120d09c98 Fix require_user! behavior when not logged in (#4604) 2017-08-15 14:14:12 +02:00
Eugen Rochko
4fcbb1f838 Re-add missing transaction around status-from-OStatus creation (#4603) 2017-08-14 21:37:21 +02:00
unarist
a855956185 Fix ActivityPub follow interaction and add more specs (#4601) 2017-08-14 16:57:46 +02:00
unarist
5b9ae7981e Update /admin/accounts/:id view for ActivityPub (#4600)
* Add protocol field
* Switch protocol specific information according to active protocol
* Hide PuSH subscription related buttons if ActivityPub is active
2017-08-14 14:09:00 +02:00
Yamagishi Kazutoshi
5f22c0189d Add support for searching AP users (#4599)
* Add support for searching AP users

* use JsonLdHelper
2017-08-14 14:08:34 +02:00
Eugen Rochko
26d26644ac Require "inbox" to be set on actor to be ActivityPub-ready (#4595) 2017-08-14 11:27:25 +02:00
Eugen Rochko
3c6503038e Add protocol handler. Handle follow intents (#4511)
* Add protocol handler. Handle follow intents

* Add share intent

* Improve code in intents controller

* Adjust share form CSS
2017-08-14 04:53:31 +02:00
Yamagishi Kazutoshi
96e9ed13de Fix search (regression from #4589) (#4594) 2017-08-14 04:50:56 +02:00
Eugen Rochko
6df8bd277b Set correct content-type for ActivityPub JSON (#4592) 2017-08-14 04:16:43 +02:00
Eugen Rochko
4e75f0d889 Hook up URL-based resource look-up to ActivityPub (#4589) 2017-08-14 02:29:36 +02:00
Eugen Rochko
a2aeacbfee Add alternate links to ActivityPub resources from HTML/HEAD variants (#4586) 2017-08-13 00:45:04 +02:00
Eugen Rochko
b7370ac8ba ActivityPub delivery (#4566)
* Deliver ActivityPub Like

* Deliver ActivityPub Undo-Like

* Deliver ActivityPub Create/Announce activities

* Deliver ActivityPub creates from mentions

* Deliver ActivityPub Block/Undo-Block

* Deliver ActivityPub Accept/Reject-Follow

* Deliver ActivityPub Undo-Follow

* Deliver ActivityPub Follow

* Deliver ActivityPub Delete activities

Incidentally fix #889

* Adjust BatchedRemoveStatusService for ActivityPub

* Add tests for ActivityPub workers

* Add tests for FollowService

* Add tests for FavouriteService, UnfollowService and PostStatusService

* Add tests for ReblogService, BlockService, UnblockService, ProcessMentionsService

* Add tests for AuthorizeFollowService, RejectFollowService, RemoveStatusService

* Add tests for BatchedRemoveStatusService

* Deliver updates to a local account to ActivityPub followers

* Minor adjustments
2017-08-13 00:44:41 +02:00
Eugen Rochko
ccdd5a9576 Add serializing/unserializing of "locked" actor attribute (#4585) 2017-08-12 17:41:03 +02:00
Eugen Rochko
40be4ea239 Extend Devise remember_me longevity to 1 year instead of 2 weeks (#4587)
Force SSL only cookies for remember_me, adjust confirmation
expiration time to fit with the user cleanup scheduler
2017-08-12 16:30:59 +02:00
Eugen Rochko
3d47154c20 Only PuSH-resubscribe to OStatus accounts (#4583) 2017-08-12 02:54:54 +02:00
Sylvhem
d0a217eb92 Minor fixes in the French translation (#4580)
* Ajout de traductions manquantes

Ajoute des traductions pour les chaînes n’en ayant pas en version 1.5.1.

Add translations for the strings that are missing them in 1.5.1.

* Remplace « ' » par « ’ »

Retire de la traduction les apostrophes droites « ' » (U+0027) au profit des apostrophes typographiques « ’ » (U+2019).
En typographie française, les apostrophes typographiques sont utilisées à la place des apostrophes droites. La traduction était incohérente et utilisait les deux.

Remove from the translation all the vertical apostrophes (U+0027) in favor of the curly ones (U+2019).
In French typography, typographic apostrophes are used instead of vertical ones. The translation was incoherent and used both.

* Ajout d’espaces insécables

Ajoute des espaces insécables suivant les régles nécessaires en typographie française.

Add non-breaking spaces following rules of French typography.

* Remplace « status » par « statut »

Remplace le mot anglais « status » par sa traduction française « statut ».

Replace the English word "status" by its French translation "statut".

* Correction de la politique de confidentialité

Apporte diverses corrections à la traduction de la politique de confidentialité.

Add various fixes to the privacy policy's translation.

* Remplace « mentionné » par « mentionné·e »

Harmonise la traduction en remplaçant « mentionné » par sa forme épicène.

Harmonize the translation by replacing "mentionné" (sure) by its epicene form.

* Remplace « Coup d’œil » par « Jeter un coup d’œil… »

Remplace la première traduction par une forme plus proche de la version originelle.

Replace the first translation by something closer to the original version.

* Remplace « Bon Appétoot ! » par « Bon appouetit ! »

Remplace « Bon Appétoot ! » par « Bon appouetit ! » pour essayer de conserver le jeu de mot.

Replace « Bon Appétoot ! » by « Bon appouetit ! » to keep the pun.

* Remplace « Bon Appétoot ! » par « Bon appouetit ! » (2)

Remplace « Bon Appétoot ! » par « Bon appouetit ! » pour essayer de conserver le jeu de mot.

Replace « Bon Appétoot ! » by « Bon appouetit ! » to keep the pun.f

* Corrections

Corrige des fautes d’orthographe et change « appouetit » pour « appouétit ».

Correct some mistakes and change "appouetit" to "appouétit".
2017-08-12 01:33:30 +02:00
Eugen Rochko
81c1303cd6 Handle ActivityPub follows correctly (#4571)
* Handle ActivityPub follows correctly

ActivityPub follows are follow-requests. Always require an Accept.
If account is not locked, auto-accept.

* Handle ActivityPub Accept/Reject-Follow

* Fix wrong method

* Fix wrong class
2017-08-10 22:33:12 +02:00
Quent-in
4b8e4dca26 l10n Update OC #4521 (#4577)
* l10n Update OC #4521

Link => token
provider => provesidor
+ more generalized way of using present participle

* Update oc.yml
2017-08-10 22:15:26 +02:00
spla
10cdad3e7d Added new catalan strings (#4574)
* Add Catalan language

* Add Catalan language

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update ca.json

* Update settings_helper.rb

* Update mastodon.js

* Update index.js

* Update application.rb

* Update ca.yml

* removed extra spaces at line 225

* Catalan translation update

added activerecord.ca.yml

* Update activerecord.ca.yml

Done

* Updated activerecord.ca.yml

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Catalan language updated

* Update ca.json

Removed :

<<<<<<< HEAD
  "getting_started.support": "{faq} • {userguide} • {apps}",
=======
>>>>>>> upstream/master

* Syncing to master

* Added new Catalan strings

* removed config.secret_key line

* Corrected <sotrong> tag to <strong>

Line 515

* Removed extra line

* Reverted

* yarn.lock reverted
2017-08-10 21:52:40 +09:00
Yamagishi Kazutoshi
d9a1fb134a Fix emoji picker scrollbar style (#4572) 2017-08-10 13:41:12 +02:00
Eugen Rochko
fdea173237 Add Digest header to requests with body, handle acct and URI keyId (#4565) 2017-08-09 23:54:14 +02:00
m4sk1n
4e1bf082ce i18n: Improve admin panel translation (pl) (#4559)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-08-09 07:46:21 +09:00
Ondřej Hruška
b1c8a702a4 Add favourited toot to favourites column (#4562)
* Add faved toot to faves column

* renamed append to prepend for clarity
2017-08-09 00:22:26 +02:00
Ondřej Hruška
820099813f add scrollTop to ui/components/column (#4563) 2017-08-09 00:21:58 +02:00
Gergely Nagy
2ebe4ff568 api/instances: Include the stats from the /about/more page (#4074)
To be able to pull some basic statistics from a Mastodon instance, include the
user, status and connected domain counters in the `/api/v1/instance` response.

Fixes #3570.

Signed-off-by: Gergely Nagy <algernon@madhouse-project.org>
2017-08-08 22:18:12 +02:00
Ondřej Hruška
61bfce5aa9 add missing @ to the onboarding modal (#4560) 2017-08-08 22:13:04 +02:00
Eugen Rochko
dd7ef0dc41 Add ActivityPub inbox (#4216)
* Add ActivityPub inbox

* Handle ActivityPub deletes

* Handle ActivityPub creates

* Handle ActivityPub announces

* Stubs for handling all activities that need to be handled

* Add ActivityPub actor resolving

* Handle conversation URI passing in ActivityPub

* Handle content language in ActivityPub

* Send accept header when fetching actor, handle JSON parse errors

* Test for ActivityPub::FetchRemoteAccountService

* Handle public key and icon/image when embedded/as array/as resolvable URI

* Implement ActivityPub::FetchRemoteStatusService

* Add stubs for more interactions

* Undo activities implemented

* Handle out of order activities

* Hook up ActivityPub to ResolveRemoteAccountService, handle
Update Account activities

* Add fragment IDs to all transient activity serializers

* Add tests and fixes

* Add stubs for missing tests

* Add more tests

* Add more tests
2017-08-08 21:52:15 +02:00
Lynx Kotoura
dcbc1af38a Fix short description in about/more page (#4554) 2017-08-08 15:49:32 +02:00
Yamagishi Kazutoshi
81c41d8681 Add coalesce option to avatar and header convert processor (#4552)
Resolve #3199
2017-08-08 15:49:14 +02:00
雨宮美羽
ec3be87a2b improve zh-CN translations (#4557) 2017-08-08 15:48:19 +02:00
unarist
b42c018bb8 Add Content-Type header on throttled response to fix mojibake (#4558)
application/json only allows Unicode, so this prevents from wrong charset detection.
2017-08-08 15:47:35 +02:00
TheInventrix
c9fd6f386c unify short description styling (#4548)
apply same style class to the About description on both the landing page
and the about/more page
2017-08-08 01:50:15 +02:00
Yamagishi Kazutoshi
1b5d26735e Revert "Set false to animated options for thumbnail processor" (#4550)
* Revert "Adjust tags and accounts page (#4534)"

This reverts commit a3e53bd442.

* Revert "feat: Cache status height to avoid expensive renders (#4439)"

This reverts commit 8eb6d171e6.

* Revert "Refactor Avatar and AvatarOverlay to have 'account' as prop instead of src and staticSrc (#4526)"

This reverts commit 5942347407.

* Revert "Update dependencies for Ruby (#4543)"

This reverts commit 22db947225.

* Revert "[Docker] Add multicore support to "make" and "bundler" (#4544)"

This reverts commit 5d408fd9aa.

* Revert "It makes no sense to try using invalid or expired link again (#4521)"

This reverts commit 47579ec58c.

* Revert "i18n: Update Polish translation (#4545)"

This reverts commit 3363a05539.

* Revert "Set false to animated options for thumbnail processor (#4547)"

This reverts commit 87f10d476c.
2017-08-08 01:49:56 +02:00
Lynx Kotoura
a3e53bd442 Adjust tags and accounts page (#4534)
* Adjust tag and accounts page

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

* feat: Escape content and emojify in reducers

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

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

* fix broken tests

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

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

* Update tilt to version 2.0.8

* Update statsd-instrument to version 2.1.4

* Update sidekiq to version 5.0.4

* Update sidekiq-scheduler to version 2.1.8

* Update sidekiq-unique-jobs to version 5.0.9

* Update redis-activesupport to version 5.0.3

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

* Update pkg-config to version 1.2.4

* Update parallel_tests to version 2.14.2

* Update jsonapi-renderer to version 0.1.3

* Update i18n-tasks to version 0.9.16

* Update httplog to version 0.99.7

* Update fabrication to version 2.16.2

* Update bootsnap to version 1.1.2

* Update aws-sigv4 to version 1.0.1

* Update aws-sdk-core to version 2.10.21

* Update hashdiff to version 0.3.5

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

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

Fix the aspect ratio of animated GIF whose background is transparent.
2017-08-07 14:41:21 +02:00
Eugen Rochko
41c3389d76 Bump to 1.5.1 2017-08-06 23:53:25 +02:00
Masoud Abkenar
e7a5a188ef i18n: update Persian translation (#4540) 2017-08-06 23:50:20 +02:00
Komic
71384b2ef9 Fix #4531 (#4533)
wrong character, sorry about that~
2017-08-06 21:52:12 +09:00
Komic
d1d465347a fr.yml update (#4531)
Translation for https://github.com/tootsuite/mastodon/pull/4523
2017-08-06 17:36:10 +09:00
Daigo 3 Dango
5eba129b0f Translate 'Signed in as' into Japanese (#4530) 2017-08-06 03:14:11 +02:00
Quent-in
021a83ead4 l10n OC - Added #4523 "signed in as" (#4529)
Neutral form used.
2017-08-06 08:10:53 +09:00
MitarashiDango
5ee45fa571 fix columns_area.js (#4528) 2017-08-05 20:33:41 +02:00
Eugen Rochko
61a06eb328 Update goldfinger to 2.0.1, see tootsuite/goldfinger#5 (#4527) 2017-08-05 15:29:52 +02:00
Eugen Rochko
df605f0f8b Add "signed in as" header to some pages (#4523) 2017-08-05 04:24:58 +02:00
Quent-in
029786442a l10n update Occitan (#4522)
Salutation mailer + invalid reset link.
2017-08-05 01:31:27 +02:00
Akihiko Odaki
9d1f8b9d6a Scroll columns area to right when children property is changed (#4517)
The feature to pin column could hide the rightmost column, which is
specified with children property of ColumnsArea.

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

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

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

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

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

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

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

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

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

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

* fix(column_back_button): Use native button

* fix(media_gallery): Keyboard navigation

* fix(status_content): Make CW content focusable
2017-07-31 00:18:15 +02:00
Yamagishi Kazutoshi
970297a138 Change to sensitive when adding content warning from web UI (#4456) 2017-07-30 16:27:57 +02:00
Yamagishi Kazutoshi
29abc9438c Remove outline from focused toot (#4448)
* Remove outline from focused toot

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

To be able to copy long email address

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

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

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

* Fix autocomplete in registrations edit.html.haml

* Fix autocomplete in passwords edit.html.haml

* Fix autocomplete in _registration.html.haml

* Fix autocomplete in new.html.haml

* Fix autocomplete in show.html.haml

* Add autocomplete option to sessions new.html.haml

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

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

* fix(column_back_button): Keyboard accessible

* fix(status_content): Make focusable and accessible

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

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

* feat(icon_button): Add aria-expanded

* fix(privacy_dropdown): Use aria-expanded instead of aria-hidden
2017-07-29 01:58:53 +02:00
Eugen Rochko
53b2b1b238 Count all URLs in text as 23 characters flat, do not count domain part of usernames (#4427)
* Count all URLs in text as 23 characters flat, do not count domain part of usernames

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

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

* fix(tabs_bar): Do not use event.path
2017-07-28 22:55:13 +02:00
Sorin Davidoi
7e0c00a555 fix(mocha): Run all tests (#4433) 2017-07-28 22:53:16 +02:00
Eugen Rochko
f0bb2c6d1e Fix web push notifications "boost" icon not being loaded (regression from #4426) (#4431) 2017-07-28 20:26:54 +02:00
585 changed files with 14337 additions and 4209 deletions

View File

@@ -26,7 +26,7 @@ LOCAL_HTTPS=true
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=
@@ -36,7 +36,7 @@ OTP_SECRET=
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY=
@@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
@@ -98,6 +98,23 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST=

View File

@@ -49,6 +49,7 @@ rules:
- warn
- allow:
- error
- warn
no-fallthrough: error
no-irregular-whitespace: error
no-mixed-spaces-and-tabs: warn

View File

@@ -10,6 +10,7 @@ AllCops:
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
- 'lib/json_ld/*'
Bundler/OrderedGems:
Enabled: false
@@ -27,6 +28,7 @@ Metrics/AbcSize:
Max: 100
Metrics/BlockLength:
Max: 35
Exclude:
- 'lib/tasks/**/*'
@@ -35,10 +37,10 @@ Metrics/BlockNesting:
Metrics/ClassLength:
CountComments: false
Max: 200
Max: 300
Metrics/CyclomaticComplexity:
Max: 15
Max: 25
Metrics/LineLength:
AllowURI: true
@@ -53,11 +55,11 @@ Metrics/ModuleLength:
Max: 200
Metrics/ParameterLists:
Max: 4
Max: 5
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 10
Max: 20
Rails:
Enabled: true

View File

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

11
Aptfile
View File

@@ -1,7 +1,10 @@
protobuf-compiler
libprotobuf-dev
ffmpeg
libicu[0-9][0-9]
libicu-dev
libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
libicu-dev
libidn11-dev
protobuf-compiler

15
CODEOWNERS Normal file
View File

@@ -0,0 +1,15 @@
# CODEOWNERS for tootsuite/mastodon
# Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.
# /app/javascript/mastodon/locales/fr.json @żelipapą
# /app/views/user_mailer/*.fr.html.erb @żelipapą
# /app/views/user_mailer/*.fr.text.erb @żelipapą
# /config/locales/*.fr.yml @żelipapą
# /config/locales/fr.yml @żelipapą
/app/javascript/mastodon/locales/pl.json @m4sk1n
/app/views/user_mailer/*.pl.html.erb @m4sk1n
/app/views/user_mailer/*.pl.text.erb @m4sk1n
/config/locales/*.pl.yml @m4sk1n
/config/locales/pl.yml @m4sk1n

View File

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

13
Gemfile
View File

@@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.8'
gem 'rails', '~> 5.1.0'
gem 'puma', '~> 3.10'
gem 'rails', '~> 5.1.4'
gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2'
@@ -15,6 +15,7 @@ gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.9'
gem 'fog-openstack', '~> 0.1'
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
@@ -22,8 +23,9 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'browser'
gem 'charlock_holmes', '~> 0.7.3'
gem 'cld3', '~> 3.1'
gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'
@@ -68,6 +70,9 @@ gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0'
gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do
gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2'

View File

@@ -1,25 +1,25 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.2)
actionpack (= 5.1.2)
actioncable (5.1.4)
actionpack (= 5.1.4)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
actionmailer (5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.2)
actionview (= 5.1.2)
activesupport (= 5.1.2)
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (~> 0.6.3)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.2)
activesupport (= 5.1.2)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -30,22 +30,22 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4)
activejob (5.1.2)
activesupport (= 5.1.2)
activejob (5.1.4)
activesupport (= 5.1.4)
globalid (>= 0.3.6)
activemodel (5.1.2)
activesupport (= 5.1.2)
activerecord (5.1.2)
activemodel (= 5.1.2)
activesupport (= 5.1.2)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.2)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.2)
@@ -57,14 +57,14 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.6)
aws-sdk-resources (= 2.10.6)
aws-sdk-core (2.10.6)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.6)
aws-sdk-core (= 2.10.6)
aws-sigv4 (1.0.0)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
bcrypt (3.1.11)
better_errors (2.1.1)
coderay (>= 1.0.0)
@@ -72,15 +72,15 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.1.1)
bootsnap (1.1.2)
msgpack (~> 1.0)
brakeman (3.6.2)
brakeman (3.7.2)
browser (2.4.0)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0)
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
capistrano (3.8.2)
@@ -108,9 +108,9 @@ GEM
xpath (~> 2.0)
case_transform (0.2)
activesupport
charlock_holmes (0.7.3)
charlock_holmes (0.7.5)
chunky_png (1.3.8)
cld3 (3.1.3)
cld3 (3.2.0)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
@@ -154,18 +154,31 @@ GEM
erubis (2.7.0)
et-orbi (1.0.5)
tzinfo
excon (0.58.0)
execjs (2.7.0)
fabrication (2.16.1)
fabrication (2.16.2)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
ffi (1.9.18)
fog-core (1.45.0)
builder
excon (~> 0.58)
formatador (~> 0.2)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-openstack (0.1.21)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (0.4.0)
activesupport (>= 4.2.0)
goldfinger (2.0.0)
goldfinger (2.0.1)
addressable (~> 2.5)
http (~> 2.2)
nokogiri (~> 1.8)
@@ -179,7 +192,9 @@ GEM
activesupport (>= 4.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
hashdiff (0.3.4)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.5)
highline (1.7.8)
hiredis (0.6.1)
hkdf (0.3.0)
@@ -194,11 +209,11 @@ GEM
http-form_data (1.0.3)
http_accept_language (2.1.1)
http_parser.rb (0.6.0)
httplog (0.99.4)
httplog (0.99.7)
colorize
rack
i18n (0.8.4)
i18n-tasks (0.9.15)
i18n (0.8.6)
i18n-tasks (0.9.16)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -209,9 +224,18 @@ GEM
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jmespath (1.3.1)
json (2.1.0)
jsonapi-renderer (0.1.2)
json-ld (2.1.5)
multi_json (~> 1.12)
rdf (~> 2.2)
json-ld-preloaded (2.2.1)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
@@ -253,7 +277,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_portile2 (2.2.0)
minitest (5.10.2)
minitest (5.10.3)
msgpack (1.1.0)
multi_json (1.12.1)
net-scp (1.2.1)
@@ -264,7 +288,7 @@ GEM
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
oj (3.2.0)
oj (3.3.4)
openssl (2.0.4)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
@@ -283,14 +307,14 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.11.2)
parallel_tests (2.14.1)
parallel_tests (2.14.2)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.3)
pkg-config (1.2.4)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -298,8 +322,8 @@ GEM
slop (~> 3.4)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (2.0.5)
puma (3.9.1)
public_suffix (3.0.0)
puma (3.10.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
rabl (0.13.1)
@@ -310,20 +334,20 @@ GEM
rack-cors (0.4.1)
rack-protection (2.0.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
rails (5.1.2)
actioncable (= 5.1.2)
actionmailer (= 5.1.2)
actionpack (= 5.1.2)
actionview (= 5.1.2)
activejob (= 5.1.2)
activemodel (= 5.1.2)
activerecord (= 5.1.2)
activesupport (= 5.1.2)
bundler (>= 1.3.0, < 2.0)
railties (= 5.1.2)
rails (5.1.4)
actioncable (= 5.1.4)
actionmailer (= 5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
activemodel (= 5.1.4)
activerecord (= 5.1.4)
activesupport (= 5.1.4)
bundler (>= 1.3.0)
railties (= 5.1.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -337,23 +361,28 @@ GEM
rails-i18n (5.0.4)
i18n (~> 0.7)
railties (~> 5.0)
rails-settings-cached (0.6.5)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.1.2)
actionpack (= 5.1.2)
activesupport (= 5.1.2)
railties (5.1.4)
actionpack (= 5.1.4)
activesupport (= 5.1.4)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.0.0)
rdf (2.2.8)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
rdf (~> 2.0)
redis (3.3.3)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.2)
redis-activesupport (5.0.3)
activesupport (>= 3, < 6)
redis-store (~> 1.3.0)
redis-namespace (1.5.3)
@@ -413,7 +442,7 @@ GEM
scss_lint (0.54.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sidekiq (5.0.3)
sidekiq (5.0.4)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
@@ -421,12 +450,12 @@ GEM
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.7)
sidekiq-scheduler (2.1.8)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.8)
sidekiq-unique-jobs (5.0.9)
sidekiq (>= 4.0, <= 6.0)
thor (~> 0)
simple-navigation (4.0.5)
@@ -443,22 +472,22 @@ GEM
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.0)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.13.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-instrument (2.1.2)
statsd-instrument (2.1.4)
temple (0.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4)
thor (0.20.0)
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.7)
twitter-text (1.14.6)
tilt (2.0.8)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
@@ -511,8 +540,8 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 2.14)
charlock_holmes (~> 0.7.3)
cld3 (~> 3.1)
charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
devise (~> 4.2)
devise-two-factor (~> 3.0)
@@ -521,6 +550,7 @@ DEPENDENCIES
fabrication (~> 2.16)
faker (~> 1.7)
fast_blank (~> 1.0)
fog-openstack (~> 0.1)
fuubar (~> 2.2)
goldfinger (~> 2.0)
hamlit-rails (~> 0.2)
@@ -531,6 +561,8 @@ DEPENDENCIES
httplog (~> 0.99)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
@@ -550,16 +582,17 @@ DEPENDENCIES
pghero (~> 1.7)
pkg-config (~> 1.2)
pry-rails (~> 0.3)
puma (~> 3.8)
puma (~> 3.10)
pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
rails (~> 5.1.0)
rails (~> 5.1.4)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3.1)
redis (~> 3.3)
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
@@ -590,4 +623,4 @@ RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH
1.15.2
1.15.4

View File

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

View File

@@ -7,24 +7,78 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
@pinned_statuses = []
if current_account && @account.blocking?(current_account)
@statuses = []
return
end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty?
end
format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end
format.json do
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end
private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
end
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested?
end
end
def default_statuses
@account.statuses.where(visibility: [:public, :unlisted])
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end
def no_replies_scope
Status.without_replies
end
def set_account
@account = Account.find_local!(params[:username])
end
def next_url
if media_requested?
short_account_media_url(@account, max_id: @statuses.last.id)
elsif replies_requested?
short_account_with_replies_url(@account, max_id: @statuses.last.id)
else
short_account_url(@account, max_id: @statuses.last.id)
end
end
def media_requested?
request.path.ends_with?('/media')
end
def replies_requested?
request.path.ends_with?('/with_replies')
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification
before_action :set_account
def create
if signed_request_account
upgrade_account
process_payload
head 201
else
head 202
end
end
private
def set_account
@account = Account.find_local!(params[:account_username]) if params[:account_username]
end
def body
@body ||= request.body.read
end
def upgrade_account
if signed_request_account.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'))
end
end

View File

@@ -7,7 +7,7 @@ class ActivityPub::OutboxesController < Api::BaseController
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private

View File

@@ -17,7 +17,7 @@ module Admin
end
def unsubscribe
UnsubscribeService.new.call(@account)
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end

View File

@@ -14,8 +14,12 @@ module Admin
private
def filtered_instances
InstanceFilter.new(filter_params).results
end
def paginated_instances
Account.remote.by_domain_accounts.page(params[:page])
filtered_instances.page(params[:page])
end
helper_method :paginated_instances
@@ -27,5 +31,11 @@ module Admin
def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain])
end
def filter_params
params.permit(
:domain_name
)
end
end
end

View File

@@ -13,6 +13,8 @@ module Admin
closed_registrations_message
open_deletion
timeline_preview
bootstrap_timeline_accounts
thumbnail
).freeze
BOOLEAN_SETTINGS = %w(
@@ -21,14 +23,23 @@ module Admin
timeline_preview
).freeze
UPLOAD_SETTINGS = %w(
thumbnail
).freeze
def edit
@admin_settings = Form::AdminSettings.new
end
def update
settings_params.each do |key, value|
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
upload.update(file: value)
else
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
end
end
flash[:notice] = I18n.t('generic.changes_saved_msg')

View File

@@ -9,7 +9,7 @@ module Admin
before_action :set_account
before_action :set_status, only: [:update, :destroy]
PAR_PAGE = 20
PER_PAGE = 20
def index
@statuses = @account.statuses
@@ -17,7 +17,7 @@ module Admin
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PAR_PAGE)
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end

View File

@@ -43,7 +43,7 @@ class Api::BaseController < ApplicationController
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links)
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def limit_param(default_limit)
@@ -62,10 +62,11 @@ class Api::BaseController < ApplicationController
end
def require_user!
current_resource_owner
set_user_activity
rescue ActiveRecord::RecordNotFound
render json: { error: 'This method requires an authenticated user' }, status: 422
if current_user
set_user_activity
else
render json: { error: 'This method requires an authenticated user' }, status: 422
end
end
def render_empty

View File

@@ -4,14 +4,14 @@ class Api::OEmbedController < Api::BaseController
respond_to :json
def show
@stream_entry = find_stream_entry.stream_entry
render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
@status = status_finder.status
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
end
private
def find_stream_entry
StreamEntryFinder.new(params[:url])
def status_finder
StatusFinder.new(params[:url])
end
def maxwidth_or_default

View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V1::Accounts::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, except: [:update]
before_action -> { doorkeeper_authorize! :write }, only: [:update]
before_action :require_user!
@@ -10,8 +11,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
end
def update
current_account.update!(account_params)
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end

View File

@@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if params[:only_media]
statuses.merge!(pinned_scope) if params[:pinned]
statuses.merge!(no_replies_scope) if params[:exclude_replies]
end
end
@@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end
def pinned_scope
@account.pinned_statuses
end
def no_replies_scope
Status.without_replies
end

View File

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

View File

@@ -10,6 +10,12 @@ class Api::V1::FollowsController < Api::BaseController
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
class Api::V1::Statuses::PinsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
before_action :set_status
respond_to :json
def create
StatusPin.create!(account: current_account, status: @status)
render json: @status, serializer: REST::StatusSerializer
end
def destroy
pin = StatusPin.find_by(account: current_account, status: @status)
pin&.destroy!
render json: @status, serializer: REST::StatusSerializer
end
private
def set_status
@status = Status.find(params[:status_id])
end
end

View File

@@ -29,7 +29,7 @@ class Api::V1::StatusesController < Api::BaseController
end
def card
@card = PreviewCard.find_by(status: @status)
@card = @status.preview_cards.first
if @card.nil?
render_empty

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Api::Web::EmbedsController < Api::BaseController
respond_to :json
before_action :require_user!
def create
status = StatusFinder.new(params[:url]).status
render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound
oembed = OEmbed::Providers.get(params[:url])
render json: Oj.dump(oembed.fields)
rescue OEmbed::NotFound
render json: {}, status: :not_found
end
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ module AccountControllerConcern
[
webfinger_account_link,
atom_account_url_link,
actor_url_link,
]
)
end
@@ -41,6 +42,13 @@ module AccountControllerConcern
]
end
def actor_url_link
[
ActivityPub::TagManager.instance.uri_for(@account),
[%w(rel alternate), %w(type application/activity+json)],
]
end
def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s)
end

View File

@@ -31,7 +31,7 @@ module SignatureVerification
return
end
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
account = account_from_key_id(signature_params['keyId'])
if account.nil?
@signed_request_account = nil
@@ -49,6 +49,10 @@ module SignatureVerification
end
end
def request_body
@request_body ||= request.raw_post
end
private
def build_signed_string(signed_headers)
@@ -57,6 +61,8 @@ module SignatureVerification
signed_headers.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == 'digest'
"digest: #{body_digest}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
@@ -73,6 +79,10 @@ module SignatureVerification
(Time.now.utc - time_sent).abs <= 30
end
def body_digest
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
end
def to_header_name(name)
name.split(/-/).map(&:capitalize).join('-')
end
@@ -81,7 +91,16 @@ module SignatureVerification
signature_params['keyId'].blank? ||
signature_params['signature'].blank? ||
signature_params['algorithm'].blank? ||
signature_params['algorithm'] != 'rsa-sha256' ||
!signature_params['keyId'].start_with?('acct:')
signature_params['algorithm'] != 'rsa-sha256'
end
def account_from_key_id(key_id)
if key_id.start_with?('acct:')
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
account
end
end
end

View File

@@ -10,7 +10,7 @@ class FollowerAccountsController < ApplicationController
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end

View File

@@ -10,7 +10,7 @@ class FollowingAccountsController < ApplicationController
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end

View File

@@ -11,7 +11,30 @@ class HomeController < ApplicationController
private
def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
redirect_to(default_redirect_path)
end
def set_initial_state_json
@@ -28,4 +51,14 @@ class HomeController < ApplicationController
admin: Account.find_local(Setting.site_contact_username),
}
end
def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.first)
else
about_path
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
class IntentsController < ApplicationController
def show
uri = Addressable::URI.parse(params[:uri])
if uri.scheme == 'web+mastodon'
case uri.host
when 'follow'
return redirect_to authorize_follow_path(acct: uri.query_values['uri'].gsub(/\Aacct:/, ''))
when 'share'
return redirect_to share_path(text: uri.query_values['text'])
end
end
not_found
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
class MediaProxyController < ApplicationController
include RoutingHelper
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id])
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
end
redirect_to full_asset_url(@media_attachment.file.url(version))
end
private
def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url
@media_attachment.touch(:created_at)
@media_attachment.save!
end
def version
if request.path.ends_with?('/small')
:small
else
:original
end
end
def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
end
def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
end
end

View File

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

View File

@@ -0,0 +1,72 @@
# frozen_string_literal: true
class Settings::ApplicationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
before_action :prepare_scopes, only: [:create, :update]
def index
@applications = current_user.applications.page(params[:page])
end
def new
@application = Doorkeeper::Application.new(
redirect_uri: Doorkeeper.configuration.native_redirect_uri,
scopes: 'read write follow'
)
end
def show; end
def create
@application = current_user.applications.build(application_params)
if @application.save
redirect_to settings_applications_path, notice: I18n.t('applications.created')
else
render :new
end
end
def update
if @application.update(application_params)
redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show
end
end
def destroy
@application.destroy
redirect_to settings_applications_path, notice: I18n.t('applications.destroyed')
end
def regenerate
@access_token = current_user.token_for_app(@application)
@access_token.destroy
redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated')
end
private
def set_application
@application = current_user.applications.find(params[:id])
end
def application_params
params.require(:doorkeeper_application).permit(
:name,
:redirect_uri,
:scopes,
:website
)
end
def prepare_scopes
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array
end
end

View File

@@ -14,7 +14,8 @@ class Settings::ProfilesController < ApplicationController
def show; end
def update
if @account.update(account_params)
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
class SharesController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
private
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
text: params[:text],
}
end
def set_body_classes
@body_classes = 'compose-standalone'
end
end

View File

@@ -9,6 +9,7 @@ class StatusesController < ApplicationController
before_action :set_status
before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
def show
respond_to do |format|
@@ -20,13 +21,18 @@ class StatusesController < ApplicationController
end
format.json do
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end
def activity
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
def embed
response.headers['X-Frame-Options'] = 'ALLOWALL'
render 'stream_entries/embed', layout: 'embedded'
end
private
@@ -36,7 +42,12 @@ class StatusesController < ApplicationController
end
def set_link_headers
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]])
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
]
)
end
def set_status
@@ -53,4 +64,8 @@ class StatusesController < ApplicationController
def check_account_suspension
gone if @account.suspended?
end
def redirect_to_original
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
end

View File

@@ -25,10 +25,7 @@ class StreamEntriesController < ApplicationController
end
def embed
response.headers['X-Frame-Options'] = 'ALLOWALL'
return gone if @stream_entry.activity.nil?
render layout: 'embedded'
redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301
end
private
@@ -38,7 +35,12 @@ class StreamEntriesController < ApplicationController
end
def set_link_headers
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]])
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
]
)
end
def set_stream_entry

View File

@@ -12,7 +12,7 @@ class TagsController < ApplicationController
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end

View File

@@ -5,6 +5,10 @@ module ApplicationHelper
current_page?(path) ? 'active' : ''
end
def active_link_to(label, path, options = {})
link_to label, path, options.merge(class: active_nav_class(path))
end
def show_landing_strip?
!user_signed_in? && !single_user_mode?
end
@@ -38,4 +42,8 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
end

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
module JsonLdHelper
def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
def first_of_value(value)
value.is_a?(Array) ? value.first : value
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end
def supported_context?(json)
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
graph.dump(:normalize)
end
def fetch_resource(uri)
response = build_request(uri).perform
return if response.code != 200
body_to_json(response.to_s)
end
def body_to_json(body)
body.is_a?(String) ? Oj.load(body, mode: :strict) : body
rescue Oj::ParseError
nil
end
def merge_context(context, new_context)
if context.is_a?(Array)
context << new_context
else
[context, new_context]
end
end
private
def build_request(uri)
request = Request.new(:get, uri)
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
request
end
end

View File

@@ -12,6 +12,14 @@ module RoutingHelper
end
def full_asset_url(source, options = {})
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source, options)).to_s
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s
end
private
def use_storage?
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
end
end

View File

@@ -30,6 +30,7 @@ module SettingsHelper
th: 'ภาษาไทย',
tr: 'Türkçe',
uk: 'Українська',
zh: '中文',
'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)',
@@ -39,6 +40,10 @@ module SettingsHelper
HUMAN_LOCALES[locale]
end
def filterable_languages
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
end
def hash_to_object(hash)
HashObject.new(hash)
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
module StreamEntriesHelper
EMBEDDED_CONTROLLER = 'stream_entries'
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'
def display_name(account)

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -1,6 +1,11 @@
import api from '../api';
import { updateTimeline } from './timelines';
import {
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
@@ -95,16 +100,20 @@ export function submitCompose() {
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
dispatch(updateTimeline('home', { ...response.data }));
const insertOrRefresh = (timelineId, refreshAction) => {
if (getState().getIn(['timelines', timelineId, 'online'])) {
dispatch(updateTimeline(timelineId, { ...response.data }));
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
dispatch(refreshAction());
}
};
insertOrRefresh('home', refreshHomeTimeline);
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
if (getState().getIn(['timelines', 'community', 'loaded'])) {
dispatch(updateTimeline('community', { ...response.data }));
}
if (getState().getIn(['timelines', 'public', 'loaded'])) {
dispatch(updateTimeline('public', { ...response.data }));
}
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));

View File

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

View File

@@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
export const UNPIN_REQUEST = 'UNPIN_REQUEST';
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
export const UNPIN_FAIL = 'UNPIN_FAIL';
export function reblog(status) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
@@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) {
error,
};
};
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
dispatch(pinSuccess(status, response.data));
}).catch(error => {
dispatch(pinFail(status, error));
});
};
};
export function pinRequest(status) {
return {
type: PIN_REQUEST,
status,
};
};
export function pinSuccess(status, response) {
return {
type: PIN_SUCCESS,
status,
response,
};
};
export function pinFail(status, error) {
return {
type: PIN_FAIL,
status,
error,
};
};
export function unpin (status) {
return (dispatch, getState) => {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
dispatch(unpinSuccess(status, response.data));
}).catch(error => {
dispatch(unpinFail(status, error));
});
};
};
export function unpinRequest(status) {
return {
type: UNPIN_REQUEST,
status,
};
};
export function unpinSuccess(status, response) {
return {
type: UNPIN_SUCCESS,
status,
response,
};
};
export function unpinFail(status, error) {
return {
type: UNPIN_FAIL,
status,
error,
};
};

View File

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

View File

@@ -0,0 +1,94 @@
import createStream from '../stream';
import {
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
connectTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, refreshNotifications } from './notifications';
import { getLocale } from '../locales';
const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
return (dispatch, getState) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']);
const locale = getState().getIn(['meta', 'locale']);
let polling = null;
const setupPolling = () => {
polling = setInterval(() => {
pollingRefresh(dispatch);
}, 20000);
};
const clearPolling = () => {
if (polling) {
clearInterval(polling);
polling = null;
}
};
const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
connected () {
if (pollingRefresh) {
clearPolling();
}
dispatch(connectTimeline(timelineId));
},
disconnected () {
if (pollingRefresh) {
setupPolling();
}
dispatch(disconnectTimeline(timelineId));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
reconnected () {
if (pollingRefresh) {
clearPolling();
pollingRefresh(dispatch);
}
dispatch(connectTimeline(timelineId));
},
});
const disconnect = () => {
if (subscription) {
subscription.close();
}
clearPolling();
};
return disconnect;
};
}
function refreshHomeTimelineAndNotification (dispatch) {
dispatch(refreshHomeTimeline());
dispatch(refreshNotifications());
}
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);

View File

@@ -26,6 +26,7 @@ export default class Account extends ImmutablePureComponent {
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
};
handleFollow = () => {
@@ -41,12 +42,21 @@ export default class Account extends ImmutablePureComponent {
}
render () {
const { account, me, intl } = this.props;
const { account, me, intl, hidden } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<div>
{account.get('display_name')}
{account.get('username')}
</div>
);
}
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
@@ -70,7 +80,7 @@ export default class Account extends ImmutablePureComponent {
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Permalink>

View File

@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
export default class Avatar extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
staticSrc: PropTypes.string,
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number.isRequired,
style: PropTypes.object,
animate: PropTypes.bool,
@@ -33,9 +33,12 @@ export default class Avatar extends React.PureComponent {
}
render () {
const { src, size, staticSrc, animate, inline } = this.props;
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
const src = account.get('avatar');
const staticSrc = account.get('avatar_static');
let className = 'account__avatar';
if (inline) {

View File

@@ -1,22 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
export default class AvatarOverlay extends React.PureComponent {
static propTypes = {
staticSrc: PropTypes.string.isRequired,
overlaySrc: PropTypes.string.isRequired,
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
};
render() {
const { staticSrc, overlaySrc } = this.props;
const { account, friend } = this.props;
const baseStyle = {
backgroundImage: `url(${staticSrc})`,
backgroundImage: `url(${account.get('avatar_static')})`,
};
const overlayStyle = {
backgroundImage: `url(${overlaySrc})`,
backgroundImage: `url(${friend.get('avatar_static')})`,
};
return (

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import detectPassiveEvents from 'detect-passive-events';
import scrollTop from '../scroll';
import { scrollTop } from '../scroll';
export default class Column extends React.PureComponent {
@@ -32,7 +32,7 @@ export default class Column extends React.PureComponent {
}
componentDidMount () {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents ? { passive: true } : false);
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
componentWillUnmount () {

View File

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

View File

@@ -1,7 +1,5 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../emoji';
export default class DisplayName extends React.PureComponent {
@@ -10,12 +8,11 @@ export default 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)) };
const displayNameHtml = { __html: this.props.account.get('display_name_html') };
return (
<span className='display-name'>
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
</span>
);
}

View File

@@ -134,7 +134,7 @@ export default class DropdownMenu extends React.PureComponent {
return (
<Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-pressed={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>

View File

@@ -13,6 +13,7 @@ export default class IconButton extends React.PureComponent {
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
@@ -77,6 +78,7 @@ export default class IconButton extends React.PureComponent {
<button
aria-label={this.props.title}
aria-pressed={this.props.pressed}
aria-expanded={this.props.expanded}
title={this.props.title}
className={classes.join(' ')}
onClick={this.handleClick}

View File

@@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class IntersectionObserverArticle extends ImmutablePureComponent {
static propTypes = {
intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node,
};
state = {
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidMount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.unobserve(id, this.node);
this.componentMounted = false;
}
handleIntersection = (entry) => {
const { onHeightChange, saveHeightKey, id } = this.props;
if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height;
if (onHeightChange && saveHeightKey) {
onHeightChange(saveHeightKey, id, this.height);
}
}
this.setState((prevState) => {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) {
return;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
render () {
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
ref={this.handleRef}
aria-posinset={index}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
{children && React.cloneElement(children, { hidden: false })}
</article>
);
}
}

View File

@@ -17,7 +17,7 @@ export default class LoadMore extends React.PureComponent {
const { visible } = this.props;
return (
<button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}>
<button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);

View File

@@ -119,8 +119,8 @@ class Item extends React.PureComponent {
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
thumbnail = (
<a
@@ -212,10 +212,10 @@ export default class MediaGallery extends React.PureComponent {
}
children = (
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
<button 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>
</button>
);
} else {
const size = media.take(4).size;

View File

@@ -0,0 +1,215 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
children: PropTypes.node,
};
static defaultProps = {
trackScroll: true,
};
state = {
lastMouseMove: null,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 150, {
trailing: true,
});
handleMouseMove = throttle(() => {
this._lastMouseMove = new Date();
}, 300);
handleMouseLeave = () => {
this._lastMouseMove = null;
}
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
}
componentDidUpdate (prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) {
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
});
}
detachIntersectionObserver () {
this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
}
getFirstChildKey (props) {
const { children } = props;
let firstChild = children;
if (children instanceof ImmutableList) {
firstChild = children.get(0);
} else if (Array.isArray(children)) {
firstChild = children[0];
}
return firstChild && firstChild.key;
}
setRef = (c) => {
this.node = c;
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
}
_recentlyMoved () {
return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600);
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
const childrenCount = React.Children.count(children);
const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
let scrollableArea = null;
if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
{prepend}
{React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticleContainer
key={child.key}
id={child.key}
index={index}
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
>
{child}
</IntersectionObserverArticleContainer>
))}
{loadMore}
</div>
</div>
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
</div>
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
}
}

View File

@@ -8,16 +8,12 @@ import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl';
import emojify from '../emoji';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
import { MediaGallery, Video } from '../features/ui/util/async-components';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class Status extends ImmutablePureComponent {
@@ -28,27 +24,25 @@ export default class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
wrapped: PropTypes.bool,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
me: PropTypes.number,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hidden: PropTypes.bool,
};
state = {
isExpanded: false,
isIntersecting: true, // assume intersecting until told otherwise
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
// Avoid checking props that are functions (and whose equality will always
@@ -56,87 +50,15 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
'wrapped',
'me',
'boostModal',
'autoPlayGif',
'muted',
'listLength',
'hidden',
]
updateOnStates = ['isExpanded']
shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidMount () {
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
return;
}
this.props.intersectionObserverWrapper.observe(
this.props.id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
if (this.props.intersectionObserverWrapper) {
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
}
this.componentMounted = false;
}
handleIntersection = (entry) => {
if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height;
}
this.setState((prevState) => {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) {
return;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
handleClick = () => {
if (!this.context.router) {
return;
@@ -166,46 +88,42 @@ export default class Status extends ImmutablePureComponent {
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
}
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () {
let media = null;
let statusAvatar;
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
const { status, account, hidden, ...other } = this.props;
const { isExpanded } = this.state;
if (status === null) {
return null;
}
if (!isIntersecting && isHidden) {
if (hidden) {
return (
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
<div>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</article>
</div>
);
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
let displayName = status.getIn(['account', 'display_name']);
if (displayName.length === 0) {
displayName = status.getIn(['account', 'username']);
}
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
return (
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
<div className='status__wrapper' data-id={status.get('id')} >
<div className='status__prepend'>
<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} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
</div>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
</article>
<Status {...other} status={status.get('reblog')} account={status.get('account')} />
</div>
);
}
@@ -213,9 +131,18 @@ export default class Status extends ImmutablePureComponent {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} >
{Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />}
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => <Component
preview={video.get('preview_url')}
src={video.get('url')}
width={239}
height={110}
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
/>}
</Bundle>
);
} else {
@@ -228,13 +155,13 @@ export default class Status extends ImmutablePureComponent {
}
if (account === undefined || account === null) {
statusAvatar = <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />;
statusAvatar = <Avatar account={status.get('account')} size={48} />;
}else{
statusAvatar = <AvatarOverlay staticSrc={status.getIn(['account', 'avatar_static'])} overlaySrc={account.get('avatar_static')} />;
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
return (
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex='0' ref={this.handleRef}>
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@@ -252,7 +179,7 @@ export default class Status extends ImmutablePureComponent {
{media}
<StatusActionBar {...this.props} />
</article>
</div>
);
}

View File

@@ -21,6 +21,9 @@ const messages = defineMessages({
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
});
@injectIntl
@@ -40,7 +43,9 @@ export default class StatusActionBar extends ImmutablePureComponent {
onMute: PropTypes.func,
onBlock: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
me: PropTypes.number,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
@@ -77,6 +82,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.props.onDelete(this.props.status);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
@@ -93,6 +102,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
handleReport = () => {
this.props.onReport(this.props.status);
}
@@ -103,9 +116,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
render () {
const { status, me, intl, withDismiss } = this.props;
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
let menu = [];
let reblogIcon = 'retweet';
@@ -113,14 +127,23 @@ export default class StatusActionBar extends ImmutablePureComponent {
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
menu.push(null);
if (withDismiss) {
if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
@@ -151,8 +174,8 @@ export default class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>

View File

@@ -1,8 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
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';
@@ -119,8 +117,8 @@ export default class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const content = { __html: emojify(status.get('content')) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
@@ -146,7 +144,7 @@ export default class StatusContent extends React.PureComponent {
}
return (
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className={classNames} ref={this.setRef} tabIndex='0' aria-label={status.get('search_index')} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
@@ -155,13 +153,15 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder}
<div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
</div>
);
} else if (this.props.onClick) {
return (
<div
ref={this.setRef}
tabIndex='0'
aria-label={status.get('search_index')}
className={classNames}
style={directionStyle}
onMouseDown={this.handleMouseDown}
@@ -172,6 +172,8 @@ export default class StatusContent extends React.PureComponent {
} else {
return (
<div
tabIndex='0'
aria-label={status.get('search_index')}
ref={this.setRef}
className='status__content'
style={directionStyle}

View File

@@ -1,12 +1,9 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import ScrollableList from './scrollable_list';
export default class StatusList extends ImmutablePureComponent {
@@ -28,145 +25,21 @@ export default class StatusList extends ImmutablePureComponent {
trackScroll: true,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 150, {
trailing: true,
});
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
}
componentDidUpdate (prevProps) {
// Reset the scroll position when a new toot comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
}
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
});
}
detachIntersectionObserver () {
this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
}
setRef = (c) => {
this.node = c;
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].nextElementSibling;
case 'PageUp':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
const { statusIds, ...other } = this.props;
const { isLoading } = other;
const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />;
let scrollableArea = null;
const scrollableContent = (isLoading || statusIds.size > 0) ? (
statusIds.map((statusId) => (
<StatusContainer key={statusId} id={statusId} />
))
) : null;
if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={this.setRef}>
<div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
{prepend}
{statusIds.map((statusId, index) => {
return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
})}
{loadMore}
</div>
</div>
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
</div>
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
return (
<ScrollableList {...other}>
{scrollableContent}
</ScrollableList>
);
}
}

View File

@@ -146,29 +146,29 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
<button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<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>
</button>
);
} else {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
<button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<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>
</button>
);
}
}
if (this.state.preview && !autoplay) {
return (
<div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
<button className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
{spoilerButton}
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
</div>
</button>
);
}

View File

@@ -32,7 +32,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,

View File

@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import Card from '../features/status/components/card';
import { fromJS } from 'immutable';
export default class CardContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string,
card: PropTypes.array.isRequired,
};
render () {
const { card, ...props } = this.props;
return <Card card={fromJS(card)} {...props} />;
}
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Compose from '../features/standalone/compose';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState));
}
export default class TimelineContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<Compose />
</Provider>
</IntlProvider>
);
}
}

View File

@@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import IntersectionObserverArticle from '../components/intersection_observer_article';
import { setHeight } from '../actions/height_cache';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
});
const mapDispatchToProps = (dispatch) => ({
onHeightChange (key, id, height) {
dispatch(setHeight(key, id, height));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);

View File

@@ -2,21 +2,13 @@ import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import {
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
connectTimeline,
disconnectTimeline,
} from '../actions/timelines';
import { showOnboardingOnce } from '../actions/onboarding';
import { updateNotifications, refreshNotifications } from '../actions/notifications';
import BrowserRouter from 'react-router-dom/BrowserRouter';
import Route from 'react-router-dom/Route';
import ScrollContext from 'react-router-scroll/lib/ScrollBehaviorContext';
import UI from '../features/ui';
import { hydrateStore } from '../actions/store';
import createStream from '../stream';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
const { localeData, messages } = getLocale();
@@ -33,74 +25,28 @@ export default class Mastodon extends React.PureComponent {
};
componentDidMount() {
const { locale } = this.props;
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = store.getState().getIn(['meta', 'access_token']);
const setupPolling = () => {
this.polling = setInterval(() => {
store.dispatch(refreshHomeTimeline());
store.dispatch(refreshNotifications());
}, 20000);
};
const clearPolling = () => {
clearInterval(this.polling);
this.polling = undefined;
};
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
connected () {
clearPolling();
store.dispatch(connectTimeline('home'));
},
disconnected () {
setupPolling();
store.dispatch(disconnectTimeline('home'));
},
received (data) {
switch(data.event) {
case 'update':
store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
break;
case 'delete':
store.dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
store.dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
reconnected () {
clearPolling();
store.dispatch(connectTimeline('home'));
store.dispatch(refreshHomeTimeline());
store.dispatch(refreshNotifications());
},
});
this.disconnect = store.dispatch(connectUserStream());
// Desktop notifications
// Ask after 1 minute
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
Notification.requestPermission();
window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
}
// Protocol handler
// Ask after 5 minutes
if (typeof navigator.registerProtocolHandler !== 'undefined') {
const handlerUrl = window.location.protocol + '//' + window.location.host + '/intent?uri=%s';
window.setTimeout(() => navigator.registerProtocolHandler('web+mastodon', handlerUrl, 'Mastodon'), 5 * 60 * 1000);
}
store.dispatch(showOnboardingOnce());
}
componentWillUnmount () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import MediaGallery from '../components/media_gallery';
import { fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class MediaGalleryContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
media: PropTypes.array.isRequired,
};
handleOpenMedia = () => {}
render () {
const { locale, media, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<MediaGallery
{...props}
media={fromJS(media)}
onOpenMedia={this.handleOpenMedia}
/>
</IntlProvider>
);
}
}

View File

@@ -11,6 +11,8 @@ import {
favourite,
unreblog,
unfavourite,
pin,
unpin,
} from '../actions/interactions';
import {
blockAccount,
@@ -72,6 +74,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
} else {
dispatch(pin(status));
}
},
onEmbed (status) {
dispatch(openModal('EMBED', { url: status.get('url') }));
},
onDelete (status) {
if (!this.deleteModal) {
dispatch(deleteStatus(status.get('id')));

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Video from '../features/video';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class VideoContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Video {...props} />
</IntlProvider>
);
}
}

View File

@@ -3,32 +3,28 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping));
function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
// and replacing valid unicode strings
// that _aren't_ within tags with an <img> version.
// The goal is to be the same as an emojione.regUnicode replacement, but faster.
let i = -1;
let insideTag = false;
let match;
while (++i < str.length) {
const char = str.charAt(i);
if (insideTag && char === '>') {
insideTag = false;
} else if (char === '<') {
insideTag = true;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in unicodeMapping) {
const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
}
const emojify = str => {
let rtn = '';
for (;;) {
let match, i = 0;
while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
if (i === str.length)
break;
else if (str[i] === '<') {
let tagend = str.indexOf('>', i + 1) + 1;
if (!tagend)
break;
rtn += str.slice(0, tagend);
str = str.slice(tagend);
} else {
const [filename, shortCode] = unicodeMapping[match];
rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.slice(i + match.length);
}
}
return str;
}
return rtn + str;
};
export default emojify;

View File

@@ -4,8 +4,10 @@
const emojione = require('emojione');
const mappedUnicode = emojione.mapUnicodeToShort();
const excluded = ['®', '©', '™'];
module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap)
.filter(c => !excluded.includes(c))
.map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
.map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] }))
.reduce((x, y) => Object.assign(x, y), { });

View File

@@ -1,8 +1,6 @@
import React from 'react';
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';
import IconButton from '../../../components/icon_button';
import Motion from 'react-motion/lib/Motion';
@@ -13,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
});
const makeMapStateToProps = () => {
@@ -92,15 +90,10 @@ export default class Header extends ImmutablePureComponent {
return null;
}
let displayName = account.get('display_name');
let info = '';
let actionBtn = '';
let lockedIcon = '';
if (displayName.length === 0) {
displayName = account.get('username');
}
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
}
@@ -109,7 +102,7 @@ export default class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'requested'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
<IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />
</div>
);
} else if (!account.getIn(['relationship', 'blocking'])) {
@@ -125,15 +118,15 @@ export default class Header extends ImmutablePureComponent {
lockedIcon = <i className='fa fa-lock' />;
}
const content = { __html: emojify(account.get('note')) };
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
<div className='account__header__content' dangerouslySetInnerHTML={content} />

View File

@@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,

View File

@@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
import {
refreshCommunityTimeline,
expandCommunityTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
import { connectCommunityStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
@@ -23,8 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@@ -35,8 +29,6 @@ export default class CommunityTimeline extends React.PureComponent {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@@ -61,46 +53,16 @@ export default class CommunityTimeline extends React.PureComponent {
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
const { dispatch } = this.props;
dispatch(refreshCommunityTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));
},
reconnected () {
dispatch(connectTimeline('community'));
},
disconnected () {
dispatch(disconnectTimeline('community'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('community', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
this.disconnect = dispatch(connectCommunityStream());
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

View File

@@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
return (
<div className='autosuggest-account'>
<div className='autosuggest-account-icon'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
<DisplayName account={account} />
</div>
);

View File

@@ -13,12 +13,12 @@ export default class CharacterCounter extends React.PureComponent {
if (diff < 0) {
return <span className='character-counter character-counter--over'>{diff}</span>;
}
return <span className='character-counter'>{diff}</span>;
}
render () {
const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff);
}

View File

@@ -18,6 +18,7 @@ import WarningContainer from '../containers/warning_container';
import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -145,9 +146,9 @@ export default class ComposeForm extends ImmutablePureComponent {
render () {
const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, this.props.text].join('');
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
let publishText = '';
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
@@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<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 || this.props.is_uploading || length(text) > 500 || (text.length !==0 && text.trim().length === 0)} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
</div>
</div>

View File

@@ -124,7 +124,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
return (
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-pressed={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
<img
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
alt='🙂'

View File

@@ -19,7 +19,7 @@ export default class NavigationBar extends ImmutablePureComponent {
<div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar src={this.props.account.get('avatar')} animate size={40} />
<Avatar account={this.props.account} size={40} />
</Permalink>
<div className='navigation-bar__profile'>

View File

@@ -109,7 +109,7 @@ export default class PrivacyDropdown extends React.PureComponent {
return (
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} pressed={open} 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} expanded={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'>
{open && this.options.map(item =>
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import emojify from '../../../emoji';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -43,7 +42,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
return null;
}
const content = { __html: emojify(status.get('content')) };
const content = { __html: status.get('contentHtml') };
return (
<div className='reply-indicator'>
@@ -51,7 +50,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
<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'>
<div className='reply-indicator__display-avatar'><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
<DisplayName account={status.get('account')} />
</a>
</div>

View File

@@ -15,6 +15,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
visible: state.getIn(['compose', 'media_attachments']).size > 0,
active: state.getIn(['compose', 'sensitive']),
disabled: state.getIn(['compose', 'spoiler']),
});
const mapDispatchToProps = dispatch => ({
@@ -30,12 +31,13 @@ class SensitiveButton extends React.PureComponent {
static propTypes = {
visible: PropTypes.bool,
active: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { visible, active, onClick, intl } = this.props;
const { visible, active, disabled, onClick, intl } = this.props;
return (
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
@@ -53,6 +55,7 @@ class SensitiveButton extends React.PureComponent {
onClick={onClick}
size={18}
active={active}
disabled={disabled}
style={{ lineHeight: null, height: null }}
inverted
/>

View File

@@ -0,0 +1,9 @@
import { urlRegex } from './url_regex';
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(urlRegex, urlPlaceholder)
.replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
};

View File

@@ -0,0 +1,196 @@
const regexen = {};
const regexSupplant = function(regex, flags) {
flags = flags || '';
if (typeof regex !== 'string') {
if (regex.global && flags.indexOf('g') < 0) {
flags += 'g';
}
if (regex.ignoreCase && flags.indexOf('i') < 0) {
flags += 'i';
}
if (regex.multiline && flags.indexOf('m') < 0) {
flags += 'm';
}
regex = regex.source;
}
return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
var newRegex = regexen[name] || '';
if (typeof newRegex !== 'string') {
newRegex = newRegex.source;
}
return newRegex;
}), flags);
};
const stringSupplant = function(str, values) {
return str.replace(/#\{(\w+)\}/g, function(match, name) {
return values[name] || '';
});
};
export const urlRegex = (function() {
regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@$##{invalid_chars_group}]|^)/);
regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validGTLD = regexSupplant(RegExp(
'(?:(?:' +
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validCCTLD = regexSupplant(RegExp(
'(?:(?:' +
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
regexen.validPortNumber = /[0-9]+/;
regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
// Allow URL paths to contain up to two nested levels of balanced parens
// 1. Used in Wikipedia URLs like /Primer_(film)
// 2. Used in IIS sessions like /S(dfd346)/
// 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
regexen.validUrlBalancedParens = regexSupplant(
'\\(' +
'(?:' +
'#{validGeneralUrlPathChars}+' +
'|' +
// allow one nested level of balanced parentheses
'(?:' +
'#{validGeneralUrlPathChars}*' +
'\\(' +
'#{validGeneralUrlPathChars}+' +
'\\)' +
'#{validGeneralUrlPathChars}*' +
')' +
')' +
'\\)'
, 'i');
// Valid end-of-path chracters (so /foo. does not gobble the period).
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}_~&\|@]|(?:#{validUrlBalancedParens})/i);
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
regexen.validUrlPath = regexSupplant('(?:' +
'(?:' +
'#{validGeneralUrlPathChars}*' +
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
'#{validUrlPathEndingChars}'+
')|(?:@#{validGeneralUrlPathChars}+\/)'+
')', 'i');
regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
regexen.validUrl = regexSupplant(
'(' + // $1 URL
'(https?:\\/\\/)' + // $2 Protocol
'(#{validDomain})' + // $3 Domain(s)
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
'(\\/#{validUrlPath}*)?' + // $5 URL Path
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
')'
, 'gi');
return regexen.validUrl;
}());

View File

@@ -16,6 +16,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});
@connect(mapStateToProps)
@@ -28,6 +29,7 @@ export default class Favourites extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
};
componentWillMount () {
@@ -62,7 +64,7 @@ export default class Favourites extends ImmutablePureComponent {
}
render () {
const { intl, statusIds, columnId, multiColumn } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
const pinned = !!columnId;
return (
@@ -75,12 +77,14 @@ export default class Favourites extends ImmutablePureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
onScrollToBottom={this.handleScrollToBottom}
/>
</Column>

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