Compare commits

..

220 Commits

Author SHA1 Message Date
renovate[bot]
cc5cf59d5c Update dependency pg to v1.5.5 (#29230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-16 11:08:01 +01:00
Claire
af6122f58e Allow JSON-LD documents with multiple profiles 2024-02-15 16:51:24 +01:00
Claire
325425780d Fix insufficient Content-Type checking of fetched ActivityStreams objects 2024-02-15 16:51:24 +01:00
Claire
8f36f89b28 Fix user creation failure handling in OAuth paths (#29207) 2024-02-14 23:13:19 +01:00
Claire
b0f01050c0 Fix OmniAuth tests (#29201) 2024-02-14 16:07:52 +01:00
Claire
fa96c733c4 Rename methods to avoid confusion between OAuth and OmniAuth 2024-02-14 14:47:45 +01:00
Claire
a4171ed3d1 Lock auth provider changes behind ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH=true 2024-02-14 14:47:45 +01:00
Claire
238d671908 Prevent different identities from a same SSO provider from accessing a same account 2024-02-14 14:47:45 +01:00
Claire
823d0806bc Improve performance of deleting OAuth tokens 2024-02-14 14:47:45 +01:00
Emelia Smith
e665e3b1f2 Ensure password resets revoke access to Streaming API 2024-02-14 14:47:45 +01:00
Emelia Smith
7f14e6f2b1 Ensure destruction of OAuth Applications notifies streaming
Due to doorkeeper using a dependent: delete_all relationship, the destroy of an OAuth Application bypassed the existing AccessTokenExtension callbacks for announcing destructing of access tokens.
2024-02-14 14:47:45 +01:00
Claire
217d45687f Add sidekiq_unique_jobs:delete_all_locks task and disable sidekiq-unique-jobs UI by default (#29199) 2024-02-14 13:49:39 +01:00
Emelia Smith
1625c82d77 Disable administrative doorkeeper routes (#29187) 2024-02-14 13:49:39 +01:00
renovate[bot]
4c8955f6fe Update dependency sidekiq-unique-jobs to v7.1.33 (#29175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-14 13:49:38 +01:00
renovate[bot]
9f685534d1 Update dependency nokogiri to v1.16.2 [SECURITY] (#29106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-14 13:49:16 +01:00
Claire
810514747b Fix insufficient origin validation 2024-02-01 15:10:01 +01:00
Claire
18856371be Merge pull request #2520 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to f476d9dab2
2023-12-18 13:19:45 +01:00
Claire
33dd5d8179 Fix Ruby lint issues 2023-12-17 23:17:55 +01:00
Claire
4113fbf6e8 Merge commit 'f476d9dab2f5cca6ae44b95961df6b6557d66dab' into glitch-soc/merge-upstream
Conflicts:
- `lib/sanitize_ext/sanitize_config.rb`:
  Upstream enforced new code style rules, where we had different code.
  Applied the new code style rules.
2023-12-17 23:04:16 +01:00
Claire
c8fe36c349 Merge pull request #2517 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 3bf896c973
2023-12-17 20:06:18 +01:00
mogaminsk
cc265f760e [Glitch] Fix inserting emojis from emoji picker fails with TypeError
Port ac8e4ed38d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-17 18:03:22 +01:00
Claire
bb4fa0c374 [Glitch] Rewrite AutosuggestTextarea as Functional Component
Port 9c8891b39a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-17 17:59:33 +01:00
Renaud Chaput
e22c3cd768 [Glitch] Improve Babel configuration and automatically load polyfills
Port 0e3401bc1c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-17 17:50:31 +01:00
Claire
ddf3ad9541 Merge commit '3bf896c973404261f4f7b25c25ea22adb1a85e7d' into glitch-soc/main
Conflicts:
- `package.json`:
  Upstream removed a dependency textually close to a glitch-only dependency.
  Updated as upstream while keeping our dependency.
2023-12-17 17:43:30 +01:00
Claire
537b88330d Merge pull request #2514 from ClearlyClaire/glitch-soc/main
Merge upstream changes up to a916251d8a
2023-12-17 17:42:22 +01:00
Claire
f62bafc7a1 Fix HAML linting issue 2023-12-17 16:37:11 +01:00
Claire
1474318691 Merge commit 'a916251d8a8fffcaeb6be80eacf50138a53650dc' into glitch-soc/main
Conflicts:
- `app/models/trends/statuses.rb`:
  Upstream fixed a bug in the trending post condition.
  Glitch-soc's condition is different because we potentially allow CWed content
  to trend.
  Ported upstream's fix while keeping glitch-soc's change.
- `config/initializers/content_security_policy.rb`:
  Kept our version for now, we will switch to upstream later down the road.
2023-12-17 15:32:29 +01:00
Claire
b7248485b1 Merge pull request #2477 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 2e6bf60f15
2023-12-12 19:45:18 +01:00
Claire
9f92b05bd2 Merge commit '2e6bf60f1549e5c1f1cfea2d614f978bea17b8a2' into glitch-soc/merge-upstream
Conflicts:
- `README.md`:
  Upstream has updated their README but we have a completely different one.
  Kept our version of `README.md`
2023-12-10 18:05:02 +01:00
Claire
98f50429d5 Merge pull request #2511 from ClearlyClaire/glitch-soc/cleanup-2
Further reduce code differences with upstream
2023-12-10 18:03:04 +01:00
Claire
df5c64fe57 Further reduce differences with upstream 2023-12-09 21:19:43 +01:00
Claire
cc1d68ace8 [Glitch] Show announcements in reverse chronological order
Port f1f0400adc and 8e2530ea16 to glitch-soc

Co-Authored-By: Darius Kazemi <darius.kazemi@gmail.com>
2023-12-09 21:19:43 +01:00
Claire
4b2ddaf106 Further reduce differences with upstream 2023-12-09 21:19:43 +01:00
Claire
408d4710ed Further reduce differences with upstream 2023-12-09 20:58:50 +01:00
Claire
a27abb4802 Further reduce code differences with upstream (#2509) 2023-12-09 20:29:23 +01:00
Claire
1ddf2012ee Fix status avatar size discrepancies (#2510)
Follow-up to #2508
2023-12-09 20:29:15 +01:00
Claire
b2647bc3f2 [Glitch] Update Avatar, AvatarComposite, and AvatarOverlay components (#2508)
Various ports including 8dfe5179ee,
d1de7fb7fa and
9f8d34620b.

Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
Co-authored-by: fusagiko / takayamaki <24884114+takayamaki@users.noreply.github.com>
2023-12-09 18:33:42 +01:00
Claire
c0e562916c Fix glitch-soc being uninstallable because of yanked dependency (#2507) 2023-12-06 13:32:27 +01:00
Claire
9fcf5d4192 [Glitch] Fix emoji picker button scrolling with textarea content in single-column view (#2501)
Port bbea052935 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-04 13:30:27 +01:00
Essem
a46b6af1d6 Fix constant redirects to onboarding page (#2505) 2023-12-04 13:30:17 +01:00
Claire
23ee393fdd Merge pull request #2499 from ClearlyClaire/glitch-soc/port-toasts
Port upstream's toast changes
2023-12-03 20:57:42 +01:00
Claire
13902903d3 Merge pull request #2497 from ClearlyClaire/glitch-soc/ports/account_notes-typescript
Port upstream's TypeScript refactor of account_notes
2023-12-03 20:57:23 +01:00
Eugen Rochko
cede2f533c [Glitch] Fix toast saying "published" instead of "saved" after editing post in web UI
Port 71641766f2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 19:39:08 +01:00
Renaud Chaput
9ac73a1fbf [Glitch] Change eslint config to autofix missing comma and indentation in JS files
Partial port of 774e1189d2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 16:44:40 +01:00
Christian Schmidt
ea004108b8 [Glitch] Make notification respect reduce-motion
Port 6d0767558a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 16:38:41 +01:00
Stanislas Signoud
811b8b200e [Glitch] Use invariant colors on notification toasts
Port ca955ada0b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 16:38:41 +01:00
Eugen Rochko
3bbe39f233 [Glitch] Add toast with option to open post after publishing in web UI
Port a7ca33ad96 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 16:38:41 +01:00
Claire
046cb408b7 [Glitch] Fix front-end bug when processing relationship-related account actions
Port 287520453c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 13:19:10 +01:00
Renaud Chaput
c3a0d5aca3 [Glitch] Fix Redux types
Port 0712cc2b99 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 13:19:10 +01:00
Renaud Chaput
6fb5fafd28 [Glitch] Convert actions/account_notes into Typescript
Port bd06c13204 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 13:19:10 +01:00
Claire
c82d4cfb71 Merge pull request #2493 from ClearlyClaire/glitch-soc/even-more-painful-backports
Port onboarding changes from upstream
2023-12-03 13:18:50 +01:00
Claire
ee58f680e8 [Glitch] Fix autocomplete suggestions being cut off in compose form
Port 6833732852 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Claire
6269a5336f [Glitch] Change composer highlight border size to be more noticeable
Port aa4c9730f6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Claire
204fe83fcc [Glitch] Fix compose textarea scroll behavior
Port 746979f75d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Claire
a8f6a5b40f Update translation strings 2023-12-03 11:23:12 +01:00
Claire
786b42e2b5 Fix SCSS and JS linting issues 2023-12-03 11:23:12 +01:00
Claire
a0943b8f6d Remove glitch-soc's old onboarding modal 2023-12-03 11:23:12 +01:00
Renaud Chaput
6dc812dd51 [Glitch] Upgrade to react-router v5 in onboarding code
Port 1b70d7ed7c to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Eugen Rochko
06e819537b [Glitch] Change labels and styles on the onboarding screen in web UI
Port a985d587e1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Eugen Rochko
d5bad93460 [Glitch] Change "Follow 7 people" to "Find at least 7 people to follow" in web UI
Port 4a5464f360 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
たいち ひ
c2c25122e8 [Glitch] Rewrite <Check /> as FC
Port 6fdbee240c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Renaud Chaput
a1667ba796 [Glitch] Upgrade react-intl usage in onboarding code
Port remaining of 44cd88adc4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Claire
e47c582283 Fix import order 2023-12-03 11:23:12 +01:00
Renaud Chaput
ba4c8a9b41 [Glitch] Use the new JSX transform in onboarding code
Port the remaining of 8f66126b10 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Steven Munn
6e4497ab2d [Glitch] Fix spelling of "Lets" on the onboarding page after clicking the confirmation email
Port 52d36f0f98 to glitch-soc

Co-authored-by: Steven Munn <stevenjmunn@gmail.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Emelia Smith
1cb8df655b [Glitch] Fix Onboarding Errors
Port b8a2430642 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Renaud Chaput
0b1556b7f7 [Glitch] Enforce stricter rules for Typescript files
Port c8181eb0a4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Eugen Rochko
542f2fb1e0 [Glitch] Add default post text to onboarding flow in web UI
Port 8979b70975 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Eugen Rochko
e0b401e295 [Glitch] Add more tips to onboarding flow in web UI
Port c35e3cb6ac to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Eugen Rochko
4537b4b961 [Glitch] Add new onboarding flow to web UI
Port 0461f83320 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-03 11:23:12 +01:00
Claire
335cfab32f Change account note design to match upstream's (#2495) 2023-12-03 11:22:38 +01:00
Claire
046141d2a4 Fix i18n unused check being tripped by no in YAML files (#2496) 2023-12-03 11:21:34 +01:00
Claire
f1241b4a3a Fix translation string for status.favourite not having been changed everywhere (#2494) 2023-12-03 09:51:29 +01:00
Claire
edd96ce786 Merge pull request #2492 from ClearlyClaire/glitch-soc/painful-backports
Port account rows design change from upstream
2023-12-03 09:51:07 +01:00
Claire
21df2a68ac Hide followers count when hidden by instance or user 2023-12-02 17:38:07 +01:00
Claire
09062d393f Fix more styling issues 2023-12-02 17:38:07 +01:00
Claire
28d4f3ab70 Fix account component styling wrt. upstream 2023-12-02 17:05:54 +01:00
fusagiko / takayamaki
dc917cfcdf [Glitch] Fix account.jsx imports (#25541)
Port remaining part of e0d230fb37,
20e85c0e83 and
9d45a444f9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Eugen Rochko
17372a3ec0 [Glitch] Change labels and styles on the onboarding screen in Account component
Partial port of a985d587e1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Eugen Rochko
0421b44f22 [Glitch] Change follow button in account row to be more obvious in web UI (#24956)
Port 0ddc895282 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Claire
f1691eca55 [Glitch] Fix overflow behavior of account rows
Port 5fae2de454 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Renaud Chaput
98a7b95058 [Glitch] Update inconsistent defaultMessage in Account component
Partial port of e58c36d308 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Claire
b047b981cd [Glitch] Enforce import order with ESLint
Port d27216dc46 to glitch-soc
2023-12-02 16:59:40 +01:00
Emelia Smith
b8cbaba283 [Glitch] Split EmptyAccount out of Account component
Partial port of b8a2430642 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Claire
32ec0d2472 [Glitch] Fix verified badge in account lists potentially including rel="me" links
Port 55e7c08a83 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Renaud Chaput
e7ec2641a0 [Glitch] Fix linting issues with VerifiedBadge component
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
fusagiko / takayamaki
2efb22f455 [Glitch] Rewrite VerifiedBadge component as function component
Port 140aa6b054 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Eugen Rochko
00db5c8ade [Glitch] Split VerifiedBadge from Account component
Partial port of 0461f83320 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Eugen Rochko
e0aba64a64 [Glitch] Fix regressions from change in account row design in web UI
Port 46483ae849

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Eugen Rochko
a262f990f8 Change design of account rows in web UI (#24247)
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-12-02 16:59:40 +01:00
Claire
73a94c3b3f Fix self-destruct page not using theme styles (#2490) 2023-12-02 14:54:35 +01:00
Plastikmensch
5b0382abc5 Remove redundant asset preload (#2488)
* Remove redundant asset preload

preloading these is already handled by glitch-soc theming system, meaning glitch packs get preloaded and if the user is signed in, vanilla packs.

The theming system preloads these unconditionally though.

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

* Remove preload of getting started

This matches upstreams preloads

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

---------

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2023-12-02 14:54:26 +01:00
Claire
660372d130 Revert recent CSP changes (#2485)
* Revert "Fix image and media loading when using external storage server"

This reverts commit 6cfa0245ca.

* Revert "Change glitch-soc's CSP config to match upstream's closer (#2474)"

This reverts commit d59196e170.
2023-11-26 15:32:35 +01:00
Jeong Arm
b3581d1e2f Fix @rails/ujs import on public.jsx (#2482)
Related: 8a131fb7bc
2023-11-24 17:40:31 +01:00
Claire
a21fe8687e Merge pull request #2480 from ClearlyClaire/glitch-soc/fixes/csp
Fix image and media loading when using external storage server
2023-11-21 17:36:09 +01:00
Claire
6cfa0245ca Fix image and media loading when using external storage server
Fixes #2479
2023-11-21 13:45:29 +01:00
Claire
769ab0ce47 Merge pull request #2475 from ClearlyClaire/glitch-soc/cleanup
Further reduce differences with upstream
2023-11-20 13:39:56 +01:00
Claire
f00fcda785 Reduce differences with upstream in Account component 2023-11-20 13:24:02 +01:00
Claire
9ab1aa15e9 Change ReplyIndicator implementation and markup to match upstream's 2023-11-20 13:24:02 +01:00
Claire
d59196e170 Change glitch-soc's CSP config to match upstream's closer (#2474) 2023-11-20 13:02:49 +01:00
Claire
c34a3a83e1 Merge pull request #2471 from ClearlyClaire/glitch-soc/cleanup
Clean up some more and reduce unwarranted differences with upstream further
2023-11-19 21:05:34 +01:00
Claire
d3ae5b21d2 Reduce code and markup discrepancies on reply indicator 2023-11-16 18:33:16 +01:00
Claire
e023acfd00 Remove unnecessary proptype discrepancy 2023-11-16 17:57:13 +01:00
Claire
7f5b164326 Merge pull request #2470 from ClearlyClaire/glitch-soc/cleanup
Clean up some more and reduce unwarranted differences with upstream further
2023-11-16 17:49:36 +01:00
Claire
36f25ea067 Fix more import discrepancies 2023-11-15 22:50:23 +01:00
Claire
4101057b9a Fix various code discrepancies 2023-11-15 22:50:23 +01:00
Claire
09a5a78527 Fix discrepancy for autosuggest-emoji class 2023-11-15 22:50:23 +01:00
Claire
f69f1e9429 Fix code discrepancies with upstream 2023-11-15 21:52:19 +01:00
Claire
d358a3cc61 Merge pull request #2468 from ClearlyClaire/glitch-soc/cleanup
Clean up some more and reduce unwarranted differences with upstream further
2023-11-15 21:49:50 +01:00
Claire
61df4f9cbf Merge pull request #2469 from ClearlyClaire/glitch-soc/fixes/compose-form-checkbox
[Glitch] Change compose form checkbox to native input with `appearance: none`
2023-11-15 20:27:37 +01:00
Claire
0d877a3076 [Glitch] Change compose form checkbox to native input with appearance: none
Port d3b4d4d4f3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 17:20:44 +01:00
Claire
e7b3598f04 No need to have a separate localStorage key for home instance 2023-11-15 17:16:21 +01:00
Claire
0c727a6790 Fix more import style discrepancies 2023-11-15 17:16:21 +01:00
Claire
50eb673494 Fix some markup discrepancies 2023-11-15 17:16:21 +01:00
Claire
349579e318 Fix more code discrepancies 2023-11-15 17:16:21 +01:00
Claire
1023c2f90b Fix more whitespace and comment discrepancies 2023-11-15 17:16:21 +01:00
Claire
7e5d00720b Merge pull request #2391 from ClearlyClaire/glitch-soc/port-upstream-hashtags
Port upstream's hashtag handling to glitch-soc
2023-11-15 17:15:21 +01:00
Claire
7a877ae3d8 Merge pull request #2467 from ClearlyClaire/glitch-soc/cleanup
Misc cleanup and backports
2023-11-15 17:14:14 +01:00
Claire
cd3a636b7f [Glitch] Fix some remote posts getting truncated
Port 4d59dfb1c6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:59:06 +01:00
Eugen Rochko
6a8623588a [Glitch] Fix colors and typography on hashtag bar in web UI
Port 10b06436d1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Claire
c226d00490 Fix interaction between CWs and hashtag bars 2023-11-15 12:56:58 +01:00
Claire
7ae45676c8 Fix hashtag bar styling 2023-11-15 12:56:58 +01:00
Claire
45690b01d1 [Glitch] Change hashtag bar tags to be de-emphasized
Port 613cfd625c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Renaud Chaput
fc514fa8c6 [Glitch] Better hashtag normalization when processing a post
Port 58acaa9ae6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Renaud Chaput
18462ee4b6 [Glitch] Remove hashtags from the last line of a status if it only contains hashtags
Port 061fd66ee6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Claire
2ce03420d6 [Glitch] Fix case-insensitive comparison of hashtags to do case-folding
Port 3ed2bf92d0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Claire
fe8d9f6221 [Glitch] Fix hashtag bar sometimes including tags that appear in the post's body
Port f0862bcf98

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Claire
a81ed84453 [Glitch] Add display of out-of-band hashtags in the web interface
Port df6e719898 to glitch-soc

Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:56:58 +01:00
Claire
3b210e093a Fix more code style discrepancies 2023-11-15 12:53:21 +01:00
Claire
542d95c2bc Fix import style discrepancy 2023-11-15 12:53:21 +01:00
Claire
8b24a9a507 Fix missed relative import discrepancy 2023-11-15 12:53:21 +01:00
Claire
b1f0457cb8 Fix whitespace and comment discrepancies 2023-11-15 12:53:21 +01:00
Eugen Rochko
08ac91c40b [Glitch] Fix follow relationships not loading after notifications fetch
Port 4f7f6b3922 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:53:21 +01:00
Eugen Rochko
103c0ca4f7 [Glitch] Hide loading bar on status interactions
Port eb2425b53b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-11-15 12:53:21 +01:00
Claire
21653beb30 Reduce more unwarranted differences with upstream 2023-11-15 12:53:21 +01:00
Claire
7b922c2d90 Reduce unwarranted differences with upstream
Ports part of 0758b00bfd and 897199910f
2023-11-15 12:53:21 +01:00
Claire
2f61b65b1e Remove dead code 2023-11-15 12:53:11 +01:00
Claire
9bbb0f13e0 Fix inconsistent React imports in JSX files (#2466) 2023-11-15 12:52:30 +01:00
Claire
a97b722ad5 Reduce differences with upstream due to import style (#2465)
Fix relative / VS absolute style imports, and fix whitespace discrepancies
2023-11-15 12:01:51 +01:00
Claire
f7d3b74e03 Merge pull request #2389 from glitch-soc/i18n/crowdin/translations
New Crowdin Translations (automated)
2023-11-15 09:42:30 +01:00
Claire
02582f7e50 Merge pull request #2464 from neatchee/pr/glitch/mobile_singlecolumn_warning_fix
Fix recurring "switch to advanced interface" warning by adding missing…
2023-11-15 07:21:16 +01:00
neatchee
bd6da814b5 Fix recurring "switch to advanced interface" warning by adding missing class to the navigation-panel__banner element 2023-11-13 13:20:51 -08:00
GitHub Actions
46a28fc41f New Crowdin translations 2023-11-11 04:27:29 +00:00
Renaud Chaput
f476d9dab2 Fix the notificationsUpdate call (#27758) 2023-11-07 14:18:59 +00:00
Matt Jankowski
45770c9306 Fix Performance/MapMethodChain cop (#27744) 2023-11-07 13:01:09 +00:00
Matt Jankowski
bbad5b6456 Remove false positive cop detection (#27457) 2023-11-07 10:44:15 +00:00
Matt Jankowski
49e2772064 Fix RSpec/MessageSpies cop (#27751) 2023-11-07 09:46:28 +00:00
Renaud Chaput
ae0d551d33 Do not copy public/packs-test into Docker (#27736) 2023-11-07 09:22:04 +00:00
Matt Jankowski
2862ad701f Stub controller methods and remove rubocop:disable in captcha feature spec (#27743) 2023-11-07 09:15:30 +00:00
Matt Jankowski
2d39268bc5 Fix Lint/OrAssignmentToConstant cop (#27750) 2023-11-07 09:11:50 +00:00
Matt Jankowski
cfa14ec6d1 Fix Lint/EmptyBlock cop (#27748) 2023-11-07 09:11:04 +00:00
Matt Jankowski
b06284c572 Fix RSpec/HookArgument cop (#27747) 2023-11-07 09:10:36 +00:00
github-actions[bot]
1b28ab7263 New Crowdin Translations (automated) (#27687)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-11-07 09:03:59 +00:00
Matt Jankowski
d6f50839e1 Fix RSpec/SpecFilePathFormat cops (#27730) 2023-11-06 16:25:40 +00:00
Matt Jankowski
c501d626e8 Fix Rails/BulkChangeTable cop (#26890) 2023-11-06 16:15:48 +00:00
Matt Jankowski
0c4e7c06dc Fix Rails/FindEach cop (#26886) 2023-11-06 15:53:29 +00:00
Matt Jankowski
fe26f33e0a Fix Rails/RedundantActiveRecordAllMethod cop (#26885) 2023-11-06 15:51:52 +00:00
Matt Jankowski
949f5eb860 Fix RSpec/MetadataStyle cop in spec/ (#27729) 2023-11-06 14:28:20 +00:00
zunda
e4e752c26e Adjust transform origin for favorite star (#27700) 2023-11-06 11:14:46 +00:00
renovate[bot]
9c34bb4d54 Update Node.js to v20.9 (#27714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 10:27:42 +00:00
renovate[bot]
0ca27b2d81 Update DefinitelyTyped types (non-major) (#27713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 10:27:19 +00:00
renovate[bot]
22b4713d2e Update eslint (non-major) (#27715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 10:26:18 +00:00
Renaud Chaput
3bf896c973 Disable Babel polyfill injection in dev (#27691) 2023-11-06 10:24:41 +00:00
renovate[bot]
1416745a2b Update dependency faker to v3.2.2 (#27718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 09:54:59 +00:00
renovate[bot]
afd1371fa3 Update dependency rubocop to v1.57.2 (#27719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 09:53:13 +00:00
renovate[bot]
ef140da349 Update dependency discard to v1.3.0 (#27720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 09:52:50 +00:00
Claire
c0989b78f8 Fix incoming status creation date not being restricted to standard ISO8601 (#27655) 2023-11-06 09:28:14 +00:00
Renaud Chaput
6712bf86cd Fixes website not loading for unlogged users (#27698) 2023-11-04 21:52:56 +00:00
Renaud Chaput
3bf2a7296e Use Immutable Record for accounts in Redux state (#26559) 2023-11-03 15:00:03 +00:00
Matt Jankowski
9d799d40ba Reduce CI job matrix job count (#27660) 2023-11-03 14:58:33 +00:00
Matt Jankowski
5d9e71ebe0 Archive uploaded CI assets into single file between build/test (#27668) 2023-11-03 14:12:14 +00:00
github-actions[bot]
2d548e273e New Crowdin Translations (automated) (#27646)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-11-03 10:10:13 +00:00
Claire
0337df3a42 Fix posts from threads received out-of-order sometimes not being inserted into timelines (#27653) 2023-11-02 14:58:37 +00:00
Renaud Chaput
2aa28e06d1 Mark version 4.0 as no longer supported (#27627) 2023-11-01 13:53:14 +00:00
mogaminsk
ac8e4ed38d Fix inserting emojis from emoji picker fails with TypeError (#27647) 2023-11-01 07:22:02 +00:00
Renaud Chaput
277e6968f5 Use helpers to check environment in frontend (#27633) 2023-10-31 16:05:44 +00:00
Matt Jankowski
7ef56d6e50 Move json_ld context loaders to config/initializers (#27590) 2023-10-31 15:21:23 +00:00
Matt Jankowski
3107a9410c Silence deprecation warning about secrets/credentials with Devise patch (#27578) 2023-10-31 11:10:15 +00:00
Matt Jankowski
b264318431 Update strong_migrations to version 1.3.0 (#25991) 2023-10-31 11:09:32 +00:00
Renaud Chaput
0e3401bc1c Improve Babel configuration and automatically load polyfills (#27333) 2023-10-31 10:55:13 +00:00
Claire
9c8891b39a Rewrite AutosuggestTextarea as Functional Component (#27618) 2023-10-31 10:17:37 +00:00
Matt Jankowski
a916251d8a Update haml-lint line length configuration to match rubocop value (#27570) 2023-10-31 09:47:16 +00:00
Claire
d649bbf28f Add some more tests and clean up domain block controller (#27469) 2023-10-31 09:40:30 +00:00
github-actions[bot]
e5b7ae9576 New Crowdin Translations (automated) (#27630)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-10-31 08:43:17 +00:00
renovate[bot]
147417a6d0 Update dependency rspec-sidekiq to v4.1.0 (#27593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 08:26:21 +00:00
renovate[bot]
204c00b5c6 Update dependency bootsnap to '~> 1.17.0' (#27617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 08:24:04 +00:00
Matt Jankowski
beee9ea991 Fix RSpec/LetSetup cop in spec/controllers/admin area (#27619) 2023-10-31 08:22:19 +00:00
Claire
6c52f8286b Fix posts from force-sensitized accounts being able to trend (#27620) 2023-10-30 22:32:25 +00:00
renovate[bot]
b8adb08f92 Update dependency axios to v1.6.0 (#27580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 22:31:32 +00:00
renovate[bot]
372494e553 Update dependency punycode to v2.3.1 (#27625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 22:31:22 +00:00
renovate[bot]
547d3c1b9b Update dependency core-js to v3.33.2 (#27624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 22:30:26 +00:00
renovate[bot]
c2cc1df5ef Update dependency @types/react to v18.2.33 (#27615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 22:30:02 +00:00
github-actions[bot]
a918208ec6 New Crowdin Translations (automated) (#27596)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-10-30 14:19:25 +00:00
renovate[bot]
bf1d452978 Update libretranslate/libretranslate Docker tag to v1.4.1 (#27616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 14:19:20 +00:00
Ricardo Trindade
33f8c1c5eb Remove version check from update cache_concern.rb (#27592) 2023-10-30 14:04:12 +00:00
Matt Jankowski
eae5c7334a Extract class from CSP configuration/initialization (#26905) 2023-10-27 16:20:40 +00:00
Matt Jankowski
2e6bf60f15 Use deliveries.size in mailer-related examples in controller specs (#27589) 2023-10-27 15:33:52 +00:00
Jonathan de Jong
1cc512909c Have Follow activities bypass availability (#27586)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-10-27 14:55:00 +00:00
Claire
93e4cdc31b Fix hashtag matching pattern matching some URLs (#27584) 2023-10-27 14:04:51 +00:00
SouthFox
08bdd5751e Fix account click on detailed status (#27587) 2023-10-27 14:03:21 +00:00
renovate[bot]
15ef654e9a Update dependency pundit to v2.3.1 (#27585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 13:43:00 +00:00
Renaud Chaput
13d310e64d Simplify column headers (#27557) 2023-10-27 13:21:07 +00:00
Matt Jankowski
1f5187e2e2 Misc spec/refactor to user mailer and user mailer spec (#27486) 2023-10-27 09:57:16 +00:00
renovate[bot]
37929b9707 Update libretranslate/libretranslate Docker tag to v1.4.0 (#27504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 09:56:45 +00:00
github-actions[bot]
8ca16f032e New Crowdin Translations (automated) (#27583)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-10-27 09:38:04 +00:00
Claire
bbf46cc418 Fix error and incorrect URLs in /api/v1/accounts/:id/featured_tags for remote accounts (#27459) 2023-10-27 08:35:21 +00:00
Jeong Arm
8f998cd96a Handle featured collections without items (#27581) 2023-10-27 02:36:22 +00:00
Eugen Rochko
fa7e64df1d Fix various icon styles in web UI (#27579) 2023-10-26 23:37:58 +00:00
Matt Jankowski
12550a6a28 Use Rails.env.local? shorthand method to check env (#27519) 2023-10-26 21:20:41 +00:00
Matt Jankowski
4aa05d45fc Capture minimum postgres version 12 (#27528) 2023-10-26 20:35:15 +00:00
Simon Rapilly
2d8f759a34 Add HTML lang attribute to preview card descriptions (#27503) 2023-10-26 20:34:15 +00:00
Claire
d2f52f7f64 Fix report processing notice not mentioning the report number when performing a custom action (#27442) 2023-10-26 17:03:31 +00:00
Mark T. Tomczak
ba8dcb50fe Issue 26048: swap "muting" and "blocking" list options in settings -> Data Exports (#26088) 2023-10-26 14:08:25 +00:00
Claire
75255c01fc Fix error when trying to delete already-deleted file with OpenStack Swift (#27569) 2023-10-26 13:09:48 +00:00
github-actions[bot]
3427b51d63 New Crowdin Translations (automated) (#27567)
Co-authored-by: GitHub Actions <noreply@github.com>
2023-10-26 11:05:47 +00:00
Renaud Chaput
537442853f Use a context to propagate column-related Props, and remove forceUpdate usage (#27548) 2023-10-26 11:00:10 +00:00
Matt Jankowski
3ca974e101 Use next keyword in field loop in admin/accounts/index view (#27559) 2023-10-26 10:52:14 +00:00
renovate[bot]
400f5c9174 Update dependency @material-symbols/svg-600 to v0.13.2 (#27565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 10:49:39 +00:00
renovate[bot]
6e018f7228 Update dependency sass to v1.69.5 (#27566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 10:48:55 +00:00
Claire
49b8433c56 Fix confusing screen when visiting a confirmation link for an already-confirmed email (#27368) 2023-10-25 21:33:44 +00:00
628 changed files with 6986 additions and 4599 deletions

View File

@@ -70,7 +70,7 @@ services:
hard: -1
libretranslate:
image: libretranslate/libretranslate:v1.3.12
image: libretranslate/libretranslate:v1.4.1
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local

View File

@@ -8,6 +8,7 @@
public/system
public/assets
public/packs
public/packs-test
node_modules
neo4j
vendor/bundle

View File

@@ -1,5 +1,5 @@
# Node.js
NODE_ENV=tests
# In test, compile the NodeJS code as if we are in production
NODE_ENV=production
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true

View File

@@ -48,12 +48,15 @@ jobs:
run: |-
./bin/rails assets:precompile
- name: Archive asset artifacts
run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
- uses: actions/upload-artifact@v3
if: matrix.mode == 'test'
with:
path: |-
./public/assets
./public/packs-test
./artifacts.tar.gz
name: ${{ github.sha }}
retention-days: 0
@@ -102,7 +105,6 @@ jobs:
SAML_ENABLED: true
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test'
CI_JOBS: ${{ matrix.ci_job }}/4
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
strategy:
@@ -112,19 +114,18 @@ jobs:
- '3.0'
- '3.1'
- '.ruby-version'
ci_job:
- 1
- 2
- 3
- 4
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
path: './public'
path: './'
name: ${{ github.sha }}
- name: Expand archived asset artifacts
run: |
tar xvzf artifacts.tar.gz
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
with:
@@ -134,7 +135,7 @@ jobs:
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- run: bundle exec rake rspec_chunked
- run: bin/rspec
test-e2e:
name: End to End testing

View File

@@ -12,3 +12,5 @@ linters:
enabled: true
MiddleDot:
enabled: true
LineLength:
max: 320

View File

@@ -1,21 +1,34 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2023-10-25 08:29:48 -0400 using Haml-Lint version 0.51.0.
# on 2023-10-26 09:32:34 -0400 using Haml-Lint version 0.51.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 945
# Offense count: 16
LineLength:
enabled: false
exclude:
- 'app/views/admin/account_actions/new.html.haml'
- 'app/views/admin/accounts/index.html.haml'
- 'app/views/admin/ip_blocks/new.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/settings/discovery/show.html.haml'
- 'app/views/auth/registrations/edit.html.haml'
- 'app/views/auth/registrations/new.html.haml'
- 'app/views/filters/_filter_fields.html.haml'
- 'app/views/media/player.html.haml'
- 'app/views/settings/applications/_fields.html.haml'
- 'app/views/settings/imports/index.html.haml'
- 'app/views/settings/preferences/appearance/show.html.haml'
- 'app/views/settings/preferences/notifications/show.html.haml'
- 'app/views/settings/preferences/other/show.html.haml'
# Offense count: 10
# Offense count: 9
RuboCop:
exclude:
- 'app/views/admin/accounts/_buttons.html.haml'
- 'app/views/admin/accounts/_local_account.html.haml'
- 'app/views/admin/accounts/index.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/layouts/application.html.haml'

View File

@@ -27,7 +27,7 @@ AllCops:
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
- 'lib/json_ld/*' # Generated files
- 'config/initializers/json_ld*' # Generated files
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
- 'lib/templates/**/*'
@@ -109,16 +109,11 @@ Rails/Exit:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath
RSpec/FilePath:
CustomTransform:
ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
JsonLdHelper: jsonld_helper
OEmbedController: oembed_controller
OStatus: ostatus
NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances
Exclude:
- 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder
- 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder
# Reason:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject
@@ -135,6 +130,16 @@ RSpec/NotToNot:
RSpec/Rails/HttpStatus:
EnforcedStyle: numeric
# Reason: Match overrides from Rspec/FilePath rule above
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
RSpec/SpecFilePathFormat:
CustomTransform:
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OEmbedController: oembed_controller
OStatus: ostatus
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
Style/ClassAndModuleChildren:

View File

@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.57.1.
# using RuboCop version 1.57.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -20,25 +20,10 @@ Layout/LineLength:
Exclude:
- 'app/models/account.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
Exclude:
- 'spec/controllers/api/v2/search_controller_spec.rb'
- 'spec/fabricators/access_token_fabricator.rb'
- 'spec/fabricators/conversation_fabricator.rb'
- 'spec/fabricators/system_key_fabricator.rb'
- 'spec/lib/activitypub/adapter_spec.rb'
- 'spec/models/user_role_spec.rb'
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/jsonld_helper.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/OrAssignmentToConstant:
Exclude:
- 'lib/sanitize_ext/sanitize_config.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@@ -67,13 +52,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity:
Max: 27
Performance/MapMethodChain:
Exclude:
- 'app/models/feed.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'spec/services/bulk_import_service_spec.rb'
- 'spec/services/import_service_spec.rb'
RSpec/AnyInstance:
Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
@@ -96,20 +74,6 @@ RSpec/AnyInstance:
RSpec/ExampleLength:
Max: 22
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
Exclude:
- 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/well_known/webfinger_controller_spec.rb'
- 'spec/helpers/instance_helper_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/rails_helper.rb'
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/import_service_spec.rb'
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Exclude:
@@ -132,11 +96,6 @@ RSpec/InstanceVariable:
RSpec/LetSetup:
Exclude:
- 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/controllers/admin/action_logs_controller_spec.rb'
- 'spec/controllers/admin/instances_controller_spec.rb'
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
- 'spec/controllers/admin/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/filters_controller_spec.rb'
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
@@ -183,24 +142,6 @@ RSpec/MessageChain:
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
# Configuration parameters: EnforcedStyle.
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
Exclude:
- 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/helpers/admin/account_moderation_notes_helper_spec.rb'
- 'spec/lib/webfinger_resource_spec.rb'
- 'spec/models/admin/account_action_spec.rb'
- 'spec/models/concerns/remotable_spec.rb'
- 'spec/models/follow_request_spec.rb'
- 'spec/models/identity_spec.rb'
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/services/activitypub/fetch_replies_service_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/spec_helper.rb'
- 'spec/validators/status_length_validator_spec.rb'
RSpec/MultipleExpectations:
Max: 8
@@ -217,13 +158,6 @@ Rails/ApplicationController:
Exclude:
- 'app/controllers/health_controller.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Severity.
Rails/DuplicateAssociation:
Exclude:
- 'app/serializers/activitypub/collection_serializer.rb'
- 'app/serializers/activitypub/note_serializer.rb'
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasAndBelongsToMany:
@@ -338,7 +272,6 @@ Rails/SkipsModelValidations:
- 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
- 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
- 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/main.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'spec/lib/activitypub/activity/follow_spec.rb'
- 'spec/services/follow_service_spec.rb'
@@ -431,7 +364,6 @@ Style/FetchEnvVar:
- 'config/initializers/3_omniauth.rb'
- 'config/initializers/blacklists.rb'
- 'config/initializers/cache_buster.rb'
- 'config/initializers/content_security_policy.rb'
- 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb'

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.4
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
ARG NODE_VERSION="20.8-bookworm-slim"
ARG NODE_VERSION="20.9-bookworm-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
FROM node:${NODE_VERSION} as build

View File

@@ -23,7 +23,7 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.16.0', require: false
gem 'bootsnap', '~> 1.17.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'
@@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2'
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 3.0.1'
gem 'strong_migrations', '~> 0.8'
gem 'strong_migrations', '1.3.0'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'
@@ -103,9 +103,6 @@ gem 'rdf-normalize', '~> 0.5'
gem 'private_address_check', '~> 0.5'
group :test do
# Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6'
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false

View File

@@ -154,6 +154,7 @@ GEM
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
bcp47_spec (0.2.1)
bcrypt (3.1.19)
better_errors (2.10.1)
erubi (>= 1.0.0)
@@ -171,7 +172,7 @@ GEM
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
blurhash (0.1.7)
bootsnap (1.16.0)
bootsnap (1.17.0)
msgpack (~> 1.2)
brakeman (6.0.1)
browser (5.3.1)
@@ -235,7 +236,7 @@ GEM
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.5.0)
discard (1.2.1)
discard (1.3.0)
activerecord (>= 4.2, < 8)
docile (1.4.0)
domain_name (0.5.20190701)
@@ -264,7 +265,7 @@ GEM
tzinfo
excon (0.100.0)
fabrication (2.30.0)
faker (3.2.1)
faker (3.2.2)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@@ -375,19 +376,19 @@ GEM
reline (>= 0.3.8)
jmespath (1.6.2)
json (2.6.3)
json-canonicalization (0.3.2)
json-canonicalization (1.0.0)
json-jwt (1.15.3)
activesupport (>= 4.2)
aes_key_wrap
bindata
httpclient
json-ld (3.2.5)
json-ld (3.3.1)
htmlentities (~> 4.3)
json-canonicalization (~> 0.3, >= 0.3.2)
json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15)
rack (>= 2.2, < 4)
rdf (~> 3.2, >= 3.2.10)
rdf (~> 3.3)
json-ld-preloaded (3.2.2)
json-ld (~> 3.2)
rdf (~> 3.2)
@@ -455,7 +456,7 @@ GEM
mini_mime (1.1.5)
mini_portile2 (2.8.4)
minitest (5.20.0)
msgpack (1.7.1)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.3.0)
mutex_m (0.1.2)
@@ -474,7 +475,7 @@ GEM
net-smtp (0.4.0)
net-protocol
nio4r (2.5.9)
nokogiri (1.15.4)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.1)
@@ -514,7 +515,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.5.4)
pg (1.5.5)
pghero (3.3.4)
activerecord (>= 6)
posix-spawn (0.3.15)
@@ -532,10 +533,10 @@ GEM
public_suffix (5.0.3)
puma (6.4.0)
nio4r (~> 2.0)
pundit (2.3.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.1)
racc (1.7.3)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
@@ -596,7 +597,8 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.0.6)
rdf (3.2.11)
rdf (3.3.1)
bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1)
rdf (~> 3.2)
@@ -631,7 +633,7 @@ GEM
rspec-support (~> 3.12.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.3)
@@ -642,15 +644,13 @@ GEM
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-sidekiq (4.0.1)
rspec-sidekiq (4.1.0)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.12.1)
rspec_chunked (0.6)
rubocop (1.57.1)
base64 (~> 0.1.1)
rubocop (1.57.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -661,21 +661,21 @@ GEM
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.19.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop-factory_bot (2.24.0)
rubocop (~> 1.33)
rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2)
rubocop-rails (2.22.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.23.2)
rubocop (~> 1.33)
rubocop-rspec (2.25.0)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-prof (1.6.3)
@@ -710,7 +710,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29)
sidekiq-unique-jobs (7.1.33)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0)
@@ -740,7 +740,7 @@ GEM
stoplight (3.0.2)
redlock (~> 1.0)
stringio (3.0.8)
strong_migrations (0.8.0)
strong_migrations (1.3.0)
activerecord (>= 5.2)
swd (1.3.0)
activesupport (>= 3)
@@ -833,7 +833,7 @@ DEPENDENCIES
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.16.0)
bootsnap (~> 1.17.0)
brakeman (~> 6.0)
browser
bundler-audit (~> 0.9)
@@ -919,7 +919,6 @@ DEPENDENCIES
rspec-github (~> 2.4)
rspec-rails (~> 6.0)
rspec-sidekiq (~> 4.0)
rspec_chunked (~> 0.6)
rubocop
rubocop-capybara
rubocop-performance
@@ -942,7 +941,7 @@ DEPENDENCIES
sprockets-rails (~> 3.4)
stackprof
stoplight (~> 3.0.1)
strong_migrations (~> 0.8)
strong_migrations (= 1.3.0)
test-prof
thor (~> 1.2)
tty-prompt (~> 0.23)

View File

@@ -17,6 +17,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| 4.0.x | Until 2023-10-31 |
| 4.0.x | No |
| 3.5.x | Until 2023-12-31 |
| < 3.5 | No |

View File

@@ -21,7 +21,7 @@ module Admin
account_action.save!
if account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else
redirect_to admin_account_path(@account.id)
end

View File

@@ -20,7 +20,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
authorize @appeal, :approve?
log_action :reject, @appeal
@appeal.reject!(current_account)
UserMailer.appeal_rejected(@appeal.account.user, @appeal)
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
redirect_to disputes_strike_path(@appeal.strike)
end

View File

@@ -33,7 +33,7 @@ module Admin
# Disallow accidentally downgrading a domain block
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save
@domain_block.validate
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
@domain_block.errors.delete(:domain)
return render :new

View File

@@ -176,7 +176,10 @@ class ApplicationController < ActionController::Base
return unless self_destruct?
respond_to do |format|
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
format.any do
use_pack 'error'
render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html]
end
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
end
end

View File

@@ -40,6 +40,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
show
end
def redirect_to_app?
truthy_param?(:redirect_to_app)
end
helper_method :redirect_to_app?
private
def require_captcha_if_needed!
@@ -87,7 +93,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
if user.created_by_application && redirect_to_app?
user.created_by_application.confirmation_redirect_uri
elsif user_signed_in?
web_url('start')

View File

@@ -7,7 +7,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
define_method provider do
@provider = provider
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
if @user.persisted?
record_login_activity
@@ -17,6 +17,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url
end
rescue ActiveRecord::RecordInvalid
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
redirect_to new_user_session_url
end
end

View File

@@ -92,18 +92,10 @@ module CacheConcern
arguments
end
if Rails.gem_version >= Gem::Version.new('7.0')
def attributes_for_database(record)
attributes = record.attributes_for_database
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
else
def attributes_for_database(record)
attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database)
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
def attributes_for_database(record)
attributes = record.attributes_for_database
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter

View File

@@ -250,7 +250,7 @@ module SignatureVerification
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e

View File

@@ -155,8 +155,8 @@ module JsonLdHelper
end
end
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
def fetch_resource(uri, id_is_known, on_behalf_of = nil)
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
@@ -174,7 +174,19 @@ module JsonLdHelper
build_request(uri, on_behalf_of).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end
end
def valid_activitypub_content_type?(response)
return true if response.mime_type == 'application/activity+json'
# When the mime type is `application/ld+json`, we need to check the profile,
# but `http.rb` does not parse it for us.
return false unless response.mime_type == 'application/ld+json'
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
end
end

View File

@@ -1,69 +0,0 @@
import api from '../api';
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
export const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT';
export const ACCOUNT_NOTE_CANCEL = 'ACCOUNT_NOTE_CANCEL';
export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
export function submitAccountNote() {
return (dispatch, getState) => {
dispatch(submitAccountNoteRequest());
const id = getState().getIn(['account_notes', 'edit', 'account_id']);
api(getState).post(`/api/v1/accounts/${id}/note`, {
comment: getState().getIn(['account_notes', 'edit', 'comment']),
}).then(response => {
dispatch(submitAccountNoteSuccess(response.data));
}).catch(error => dispatch(submitAccountNoteFail(error)));
};
}
export function submitAccountNoteRequest() {
return {
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
};
}
export function submitAccountNoteSuccess(relationship) {
return {
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
relationship,
};
}
export function submitAccountNoteFail(error) {
return {
type: ACCOUNT_NOTE_SUBMIT_FAIL,
error,
};
}
export function initEditAccountNote(account) {
return (dispatch, getState) => {
const comment = getState().getIn(['relationships', account.get('id'), 'note']);
dispatch({
type: ACCOUNT_NOTE_INIT_EDIT,
account,
comment,
});
};
}
export function cancelAccountNote() {
return {
type: ACCOUNT_NOTE_CANCEL,
};
}
export function changeAccountNoteComment(comment) {
return {
type: ACCOUNT_NOTE_CHANGE_COMMENT,
comment,
};
}

View File

@@ -0,0 +1,18 @@
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
import api from '../api';
export const submitAccountNote = createAppAsyncThunk(
'account_note/submit',
async (args: { id: string; value: string }, { getState }) => {
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
const response = await api(getState).post<unknown>(
`/api/v1/accounts/${args.id}/note`,
{
comment: args.value,
},
);
return { relationship: response.data };
},
);

View File

@@ -106,7 +106,6 @@ export function fetchAccount(id) {
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
}).then(() => {
dispatch(fetchAccountSuccess());
}).catch(error => {
dispatch(fetchAccountFail(id, error));

View File

@@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export function dismissAlert(alert) {
return {
type: ALERT_DISMISS,
alert,
};
}
export const dismissAlert = alert => ({
type: ALERT_DISMISS,
alert,
});
export function clearAlert() {
return {
type: ALERT_CLEAR,
};
}
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
return {
type: ALERT_SHOW,
title,
message,
message_values,
};
}
export const showAlert = alert => ({
type: ALERT_SHOW,
alert,
});
export function showAlertForError(error, skipNotFound = false) {
export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) {
const { data, status, statusText, headers } = error.response;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
const reset_date = new Date(headers['x-ratelimit-reset']);
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
return showAlert({
title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
});
}
let message = statusText;
let title = `${status}`;
if (data.error) {
message = data.error;
}
return showAlert(title, message);
} else {
console.error(error);
return showAlert();
return showAlert({
title: `${status}`,
message: data.error || statusText,
});
}
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
};

View File

@@ -84,10 +84,14 @@ export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTIO
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ -144,6 +148,15 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => dispatch => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(routerHistory);
};
export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
@@ -264,6 +277,13 @@ export function submitCompose(routerHistory) {
} else if (statusId === null && response.data.visibility === 'direct') {
insertIfOnline('direct');
}
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@@ -300,18 +320,19 @@ export function doodleSet(options) {
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
dispatch(showAlert({ message: messages.uploadErrorLimit }));
return;
}
if (getState().getIn(['compose', 'poll'])) {
dispatch(showAlert(undefined, messages.uploadErrorPoll));
dispatch(showAlert({ message: messages.uploadErrorPoll }));
return;
}

View File

@@ -1,31 +0,0 @@
import api from '../api';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
dispatch(fetchAccountIdentityProofsRequest(accountId));
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
};
export const fetchAccountIdentityProofsRequest = id => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
id,
});
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
accountId,
identity_proofs,
});
export const fetchAccountIdentityProofsFail = (accountId, err) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
accountId,
err,
skipNotFound: true,
});

View File

@@ -1,8 +1,8 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'flavours/glitch/features/emoji/emoji';
import { autoHideCW } from 'flavours/glitch/utils/content_warning';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import emojify from '../../features/emoji/emoji';
import { autoHideCW } from '../../utils/content_warning';
import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser();

View File

@@ -83,6 +83,7 @@ export function reblogRequest(status) {
return {
type: REBLOG_REQUEST,
status: status,
skipLoading: true,
};
}
@@ -90,6 +91,7 @@ export function reblogSuccess(status) {
return {
type: REBLOG_SUCCESS,
status: status,
skipLoading: true,
};
}
@@ -98,6 +100,7 @@ export function reblogFail(status, error) {
type: REBLOG_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
@@ -105,6 +108,7 @@ export function unreblogRequest(status) {
return {
type: UNREBLOG_REQUEST,
status: status,
skipLoading: true,
};
}
@@ -112,6 +116,7 @@ export function unreblogSuccess(status) {
return {
type: UNREBLOG_SUCCESS,
status: status,
skipLoading: true,
};
}
@@ -120,6 +125,7 @@ export function unreblogFail(status, error) {
type: UNREBLOG_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
@@ -153,6 +159,7 @@ export function favouriteRequest(status) {
return {
type: FAVOURITE_REQUEST,
status: status,
skipLoading: true,
};
}
@@ -160,6 +167,7 @@ export function favouriteSuccess(status) {
return {
type: FAVOURITE_SUCCESS,
status: status,
skipLoading: true,
};
}
@@ -168,6 +176,7 @@ export function favouriteFail(status, error) {
type: FAVOURITE_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
@@ -175,6 +184,7 @@ export function unfavouriteRequest(status) {
return {
type: UNFAVOURITE_REQUEST,
status: status,
skipLoading: true,
};
}
@@ -182,6 +192,7 @@ export function unfavouriteSuccess(status) {
return {
type: UNFAVOURITE_SUCCESS,
status: status,
skipLoading: true,
};
}
@@ -190,6 +201,7 @@ export function unfavouriteFail(status, error) {
type: UNFAVOURITE_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
@@ -199,7 +211,7 @@ export function bookmark(status) {
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status));
dispatch(bookmarkSuccess(status, response.data));
}).catch(function (error) {
dispatch(bookmarkFail(status, error));
});
@@ -212,7 +224,7 @@ export function unbookmark(status) {
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status));
dispatch(unbookmarkSuccess(status, response.data));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
@@ -226,10 +238,11 @@ export function bookmarkRequest(status) {
};
}
export function bookmarkSuccess(status) {
export function bookmarkSuccess(status, response) {
return {
type: BOOKMARK_SUCCESS,
status: status,
response: response,
};
}
@@ -248,10 +261,11 @@ export function unbookmarkRequest(status) {
};
}
export function unbookmarkSuccess(status) {
export function unbookmarkSuccess(status, response) {
return {
type: UNBOOKMARK_SUCCESS,
status: status,
response: response,
};
}
@@ -444,6 +458,7 @@ export function pinRequest(status) {
return {
type: PIN_REQUEST,
status,
skipLoading: true,
};
}
@@ -451,6 +466,7 @@ export function pinSuccess(status) {
return {
type: PIN_SUCCESS,
status,
skipLoading: true,
};
}
@@ -459,6 +475,7 @@ export function pinFail(status, error) {
type: PIN_FAIL,
status,
error,
skipLoading: true,
};
}
@@ -479,6 +496,7 @@ export function unpinRequest(status) {
return {
type: UNPIN_REQUEST,
status,
skipLoading: true,
};
}
@@ -486,6 +504,7 @@ export function unpinSuccess(status) {
return {
type: UNPIN_SUCCESS,
status,
skipLoading: true,
};
}
@@ -494,5 +513,6 @@ export function unpinFail(status, error) {
type: UNPIN_FAIL,
status,
error,
skipLoading: true,
};
}

View File

@@ -1,9 +1,8 @@
import { openModal } from 'flavours/glitch/actions/modal';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';

View File

@@ -5,10 +5,10 @@ import { List as ImmutableList } from 'immutable';
import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import { requestNotificationPermission } from 'flavours/glitch/utils/notifications';
import api, { getLinks } from '../api';
import { unescapeHTML } from '../utils/html';
import { requestNotificationPermission } from '../utils/notifications';
import { fetchFollowRequests, fetchRelationships } from './accounts';
import {
@@ -21,10 +21,7 @@ import { submitMarkers } from './markers';
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
// tracking the notif cleaning request
@@ -65,7 +62,7 @@ defineMessages({
const fetchRelatedRelationships = (dispatch, notifications) => {
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
if (accountIds > 0) {
if (accountIds.length > 0) {
dispatch(fetchRelationships(accountIds));
}
};
@@ -131,6 +128,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener('click', () => {
window.focus();
notify.close();
@@ -141,7 +139,6 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList([
'follow',

View File

@@ -1,16 +1,8 @@
import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';
export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
export const INTRODUCTION_VERSION = 20181216044202;
if (!alreadySeen) {
dispatch(openModal({
modalType: 'ONBOARDING',
}));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
};
}
export const closeOnboarding = () => dispatch => {
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
dispatch(saveSettings());
};

View File

@@ -1,10 +1,8 @@
import { me } from 'flavours/glitch/initial_state';
import api from '../api';
import { me } from '../initial_state';
import { importFetchedStatuses } from './importer';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';

View File

@@ -1,5 +1,7 @@
import api from '../../api';
import { me } from '../../initial_state';
import { pushNotificationsSetting } from '../../settings';
import { decode as decodeBase64 } from '../../utils/base64';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
@@ -10,13 +12,7 @@ const urlBase64ToUint8Array = (base64String) => {
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
return decodeBase64(base64);
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
@@ -36,7 +32,7 @@ const subscribe = (registration) =>
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (getState, subscription, me) => {
const sendSubscriptionToBackend = (subscription) => {
const params = { subscription };
if (me) {
@@ -46,7 +42,7 @@ const sendSubscriptionToBackend = (getState, subscription, me) => {
}
}
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
return api().post('/api/web/push_subscriptions', params).then(response => response.data);
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
@@ -55,7 +51,6 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
export function register () {
return (dispatch, getState) => {
dispatch(setBrowserSupport(supportsPushNotifications));
const me = getState().getIn(['meta', 'me']);
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
@@ -79,13 +74,13 @@ export function register () {
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
subscription => sendSubscriptionToBackend(subscription));
}
}
// No subscription, try to subscribe
return subscribe(registration).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
subscription => sendSubscriptionToBackend(subscription));
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
@@ -128,10 +123,9 @@ export function saveSettings() {
const alerts = state.get('alerts');
const data = { alerts };
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data,
}).then(() => {
const me = getState().getIn(['meta', 'me']);
if (me) {
pushNotificationsSetting.set(me, data);
}

View File

@@ -26,7 +26,7 @@ const debouncedSave = debounce((dispatch, getState) => {
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
api(getState).put('/api/web/settings', { data })
api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE }))
.catch(error => dispatch(showAlertForError(error)));
}, 5000, { trailing: true });

View File

@@ -1,7 +1,6 @@
// @ts-check
import { getLocale } from 'flavours/glitch/locales';
import { getLocale } from '../locales';
import { connectStream } from '../stream';
import {
@@ -68,8 +67,8 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
// @ts-expect-error
if (pollingId) {
clearTimeout(pollingId);
pollingId = null;
// @ts-ignore
clearTimeout(pollingId); pollingId = null;
}
if (options.fillGaps) {
@@ -86,8 +85,8 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}
},
onReceive (data) {
switch(data.event) {
onReceive(data) {
switch (data.event) {
case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));

View File

@@ -1,75 +0,0 @@
// @ts-check
import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready';
/**
* @param {import('axios').AxiosResponse} response
* @returns {LinkHeader}
*/
export const getLinks = response => {
const value = response.headers.link;
if (!value) {
return new LinkHeader();
}
return LinkHeader.parse(value);
};
/** @type {import('axios').RawAxiosRequestHeaders} */
const csrfHeader = {};
/**
* @returns {void}
*/
const setCSRFHeader = () => {
/** @type {HTMLMetaElement | null} */
const csrfToken = document.querySelector('meta[name=csrf-token]');
if (csrfToken) {
csrfHeader['X-CSRF-Token'] = csrfToken.content;
}
};
ready(setCSRFHeader);
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').RawAxiosRequestHeaders}
*/
const authorizationHeaderFromState = getState => {
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
if (!accessToken) {
return {};
}
return {
'Authorization': `Bearer ${accessToken}`,
};
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').AxiosInstance}
*/
export default function api(getState) {
return axios.create({
headers: {
...csrfHeader,
...authorizationHeaderFromState(getState),
},
transformResponse: [
function (data) {
try {
return JSON.parse(data);
} catch {
return data;
}
},
],
});
}

View File

@@ -0,0 +1,63 @@
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready';
import type { GetState } from './store';
export const getLinks = (response: AxiosResponse) => {
const value = response.headers.link as string | undefined;
if (!value) {
return new LinkHeader();
}
return LinkHeader.parse(value);
};
const csrfHeader: RawAxiosRequestHeaders = {};
const setCSRFHeader = () => {
const csrfToken = document.querySelector<HTMLMetaElement>(
'meta[name=csrf-token]',
);
if (csrfToken) {
csrfHeader['X-CSRF-Token'] = csrfToken.content;
}
};
void ready(setCSRFHeader);
const authorizationHeaderFromState = (getState?: GetState) => {
const accessToken =
getState && (getState().meta.get('access_token', '') as string);
if (!accessToken) {
return {};
}
return {
Authorization: `Bearer ${accessToken}`,
} as RawAxiosRequestHeaders;
};
// eslint-disable-next-line import/no-default-export
export default function api(getState: GetState) {
return axios.create({
headers: {
...csrfHeader,
...authorizationHeaderFromState(getState),
},
transformResponse: [
function (data: unknown) {
try {
return JSON.parse(data as string) as unknown;
} catch {
return data;
}
},
],
});
}

View File

@@ -0,0 +1,214 @@
import { fromJS } from 'immutable';
import type { StatusLike } from '../hashtag_bar';
import { computeHashtagBarForStatus } from '../hashtag_bar';
function createStatus(
content: string,
hashtags: string[],
hasMedia = false,
spoilerText?: string,
) {
return fromJS({
tags: hashtags.map((name) => ({ name })),
contentHtml: content,
media_attachments: hasMedia ? ['fakeMedia'] : [],
spoiler_text: spoilerText,
}) as unknown as StatusLike; // need to force the type here, as it is not properly defined
}
describe('computeHashtagBarForStatus', () => {
it('does nothing when there are no tags', () => {
const status = createStatus('<p>Simple text</p>', []);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
);
});
it('displays out of band hashtags in the bar', () => {
const status = createStatus(
'<p>Simple text <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text <a href="test">#hashtag</a></p>"`,
);
});
it('does not truncate the contents when the last child is a text node', () => {
const status = createStatus(
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
['test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
);
});
it('extract tags from the last line', () => {
const status = createStatus(
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
['hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['hashtag']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
);
});
it('does not include tags from content', () => {
const status = createStatus(
'<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
['hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
);
});
it('works with one line status and hashtags', () => {
const status = createStatus(
'<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
);
});
it('de-duplicate accentuated characters with case differences', () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['éaa'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
);
});
it('handles server-side normalized tags with accentuated characters', () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['eaa'], // The server may normalize the hashtags in the `tags` attribute
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
);
});
it('does not display in bar a hashtag in content with a case difference', () => {
const status = createStatus(
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
['éaa'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text <a href="test">#Éaa</a></p>"`,
);
});
it('does not modify a status with a line of hashtags only', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
);
});
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
const status = createStatus(
'<p>This is my content! <a href="test">#hashtag</a></p>',
['hashtag'],
true,
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>This is my content! <a href="test">#hashtag</a></p>"`,
);
});
it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
true,
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test', 'hashtag']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
});
it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
true,
'My CW text',
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
);
});
});

View File

@@ -1,30 +1,35 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import { me } from 'flavours/glitch/initial_state';
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
import { me } from '../initial_state';
import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';
import Permalink from './permalink';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
});
class Account extends ImmutablePureComponent {
@@ -38,15 +43,13 @@ class Account extends ImmutablePureComponent {
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
small: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
withBio: PropTypes.bool,
};
static defaultProps = {
size: 36,
size: 46,
};
handleFollow = () => {
@@ -69,34 +72,11 @@ class Account extends ImmutablePureComponent {
this.props.onMuteNotifications(this.props.account, false);
};
handleAction = () => {
this.props.onActionClick(this.props.account);
};
render () {
const {
account,
hidden,
intl,
small,
onActionClick,
actionIcon,
actionTitle,
defaultAction,
size,
} = this.props;
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
if (!account) {
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
<DisplayName />
</div>
</div>
</div>
);
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
@@ -110,78 +90,89 @@ class Account extends ImmutablePureComponent {
let buttons;
if (onActionClick) {
if (actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
}
} else if (account.get('id') !== me && !small && account.get('relationship', null) !== null) {
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
}
buttons = (
<>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
{hidingNotificationsButton}
</>
);
} else if (defaultAction === 'mute') {
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
}
}
let mute_expires_at;
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>;
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
return small ? (
<Permalink
className='account small'
href={account.get('url')}
to={`/@${account.get('acct')}`}
>
<div className='account__avatar-wrapper'>
<Avatar
account={account}
size={24}
/>
</div>
<DisplayName
account={account}
inline
/>
</Permalink>
) : (
<div className='account'>
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
{mute_expires_at}
<DisplayName account={account} />
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} inline />
{!minimal && (
<div className='account__details'>
{account.get('followers_count') !== -1 && (
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
)} {verification} {muteTimeRemaining}
</div>
)}
</div>
</Permalink>
{buttons ?
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
: null}
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
}

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
import { TransitionMotion, spring } from 'react-motion';

View File

@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { unicodeMapping } from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
import { assetHost } from 'flavours/glitch/utils/config';
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
export default class AutosuggestEmoji extends PureComponent {
static propTypes = {
@@ -27,7 +28,7 @@ export default class AutosuggestEmoji extends PureComponent {
}
return (
<div className='emoji'>
<div className='autosuggest-emoji'>
<img
className='emojione'
src={url}

View File

@@ -5,7 +5,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';
@@ -37,54 +37,46 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
}
};
export default class AutosuggestTextarea extends ImmutablePureComponent {
const AutosuggestTextarea = forwardRef(({
value,
suggestions,
disabled,
placeholder,
onSuggestionSelected,
onSuggestionsClearRequested,
onSuggestionsFetchRequested,
onChange,
onKeyUp,
onKeyDown,
onPaste,
onFocus,
autoFocus = true,
lang,
children,
}, textareaRef) => {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
autoFocus: PropTypes.bool,
lang: PropTypes.string,
};
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
const lastTokenRef = useRef(null);
const tokenStartRef = useRef(0);
static defaultProps = {
autoFocus: true,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const handleChange = useCallback((e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
if (token !== null && lastTokenRef.current !== token) {
tokenStartRef.current = tokenStart;
lastTokenRef.current = token;
setSelectedSuggestion(0);
onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
lastTokenRef.current = null;
onSuggestionsClearRequested();
}
this.props.onChange(e);
};
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
onChange(e);
}, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
const handleKeyDown = useCallback((e) => {
if (disabled) {
e.preventDefault();
return;
@@ -102,80 +94,75 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
setSuggestionsHidden(true);
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
if (e.defaultPrevented || !onKeyDown) {
return;
}
this.props.onKeyDown(e);
};
onKeyDown(e);
}, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
};
const handleBlur = useCallback(() => {
setSuggestionsHidden(true);
}, [setSuggestionsHidden]);
onFocus = (e) => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
const handleFocus = useCallback((e) => {
if (onFocus) {
onFocus(e);
}
};
}, [onFocus]);
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
const handleSuggestionClick = useCallback((e) => {
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus();
};
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
textareaRef.current?.focus();
}, [suggestions, onSuggestionSelected, textareaRef]);
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setTextarea = (c) => {
this.textarea = c;
};
onPaste = (e) => {
const handlePaste = useCallback((e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
onPaste(e.clipboardData.files);
e.preventDefault();
}
};
}, [onPaste]);
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
// Show the suggestions again whenever they change and the textarea is focused
useEffect(() => {
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
setSuggestionsHidden(false);
}
}, [suggestions, textareaRef, setSuggestionsHidden]);
const renderSuggestion = (suggestion, i) => {
let inner, key;
if (suggestion.type === 'emoji') {
@@ -190,50 +177,64 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
return (
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
{inner}
</div>
);
};
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
const { suggestionsHidden } = this.state;
return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
ref={textareaRef}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onKeyUp={onKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
dir='auto'
aria-autocomplete='list'
lang={lang}
/>
</label>
</div>
{children}
</div>,
<Textarea
ref={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
dir='auto'
aria-autocomplete='list'
lang={lang}
/>
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(renderSuggestion)}
</div>
</div>,
];
});
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,
];
}
AutosuggestTextarea.propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
onFocus:PropTypes.func,
children: PropTypes.node,
autoFocus: PropTypes.bool,
lang: PropTypes.string,
};
}
export default AutosuggestTextarea;

View File

@@ -1,55 +1,48 @@
import * as React from 'react';
import classNames from 'classnames';
import { useHovering } from 'flavours/glitch/hooks/useHovering';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import type { Account } from 'flavours/glitch/types/resources';
import { useHovering } from '../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources';
interface Props {
account: Account | undefined;
className?: string;
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
}
export const Avatar: React.FC<Props> = ({
account,
className,
animate = autoPlayGif,
size = 20,
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
const style = {
...styleFromParent,
width: `${size}px`,
height: `${size}px`,
backgroundSize: `${size}px ${size}px`,
};
if (account) {
style.backgroundImage = `url(${account.get(
hovering ? 'avatar' : 'avatar_static',
)})`;
}
const src =
hovering || animate
? account?.get('avatar')
: account?.get('avatar_static');
return (
<div
className={classNames(
'account__avatar',
{ 'account__avatar-inline': inline },
className,
)}
className={classNames('account__avatar', {
'account__avatar-inline': inline,
})}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
data-avatar-of={account && `@${account.get('acct')}`}
role='img'
aria-label={account?.get('acct')}
/>
>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
};

View File

@@ -3,7 +3,9 @@ import { PureComponent } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import { autoPlayGif } from '../initial_state';
import { Avatar } from './avatar';
export default class AvatarComposite extends PureComponent {
@@ -76,12 +78,12 @@ export default class AvatarComposite extends PureComponent {
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div key={account.get('id')} style={style} data-avatar-of={`@${account.get('acct')}`} />
<div key={account.get('id')} style={style}>
<Avatar account={account} animate={animate} />
</div>
);
}

View File

@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/initial_state';
export default class AvatarOverlay extends PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend, animate } = this.props;
const baseStyle = {
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
const overlayStyle = {
backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div className='account__avatar-overlay'>
<div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} />
</div>
);
}
}

View File

@@ -0,0 +1,56 @@
import { useHovering } from '../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources';
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size?: number;
baseSize?: number;
overlaySize?: number;
}
export const AvatarOverlay: React.FC<Props> = ({
account,
friend,
size = 46,
baseSize = 36,
overlaySize = 24,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const accountSrc = hovering
? account?.get('avatar')
: account?.get('avatar_static');
const friendSrc = hovering
? friend?.get('avatar')
: friend?.get('avatar_static');
return (
<div
className='account__avatar-overlay'
style={{ width: size, height: size }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className='account__avatar-overlay-base'>
<div
className='account__avatar'
style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
data-avatar-of={`@${account?.get('acct')}`}
>
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
</div>
</div>
<div className='account__avatar-overlay-overlay'>
<div
className='account__avatar'
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
data-avatar-of={`@${friend?.get('acct')}`}
>
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
</div>
</div>
</div>
);
};

View File

@@ -1,5 +1,4 @@
import { useRef, useEffect } from 'react';
import * as React from 'react';
import { memo, useRef, useEffect } from 'react';
import { decode } from 'blurhash';
@@ -44,6 +43,6 @@ const Blurhash: React.FC<Props> = ({
);
};
const MemoizedBlurhash = React.memo(Blurhash);
const MemoizedBlurhash = memo(Blurhash);
export { MemoizedBlurhash as Blurhash };

View File

@@ -1,7 +0,0 @@
const Check = () => (
<svg width='14' height='11' viewBox='0 0 14 11'>
<path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
</svg>
);
export default Check;

View File

@@ -0,0 +1,13 @@
export const Check: React.FC = () => (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
fill='currentColor'
>
<path
fillRule='evenodd'
d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
clipRule='evenodd'
/>
</svg>
);

View File

@@ -12,7 +12,6 @@ export default class Column extends PureComponent {
static propTypes = {
children: PropTypes.node,
extraClasses: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
bindToDocument: PropTypes.bool,
};
@@ -62,10 +61,10 @@ export default class Column extends PureComponent {
}
render () {
const { children, extraClasses, name, label } = this.props;
const { label, children, extraClasses } = this.props;
return (
<div role='region' aria-label={label} data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
<div role='region' aria-label={label} className={`column ${extraClasses || ''}`} ref={this.setRef}>
{children}
</div>
);

View File

@@ -13,13 +13,16 @@ export class ColumnBackButton extends PureComponent {
static propTypes = {
multiColumn: PropTypes.bool,
onClick: PropTypes.func,
...WithRouterPropTypes,
};
handleClick = () => {
const { history } = this.props;
const { onClick, history } = this.props;
if (history.location?.state?.fromMastodon) {
if (onClick) {
onClick();
} else if (history.location?.state?.fromMastodon) {
history.goBack();
} else {
history.push('/');

View File

@@ -4,9 +4,8 @@ import classNames from 'classnames';
import type { List } from 'immutable';
import type { Account } from 'flavours/glitch/types/resources';
import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources';
import { Skeleton } from './skeleton';

View File

@@ -1,5 +1,4 @@
import { useCallback } from 'react';
import * as React from 'react';
import { defineMessages, useIntl } from 'react-intl';

View File

@@ -6,7 +6,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import InlineAccount from 'flavours/glitch/components/inline_account';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';

View File

@@ -0,0 +1,33 @@
import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { Skeleton } from 'flavours/glitch/components/skeleton';
interface Props {
size?: number;
minimal?: boolean;
}
export const EmptyAccount: React.FC<Props> = ({
size = 46,
minimal = false,
}) => {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'>
<Skeleton width={size} height={size} />
</div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
interface Props {
src: string;

View File

@@ -25,11 +25,11 @@ class SilentErrorBoundary extends Component {
error: false,
};
componentDidCatch () {
componentDidCatch() {
this.setState({ error: true });
}
render () {
render() {
if (this.state.error) {
return null;
}

View File

@@ -0,0 +1,234 @@
import { useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import type { List, Record } from 'immutable';
import { groupBy, minBy } from 'lodash';
import { getStatusContent } from './status_content';
// Fit on a single line on desktop
const VISIBLE_HASHTAGS = 3;
// Those types are not correct, they need to be replaced once this part of the state is typed
export type TagLike = Record<{ name: string }>;
export type StatusLike = Record<{
tags: List<TagLike>;
contentHTML: string;
media_attachments: List<unknown>;
spoiler_text?: string;
}>;
function normalizeHashtag(hashtag: string) {
return (
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
).normalize('NFKC');
}
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
return (
element instanceof HTMLAnchorElement &&
// it may be a <a> starting with a hashtag
(element.textContent?.[0] === '#' ||
// or a #<a>
element.previousSibling?.textContent?.[
element.previousSibling.textContent.length - 1
] === '#')
);
}
/**
* Removes duplicates from an hashtag list, case-insensitive, keeping only the best one
* "Best" here is defined by the one with the more casing difference (ie, the most camel-cased one)
* @param hashtags The list of hashtags
* @returns The input hashtags, but with only 1 occurence of each (case-insensitive)
*/
function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
const groups = groupBy(hashtags, (tag) =>
tag.normalize('NFKD').toLowerCase(),
);
return Object.values(groups).map((tags) => {
if (tags.length === 1) return tags[0];
// The best match is the one where we have the less difference between upper and lower case letter count
const best = minBy(tags, (tag) => {
const upperCase = Array.from(tag).reduce(
(acc, char) => (acc += char.toUpperCase() === char ? 1 : 0),
0,
);
const lowerCase = tag.length - upperCase;
return Math.abs(lowerCase - upperCase);
});
return best ?? tags[0];
});
}
// Create the collator once, this is much more efficient
const collator = new Intl.Collator(undefined, {
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
});
function localeAwareInclude(collection: string[], value: string) {
const normalizedValue = value.normalize('NFKC');
return !!collection.find(
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
);
}
// We use an intermediate function here to make it easier to test
export function computeHashtagBarForStatus(status: StatusLike): {
statusContentProps: { statusContent: string };
hashtagsInBar: string[];
} {
let statusContent = getStatusContent(status);
const tagNames = status
.get('tags')
.map((tag) => tag.get('name'))
.toJS();
// this is returned if we stop the processing early, it does not change what is displayed
const defaultResult = {
statusContentProps: { statusContent },
hashtagsInBar: [],
};
// return early if this status does not have any tags
if (tagNames.length === 0) return defaultResult;
const template = document.createElement('template');
template.innerHTML = statusContent.trim();
const lastChild = template.content.lastChild;
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
template.content.removeChild(lastChild);
const contentWithoutLastLine = template;
// First, try to parse
const contentHashtags = Array.from(
contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
).reduce<string[]>((result, link) => {
if (isNodeLinkHashtag(link)) {
if (link.textContent) result.push(normalizeHashtag(link.textContent));
}
return result;
}, []);
// Now we parse the last line, and try to see if it only contains hashtags
const lastLineHashtags: string[] = [];
// try to see if the last line is only hashtags
let onlyHashtags = true;
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
Array.from(lastChild.childNodes).forEach((node) => {
if (isNodeLinkHashtag(node) && node.textContent) {
const normalized = normalizeHashtag(node.textContent);
if (!localeAwareInclude(normalizedTagNames, normalized)) {
// stop here, this is not a real hashtag, so consider it as text
onlyHashtags = false;
return;
}
if (!localeAwareInclude(contentHashtags, normalized))
// only add it if it does not appear in the rest of the content
lastLineHashtags.push(normalized);
} else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
// not a space
onlyHashtags = false;
}
});
const hashtagsInBar = tagNames.filter((tag) => {
const normalizedTag = tag.normalize('NFKC');
// the tag does not appear at all in the status content, it is an out-of-band tag
return (
!localeAwareInclude(contentHashtags, normalizedTag) &&
!localeAwareInclude(lastLineHashtags, normalizedTag)
);
});
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
const hasMedia = status.get('media_attachments').size > 0;
const hasSpoiler = !!status.get('spoiler_text');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
// if the last line only contains hashtags, and we either:
// - have other content in the status
// - dont have other content, but a media and no CW. If it has a CW, then we do not remove the content to avoid having an empty content behind the CW button
statusContent = contentWithoutLastLine.innerHTML;
// and add the tags to the bar
hashtagsInBar.push(...lastLineHashtags);
}
return {
statusContentProps: { statusContent },
hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
};
}
/**
* This function will process a status to, at the same time (avoiding parsing it twice):
* - build the HashtagBar for this status
* - remove the last-line hashtags from the status content
* @param status The status to process
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
*/
export function getHashtagBarForStatus(status: StatusLike) {
const { statusContentProps, hashtagsInBar } =
computeHashtagBarForStatus(status);
return {
statusContentProps,
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
};
}
const HashtagBar: React.FC<{
hashtags: string[];
}> = ({ hashtags }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = useCallback(() => {
setExpanded(true);
}, []);
if (hashtags.length === 0) {
return null;
}
const revealedHashtags = expanded
? hashtags
: hashtags.slice(0, VISIBLE_HASHTAGS);
return (
<div className='hashtag-bar'>
{revealedHashtags.map((hashtag) => (
<Link key={hashtag} to={`/tags/${hashtag}`}>
#<span>{hashtag}</span>
</Link>
))}
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='hashtags.and_other'
defaultMessage='…and {count, plural, other {# more}}'
values={{ count: hashtags.length - VISIBLE_HASHTAGS }}
/>
</button>
)}
</div>
);
};

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
interface Props extends React.HTMLAttributes<HTMLImageElement> {

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import { PureComponent } from 'react';
import classNames from 'classnames';
@@ -33,7 +33,7 @@ interface States {
activate: boolean;
deactivate: boolean;
}
export class IconButton extends React.PureComponent<Props, States> {
export class IconButton extends PureComponent<Props, States> {
static defaultProps = {
size: 18,
active: false,

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
import { Icon } from './icon';
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);

View File

@@ -3,6 +3,7 @@ import { cloneElement, Component } from 'react';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
@@ -38,7 +39,6 @@ export default class IntersectionObserverArticle extends Component {
return true;
}
componentDidMount () {
const { intersectionObserverWrapper, id } = this.props;
@@ -106,24 +106,24 @@ export default class IntersectionObserverArticle extends Component {
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
const style = {};
if (!isIntersecting && (isHidden || cachedHeight)) {
style.height = `${this.height || cachedHeight || 150}px`;
style.opacity = 0;
style.overflow = 'hidden';
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex={-1}
>
{children && cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
data-id={id}
tabIndex={0}
style={style}
>
{children && cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })}
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={-1}>
{children && cloneElement(children, { hidden: false })}
</article>
);
}

View File

@@ -11,7 +11,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { IconButton } from './icon_button';

View File

@@ -110,8 +110,9 @@ class ModalRoot extends PureComponent {
}
_handleModalClose () {
this.unlistenHistory();
if (this.unlistenHistory) {
this.unlistenHistory();
}
const { state } = this.history.location;
if (state && state.mastodonModalKey === this._modalHistoryKey) {
this.history.goBack();

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
export const NotSignedInIndicator: React.FC = () => (

View File

@@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
class PictureInPicturePlaceholder extends PureComponent {

View File

@@ -10,13 +10,12 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import escapeTextContentForBrowser from 'escape-html';
import spring from 'react-motion/lib/spring';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import emojify from 'flavours/glitch/features/emoji/emoji';
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({
closed: {
id: 'poll.closed',

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
import classNames from 'classnames';
interface Props {

View File

@@ -10,11 +10,11 @@ import { connect } from 'react-redux';
import { supportsPassiveEvents } from 'detect-passive-events';
import { throttle } from 'lodash';
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { LoadMore } from './load_more';
import { LoadPending } from './load_pending';

View File

@@ -63,7 +63,7 @@ class ServerBanner extends PureComponent {
<div className='server-banner__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div>
<div className='server-banner__meta__column'>

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
import classNames from 'classnames';

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
interface Props {
width?: number | string;
height?: number | string;

View File

@@ -12,15 +12,18 @@ import { HotKeys } from 'react-hotkeys';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import PollContainer from 'flavours/glitch/containers/poll_container';
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
import { displayMedia } from 'flavours/glitch/initial_state';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
import Card from '../features/status/components/card';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state';
import AttachmentList from './attachment_list';
import { getHashtagBarForStatus } from './hashtag_bar';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import StatusHeader from './status_header';
@@ -76,6 +79,7 @@ class Status extends ImmutablePureComponent {
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@@ -106,7 +110,6 @@ class Status extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
onClick: PropTypes.func,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
settings: ImmutablePropTypes.map.isRequired,
@@ -566,7 +569,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleSpoiler: this.handleExpandedToggle,
toggleHidden: this.handleExpandedToggle,
bookmark: this.handleHotkeyBookmark,
toggleCollapse: this.handleHotkeyCollapse,
toggleSensitive: this.handleHotkeyToggleSensitive,
@@ -779,6 +782,9 @@ class Status extends ImmutablePureComponent {
muted,
}, 'focusable');
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
return (
<HotKeys handlers={handlers}>
<div
@@ -828,6 +834,7 @@ class Status extends ImmutablePureComponent {
disabled={!history}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (

View File

@@ -8,12 +8,13 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { me } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';

View File

@@ -69,6 +69,15 @@ const isLinkMisleading = (link) => {
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
};
/**
*
* @param {any} status
* @returns {string}
*/
export function getStatusContent(status) {
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
}
class TranslateButton extends PureComponent {
static propTypes = {
@@ -118,6 +127,7 @@ class StatusContent extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool,
collapsed: PropTypes.bool,
onExpandedToggle: PropTypes.func,
@@ -327,6 +337,7 @@ class StatusContent extends PureComponent {
tagLinks,
rewriteMentions,
intl,
statusContent,
} = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
@@ -334,7 +345,7 @@ class StatusContent extends PureComponent {
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {

View File

@@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
// Mastodon imports.
import { Avatar } from './avatar';
import AvatarOverlay from './avatar_overlay';
import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name';
export default class StatusHeader extends PureComponent {
@@ -39,7 +39,7 @@ export default class StatusHeader extends PureComponent {
let statusAvatar;
if (friend === undefined || friend === null) {
statusAvatar = <Avatar account={account} size={48} />;
statusAvatar = <Avatar account={account} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
}

View File

@@ -6,7 +6,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
import RegenerationIndicator from 'flavours/glitch/components/regeneration_indicator';
import StatusContainer from 'flavours/glitch/containers/status_container';
import StatusContainer from '../containers/status_container';
import { LoadGap } from './load_gap';
import ScrollableList from './scrollable_list';

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
interface Props {

View File

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

View File

@@ -9,12 +9,12 @@ import {
unblockAccount,
muteAccount,
unmuteAccount,
} from 'flavours/glitch/actions/accounts';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import Account from 'flavours/glitch/components/account';
import { unfollowModal } from 'flavours/glitch/initial_state';
import { makeGetAccount } from 'flavours/glitch/selectors';
} from '../actions/accounts';
import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes';
import Account from '../components/account';
import { unfollowModal } from '../initial_state';
import { makeGetAccount } from '../selectors';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },

View File

@@ -2,12 +2,13 @@ import { PureComponent } from 'react';
import { Provider } from 'react-redux';
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
import { hydrateStore } from 'flavours/glitch/actions/store';
import Compose from 'flavours/glitch/features/standalone/compose';
import initialState from 'flavours/glitch/initial_state';
import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import Compose from '../features/standalone/compose';
import initialState from '../initial_state';
import { IntlProvider } from '../locales';
import { store } from '../store';
if (initialState) {
store.dispatch(hydrateStore(initialState));

View File

@@ -1,9 +1,8 @@
import { connect } from 'react-redux';
import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu';
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
import { openModal, closeModal } from '../actions/modal';
import DropdownMenu from '../components/dropdown_menu';
import { isUserTouching } from '../is_mobile';
/**

View File

@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { setHeight } from 'flavours/glitch/actions/height_cache';
import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article';
import { setHeight } from '../actions/height_cache';
import IntersectionObserverArticle from '../components/intersection_observer_article';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),

View File

@@ -22,6 +22,7 @@ import { store } from 'flavours/glitch/store';
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
// check for deprecated local settings
@@ -71,8 +72,8 @@ export default class Mastodon extends PureComponent {
}
}
shouldUpdateScroll (_, { location }) {
return !(location.state?.mastodonModalKey);
shouldUpdateScroll (prevRouterProps, { location }) {
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
}
render () {

View File

@@ -10,9 +10,9 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
import Column from 'flavours/glitch/components/column';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import Account from 'flavours/glitch/containers/account_container';
@@ -128,7 +128,7 @@ class About extends PureComponent {
<div className='about__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div>
<hr className='about__meta__divider' />

View File

@@ -1,108 +1,174 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { is } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
placeholder: { id: 'account_note.glitch_placeholder', defaultMessage: 'No comment provided' },
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
});
class Header extends ImmutablePureComponent {
class InlineAlert extends PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
isEditing: PropTypes.bool,
isSubmitting: PropTypes.bool,
accountNote: PropTypes.string,
onEditAccountNote: PropTypes.func.isRequired,
onCancelAccountNote: PropTypes.func.isRequired,
onSaveAccountNote: PropTypes.func.isRequired,
onChangeAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
show: PropTypes.bool,
};
handleChangeAccountNote = (e) => {
this.props.onChangeAccountNote(e.target.value);
state = {
mountMessage: false,
};
componentWillUnmount () {
if (this.props.isEditing) {
this.props.onCancelAccountNote();
static TRANSITION_DELAY = 200;
UNSAFE_componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({ mountMessage: true });
} else if (this.props.show && !nextProps.show) {
setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
}
}
render () {
const { show } = this.props;
const { mountMessage } = this.state;
return (
<span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
{mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
</span>
);
}
}
class AccountNote extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
value: PropTypes.string,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
value: null,
saving: false,
saved: false,
};
UNSAFE_componentWillMount () {
this._reset();
}
UNSAFE_componentWillReceiveProps (nextProps) {
const accountWillChange = !is(this.props.account, nextProps.account);
const newState = {};
if (accountWillChange && this._isDirty()) {
this._save(false);
}
if (accountWillChange || nextProps.value === this.state.value) {
newState.saving = false;
}
if (this.props.value !== nextProps.value) {
newState.value = nextProps.value;
}
this.setState(newState);
}
componentWillUnmount () {
if (this._isDirty()) {
this._save(false);
}
}
setTextareaRef = c => {
this.textarea = c;
};
handleChange = e => {
this.setState({ value: e.target.value, saving: false });
};
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.props.onSaveAccountNote();
e.preventDefault();
this._save();
if (this.textarea) {
this.textarea.blur();
}
} else if (e.keyCode === 27) {
this.props.onCancelAccountNote();
e.preventDefault();
this._reset(() => {
if (this.textarea) {
this.textarea.blur();
}
});
}
};
handleBlur = () => {
if (this._isDirty()) {
this._save();
}
};
_save (showMessage = true) {
this.setState({ saving: true }, () => this.props.onSave(this.state.value));
if (showMessage) {
this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
}
}
_reset (callback) {
this.setState({ value: this.props.value }, callback);
}
_isDirty () {
return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
}
render () {
const { account, accountNote, isEditing, isSubmitting, intl } = this.props;
const { account, intl } = this.props;
const { value, saved } = this.state;
if (!account || (!accountNote && !isEditing)) {
if (!account) {
return null;
}
let action_buttons = null;
if (isEditing) {
action_buttons = (
<div className='account__header__account-note__buttons'>
<button className='icon-button' tabIndex={0} onClick={this.props.onCancelAccountNote} disabled={isSubmitting}>
<Icon id='times' size={15} /> <FormattedMessage id='account_note.cancel' defaultMessage='Cancel' />
</button>
<div className='flex-spacer' />
<button className='icon-button' tabIndex={0} onClick={this.props.onSaveAccountNote} disabled={isSubmitting}>
<Icon id='check' size={15} /> <FormattedMessage id='account_note.save' defaultMessage='Save' />
</button>
</div>
);
} else {
action_buttons = (
<div className='account__header__account-note__buttons'>
<button className='icon-button' tabIndex={0} onClick={this.props.onEditAccountNote} disabled={isSubmitting}>
<Icon id='pencil' size={15} /> <FormattedMessage id='account_note.edit' defaultMessage='Edit' />
</button>
</div>
);
}
let note_container = null;
if (isEditing) {
note_container = (
<Textarea
className='account__header__account-note__content'
disabled={isSubmitting}
placeholder={intl.formatMessage(messages.placeholder)}
value={accountNote}
onChange={this.handleChangeAccountNote}
onKeyDown={this.handleKeyDown}
autoFocus
/>
);
} else {
note_container = (<div className='account__header__account-note__content'>{accountNote}</div>);
}
return (
<div className='account__header__account-note'>
<div className='account__header__account-note__header'>
<strong><FormattedMessage id='account.account_note_header' defaultMessage='Note' /></strong>
{action_buttons}
</div>
{note_container}
<label htmlFor={`account-note-${account.get('id')}`}>
<FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
</label>
<Textarea
id={`account-note-${account.get('id')}`}
className='account__header__account-note__content'
disabled={this.props.value === null || value === null}
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleBlur}
ref={this.setTextareaRef}
/>
</div>
);
}
}
export default injectIntl(Header);
export default injectIntl(AccountNote);

View File

@@ -38,7 +38,7 @@ class FeaturedTags extends ImmutablePureComponent {
name={featuredTag.get('name')}
href={featuredTag.get('url')}
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
uses={featuredTag.get('statuses_count')}
uses={featuredTag.get('statuses_count') * 1}
withGraph={false}
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
/>

View File

@@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
@@ -59,7 +59,6 @@ const messages = defineMessages({
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
});
@@ -98,7 +97,6 @@ class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
@@ -167,8 +165,6 @@ class Header extends ImmutablePureComponent {
return null;
}
const accountNote = account.getIn(['relationship', 'note']);
const suspended = account.get('suspended');
const isRemote = account.get('acct') !== account.get('username');
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
@@ -237,10 +233,6 @@ class Header extends ImmutablePureComponent {
menu.push(null);
}
if (accountNote === null || accountNote === '') {
menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote });
}
if (account.get('id') === me) {
if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });

View File

@@ -1,36 +1,19 @@
import { connect } from 'react-redux';
import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'flavours/glitch/actions/account_notes';
import { submitAccountNote } from 'flavours/glitch/actions/account_notes';
import AccountNote from '../components/account_note';
const mapStateToProps = (state, { account }) => {
const isEditing = state.getIn(['account_notes', 'edit', 'account_id']) === account.get('id');
return {
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
accountNote: isEditing ? state.getIn(['account_notes', 'edit', 'comment']) : account.getIn(['relationship', 'note']),
isEditing,
};
};
const mapStateToProps = (state, { account }) => ({
value: account.getIn(['relationship', 'note']),
});
const mapDispatchToProps = (dispatch, { account }) => ({
onEditAccountNote() {
dispatch(initEditAccountNote(account));
onSave (value) {
dispatch(submitAccountNote({ id: account.get('id'), value}));
},
onSaveAccountNote() {
dispatch(submitAccountNote());
},
onCancelAccountNote() {
dispatch(cancelAccountNote());
},
onChangeAccountNote(comment) {
dispatch(changeAccountNoteComment(comment));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);

View File

@@ -6,11 +6,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
@@ -78,7 +76,7 @@ export default class MediaItem extends ImmutablePureComponent {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}

View File

@@ -8,17 +8,18 @@ import { connect } from 'react-redux';
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
import { openModal } from 'flavours/glitch/actions/modal';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
import { LoadMore } from 'flavours/glitch/components/load_more';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountGallery } from 'flavours/glitch/selectors';
import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
import MediaItem from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {

View File

@@ -7,10 +7,11 @@ import { NavLink, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ActionBar from 'flavours/glitch/features/account/components/action_bar';
import InnerHeader from 'flavours/glitch/features/account/components/header';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import ActionBar from '../../account/components/action_bar';
import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note';
import MovedNote from './moved_note';

View File

@@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Icon } from 'flavours/glitch/components/icon';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import AvatarOverlay from '../../../components/avatar_overlay';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import { DisplayName } from '../../../components/display_name';
class MovedNote extends ImmutablePureComponent {

View File

@@ -2,7 +2,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
import {
followAccount,
unfollowAccount,
@@ -10,19 +9,18 @@ import {
unmuteAccount,
pinAccount,
unpinAccount,
} from 'flavours/glitch/actions/accounts';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
} from '../../../actions/accounts';
import { initBlockModal } from '../../../actions/blocks';
import {
mentionCompose,
directCompose,
} from 'flavours/glitch/actions/compose';
import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initReport } from 'flavours/glitch/actions/reports';
import { unfollowModal } from 'flavours/glitch/initial_state';
import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors';
} from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { unfollowModal } from '../../../initial_state';
import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header';
const messages = defineMessages({
@@ -140,10 +138,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onEditAccountNote (account) {
dispatch(initEditAccountNote(account));
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',

View File

@@ -7,13 +7,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
import { LoadingIndicator } from '../../components/loading_indicator';
@@ -23,13 +23,6 @@ import Column from '../ui/components/column';
import LimitedAccountHint from './components/limited_account_hint';
import HeaderContainer from './containers/header_container';
const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
@@ -191,7 +184,7 @@ class AccountTimeline extends ImmutablePureComponent {
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column ref={this.setRef} name='account'>
<Column ref={this.setRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<StatusList

View File

@@ -9,15 +9,14 @@ import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state';
import Visualizer from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },

View File

@@ -8,13 +8,12 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks';
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import AccountContainer from 'flavours/glitch/containers/account_container';
import Column from 'flavours/glitch/features/ui/components/column';
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
@@ -60,7 +59,7 @@ class Blocks extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
return (
<Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
<Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollableList
scrollKey='blocks'

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