Compare commits

..

134 commits

Author SHA1 Message Date
Claire a40529fa79
Merge pull request #2279 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-07-07 20:33:32 +02:00
Claire b9aa228c54 Merge branch 'main' into glitch-soc/merge-upstream
Conflicts:
- `.github/workflows/build-image.yml`:
  Upstream attempted something with tags.
  Kept our version.
2023-07-07 19:59:43 +02:00
Claire 0051128387
Bump version to v4.1.4 (#25805) 2023-07-07 19:42:03 +02:00
Renaud Chaput d481e72e85
Tag images with the latest tag only when running against the latest stable branch (#25803) 2023-07-07 19:31:55 +02:00
Claire b6d173b459
Fix crash in admin interface when viewing a remote user with verified links (#25796) 2023-07-07 18:10:17 +02:00
Claire 71d44949bf
Fix branding:generate_app_icons failing because of disallowed ICO coder (#25794) 2023-07-07 18:10:00 +02:00
nemobis dfedf0ec64
Fix typo in CHANGELOG.md (#25764) 2023-07-07 14:15:54 +02:00
renovate[bot] 8b624553ef
Update dependency sanitize to v6.0.2 [SECURITY] (#25777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-07 13:35:54 +02:00
Claire 94fbac77e7
Fix processing of media files with unusual names (#25788) 2023-07-07 13:35:22 +02:00
Claire 513078de7a
Fix incorrect secondary button size (#2276) 2023-07-07 09:50:06 +02:00
Claire c25ba31e95
Merge pull request #2274 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-07-06 15:21:10 +02:00
Claire ff7aae3037 Merge branch 'main' into glitch-soc/merge-upstream 2023-07-06 15:16:34 +02:00
Claire 5e1752ce3f
Bump version to v4.1.3 (#25757) 2023-07-06 15:14:42 +02:00
Claire 610731b03d
Merge pull request from GHSA-55j9-c3mp-6fcq 2023-07-06 15:06:49 +02:00
Claire c5929798bf
Merge pull request from GHSA-9pxv-6qvf-pjwc
* Fix timeout handling of outbound HTTP requests

* Use CLOCK_MONOTONIC instead of Time.now
2023-07-06 15:06:23 +02:00
Claire dc8f1fbd97
Merge pull request from GHSA-9928-3cp5-93fm
* Fix attachments getting processed despite failing content-type validation

* Add a restrictive ImageMagick security policy tailored for Mastodon

* Fix misdetection of MP3 files with large cover art

* Reject unprocessable audio/video files instead of keeping them unchanged
2023-07-06 15:05:05 +02:00
Claire 6d8e0fae3e
Merge pull request from GHSA-ccm4-vgcc-73hp
* Tighten allowed HTML in oEmbed-based preview cards

* Sanitize preview cards at render time

* Add `sandbox` attribute to preview card iframes
2023-07-06 15:03:33 +02:00
Claire fed9cbfd2b
Add hardened headers to user-uploaded files (#25756) 2023-07-06 14:31:37 +02:00
Claire 30ad9d976b
Merge pull request #2272 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-07-05 16:47:23 +02:00
Eugen Rochko c0fa85b8dd [Glitch] Fix regression of icon button colors in web UI
Port 69e124e2ed to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 13:31:26 +02:00
Trevor Wolf 4719d88117 [Glitch] Change button colors to increase hover/focus contrast and consistency
Port 54cb679c19 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 13:30:52 +02:00
Eugen Rochko a117c953d0 [Glitch] Add button to see results for polls in web UI
Port b7910bc751 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 12:42:24 +02:00
Eugen Rochko b3f209370b [Glitch] Add canonical link tags in web UI
Port 000b835803 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 12:41:52 +02:00
mogaminsk c6246807f5 [Glitch] Fix local live feeds does not expand
Port 6268188543 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 12:39:46 +02:00
forsamori ac74374311 [Glitch] Add at-symbol prepended to mention span title
Port d9a5c1acfa to glitch-soc

Co-authored-by: Sam BC <samuel.balbirnie-cumming@xdesign.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 12:38:54 +02:00
Eugen Rochko 0a5271629c [Glitch] Change labels of live feeds tabs in web UI
Port 54a10523e2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-05 12:37:37 +02:00
Claire 5de49e74d4 Merge branch 'main' into glitch-soc/merge-upstream 2023-07-05 12:01:26 +02:00
Eugen Rochko 000b835803
Add canonical link tags in web UI (#25715) 2023-07-05 11:25:27 +02:00
Eugen Rochko b7910bc751
Add button to see results for polls in web UI (#25726) 2023-07-05 10:32:04 +02:00
Claire eb2417ce99
Fix OAuth apps page crashing when listing apps with certain admin API scopes (#25713) 2023-07-04 18:58:23 +02:00
Claire 4658263b4a
Fix re-activated accounts being deleted by AccountDeletionWorker (#25711) 2023-07-04 18:36:24 +02:00
Trevor Wolf 182fd93a07
fix read more button overlapping thread line bug (#25706) 2023-07-04 14:57:46 +02:00
Claire 12fa24a885
Fix forgotten unconfirmed_email migration file (#25702) 2023-07-04 11:25:29 +02:00
mogaminsk 6268188543
Fix local live feeds does not expand (#25694) 2023-07-04 00:37:57 +02:00
forsamori d9a5c1acfa
Add at-symbol prepended to mention span title (#25684)
Co-authored-by: Sam BC <samuel.balbirnie-cumming@xdesign.com>
2023-07-03 22:58:10 +02:00
Eugen Rochko 54a10523e2
Change labels of live feeds tabs in web UI (#25683) 2023-07-03 22:57:18 +02:00
Daniel M Brasil 383c00819c
Fix /api/v2/search not working with following query param (#25681) 2023-07-03 18:06:57 +02:00
Eugen Rochko 69e124e2ed
Fix regression of icon button colors in web UI (#25679) 2023-07-03 16:51:04 +02:00
Plastikmensch ed15893eed
Add regex filter back to firehose (#2266)
* Add regex filter back to firehose

The regex filter will apply to all tabs and not be automatically applied when pinned.

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>

* Keep regex when pinned

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>

---------

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2023-07-03 15:41:50 +02:00
Trevor Wolf 54cb679c19
Change button colors to increase hover/focus contrast and consistency (#25677) 2023-07-03 11:32:31 +02:00
Plastikmensch b422b5eebd
Fix showing local only toots in "All" (#2265)
* Fix warnings about missing dependency in hooks

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>

* Add `allowLocalOnly` to timelineId

Without this local-only toots will never be loaded.

feedType is checked to be public to not show local-only toots in the "Remote" tab.

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>

---------

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2023-07-03 07:00:38 +02:00
Claire ed567c9de6
Merge pull request #2263 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-07-02 22:14:45 +02:00
Claire 9f3c3f5209 Show local-only posts in “All” by default, and add back option to toggle it 2023-07-02 20:28:02 +02:00
Claire e6a8faae81
Add users index on unconfirmed_email (#25672) 2023-07-02 19:41:35 +02:00
Claire 933ba1a3eb
Add superapp index on oauth_applications (#25670) 2023-07-02 16:56:16 +02:00
Claire 180f0e6715
Fix inefficient query when requesting a new confirmation email from a logged-in account (#25669) 2023-07-02 16:08:58 +02:00
mogaminsk 587ddc2c7f [Glitch] Prevent duplicate concurrent calls of /api/*/instance in web UI
Port 5b46345459 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 12:00:03 +02:00
Eugen Rochko c49e339c89 [Glitch] Change dropdown icon above compose form from ellipsis to bars in web UI
Port 0512537eb6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 11:59:31 +02:00
Claire 7cc2c1be29 [Glitch] Change local and federated timelines to be in a single firehose column
Port cea9db5a0b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 11:58:51 +02:00
Renaud Chaput eb1cb8224a [Glitch] Use an Immutable Record as the root state
Port 78ba12f0bf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 11:55:34 +02:00
Claire 2ba4773ebe [Glitch] Fix onboarding prompt being displayed because of disconnection gaps
Port 9934949fc4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 11:55:08 +02:00
Claire b75aa6b819 [Glitch] Remove the search button from UI header when logged out
Port 285a691936 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-07-02 11:54:47 +02:00
Claire 44e98a2740 Merge branch 'main' into glitch-soc/merge-upstream 2023-07-02 11:49:08 +02:00
Eugen Rochko ba06a2f104
Revert "Rails 7 update" (#25667) 2023-07-02 11:14:22 +02:00
mogaminsk 5b46345459
Prevent duplicate concurrent calls of /api/*/instance in web UI (#25663) 2023-07-02 11:12:16 +02:00
Eugen Rochko 0512537eb6
Change dropdown icon above compose form from ellipsis to bars in web UI (#25661) 2023-07-02 10:39:55 +02:00
Matt Jankowski 50c2a03695
Rails 7 update (#24241) 2023-07-02 10:38:53 +02:00
Daniel M Brasil 4fe2d7cb59
Fix HTTP 500 in /api/v1/emails/check_confirmation (#25595) 2023-07-02 00:05:44 +02:00
Claire cea9db5a0b
Change local and federated timelines to be in a single firehose column (#25641) 2023-07-02 00:05:10 +02:00
Matt Jankowski 0139b1c8e1
Update uri to version 0.12.2 (CVE fix) (#25657) 2023-07-02 00:04:21 +02:00
Vivianne 155fc459e3
Fixing an issue with a missing argument (#2261)
undefined
2023-07-01 22:55:43 +02:00
Matt Jankowski f8bd581126
Remove unused routes (#25578) 2023-07-01 21:48:53 +02:00
Matt Jankowski 683ba5ecb1
Fix rails rewhere deprecation warning in directories api controller (#25625) 2023-07-01 21:48:16 +02:00
Matt Jankowski c47cdf6e17
Add index to backups on user_id column (#25647) 2023-06-30 19:09:03 +02:00
Renaud Chaput 78ba12f0bf
Use an Immutable Record as the root state (#25584) 2023-06-30 17:03:25 +02:00
Claire 9934949fc4
Fix onboarding prompt being displayed because of disconnection gaps (#25617) 2023-06-30 16:32:12 +02:00
Eugen Rochko 8bfbd19d2b Update Crowdin configuration file 2023-06-30 16:22:40 +02:00
Renaud Chaput c4a8c332b2
Remove pkg-config gem dependency (#25615) 2023-06-30 14:59:07 +02:00
Claire a209d1e683
Fix ResolveURLService not resolving local URLs for remote content (#25637) 2023-06-29 14:48:54 +02:00
jsgoldstein 4581a528f7
Change account search to match by text when opted-in (#25599)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
2023-06-29 13:05:21 +02:00
Claire 285a691936
Remove the search button from UI header when logged out (#25631) 2023-06-28 14:57:51 +02:00
Claire 8b4df95dbe
Merge pull request #2258 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-06-27 14:47:30 +02:00
Renaud Chaput c43cfd2406 [Glitch] Improve dismissable banner buttons when they dont fit on 1 line
Port ae30a60b1f to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-27 13:25:21 +02:00
Eugen Rochko 4faa4eb3c4 [Glitch] Fix onboarding prompt flashing while home feed is loading in web UI
Port 65aa04647a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-27 13:21:55 +02:00
Eugen Rochko 2f996375e5 [Glitch] Fix search not being easily findable on smaller screens in web UI
Port 2b78c07ef1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-27 13:21:14 +02:00
Claire 80a5c16ae1 Merge branch 'main' into glitch-soc/merge-upstream 2023-06-27 13:15:41 +02:00
Claire c7c6f02ae6
Fix suspending an already-limited domain (#25603) 2023-06-27 12:32:51 +02:00
renovate[bot] ceca93d0d1
Update dependency glob to v10.3.0 (#25608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 12:16:17 +02:00
renovate[bot] a9ba8263a0
Update mcr.microsoft.com/devcontainers/ruby Docker tag to v1 (#25613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 11:28:07 +02:00
renovate[bot] dbd37f129d
Update dependency pg to v8.11.1 (#25604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 10:36:21 +02:00
renovate[bot] b7f6280ef4
Update dependency pg-connection-string to v2.6.1 (#25605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 10:21:54 +02:00
renovate[bot] a90190f813
Update dependency react-textarea-autosize to v8.5.0 (#25610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 10:20:46 +02:00
renovate[bot] ccaa676452
Update dependency sass to v1.63.6 (#25607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 10:20:09 +02:00
renovate[bot] d9b07b6a11
Update dependency rails to v6.1.7.4 (#25606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 10:19:51 +02:00
Claire 0ccf6c0eb7
Fix batch attachment deletion leaving empty directories (#25587) 2023-06-27 09:36:11 +02:00
renovate[bot] 9caa0475f8
Update dependency react-redux to v8.1.1 (#25432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 20:59:58 +02:00
S.H 7a25af64dd
Remove media attachment only when file was exist (#25586) 2023-06-26 16:38:19 +02:00
Eugen Rochko bb4756c823
Change files to be deleted in batches instead of one-by-one (#23302) 2023-06-26 14:17:41 +02:00
Renaud Chaput ae30a60b1f
Improve dismissable banner buttons when they dont fit on 1 line (#25580)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-06-26 12:31:48 +02:00
Claire ed96e28c9e
Fix compose form not being shown when clicking “Make your first post” on mobile (#25581) 2023-06-26 12:30:35 +02:00
Eugen Rochko 7b024baf50
Change header backgrounds to use fewer different colors in web UI (#25577) 2023-06-26 05:27:07 +02:00
Eugen Rochko 65aa04647a
Fix onboarding prompt flashing while home feed is loading in web UI (#25579) 2023-06-26 05:26:54 +02:00
Eugen Rochko 2b78c07ef1
Fix search not being easily findable on smaller screens in web UI (#25576) 2023-06-26 05:26:41 +02:00
Claire b052a7ed95
Merge pull request #2256 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-06-25 21:01:10 +02:00
Claire 5def74a436 [Glitch] Remove unique accounts condition from Home onboarding prompt
Port 0842a68532 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 17:40:01 +02:00
Eugen Rochko 6fb34258a4 [Glitch] Add onboarding prompt when home feed too slow in web UI
Port 00ec43914a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 17:40:01 +02:00
Claire 7d160d2272 [Glitch] Fix j/k keyboard shortcuts on some status lists
Port a8c1c8bd37 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 17:40:01 +02:00
mogaminsk 222713a768 [Glitch] Fix custom signup URL may not loaded
Port 8d2c26834f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 14:27:46 +02:00
Ian 6fe345c383 [Glitch] Change emoji picker icon
Port 69db507924 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 14:27:46 +02:00
たいち ひ c71fc42f4e [Glitch] Rewrite <AutosuggestHashtag /> as FC and TS
Port 804488d38e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-06-25 14:27:46 +02:00
Claire 178e151019 Merge commit '55e7c08a83547424024bac311d5459cb82cf6dae' into glitch-soc/merge-upstream
Conflicts:
- `app/models/user_settings.rb`:
  Upstream added a constraint on a setting textually close
  to glitch-soc-only settings.
  Applied upstream's change.
- `lib/sanitize_ext/sanitize_config.rb`:
  Upstream added support for the `translate` attribute on a few elements,
  where glitch-soc had a different set of allowed elements and attributes.
  Extended glitch-soc's allowed attributes with `translate` as upstream did.
- `spec/validators/status_length_validator_spec.rb`:
  Upstream refactored to use RSpec's `instance_double` instead of `double`,
  but glitch-soc had changes to tests due to configurable max toot chars.
  Applied upstream's changes while keeping tests against configurable max
  toot chars.
2023-06-25 14:27:38 +02:00
Claire 55e7c08a83
Fix verified badge in account lists potentially including rel="me" links (#25561) 2023-06-24 17:24:31 +02:00
Eugen Rochko a985d587e1
Change labels and styles on the onboarding screen in web UI (#25559) 2023-06-23 16:34:27 +02:00
Claire 0842a68532
Remove unique accounts condition from Home onboarding prompt (#25556) 2023-06-23 14:44:54 +02:00
Eugen Rochko 00ec43914a
Add onboarding prompt when home feed too slow in web UI (#25267) 2023-06-22 23:48:40 +02:00
Claire 1d622c8033
Add POST /api/v1/conversations/:id/unread (#25509) 2023-06-22 18:46:43 +02:00
Matt Jankowski c9cd634184
Use default bootsnap/setup in boot.rb (#25502) 2023-06-22 18:46:32 +02:00
Claire a8c1c8bd37
Fix j/k keyboard shortcuts on some status lists (#25554) 2023-06-22 17:54:43 +02:00
Claire a5b6f6da80
Change /api/v1/statuses/:id/history to always return at least one item (#25510) 2023-06-22 14:56:14 +02:00
Matt Jankowski 05f9e39b32
Fix RSpec/VerifiedDoubles cop (#25469) 2023-06-22 14:55:22 +02:00
Matt Jankowski 38433ccd0b
Reduce Admin::Reports::Actions spec db activity (#25465) 2023-06-22 14:53:13 +02:00
Claire 602c458ab6
Add finer permission requirements for managing webhooks (#25463) 2023-06-22 14:52:25 +02:00
Matt Jankowski 63d15d5330
Speed-up on StatusesController spec (#25549) 2023-06-22 14:51:53 +02:00
mogaminsk 8d2c26834f
Fix custom signup URL may not loaded (#25531) 2023-06-22 12:10:49 +02:00
Matt Jankowski 0b39b9abee
Speed-up on BackupService spec (#25527) 2023-06-22 11:53:28 +02:00
Daniel M Brasil 6ac271c2a0
Migrate to request specs in /api/v1/suggestions (#25540) 2023-06-22 11:49:35 +02:00
Ian 69db507924
Change emoji picker icon (#25479) 2023-06-21 17:58:00 +02:00
Claire 37a9c2258a
Add per-test timeouts to AutoStatusesCleanupScheduler tests (#24841) 2023-06-20 18:54:05 +02:00
Claire ebfeaebedb
Fix /api/v1/conversations sometimes returning empty accounts (#25499) 2023-06-20 18:32:26 +02:00
Claire ec91ea4457
Fix missing validation on default_privacy setting (#25513) 2023-06-20 18:32:14 +02:00
Daniel M Brasil e53eb38a8d
Migrate to request specs in /api/v1/admin/account_actions (#25514) 2023-06-20 18:16:48 +02:00
Claire fd23f50243
Fix wrong view being displayed when a webhook fails validation (#25464) 2023-06-20 18:15:35 +02:00
Claire c78280a8ce
Add translate="no" to outgoing mentions and links (#25524) 2023-06-20 18:10:19 +02:00
Emelia Smith 3a91603b15
Prevent UserCleanupScheduler from overwhelming streaming (#25519) 2023-06-20 18:04:35 +02:00
Claire dd07393e75
Fix user settings not getting validated (#25508) 2023-06-19 14:06:06 +01:00
たいち ひ 804488d38e
Rewrite <AutosuggestHashtag /> as FC and TS (#25481) 2023-06-19 14:11:46 +02:00
Matt Jankowski e1c9d52e91
Reduce sleep time in request pool spec (#25470) 2023-06-19 13:48:25 +02:00
renovate[bot] cec4f1d506
Update dependency dotenv to v16.2.0 (#25506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-19 11:28:59 +02:00
Matt Jankowski 3a65fb044f
Add coverage for UserMailer methods (#25484) 2023-06-19 09:50:35 +02:00
Matt Jankowski e835198b26
Combine assertions in api/v1/notifications spec (#25486) 2023-06-19 09:05:42 +02:00
Matt Jankowski 155ec185b2
Remove unused picture_hint helper method (#25485) 2023-06-19 09:04:15 +02:00
renovate[bot] a0d7ae257d
Update dependency aws-sdk-s3 to v1.126.0 (#25480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-19 09:03:50 +02:00
Daniel M Brasil b9bc9d0bda
Fix incorrect pagination headers in /api/v2/admin/accounts (#25477) 2023-06-19 08:53:05 +02:00
Daniel M Brasil 0a0a1f1495
Migrate to request specs in /api/v1/tags (#25439) 2023-06-19 08:51:40 +02:00
225 changed files with 3079 additions and 1477 deletions

View file

@ -1,5 +1,5 @@
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
# Install Rails
# RUN gem install rails webdrivers

View file

@ -318,7 +318,6 @@ RSpec/LetSetup:
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v1/filters_controller_spec.rb'
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
- 'spec/controllers/api/v1/tags_controller_spec.rb'
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
@ -440,45 +439,6 @@ RSpec/SubjectStub:
- 'spec/services/unallow_domain_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
- 'spec/controllers/admin/change_emails_controller_spec.rb'
- 'spec/controllers/admin/confirmations_controller_spec.rb'
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
- 'spec/controllers/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/reports_controller_spec.rb'
- 'spec/controllers/api/web/embeds_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/disputes/appeals_controller_spec.rb'
- 'spec/helpers/statuses_helper_spec.rb'
- 'spec/lib/suspicious_sign_in_detector_spec.rb'
- 'spec/models/account/field_spec.rb'
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/services/account_search_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/search_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
- 'spec/validators/disallowed_hashtags_validator_spec.rb'
- 'spec/validators/email_mx_validator_spec.rb'
- 'spec/validators/follow_limit_validator_spec.rb'
- 'spec/validators/note_length_validator_spec.rb'
- 'spec/validators/poll_validator_spec.rb'
- 'spec/validators/status_length_validator_spec.rb'
- 'spec/validators/status_pin_validator_spec.rb'
- 'spec/validators/unique_username_validator_spec.rb'
- 'spec/validators/unreserved_username_validator_spec.rb'
- 'spec/validators/url_validator_spec.rb'
- 'spec/views/statuses/show.html.haml_spec.rb'
- 'spec/workers/activitypub/processing_worker_spec.rb'
- 'spec/workers/admin/domain_purge_worker_spec.rb'
- 'spec/workers/domain_block_worker_spec.rb'
- 'spec/workers/domain_clear_media_worker_spec.rb'
- 'spec/workers/feed_insert_worker_spec.rb'
- 'spec/workers/regeneration_worker_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController:
Exclude:
@ -759,7 +719,6 @@ Rails/WhereExists:
- 'app/workers/move_worker.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'lib/tasks/tests.rake'
- 'spec/controllers/api/v1/tags_controller_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb'

View file

@ -2,6 +2,62 @@
All notable changes to this project will be documented in this file.
## [4.1.4] - 2023-07-07
### Fixed
- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794))
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
## [4.1.3] - 2023-07-06
### Added
- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600))
### Changed
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868))
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614))
- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510))
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
### Removed
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
### Fixed
- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464))
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840))
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988))
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713))
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
### Security
- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463))
- Update dependencies
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
- Fix arbitrary file creation through media processing (CVE-2023-36460)
- Fix possible XSS in preview cards (CVE-2023-36459)
## [4.1.2] - 2023-04-04
### Fixed

View file

@ -3,8 +3,6 @@
source 'https://rubygems.org'
ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5'
gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2'

View file

@ -18,40 +18,40 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
actioncable (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionmailbox (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
mail (>= 2.7.1)
actionmailer (6.1.7.3)
actionpack (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionmailer (6.1.7.4)
actionpack (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activesupport (= 6.1.7.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.7.3)
actionview (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionpack (6.1.7.4)
actionview (= 6.1.7.4)
activesupport (= 6.1.7.4)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.3)
actionpack (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
actiontext (6.1.7.4)
actionpack (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
nokogiri (>= 1.8.5)
actionview (6.1.7.3)
activesupport (= 6.1.7.3)
actionview (6.1.7.4)
activesupport (= 6.1.7.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -61,22 +61,22 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.1.7.3)
activesupport (= 6.1.7.3)
activejob (6.1.7.4)
activesupport (= 6.1.7.4)
globalid (>= 0.3.6)
activemodel (6.1.7.3)
activesupport (= 6.1.7.3)
activerecord (6.1.7.3)
activemodel (= 6.1.7.3)
activesupport (= 6.1.7.3)
activestorage (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activesupport (= 6.1.7.3)
activemodel (6.1.7.4)
activesupport (= 6.1.7.4)
activerecord (6.1.7.4)
activemodel (= 6.1.7.4)
activesupport (= 6.1.7.4)
activestorage (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activesupport (= 6.1.7.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.7.3)
activesupport (6.1.7.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -106,7 +106,7 @@ GEM
aws-sdk-kms (1.67.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.125.0)
aws-sdk-s3 (1.126.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -412,7 +412,7 @@ GEM
mime-types-data (3.2023.0218.1)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.0)
minitest (5.18.1)
msgpack (1.7.1)
multi_json (1.15.0)
multipart-post (2.3.0)
@ -432,7 +432,7 @@ GEM
net-protocol
net-ssh (7.1.0)
nio4r (2.5.9)
nokogiri (1.15.2)
nokogiri (1.15.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.15.0)
@ -478,7 +478,6 @@ GEM
pg (1.5.3)
pghero (3.3.3)
activerecord (>= 6)
pkg-config (1.5.1)
posix-spawn (0.3.15)
premailer (1.21.0)
addressable
@ -511,20 +510,20 @@ GEM
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.3)
actioncable (= 6.1.7.3)
actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.7.3)
actionpack (= 6.1.7.3)
actiontext (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activemodel (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
rails (6.1.7.4)
actioncable (= 6.1.7.4)
actionmailbox (= 6.1.7.4)
actionmailer (= 6.1.7.4)
actionpack (= 6.1.7.4)
actiontext (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activemodel (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
bundler (>= 1.15.0)
railties (= 6.1.7.3)
railties (= 6.1.7.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -539,9 +538,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
railties (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -629,7 +628,7 @@ GEM
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (6.0.1)
sanitize (6.0.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
scenic (1.7.0)
@ -717,7 +716,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uri (0.12.1)
uri (0.12.2)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
@ -833,7 +832,6 @@ DEPENDENCIES
parslet
pg (~> 1.5)
pghero
pkg-config (~> 1.5)
posix-spawn
premailer-rails
private_address_check (~> 0.5)

View file

@ -2,8 +2,37 @@
class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '30s' }, analysis: {
filter: {
english_stop: {
type: 'stop',
stopwords: '_english_',
},
english_stemmer: {
type: 'stemmer',
language: 'english',
},
english_possessive_stemmer: {
type: 'stemmer',
language: 'possessive_english',
},
},
analyzer: {
content: {
natural: {
tokenizer: 'uax_url_email',
filter: %w(
english_possessive_stemmer
lowercase
asciifolding
cjk_width
english_stop
english_stemmer
),
},
verbatim: {
tokenizer: 'whitespace',
filter: %w(lowercase asciifolding cjk_width),
},
@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index
index_scope ::Account.searchable.includes(:account_stat)
root date_detection: false do
field :id, type: 'long'
field :display_name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following_count }
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
field(:id, type: 'long')
field(:following_count, type: 'long')
field(:followers_count, type: 'long')
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
end
end

View file

@ -28,6 +28,7 @@ module Admin
authorize :webhook, :create?
@webhook = Webhook.new(resource_params)
@webhook.current_account = current_account
if @webhook.save
redirect_to admin_webhook_path(@webhook)
@ -39,10 +40,12 @@ module Admin
def update
authorize @webhook, :update?
@webhook.current_account = current_account
if @webhook.update(resource_params)
redirect_to admin_webhook_path(@webhook)
else
render :show
render :edit
end
end

View file

@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController
render json: @conversation, serializer: REST::ConversationSerializer
end
def unread
@conversation.update!(unread: true)
render json: @conversation, serializer: REST::ConversationSerializer
end
def destroy
@conversation.destroy!
render_empty

View file

@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController
def accounts_scope
Account.discoverable.tap do |scope|
scope.merge!(Account.local) if truthy_param?(:local)
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
scope.merge!(account_order_scope)
scope.merge!(local_account_scope) if local_accounts?
scope.merge!(account_exclusion_scope) if current_account
scope.merge!(account_domain_block_scope) if current_account && !local_accounts?
end
end
def local_accounts?
truthy_param?(:local)
end
def account_order_scope
case params[:order]
when 'new'
Account.order(id: :desc)
when 'active', nil
Account.by_recent_status
end
end
def local_account_scope
Account.local
end
def account_exclusion_scope
Account.not_excluded_by_account(current_account)
end
def account_domain_block_scope
Account.not_domain_blocked_by_account(current_account)
end
end

View file

@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
before_action :require_user_owned_by_application!, except: :check
before_action :require_user_not_confirmed!, except: :check
before_action :require_authenticated_user!, only: :check
def create
current_user.update!(email: params[:email]) if params.key?(:email)

View file

@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
def show
cache_if_unauthenticated!
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
render json: status_edits, each_serializer: REST::StatusEditSerializer
end
private
def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?

View file

@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private
def next_path
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def filtered_accounts
AccountFilter.new(translated_filter_params).results
end

View file

@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController
params[:q],
current_account,
limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following))
)
end
def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
params.permit(:type, :offset, :min_id, :max_id, :account_id, :following)
end
end

View file

@ -75,7 +75,7 @@ module ThemingConcern
end
fallbacks.each do |fallback|
return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback)
end
nil

View file

@ -54,6 +54,10 @@ module FormattingHelper
end
def account_field_value_format(field, with_rel_me: true)
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
if field.verified? && !field.account.local?
TextFormatter.shortened_link(field.value_for_verification)
else
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
end
end
end

View file

@ -24,13 +24,4 @@ module SettingsHelper
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
end
end
def picture_hint(hint, picture)
if picture.original_filename.nil?
hint
else
link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete }
safe_join([hint, link], '<br/>'.html_safe)
end
end
end

View file

@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => {
if (getState().getIn(['server', 'server', 'isLoading'])) {
return;
}
dispatch(fetchServerRequest());
api(getState)
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
});
export const fetchExtendedDescription = () => (dispatch, getState) => {
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
return;
}
dispatch(fetchExtendedDescriptionRequest());
api(getState)
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
});
export const fetchDomainBlocks = () => (dispatch, getState) => {
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
return;
}
dispatch(fetchDomainBlocksRequest());
api(getState)

View file

@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import ShortNumber from 'flavours/glitch/components/short_number';
export default class AutosuggestHashtag extends PureComponent {
static propTypes = {
tag: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string,
history: PropTypes.array,
}).isRequired,
};
render() {
const { tag } = this.props;
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,42 @@
import { FormattedMessage } from 'react-intl';
import ShortNumber from 'flavours/glitch/components/short_number';
interface Props {
tag: {
name: string;
url?: string;
history?: Array<{
uses: number;
accounts: string;
day: string;
}>;
following?: boolean;
type: 'hashtag';
};
}
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
};

View file

@ -8,9 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;

View file

@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;

View file

@ -131,6 +131,10 @@ class Poll extends ImmutablePureComponent {
this.props.refresh();
};
handleReveal = () => {
this.setState({ revealed: true });
}
renderOption (option, optionIndex, showResults) {
const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
@ -206,14 +210,14 @@ class Poll extends ImmutablePureComponent {
render () {
const { poll, intl } = this.props;
const { expired } = this.state;
const { revealed, expired } = this.state;
if (!poll) {
return null;
}
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || expired;
const showResults = poll.get('voted') || revealed || expired;
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
let votesCount = null;
@ -232,9 +236,10 @@ class Poll extends ImmutablePureComponent {
<div className='poll__footer'>
{!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
{votesCount}
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
{poll.get('expires_at') && <> · {timeRemaining}</>}
</div>
</div>
);

View file

@ -163,7 +163,7 @@ class StatusContent extends PureComponent {
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', mention.get('acct'));
link.setAttribute('title', `@${mention.get('acct')}`);
if (rewriteMentions !== 'no') {
while (link.firstChild) link.removeChild(link.firstChild);
link.appendChild(document.createTextNode('@'));

View file

@ -161,7 +161,7 @@ class About extends PureComponent {
</Section>
<Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules').isEmpty() ? (
{!isLoading && (server.get('rules', []).isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>

View file

@ -398,6 +398,7 @@ class Header extends ImmutablePureComponent {
<Helmet>
<title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
</Helmet>
</div>
);

View file

@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
import ColumnHeader from 'flavours/glitch/components/column_header';
import StatusList from 'flavours/glitch/components/status_list';
import Column from 'flavours/glitch/features/ui/components/column';
import { getStatusList } from 'flavours/glitch/selectors';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
statusIds: getStatusList(state, 'bookmarks'),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});

View file

@ -142,11 +142,8 @@ class CommunityTimeline extends PureComponent {
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>
<DismissableBanner id='community_timeline'>
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
</DismissableBanner>
<StatusListContainer
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`}

View file

@ -62,7 +62,7 @@ class ActionBar extends PureComponent {
return (
<div className='compose__action-bar'>
<div className='compose__action-bar-dropdown'>
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' />
<DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
</div>
</div>
);

View file

@ -391,7 +391,7 @@ class EmojiPickerDropdown extends PureComponent {
{button || <img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={`${assetHost}/emoji/1f602.svg`}
src={`${assetHost}/emoji/1f642.svg`}
/>}
</div>

View file

@ -35,7 +35,7 @@ class Links extends PureComponent {
const banner = (
<DismissableBanner id='explore/links'>
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' />
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -11,9 +11,10 @@ import { debounce } from 'lodash';
import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
import StatusList from 'flavours/glitch/components/status_list';
import { getStatusList } from 'flavours/glitch/selectors';
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'trending', 'items']),
statusIds: getStatusList(state, 'trending'),
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
});
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
return (
<>
<DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' />
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
</DismissableBanner>
<StatusList

View file

@ -36,7 +36,7 @@ class Tags extends PureComponent {
const banner = (
<DismissableBanner id='explore/tags'>
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' />
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glit
import ColumnHeader from 'flavours/glitch/components/column_header';
import StatusList from 'flavours/glitch/components/status_list';
import Column from 'flavours/glitch/features/ui/components/column';
import { getStatusList } from 'flavours/glitch/selectors';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
statusIds: getStatusList(state, 'favourites'),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});

View file

@ -0,0 +1,231 @@
import PropTypes from 'prop-types';
import { useRef, useCallback, useEffect } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
import SettingText from 'flavours/glitch/components/setting_text';
import initialState, { domain } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import SettingToggle from '../notifications/components/setting_toggle';
import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
});
// TODO: use a proper React context later on
const useIdentity = () => ({
signedIn: !!initialState.meta.me,
accountId: initialState.meta.me,
disabledAccountId: initialState.meta.disabled_account_id,
accessToken: initialState.meta.access_token,
permissions: initialState.role ? initialState.role.permissions : 0,
});
const ColumnSettings = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const settings = useAppSelector((state) => state.getIn(['settings', 'firehose']));
const onChange = useCallback(
(key, checked) => dispatch(changeSetting(['firehose', ...key], checked)),
[dispatch],
);
return (
<div>
<div className='column-settings__row'>
<SettingToggle
settings={settings}
settingPath={['onlyMedia']}
onChange={onChange}
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
/>
<SettingToggle
settings={settings}
settingPath={['allowLocalOnly']}
onChange={onChange}
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
/>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<SettingText
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
</div>
);
};
const Firehose = ({ feedType, multiColumn }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { signedIn } = useIdentity();
const columnRef = useRef(null);
const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
const allowLocalOnly = useAppSelector((state) => state.getIn(['settings', 'firehose', 'allowLocalOnly']));
const regex = useAppSelector((state) => state.getIn(['settings', 'firehose', 'regex', 'body']));
const handlePin = useCallback(
() => {
switch(feedType) {
case 'community':
dispatch(addColumn('COMMUNITY', { other: { onlyMedia }, regex: { body: regex } }));
break;
case 'public':
dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } }));
break;
case 'public:remote':
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } }));
break;
}
},
[dispatch, onlyMedia, feedType, allowLocalOnly, regex],
);
const handleLoadMore = useCallback(
(maxId) => {
switch(feedType) {
case 'community':
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
break;
case 'public':
dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
break;
case 'public:remote':
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true }));
break;
}
},
[dispatch, onlyMedia, allowLocalOnly, feedType],
);
const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []);
useEffect(() => {
let disconnect;
switch(feedType) {
case 'community':
dispatch(expandCommunityTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
break;
case 'public':
dispatch(expandPublicTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectPublicStream({ onlyMedia, allowLocalOnly }));
}
break;
case 'public:remote':
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true }));
if (signedIn) {
disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true }));
}
break;
}
return () => disconnect?.();
}, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);
const prependBanner = feedType === 'community' ? (
<DismissableBanner id='community_timeline'>
<FormattedMessage
id='dismissable_banner.community_timeline'
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
values={{ domain }}
/>
</DismissableBanner>
) : (
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
values={{ domain }}
/>
</DismissableBanner>
);
const emptyMessage = feedType === 'community' ? (
<FormattedMessage
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
) : (
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
);
return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={handlePin}
onClick={handleHeaderClick}
multiColumn={multiColumn}
>
<ColumnSettings />
</ColumnHeader>
<div className='scrollable scrollable--flex'>
<div className='account__section-headline'>
<NavLink exact to='/public/local'>
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
</NavLink>
<NavLink exact to='/public/remote'>
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
</NavLink>
<NavLink exact to='/public'>
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
</NavLink>
</div>
<StatusListContainer
prepend={prependBanner}
timelineId={`${feedType}${feedType === 'public' && allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={handleLoadMore}
trackScroll
scrollKey='firehose'
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
regex={regex}
/>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
Firehose.propTypes = {
multiColumn: PropTypes.bool,
feedType: PropTypes.string,
};
export default Firehose;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
import background from 'mastodon/../images/friends-cropped.png';
export const ExplorePrompt = () => (
<DismissableBanner id='home.explore_prompt'>
<img src={background} alt='' className='dismissable-banner__background-image' />
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
<div className='dismissable-banner__message__actions__wrapper'>
<div className='dismissable-banner__message__actions'>
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
</div>
</div>
</DismissableBanner>
);

View file

@ -5,35 +5,66 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { me } from 'flavours/glitch/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { expandHomeTimeline } from '../../actions/timelines';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ExplorePrompt } from './components/explore_prompt';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
});
const getHomeFeedSpeed = createSelector([
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
state => state.get('statuses'),
], (statusIds, pendingStatusIds, statusMap) => {
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
return {
gap: averageGap,
newest,
};
});
const homeTooSlow = createSelector([
state => state.getIn(['timelines', 'home', 'isLoading']),
state => state.getIn(['timelines', 'home', 'isPartial']),
getHomeFeedSpeed,
], (isLoading, isPartial, speed) =>
!isLoading && !isPartial // Only if the home feed has finished loading
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
);
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements: state.getIn(['announcements', 'show']),
tooSlow: homeTooSlow(state),
regex: state.getIn(['settings', 'home', 'regex', 'body']),
});
@ -53,6 +84,7 @@ class HomeTimeline extends PureComponent {
hasAnnouncements: PropTypes.bool,
unreadAnnouncements: PropTypes.number,
showAnnouncements: PropTypes.bool,
tooSlow: PropTypes.bool,
regex: PropTypes.string,
};
@ -123,11 +155,11 @@ class HomeTimeline extends PureComponent {
};
render () {
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
let announcementsButton = null;
let announcementsButton, banner;
if (hasAnnouncements) {
announcementsButton = (
@ -142,6 +174,10 @@ class HomeTimeline extends PureComponent {
);
}
if (tooSlow) {
banner = <ExplorePrompt />;
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
<ColumnHeader
@ -161,11 +197,13 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
<StatusListContainer
prepend={banner}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
bindToDocument={!multiColumn}
regex={this.props.regex}
/>

View file

@ -8,17 +8,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses';
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
import StatusList from 'flavours/glitch/components/status_list';
import Column from 'flavours/glitch/features/ui/components/column';
import { getStatusList } from 'flavours/glitch/selectors';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'pins', 'items']),
statusIds: getStatusList(state, 'pins'),
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
});

View file

@ -13,6 +13,7 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import ColumnSettingsContainer from './containers/column_settings_container';
@ -146,11 +147,8 @@ class PublicTimeline extends PureComponent {
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>
<DismissableBanner id='public_timeline'>
<FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' />
</DismissableBanner>
<StatusListContainer
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
trackScroll={!pinned}

View file

@ -754,6 +754,7 @@ class Status extends ImmutablePureComponent {
<Helmet>
<title>{titleFromStatus(intl, status)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={status.get('url')} />
</Helmet>
</Column>
);

View file

@ -1,14 +1,16 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import { fetchServer } from 'flavours/glitch/actions/server';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Icon } from 'flavours/glitch/components/icon';
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
import Permalink from 'flavours/glitch/components/permalink';
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
@ -21,6 +23,10 @@ const Account = connect(state => ({
</Permalink>
));
const messages = defineMessages({
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
});
const mapStateToProps = (state) => ({
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
@ -29,6 +35,9 @@ const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() {
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
},
dispatchServer() {
dispatch(fetchServer());
}
});
class Header extends PureComponent {
@ -41,18 +50,26 @@ class Header extends PureComponent {
openClosedRegistrationsModal: PropTypes.func,
location: PropTypes.object,
signupUrl: PropTypes.string.isRequired,
dispatchServer: PropTypes.func,
intl: PropTypes.object.isRequired,
};
componentDidMount () {
const { dispatchServer } = this.props;
dispatchServer();
}
render () {
const { signedIn } = this.context.identity;
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
let content;
if (signedIn) {
content = (
<>
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
<Account />
</>
);
@ -97,4 +114,4 @@ class Header extends PureComponent {
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));

View file

@ -18,8 +18,7 @@ const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
explore: { id: 'explore.title', defaultMessage: 'Explore' },
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
@ -43,6 +42,10 @@ class NavigationPanel extends Component {
onOpenSettings: PropTypes.func,
};
isFirehoseActive = (match, location) => {
return match || location.pathname.startsWith('/public');
};
render() {
const { intl, onOpenSettings } = this.props;
const { signedIn, disabledAccountId } = this.context.identity;
@ -64,10 +67,7 @@ class NavigationPanel extends Component {
)}
{(signedIn || timelinePreview) && (
<>
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
</>
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
)}
{!signedIn && (

View file

@ -37,8 +37,7 @@ import {
Status,
GettingStarted,
KeyboardShortcuts,
PublicTimeline,
CommunityTimeline,
Firehose,
AccountTimeline,
AccountGallery,
HomeTimeline,
@ -196,8 +195,11 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
<WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact />
<Redirect from='/timelines/public/local' to='/public/local' exact />
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />

View file

@ -22,6 +22,10 @@ export function CommunityTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline');
}
export function Firehose () {
return import(/* webpackChunkName: "flavours/glitch/async/firehose" */'../../firehose');
}
export function HashtagTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
}

View file

@ -52,6 +52,7 @@
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
"favourite_modal.combo": "You can press {combo} to skip this next time",
"firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",

View file

@ -1,3 +1,5 @@
import { Record as ImmutableRecord } from 'immutable';
import { loadingBarReducer } from 'react-redux-loading-bar';
import { combineReducers } from 'redux-immutable';
@ -92,6 +94,22 @@ const reducers = {
followed_tags,
};
const rootReducer = combineReducers(reducers);
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
// so it is properly typed and keys can be accessed using `state.<key>` syntax.
// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state
// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record
const initialRootState = Object.fromEntries(
Object.entries(reducers).map(([name, reducer]) => [
name,
reducer(undefined, {
// empty action
}),
])
);
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
const rootReducer = combineReducers(reducers, RootStateRecord);
export { rootReducer };

View file

@ -17,15 +17,15 @@ import {
const initialState = ImmutableMap({
server: ImmutableMap({
isLoading: true,
isLoading: false,
}),
extendedDescription: ImmutableMap({
isLoading: true,
isLoading: false,
}),
domainBlocks: ImmutableMap({
isLoading: true,
isLoading: false,
isAvailable: true,
items: ImmutableList(),
}),

View file

@ -84,6 +84,15 @@ const initialState = ImmutableMap({
}),
}),
firehose: ImmutableMap({
onlyMedia: false,
allowLocalOnly: true,
regex: ImmutableMap({
body: '',
}),
}),
community: ImmutableMap({
regex: ImmutableMap({
body: '',

View file

@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
], (hidden, followingOrRequested, isSelf) => {
return hidden && !(isSelf || followingOrRequested);
});
export const getStatusList = createSelector([
(state, type) => state.getIn(['status_lists', type, 'items']),
], (items) => items.toList());

View file

@ -128,7 +128,6 @@ $content-width: 840px;
}
&.selected {
background: darken($ui-base-color, 2%);
border-radius: 4px 0 0;
}
}
@ -146,13 +145,9 @@ $content-width: 840px;
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: darken($ui-highlight-color, 2%);
background-color: $ui-highlight-color;
border-bottom: 0;
border-radius: 0;
&:hover {
background-color: $ui-highlight-color;
}
}
}
@ -246,12 +241,6 @@ $content-width: 840px;
font-weight: 700;
color: $primary-text-color;
background: $ui-highlight-color;
&:hover,
&:focus,
&:active {
background: lighten($ui-highlight-color, 4%);
}
}
}
}

View file

@ -960,26 +960,79 @@ $ui-header-height: 55px;
}
.dismissable-banner {
background: $ui-base-color;
border-bottom: 1px solid lighten($ui-base-color, 8%);
display: flex;
align-items: center;
gap: 30px;
position: relative;
margin: 10px;
margin-bottom: 5px;
border-radius: 8px;
border: 1px solid $highlight-text-color;
background: rgba($highlight-text-color, 0.15);
padding-inline-end: 45px;
overflow: hidden;
&__background-image {
width: 125%;
position: absolute;
bottom: -25%;
inset-inline-end: -25%;
z-index: -1;
opacity: 0.15;
mix-blend-mode: luminosity;
}
&__message {
flex: 1 1 auto;
padding: 20px 15px;
cursor: default;
font-size: 14px;
line-height: 18px;
padding: 15px;
font-size: 15px;
line-height: 22px;
font-weight: 500;
color: $primary-text-color;
p {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
h1 {
color: $highlight-text-color;
font-size: 22px;
line-height: 33px;
font-weight: 700;
margin-bottom: 15px;
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: 4px;
&__wrapper {
display: flex;
margin-top: 30px;
}
.button {
display: block;
flex-grow: 1;
}
}
.button-tertiary {
background: rgba($ui-base-color, 0.15);
backdrop-filter: blur(8px);
}
}
&__action {
padding: 15px;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
inset-inline-end: 0;
top: 0;
padding: 10px;
.icon-button {
color: $highlight-text-color;
}
}
}

View file

@ -38,11 +38,11 @@
}
.button {
background-color: darken($ui-highlight-color, 3%);
background-color: $ui-button-background-color;
border: 10px none;
border-radius: 4px;
box-sizing: border-box;
color: $primary-text-color;
color: $ui-button-color;
cursor: pointer;
display: inline-block;
font-family: inherit;
@ -62,14 +62,14 @@
&:active,
&:focus,
&:hover {
background-color: $ui-highlight-color;
background-color: $ui-button-focus-background-color;
}
&--destructive {
&:active,
&:focus,
&:hover {
background-color: $error-red;
background-color: $ui-button-destructive-focus-background-color;
transition: none;
}
}
@ -79,42 +79,18 @@
cursor: default;
}
&.button-alternative {
color: $inverted-text-color;
background: $ui-primary-color;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-primary-color, 4%);
}
}
&.button-alternative-2 {
background: $ui-base-lighter-color;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-base-lighter-color, 4%);
}
}
&.button-secondary {
font-size: 16px;
line-height: 36px;
height: auto;
color: $darker-text-color;
text-transform: none;
color: $ui-button-secondary-color;
background: transparent;
padding: 6px 17px;
border: 1px solid $ui-primary-color;
border: 1px solid $ui-button-secondary-border-color;
&:active,
&:focus,
&:hover {
border-color: lighten($ui-primary-color, 4%);
color: lighten($darker-text-color, 4%);
border-color: $ui-button-secondary-focus-background-color;
color: $ui-button-secondary-focus-color;
background-color: $ui-button-secondary-focus-background-color;
text-decoration: none;
}
@ -126,14 +102,14 @@
&.button-tertiary {
background: transparent;
padding: 6px 17px;
color: $highlight-text-color;
border: 1px solid $highlight-text-color;
color: $ui-button-tertiary-color;
border: 1px solid $ui-button-tertiary-border-color;
&:active,
&:focus,
&:hover {
background: $ui-highlight-color;
color: $primary-text-color;
background-color: $ui-button-tertiary-focus-background-color;
color: $ui-button-tertiary-focus-color;
border: 0;
padding: 7px 18px;
}

View file

@ -718,15 +718,15 @@
}
.button.button-secondary {
border-color: $inverted-text-color;
color: $inverted-text-color;
border-color: $ui-button-secondary-border-color;
color: $ui-button-secondary-color;
flex: 0 0 auto;
&:hover,
&:focus,
&:active {
border-color: lighten($inverted-text-color, 15%);
color: lighten($inverted-text-color, 15%);
border-color: $ui-button-secondary-focus-background-color;
color: $ui-button-secondary-focus-color;
}
}

View file

@ -81,7 +81,7 @@
display: flex;
align-items: baseline;
border-radius: 4px;
background: darken($ui-highlight-color, 2%);
background: $ui-button-background-color;
color: $primary-text-color;
transition: all 100ms ease-in;
font-size: 14px;
@ -94,7 +94,7 @@
&:active,
&:focus,
&:hover {
background-color: $ui-highlight-color;
background-color: $ui-button-focus-background-color;
transition: all 200ms ease-out;
}

View file

@ -512,8 +512,8 @@ code {
width: 100%;
border: 0;
border-radius: 4px;
background: darken($ui-highlight-color, 2%);
color: $primary-text-color;
background: $ui-button-background-color;
color: $ui-button-color;
font-size: 18px;
line-height: inherit;
height: auto;
@ -535,7 +535,7 @@ code {
&:active,
&:focus,
&:hover {
background-color: $ui-highlight-color;
background-color: $ui-button-focus-background-color;
}
&:disabled:hover {
@ -543,15 +543,12 @@ code {
}
&.negative {
background: $error-value-color;
&:hover {
background-color: lighten($error-value-color, 5%);
}
background: $ui-button-destructive-background-color;
&:hover,
&:active,
&:focus {
background-color: darken($error-value-color, 5%);
background-color: $ui-button-destructive-focus-background-color;
}
}
}

View file

@ -5,19 +5,6 @@ html {
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
// Change the colors of button texts
.button {
color: $white;
&.button-alternative-2 {
color: $white;
}
&.button-tertiary {
color: $highlight-text-color;
}
}
.simple_form .button.button-tertiary {
color: $highlight-text-color;
}
@ -436,26 +423,6 @@ html {
color: $white;
}
.button.button-tertiary {
&:hover,
&:focus,
&:active {
color: $white;
}
}
.button.button-secondary {
border-color: $darker-text-color;
color: $darker-text-color;
&:hover,
&:focus,
&:active {
border-color: darken($darker-text-color, 8%);
color: darken($darker-text-color, 8%);
}
}
.flash-message.warning {
color: lighten($gold-star, 16%);
}
@ -653,11 +620,6 @@ html {
border: 1px solid lighten($ui-base-color, 8%);
}
.dismissable-banner {
border-left: 1px solid lighten($ui-base-color, 8%);
border-right: 1px solid lighten($ui-base-color, 8%);
}
.status__content,
.reply-indicator__content {
a {

View file

@ -7,6 +7,12 @@ $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #6364ff;
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz
// Differences
$success-green: lighten(#3c754d, 8%);
@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-secondary-color: $grey-600 !default;
$ui-button-secondary-border-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;
$ui-button-tertiary-color: $blurple-500 !default;
$ui-button-tertiary-border-color: $blurple-500 !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;

View file

@ -1,10 +1,18 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a; // Padua
$error-red: #df405a; // Cerise
$warning-red: #ff5050; // Sunset Orange
$gold-star: #ca8f04; // Dark Goldenrod
$red-600: #b7253d !default; // Deep Carmine
$red-500: #df405a !default; // Cerise
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz
$success-green: #79bd9a !default; // Padua
$error-red: $red-500 !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
@ -31,6 +39,22 @@ $ui-base-lighter-color: lighten(
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-color: $white !default;
$ui-button-background-color: $blurple-500 !default;
$ui-button-focus-background-color: $blurple-600 !default;
$ui-button-secondary-color: $grey-100 !default;
$ui-button-secondary-border-color: $grey-100 !default;
$ui-button-secondary-focus-background-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;
$ui-button-tertiary-color: $blurple-300 !default;
$ui-button-tertiary-border-color: $blurple-300 !default;
$ui-button-tertiary-focus-background-color: $blurple-600 !default;
$ui-button-tertiary-focus-color: $white !default;
$ui-button-destructive-background-color: $red-500 !default;
$ui-button-destructive-focus-background-color: $red-600 !default;
// Variables for texts
$primary-text-color: $white !default;
@ -39,6 +63,7 @@ $dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
$action-button-color: $ui-base-lighter-color !default;
$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default;
$passive-text-color: $gold-star !default;
$active-passive-text-color: $success-green !default;

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View file

@ -129,13 +129,13 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => dispatch => {
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(routerHistory);
ensureComposeIsVisible(getState, routerHistory);
};
export function mentionCompose(account, routerHistory) {

View file

@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => {
if (getState().getIn(['server', 'server', 'isLoading'])) {
return;
}
dispatch(fetchServerRequest());
api(getState)
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
});
export const fetchExtendedDescription = () => (dispatch, getState) => {
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
return;
}
dispatch(fetchExtendedDescriptionRequest());
api(getState)
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
});
export const fetchDomainBlocks = () => (dispatch, getState) => {
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
return;
}
dispatch(fetchDomainBlocksRequest());
api(getState)

View file

@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
@ -49,6 +49,7 @@ class Account extends ImmutablePureComponent {
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
withBio: PropTypes.bool,
};
static defaultProps = {
@ -80,7 +81,7 @@ class Account extends ImmutablePureComponent {
};
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
@ -171,6 +172,15 @@ class Account extends ImmutablePureComponent {
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
}

View file

@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import ShortNumber from 'mastodon/components/short_number';
export default class AutosuggestHashtag extends PureComponent {
static propTypes = {
tag: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string,
history: PropTypes.array,
}).isRequired,
};
render() {
const { tag } = this.props;
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,42 @@
import { FormattedMessage } from 'react-intl';
import ShortNumber from 'mastodon/components/short_number';
interface Props {
tag: {
name: string;
url?: string;
history?: Array<{
uses: number;
accounts: string;
day: string;
}>;
following?: boolean;
type: 'hashtag';
};
}
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
};

View file

@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;

View file

@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;

View file

@ -130,6 +130,10 @@ class Poll extends ImmutablePureComponent {
this.props.refresh();
};
handleReveal = () => {
this.setState({ revealed: true });
}
renderOption (option, optionIndex, showResults) {
const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
@ -205,14 +209,14 @@ class Poll extends ImmutablePureComponent {
render () {
const { poll, intl } = this.props;
const { expired } = this.state;
const { revealed, expired } = this.state;
if (!poll) {
return null;
}
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || expired;
const showResults = poll.get('voted') || revealed || expired;
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
let votesCount = null;
@ -231,9 +235,10 @@ class Poll extends ImmutablePureComponent {
<div className='poll__footer'>
{!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
{votesCount}
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
{poll.get('expires_at') && <> · {timeRemaining}</>}
</div>
</div>
);

View file

@ -104,7 +104,7 @@ class StatusContent extends PureComponent {
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', mention.get('acct'));
link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);

View file

@ -1,11 +1,27 @@
import { Icon } from './icon';
const domParser = new DOMParser();
const stripRelMe = (html: string) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
link.rel = link.rel
.split(' ')
.filter((x: string) => x !== 'me')
.join(' ');
});
const body = document.querySelector('body');
return body ? { __html: body.innerHTML } : undefined;
};
interface Props {
link: string;
}
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={{ __html: link }} />
<span dangerouslySetInnerHTML={stripRelMe(link)} />
</span>
);

View file

@ -161,7 +161,7 @@ class About extends PureComponent {
</Section>
<Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules').isEmpty() ? (
{!isLoading && (server.get('rules', []).isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>

View file

@ -476,6 +476,7 @@ class Header extends ImmutablePureComponent {
<Helmet>
<title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
</Helmet>
</div>
);

View file

@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import ColumnHeader from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
statusIds: getStatusList(state, 'bookmarks'),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});

View file

@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent {
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>
<DismissableBanner id='community_timeline'>
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
</DismissableBanner>
<StatusListContainer
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`}

View file

@ -60,7 +60,7 @@ class ActionBar extends PureComponent {
return (
<div className='compose__action-bar'>
<div className='compose__action-bar-dropdown'>
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' />
<DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
</div>
</div>
);

View file

@ -389,7 +389,7 @@ class EmojiPickerDropdown extends PureComponent {
{button || <img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={`${assetHost}/emoji/1f602.svg`}
src={`${assetHost}/emoji/1f642.svg`}
/>}
</div>

View file

@ -35,7 +35,7 @@ class Links extends PureComponent {
const banner = (
<DismissableBanner id='explore/links'>
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' />
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -11,9 +11,10 @@ import { debounce } from 'lodash';
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner';
import StatusList from 'mastodon/components/status_list';
import { getStatusList } from 'mastodon/selectors';
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'trending', 'items']),
statusIds: getStatusList(state, 'trending'),
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
});
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
return (
<>
<DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' />
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
</DismissableBanner>
<StatusList

View file

@ -34,7 +34,7 @@ class Tags extends PureComponent {
const banner = (
<DismissableBanner id='explore/tags'>
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' />
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/acti
import ColumnHeader from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list';
import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
statusIds: getStatusList(state, 'favourites'),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});

View file

@ -0,0 +1,211 @@
import PropTypes from 'prop-types';
import { useRef, useCallback, useEffect } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { addColumn } from 'mastodon/actions/columns';
import { changeSetting } from 'mastodon/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
import DismissableBanner from 'mastodon/components/dismissable_banner';
import initialState, { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import SettingToggle from '../notifications/components/setting_toggle';
import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
});
// TODO: use a proper React context later on
const useIdentity = () => ({
signedIn: !!initialState.meta.me,
accountId: initialState.meta.me,
disabledAccountId: initialState.meta.disabled_account_id,
accessToken: initialState.meta.access_token,
permissions: initialState.role ? initialState.role.permissions : 0,
});
const ColumnSettings = () => {
const dispatch = useAppDispatch();
const settings = useAppSelector((state) => state.getIn(['settings', 'firehose']));
const onChange = useCallback(
(key, checked) => dispatch(changeSetting(['firehose', ...key], checked)),
[dispatch],
);
return (
<div>
<div className='column-settings__row'>
<SettingToggle
settings={settings}
settingPath={['onlyMedia']}
onChange={onChange}
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
/>
</div>
</div>
);
};
const Firehose = ({ feedType, multiColumn }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { signedIn } = useIdentity();
const columnRef = useRef(null);
const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
const handlePin = useCallback(
() => {
switch(feedType) {
case 'community':
dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
break;
case 'public':
dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
break;
case 'public:remote':
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } }));
break;
}
},
[dispatch, onlyMedia, feedType],
);
const handleLoadMore = useCallback(
(maxId) => {
switch(feedType) {
case 'community':
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
break;
case 'public':
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
break;
case 'public:remote':
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true }));
break;
}
},
[dispatch, onlyMedia, feedType],
);
const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []);
useEffect(() => {
let disconnect;
switch(feedType) {
case 'community':
dispatch(expandCommunityTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
break;
case 'public':
dispatch(expandPublicTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
break;
case 'public:remote':
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true }));
if (signedIn) {
disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true }));
}
break;
}
return () => disconnect?.();
}, [dispatch, signedIn, feedType, onlyMedia]);
const prependBanner = feedType === 'community' ? (
<DismissableBanner id='community_timeline'>
<FormattedMessage
id='dismissable_banner.community_timeline'
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
values={{ domain }}
/>
</DismissableBanner>
) : (
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
values={{ domain }}
/>
</DismissableBanner>
);
const emptyMessage = feedType === 'community' ? (
<FormattedMessage
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
) : (
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
);
return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={handlePin}
onClick={handleHeaderClick}
multiColumn={multiColumn}
>
<ColumnSettings />
</ColumnHeader>
<div className='scrollable scrollable--flex'>
<div className='account__section-headline'>
<NavLink exact to='/public/local'>
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
</NavLink>
<NavLink exact to='/public/remote'>
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
</NavLink>
<NavLink exact to='/public'>
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
</NavLink>
</div>
<StatusListContainer
prepend={prependBanner}
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
onLoadMore={handleLoadMore}
trackScroll
scrollKey='firehose'
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
Firehose.propTypes = {
multiColumn: PropTypes.bool,
feedType: PropTypes.string,
};
export default Firehose;

View file

@ -0,0 +1,26 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import background from 'mastodon/../images/friends-cropped.png';
import DismissableBanner from 'mastodon/components/dismissable_banner';
export const ExplorePrompt = () => (
<DismissableBanner id='home.explore_prompt'>
<img src={background} alt='' className='dismissable-banner__background-image' />
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
<div className='dismissable-banner__message__actions__wrapper'>
<div className='dismissable-banner__message__actions'>
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
</div>
</div>
</DismissableBanner>
);

View file

@ -5,14 +5,16 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
import { me } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { expandHomeTimeline } from '../../actions/timelines';
@ -20,6 +22,7 @@ import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ExplorePrompt } from './components/explore_prompt';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
@ -28,12 +31,40 @@ const messages = defineMessages({
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
});
const getHomeFeedSpeed = createSelector([
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
state => state.get('statuses'),
], (statusIds, pendingStatusIds, statusMap) => {
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
return {
gap: averageGap,
newest,
};
});
const homeTooSlow = createSelector([
state => state.getIn(['timelines', 'home', 'isLoading']),
state => state.getIn(['timelines', 'home', 'isPartial']),
getHomeFeedSpeed,
], (isLoading, isPartial, speed) =>
!isLoading && !isPartial // Only if the home feed has finished loading
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
);
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements: state.getIn(['announcements', 'show']),
tooSlow: homeTooSlow(state),
});
class HomeTimeline extends PureComponent {
@ -52,6 +83,7 @@ class HomeTimeline extends PureComponent {
hasAnnouncements: PropTypes.bool,
unreadAnnouncements: PropTypes.number,
showAnnouncements: PropTypes.bool,
tooSlow: PropTypes.bool,
};
handlePin = () => {
@ -121,11 +153,11 @@ class HomeTimeline extends PureComponent {
};
render () {
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
let announcementsButton = null;
let announcementsButton, banner;
if (hasAnnouncements) {
announcementsButton = (
@ -141,6 +173,10 @@ class HomeTimeline extends PureComponent {
);
}
if (tooSlow) {
banner = <ExplorePrompt />;
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
@ -160,11 +196,13 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
<StatusListContainer
prepend={banner}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
bindToDocument={!multiColumn}
/>
) : <NotSignedInIndicator />}

View file

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { Check } from 'mastodon/components/check';
import { Icon } from 'mastodon/components/icon';
import ArrowSmallRight from './arrow_small_right';
const Step = ({ label, description, icon, completed, onClick, href }) => {
const content = (
<>
@ -15,11 +17,9 @@ const Step = ({ label, description, icon, completed, onClick, href }) => {
<p>{description}</p>
</div>
{completed && (
<div className='onboarding__steps__item__progress'>
<Check />
</div>
)}
<div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
{completed ? <Check /> : <ArrowSmallRight />}
</div>
</>
);

View file

@ -12,20 +12,11 @@ import Column from 'mastodon/components/column';
import ColumnBackButton from 'mastodon/components/column_back_button';
import { EmptyAccount } from 'mastodon/components/empty_account';
import Account from 'mastodon/containers/account_container';
import { me } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors';
import ProgressIndicator from './components/progress_indicator';
const mapStateToProps = () => {
const getAccount = makeGetAccount();
return state => ({
account: getAccount(state, me),
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
});
};
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
});
class Follows extends PureComponent {
@ -33,7 +24,6 @@ class Follows extends PureComponent {
onBack: PropTypes.func,
dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
account: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@ -49,7 +39,7 @@ class Follows extends PureComponent {
}
render () {
const { onBack, isLoading, suggestions, account, multiColumn } = this.props;
const { onBack, isLoading, suggestions, multiColumn } = this.props;
let loadedContent;
@ -58,7 +48,7 @@ class Follows extends PureComponent {
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} />);
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}
return (
@ -71,8 +61,6 @@ class Follows extends PureComponent {
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
<div className='follow-recommendations'>
{loadedContent}
</div>

View file

@ -19,6 +19,7 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors';
import { assetHost } from 'mastodon/utils/config';
import ArrowSmallRight from './components/arrow_small_right';
import Step from './components/step';
@ -122,21 +123,22 @@ class Onboarding extends ImmutablePureComponent {
<div className='onboarding__steps'>
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage='Want to skip right ahead?' /></p>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<ArrowSmallRight />
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
</Link>
</div>
<div className='onboarding__footer'>
<button className='link-button' onClick={this.handleClose}><FormattedMessage id='onboarding.actions.close' defaultMessage="Don't show this screen again" /></button>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<ArrowSmallRight />
</Link>
</div>
</div>

View file

@ -177,13 +177,13 @@ class Share extends PureComponent {
<div className='onboarding__links'>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<ArrowSmallRight />
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Go to your home feed' />
</Link>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<ArrowSmallRight />
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
</Link>
</div>

View file

@ -8,6 +8,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { getStatusList } from 'mastodon/selectors';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import StatusList from '../../components/status_list';
@ -18,7 +20,7 @@ const messages = defineMessages({
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'pins', 'items']),
statusIds: getStatusList(state, 'pins'),
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
});

View file

@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import DismissableBanner from 'mastodon/components/dismissable_banner';
import { domain } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { connectPublicStream } from '../../actions/streaming';
@ -142,11 +143,8 @@ class PublicTimeline extends PureComponent {
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>
<DismissableBanner id='public_timeline'>
<FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' />
</DismissableBanner>
<StatusListContainer
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
trackScroll={!pinned}

View file

@ -713,6 +713,7 @@ class Status extends ImmutablePureComponent {
<Helmet>
<title>{titleFromStatus(intl, status)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={status.get('url')} />
</Helmet>
</Column>
);

View file

@ -1,14 +1,16 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { fetchServer } from 'mastodon/actions/server';
import { Avatar } from 'mastodon/components/avatar';
import { Icon } from 'mastodon/components/icon';
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
import { registrationsOpen, me } from 'mastodon/initial_state';
@ -20,6 +22,10 @@ const Account = connect(state => ({
</Link>
));
const messages = defineMessages({
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
});
const mapStateToProps = (state) => ({
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
@ -28,6 +34,9 @@ const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() {
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
},
dispatchServer() {
dispatch(fetchServer());
}
});
class Header extends PureComponent {
@ -40,18 +49,26 @@ class Header extends PureComponent {
openClosedRegistrationsModal: PropTypes.func,
location: PropTypes.object,
signupUrl: PropTypes.string.isRequired,
dispatchServer: PropTypes.func,
intl: PropTypes.object.isRequired,
};
componentDidMount () {
const { dispatchServer } = this.props;
dispatchServer();
}
render () {
const { signedIn } = this.context.identity;
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
let content;
if (signedIn) {
content = (
<>
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
<Account />
</>
);
@ -96,4 +113,4 @@ class Header extends PureComponent {
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));

View file

@ -20,8 +20,7 @@ const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
explore: { id: 'explore.title', defaultMessage: 'Explore' },
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
@ -43,6 +42,10 @@ class NavigationPanel extends Component {
intl: PropTypes.object.isRequired,
};
isFirehoseActive = (match, location) => {
return match || location.pathname.startsWith('/public');
};
render () {
const { intl } = this.props;
const { signedIn, disabledAccountId } = this.context.identity;
@ -69,10 +72,7 @@ class NavigationPanel extends Component {
)}
{(signedIn || timelinePreview) && (
<>
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
</>
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
)}
{!signedIn && (

View file

@ -36,8 +36,7 @@ import {
Status,
GettingStarted,
KeyboardShortcuts,
PublicTimeline,
CommunityTimeline,
Firehose,
AccountTimeline,
AccountGallery,
HomeTimeline,
@ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
<WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact />
<Redirect from='/timelines/public/local' to='/public/local' exact />
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />

View file

@ -22,6 +22,10 @@ export function CommunityTimeline () {
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
}
export function Firehose () {
return import(/* webpackChunkName: "features/firehose" */'../../firehose');
}
export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
}

View file

@ -52,6 +52,7 @@
"account.mute_notifications_short": "Mute notifications",
"account.mute_short": "Mute",
"account.muted": "Muted",
"account.no_bio": "No description provided.",
"account.open_original_page": "Open original page",
"account.posts": "Posts",
"account.posts_with_replies": "Posts and replies",
@ -113,6 +114,7 @@
"column.directory": "Browse profiles",
"column.domain_blocks": "Blocked domains",
"column.favourites": "Favourites",
"column.firehose": "Live feeds",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.lists": "Lists",
@ -146,7 +148,7 @@
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Publish",
"compose_form.publish_form": "Publish",
"compose_form.publish_form": "New post",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
@ -197,10 +199,10 @@
"disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
"dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
"dismissable_banner.dismiss": "Dismiss",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_statuses": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.",
"dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.",
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
"dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@ -232,8 +234,7 @@
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
"empty_column.home.suggestions": "See some suggestions",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
@ -267,6 +268,9 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post",
"firehose.all": "All",
"firehose.local": "This server",
"firehose.remote": "Other servers",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
@ -292,9 +296,13 @@
"hashtag.column_settings.tag_toggle": "Include additional tags for this column",
"hashtag.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow hashtag",
"home.actions.go_to_explore": "See what's trending",
"home.actions.go_to_suggestions": "Find people to follow",
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:",
"home.explore_prompt.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show announcements",
"interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.",
@ -449,28 +457,27 @@
"notifications_permission_banner.title": "Never miss a thing",
"onboarding.action.back": "Take me back",
"onboarding.actions.back": "Take me back",
"onboarding.actions.close": "Don't show this screen again",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.actions.go_to_explore": "Take me to trending",
"onboarding.actions.go_to_home": "Take me to my home feed",
"onboarding.compose.template": "Hello #Mastodon!",
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
"onboarding.follows.title": "Personalize your home feed",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:",
"onboarding.start.skip": "Don't need help getting started?",
"onboarding.start.title": "You've made it!",
"onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.",
"onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.",
"onboarding.steps.follow_people.title": "Personalize your home feed",
"onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}",
"onboarding.steps.publish_status.title": "Make your first post",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.steps.setup_profile.body": "Boost your interactions by having a comprehensive profile.",
"onboarding.steps.setup_profile.title": "Personalize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon",
"onboarding.steps.share_profile.title": "Share your Mastodon profile",
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
@ -480,6 +487,7 @@
"picture_in_picture.restore": "Put it back",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.reveal": "See results",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
@ -646,9 +654,7 @@
"subscribed_languages.target": "Change subscribed languages for {target}",
"suggestions.dismiss": "Dismiss suggestion",
"suggestions.header": "You might be interested in…",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",

View file

@ -1,3 +1,5 @@
import { Record as ImmutableRecord } from 'immutable';
import { loadingBarReducer } from 'react-redux-loading-bar';
import { combineReducers } from 'redux-immutable';
@ -88,6 +90,22 @@ const reducers = {
followed_tags,
};
const rootReducer = combineReducers(reducers);
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
// so it is properly typed and keys can be accessed using `state.<key>` syntax.
// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state
// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record
const initialRootState = Object.fromEntries(
Object.entries(reducers).map(([name, reducer]) => [
name,
reducer(undefined, {
// empty action
}),
])
);
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
const rootReducer = combineReducers(reducers, RootStateRecord);
export { rootReducer };

View file

@ -17,15 +17,15 @@ import {
const initialState = ImmutableMap({
server: ImmutableMap({
isLoading: true,
isLoading: false,
}),
extendedDescription: ImmutableMap({
isLoading: true,
isLoading: false,
}),
domainBlocks: ImmutableMap({
isLoading: true,
isLoading: false,
isAvailable: true,
items: ImmutableList(),
}),

View file

@ -79,6 +79,10 @@ const initialState = ImmutableMap({
}),
}),
firehose: ImmutableMap({
onlyMedia: false,
}),
community: ImmutableMap({
regex: ImmutableMap({
body: '',

View file

@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
], (hidden, followingOrRequested, isSelf) => {
return hidden && !(isSelf || followingOrRequested);
});
export const getStatusList = createSelector([
(state, type) => state.getIn(['status_lists', type, 'items']),
], (items) => items.toList());

View file

@ -5,19 +5,6 @@ html {
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
// Change the colors of button texts
.button {
color: $white;
&.button-alternative-2 {
color: $white;
}
&.button-tertiary {
color: $highlight-text-color;
}
}
.simple_form .button.button-tertiary {
color: $highlight-text-color;
}
@ -436,26 +423,6 @@ html {
color: $white;
}
.button.button-tertiary {
&:hover,
&:focus,
&:active {
color: $white;
}
}
.button.button-secondary {
border-color: $darker-text-color;
color: $darker-text-color;
&:hover,
&:focus,
&:active {
border-color: darken($darker-text-color, 8%);
color: darken($darker-text-color, 8%);
}
}
.flash-message.warning {
color: lighten($gold-star, 16%);
}
@ -653,11 +620,6 @@ html {
border: 1px solid lighten($ui-base-color, 8%);
}
.dismissable-banner {
border-left: 1px solid lighten($ui-base-color, 8%);
border-right: 1px solid lighten($ui-base-color, 8%);
}
.status__content,
.reply-indicator__content {
a {

View file

@ -7,6 +7,12 @@ $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #6364ff;
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz
// Differences
$success-green: lighten(#3c754d, 8%);
@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-secondary-color: $grey-600 !default;
$ui-button-secondary-border-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;
$ui-button-tertiary-color: $blurple-500 !default;
$ui-button-tertiary-border-color: $blurple-500 !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;

View file

@ -128,7 +128,6 @@ $content-width: 840px;
}
&.selected {
background: darken($ui-base-color, 2%);
border-radius: 4px 0 0;
}
}
@ -146,13 +145,9 @@ $content-width: 840px;
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: darken($ui-highlight-color, 2%);
background-color: $ui-highlight-color;
border-bottom: 0;
border-radius: 0;
&:hover {
background-color: $ui-highlight-color;
}
}
}
@ -246,12 +241,6 @@ $content-width: 840px;
font-weight: 700;
color: $primary-text-color;
background: $ui-highlight-color;
&:hover,
&:focus,
&:active {
background: lighten($ui-highlight-color, 4%);
}
}
}
}

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