mirror of
https://github.com/lunaisnotaboy/mastodon.git
synced 2024-10-18 13:34:22 +00:00
Compare commits
134 commits
eba3411bfa
...
a40529fa79
Author | SHA1 | Date | |
---|---|---|---|
a40529fa79 | |||
b9aa228c54 | |||
0051128387 | |||
d481e72e85 | |||
b6d173b459 | |||
71d44949bf | |||
dfedf0ec64 | |||
8b624553ef | |||
94fbac77e7 | |||
513078de7a | |||
c25ba31e95 | |||
ff7aae3037 | |||
5e1752ce3f | |||
610731b03d | |||
c5929798bf | |||
dc8f1fbd97 | |||
6d8e0fae3e | |||
fed9cbfd2b | |||
30ad9d976b | |||
c0fa85b8dd | |||
4719d88117 | |||
a117c953d0 | |||
b3f209370b | |||
c6246807f5 | |||
ac74374311 | |||
0a5271629c | |||
5de49e74d4 | |||
000b835803 | |||
b7910bc751 | |||
eb2417ce99 | |||
4658263b4a | |||
182fd93a07 | |||
12fa24a885 | |||
6268188543 | |||
d9a5c1acfa | |||
54a10523e2 | |||
383c00819c | |||
69e124e2ed | |||
ed15893eed | |||
54cb679c19 | |||
b422b5eebd | |||
ed567c9de6 | |||
9f3c3f5209 | |||
e6a8faae81 | |||
933ba1a3eb | |||
180f0e6715 | |||
587ddc2c7f | |||
c49e339c89 | |||
7cc2c1be29 | |||
eb1cb8224a | |||
2ba4773ebe | |||
b75aa6b819 | |||
44e98a2740 | |||
ba06a2f104 | |||
5b46345459 | |||
0512537eb6 | |||
50c2a03695 | |||
4fe2d7cb59 | |||
cea9db5a0b | |||
0139b1c8e1 | |||
155fc459e3 | |||
f8bd581126 | |||
683ba5ecb1 | |||
c47cdf6e17 | |||
78ba12f0bf | |||
9934949fc4 | |||
8bfbd19d2b | |||
c4a8c332b2 | |||
a209d1e683 | |||
4581a528f7 | |||
285a691936 | |||
8b4df95dbe | |||
c43cfd2406 | |||
4faa4eb3c4 | |||
2f996375e5 | |||
80a5c16ae1 | |||
c7c6f02ae6 | |||
ceca93d0d1 | |||
a9ba8263a0 | |||
dbd37f129d | |||
b7f6280ef4 | |||
a90190f813 | |||
ccaa676452 | |||
d9b07b6a11 | |||
0ccf6c0eb7 | |||
9caa0475f8 | |||
7a25af64dd | |||
bb4756c823 | |||
ae30a60b1f | |||
ed96e28c9e | |||
7b024baf50 | |||
65aa04647a | |||
2b78c07ef1 | |||
b052a7ed95 | |||
5def74a436 | |||
6fb34258a4 | |||
7d160d2272 | |||
222713a768 | |||
6fe345c383 | |||
c71fc42f4e | |||
178e151019 | |||
55e7c08a83 | |||
a985d587e1 | |||
0842a68532 | |||
00ec43914a | |||
1d622c8033 | |||
c9cd634184 | |||
a8c1c8bd37 | |||
a5b6f6da80 | |||
05f9e39b32 | |||
38433ccd0b | |||
602c458ab6 | |||
63d15d5330 | |||
8d2c26834f | |||
0b39b9abee | |||
6ac271c2a0 | |||
69db507924 | |||
37a9c2258a | |||
ebfeaebedb | |||
ec91ea4457 | |||
e53eb38a8d | |||
fd23f50243 | |||
c78280a8ce | |||
3a91603b15 | |||
dd07393e75 | |||
804488d38e | |||
e1c9d52e91 | |||
cec4f1d506 | |||
3a65fb044f | |||
e835198b26 | |||
155ec185b2 | |||
a0d7ae257d | |||
b9bc9d0bda | |||
0a0a1f1495 |
|
@ -1,5 +1,5 @@
|
||||||
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
|
# 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
|
# Install Rails
|
||||||
# RUN gem install rails webdrivers
|
# RUN gem install rails webdrivers
|
||||||
|
|
|
@ -318,7 +318,6 @@ RSpec/LetSetup:
|
||||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v1/followed_tags_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/admin/accounts_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v2/filters/statuses_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/services/unallow_domain_service_spec.rb'
|
||||||
- 'spec/validators/blacklisted_email_validator_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).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -759,7 +719,6 @@ Rails/WhereExists:
|
||||||
- 'app/workers/move_worker.rb'
|
- 'app/workers/move_worker.rb'
|
||||||
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
|
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
|
||||||
- 'lib/tasks/tests.rake'
|
- 'lib/tasks/tests.rake'
|
||||||
- 'spec/controllers/api/v1/tags_controller_spec.rb'
|
|
||||||
- 'spec/models/account_spec.rb'
|
- 'spec/models/account_spec.rb'
|
||||||
- 'spec/services/activitypub/process_collection_service_spec.rb'
|
- 'spec/services/activitypub/process_collection_service_spec.rb'
|
||||||
- 'spec/services/purge_domain_service_spec.rb'
|
- 'spec/services/purge_domain_service_spec.rb'
|
||||||
|
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -2,6 +2,62 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [4.1.2] - 2023-04-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -3,8 +3,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 3.0.0'
|
ruby '>= 3.0.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.5'
|
|
||||||
|
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rails', '~> 6.1.7'
|
gem 'rails', '~> 6.1.7'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
|
|
118
Gemfile.lock
118
Gemfile.lock
|
@ -18,40 +18,40 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.3)
|
actioncable (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.3)
|
actionmailbox (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.3)
|
actionmailer (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.3)
|
actionpack (6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.3)
|
actiontext (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.3)
|
actionview (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -61,22 +61,22 @@ GEM
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (6.1.7.3)
|
activejob (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.3)
|
activemodel (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activerecord (6.1.7.3)
|
activerecord (6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activestorage (6.1.7.3)
|
activestorage (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.3)
|
activesupport (6.1.7.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -106,7 +106,7 @@ GEM
|
||||||
aws-sdk-kms (1.67.0)
|
aws-sdk-kms (1.67.0)
|
||||||
aws-sdk-core (~> 3, >= 3.174.0)
|
aws-sdk-core (~> 3, >= 3.174.0)
|
||||||
aws-sigv4 (~> 1.1)
|
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-core (~> 3, >= 3.174.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
|
@ -412,7 +412,7 @@ GEM
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.2)
|
mini_portile2 (2.8.2)
|
||||||
minitest (5.18.0)
|
minitest (5.18.1)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
@ -432,7 +432,7 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.1.0)
|
net-ssh (7.1.0)
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.2)
|
nokogiri (1.15.3)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.15.0)
|
oj (3.15.0)
|
||||||
|
@ -478,7 +478,6 @@ GEM
|
||||||
pg (1.5.3)
|
pg (1.5.3)
|
||||||
pghero (3.3.3)
|
pghero (3.3.3)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
pkg-config (1.5.1)
|
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -511,20 +510,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.3)
|
rails (6.1.7.4)
|
||||||
actioncable (= 6.1.7.3)
|
actioncable (= 6.1.7.4)
|
||||||
actionmailbox (= 6.1.7.3)
|
actionmailbox (= 6.1.7.4)
|
||||||
actionmailer (= 6.1.7.3)
|
actionmailer (= 6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actiontext (= 6.1.7.3)
|
actiontext (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.3)
|
railties (= 6.1.7.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
@ -539,9 +538,9 @@ GEM
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
railties (6.1.7.3)
|
railties (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -629,7 +628,7 @@ GEM
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.0.1)
|
sanitize (6.0.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.7.0)
|
scenic (1.7.0)
|
||||||
|
@ -717,7 +716,7 @@ GEM
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uri (0.12.1)
|
uri (0.12.2)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
|
@ -833,7 +832,6 @@ DEPENDENCIES
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
pkg-config (~> 1.5)
|
|
||||||
posix-spawn
|
posix-spawn
|
||||||
premailer-rails
|
premailer-rails
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
|
|
|
@ -2,8 +2,37 @@
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
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: {
|
analyzer: {
|
||||||
content: {
|
natural: {
|
||||||
|
tokenizer: 'uax_url_email',
|
||||||
|
filter: %w(
|
||||||
|
english_possessive_stemmer
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
english_stop
|
||||||
|
english_stemmer
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
verbatim: {
|
||||||
tokenizer: 'whitespace',
|
tokenizer: 'whitespace',
|
||||||
filter: %w(lowercase asciifolding cjk_width),
|
filter: %w(lowercase asciifolding cjk_width),
|
||||||
},
|
},
|
||||||
|
@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index
|
||||||
index_scope ::Account.searchable.includes(:account_stat)
|
index_scope ::Account.searchable.includes(:account_stat)
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field(:id, type: 'long')
|
||||||
|
field(:following_count, type: 'long')
|
||||||
field :display_name, type: 'text', analyzer: 'content' do
|
field(:followers_count, type: 'long')
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
||||||
end
|
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 :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
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 :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||||
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 }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,7 @@ module Admin
|
||||||
authorize :webhook, :create?
|
authorize :webhook, :create?
|
||||||
|
|
||||||
@webhook = Webhook.new(resource_params)
|
@webhook = Webhook.new(resource_params)
|
||||||
|
@webhook.current_account = current_account
|
||||||
|
|
||||||
if @webhook.save
|
if @webhook.save
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
|
@ -39,10 +40,12 @@ module Admin
|
||||||
def update
|
def update
|
||||||
authorize @webhook, :update?
|
authorize @webhook, :update?
|
||||||
|
|
||||||
|
@webhook.current_account = current_account
|
||||||
|
|
||||||
if @webhook.update(resource_params)
|
if @webhook.update(resource_params)
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
else
|
else
|
||||||
render :show
|
render :edit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
render json: @conversation, serializer: REST::ConversationSerializer
|
render json: @conversation, serializer: REST::ConversationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread
|
||||||
|
@conversation.update!(unread: true)
|
||||||
|
render json: @conversation, serializer: REST::ConversationSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@conversation.destroy!
|
@conversation.destroy!
|
||||||
render_empty
|
render_empty
|
||||||
|
|
|
@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController
|
||||||
|
|
||||||
def accounts_scope
|
def accounts_scope
|
||||||
Account.discoverable.tap do |scope|
|
Account.discoverable.tap do |scope|
|
||||||
scope.merge!(Account.local) if truthy_param?(:local)
|
scope.merge!(account_order_scope)
|
||||||
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
scope.merge!(local_account_scope) if local_accounts?
|
||||||
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
scope.merge!(account_exclusion_scope) if current_account
|
||||||
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
scope.merge!(account_domain_block_scope) if current_account && !local_accounts?
|
||||||
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
|
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
|
||||||
before_action :require_user_owned_by_application!, except: :check
|
before_action :require_user_owned_by_application!, except: :check
|
||||||
before_action :require_user_not_confirmed!, except: :check
|
before_action :require_user_not_confirmed!, except: :check
|
||||||
|
before_action :require_authenticated_user!, only: :check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
current_user.update!(email: params[:email]) if params.key?(:email)
|
current_user.update!(email: params[:email]) if params.key?(:email)
|
||||||
|
|
|
@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_if_unauthenticated!
|
cache_if_unauthenticated!
|
||||||
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
render json: status_edits, each_serializer: REST::StatusEditSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
def set_status
|
||||||
@status = Status.find(params[:status_id])
|
@status = Status.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
|
|
|
@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
|
||||||
|
|
||||||
private
|
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
|
def filtered_accounts
|
||||||
AccountFilter.new(translated_filter_params).results
|
AccountFilter.new(translated_filter_params).results
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController
|
||||||
params[:q],
|
params[:q],
|
||||||
current_account,
|
current_account,
|
||||||
limit_param(RESULTS_LIMIT),
|
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
|
end
|
||||||
|
|
||||||
def search_params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -75,7 +75,7 @@ module ThemingConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
fallbacks.each do |fallback|
|
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
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -54,6 +54,10 @@ module FormattingHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_field_value_format(field, with_rel_me: true)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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')], ' ')
|
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
|
||||||
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
|
end
|
||||||
|
|
|
@ -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 SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchServer = () => (dispatch, getState) => {
|
export const fetchServer = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'server', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchServerRequest());
|
dispatch(fetchServerRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchExtendedDescriptionRequest());
|
dispatch(fetchExtendedDescriptionRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchDomainBlocksRequest());
|
dispatch(fetchDomainBlocksRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,9 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||||
let word;
|
let word;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
||||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
let word;
|
let word;
|
||||||
|
|
|
@ -131,6 +131,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
this.props.refresh();
|
this.props.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleReveal = () => {
|
||||||
|
this.setState({ revealed: true });
|
||||||
|
}
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, lang, disabled, intl } = this.props;
|
const { poll, lang, disabled, intl } = this.props;
|
||||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||||
|
@ -206,14 +210,14 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { poll, intl } = this.props;
|
const { poll, intl } = this.props;
|
||||||
const { expired } = this.state;
|
const { revealed, expired } = this.state;
|
||||||
|
|
||||||
if (!poll) {
|
if (!poll) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
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);
|
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||||
|
|
||||||
let votesCount = null;
|
let votesCount = null;
|
||||||
|
@ -232,9 +236,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='poll__footer'>
|
<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 && <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}
|
{votesCount}
|
||||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
{poll.get('expires_at') && <> · {timeRemaining}</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -163,7 +163,7 @@ class StatusContent extends PureComponent {
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
link.setAttribute('title', mention.get('acct'));
|
link.setAttribute('title', `@${mention.get('acct')}`);
|
||||||
if (rewriteMentions !== 'no') {
|
if (rewriteMentions !== 'no') {
|
||||||
while (link.firstChild) link.removeChild(link.firstChild);
|
while (link.firstChild) link.removeChild(link.firstChild);
|
||||||
link.appendChild(document.createTextNode('@'));
|
link.appendChild(document.createTextNode('@'));
|
||||||
|
|
|
@ -161,7 +161,7 @@ class About extends PureComponent {
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={intl.formatMessage(messages.rules)}>
|
<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>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
) : (
|
) : (
|
||||||
<ol className='rules-list'>
|
<ol className='rules-list'>
|
||||||
|
|
|
@ -398,6 +398,7 @@ class Header extends ImmutablePureComponent {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{titleFromAccount(account)}</title>
|
<title>{titleFromAccount(account)}</title>
|
||||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||||
|
<link rel='canonical' href={account.get('url')} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import StatusList from 'flavours/glitch/components/status_list';
|
import StatusList from 'flavours/glitch/components/status_list';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
statusIds: getStatusList(state, 'bookmarks'),
|
||||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
|
@ -142,11 +142,8 @@ class CommunityTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</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
|
<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}
|
trackScroll={!pinned}
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ActionBar extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='compose__action-bar'>
|
<div className='compose__action-bar'>
|
||||||
<div className='compose__action-bar-dropdown'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -391,7 +391,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
{button || <img
|
{button || <img
|
||||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||||
alt='🙂'
|
alt='🙂'
|
||||||
src={`${assetHost}/emoji/1f602.svg`}
|
src={`${assetHost}/emoji/1f642.svg`}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Links extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/links'>
|
<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>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
||||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
|
import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
|
||||||
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
||||||
import StatusList from 'flavours/glitch/components/status_list';
|
import StatusList from 'flavours/glitch/components/status_list';
|
||||||
|
import { getStatusList } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
statusIds: getStatusList(state, 'trending'),
|
||||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||||
});
|
});
|
||||||
|
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DismissableBanner id='explore/statuses'>
|
<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>
|
</DismissableBanner>
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Tags extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/tags'>
|
<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>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glit
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import StatusList from 'flavours/glitch/components/status_list';
|
import StatusList from 'flavours/glitch/components/status_list';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
statusIds: getStatusList(state, 'favourites'),
|
||||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||||
});
|
});
|
||||||
|
|
231
app/javascript/flavours/glitch/features/firehose/index.jsx
Normal file
231
app/javascript/flavours/glitch/features/firehose/index.jsx
Normal 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;
|
|
@ -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>
|
||||||
|
);
|
|
@ -5,35 +5,66 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
|
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 { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
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';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||||
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
|
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
|
||||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide 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 => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||||
|
tooSlow: homeTooSlow(state),
|
||||||
regex: state.getIn(['settings', 'home', 'regex', 'body']),
|
regex: state.getIn(['settings', 'home', 'regex', 'body']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,6 +84,7 @@ class HomeTimeline extends PureComponent {
|
||||||
hasAnnouncements: PropTypes.bool,
|
hasAnnouncements: PropTypes.bool,
|
||||||
unreadAnnouncements: PropTypes.number,
|
unreadAnnouncements: PropTypes.number,
|
||||||
showAnnouncements: PropTypes.bool,
|
showAnnouncements: PropTypes.bool,
|
||||||
|
tooSlow: PropTypes.bool,
|
||||||
regex: PropTypes.string,
|
regex: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,11 +155,11 @@ class HomeTimeline extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
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 pinned = !!columnId;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
let announcementsButton = null;
|
let announcementsButton, banner;
|
||||||
|
|
||||||
if (hasAnnouncements) {
|
if (hasAnnouncements) {
|
||||||
announcementsButton = (
|
announcementsButton = (
|
||||||
|
@ -142,6 +174,10 @@ class HomeTimeline extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tooSlow) {
|
||||||
|
banner = <ExplorePrompt />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
|
@ -161,11 +197,13 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
prepend={banner}
|
||||||
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
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}
|
bindToDocument={!multiColumn}
|
||||||
regex={this.props.regex}
|
regex={this.props.regex}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,17 +8,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses';
|
import { getStatusList } from 'flavours/glitch/selectors';
|
||||||
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
|
|
||||||
import StatusList from 'flavours/glitch/components/status_list';
|
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||||
|
import StatusList from '../../components/status_list';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
|
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'pins', 'items']),
|
statusIds: getStatusList(state, 'pins'),
|
||||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
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 StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||||
|
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
@ -146,11 +147,8 @@ class PublicTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</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
|
<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' : ''}`}
|
timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
|
|
|
@ -754,6 +754,7 @@ class Status extends ImmutablePureComponent {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{titleFromStatus(intl, status)}</title>
|
<title>{titleFromStatus(intl, status)}</title>
|
||||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||||
|
<link rel='canonical' href={status.get('url')} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
||||||
import Permalink from 'flavours/glitch/components/permalink';
|
import Permalink from 'flavours/glitch/components/permalink';
|
||||||
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
|
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
|
||||||
|
@ -21,6 +23,10 @@ const Account = connect(state => ({
|
||||||
</Permalink>
|
</Permalink>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||||
});
|
});
|
||||||
|
@ -29,6 +35,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
openClosedRegistrationsModal() {
|
openClosedRegistrationsModal() {
|
||||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||||
},
|
},
|
||||||
|
dispatchServer() {
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class Header extends PureComponent {
|
class Header extends PureComponent {
|
||||||
|
@ -41,18 +50,26 @@ class Header extends PureComponent {
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
openClosedRegistrationsModal: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
|
dispatchServer: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatchServer } = this.props;
|
||||||
|
dispatchServer();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
content = (
|
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 />
|
<Account />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -97,4 +114,4 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||||
|
|
|
@ -18,8 +18,7 @@ const messages = defineMessages({
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
|
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
|
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -43,6 +42,10 @@ class NavigationPanel extends Component {
|
||||||
onOpenSettings: PropTypes.func,
|
onOpenSettings: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isFirehoseActive = (match, location) => {
|
||||||
|
return match || location.pathname.startsWith('/public');
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, onOpenSettings } = this.props;
|
const { intl, onOpenSettings } = this.props;
|
||||||
const { signedIn, disabledAccountId } = this.context.identity;
|
const { signedIn, disabledAccountId } = this.context.identity;
|
||||||
|
@ -64,10 +67,7 @@ class NavigationPanel extends Component {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(signedIn || timelinePreview) && (
|
{(signedIn || timelinePreview) && (
|
||||||
<>
|
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
|
||||||
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
|
|
||||||
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!signedIn && (
|
{!signedIn && (
|
||||||
|
|
|
@ -37,8 +37,7 @@ import {
|
||||||
Status,
|
Status,
|
||||||
GettingStarted,
|
GettingStarted,
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
PublicTimeline,
|
Firehose,
|
||||||
CommunityTimeline,
|
|
||||||
AccountTimeline,
|
AccountTimeline,
|
||||||
AccountGallery,
|
AccountGallery,
|
||||||
HomeTimeline,
|
HomeTimeline,
|
||||||
|
@ -196,8 +195,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
<WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} />
|
<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={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
|
|
|
@ -22,6 +22,10 @@ export function CommunityTimeline () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline');
|
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 () {
|
export function HashtagTimeline () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
|
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
"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",
|
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
|
||||||
"favourite_modal.combo": "You can press {combo} to skip this next time",
|
"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.done": "Done",
|
||||||
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
|
"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!",
|
"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!",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||||
import { combineReducers } from 'redux-immutable';
|
import { combineReducers } from 'redux-immutable';
|
||||||
|
|
||||||
|
@ -92,6 +94,22 @@ const reducers = {
|
||||||
followed_tags,
|
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 };
|
export { rootReducer };
|
||||||
|
|
|
@ -17,15 +17,15 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
server: ImmutableMap({
|
server: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
extendedDescription: ImmutableMap({
|
extendedDescription: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
domainBlocks: ImmutableMap({
|
domainBlocks: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -84,6 +84,15 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
firehose: ImmutableMap({
|
||||||
|
onlyMedia: false,
|
||||||
|
allowLocalOnly: true,
|
||||||
|
|
||||||
|
regex: ImmutableMap({
|
||||||
|
body: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
community: ImmutableMap({
|
community: ImmutableMap({
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
|
|
|
@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
|
||||||
], (hidden, followingOrRequested, isSelf) => {
|
], (hidden, followingOrRequested, isSelf) => {
|
||||||
return hidden && !(isSelf || followingOrRequested);
|
return hidden && !(isSelf || followingOrRequested);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getStatusList = createSelector([
|
||||||
|
(state, type) => state.getIn(['status_lists', type, 'items']),
|
||||||
|
], (items) => items.toList());
|
||||||
|
|
|
@ -128,7 +128,6 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: darken($ui-base-color, 2%);
|
|
||||||
border-radius: 4px 0 0;
|
border-radius: 4px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,13 +145,9 @@ $content-width: 840px;
|
||||||
|
|
||||||
.simple-navigation-active-leaf a {
|
.simple-navigation-active-leaf a {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background-color: darken($ui-highlight-color, 2%);
|
background-color: $ui-highlight-color;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,12 +241,6 @@ $content-width: 840px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: $ui-highlight-color;
|
background: $ui-highlight-color;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background: lighten($ui-highlight-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -960,26 +960,79 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismissable-banner {
|
.dismissable-banner {
|
||||||
background: $ui-base-color;
|
position: relative;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
margin: 10px;
|
||||||
display: flex;
|
margin-bottom: 5px;
|
||||||
align-items: center;
|
border-radius: 8px;
|
||||||
gap: 30px;
|
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 {
|
&__message {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 20px 15px;
|
padding: 15px;
|
||||||
cursor: default;
|
font-size: 15px;
|
||||||
font-size: 14px;
|
line-height: 22px;
|
||||||
line-height: 18px;
|
font-weight: 500;
|
||||||
color: $primary-text-color;
|
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 {
|
&__action {
|
||||||
padding: 15px;
|
position: absolute;
|
||||||
flex: 0 0 auto;
|
inset-inline-end: 0;
|
||||||
display: flex;
|
top: 0;
|
||||||
align-items: center;
|
padding: 10px;
|
||||||
justify-content: center;
|
|
||||||
|
.icon-button {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
background-color: darken($ui-highlight-color, 3%);
|
background-color: $ui-button-background-color;
|
||||||
border: 10px none;
|
border: 10px none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: $primary-text-color;
|
color: $ui-button-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
@ -62,14 +62,14 @@
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--destructive {
|
&--destructive {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $error-red;
|
background-color: $ui-button-destructive-focus-background-color;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,42 +79,18 @@
|
||||||
cursor: default;
|
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 {
|
&.button-secondary {
|
||||||
font-size: 16px;
|
color: $ui-button-secondary-color;
|
||||||
line-height: 36px;
|
|
||||||
height: auto;
|
|
||||||
color: $darker-text-color;
|
|
||||||
text-transform: none;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid $ui-button-secondary-border-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: lighten($ui-primary-color, 4%);
|
border-color: $ui-button-secondary-focus-background-color;
|
||||||
color: lighten($darker-text-color, 4%);
|
color: $ui-button-secondary-focus-color;
|
||||||
|
background-color: $ui-button-secondary-focus-background-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,14 +102,14 @@
|
||||||
&.button-tertiary {
|
&.button-tertiary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
color: $highlight-text-color;
|
color: $ui-button-tertiary-color;
|
||||||
border: 1px solid $highlight-text-color;
|
border: 1px solid $ui-button-tertiary-border-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $ui-highlight-color;
|
background-color: $ui-button-tertiary-focus-background-color;
|
||||||
color: $primary-text-color;
|
color: $ui-button-tertiary-focus-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 7px 18px;
|
padding: 7px 18px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -718,15 +718,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.button-secondary {
|
.button.button-secondary {
|
||||||
border-color: $inverted-text-color;
|
border-color: $ui-button-secondary-border-color;
|
||||||
color: $inverted-text-color;
|
color: $ui-button-secondary-color;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
border-color: lighten($inverted-text-color, 15%);
|
border-color: $ui-button-secondary-focus-background-color;
|
||||||
color: lighten($inverted-text-color, 15%);
|
color: $ui-button-secondary-focus-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: darken($ui-highlight-color, 2%);
|
background: $ui-button-background-color;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
transition: all 100ms ease-in;
|
transition: all 100ms ease-in;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
transition: all 200ms ease-out;
|
transition: all 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -512,8 +512,8 @@ code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: darken($ui-highlight-color, 2%);
|
background: $ui-button-background-color;
|
||||||
color: $primary-text-color;
|
color: $ui-button-color;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -535,7 +535,7 @@ code {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled:hover {
|
&:disabled:hover {
|
||||||
|
@ -543,15 +543,12 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.negative {
|
&.negative {
|
||||||
background: $error-value-color;
|
background: $ui-button-destructive-background-color;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($error-value-color, 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: darken($error-value-color, 5%);
|
background-color: $ui-button-destructive-focus-background-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,6 @@ html {
|
||||||
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
|
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 {
|
.simple_form .button.button-tertiary {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
@ -436,26 +423,6 @@ html {
|
||||||
color: $white;
|
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 {
|
.flash-message.warning {
|
||||||
color: lighten($gold-star, 16%);
|
color: lighten($gold-star, 16%);
|
||||||
}
|
}
|
||||||
|
@ -653,11 +620,6 @@ html {
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
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,
|
.status__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -7,6 +7,12 @@ $classic-primary-color: #9baec8;
|
||||||
$classic-secondary-color: #d9e1e8;
|
$classic-secondary-color: #d9e1e8;
|
||||||
$classic-highlight-color: #6364ff;
|
$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
|
// Differences
|
||||||
$success-green: lighten(#3c754d, 8%);
|
$success-green: lighten(#3c754d, 8%);
|
||||||
|
|
||||||
|
@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed;
|
||||||
$ui-secondary-color: $classic-base-color !default;
|
$ui-secondary-color: $classic-base-color !default;
|
||||||
$ui-highlight-color: $classic-highlight-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;
|
$primary-text-color: $black !default;
|
||||||
$darker-text-color: $classic-base-color !default;
|
$darker-text-color: $classic-base-color !default;
|
||||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
// Commonly used web colors
|
// Commonly used web colors
|
||||||
$black: #000000; // Black
|
$black: #000000; // Black
|
||||||
$white: #ffffff; // White
|
$white: #ffffff; // White
|
||||||
$success-green: #79bd9a; // Padua
|
$red-600: #b7253d !default; // Deep Carmine
|
||||||
$error-red: #df405a; // Cerise
|
$red-500: #df405a !default; // Cerise
|
||||||
$warning-red: #ff5050; // Sunset Orange
|
$blurple-600: #563acc; // Iris
|
||||||
$gold-star: #ca8f04; // Dark Goldenrod
|
$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;
|
$red-bookmark: $warning-red;
|
||||||
|
|
||||||
|
@ -31,6 +39,22 @@ $ui-base-lighter-color: lighten(
|
||||||
$ui-primary-color: $classic-primary-color !default; // Lighter
|
$ui-primary-color: $classic-primary-color !default; // Lighter
|
||||||
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
||||||
$ui-highlight-color: $classic-highlight-color !default;
|
$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
|
// Variables for texts
|
||||||
$primary-text-color: $white !default;
|
$primary-text-color: $white !default;
|
||||||
|
@ -39,6 +63,7 @@ $dark-text-color: $ui-base-lighter-color !default;
|
||||||
$secondary-text-color: $ui-secondary-color !default;
|
$secondary-text-color: $ui-secondary-color !default;
|
||||||
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
|
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
|
||||||
$action-button-color: $ui-base-lighter-color !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;
|
$passive-text-color: $gold-star !default;
|
||||||
$active-passive-text-color: $success-green !default;
|
$active-passive-text-color: $success-green !default;
|
||||||
|
|
||||||
|
|
BIN
app/javascript/images/friends-cropped.png
Executable file
BIN
app/javascript/images/friends-cropped.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
|
@ -129,13 +129,13 @@ export function resetCompose() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (routerHistory, defaultText) => dispatch => {
|
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(routerHistory);
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mentionCompose(account, routerHistory) {
|
export function mentionCompose(account, routerHistory) {
|
||||||
|
|
|
@ -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 SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchServer = () => (dispatch, getState) => {
|
export const fetchServer = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'server', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchServerRequest());
|
dispatch(fetchServerRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchExtendedDescriptionRequest());
|
dispatch(fetchExtendedDescriptionRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchDomainBlocksRequest());
|
dispatch(fetchDomainBlocksRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -49,6 +49,7 @@ class Account extends ImmutablePureComponent {
|
||||||
actionTitle: PropTypes.string,
|
actionTitle: PropTypes.string,
|
||||||
defaultAction: PropTypes.string,
|
defaultAction: PropTypes.string,
|
||||||
onActionClick: PropTypes.func,
|
onActionClick: PropTypes.func,
|
||||||
|
withBio: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -80,7 +81,7 @@ class Account extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
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) {
|
if (!account) {
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
return <EmptyAccount size={size} minimal={minimal} />;
|
||||||
|
@ -171,6 +172,15 @@ class Account extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
42
app/javascript/mastodon/components/autosuggest_hashtag.tsx
Normal file
42
app/javascript/mastodon/components/autosuggest_hashtag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
||||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||||
let word;
|
let word;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
||||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
let word;
|
let word;
|
||||||
|
|
|
@ -130,6 +130,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
this.props.refresh();
|
this.props.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleReveal = () => {
|
||||||
|
this.setState({ revealed: true });
|
||||||
|
}
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, lang, disabled, intl } = this.props;
|
const { poll, lang, disabled, intl } = this.props;
|
||||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||||
|
@ -205,14 +209,14 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { poll, intl } = this.props;
|
const { poll, intl } = this.props;
|
||||||
const { expired } = this.state;
|
const { revealed, expired } = this.state;
|
||||||
|
|
||||||
if (!poll) {
|
if (!poll) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
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);
|
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||||
|
|
||||||
let votesCount = null;
|
let votesCount = null;
|
||||||
|
@ -231,9 +235,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='poll__footer'>
|
<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 && <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}
|
{votesCount}
|
||||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
{poll.get('expires_at') && <> · {timeRemaining}</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -104,7 +104,7 @@ class StatusContent extends PureComponent {
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
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')}`);
|
link.setAttribute('href', `/@${mention.get('acct')}`);
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} 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);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import { Icon } from './icon';
|
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 {
|
interface Props {
|
||||||
link: string;
|
link: string;
|
||||||
}
|
}
|
||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' className='verified-badge__mark' />
|
<Icon id='check' className='verified-badge__mark' />
|
||||||
<span dangerouslySetInnerHTML={{ __html: link }} />
|
<span dangerouslySetInnerHTML={stripRelMe(link)} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -161,7 +161,7 @@ class About extends PureComponent {
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={intl.formatMessage(messages.rules)}>
|
<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>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
) : (
|
) : (
|
||||||
<ol className='rules-list'>
|
<ol className='rules-list'>
|
||||||
|
|
|
@ -476,6 +476,7 @@ class Header extends ImmutablePureComponent {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{titleFromAccount(account)}</title>
|
<title>{titleFromAccount(account)}</title>
|
||||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||||
|
<link rel='canonical' href={account.get('url')} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
statusIds: getStatusList(state, 'bookmarks'),
|
||||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
|
@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</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
|
<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}
|
trackScroll={!pinned}
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||||
|
|
|
@ -60,7 +60,7 @@ class ActionBar extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='compose__action-bar'>
|
<div className='compose__action-bar'>
|
||||||
<div className='compose__action-bar-dropdown'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -389,7 +389,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
{button || <img
|
{button || <img
|
||||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||||
alt='🙂'
|
alt='🙂'
|
||||||
src={`${assetHost}/emoji/1f602.svg`}
|
src={`${assetHost}/emoji/1f642.svg`}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Links extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/links'>
|
<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>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
||||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
||||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
statusIds: getStatusList(state, 'trending'),
|
||||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||||
});
|
});
|
||||||
|
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DismissableBanner id='explore/statuses'>
|
<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>
|
</DismissableBanner>
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Tags extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/tags'>
|
<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>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/acti
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
statusIds: getStatusList(state, 'favourites'),
|
||||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||||
});
|
});
|
||||||
|
|
211
app/javascript/mastodon/features/firehose/index.jsx
Normal file
211
app/javascript/mastodon/features/firehose/index.jsx
Normal 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;
|
|
@ -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>
|
||||||
|
);
|
|
@ -5,14 +5,16 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
@ -20,6 +22,7 @@ import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -28,12 +31,40 @@ const messages = defineMessages({
|
||||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide 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 => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||||
|
tooSlow: homeTooSlow(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
class HomeTimeline extends PureComponent {
|
class HomeTimeline extends PureComponent {
|
||||||
|
@ -52,6 +83,7 @@ class HomeTimeline extends PureComponent {
|
||||||
hasAnnouncements: PropTypes.bool,
|
hasAnnouncements: PropTypes.bool,
|
||||||
unreadAnnouncements: PropTypes.number,
|
unreadAnnouncements: PropTypes.number,
|
||||||
showAnnouncements: PropTypes.bool,
|
showAnnouncements: PropTypes.bool,
|
||||||
|
tooSlow: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -121,11 +153,11 @@ class HomeTimeline extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
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 pinned = !!columnId;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
let announcementsButton = null;
|
let announcementsButton, banner;
|
||||||
|
|
||||||
if (hasAnnouncements) {
|
if (hasAnnouncements) {
|
||||||
announcementsButton = (
|
announcementsButton = (
|
||||||
|
@ -141,6 +173,10 @@ class HomeTimeline extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tooSlow) {
|
||||||
|
banner = <ExplorePrompt />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
|
@ -160,11 +196,13 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
prepend={banner}
|
||||||
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
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}
|
bindToDocument={!multiColumn}
|
||||||
/>
|
/>
|
||||||
) : <NotSignedInIndicator />}
|
) : <NotSignedInIndicator />}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||||
import { Check } from 'mastodon/components/check';
|
import { Check } from 'mastodon/components/check';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
import ArrowSmallRight from './arrow_small_right';
|
||||||
|
|
||||||
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
@ -15,11 +17,9 @@ const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{completed && (
|
<div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
|
||||||
<div className='onboarding__steps__item__progress'>
|
{completed ? <Check /> : <ArrowSmallRight />}
|
||||||
<Check />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -12,20 +12,11 @@ import Column from 'mastodon/components/column';
|
||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||||
import Account from 'mastodon/containers/account_container';
|
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 = state => ({
|
||||||
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
const mapStateToProps = () => {
|
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||||
const getAccount = makeGetAccount();
|
});
|
||||||
|
|
||||||
return state => ({
|
|
||||||
account: getAccount(state, me),
|
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
|
||||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
class Follows extends PureComponent {
|
class Follows extends PureComponent {
|
||||||
|
|
||||||
|
@ -33,7 +24,6 @@ class Follows extends PureComponent {
|
||||||
onBack: PropTypes.func,
|
onBack: PropTypes.func,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
suggestions: ImmutablePropTypes.list,
|
suggestions: ImmutablePropTypes.list,
|
||||||
account: ImmutablePropTypes.map,
|
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -49,7 +39,7 @@ class Follows extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { onBack, isLoading, suggestions, account, multiColumn } = this.props;
|
const { onBack, isLoading, suggestions, multiColumn } = this.props;
|
||||||
|
|
||||||
let loadedContent;
|
let loadedContent;
|
||||||
|
|
||||||
|
@ -58,7 +48,7 @@ class Follows extends PureComponent {
|
||||||
} else if (suggestions.isEmpty()) {
|
} 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>;
|
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 {
|
} 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 (
|
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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
|
|
||||||
|
|
||||||
<div className='follow-recommendations'>
|
<div className='follow-recommendations'>
|
||||||
{loadedContent}
|
{loadedContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import ArrowSmallRight from './components/arrow_small_right';
|
import ArrowSmallRight from './components/arrow_small_right';
|
||||||
import Step from './components/step';
|
import Step from './components/step';
|
||||||
|
@ -122,21 +123,22 @@ class Onboarding extends ImmutablePureComponent {
|
||||||
<div className='onboarding__steps'>
|
<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.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.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!' />} />
|
<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>
|
</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'>
|
<div className='onboarding__links'>
|
||||||
<Link to='/explore' className='onboarding__link'>
|
<Link to='/explore' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='onboarding__footer'>
|
<Link to='/home' className='onboarding__link'>
|
||||||
<button className='link-button' onClick={this.handleClose}><FormattedMessage id='onboarding.actions.close' defaultMessage="Don't show this screen again" /></button>
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
|
<ArrowSmallRight />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -177,13 +177,13 @@ class Share extends PureComponent {
|
||||||
|
|
||||||
<div className='onboarding__links'>
|
<div className='onboarding__links'>
|
||||||
<Link to='/home' className='onboarding__link'>
|
<Link to='/home' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Go to your home feed' />
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to='/explore' className='onboarding__link'>
|
<Link to='/explore' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
|
@ -18,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'pins', 'items']),
|
statusIds: getStatusList(state, 'pins'),
|
||||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { connectPublicStream } from '../../actions/streaming';
|
import { connectPublicStream } from '../../actions/streaming';
|
||||||
|
@ -142,11 +143,8 @@ class PublicTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</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
|
<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' : ''}`}
|
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
|
|
|
@ -713,6 +713,7 @@ class Status extends ImmutablePureComponent {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{titleFromStatus(intl, status)}</title>
|
<title>{titleFromStatus(intl, status)}</title>
|
||||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||||
|
<link rel='canonical' href={status.get('url')} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
||||||
import { registrationsOpen, me } from 'mastodon/initial_state';
|
import { registrationsOpen, me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -20,6 +22,10 @@ const Account = connect(state => ({
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||||
});
|
});
|
||||||
|
@ -28,6 +34,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
openClosedRegistrationsModal() {
|
openClosedRegistrationsModal() {
|
||||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||||
},
|
},
|
||||||
|
dispatchServer() {
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class Header extends PureComponent {
|
class Header extends PureComponent {
|
||||||
|
@ -40,18 +49,26 @@ class Header extends PureComponent {
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
openClosedRegistrationsModal: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
|
dispatchServer: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatchServer } = this.props;
|
||||||
|
dispatchServer();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
content = (
|
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 />
|
<Account />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -96,4 +113,4 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||||
|
|
|
@ -20,8 +20,7 @@ const messages = defineMessages({
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
|
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
|
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -43,6 +42,10 @@ class NavigationPanel extends Component {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isFirehoseActive = (match, location) => {
|
||||||
|
return match || location.pathname.startsWith('/public');
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
const { signedIn, disabledAccountId } = this.context.identity;
|
const { signedIn, disabledAccountId } = this.context.identity;
|
||||||
|
@ -69,10 +72,7 @@ class NavigationPanel extends Component {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(signedIn || timelinePreview) && (
|
{(signedIn || timelinePreview) && (
|
||||||
<>
|
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
|
||||||
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
|
|
||||||
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!signedIn && (
|
{!signedIn && (
|
||||||
|
|
|
@ -36,8 +36,7 @@ import {
|
||||||
Status,
|
Status,
|
||||||
GettingStarted,
|
GettingStarted,
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
PublicTimeline,
|
Firehose,
|
||||||
CommunityTimeline,
|
|
||||||
AccountTimeline,
|
AccountTimeline,
|
||||||
AccountGallery,
|
AccountGallery,
|
||||||
HomeTimeline,
|
HomeTimeline,
|
||||||
|
@ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
<WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} />
|
<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={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
|
|
|
@ -22,6 +22,10 @@ export function CommunityTimeline () {
|
||||||
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Firehose () {
|
||||||
|
return import(/* webpackChunkName: "features/firehose" */'../../firehose');
|
||||||
|
}
|
||||||
|
|
||||||
export function HashtagTimeline () {
|
export function HashtagTimeline () {
|
||||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
"account.mute_notifications_short": "Mute notifications",
|
"account.mute_notifications_short": "Mute notifications",
|
||||||
"account.mute_short": "Mute",
|
"account.mute_short": "Mute",
|
||||||
"account.muted": "Muted",
|
"account.muted": "Muted",
|
||||||
|
"account.no_bio": "No description provided.",
|
||||||
"account.open_original_page": "Open original page",
|
"account.open_original_page": "Open original page",
|
||||||
"account.posts": "Posts",
|
"account.posts": "Posts",
|
||||||
"account.posts_with_replies": "Posts and replies",
|
"account.posts_with_replies": "Posts and replies",
|
||||||
|
@ -113,6 +114,7 @@
|
||||||
"column.directory": "Browse profiles",
|
"column.directory": "Browse profiles",
|
||||||
"column.domain_blocks": "Blocked domains",
|
"column.domain_blocks": "Blocked domains",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
"column.firehose": "Live feeds",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
|
@ -146,7 +148,7 @@
|
||||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
"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.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "Publish",
|
||||||
"compose_form.publish_form": "Publish",
|
"compose_form.publish_form": "New post",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.save_changes": "Save changes",
|
"compose_form.save_changes": "Save changes",
|
||||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
"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.",
|
"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.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
|
||||||
"dismissable_banner.dismiss": "Dismiss",
|
"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_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 posts from this and other servers in the decentralized network are gaining traction on this server right now.",
|
"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 hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
|
"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 this and other servers of the decentralized network that this server knows about.",
|
"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.instructions": "Embed this post on your website by copying the code below.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Here is what it will look like:",
|
||||||
"emoji_button.activity": "Activity",
|
"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.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.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.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": "Your home timeline is empty! Follow more people to fill it up.",
|
||||||
"empty_column.home.suggestions": "See some suggestions",
|
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
"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.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.",
|
"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.subtitle": "Use an existing category or create a new one",
|
||||||
"filter_modal.select_filter.title": "Filter this post",
|
"filter_modal.select_filter.title": "Filter this post",
|
||||||
"filter_modal.title.status": "Filter a 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.authorize": "Authorize",
|
||||||
"follow_request.reject": "Reject",
|
"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.",
|
"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.column_settings.tag_toggle": "Include additional tags for this column",
|
||||||
"hashtag.follow": "Follow hashtag",
|
"hashtag.follow": "Follow hashtag",
|
||||||
"hashtag.unfollow": "Unfollow 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.basic": "Basic",
|
||||||
"home.column_settings.show_reblogs": "Show boosts",
|
"home.column_settings.show_reblogs": "Show boosts",
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"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.hide_announcements": "Hide announcements",
|
||||||
"home.show_announcements": "Show 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.",
|
"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",
|
"notifications_permission_banner.title": "Never miss a thing",
|
||||||
"onboarding.action.back": "Take me back",
|
"onboarding.action.back": "Take me back",
|
||||||
"onboarding.actions.back": "Take me back",
|
"onboarding.actions.back": "Take me back",
|
||||||
"onboarding.actions.close": "Don't show this screen again",
|
"onboarding.actions.go_to_explore": "Take me to trending",
|
||||||
"onboarding.actions.go_to_explore": "See what's trending",
|
"onboarding.actions.go_to_home": "Take me to my home feed",
|
||||||
"onboarding.actions.go_to_home": "Go to your home feed",
|
|
||||||
"onboarding.compose.template": "Hello #Mastodon!",
|
"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.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.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": "Popular on Mastodon",
|
"onboarding.follows.title": "Personalize your home feed",
|
||||||
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
|
"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.message": "I'm {username} on #Mastodon! Come follow me at {url}",
|
||||||
"onboarding.share.next_steps": "Possible next steps:",
|
"onboarding.share.next_steps": "Possible next steps:",
|
||||||
"onboarding.share.title": "Share your profile",
|
"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.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": "Want to skip right ahead?",
|
"onboarding.start.skip": "Don't need help getting started?",
|
||||||
"onboarding.start.title": "You've made it!",
|
"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.body": "Following interesting people is what Mastodon is all about.",
|
||||||
"onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow",
|
"onboarding.steps.follow_people.title": "Personalize your home feed",
|
||||||
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
"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.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.body": "Boost your interactions by having a comprehensive profile.",
|
||||||
"onboarding.steps.setup_profile.title": "Customize your 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.body": "Let your friends know how to find you on Mastodon",
|
||||||
"onboarding.steps.share_profile.title": "Share your profile",
|
"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.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.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!",
|
"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",
|
"picture_in_picture.restore": "Put it back",
|
||||||
"poll.closed": "Closed",
|
"poll.closed": "Closed",
|
||||||
"poll.refresh": "Refresh",
|
"poll.refresh": "Refresh",
|
||||||
|
"poll.reveal": "See results",
|
||||||
"poll.total_people": "{count, plural, one {# person} other {# people}}",
|
"poll.total_people": "{count, plural, one {# person} other {# people}}",
|
||||||
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
|
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
|
||||||
"poll.vote": "Vote",
|
"poll.vote": "Vote",
|
||||||
|
@ -646,9 +654,7 @@
|
||||||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||||
"suggestions.dismiss": "Dismiss suggestion",
|
"suggestions.dismiss": "Dismiss suggestion",
|
||||||
"suggestions.header": "You might be interested in…",
|
"suggestions.header": "You might be interested in…",
|
||||||
"tabs_bar.federated_timeline": "Federated",
|
|
||||||
"tabs_bar.home": "Home",
|
"tabs_bar.home": "Home",
|
||||||
"tabs_bar.local_timeline": "Local",
|
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
||||||
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||||
import { combineReducers } from 'redux-immutable';
|
import { combineReducers } from 'redux-immutable';
|
||||||
|
|
||||||
|
@ -88,6 +90,22 @@ const reducers = {
|
||||||
followed_tags,
|
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 };
|
export { rootReducer };
|
||||||
|
|
|
@ -17,15 +17,15 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
server: ImmutableMap({
|
server: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
extendedDescription: ImmutableMap({
|
extendedDescription: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
domainBlocks: ImmutableMap({
|
domainBlocks: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -79,6 +79,10 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
firehose: ImmutableMap({
|
||||||
|
onlyMedia: false,
|
||||||
|
}),
|
||||||
|
|
||||||
community: ImmutableMap({
|
community: ImmutableMap({
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
|
|
|
@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
|
||||||
], (hidden, followingOrRequested, isSelf) => {
|
], (hidden, followingOrRequested, isSelf) => {
|
||||||
return hidden && !(isSelf || followingOrRequested);
|
return hidden && !(isSelf || followingOrRequested);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getStatusList = createSelector([
|
||||||
|
(state, type) => state.getIn(['status_lists', type, 'items']),
|
||||||
|
], (items) => items.toList());
|
||||||
|
|
|
@ -5,19 +5,6 @@ html {
|
||||||
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
|
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 {
|
.simple_form .button.button-tertiary {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
@ -436,26 +423,6 @@ html {
|
||||||
color: $white;
|
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 {
|
.flash-message.warning {
|
||||||
color: lighten($gold-star, 16%);
|
color: lighten($gold-star, 16%);
|
||||||
}
|
}
|
||||||
|
@ -653,11 +620,6 @@ html {
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
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,
|
.status__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -7,6 +7,12 @@ $classic-primary-color: #9baec8;
|
||||||
$classic-secondary-color: #d9e1e8;
|
$classic-secondary-color: #d9e1e8;
|
||||||
$classic-highlight-color: #6364ff;
|
$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
|
// Differences
|
||||||
$success-green: lighten(#3c754d, 8%);
|
$success-green: lighten(#3c754d, 8%);
|
||||||
|
|
||||||
|
@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed;
|
||||||
$ui-secondary-color: $classic-base-color !default;
|
$ui-secondary-color: $classic-base-color !default;
|
||||||
$ui-highlight-color: $classic-highlight-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;
|
$primary-text-color: $black !default;
|
||||||
$darker-text-color: $classic-base-color !default;
|
$darker-text-color: $classic-base-color !default;
|
||||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
||||||
|
|
|
@ -128,7 +128,6 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: darken($ui-base-color, 2%);
|
|
||||||
border-radius: 4px 0 0;
|
border-radius: 4px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,13 +145,9 @@ $content-width: 840px;
|
||||||
|
|
||||||
.simple-navigation-active-leaf a {
|
.simple-navigation-active-leaf a {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background-color: darken($ui-highlight-color, 2%);
|
background-color: $ui-highlight-color;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,12 +241,6 @@ $content-width: 840px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: $ui-highlight-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
Loading…
Reference in a new issue