v4: asking for a password reset link is a page, /reset-password — "Forgot password?" unfolded the request form on /login as a state of the page, which a reload or a link lost, while /reset-password, where the emailed link leads, answered without its token "Invalid link". The page now holds both steps: without a token it asks for the link, with one it sets the new password, and "Forgot password?" links to it. The login page's unfolding panel and its styles are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 22:24:13 +00:00
parent 9a215b0cc5
commit 71df0268e3
11 changed files with 322 additions and 381 deletions

5
API.md
View File

@@ -23,7 +23,7 @@ has one word, the same as in pages.
## Pages
| Path | Views | Access | Page |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------ |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------- |
| `/` | `/hot`, `/new`, `/journal`, `/follow`, `/follow/playlists`; the bare `/` is the viewer's default tab, the bare `/follow` the users followed | anyone, `/follow` signed in | `Index` |
| `/<category>` | `/<category>/hot`, `/new`, `/journal`, `/follow`, `/follow/playlists` | anyone, `/follow` signed in | `Index` |
| `/@<name>` | the overview: dumps beside upvoted | anyone | `UserPublicProfile` |
@@ -42,7 +42,8 @@ has one word, the same as in pages.
| `/login`, `/register?token=` | `/login?next=<path>` comes back to a page | signed out | `UserLogin`, `UserRegister` |
| `/authorize?client_id=…` | | signed in, else via `/login?next=` | `Authorize` |
| `/federation/callback?code=&state=` | | anyone | `FederationCallback` |
| `/reset-password?token=`, `/unsubscribe?token=` | | anyone | `ResetPassword`, `Unsubscribe` |
| `/reset-password` | `/reset-password?token=`, from the emailed link, sets the new password | anyone | `ResetPassword` |
| `/unsubscribe?token=` | | anyone | `Unsubscribe` |
`/?share_url=` opens the create form on a link shared from Android,
`/?chat=open` opens the chat over any page, and `#comment-<id>` scrolls a dump's

View File

@@ -206,14 +206,14 @@ msgstr "# Welcome to {site}!\n"
"Your account has been created successfully. Welcome aboard!"
#: src/components/dump/create/DumpCreateModal.tsx:736
#: src/pages/PlaylistDetail.tsx:80
#: src/pages/PlaylistDetail.tsx:98
#: src/pages/UserPublicProfile.tsx:125
msgid "← Back"
msgstr "← Back"
#: src/pages/Dump.tsx:325
#: src/pages/Dump.tsx:552
#: src/pages/DumpEdit.tsx:233
#: src/pages/Dump.tsx:330
#: src/pages/Dump.tsx:557
#: src/pages/DumpEdit.tsx:234
msgid "← Back to all dumps"
msgstr "← Back to all dumps"
@@ -225,7 +225,7 @@ msgstr "+ Invite someone"
msgid "+ New playlist"
msgstr "+ New playlist"
#: src/pages/Dump.tsx:399
#: src/pages/Dump.tsx:404
msgid "+ Playlist"
msgstr "+ Playlist"
@@ -377,7 +377,7 @@ msgstr "Appearance"
#. placeholder {0}: VALIDATION.PASSWORD_MIN
#: src/components/profile/ChangePasswordModal.tsx:101
#: src/pages/ResetPassword.tsx:124
#: src/pages/ResetPassword.tsx:176
#: src/pages/UserRegister.tsx:168
msgid "At least {0} characters"
msgstr "At least {0} characters"
@@ -390,8 +390,8 @@ msgstr "At most {maxLength} characters."
msgid "Auto"
msgstr "Auto"
#: src/pages/ResetPassword.tsx:74
#: src/pages/ResetPassword.tsx:147
#: src/pages/ResetPassword.tsx:101
#: src/pages/ResetPassword.tsx:199
msgid "Back to login"
msgstr "Back to login"
@@ -439,8 +439,8 @@ msgstr "Can't sign in there"
#: src/components/profile/EmailEditor.tsx:83
#: src/components/shared/ConfirmModal.tsx:63
#: src/pages/Authorize.tsx:158
#: src/pages/Dump.tsx:441
#: src/pages/DumpEdit.tsx:560
#: src/pages/Dump.tsx:446
#: src/pages/DumpEdit.tsx:561
msgid "Cancel"
msgstr "Cancel"
@@ -546,7 +546,7 @@ msgid "digest.community"
msgstr "Community"
#: src/components/profile/ChangePasswordModal.tsx:108
#: src/pages/ResetPassword.tsx:131
#: src/pages/ResetPassword.tsx:183
msgid "Confirm new password"
msgstr "Confirm new password"
@@ -582,8 +582,8 @@ msgstr "Could not change password"
msgid "Could not connect to the server. Check your connection and try again."
msgstr "Could not connect to the server. Check your connection and try again."
#: src/components/profile/ProfilePlaylistsTab.tsx:177
#: src/pages/DumpEdit.tsx:554
#: src/components/profile/ProfilePlaylistsTab.tsx:178
#: src/pages/DumpEdit.tsx:555
msgid "Could not delete"
msgstr "Could not delete"
@@ -591,7 +591,7 @@ msgstr "Could not delete"
msgid "Could not rename"
msgstr "Could not rename"
#: src/pages/DumpEdit.tsx:506
#: src/pages/DumpEdit.tsx:507
msgid "Could not save"
msgstr "Could not save"
@@ -668,20 +668,20 @@ msgstr "Delete category"
msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Delete category \"{0}\"? This cannot be undone."
#: src/pages/DumpEdit.tsx:315
#: src/pages/DumpEdit.tsx:557
#: src/pages/DumpEdit.tsx:316
#: src/pages/DumpEdit.tsx:558
msgid "Delete dump"
msgstr "Delete dump"
#. placeholder {0}: res.status
#: src/pages/DumpEdit.tsx:194
#: src/pages/DumpEdit.tsx:195
msgid "Delete failed ({0})"
msgstr "Delete failed ({0})"
#: src/components/playlist/PlaylistCard.tsx:149
#: src/components/playlist/PlaylistEditForm.tsx:213
#: src/components/profile/ProfilePlaylistsTab.tsx:207
#: src/pages/PlaylistDetail.tsx:141
#: src/components/profile/ProfilePlaylistsTab.tsx:208
#: src/pages/PlaylistDetail.tsx:172
msgid "Delete playlist"
msgstr "Delete playlist"
@@ -689,7 +689,7 @@ msgstr "Delete playlist"
msgid "Delete this comment?"
msgstr "Delete this comment?"
#: src/pages/DumpEdit.tsx:314
#: src/pages/DumpEdit.tsx:315
msgid "Delete this dump? This cannot be undone."
msgstr "Delete this dump? This cannot be undone."
@@ -697,8 +697,8 @@ msgstr "Delete this dump? This cannot be undone."
msgid "Delete this message?"
msgstr "Delete this message?"
#: src/components/profile/ProfilePlaylistsTab.tsx:206
#: src/pages/PlaylistDetail.tsx:140
#: src/components/profile/ProfilePlaylistsTab.tsx:207
#: src/pages/PlaylistDetail.tsx:171
msgid "Delete this playlist? This cannot be undone."
msgstr "Delete this playlist? This cannot be undone."
@@ -748,7 +748,7 @@ msgstr "Drop a file here"
msgid "Drop a file here, or paste one"
msgstr "Drop a file here, or paste one"
#: src/pages/DumpEdit.tsx:497
#: src/pages/DumpEdit.tsx:498
msgid "Drop a replacement here"
msgstr "Drop a replacement here"
@@ -788,7 +788,7 @@ msgstr "Earlier"
#: src/components/chat/PendingItem.tsx:93
#: src/components/comment/CommentNode.tsx:256
#: src/components/playlist/PlaylistHeader.tsx:60
#: src/pages/Dump.tsx:548
#: src/pages/Dump.tsx:553
msgid "Edit"
msgstr "Edit"
@@ -806,7 +806,7 @@ msgid "Edit email"
msgstr "Edit email"
#: src/components/dump/create/TitleField.tsx:69
#: src/pages/Dump.tsx:458
#: src/pages/Dump.tsx:463
msgid "Edit title"
msgstr "Edit title"
@@ -820,7 +820,7 @@ msgstr "edited"
#: src/components/chat/ChatMessageItem.tsx:132
#: src/components/comment/CommentNode.tsx:205
#: src/components/playlist/PlaylistHeader.tsx:95
#: src/pages/Dump.tsx:501
#: src/pages/Dump.tsx:506
msgid "edited {0}"
msgstr "edited {0}"
@@ -830,16 +830,16 @@ msgstr "edited {0}"
#: src/components/chat/ChatMessageItem.tsx:128
#: src/components/comment/CommentNode.tsx:203
#: src/components/playlist/PlaylistHeader.tsx:93
#: src/pages/Dump.tsx:499
#: src/pages/Dump.tsx:504
msgid "Edited {0}"
msgstr "Edited {0}"
#: src/pages/DumpEdit.tsx:258
#: src/pages/DumpEdit.tsx:259
msgid "Editing"
msgstr "Editing"
#. placeholder {0}: state.dump.title
#: src/pages/DumpEdit.tsx:81
#: src/pages/DumpEdit.tsx:82
msgid "Editing {0}"
msgstr "Editing {0}"
@@ -942,15 +942,15 @@ msgstr "Follow {targetUsername}"
msgid "Follow playlist"
msgstr "Follow playlist"
#: src/components/feed/FollowedFeed.tsx:151
#: src/components/feed/FollowedFeed.tsx:164
msgid "Follow some public playlists to see their dumps here."
msgstr "Follow some public playlists to see their dumps here."
#: src/components/feed/FollowedFeed.tsx:143
#: src/components/feed/FollowedFeed.tsx:156
msgid "Follow some users to see their dumps here."
msgstr "Follow some users to see their dumps here."
#: src/components/feed/FeedTabBar.tsx:44
#: src/components/feed/FeedTabBar.tsx:61
#: src/components/profile/ProfileFollowTab.tsx:140
#: src/pages/Settings.tsx:57
#: src/pages/UserPublicProfile.tsx:163
@@ -967,11 +967,11 @@ msgstr "Following"
msgid "email.digest.forYou"
msgstr "For you"
#: src/pages/UserLogin.tsx:103
#: src/pages/UserLogin.tsx:92
msgid "Forgot password?"
msgstr "Forgot password?"
#: src/components/feed/FollowedFeed.tsx:132
#: src/components/feed/FollowedFeed.tsx:137
msgid "From people"
msgstr "From people"
@@ -980,7 +980,7 @@ msgstr "From people"
msgid "email.digest.network"
msgstr "From people you follow"
#: src/components/feed/FollowedFeed.tsx:133
#: src/components/feed/FollowedFeed.tsx:138
msgid "From playlists"
msgstr "From playlists"
@@ -992,7 +992,7 @@ msgstr "Fullscreen"
msgid "Go home"
msgstr "Go home"
#: src/pages/ResetPassword.tsx:96
#: src/pages/ResetPassword.tsx:148
msgid "Go to login"
msgstr "Go to login"
@@ -1001,12 +1001,12 @@ msgstr "Go to login"
msgid "email.reset.hi"
msgstr "Hi {username},"
#: src/components/feed/FeedTabBar.tsx:40
#: src/components/feed/FeedTabBar.tsx:57
#: src/pages/Settings.tsx:54
msgid "Hot"
msgstr "Hot"
#: src/pages/UserLogin.tsx:144
#: src/pages/ResetPassword.tsx:76
msgid "If that address is registered you'll receive a reset link shortly."
msgstr "If that address is registered you'll receive a reset link shortly."
@@ -1024,7 +1024,7 @@ msgstr "If you didn't ask for this, ignore this email. Your password stays the s
msgid "image"
msgstr "image"
#: src/pages/Dump.tsx:568
#: src/pages/Dump.tsx:573
msgid "In collections"
msgstr "In collections"
@@ -1046,7 +1046,6 @@ msgstr "Instances"
msgid "Invalid invite"
msgstr "Invalid invite"
#: src/pages/ResetPassword.tsx:64
#: src/pages/Unsubscribe.tsx:66
msgid "Invalid link"
msgstr "Invalid link"
@@ -1075,7 +1074,7 @@ msgstr "invited by you"
msgid "Invitees"
msgstr "Invitees"
#: src/components/feed/FeedTabBar.tsx:42
#: src/components/feed/FeedTabBar.tsx:59
#: src/pages/Settings.tsx:56
msgid "Journal"
msgstr "Journal"
@@ -1126,8 +1125,8 @@ msgstr "Load more"
msgid "Load older messages"
msgstr "Load older messages"
#: src/pages/Dump.tsx:301
#: src/pages/DumpEdit.tsx:209
#: src/pages/Dump.tsx:306
#: src/pages/DumpEdit.tsx:210
msgid "Loading dump…"
msgstr "Loading dump…"
@@ -1139,7 +1138,7 @@ msgstr "Loading messages…"
msgid "Loading more…"
msgstr "Loading more…"
#: src/pages/PlaylistDetail.tsx:64
#: src/pages/PlaylistDetail.tsx:82
msgid "Loading playlist…"
msgstr "Loading playlist…"
@@ -1161,9 +1160,9 @@ msgstr "Loading…"
#: src/components/layout/AppHeader.tsx:208
#: src/pages/FederationCallback.tsx:32
#: src/pages/UserLogin.tsx:46
#: src/pages/UserLogin.tsx:73
#: src/pages/UserLogin.tsx:92
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:66
#: src/pages/UserLogin.tsx:85
msgid "Log in"
msgstr "Log in"
@@ -1185,12 +1184,12 @@ msgstr "Log in with {0}"
msgid "Log out"
msgstr "Log out"
#: src/pages/UserLogin.tsx:91
#: src/pages/UserLogin.tsx:84
msgid "Logging in…"
msgstr "Logging in…"
#: src/pages/FederationCallback.tsx:69
#: src/pages/UserLogin.tsx:78
#: src/pages/UserLogin.tsx:71
msgid "Login failed"
msgstr "Login failed"
@@ -1228,7 +1227,7 @@ msgid "Name"
msgstr "Name"
#: src/components/chat/ChatModal.tsx:692
#: src/components/feed/FeedTabBar.tsx:41
#: src/components/feed/FeedTabBar.tsx:58
#: src/pages/Settings.tsx:55
msgid "New"
msgstr "New"
@@ -1246,7 +1245,7 @@ msgid "digest.newMembers"
msgstr "New members"
#: src/components/profile/ChangePasswordModal.tsx:94
#: src/pages/ResetPassword.tsx:116
#: src/pages/ResetPassword.tsx:168
msgid "New password"
msgstr "New password"
@@ -1306,7 +1305,7 @@ msgid "No playlists match \"{q}\"."
msgstr "No playlists match \"{q}\"."
#: src/components/playlist/PlaylistMembershipPanel.tsx:51
#: src/components/profile/ProfilePlaylistsTab.tsx:184
#: src/components/profile/ProfilePlaylistsTab.tsx:185
#: src/pages/Playlists.tsx:118
msgid "No playlists yet."
msgstr "No playlists yet."
@@ -1369,7 +1368,7 @@ msgstr "Once a week you'll get a digest of what happened here, and once a month
msgid "Only images can be dropped here."
msgstr "Only images can be dropped here."
#: src/pages/DumpEdit.tsx:119
#: src/pages/DumpEdit.tsx:120
msgid "Only its poster or a moderator can edit this dump."
msgstr "Only its poster or a moderator can edit this dump."
@@ -1418,7 +1417,7 @@ msgstr "Page not found"
#: src/pages/Settings.tsx:96
#: src/pages/Settings.tsx:106
#: src/pages/UserLogin.tsx:88
#: src/pages/UserLogin.tsx:81
msgid "Password"
msgstr "Password"
@@ -1431,12 +1430,12 @@ msgstr "Password (min. {0} characters)"
msgid "Password changed successfully."
msgstr "Password changed successfully."
#: src/pages/ResetPassword.tsx:86
#: src/pages/ResetPassword.tsx:138
msgid "Password updated"
msgstr "Password updated"
#: src/components/profile/ChangePasswordModal.tsx:114
#: src/pages/ResetPassword.tsx:136
#: src/pages/ResetPassword.tsx:188
msgid "Passwords do not match"
msgstr "Passwords do not match"
@@ -1471,7 +1470,7 @@ msgstr "Playlist categories"
#: src/components/layout/AppHeader.tsx:172
#: src/components/layout/UserMenu.tsx:92
#: src/components/profile/ProfileFollowTab.tsx:144
#: src/components/profile/ProfilePlaylistsTab.tsx:155
#: src/components/profile/ProfilePlaylistsTab.tsx:156
#: src/pages/Playlists.tsx:52
#: src/pages/Playlists.tsx:106
#: src/pages/Search.tsx:151
@@ -1502,7 +1501,7 @@ msgstr "Previous track"
#: src/components/playlist/PlaylistCard.tsx:106
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/playlist/PlaylistMembershipPanel.tsx:81
#: src/pages/Dump.tsx:507
#: src/pages/Dump.tsx:512
msgid "private"
msgstr "private"
@@ -1519,7 +1518,7 @@ msgstr "public"
msgid "Public"
msgstr "Public"
#: src/pages/DumpEdit.tsx:291
#: src/pages/DumpEdit.tsx:292
msgid "Refresh metadata"
msgstr "Refresh metadata"
@@ -1527,7 +1526,7 @@ msgstr "Refresh metadata"
msgid "Refreshing stream…"
msgstr "Refreshing stream…"
#: src/pages/DumpEdit.tsx:290
#: src/pages/DumpEdit.tsx:291
msgid "Refreshing…"
msgstr "Refreshing…"
@@ -1545,7 +1544,7 @@ msgstr "Registering…"
msgid "Registration failed"
msgstr "Registration failed"
#: src/pages/Dump.tsx:559
#: src/pages/Dump.tsx:564
msgid "Related"
msgstr "Related"
@@ -1579,7 +1578,7 @@ msgstr "Rename"
msgid "Renaming…"
msgstr "Renaming…"
#: src/pages/DumpEdit.tsx:489
#: src/pages/DumpEdit.tsx:490
msgid "Replace file"
msgstr "Replace file"
@@ -1593,11 +1592,11 @@ msgstr "Reply"
msgid "Replying to"
msgstr "Replying to"
#: src/pages/UserLogin.tsx:156
#: src/pages/ResetPassword.tsx:85
msgid "Request failed"
msgstr "Request failed"
#: src/pages/ResetPassword.tsx:112
#: src/pages/ResetPassword.tsx:164
msgid "Reset failed"
msgstr "Reset failed"
@@ -1606,12 +1605,13 @@ msgstr "Reset failed"
msgid "email.reset.button"
msgstr "Reset password"
#: src/pages/ResetPassword.tsx:34
#: src/pages/ResetPassword.tsx:40
#: src/pages/ResetPassword.tsx:71
msgid "Reset password"
msgstr "Reset password"
#: src/pages/DumpEdit.tsx:465
#: src/pages/DumpEdit.tsx:520
#: src/pages/DumpEdit.tsx:466
#: src/pages/DumpEdit.tsx:521
msgid "Reset to default"
msgstr "Reset to default"
@@ -1631,8 +1631,8 @@ msgid "email.reset.title"
msgstr "Reset your password"
#: src/components/chat/PendingItem.tsx:86
#: src/pages/Dump.tsx:318
#: src/pages/DumpEdit.tsx:226
#: src/pages/Dump.tsx:323
#: src/pages/DumpEdit.tsx:227
msgid "Retry"
msgstr "Retry"
@@ -1646,8 +1646,8 @@ msgstr "Role"
#: src/components/playlist/PlaylistEditForm.tsx:229
#: src/components/profile/DescriptionEditor.tsx:89
#: src/components/profile/EmailEditor.tsx:75
#: src/pages/Dump.tsx:433
#: src/pages/DumpEdit.tsx:563
#: src/pages/Dump.tsx:438
#: src/pages/DumpEdit.tsx:564
msgid "Save"
msgstr "Save"
@@ -1656,8 +1656,8 @@ msgstr "Save"
#: src/components/profile/ChangePasswordModal.tsx:119
#: src/components/profile/DescriptionEditor.tsx:86
#: src/components/profile/EmailEditor.tsx:72
#: src/pages/Dump.tsx:432
#: src/pages/ResetPassword.tsx:139
#: src/pages/Dump.tsx:437
#: src/pages/ResetPassword.tsx:191
msgid "Saving…"
msgstr "Saving…"
@@ -1700,12 +1700,12 @@ msgstr "Seek"
msgid "Send"
msgstr "Send"
#: src/pages/UserLogin.tsx:165
#: src/pages/ResetPassword.tsx:94
msgid "Send reset link"
msgstr "Send reset link"
#: src/components/chat/PendingItem.tsx:104
#: src/pages/UserLogin.tsx:164
#: src/pages/ResetPassword.tsx:93
msgid "Sending…"
msgstr "Sending…"
@@ -1713,8 +1713,8 @@ msgstr "Sending…"
msgid "Server unreachable"
msgstr "Server unreachable"
#: src/pages/ResetPassword.tsx:107
#: src/pages/ResetPassword.tsx:140
#: src/pages/ResetPassword.tsx:159
#: src/pages/ResetPassword.tsx:192
msgid "Set new password"
msgstr "Set new password"
@@ -1766,7 +1766,7 @@ msgstr "Someone asked to reset the password of your {site} account. The button b
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:732
#: src/pages/PlaylistDetail.tsx:124
#: src/pages/PlaylistDetail.tsx:155
msgid "Something went wrong"
msgstr "Something went wrong"
@@ -1799,7 +1799,7 @@ msgstr "the chat"
msgid "the chat"
msgstr "the chat"
#: src/hooks/playlist/usePlaylist.ts:179
#: src/hooks/playlist/usePlaylist.ts:182
msgid "The dump couldn't be removed. Try again."
msgstr "The dump couldn't be removed. Try again."
@@ -1822,7 +1822,7 @@ msgstr "The month on {site}"
msgid "email.digest.monthly.subject"
msgstr "The month on {site}, {range}"
#: src/hooks/playlist/usePlaylist.ts:322
#: src/hooks/playlist/usePlaylist.ts:325
msgid "The new order couldn't be saved. Try again."
msgstr "The new order couldn't be saved. Try again."
@@ -1834,7 +1834,7 @@ msgstr "This comment couldn't be deleted. Try again."
msgid "This invite link is missing, expired, or already used."
msgstr "This invite link is missing, expired, or already used."
#: src/pages/UserLogin.tsx:112
#: src/pages/UserLogin.tsx:99
msgid "This is a mirage."
msgstr "This is a mirage."
@@ -1846,10 +1846,6 @@ msgstr "This month"
msgid "This page does not exist."
msgstr "This page does not exist."
#: src/pages/ResetPassword.tsx:67
msgid "This reset link is missing or malformed."
msgstr "This reset link is missing or malformed."
#: src/pages/FederationCallback.tsx:40
msgid "This sign-in link is incomplete."
msgstr "This sign-in link is incomplete."
@@ -1870,14 +1866,14 @@ msgstr "This unsubscribe link is missing or malformed."
msgid "This week"
msgstr "This week"
#: src/pages/DumpEdit.tsx:449
#: src/pages/DumpEdit.tsx:450
msgid "Thumbnail"
msgstr "Thumbnail"
#: src/components/dump/create/TitleField.tsx:45
#: src/components/playlist/PlaylistCreateForm.tsx:100
#: src/components/playlist/PlaylistEditForm.tsx:175
#: src/pages/DumpEdit.tsx:510
#: src/pages/DumpEdit.tsx:511
msgid "Title"
msgstr "Title"
@@ -2007,7 +2003,7 @@ msgstr "Upvoted"
msgid "Upvoted ({0})"
msgstr "Upvoted ({0})"
#: src/pages/DumpEdit.tsx:481
#: src/pages/DumpEdit.tsx:482
msgid "URL"
msgstr "URL"
@@ -2023,7 +2019,7 @@ msgstr "URL slug"
msgid "User"
msgstr "User"
#: src/components/profile/ProfileManageTab.tsx:54
#: src/components/profile/ProfileManageTab.tsx:56
msgid "User management"
msgstr "User management"
@@ -2031,10 +2027,10 @@ msgstr "User management"
msgid "User menu"
msgstr "User menu"
#: src/components/profile/ProfileManageTab.tsx:61
#: src/components/profile/ProfileManageTab.tsx:63
#: src/components/profile/UsernameField.tsx:51
#: src/pages/Settings.tsx:91
#: src/pages/UserLogin.tsx:81
#: src/pages/UserLogin.tsx:74
#: src/pages/UserRegister.tsx:141
msgid "Username"
msgstr "Username"
@@ -2079,7 +2075,7 @@ msgid "email.welcome.subject"
msgstr "Welcome to {site}"
#: src/components/dump/create/DumpCreateModal.tsx:705
#: src/pages/DumpEdit.tsx:538
#: src/pages/DumpEdit.tsx:539
msgid "What makes it worth it?"
msgstr "What makes it worth it?"
@@ -2097,7 +2093,7 @@ msgid "Why & where"
msgstr "Why & where"
#: src/components/dump/create/DumpCreateModal.tsx:704
#: src/pages/DumpEdit.tsx:537
#: src/pages/DumpEdit.tsx:538
msgid "Why?"
msgstr "Why?"
@@ -2171,7 +2167,7 @@ msgstr "Your account lives on {home}: sign in to other instances from there."
msgid "Your account lives on {home}: your password, email address, avatar and username change there."
msgstr "Your account lives on {home}: your password, email address, avatar and username change there."
#: src/pages/UserLogin.tsx:160
#: src/pages/ResetPassword.tsx:89
msgid "Your email address"
msgstr "Your email address"
@@ -2179,7 +2175,7 @@ msgstr "Your email address"
msgid "Your gerbeur's URL"
msgstr "Your gerbeur's URL"
#: src/pages/ResetPassword.tsx:89
#: src/pages/ResetPassword.tsx:141
msgid "Your password has been changed. You can now log in."
msgstr "Your password has been changed. You can now log in."

View File

@@ -206,14 +206,14 @@ msgstr "# Bienvenue sur {site} !\n"
"Votre compte a bien été créé. Bienvenue à bord !"
#: src/components/dump/create/DumpCreateModal.tsx:736
#: src/pages/PlaylistDetail.tsx:80
#: src/pages/PlaylistDetail.tsx:98
#: src/pages/UserPublicProfile.tsx:125
msgid "← Back"
msgstr "← Retour"
#: src/pages/Dump.tsx:325
#: src/pages/Dump.tsx:552
#: src/pages/DumpEdit.tsx:233
#: src/pages/Dump.tsx:330
#: src/pages/Dump.tsx:557
#: src/pages/DumpEdit.tsx:234
msgid "← Back to all dumps"
msgstr "← Retour à toutes les recos"
@@ -225,7 +225,7 @@ msgstr "+ Inviter quelqu'un"
msgid "+ New playlist"
msgstr "+ Nouvelle collection"
#: src/pages/Dump.tsx:399
#: src/pages/Dump.tsx:404
msgid "+ Playlist"
msgstr "+ Collection"
@@ -377,7 +377,7 @@ msgstr "Apparence"
#. placeholder {0}: VALIDATION.PASSWORD_MIN
#: src/components/profile/ChangePasswordModal.tsx:101
#: src/pages/ResetPassword.tsx:124
#: src/pages/ResetPassword.tsx:176
#: src/pages/UserRegister.tsx:168
msgid "At least {0} characters"
msgstr "Au moins {0} caractères"
@@ -390,8 +390,8 @@ msgstr "{maxLength} caractères au maximum."
msgid "Auto"
msgstr "Auto"
#: src/pages/ResetPassword.tsx:74
#: src/pages/ResetPassword.tsx:147
#: src/pages/ResetPassword.tsx:101
#: src/pages/ResetPassword.tsx:199
msgid "Back to login"
msgstr "Retour à la connexion"
@@ -439,8 +439,8 @@ msgstr "Connexion impossible"
#: src/components/profile/EmailEditor.tsx:83
#: src/components/shared/ConfirmModal.tsx:63
#: src/pages/Authorize.tsx:158
#: src/pages/Dump.tsx:441
#: src/pages/DumpEdit.tsx:560
#: src/pages/Dump.tsx:446
#: src/pages/DumpEdit.tsx:561
msgid "Cancel"
msgstr "Annuler"
@@ -546,7 +546,7 @@ msgid "digest.community"
msgstr "Communauté"
#: src/components/profile/ChangePasswordModal.tsx:108
#: src/pages/ResetPassword.tsx:131
#: src/pages/ResetPassword.tsx:183
msgid "Confirm new password"
msgstr "Confirmer le nouveau mot de passe"
@@ -582,8 +582,8 @@ msgstr "Impossible de changer le mot de passe"
msgid "Could not connect to the server. Check your connection and try again."
msgstr "Impossible de joindre le serveur. Vérifiez votre connexion et réessayez."
#: src/components/profile/ProfilePlaylistsTab.tsx:177
#: src/pages/DumpEdit.tsx:554
#: src/components/profile/ProfilePlaylistsTab.tsx:178
#: src/pages/DumpEdit.tsx:555
msgid "Could not delete"
msgstr "Suppression impossible"
@@ -591,7 +591,7 @@ msgstr "Suppression impossible"
msgid "Could not rename"
msgstr "Renommage impossible"
#: src/pages/DumpEdit.tsx:506
#: src/pages/DumpEdit.tsx:507
msgid "Could not save"
msgstr "Sauvegarde impossible"
@@ -668,20 +668,20 @@ msgstr "Supprimer la catégorie"
msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Supprimer la catégorie \"{0}\" ? Cette action est irréversible."
#: src/pages/DumpEdit.tsx:315
#: src/pages/DumpEdit.tsx:557
#: src/pages/DumpEdit.tsx:316
#: src/pages/DumpEdit.tsx:558
msgid "Delete dump"
msgstr "Supprimer la reco"
#. placeholder {0}: res.status
#: src/pages/DumpEdit.tsx:194
#: src/pages/DumpEdit.tsx:195
msgid "Delete failed ({0})"
msgstr "Échec de la suppression ({0})"
#: src/components/playlist/PlaylistCard.tsx:149
#: src/components/playlist/PlaylistEditForm.tsx:213
#: src/components/profile/ProfilePlaylistsTab.tsx:207
#: src/pages/PlaylistDetail.tsx:141
#: src/components/profile/ProfilePlaylistsTab.tsx:208
#: src/pages/PlaylistDetail.tsx:172
msgid "Delete playlist"
msgstr "Supprimer la collection"
@@ -689,7 +689,7 @@ msgstr "Supprimer la collection"
msgid "Delete this comment?"
msgstr "Supprimer ce commentaire ?"
#: src/pages/DumpEdit.tsx:314
#: src/pages/DumpEdit.tsx:315
msgid "Delete this dump? This cannot be undone."
msgstr "Supprimer cette reco ? Cette action est irréversible."
@@ -697,8 +697,8 @@ msgstr "Supprimer cette reco ? Cette action est irréversible."
msgid "Delete this message?"
msgstr "Supprimer ce message ?"
#: src/components/profile/ProfilePlaylistsTab.tsx:206
#: src/pages/PlaylistDetail.tsx:140
#: src/components/profile/ProfilePlaylistsTab.tsx:207
#: src/pages/PlaylistDetail.tsx:171
msgid "Delete this playlist? This cannot be undone."
msgstr "Supprimer cette collection ? Cette action est irréversible."
@@ -748,7 +748,7 @@ msgstr "Déposez un fichier ici"
msgid "Drop a file here, or paste one"
msgstr "Déposez un fichier ici, ou collez-en un"
#: src/pages/DumpEdit.tsx:497
#: src/pages/DumpEdit.tsx:498
msgid "Drop a replacement here"
msgstr "Déposez un fichier de remplacement ici"
@@ -788,7 +788,7 @@ msgstr "Plus tôt"
#: src/components/chat/PendingItem.tsx:93
#: src/components/comment/CommentNode.tsx:256
#: src/components/playlist/PlaylistHeader.tsx:60
#: src/pages/Dump.tsx:548
#: src/pages/Dump.tsx:553
msgid "Edit"
msgstr "Modifier"
@@ -806,7 +806,7 @@ msgid "Edit email"
msgstr "Modifier l'e-mail"
#: src/components/dump/create/TitleField.tsx:69
#: src/pages/Dump.tsx:458
#: src/pages/Dump.tsx:463
msgid "Edit title"
msgstr "Modifier le titre"
@@ -820,7 +820,7 @@ msgstr "modifié"
#: src/components/chat/ChatMessageItem.tsx:132
#: src/components/comment/CommentNode.tsx:205
#: src/components/playlist/PlaylistHeader.tsx:95
#: src/pages/Dump.tsx:501
#: src/pages/Dump.tsx:506
msgid "edited {0}"
msgstr "modifié {0}"
@@ -830,16 +830,16 @@ msgstr "modifié {0}"
#: src/components/chat/ChatMessageItem.tsx:128
#: src/components/comment/CommentNode.tsx:203
#: src/components/playlist/PlaylistHeader.tsx:93
#: src/pages/Dump.tsx:499
#: src/pages/Dump.tsx:504
msgid "Edited {0}"
msgstr "Modifié le {0}"
#: src/pages/DumpEdit.tsx:258
#: src/pages/DumpEdit.tsx:259
msgid "Editing"
msgstr "Modification"
#. placeholder {0}: state.dump.title
#: src/pages/DumpEdit.tsx:81
#: src/pages/DumpEdit.tsx:82
msgid "Editing {0}"
msgstr "Édition de {0}"
@@ -942,15 +942,15 @@ msgstr "Suivre {targetUsername}"
msgid "Follow playlist"
msgstr "Suivre la collection"
#: src/components/feed/FollowedFeed.tsx:151
#: src/components/feed/FollowedFeed.tsx:164
msgid "Follow some public playlists to see their dumps here."
msgstr "Suivez des collections publiques pour voir leurs recos ici."
#: src/components/feed/FollowedFeed.tsx:143
#: src/components/feed/FollowedFeed.tsx:156
msgid "Follow some users to see their dumps here."
msgstr "Suivez des utilisateurs pour voir leurs recos ici."
#: src/components/feed/FeedTabBar.tsx:44
#: src/components/feed/FeedTabBar.tsx:61
#: src/components/profile/ProfileFollowTab.tsx:140
#: src/pages/Settings.tsx:57
#: src/pages/UserPublicProfile.tsx:163
@@ -967,11 +967,11 @@ msgstr "Abonné"
msgid "email.digest.forYou"
msgstr "Pour vous"
#: src/pages/UserLogin.tsx:103
#: src/pages/UserLogin.tsx:92
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
#: src/components/feed/FollowedFeed.tsx:132
#: src/components/feed/FollowedFeed.tsx:137
msgid "From people"
msgstr "De personnes"
@@ -980,7 +980,7 @@ msgstr "De personnes"
msgid "email.digest.network"
msgstr "De ceux que vous suivez"
#: src/components/feed/FollowedFeed.tsx:133
#: src/components/feed/FollowedFeed.tsx:138
msgid "From playlists"
msgstr "De collections"
@@ -992,7 +992,7 @@ msgstr "Plein écran"
msgid "Go home"
msgstr "Accueil"
#: src/pages/ResetPassword.tsx:96
#: src/pages/ResetPassword.tsx:148
msgid "Go to login"
msgstr "Aller à la connexion"
@@ -1001,12 +1001,12 @@ msgstr "Aller à la connexion"
msgid "email.reset.hi"
msgstr "Bonjour {username},"
#: src/components/feed/FeedTabBar.tsx:40
#: src/components/feed/FeedTabBar.tsx:57
#: src/pages/Settings.tsx:54
msgid "Hot"
msgstr "Tendances"
#: src/pages/UserLogin.tsx:144
#: src/pages/ResetPassword.tsx:76
msgid "If that address is registered you'll receive a reset link shortly."
msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."
@@ -1024,7 +1024,7 @@ msgstr "Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. V
msgid "image"
msgstr "image"
#: src/pages/Dump.tsx:568
#: src/pages/Dump.tsx:573
msgid "In collections"
msgstr "Dans les collections"
@@ -1046,7 +1046,6 @@ msgstr "Instances"
msgid "Invalid invite"
msgstr "Invitation invalide"
#: src/pages/ResetPassword.tsx:64
#: src/pages/Unsubscribe.tsx:66
msgid "Invalid link"
msgstr "Lien invalide"
@@ -1075,7 +1074,7 @@ msgstr "sur votre invitation"
msgid "Invitees"
msgstr "Invités"
#: src/components/feed/FeedTabBar.tsx:42
#: src/components/feed/FeedTabBar.tsx:59
#: src/pages/Settings.tsx:56
msgid "Journal"
msgstr "Journal"
@@ -1126,8 +1125,8 @@ msgstr "Charger plus"
msgid "Load older messages"
msgstr "Charger les messages plus anciens"
#: src/pages/Dump.tsx:301
#: src/pages/DumpEdit.tsx:209
#: src/pages/Dump.tsx:306
#: src/pages/DumpEdit.tsx:210
msgid "Loading dump…"
msgstr "Chargement de la reco…"
@@ -1139,7 +1138,7 @@ msgstr "Chargement des messages…"
msgid "Loading more…"
msgstr "Chargement…"
#: src/pages/PlaylistDetail.tsx:64
#: src/pages/PlaylistDetail.tsx:82
msgid "Loading playlist…"
msgstr "Chargement de la collection…"
@@ -1161,9 +1160,9 @@ msgstr "Chargement…"
#: src/components/layout/AppHeader.tsx:208
#: src/pages/FederationCallback.tsx:32
#: src/pages/UserLogin.tsx:46
#: src/pages/UserLogin.tsx:73
#: src/pages/UserLogin.tsx:92
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:66
#: src/pages/UserLogin.tsx:85
msgid "Log in"
msgstr "Se connecter"
@@ -1185,12 +1184,12 @@ msgstr "Se connecter avec {0}"
msgid "Log out"
msgstr "Se déconnecter"
#: src/pages/UserLogin.tsx:91
#: src/pages/UserLogin.tsx:84
msgid "Logging in…"
msgstr "Connexion…"
#: src/pages/FederationCallback.tsx:69
#: src/pages/UserLogin.tsx:78
#: src/pages/UserLogin.tsx:71
msgid "Login failed"
msgstr "Connexion échouée"
@@ -1228,7 +1227,7 @@ msgid "Name"
msgstr "Nom"
#: src/components/chat/ChatModal.tsx:692
#: src/components/feed/FeedTabBar.tsx:41
#: src/components/feed/FeedTabBar.tsx:58
#: src/pages/Settings.tsx:55
msgid "New"
msgstr "Nouveau"
@@ -1246,7 +1245,7 @@ msgid "digest.newMembers"
msgstr "Nouveaux membres"
#: src/components/profile/ChangePasswordModal.tsx:94
#: src/pages/ResetPassword.tsx:116
#: src/pages/ResetPassword.tsx:168
msgid "New password"
msgstr "Nouveau mot de passe"
@@ -1306,7 +1305,7 @@ msgid "No playlists match \"{q}\"."
msgstr "Aucune collection ne correspond à « {q} »."
#: src/components/playlist/PlaylistMembershipPanel.tsx:51
#: src/components/profile/ProfilePlaylistsTab.tsx:184
#: src/components/profile/ProfilePlaylistsTab.tsx:185
#: src/pages/Playlists.tsx:118
msgid "No playlists yet."
msgstr "Pas encore de collections."
@@ -1369,7 +1368,7 @@ msgstr "Chaque semaine, vous recevrez un résumé de ce qui s'est passé ici, et
msgid "Only images can be dropped here."
msgstr "Seules les images peuvent être déposées ici."
#: src/pages/DumpEdit.tsx:119
#: src/pages/DumpEdit.tsx:120
msgid "Only its poster or a moderator can edit this dump."
msgstr "Seuls son auteur et les modérateurs peuvent modifier ce dump."
@@ -1418,7 +1417,7 @@ msgstr "Page introuvable"
#: src/pages/Settings.tsx:96
#: src/pages/Settings.tsx:106
#: src/pages/UserLogin.tsx:88
#: src/pages/UserLogin.tsx:81
msgid "Password"
msgstr "Mot de passe"
@@ -1431,12 +1430,12 @@ msgstr "Mot de passe (min. {0} caractères)"
msgid "Password changed successfully."
msgstr "Mot de passe modifié avec succès."
#: src/pages/ResetPassword.tsx:86
#: src/pages/ResetPassword.tsx:138
msgid "Password updated"
msgstr "Mot de passe mis à jour"
#: src/components/profile/ChangePasswordModal.tsx:114
#: src/pages/ResetPassword.tsx:136
#: src/pages/ResetPassword.tsx:188
msgid "Passwords do not match"
msgstr "Les mots de passe ne correspondent pas"
@@ -1471,7 +1470,7 @@ msgstr "Catégories de collections"
#: src/components/layout/AppHeader.tsx:172
#: src/components/layout/UserMenu.tsx:92
#: src/components/profile/ProfileFollowTab.tsx:144
#: src/components/profile/ProfilePlaylistsTab.tsx:155
#: src/components/profile/ProfilePlaylistsTab.tsx:156
#: src/pages/Playlists.tsx:52
#: src/pages/Playlists.tsx:106
#: src/pages/Search.tsx:151
@@ -1502,7 +1501,7 @@ msgstr "Piste précédente"
#: src/components/playlist/PlaylistCard.tsx:106
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/playlist/PlaylistMembershipPanel.tsx:81
#: src/pages/Dump.tsx:507
#: src/pages/Dump.tsx:512
msgid "private"
msgstr "privé"
@@ -1519,7 +1518,7 @@ msgstr "public"
msgid "Public"
msgstr "Public"
#: src/pages/DumpEdit.tsx:291
#: src/pages/DumpEdit.tsx:292
msgid "Refresh metadata"
msgstr "Actualiser les métadonnées"
@@ -1527,7 +1526,7 @@ msgstr "Actualiser les métadonnées"
msgid "Refreshing stream…"
msgstr "Actualisation du flux…"
#: src/pages/DumpEdit.tsx:290
#: src/pages/DumpEdit.tsx:291
msgid "Refreshing…"
msgstr "Actualisation…"
@@ -1545,7 +1544,7 @@ msgstr "Inscription…"
msgid "Registration failed"
msgstr "Inscription échouée"
#: src/pages/Dump.tsx:559
#: src/pages/Dump.tsx:564
msgid "Related"
msgstr "Connexe"
@@ -1579,7 +1578,7 @@ msgstr "Renommer"
msgid "Renaming…"
msgstr "Renommage…"
#: src/pages/DumpEdit.tsx:489
#: src/pages/DumpEdit.tsx:490
msgid "Replace file"
msgstr "Remplacer le fichier"
@@ -1593,11 +1592,11 @@ msgstr "Répondre"
msgid "Replying to"
msgstr "En réponse à"
#: src/pages/UserLogin.tsx:156
#: src/pages/ResetPassword.tsx:85
msgid "Request failed"
msgstr "Échec de la demande"
#: src/pages/ResetPassword.tsx:112
#: src/pages/ResetPassword.tsx:164
msgid "Reset failed"
msgstr "Échec de la réinitialisation"
@@ -1606,12 +1605,13 @@ msgstr "Échec de la réinitialisation"
msgid "email.reset.button"
msgstr "Choisir un nouveau mot de passe"
#: src/pages/ResetPassword.tsx:34
#: src/pages/ResetPassword.tsx:40
#: src/pages/ResetPassword.tsx:71
msgid "Reset password"
msgstr "Réinitialiser le mot de passe"
#: src/pages/DumpEdit.tsx:465
#: src/pages/DumpEdit.tsx:520
#: src/pages/DumpEdit.tsx:466
#: src/pages/DumpEdit.tsx:521
msgid "Reset to default"
msgstr "Réinitialiser par défaut"
@@ -1631,8 +1631,8 @@ msgid "email.reset.title"
msgstr "Réinitialiser votre mot de passe"
#: src/components/chat/PendingItem.tsx:86
#: src/pages/Dump.tsx:318
#: src/pages/DumpEdit.tsx:226
#: src/pages/Dump.tsx:323
#: src/pages/DumpEdit.tsx:227
msgid "Retry"
msgstr "Réessayer"
@@ -1646,8 +1646,8 @@ msgstr "Rôle"
#: src/components/playlist/PlaylistEditForm.tsx:229
#: src/components/profile/DescriptionEditor.tsx:89
#: src/components/profile/EmailEditor.tsx:75
#: src/pages/Dump.tsx:433
#: src/pages/DumpEdit.tsx:563
#: src/pages/Dump.tsx:438
#: src/pages/DumpEdit.tsx:564
msgid "Save"
msgstr "Enregistrer"
@@ -1656,8 +1656,8 @@ msgstr "Enregistrer"
#: src/components/profile/ChangePasswordModal.tsx:119
#: src/components/profile/DescriptionEditor.tsx:86
#: src/components/profile/EmailEditor.tsx:72
#: src/pages/Dump.tsx:432
#: src/pages/ResetPassword.tsx:139
#: src/pages/Dump.tsx:437
#: src/pages/ResetPassword.tsx:191
msgid "Saving…"
msgstr "Enregistrement…"
@@ -1700,12 +1700,12 @@ msgstr "Position"
msgid "Send"
msgstr "Envoyer"
#: src/pages/UserLogin.tsx:165
#: src/pages/ResetPassword.tsx:94
msgid "Send reset link"
msgstr "Envoyer le lien de réinitialisation"
#: src/components/chat/PendingItem.tsx:104
#: src/pages/UserLogin.tsx:164
#: src/pages/ResetPassword.tsx:93
msgid "Sending…"
msgstr "Envoi…"
@@ -1713,8 +1713,8 @@ msgstr "Envoi…"
msgid "Server unreachable"
msgstr "Serveur inaccessible"
#: src/pages/ResetPassword.tsx:107
#: src/pages/ResetPassword.tsx:140
#: src/pages/ResetPassword.tsx:159
#: src/pages/ResetPassword.tsx:192
msgid "Set new password"
msgstr "Définir un nouveau mot de passe"
@@ -1766,7 +1766,7 @@ msgstr "Quelqu'un a demandé à réinitialiser le mot de passe de votre compte {
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:732
#: src/pages/PlaylistDetail.tsx:124
#: src/pages/PlaylistDetail.tsx:155
msgid "Something went wrong"
msgstr "Une erreur est survenue"
@@ -1799,7 +1799,7 @@ msgstr "la tribune"
msgid "the chat"
msgstr "la tribune"
#: src/hooks/playlist/usePlaylist.ts:179
#: src/hooks/playlist/usePlaylist.ts:182
msgid "The dump couldn't be removed. Try again."
msgstr "Le dump n'a pas pu être retiré. Réessayez."
@@ -1822,7 +1822,7 @@ msgstr "Le mois sur {site}"
msgid "email.digest.monthly.subject"
msgstr "Le mois sur {site}, {range}"
#: src/hooks/playlist/usePlaylist.ts:322
#: src/hooks/playlist/usePlaylist.ts:325
msgid "The new order couldn't be saved. Try again."
msgstr "Le nouvel ordre n'a pas pu être enregistré. Réessayez."
@@ -1834,7 +1834,7 @@ msgstr "Ce commentaire n'a pas pu être supprimé. Réessayez."
msgid "This invite link is missing, expired, or already used."
msgstr "Ce lien d'invitation est manquant, expiré ou déjà utilisé."
#: src/pages/UserLogin.tsx:112
#: src/pages/UserLogin.tsx:99
msgid "This is a mirage."
msgstr "C'est un mirage."
@@ -1846,10 +1846,6 @@ msgstr "Ce mois-ci"
msgid "This page does not exist."
msgstr "Rien à voir, circulez."
#: src/pages/ResetPassword.tsx:67
msgid "This reset link is missing or malformed."
msgstr "Ce lien de réinitialisation est absent ou malformé."
#: src/pages/FederationCallback.tsx:40
msgid "This sign-in link is incomplete."
msgstr "Ce lien de connexion est incomplet."
@@ -1870,14 +1866,14 @@ msgstr "Ce lien de désabonnement est absent ou mal formé."
msgid "This week"
msgstr "Cette semaine"
#: src/pages/DumpEdit.tsx:449
#: src/pages/DumpEdit.tsx:450
msgid "Thumbnail"
msgstr "Miniature"
#: src/components/dump/create/TitleField.tsx:45
#: src/components/playlist/PlaylistCreateForm.tsx:100
#: src/components/playlist/PlaylistEditForm.tsx:175
#: src/pages/DumpEdit.tsx:510
#: src/pages/DumpEdit.tsx:511
msgid "Title"
msgstr "Titre"
@@ -2007,7 +2003,7 @@ msgstr "Validé"
msgid "Upvoted ({0})"
msgstr "Validé ({0})"
#: src/pages/DumpEdit.tsx:481
#: src/pages/DumpEdit.tsx:482
msgid "URL"
msgstr "URL"
@@ -2023,7 +2019,7 @@ msgstr "Identifiant d’URL"
msgid "User"
msgstr "Utilisateur"
#: src/components/profile/ProfileManageTab.tsx:54
#: src/components/profile/ProfileManageTab.tsx:56
msgid "User management"
msgstr "Gestion utilisateur"
@@ -2031,10 +2027,10 @@ msgstr "Gestion utilisateur"
msgid "User menu"
msgstr "Menu utilisateur"
#: src/components/profile/ProfileManageTab.tsx:61
#: src/components/profile/ProfileManageTab.tsx:63
#: src/components/profile/UsernameField.tsx:51
#: src/pages/Settings.tsx:91
#: src/pages/UserLogin.tsx:81
#: src/pages/UserLogin.tsx:74
#: src/pages/UserRegister.tsx:141
msgid "Username"
msgstr "Nom d'utilisateur"
@@ -2079,7 +2075,7 @@ msgid "email.welcome.subject"
msgstr "Bienvenue sur {site}"
#: src/components/dump/create/DumpCreateModal.tsx:705
#: src/pages/DumpEdit.tsx:538
#: src/pages/DumpEdit.tsx:539
msgid "What makes it worth it?"
msgstr "Pourquoi on en voudrait ?"
@@ -2097,7 +2093,7 @@ msgid "Why & where"
msgstr "Pourquoi & où"
#: src/components/dump/create/DumpCreateModal.tsx:704
#: src/pages/DumpEdit.tsx:537
#: src/pages/DumpEdit.tsx:538
msgid "Why?"
msgstr "Pourquoi ?"
@@ -2171,7 +2167,7 @@ msgstr "Votre compte est sur {home} : connectez-vous aux autres instances depuis
msgid "Your account lives on {home}: your password, email address, avatar and username change there."
msgstr "Votre compte est sur {home} : votre mot de passe, votre adresse e-mail, votre avatar et votre nom d'utilisateur se changent là-bas."
#: src/pages/UserLogin.tsx:160
#: src/pages/ResetPassword.tsx:89
msgid "Your email address"
msgstr "Votre adresse e-mail"
@@ -2179,7 +2175,7 @@ msgstr "Votre adresse e-mail"
msgid "Your gerbeur's URL"
msgstr "URL de votre gerbeur"
#: src/pages/ResetPassword.tsx:89
#: src/pages/ResetPassword.tsx:141
msgid "Your password has been changed. You can now log in."
msgstr "Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter."

View File

@@ -3316,34 +3316,6 @@ body.has-player .chat-fab {
margin: 0;
}
.auth-link-btn {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--color-accent);
font-size: inherit;
font-family: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
.auth-link-btn:hover {
opacity: 0.8;
}
.auth-reset-panel {
border-top: 1px solid var(--color-border-subtle);
padding-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.auth-reset-form {
margin: 0;
}
.auth-reset-sent {
font-size: var(--text-md);
text-align: center;

View File

@@ -14,9 +14,9 @@
* - `/authorize` lets another gerbeur instance sign the user in with their
* account here (`Authorize`), and `/federation/callback` finishes a sign-in
* here with an account on another (`FederationCallback`).
* - `/reset-password` sets a new password from an emailed link
* (`ResetPassword`), and `/unsubscribe` turns off a digest from its emailed
* link (`Unsubscribe`).
* - `/reset-password` asks for a reset link by email, and sets a new password
* from it, `?token=` (`ResetPassword`), and `/unsubscribe` turns off a digest
* from its emailed link (`Unsubscribe`).
* - `/notifications` (`Notifications`) and `/settings` (`Settings`), signed in
* only, and `/manage`, the site's tools, for `category:manage` or
* `federation:manage` (`Manage`).

View File

@@ -1,6 +1,7 @@
/**
* Renders the page a password reset email links to, where the user sets a new
* password.
* Renders the reset of a forgotten password, both its steps on one page: the
* request for a link by email, which "Forgot password?" on the login page
* leads to, and the new password, which the link leads to.
*
* @module
*/
@@ -26,15 +27,88 @@ interface Values {
confirm: string;
}
interface RequestValues {
email: string;
}
/**
* Renders `/reset-password?token=`, for anyone holding a reset link. It doesn't
* sign the user in: a success links to `/login`.
* Renders `/reset-password`, for anyone: the request for a reset link, or,
* with the link's `?token=`, the new password. It doesn't sign the user in: a
* success links to `/login`.
*/
export function ResetPassword(): React.JSX.Element {
useDocumentTitle(t`Reset password`);
const [params] = useSearchParams();
const navigate = useNavigate();
const token = params.get("token") ?? "";
return token ? <NewPassword token={token} /> : <ResetRequest />;
}
/**
* Renders the form that asks the server to email a reset link, which leads
* back here with its token.
*/
function ResetRequest() {
const [sent, setSent] = useState(false);
const form = useApiForm<RequestValues>({ defaultValues: { email: "" } });
const onSubmit = form.submit(async ({ email }) => {
// The server answers 200 whether or not the address is known, so as not
// to reveal which ones are. An error here means it failed.
await expectOk(
await fetch(`${API_URL}/api/password-resets`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim() }),
}),
);
setSent(true);
});
return (
<PageShell centered>
<div className="auth-card">
<h1 className="auth-card-title">
<Trans>Reset password</Trans>
</h1>
{sent
? (
<p className="auth-reset-sent">
<Trans>
If that address is registered you'll receive a reset link
shortly.
</Trans>
</p>
)
: (
<FormProvider {...form}>
<form onSubmit={onSubmit} className="form">
<FormError title={t`Request failed`} />
<TextField<RequestValues>
name="email"
type="email"
placeholder={t`Your email address`}
autoFocus
rules={{ required: true }}
/>
<SubmitButton pendingLabel={<Trans>Sending…</Trans>}>
<Trans>Send reset link</Trans>
</SubmitButton>
</form>
</FormProvider>
)}
<p className="auth-card-footer">
<Link to="/login">
<Trans>Back to login</Trans>
</Link>
</p>
</div>
</PageShell>
);
}
/** Renders the form that sets a new password with the link's `token`. */
function NewPassword({ token }: { token: string }) {
const navigate = useNavigate();
const [done, setDone] = useState(false);
const form = useApiForm<Values>({
@@ -56,28 +130,6 @@ export function ResetPassword(): React.JSX.Element {
setDone(true);
});
if (!token) {
return (
<PageShell centered>
<div className="auth-card">
<h1 className="auth-card-title">
<Trans>Invalid link</Trans>
</h1>
<p>
<Trans>This reset link is missing or malformed.</Trans>
</p>
<Link
to="/login"
className="btn-primary"
style={{ marginTop: "1rem", display: "inline-block" }}
>
<Trans>Back to login</Trans>
</Link>
</div>
</PageShell>
);
}
if (done) {
return (
<PageShell centered>

View File

@@ -1,11 +1,10 @@
/**
* Renders the sign-in page, which also requests password reset links.
* Renders the sign-in page.
*
* @module
*/
import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { Link, useNavigate, useSearchParams } from "react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
@@ -15,7 +14,6 @@ import type { AuthResponse, LoginUserRequest } from "../../shared/user.ts";
import type { Wire } from "../../shared/api.ts";
import { useAuth } from "../hooks/auth/useAuth.ts";
import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts";
import { useHeightTransition } from "../hooks/shared/useHeightTransition.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { expectOk } from "../utils/shared/api.ts";
import { FormError } from "../components/form/FormError.tsx";
@@ -32,13 +30,10 @@ interface LoginValues {
password: string;
}
interface ResetValues {
email: string;
}
/**
* Renders `/login`, for the signed out. A sign-in lands on the page `?next=`
* names, or `/`, and "Forgot password?" unfolds the reset request form below.
* names, or `/`, and "Forgot password?" leads to `/reset-password`, which asks
* for a reset link.
* Users of another instance sign in through {@link FederatedSignIn}, unless
* the instance takes none.
*/
@@ -47,8 +42,6 @@ export function UserLogin(): React.JSX.Element {
const navigate = useNavigate();
const [params] = useSearchParams();
const { login } = useAuth();
const [showReset, setShowReset] = useState(false);
const cardRef = useHeightTransition<HTMLDivElement>(showReset);
const form = useApiForm<LoginValues>({
defaultValues: { username: "", password: "" },
@@ -68,7 +61,7 @@ export function UserLogin(): React.JSX.Element {
return (
<PageShell centered>
<div className="auth-card" ref={cardRef}>
<div className="auth-card">
<h1 className="auth-card-title">
<Trans>Log in</Trans>
</h1>
@@ -95,17 +88,11 @@ export function UserLogin(): React.JSX.Element {
</FormProvider>
<p className="auth-card-footer">
<button
type="button"
className="auth-link-btn"
onClick={() => setShowReset((v) => !v)}
>
<Link to="/reset-password">
<Trans>Forgot password?</Trans>
</button>
</Link>
</p>
{showReset && <ResetRequestPanel />}
{FEDERATION_INCOMING && <FederatedSignIn />}
<p className="auth-card-footer">
@@ -115,57 +102,3 @@ export function UserLogin(): React.JSX.Element {
</PageShell>
);
}
/**
* Renders the form that asks the server to email a password reset link, which
* leads to `/reset-password`.
*/
function ResetRequestPanel() {
const [sent, setSent] = useState(false);
const form = useApiForm<ResetValues>({ defaultValues: { email: "" } });
const onSubmit = form.submit(async ({ email }) => {
// The server answers 200 whether or not the address is known, so as not
// to reveal which ones are. An error here means it failed.
await expectOk(
await fetch(`${API_URL}/api/password-resets`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim() }),
}),
);
setSent(true);
});
if (sent) {
return (
<div className="auth-reset-panel animate-in">
<p className="auth-reset-sent">
<Trans>
If that address is registered you'll receive a reset link shortly.
</Trans>
</p>
</div>
);
}
return (
<div className="auth-reset-panel animate-in">
<FormProvider {...form}>
<form onSubmit={onSubmit} className="form auth-reset-form">
<FormError title={t`Request failed`} />
<TextField<ResetValues>
name="email"
type="email"
placeholder={t`Your email address`}
autoFocus
rules={{ required: true }}
/>
<SubmitButton pendingLabel={<Trans>Sending…</Trans>}>
<Trans>Send reset link</Trans>
</SubmitButton>
</form>
</FormProvider>
</div>
);
}

View File

@@ -17,16 +17,37 @@ describe("ResetPassword", () => {
return user;
}
test("no token: an invalid link", () => {
setup("");
expect(screen.getByRole("heading", { name: "Invalid link" }))
.toBeInTheDocument();
test("without a token, it asks for a link by email", async () => {
route("POST /api/password-resets", () => ({}));
const user = setup("");
await user.type(
screen.getByPlaceholderText("Your email address"),
" a@b.c {Enter}",
);
await screen.findByText(/you'll receive a reset link/);
expect(calls("POST /api/password-resets")[0].body).toEqual({
email: "a@b.c",
});
expect(screen.getByRole("link", { name: "Back to login" })).toHaveAttribute(
"href",
"/login",
);
});
test("a failed request is reported, not claimed as sent", async () => {
route("POST /api/password-resets", () => fail("Internal error", 500));
const user = setup("");
await user.type(
screen.getByPlaceholderText("Your email address"),
"a@b.c{Enter}",
);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Request failed",
);
expect(screen.queryByText(/you'll receive a reset link/)).not
.toBeInTheDocument();
});
test("the two passwords must match", async () => {
const user = setup();
await user.type(screen.getByPlaceholderText("New password"), "longenough");

View File

@@ -97,36 +97,10 @@ describe("UserLogin", () => {
await screen.findByText("home page");
});
test("password reset request", async () => {
route("POST /api/password-resets", () => ({}));
const user = setup();
await user.click(screen.getByRole("button", { name: "Forgot password?" }));
await user.type(
screen.getByPlaceholderText("Your email address"),
" a@b.c {Enter}",
);
await screen.findByText(/you'll receive a reset link/);
expect(calls("POST /api/password-resets")[0].body).toEqual({
email: "a@b.c",
});
});
test("a failed reset request is reported, not claimed as sent", async () => {
route(
"POST /api/password-resets",
() => fail("Internal error", 500),
);
const user = setup();
await user.click(screen.getByRole("button", { name: "Forgot password?" }));
await user.type(
screen.getByPlaceholderText("Your email address"),
"a@b.c{Enter}",
);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Request failed",
);
expect(screen.queryByText(/you'll receive a reset link/)).not
.toBeInTheDocument();
test("Forgot password? leads to the reset page", () => {
setup();
expect(screen.getByRole("link", { name: "Forgot password?" }))
.toHaveAttribute("href", "/reset-password");
});
test("a sign-in follows ?next= on this site, and only there", async () => {

View File

@@ -107,7 +107,6 @@
[data-style="brutalist"] .rich-content-thumbnail-btn,
[data-style="brutalist"] .user-menu-trigger,
[data-style="brutalist"] .user-menu-item,
[data-style="brutalist"] .auth-link-btn,
[data-style="brutalist"] .form-cancel,
[data-style="brutalist"] .form-label-action {
border: none !important;
@@ -118,8 +117,6 @@
[data-style="brutalist"] .user-menu-trigger:active,
[data-style="brutalist"] .user-menu-item:hover:not(:disabled),
[data-style="brutalist"] .user-menu-item:active,
[data-style="brutalist"] .auth-link-btn:hover:not(:disabled),
[data-style="brutalist"] .auth-link-btn:active,
[data-style="brutalist"] .form-cancel:hover:not(:disabled),
[data-style="brutalist"] .form-cancel:active,
[data-style="brutalist"] .form-label-action:hover:not(:disabled),

View File

@@ -218,7 +218,6 @@
[data-style="geocities"] .rich-content-thumbnail-btn,
[data-style="geocities"] .user-menu-trigger,
[data-style="geocities"] .user-menu-item,
[data-style="geocities"] .auth-link-btn,
[data-style="geocities"] .form-cancel,
[data-style="geocities"] .form-label-action,
[data-style="geocities"] .category-editor-btn {