Compare commits

...

134 commits

Author SHA1 Message Date
SleeplessOne1917
26979b91c2
Merge pull request #1797 from jsit/fix/markdown-toolbar-button-focus
fix: Add focus border to markdown toolbar buttons
2023-07-04 05:30:10 +00:00
Jay Sitter
21c8b64cda fix: Add focus border to markdown toolbar buttons 2023-07-04 00:53:52 -04:00
Jay Sitter
b1292b958a
fix: Add data-bs-theme attribute for user dark/light modes (#1782)
* fix: Add data-bs-theme attribute for user dark/light modes

* fix: Remove unnecessary optional chain

* fix: Oops -- add missing files
2023-07-03 20:02:24 -04:00
SleeplessOne1917
8730f157da
Merge pull request #1771 from jsit/fix/fix-long-words-in-titles-overflow
fix: Break text on post titles so long words don't overflow
2023-07-03 21:30:32 +00:00
Jay Sitter
976812a446 Merge remote-tracking branch 'lemmy/main' into fix/fix-long-words-in-titles-overflow
* lemmy/main:
  v0.18.1-rc.9
  fix: Fix comment collapse and vote buttons not having focus style (#1789)
  Add missing modlog reasons (#1787)
  Fix search page breaking on initial load when logged in (#1781)
  feat: Add PR template (#1785)
  v0.18.1-rc.8
  Fix profile loading spinner
  fix: Move getRoleLabelPill to the only component that uses it
  fix: Remove unused hasBadges() function
  fix: Fix badge alignment and break out into component
  fix: Fix up filter row gaps and margins a little
  fix: Fix heading levels
  fix: Simplify row classes a bit
  fix: Fix some gaps in search filters
  fix: Fix row gap on search options
  fix: Add bottom margin to inbox controls
  fix: Small cleanup to search/inbox controls
2023-07-03 17:13:33 -04:00
Dessalines
b9d9231520 v0.18.1-rc.9 2023-07-03 16:55:54 -04:00
Jay Sitter
5a95a058ae
fix: Fix comment collapse and vote buttons not having focus style (#1789) 2023-07-03 16:53:10 -04:00
SleeplessOne1917
e829b13053
Add missing modlog reasons (#1787) 2023-07-03 16:52:33 -04:00
SleeplessOne1917
a0cf54c0a0
Fix search page breaking on initial load when logged in (#1781)
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-07-03 16:43:52 -04:00
Jay Sitter
ca05902423
feat: Add PR template (#1785) 2023-07-03 14:18:22 -04:00
SleeplessOne1917
8b478e9712 v0.18.1-rc.8 2023-07-03 10:22:12 -04:00
SleeplessOne1917
43869824bf
Merge pull request #1780 from m-gail/fix-profile-loading-spinner
Fix profile loading spinner
2023-07-03 14:15:22 +00:00
SleeplessOne1917
f89a788a9f
Merge branch 'main' into fix-profile-loading-spinner 2023-07-03 14:12:14 +00:00
SleeplessOne1917
a19358333a
Merge pull request #1770 from jsit/fix/fix-badges-spacing-componentize
fix: Fix badge alignment and break out into component
2023-07-03 12:35:35 +00:00
SleeplessOne1917
34f04e6758
Merge branch 'main' into fix/fix-badges-spacing-componentize 2023-07-03 12:29:55 +00:00
m-gail
fa4fe57a86
Fix profile loading spinner 2023-07-03 12:56:22 +02:00
SleeplessOne1917
93d2d56cce
Merge pull request #1762 from jsit/fix/h1-page-titles
fix: Fix heading levels
2023-07-03 08:07:17 +00:00
Jay Sitter
8a163fedde fix: Break text on post titles so long words don't overflow 2023-07-02 20:20:08 -04:00
Jay Sitter
1e69129b10
Merge branch 'main' into fix/h1-page-titles 2023-07-02 20:08:55 -04:00
SleeplessOne1917
90a9b6bda1
Merge pull request #1760 from jsit/fix/use-row-in-filters
fix: Small cleanup to search/inbox controls
2023-07-03 00:02:16 +00:00
SleeplessOne1917
7389b1f9c8
Merge branch 'main' into fix/use-row-in-filters 2023-07-02 23:56:46 +00:00
Jay Sitter
7b34022e0d
Merge branch 'main' into fix/h1-page-titles 2023-07-02 19:54:03 -04:00
Jay Sitter
4e1e29b9a5
Merge branch 'main' into fix/fix-badges-spacing-componentize 2023-07-02 19:47:41 -04:00
Jay Sitter
6cb49967c2 fix: Move getRoleLabelPill to the only component that uses it 2023-07-02 19:47:12 -04:00
SleeplessOne1917
8e20d16f71
Merge pull request #1757 from jsit/fix/expand-button-when-no-link
fix: Post expand button was not showing if body-only post
2023-07-02 23:23:08 +00:00
Alec Armbruster
c0193fa2de
Merge branch 'main' into fix/expand-button-when-no-link 2023-07-02 19:20:32 -04:00
Jay Sitter
61867ee73d fix: Remove unused hasBadges() function 2023-07-02 19:01:14 -04:00
Jay Sitter
50b1a395f1
Merge branch 'main' into fix/fix-badges-spacing-componentize 2023-07-02 18:59:15 -04:00
Jay Sitter
9869b911cf fix: Fix badge alignment and break out into component 2023-07-02 18:48:35 -04:00
SleeplessOne1917
ce1f979c36
Merge pull request #1766 from LemmyNet/auth-error-message
Prevent JWT token from showing up on error page
2023-07-02 22:37:37 +00:00
SleeplessOne1917
74727cacd4
Merge branch 'main' into auth-error-message 2023-07-02 22:35:17 +00:00
SleeplessOne1917
c6db20cf7f
Merge pull request #1577 from jsit/feat/create-post-file-upload-a11y
feat(a11y): Change behavior of some file upload fields
2023-07-02 22:27:42 +00:00
SleeplessOne1917
e7f7bc2e36
Merge branch 'main' into feat/create-post-file-upload-a11y 2023-07-02 22:22:15 +00:00
SleeplessOne1917
9685d6782b Merge branch 'auth-error-message' of https://github.com/LemmyNet/lemmy-ui into auth-error-message 2023-07-02 18:19:04 -04:00
SleeplessOne1917
3fc4d85921 Update yarn lock 2023-07-02 18:18:33 -04:00
SleeplessOne1917
73198ac03a
Merge branch 'main' into auth-error-message 2023-07-02 22:14:46 +00:00
SleeplessOne1917
02adbea61f Move export to barrel file 2023-07-02 18:14:13 -04:00
Jay Sitter
083d5aa751
Merge pull request #1764 from jsit/fix/vote-button-no-spinners-1761
fix: Fix vote buttons not showing spinners while registering vote #1761
2023-07-02 17:39:33 -04:00
Jay Sitter
23ee563dfb
Merge branch 'main' into fix/vote-button-no-spinners-1761 2023-07-02 17:36:52 -04:00
Jay Sitter
d620dea0b3
Merge branch 'main' into feat/create-post-file-upload-a11y 2023-07-02 17:33:01 -04:00
Jay Sitter
0047c17eb3 fix: Fix avatar image overlay aspect ratio 2023-07-02 17:24:06 -04:00
Jay Sitter
61276ec4e5 fix: Fix circle image aspect ratio 2023-07-02 17:14:00 -04:00
Jay Sitter
3c6505b9fd fix: Add some spacing between upload field and image; fix circle image aspect ratio 2023-07-02 16:08:55 -04:00
SleeplessOne1917
31789495a2 Prevent JWT token from showing up on error page 2023-07-02 15:45:45 -04:00
SleeplessOne1917
3e7aca043b
Merge pull request #1763 from jsit/fix/fix-joined-check-color
fix: Fix joined button check color
2023-07-02 19:28:40 +00:00
SleeplessOne1917
b3d096d41d
Merge branch 'main' into fix/fix-joined-check-color 2023-07-02 19:25:36 +00:00
Jay Sitter
0bd0a49730
Merge branch 'main' into feat/create-post-file-upload-a11y 2023-07-02 15:24:18 -04:00
Jay Sitter
efccabe1ad
Merge pull request #1732 from sunaurus/undefined_quote
Fix markdown editor quote bugs
2023-07-02 15:14:38 -04:00
Jay Sitter
c2990fd644
Merge branch 'main' into undefined_quote 2023-07-02 15:11:48 -04:00
Jay Sitter
65fcaafab7 fix: Fix vote buttons not showing spinners while registering vote #1761 2023-07-02 15:07:37 -04:00
Jay Sitter
548e5560a1 fix: Fix joined button check color 2023-07-02 14:47:45 -04:00
Jay Sitter
b8c2e1e2ca fix: Fix up filter row gaps and margins a little 2023-07-02 14:42:30 -04:00
Jay Sitter
a48c20a772 fix: Fix heading levels 2023-07-02 14:29:56 -04:00
Jay Sitter
93d0bc44ea fix: Simplify row classes a bit 2023-07-02 13:43:28 -04:00
Jay Sitter
302c6b496d fix: Fix some gaps in search filters 2023-07-02 13:36:29 -04:00
Jay Sitter
2c56c2e59d fix: Fix row gap on search options 2023-07-02 13:26:07 -04:00
Jay Sitter
350b1b28ee fix: Add bottom margin to inbox controls 2023-07-02 13:22:03 -04:00
Jay Sitter
d258edfbaf fix: Small cleanup to search/inbox controls 2023-07-02 13:18:16 -04:00
Jay Sitter
1700476528
Merge pull request #1730 from sunaurus/dm_fix
Fix DM replies not working
2023-07-02 11:49:38 -04:00
SleeplessOne1917
1e2f333bb0
Merge branch 'main' into fix/expand-button-when-no-link 2023-07-02 15:48:24 +00:00
Jay Sitter
28dc62d44c
Merge branch 'main' into dm_fix 2023-07-02 11:46:51 -04:00
SleeplessOne1917
b6513cc56b
Merge pull request #1756 from jsit/fix/list-view-vote-button-width
fix: Fix vote buttons in list view variable width
2023-07-02 15:28:44 +00:00
Jay Sitter
868601fa58 fix: Update comment to reflect new logic 2023-07-02 11:26:44 -04:00
Jay Sitter
195e258a51 fix: Post expand button was not showing if body-only post 2023-07-02 11:23:42 -04:00
SleeplessOne1917
53a5bdbb5c
Merge branch 'main' into fix/list-view-vote-button-width 2023-07-02 15:23:23 +00:00
Jay Sitter
a72cc6807e fix: Fix vote buttons in list view variable width 2023-07-02 11:20:02 -04:00
SleeplessOne1917
3a4454499d
Merge pull request #1755 from jsit/fix/thumb-button-transparent-1754
fix: Fix thumb buttons having gray background #1754
2023-07-02 15:06:03 +00:00
Jay Sitter
34f2b73063 fix: Fix thumb buttons having gray background #1754 2023-07-02 11:02:53 -04:00
SleeplessOne1917
6a62d48257
Merge pull request #1750 from paradox460/main
Add support for Command (⌘) key shortcuts on Markdown text areas
2023-07-02 11:03:10 +00:00
Jeff Sandberg
17bcfe5257
Add metaKey to markdown-textarea, for macos
On MacOS, Ctrl is less commonly used than Command (⌘).
Javascript expresses command as `metaKey`.

This allows for _either_ Ctrl or Command to be used in the Markdown text
area.

Note that on Windows, on some browsers, the "windows" key is labeled as meta, so it would
work on windows as well.

http://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey
2023-07-02 04:11:09 -06:00
SleeplessOne1917
f87dad3a5c
Merge pull request #1741 from sunaurus/cookie_path
Fix jwt cookie path
2023-07-01 23:33:56 +00:00
SleeplessOne1917
2c335483dd
Merge branch 'main' into undefined_quote 2023-07-01 23:32:59 +00:00
sunaurus
c68d2b8ee6
Fix jwt cookie path 2023-07-02 02:04:44 +03:00
SleeplessOne1917
d00a4fa546
Merge pull request #1729 from sunaurus/locale_fallback
Add fallback date-fns locale import
2023-07-01 22:39:22 +00:00
sunaurus
47cbe3e002
Fix quotedText bugs in markdown editor 2023-07-01 19:57:06 +03:00
sunaurus
653094eddb
Fix DM replies 2023-07-01 19:26:30 +03:00
sunaurus
515f5f5af3
Add fallback date-fns locale import 2023-07-01 18:57:19 +03:00
SleeplessOne1917
61255bf01a v0.18.1-rc.7 2023-06-30 16:26:09 -04:00
SleeplessOne1917
e658e36420 v0.18.1-rc.6 2023-06-30 16:26:00 -04:00
SleeplessOne1917
93fe23a305
Merge pull request #1721 from sunaurus/date_fns_lang
Fix date-fns locale import failing for some locales
2023-06-30 15:11:37 -04:00
sunaurus
ed1f208e47
Fix date-fns locale import failing for some locales 2023-06-30 19:59:44 +03:00
SleeplessOne1917
cf9f37fbc4
Merge pull request #1719 from LemmyNet/fix-cookie-in-catch-all
Fix nonexistent property `req.cookies`
2023-06-30 10:40:41 -04:00
Alec Armbruster
af22947c0f
fix req.cookie nonexistent -.- 2023-06-30 10:35:57 -04:00
SleeplessOne1917
c8e3256c88
Merge pull request #1718 from LemmyNet/fix-cache-auth
Fix broken `user.auth()` method on `middleware.ts`
2023-06-30 10:17:14 -04:00
Alec Armbruster
c8ed02cead
Merge branch 'fix-cache-auth' of https://github.com/LemmyNet/lemmy-ui into fix-cache-auth 2023-06-30 10:04:27 -04:00
Alec Armbruster
0bcb2d77be
wip 2023-06-30 10:04:19 -04:00
Alec Armbruster
7743fa98b9
wip 2023-06-30 10:04:01 -04:00
SleeplessOne1917
213dadb654
Merge branch 'main' into fix-cache-auth 2023-06-30 09:52:31 -04:00
Alec Armbruster
c804cf958a
Merge branch 'main' of https://github.com/LemmyNet/lemmy-ui into fix-cache-auth 2023-06-30 09:52:15 -04:00
Diamond
a7592d74bb
Enforce SameSite=Strict (#1713) 2023-06-30 09:51:41 -04:00
Alec Armbruster
1b7a9dcb8b
fix service worker path 2023-06-30 09:50:19 -04:00
Alec Armbruster
da45ffb46b
fix cache auth method 2023-06-30 09:42:09 -04:00
SleeplessOne1917
b6415f828e
Merge pull request #1711 from LemmyNet/cache-dev
Fix some issues
2023-06-29 19:26:40 -04:00
SleeplessOne1917
cc184a86c8 Fix authorized route false flag 2023-06-29 18:12:22 -04:00
SleeplessOne1917
2d88e42cab Fix dev caching issue 2023-06-29 16:33:08 -04:00
Dessalines
9e7fec772d v0.18.1-rc.5 2023-06-29 16:20:38 -04:00
SleeplessOne1917
df39e0fe5d
Merge pull request #1708 from LemmyNet/cache-control
Cache static data for a day
2023-06-29 14:19:07 -04:00
SleeplessOne1917
fa41117320
Merge branch 'main' into cache-control 2023-06-29 13:35:44 -04:00
Alec Armbruster
d8ee0ec78a
change max-age to 5 for non-authed responses 2023-06-29 13:33:30 -04:00
Alec Armbruster
fead020bdc
Fix PostListing mobile margin layout issue (#1706) 2023-06-29 17:28:55 +00:00
SleeplessOne1917
339cefa2b0 Cache static data for a day 2023-06-29 13:14:48 -04:00
Dessalines
08370d4c4e Try increasing node memory. 2023-06-29 11:12:12 -04:00
Dessalines
b73cb808e4 v0.18.1-rc.4 2023-06-29 10:40:19 -04:00
Alec Armbruster
80d9aac1ca
Fix taglines on Home (#1701)
* fix taglines on home

* fix error on admin panel
2023-06-29 10:38:35 -04:00
SleeplessOne1917
751495702c
Use git hash to break cache (#1684)
* Use git hash to break cache

* Address PR feedback

* Make hash docker agnostic

* Add trailing slash

* Update .prettierignore

Co-authored-by: Alec Armbruster <35377827+alectrocute@users.noreply.github.com>

* Remove debugging log

* implement getStaticDir util

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
Co-authored-by: Alec Armbruster <35377827+alectrocute@users.noreply.github.com>
2023-06-29 10:29:33 -04:00
SleeplessOne1917
ad6db69dda
Merge pull request #1698 from LemmyNet/fix_datetime_2
Removing unecessary timezone adjusting
2023-06-29 09:42:57 -04:00
SleeplessOne1917
6bf159261a
Merge branch 'main' into fix_datetime_2 2023-06-29 09:36:24 -04:00
Alec Armbruster
73be96880a
fix issue with thumbnails (#1695) 2023-06-29 09:34:47 -04:00
Alec Armbruster
dc439b8dee
Merge branch 'main' into fix_datetime_2 2023-06-29 09:32:03 -04:00
Dessalines
eee1f443a8 Removing unecessary timezone adjusting 2023-06-29 08:59:54 -04:00
Jay Sitter
044441d6c9
Merge branch 'main' into feat/create-post-file-upload-a11y 2023-06-26 18:43:26 -04:00
Jay Sitter
b2b6f4521f fix: Use Bootstrap file upload form control styles 2023-06-26 18:42:44 -04:00
Pascal de Vink
8e2f83eb4e Fix feedback on banning an unbanning
When banning or unbanning, the API call was done, but updating the
frontend failed. This caused a confusing experience for an admin, until
the page was reloaded.
2023-06-26 18:30:31 -04:00
Alec Armbruster
28661dfacf fix vote button alignment 2023-06-26 18:30:31 -04:00
Alec Armbruster
d816b8f863 remove icon (#1618)
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-06-26 18:30:31 -04:00
dullbananas
dd09b3cade Indicate valid and invalid fields in signup form (#1450)
* Use was-validated class in signup form

* Update signup.tsx

* Update signup.tsx

---------

Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>
2023-06-26 18:30:31 -04:00
Alec Armbruster
022c27aec2 capitalize button (#1616)
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-06-26 18:30:31 -04:00
Alec Armbruster
950dfad659 Move password reset form to separate route, view (#1390)
* rework password reset form

* make self-suggested changes

* cleaning

* validate in handlePasswordReset as well

* update submodule

* partially make suggested changes

* make suggested changes

* resolve merge conflicts

* resolve merge conflicts

* resolve merge conflicts

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-06-26 18:30:31 -04:00
Jay Sitter
f5be909d64 feat(UI): Reduce base font size (#1591)
* feat: Reduce base font size

* chore: Build themes

---------

Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-06-26 18:30:31 -04:00
Anansi
cf10bd64ad Fix: missing semantic css classes and html elements (#1583)
* Fix: missing semantic css classes and html elements.

Now all pages have a main and aside element when a sidebar is present to facilitate custom theming. This does not impact the default behavior of the front.

* Fix: re-added communityref on main element

---------

Co-authored-by: 0xAnansi <0xAnansi@omageni.com>
Co-authored-by: Jay Sitter <jsit@users.noreply.github.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2023-06-26 18:30:31 -04:00
Jay Sitter
91122f4061 chore(DX): Add prettier to eslint config for use with editors 2023-06-26 18:30:31 -04:00
SleeplessOne1917
308785b472 Only give child comments colored borders 2023-06-26 18:30:31 -04:00
Dominic Mazzoni
9dc25f2247 Associate NSFW label with its checkbox 2023-06-26 18:30:31 -04:00
Jay Sitter
8fd08e58e3 fix: Remove unnecessary string interpolations 2023-06-26 18:30:31 -04:00
Jay Sitter
04770d7ef8 fix: Remove unnecessary class 2023-06-26 18:30:31 -04:00
Jay Sitter
32cc2538d4 fix: Remove unnecessary classes 2023-06-26 18:30:30 -04:00
Jay Sitter
984de714cd fix: Restore removed classes 2023-06-26 18:30:30 -04:00
Jay Sitter
bbe897dc01 fix: Remove wrapping li's 2023-06-26 18:30:30 -04:00
Jay Sitter
1db654ecf5 fix: Remove extraneous classes 2023-06-26 18:30:30 -04:00
Jay Sitter
00d7a8dbb7 fix: Move things back to where they were 2023-06-26 18:30:30 -04:00
Jay Sitter
f27bb07fcd chore: Separate post mod buttons into functions 2023-06-26 18:30:30 -04:00
Jay Sitter
78e1ab994e
Merge branch 'main' into feat/create-post-file-upload-a11y 2023-06-25 18:20:04 -04:00
Jay Sitter
7d44bc4993 Merge remote-tracking branch 'lemmy/main' into feat/create-post-file-upload-a11y
* lemmy/main:
  fix: Add type=button to buttons
  chore: Empty commit to re-trigger Woodpecker
  fix: Fix some Bootstrap 5 font classes
  fix: Fix some Bootstrap 5 font classes
  fix: Specify vote content type so buttons work for both comments and posts
  v0.18.0
  Fix homepage `scrollTo(0, 0)` failing when document size changes.
  v0.18.0-rc.8
  Moved `!isBrowser()` check to `FirstLoadServer.isFirstLoad`
  Fix server-side rendering after first load.
  fix!: Try to get Vote Buttons component working in Comments
  fix: Remove unused prop
  fix: Rework some vote buttons architecture
  fix: Undo some other extraneous changes
  fix: Undo some extraneous changes
  fix: Remove tippy duplicate functions
  fix: Revert to old mobile vote style
  feat: Move vote buttons to separate component
2023-06-25 02:27:37 -04:00
Jay Sitter
dc43c51b0d fix(a11y): Change the look and behavior of some file upload fields 2023-06-25 02:27:07 -04:00
68 changed files with 1932 additions and 1916 deletions

12
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,12 @@
## Description
<!-- Please describe exactly what this PR changes, including URLs and issue
numbers. If it fixes an issue, add "Fixes #XXXX" -->
## Screenshots
<!-- Please include before and after screenshots if applicable -->
### Before
### After

View file

@ -2,3 +2,4 @@ src/shared/translations
lemmy-translations
src/assets/css/themes/*.css
stats.json
dist

View file

@ -27,7 +27,7 @@ COPY .git .git
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
RUN yarn --production --prefer-offline
RUN yarn build:prod
RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod
# Prune the image
RUN node-prune /usr/src/app/node_modules

View file

@ -20,6 +20,7 @@ COPY generate_translations.js \
COPY lemmy-translations lemmy-translations
COPY src src
COPY .git .git
# Set UI version
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"

View file

@ -1,6 +1,6 @@
{
"name": "lemmy-ui",
"version": "0.18.1-rc.3",
"version": "0.18.1-rc.9",
"description": "An isomorphic UI for lemmy",
"repository": "https://github.com/LemmyNet/lemmy-ui",
"license": "AGPL-3.0",
@ -8,9 +8,9 @@
"scripts": {
"analyze": "webpack --mode=none",
"prebuild:dev": "yarn clean && node generate_translations.js",
"build:dev": "webpack --mode=development",
"build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development",
"prebuild:prod": "yarn clean && node generate_translations.js",
"build:prod": "webpack --mode=production",
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
"clean": "yarn run rimraf dist",
"dev": "yarn build:dev --watch",
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
@ -48,11 +48,11 @@
"check-password-strength": "^2.0.7",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
"cookie": "^0.5.0",
"copy-webpack-plugin": "^11.0.0",
"cross-fetch": "^3.1.5",
"css-loader": "^6.7.3",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"emoji-mart": "^5.4.0",
"emoji-short-name": "^2.0.0",
"express": "~4.18.2",
@ -66,7 +66,6 @@
"inferno-i18next-dess": "0.0.2",
"inferno-router": "^8.1.1",
"inferno-server": "^8.1.1",
"isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2",
"lemmy-js-client": "0.18.0-rc.2",
"lodash.isequal": "^4.5.0",
@ -98,6 +97,7 @@
"@babel/core": "^7.21.8",
"@types/autosize": "^4.0.0",
"@types/bootstrap": "^5.2.6",
"@types/cookie": "^0.5.1",
"@types/express": "^4.17.17",
"@types/html-to-text": "^9.0.0",
"@types/lodash.isequal": "^4.5.6",
@ -126,6 +126,7 @@
"style-loader": "^3.3.2",
"terser": "^5.17.3",
"typescript": "^5.0.4",
"typescript-language-server": "^3.3.2",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-dev-server": "4.15.0"
},

View file

@ -81,6 +81,7 @@
}
.vote-bar {
min-width: 5ch;
margin-top: -6.5px;
}
@ -253,10 +254,6 @@ hr {
-ms-filter: blur(10px);
}
.img-cover {
object-fit: cover;
}
.img-expanded {
max-height: 90vh;
}
@ -349,10 +346,12 @@ br.big {
}
.avatar-overlay {
width: 20%;
height: 20%;
width: 20vw;
height: 20vw;
max-width: 120px;
max-height: 120px;
min-width: 80px;
min-height: 80px;
}
.avatar-pushup {

View file

@ -1,11 +1,11 @@
import { initializeSite, isAuthPath } from "@utils/app";
import { getHttpBaseInternal } from "@utils/env";
import { ErrorPageData } from "@utils/types";
import * as cookie from "cookie";
import fetch from "cross-fetch";
import type { Request, Response } from "express";
import { StaticRouter, matchPath } from "inferno-router";
import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie";
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import { App } from "../../shared/components/app/app";
import {
@ -25,11 +25,15 @@ import { setForwardedHeaders } from "../utils/set-forwarded-headers";
export default async (req: Request, res: Response) => {
try {
const activeRoute = routes.find(route => matchPath(req.path, route));
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
let auth = req.headers.cookie
? cookie.parse(req.headers.cookie).jwt
: undefined;
const getSiteForm: GetSite = { auth };
const headers = setForwardedHeaders(req.headers);
const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
);
@ -43,6 +47,7 @@ export default async (req: Request, res: Response) => {
let routeData: RouteData = {};
let errorPageData: ErrorPageData | undefined = undefined;
let try_site = await client.getSite(getSiteForm);
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
@ -85,12 +90,13 @@ export default async (req: Request, res: Response) => {
}
const error = Object.values(routeData).find(
res => res.state === "failed"
res => res.state === "failed" && res.msg !== "couldnt_find_object" // TODO: find a better way of handling errors
) as FailedRequestState | undefined;
// Redirect to the 404 if there's an API error
if (error) {
console.error(error.msg);
if (error.msg === "instance_is_private") {
return res.redirect(`/signup`);
} else {
@ -119,6 +125,7 @@ export default async (req: Request, res: Response) => {
// If an error is caught here, the error page couldn't even be rendered
console.error(err);
res.statusCode = 500;
return res.send(
process.env.NODE_ENV === "development" ? err.message : "Server error"
);

View file

@ -1,4 +1,5 @@
import { setupDateFns } from "@utils/app";
import { getStaticDir } from "@utils/env";
import express from "express";
import path from "path";
import process from "process";
@ -19,7 +20,13 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"]
server.use(express.json());
server.use(express.urlencoded({ extended: false }));
server.use("/static", express.static(path.resolve("./dist")));
server.use(
getStaticDir(),
express.static(path.resolve("./dist"), {
maxAge: 24 * 60 * 60 * 1000, // 1 day
immutable: true,
})
);
server.use(setCacheControl);
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {

View file

@ -1,5 +1,5 @@
import type { NextFunction, Response } from "express";
import { UserService } from "../shared/services";
import type { NextFunction, Request, Response } from "express";
import { hasJwtCookie } from "./utils/has-jwt-cookie";
export function setDefaultCsp({
res,
@ -18,24 +18,35 @@ export function setDefaultCsp({
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
// all responses for 60 seconds to reduce load on backend and database. The specific cache
// all responses for 5 seconds to reduce load on backend and database. The specific cache
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
export function setCacheControl({
res,
next,
}: {
res: Response;
next: NextFunction;
}) {
const user = UserService.Instance;
let caching: string;
if (user.auth()) {
caching = "private";
} else {
caching = "public, max-age=60";
export function setCacheControl(
req: Request,
res: Response,
next: NextFunction
) {
if (process.env.NODE_ENV !== "production") {
return next();
}
let caching: string;
if (
req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) ||
req.path.includes("/css/themelist")
) {
// Static content gets cached publicly for a day
caching = "public, max-age=86400";
} else {
if (hasJwtCookie(req)) {
caching = "private";
} else {
caching = "public, max-age=5";
}
}
res.setHeader("Cache-Control", caching);
next();

View file

@ -1,3 +1,4 @@
import { getStaticDir } from "@utils/env";
import { Helmet } from "inferno-helmet";
import { renderToString } from "inferno-server";
import serialize from "serialize-javascript";
@ -23,7 +24,7 @@ export async function createSsrHtml(
if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${sharp(
? `data:image/png;base64,${await sharp(
await fetchIconPng(site.site_view.site.icon)
)
.resize(180, 180)
@ -87,7 +88,7 @@ export async function createSsrHtml(
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<link rel="stylesheet" type="text/css" href="${getStaticDir()}/styles/styles.css" />
<!-- Current theme and more -->
${helmet.link.toString() || fallbackTheme}
@ -102,7 +103,7 @@ export async function createSsrHtml(
</noscript>
<div id='root'>${root}</div>
<script defer src='/static/js/client.js'></script>
<script defer src='${getStaticDir()}/js/client.js'></script>
</body>
</html>
`;

View file

@ -0,0 +1,6 @@
import * as cookie from "cookie";
import type { Request } from "express";
export function hasJwtCookie(req: Request): boolean {
return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length);
}

View file

@ -1,10 +1,11 @@
import { isAuthPath, setIsoData } from "@utils/app";
import { dataBsTheme } from "@utils/browser";
import { Component, RefObject, createRef, linkEvent } from "inferno";
import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router";
import { IsoDataOptionalSite } from "../../interfaces";
import { routes } from "../../routes";
import { FirstLoadService, I18NextService } from "../../services";
import { FirstLoadService, I18NextService, UserService } from "../../services";
import AuthGuard from "../common/auth-guard";
import ErrorGuard from "../common/error-guard";
import { ErrorPage } from "./error-page";
@ -25,6 +26,13 @@ export class App extends Component<any, any> {
event.preventDefault();
this.mainContentRef.current?.focus();
}
user = UserService.Instance.myUserInfo;
componentDidMount() {
this.setState({ bsTheme: dataBsTheme(this.user) });
}
render() {
const siteRes = this.isoData.site_res;
const siteView = siteRes?.site_view;
@ -32,7 +40,11 @@ export class App extends Component<any, any> {
return (
<>
<Provider i18next={I18NextService.i18n}>
<div id="app" className="lemmy-site">
<div
id="app"
className="lemmy-site"
data-bs-theme={this.state?.bsTheme}
>
<button
type="button"
className="btn skip-link bg-light position-absolute start-0 z-3"

View file

@ -1,4 +1,5 @@
import { setIsoData } from "@utils/app";
import { removeAuthParam } from "@utils/helpers";
import { Component } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
@ -58,7 +59,7 @@ export class ErrorPage extends Component<any, any> {
<T
i18nKey="error_code_message"
parent="p"
interpolation={{ error: errorPageData.error }}
interpolation={{ error: removeAuthParam(errorPageData.error) }}
>
#<strong className="text-danger">#</strong>#
</T>

View file

@ -1,7 +1,6 @@
import {
colorList,
getCommentParentId,
getRoleLabelPill,
myAuth,
myAuthRequired,
showScores,
@ -63,6 +62,7 @@ import { I18NextService, UserService } from "../../services";
import { setupTippy } from "../../tippy";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { UserBadges } from "../common/user-badges";
import { VoteButtonsCompact } from "../common/vote-buttons";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing";
@ -299,7 +299,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
>
<div className="d-flex flex-wrap align-items-center text-muted small">
<button
className="btn btn-sm text-muted me-2"
className="btn btn-sm btn-link text-muted me-2"
onClick={linkEvent(this, this.handleCommentCollapse)}
aria-label={this.expandText}
data-tippy-content={this.expandText}
@ -310,41 +310,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
/>
</button>
<span className="me-2">
<PersonListing person={cv.creator} />
</span>
<PersonListing person={cv.creator} />
{cv.comment.distinguished && (
<Icon icon="shield" inline classes="text-danger me-2" />
<Icon icon="shield" inline classes="text-danger ms-1" />
)}
{this.isPostCreator &&
getRoleLabelPill({
label: I18NextService.i18n.t("op").toUpperCase(),
tooltip: I18NextService.i18n.t("creator"),
classes: "text-bg-info",
shrink: false,
})}
{isMod_ &&
getRoleLabelPill({
label: I18NextService.i18n.t("mod"),
tooltip: I18NextService.i18n.t("mod"),
classes: "text-bg-primary",
})}
{isAdmin_ &&
getRoleLabelPill({
label: I18NextService.i18n.t("admin"),
tooltip: I18NextService.i18n.t("admin"),
classes: "text-bg-danger",
})}
{cv.creator.bot_account &&
getRoleLabelPill({
label: I18NextService.i18n.t("bot_account").toLowerCase(),
tooltip: I18NextService.i18n.t("bot_account"),
})}
<UserBadges
classNames="ms-1"
isPostCreator={this.isPostCreator}
isMod={isMod_}
isAdmin={isAdmin_}
isBot={cv.creator.bot_account}
/>
{this.props.showCommunity && (
<>
@ -1483,6 +1461,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
comment_id: i.commentId,
removed: !i.commentView.comment.removed,
auth: myAuthRequired(),
reason: i.state.removeReason,
});
}

View file

@ -32,7 +32,7 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
return (
<span className="emoji-picker">
<button
className="btn btn-sm text-muted"
className="btn btn-sm btn-link rounded-0 text-muted"
data-tippy-content={I18NextService.i18n.t("emoji")}
aria-label={I18NextService.i18n.t("emoji")}
disabled={this.props.disabled}

View file

@ -1,3 +1,4 @@
import { getStaticDir } from "@utils/env";
import classNames from "classnames";
import { Component } from "inferno";
import { I18NextService } from "../../services";
@ -23,7 +24,9 @@ export class Icon extends Component<IconProps, any> {
})}
>
<use
xlinkHref={`/static/assets/symbols.svg#icon-${this.props.icon}`}
xlinkHref={`${getStaticDir()}/assets/symbols.svg#icon-${
this.props.icon
}`}
></use>
<div className="visually-hidden">
<title>{this.props.icon}</title>

View file

@ -1,4 +1,5 @@
import { randomStr } from "@utils/helpers";
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { HttpService, I18NextService, UserService } from "../../services";
import { toast } from "../../toast";
@ -33,38 +34,35 @@ export class ImageUploadForm extends Component<
render() {
return (
<form className="image-upload-form d-inline">
<label htmlFor={this.id} className="pointer text-muted small fw-bold">
{this.props.imageSrc ? (
<span className="d-inline-block position-relative">
{/* TODO: Create "Current Iamge" translation for alt text */}
<img
alt=""
src={this.props.imageSrc}
height={this.props.rounded ? 60 : ""}
width={this.props.rounded ? 60 : ""}
className={`img-fluid ${
this.props.rounded ? "rounded-circle" : ""
}`}
/>
<button
className="position-absolute d-block p-0 end-0 border-0 top-0 bg-transparent text-white"
type="button"
onClick={linkEvent(this, this.handleRemoveImage)}
aria-label={I18NextService.i18n.t("remove")}
>
<Icon icon="x" classes="mini-overlay" />
</button>
</span>
) : (
<span className="btn btn-secondary">{this.props.uploadTitle}</span>
)}
</label>
{this.props.imageSrc && (
<span className="d-inline-block position-relative mb-2">
{/* TODO: Create "Current Iamge" translation for alt text */}
<img
alt=""
src={this.props.imageSrc}
height={this.props.rounded ? 60 : ""}
width={this.props.rounded ? 60 : ""}
className={classNames({
"rounded-circle object-fit-cover": this.props.rounded,
"img-fluid": !this.props.rounded,
})}
/>
<button
className="position-absolute d-block p-0 end-0 border-0 top-0 bg-transparent text-white"
type="button"
onClick={linkEvent(this, this.handleRemoveImage)}
aria-label={I18NextService.i18n.t("remove")}
>
<Icon icon="x" classes="mini-overlay" />
</button>
</span>
)}
<input
id={this.id}
type="file"
accept="image/*,video/*"
className="small form-control"
name={this.id}
className="d-none"
disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)}
/>

View file

@ -170,31 +170,39 @@ export class MarkdownTextArea extends Component<
<EmojiPicker
onEmojiClick={e => this.handleEmoji(this, e)}
></EmojiPicker>
<form className="btn btn-sm text-muted fw-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${
UserService.Instance.myUserInfo && "pointer"
}`}
data-tippy-content={I18NextService.i18n.t("upload_image")}
>
{this.state.imageUploadStatus ? (
<Spinner />
) : (
<label
htmlFor={`file-upload-${this.id}`}
className={classNames("mb-0", {
pointer: UserService.Instance.myUserInfo,
})}
data-tippy-content={I18NextService.i18n.t("upload_image")}
>
{this.state.imageUploadStatus ? (
<Spinner />
) : (
<button
type="button"
className="btn btn-sm btn-link rounded-0 text-muted mb-0"
onClick={() => {
document
.getElementById(`file-upload-${this.id}`)
?.click();
}}
>
<Icon icon="image" classes="icon-inline" />
)}
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
className="d-none"
multiple
disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
</button>
)}
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
className="d-none"
multiple
disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)}
/>
{this.getFormatButton("header", this.handleInsertHeader)}
{this.getFormatButton(
"strikethrough",
@ -345,7 +353,7 @@ export class MarkdownTextArea extends Component<
return (
<button
className="btn btn-sm text-muted"
className="btn btn-sm btn-link rounded-0 text-muted"
data-tippy-content={I18NextService.i18n.t(type)}
aria-label={I18NextService.i18n.t(type)}
onClick={linkEvent(this, handleClick)}
@ -473,7 +481,7 @@ export class MarkdownTextArea extends Component<
// Keybind handler
// Keybinds inspired by github comment area
handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
if (event.ctrlKey) {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case "k": {
i.handleInsertLink(i, event);
@ -702,18 +710,20 @@ export class MarkdownTextArea extends Component<
quoteInsert() {
const textarea: any = document.getElementById(this.id);
const selectedText = window.getSelection()?.toString();
const { content } = this.state;
let { content } = this.state;
if (selectedText) {
const quotedText =
selectedText
.split("\n")
.map(t => `> ${t}`)
.join("\n") + "\n\n";
if (!content) {
this.setState({ content: "" });
content = "";
} else {
this.setState({ content: `${content}\n` });
content = `${content}\n\n`;
}
this.setState({
content: `${content}${quotedText}`,
});

View file

@ -1,5 +1,5 @@
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
import { formatInTimeZone } from "date-fns-tz";
import { format } from "date-fns";
import parseISO from "date-fns/parseISO";
import { Component } from "inferno";
import { I18NextService } from "../../services";
@ -13,9 +13,8 @@ interface MomentTimeProps {
}
function formatDate(input: string) {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const parsed = parseISO(input + "Z");
return formatInTimeZone(parsed, tz, "PPPPpppp");
return format(parsed, "PPPPpppp");
}
export class MomentTime extends Component<MomentTimeProps, any> {

View file

@ -34,13 +34,13 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
className={classNames("overflow-hidden pictrs-image", {
"img-fluid": !this.props.icon && !this.props.iconOverlay,
banner: this.props.banner,
"thumbnail rounded":
"thumbnail rounded object-fit-cover":
this.props.thumbnail && !this.props.icon && !this.props.banner,
"img-expanded slight-radius":
!this.props.thumbnail && !this.props.icon,
"img-blur": this.props.thumbnail && this.props.nsfw,
"img-cover img-icon me-1": this.props.icon,
"ms-2 mb-0 rounded-circle img-cover avatar-overlay":
"object-fit-cover img-icon me-1": this.props.icon,
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
this.props.iconOverlay,
"avatar-pushup": this.props.pushup,
})}

View file

@ -102,7 +102,7 @@ export class SearchableSelect extends Component<
const { searchText, selectedIndex, loadingEllipses } = this.state;
return (
<div className="searchable-select dropdown">
<div className="searchable-select dropdown col-12 col-sm-auto flex-grow-1">
<button
id={id}
type="button"

View file

@ -0,0 +1,112 @@
import classNames from "classnames";
import { Component } from "inferno";
import { I18NextService } from "../../services";
interface UserBadgesProps {
isBanned?: boolean;
isDeleted?: boolean;
isPostCreator?: boolean;
isMod?: boolean;
isAdmin?: boolean;
isBot?: boolean;
classNames?: string;
}
export function getRoleLabelPill({
label,
tooltip,
classes,
shrink = true,
}: {
label: string;
tooltip: string;
classes?: string;
shrink?: boolean;
}) {
return (
<span
className={`badge ${classes ?? "text-bg-light"}`}
aria-label={tooltip}
data-tippy-content={tooltip}
>
{shrink ? label[0].toUpperCase() : label}
</span>
);
}
export class UserBadges extends Component<UserBadgesProps> {
render() {
return (
(this.props.isBanned ||
this.props.isPostCreator ||
this.props.isMod ||
this.props.isAdmin ||
this.props.isBot) && (
<span
className={classNames(
"row d-inline-flex gx-1",
this.props.classNames
)}
>
{this.props.isBanned && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("banned"),
tooltip: I18NextService.i18n.t("banned"),
classes: "text-bg-danger",
shrink: false,
})}
</span>
)}
{this.props.isDeleted && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("deleted"),
tooltip: I18NextService.i18n.t("deleted"),
classes: "text-bg-danger",
shrink: false,
})}
</span>
)}
{this.props.isPostCreator && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("op").toUpperCase(),
tooltip: I18NextService.i18n.t("creator"),
classes: "text-bg-info",
shrink: false,
})}
</span>
)}
{this.props.isMod && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("mod"),
tooltip: I18NextService.i18n.t("mod"),
classes: "text-bg-primary",
})}
</span>
)}
{this.props.isAdmin && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("admin"),
tooltip: I18NextService.i18n.t("admin"),
classes: "text-bg-danger",
})}
</span>
)}
{this.props.isBot && (
<span className="col">
{getRoleLabelPill({
label: I18NextService.i18n.t("bot_account").toLowerCase(),
tooltip: I18NextService.i18n.t("bot_account"),
})}
</span>
)}
</span>
)
);
}
}

View file

@ -64,8 +64,6 @@ const handleUpvote = (i: VoteButtons) => {
auth: myAuthRequired(),
});
}
i.setState({ upvoteLoading: false });
};
const handleDownvote = (i: VoteButtons) => {
@ -86,7 +84,6 @@ const handleDownvote = (i: VoteButtons) => {
auth: myAuthRequired(),
});
}
i.setState({ downvoteLoading: false });
};
export class VoteButtonsCompact extends Component<
@ -102,12 +99,21 @@ export class VoteButtonsCompact extends Component<
super(props, context);
}
componentWillReceiveProps(nextProps: VoteButtonsProps) {
if (this.props !== nextProps) {
this.setState({
upvoteLoading: false,
downvoteLoading: false,
});
}
}
render() {
return (
<>
<button
type="button"
className={`btn-animate btn py-0 px-1 ${
className={`btn btn-animate btn-sm btn-link py-0 px-1 ${
this.props.my_vote === 1 ? "text-info" : "text-muted"
}`}
data-tippy-content={tippy(this.props.counts)}
@ -131,7 +137,7 @@ export class VoteButtonsCompact extends Component<
{this.props.enableDownvotes && (
<button
type="button"
className={`ms-2 btn-animate btn py-0 px-1 ${
className={`ms-2 btn btn-sm btn-link btn-animate btn py-0 px-1 ${
this.props.my_vote === -1 ? "text-danger" : "text-muted"
}`}
onClick={linkEvent(this, handleDownvote)}
@ -172,9 +178,18 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
super(props, context);
}
componentWillReceiveProps(nextProps: VoteButtonsProps) {
if (this.props !== nextProps) {
this.setState({
upvoteLoading: false,
downvoteLoading: false,
});
}
}
render() {
return (
<div className="vote-bar pe-0 small text-center">
<div className="vote-bar small text-center">
<button
type="button"
className={`btn-animate btn btn-link p-0 ${
@ -193,7 +208,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
</button>
{showScores() ? (
<div
className="unselectable pointer text-muted px-1 post-score"
className="unselectable pointer text-muted post-score"
data-tippy-content={tippy(this.props.counts)}
>
{numToSI(this.props.counts.score)}

View file

@ -102,7 +102,7 @@ export class Communities extends Component<any, CommunitiesState> {
const { listingType, page } = this.getCommunitiesQueryParams();
return (
<div>
<h1 className="h4">
<h1 className="h4 mb-4">
{I18NextService.i18n.t("list_of_communities")}
</h1>
<div className="row g-2 justify-content-between">

View file

@ -485,7 +485,7 @@ export class Community extends Component<
community && (
<div className="mb-2">
<BannerIconHeader banner={community.banner} icon={community.icon} />
<h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
<h1 className="h4 mb-0 overflow-wrap-anywhere">{community.title}</h1>
<CommunityLink
community={community}
realLink

View file

@ -39,7 +39,9 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
/>
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{I18NextService.i18n.t("create_community")}</h5>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("create_community")}
</h1>
<CommunityForm
onUpsertCommunity={this.handleCommunityCreate}
enableNsfw={enableNsfw(this.state.siteRes)}

View file

@ -169,7 +169,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return (
<div>
<h5 className="mb-0">
<h2 className="h5 mb-0">
{this.props.showIcon && !community.removed && (
<BannerIconHeader icon={community.icon} banner={community.banner} />
)}
@ -191,7 +191,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{I18NextService.i18n.t("nsfw")}
</small>
)}
</h5>
</h2>
<CommunityLink
community={community}
realLink
@ -258,7 +258,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<Spinner />
) : (
<>
<Icon icon="check" classes="icon-inline text-success me-1" />
<Icon icon="check" classes="icon-inline me-1" />
{I18NextService.i18n.t("joined")}
</>
)}

View file

@ -135,6 +135,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
role="tabpanel"
id="site-tab-pane"
>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("site_config")}
</h1>
<div className="row">
<div className="col-12 col-md-6">
<SiteForm
@ -149,6 +152,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
</div>
<div className="col-12 col-md-6">
{this.admins()}
<hr />
{this.bannedUsers()}
</div>
</div>
@ -249,7 +253,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
admins() {
return (
<>
<h5>{capitalizeFirstLetter(I18NextService.i18n.t("admins"))}</h5>
<h2 className="h5">
{capitalizeFirstLetter(I18NextService.i18n.t("admins"))}
</h2>
<ul className="list-unstyled">
{this.state.siteRes.admins.map(admin => (
<li key={admin.person.id} className="list-inline-item">
@ -289,7 +295,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
const bans = this.state.bannedRes.data.banned;
return (
<>
<h5>{I18NextService.i18n.t("banned_users")}</h5>
<h2 className="h5">{I18NextService.i18n.t("banned_users")}</h2>
<ul className="list-unstyled">
{bans.map(banned => (
<li key={banned.person.id} className="list-inline-item">

View file

@ -77,7 +77,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 className="col-12">{I18NextService.i18n.t("custom_emojis")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1>
{customEmojisLookup.size > 0 && (
<div>
<EmojiMart
@ -87,7 +87,10 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
</div>
)}
<div className="table-responsive">
<table id="emojis_table" className="table table-sm table-hover">
<table
id="emojis_table"
className="table table-sm table-hover align-middle"
>
<thead className="pointer">
<tr>
<th>{I18NextService.i18n.t("column_emoji")}</th>
@ -129,30 +132,31 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
/>
)}
{cv.image_url.length === 0 && (
<form>
<label
className="btn btn-sm btn-secondary pointer"
htmlFor={`file-uploader-${index}`}
data-tippy-content={I18NextService.i18n.t(
"upload_image"
<label
// TODO: Fix this linting violation
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className="btn btn-sm btn-secondary pointer"
htmlFor={`file-uploader-${index}`}
data-tippy-content={I18NextService.i18n.t(
"upload_image"
)}
>
{capitalizeFirstLetter(
I18NextService.i18n.t("upload")
)}
<input
name={`file-uploader-${index}`}
id={`file-uploader-${index}`}
type="file"
accept="image/*"
className="d-none"
onChange={linkEvent(
{ form: this, index: index },
this.handleImageUpload
)}
>
{capitalizeFirstLetter(
I18NextService.i18n.t("upload")
)}
<input
name={`file-uploader-${index}`}
id={`file-uploader-${index}`}
type="file"
accept="image/*"
className="d-none"
onChange={linkEvent(
{ form: this, index: index },
this.handleImageUpload
)}
/>
</label>
</form>
/>
</label>
)}
</td>
<td className="text-right">

View file

@ -279,13 +279,15 @@ export class Home extends Component<any, HomeState> {
trendingCommunitiesRes,
commentsRes,
postsRes,
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
?.content,
isIsomorphic: true,
};
HomeCacheService.postsRes = postsRes;
}
this.state.tagline = getRandomFromList(
this.state?.siteRes?.taglines ?? []
)?.content;
}
componentWillUnmount() {

View file

@ -85,24 +85,35 @@ export class Instances extends Component<any, InstancesState> {
case "success": {
const instances = this.state.instancesRes.data.federated_instances;
return instances ? (
<div className="row">
<div className="col-md-6">
<h5>{I18NextService.i18n.t("linked_instances")}</h5>
{this.itemList(instances.linked)}
<>
<h1 className="h4 mb-4">{I18NextService.i18n.t("instances")}</h1>
<div className="row">
<div className="col-md-6">
<h2 className="h5 mb-3">
{I18NextService.i18n.t("linked_instances")}
</h2>
{this.itemList(instances.linked)}
</div>
</div>
{instances.allowed && instances.allowed.length > 0 && (
<div className="col-md-6">
<h5>{I18NextService.i18n.t("allowed_instances")}</h5>
{this.itemList(instances.allowed)}
</div>
)}
{instances.blocked && instances.blocked.length > 0 && (
<div className="col-md-6">
<h5>{I18NextService.i18n.t("blocked_instances")}</h5>
{this.itemList(instances.blocked)}
</div>
)}
</div>
<div className="row">
{instances.allowed && instances.allowed.length > 0 && (
<div className="col-md-6">
<h2 className="h5 mb-3">
{I18NextService.i18n.t("allowed_instances")}
</h2>
{this.itemList(instances.allowed)}
</div>
)}
{instances.blocked && instances.blocked.length > 0 && (
<div className="col-md-6">
<h2 className="h5 mb-3">
{I18NextService.i18n.t("blocked_instances")}
</h2>
{this.itemList(instances.blocked)}
</div>
)}
</div>
</>
) : (
<></>
);

View file

@ -59,9 +59,9 @@ export class LoginReset extends Component<any, State> {
loginResetForm() {
return (
<form onSubmit={linkEvent(this, this.handlePasswordReset)}>
<h5>
<h1 className="h4 mb-4">
{capitalizeFirstLetter(I18NextService.i18n.t("forgot_password"))}
</h5>
</h1>
<div className="form-group row">
<label className="col-form-label">

View file

@ -69,7 +69,7 @@ export class Login extends Component<any, State> {
return (
<div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<h5>{I18NextService.i18n.t("login")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1>
<div className="mb-3 row">
<label
className="col-sm-2 col-form-label"

View file

@ -145,7 +145,9 @@ export default class RateLimitsForm extends Component<
className="rate-limit-form"
onSubmit={linkEvent(this, submitRateLimitForm)}
>
<h5>{I18NextService.i18n.t("rate_limit_header")}</h5>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("rate_limit_header")}
</h1>
<Tabs
tabs={rateLimitTypes.map(rateLimitType => ({
key: rateLimitType,

View file

@ -63,7 +63,9 @@ export class Setup extends Component<any, State> {
<Helmet title={this.documentTitle} />
<div className="row">
<div className="col-12 offset-lg-3 col-lg-6">
<h3>{I18NextService.i18n.t("lemmy_instance_setup")}</h3>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("lemmy_instance_setup")}
</h1>
{!this.state.doneRegisteringUser ? (
this.registerUser()
) : (
@ -84,7 +86,7 @@ export class Setup extends Component<any, State> {
registerUser() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>{I18NextService.i18n.t("setup_admin")}</h5>
<h2 className="h5 mb-3">{I18NextService.i18n.t("setup_admin")}</h2>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="username">
{I18NextService.i18n.t("username")}

View file

@ -144,7 +144,7 @@ export class Signup extends Component<any, State> {
className="was-validated"
onSubmit={linkEvent(this, this.handleRegisterSubmit)}
>
<h5>{this.titleName(siteView)}</h5>
<h1 className="h4 mb-4">{this.titleName(siteView)}</h1>
{this.isLemmyMl && (
<div className="mb-3 row">

View file

@ -136,11 +136,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
!this.state.submitted
}
/>
<h5>{`${
<h2 className="h5">{`${
siteSetup
? capitalizeFirstLetter(I18NextService.i18n.t("edit"))
: capitalizeFirstLetter(I18NextService.i18n.t("setup"))
} ${I18NextService.i18n.t("your_site")}`}</h5>
} ${I18NextService.i18n.t("your_site")}`}</h2>
<div className="mb-3 row">
<label className="col-12 col-form-label" htmlFor="create-site-name">
{I18NextService.i18n.t("name")}
@ -158,28 +158,32 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/>
</div>
</div>
<div className="input-group mb-3">
<label className="me-2 col-form-label">
<div className="row mb-3">
<label className="col-sm-2 col-form-label">
{I18NextService.i18n.t("icon")}
</label>
<ImageUploadForm
uploadTitle={I18NextService.i18n.t("upload_icon")}
imageSrc={this.state.siteForm.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
rounded
/>
<div className="col-sm-10">
<ImageUploadForm
uploadTitle={I18NextService.i18n.t("upload_icon")}
imageSrc={this.state.siteForm.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
rounded
/>
</div>
</div>
<div className="input-group mb-3">
<label className="me-2 col-form-label">
<div className="row mb-3">
<label className="col-sm-2 col-form-label">
{I18NextService.i18n.t("banner")}
</label>
<ImageUploadForm
uploadTitle={I18NextService.i18n.t("upload_banner")}
imageSrc={this.state.siteForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
<div className="col-sm-10">
<ImageUploadForm
uploadTitle={I18NextService.i18n.t("upload_banner")}
imageSrc={this.state.siteForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-12 col-form-label" htmlFor="site-desc">

View file

@ -37,7 +37,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 className="col-12">{I18NextService.i18n.t("taglines")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("taglines")}</h1>
<div className="table-responsive col-12">
<table id="taglines_table" className="table table-sm table-hover">
<thead className="pointer">
@ -141,7 +141,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
event.preventDefault();
if (this.state.editingRow == d.index) {
if (d.i.state.editingRow == d.index) {
d.i.setState({ editingRow: undefined });
} else {
d.i.setState({ editingRow: d.index });

View file

@ -751,87 +751,83 @@ export class Modlog extends Component<
path={this.context.router.route.match.url}
/>
<div>
<div
className="alert alert-warning text-sm-start text-xs-center"
role="alert"
>
<Icon
icon="alert-triangle"
inline
classes="me-sm-2 mx-auto d-sm-inline d-block"
/>
<T i18nKey="modlog_content_warning" class="d-inline">
#<strong>#</strong>#
</T>
</div>
{this.state.communityRes.state === "success" && (
<h5>
<Link
className="text-body"
to={`/c/${this.state.communityRes.data.community_view.community.name}`}
>
/c/{this.state.communityRes.data.community_view.community.name}{" "}
</Link>
<span>{I18NextService.i18n.t("modlog")}</span>
</h5>
)}
<div className="row mb-2">
<div className="col-sm-6">
<select
value={actionType}
onChange={linkEvent(this, this.handleFilterActionChange)}
className="form-select"
aria-label="action"
>
<option disabled aria-hidden="true">
{I18NextService.i18n.t("filter_by_action")}
</option>
<option value={"All"}>{I18NextService.i18n.t("all")}</option>
<option value={"ModRemovePost"}>Removing Posts</option>
<option value={"ModLockPost"}>Locking Posts</option>
<option value={"ModFeaturePost"}>Featuring Posts</option>
<option value={"ModRemoveComment"}>Removing Comments</option>
<option value={"ModRemoveCommunity"}>
Removing Communities
</option>
<option value={"ModBanFromCommunity"}>
Banning From Communities
</option>
<option value={"ModAddCommunity"}>
Adding Mod to Community
</option>
<option value={"ModTransferCommunity"}>
Transferring Communities
</option>
<option value={"ModAdd"}>Adding Mod to Site</option>
<option value={"ModBan"}>Banning From Site</option>
</select>
</div>
</div>
<div className="row mb-2">
<Filter
filterType="user"
onChange={this.handleUserChange}
onSearch={this.handleSearchUsers}
value={userId}
options={userSearchOptions}
loading={loadingUserSearch}
/>
{!this.isoData.site_res.site_view.local_site
.hide_modlog_mod_names && (
<Filter
filterType="mod"
onChange={this.handleModChange}
onSearch={this.handleSearchMods}
value={modId}
options={modSearchOptions}
loading={loadingModSearch}
/>
)}
</div>
{this.renderModlogTable()}
<h1 className="h4 mb-4">{I18NextService.i18n.t("modlog")}</h1>
<div
className="alert alert-warning text-sm-start text-xs-center"
role="alert"
>
<Icon
icon="alert-triangle"
inline
classes="me-sm-2 mx-auto d-sm-inline d-block"
/>
<T i18nKey="modlog_content_warning" class="d-inline">
#<strong>#</strong>#
</T>
</div>
{this.state.communityRes.state === "success" && (
<h5>
<Link
className="text-body"
to={`/c/${this.state.communityRes.data.community_view.community.name}`}
>
/c/{this.state.communityRes.data.community_view.community.name}{" "}
</Link>
<span>{I18NextService.i18n.t("modlog")}</span>
</h5>
)}
<div className="row mb-2">
<div className="col-sm-6">
<select
value={actionType}
onChange={linkEvent(this, this.handleFilterActionChange)}
className="form-select"
aria-label="action"
>
<option disabled aria-hidden="true">
{I18NextService.i18n.t("filter_by_action")}
</option>
<option value={"All"}>{I18NextService.i18n.t("all")}</option>
<option value={"ModRemovePost"}>Removing Posts</option>
<option value={"ModLockPost"}>Locking Posts</option>
<option value={"ModFeaturePost"}>Featuring Posts</option>
<option value={"ModRemoveComment"}>Removing Comments</option>
<option value={"ModRemoveCommunity"}>Removing Communities</option>
<option value={"ModBanFromCommunity"}>
Banning From Communities
</option>
<option value={"ModAddCommunity"}>Adding Mod to Community</option>
<option value={"ModTransferCommunity"}>
Transferring Communities
</option>
<option value={"ModAdd"}>Adding Mod to Site</option>
<option value={"ModBan"}>Banning From Site</option>
</select>
</div>
</div>
<div className="row mb-2">
<Filter
filterType="user"
onChange={this.handleUserChange}
onSearch={this.handleSearchUsers}
value={userId}
options={userSearchOptions}
loading={loadingUserSearch}
/>
{!this.isoData.site_res.site_view.local_site
.hide_modlog_mod_names && (
<Filter
filterType="mod"
onChange={this.handleModChange}
onSearch={this.handleSearchMods}
value={modId}
options={modSearchOptions}
loading={loadingModSearch}
/>
)}
</div>
{this.renderModlogTable()}
</div>
);
}

View file

@ -221,7 +221,7 @@ export class Inbox extends Component<any, InboxState> {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 className="mb-2">
<h1 className="h4 mb-4">
{I18NextService.i18n.t("inbox")}
{inboxRss && (
<small>
@ -235,10 +235,10 @@ export class Inbox extends Component<any, InboxState> {
/>
</small>
)}
</h5>
</h1>
{this.hasUnreads && (
<button
className="btn btn-secondary mb-2"
className="btn btn-secondary mb-2 mb-sm-3"
onClick={linkEvent(this, this.handleMarkAllAsRead)}
>
{this.state.markAllAsReadRes.state == "loading" ? (
@ -284,7 +284,7 @@ export class Inbox extends Component<any, InboxState> {
unreadOrAllRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
<div className="btn-group btn-group-toggle flex-wrap">
<label
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
@ -319,7 +319,7 @@ export class Inbox extends Component<any, InboxState> {
messageTypeRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
<div className="btn-group btn-group-toggle flex-wrap">
<label
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && "active"}
@ -382,13 +382,15 @@ export class Inbox extends Component<any, InboxState> {
selects() {
return (
<div className="mb-2">
<span className="me-3">{this.unreadOrAllRadios()}</span>
<span className="me-3">{this.messageTypeRadios()}</span>
<CommentSortSelect
sort={this.state.sort}
onChange={this.handleSortChange}
/>
<div className="row row-cols-auto g-2 g-sm-3 mb-2 mb-sm-3">
<div className="col">{this.unreadOrAllRadios()}</div>
<div className="col">{this.messageTypeRadios()}</div>
<div className="col">
<CommentSortSelect
sort={this.state.sort}
onChange={this.handleSortChange}
/>
</div>
</div>
);
}
@ -541,9 +543,9 @@ export class Inbox extends Component<any, InboxState> {
this.state.messagesRes.state == "loading"
) {
return (
<h5>
<h1 className="h4">
<Spinner large />
</h5>
</h1>
);
} else {
return (
@ -556,9 +558,9 @@ export class Inbox extends Component<any, InboxState> {
switch (this.state.repliesRes.state) {
case "loading":
return (
<h5>
<h1 className="h4">
<Spinner large />
</h5>
</h1>
);
case "success": {
const replies = this.state.repliesRes.data.replies;
@ -603,9 +605,9 @@ export class Inbox extends Component<any, InboxState> {
switch (this.state.mentionsRes.state) {
case "loading":
return (
<h5>
<h1 className="h4">
<Spinner large />
</h5>
</h1>
);
case "success": {
const mentions = this.state.mentionsRes.data.mentions;
@ -653,9 +655,9 @@ export class Inbox extends Component<any, InboxState> {
switch (this.state.messagesRes.state) {
case "loading":
return (
<h5>
<h1 className="h4">
<Spinner large />
</h5>
</h1>
);
case "success": {
const messages = this.state.messagesRes.data.private_messages;

View file

@ -47,7 +47,9 @@ export class PasswordChange extends Component<any, State> {
/>
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{I18NextService.i18n.t("password_change")}</h5>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("password_change")}
</h1>
{this.passwordChangeForm()}
</div>
</div>

View file

@ -1,4 +1,5 @@
import { showAvatars } from "@utils/app";
import { getStaticDir } from "@utils/env";
import { hostname, isCakeDay } from "@utils/helpers";
import classNames from "classnames";
import { Component } from "inferno";
@ -88,7 +89,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
!this.props.person.banned &&
showAvatars() && (
<PictrsImage
src={avatar ?? "/static/assets/icons/icon-96x96.png"}
src={avatar ?? `${getStaticDir()}/assets/icons/icon-96x96.png`}
icon
/>
)}

View file

@ -5,7 +5,6 @@ import {
enableDownvotes,
enableNsfw,
getCommentParentId,
getRoleLabelPill,
myAuth,
myAuthRequired,
setIsoData,
@ -85,6 +84,7 @@ import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { SortSelect } from "../common/sort-select";
import { UserBadges } from "../common/user-badges";
import { CommunityLink } from "../community/community-link";
import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing";
@ -137,7 +137,7 @@ const getCommunitiesListing = (
communityViews.length > 0 && (
<div className="card border-secondary mb-3">
<div className="card-body">
<h5>{I18NextService.i18n.t(translationKey)}</h5>
<h2 className="h5">{I18NextService.i18n.t(translationKey)}</h2>
<ul className="list-unstyled mb-0">
{communityViews.map(({ community }) => (
<li key={community.id}>
@ -232,7 +232,7 @@ export class Profile extends Component<
async fetchUserData() {
const { page, sort, view } = getProfileQueryParams();
this.setState({ personRes: { state: "empty" } });
this.setState({ personRes: { state: "loading" } });
this.setState({
personRes: await HttpService.client.getPersonDetails({
username: this.props.match.params.username,
@ -472,7 +472,7 @@ export class Profile extends Component<
<div className="mb-0 d-flex flex-wrap">
<div>
{pv.person.display_name && (
<h5 className="mb-0">{pv.person.display_name}</h5>
<h1 className="h4 mb-4">{pv.person.display_name}</h1>
)}
<ul className="list-inline mb-2">
<li className="list-inline-item">
@ -484,46 +484,15 @@ export class Profile extends Component<
hideAvatar
/>
</li>
{isBanned(pv.person) && (
<li className="list-inline-item">
{getRoleLabelPill({
label: I18NextService.i18n.t("banned"),
tooltip: I18NextService.i18n.t("banned"),
classes: "text-bg-danger",
shrink: false,
})}
</li>
)}
{pv.person.deleted && (
<li className="list-inline-item">
{getRoleLabelPill({
label: I18NextService.i18n.t("deleted"),
tooltip: I18NextService.i18n.t("deleted"),
classes: "text-bg-danger",
shrink: false,
})}
</li>
)}
{pv.person.admin && (
<li className="list-inline-item">
{getRoleLabelPill({
label: I18NextService.i18n.t("admin"),
tooltip: I18NextService.i18n.t("admin"),
shrink: false,
})}
</li>
)}
{pv.person.bot_account && (
<li className="list-inline-item">
{getRoleLabelPill({
label: I18NextService.i18n
.t("bot_account")
.toLowerCase(),
tooltip: I18NextService.i18n.t("bot_account"),
shrink: false,
})}
</li>
)}
<li className="list-inline-item">
<UserBadges
classNames="ms-1"
isBanned={isBanned(pv.person)}
isDeleted={pv.person.deleted}
isAdmin={pv.person.admin}
isBot={pv.person.bot_account}
/>
</li>
</ul>
</div>
{this.banDialog(pv)}

View file

@ -100,9 +100,9 @@ export class RegistrationApplications extends Component<
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 className="mb-2">
<h1 className="h4 mb-4">
{I18NextService.i18n.t("registration_applications")}
</h5>
</h1>
{this.selects()}
{this.applicationList(apps)}
<Paginator

View file

@ -152,7 +152,7 @@ export class Reports extends Component<any, ReportsState> {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 className="mb-2">{I18NextService.i18n.t("reports")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("reports")}</h1>
{this.selects()}
{this.section}
<Paginator

View file

@ -316,7 +316,7 @@ export class Settings extends Component<any, SettingsState> {
changePasswordHtmlForm() {
return (
<>
<h5>{I18NextService.i18n.t("change_password")}</h5>
<h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
<div className="mb-3 row">
<label className="col-sm-5 col-form-label" htmlFor="user-password">
@ -409,7 +409,7 @@ export class Settings extends Component<any, SettingsState> {
blockedUsersList() {
return (
<>
<h5>{I18NextService.i18n.t("blocked_users")}</h5>
<h2 className="h5">{I18NextService.i18n.t("blocked_users")}</h2>
<ul className="list-unstyled mb-0">
{this.state.personBlocks.map(pb => (
<li key={pb.target.id}>
@ -453,7 +453,7 @@ export class Settings extends Component<any, SettingsState> {
blockedCommunitiesList() {
return (
<>
<h5>{I18NextService.i18n.t("blocked_communities")}</h5>
<h2 className="h5">{I18NextService.i18n.t("blocked_communities")}</h2>
<ul className="list-unstyled mb-0">
{this.state.communityBlocks.map(cb => (
<li key={cb.community.id}>
@ -484,7 +484,7 @@ export class Settings extends Component<any, SettingsState> {
return (
<>
<h5>{I18NextService.i18n.t("settings")}</h5>
<h2 className="h5">{I18NextService.i18n.t("settings")}</h2>
<form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label" htmlFor="display-name">

View file

@ -60,7 +60,7 @@ export class VerifyEmail extends Component<any, State> {
/>
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{I18NextService.i18n.t("verify_email")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("verify_email")}</h1>
{this.state.verifyRes.state == "loading" && (
<h5>
<Spinner large />

View file

@ -170,7 +170,9 @@ export class CreatePost extends Component<
id="createPostForm"
className="col-12 col-lg-6 offset-lg-3 mb-4"
>
<h1 className="h4">{I18NextService.i18n.t("create_post")}</h1>
<h1 className="h4 mb-4">
{I18NextService.i18n.t("create_post")}
</h1>
<PostForm
onCreate={this.handlePostCreate}
params={locationState}

View file

@ -349,32 +349,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<input
type="url"
id="post-url"
className="form-control"
className="form-control mb-3"
value={url}
onInput={linkEvent(this, handlePostUrlChange)}
onPaste={linkEvent(this, handleImageUploadPaste)}
/>
{this.renderSuggestedTitleCopy()}
<form>
<label
htmlFor="file-upload"
className={`${
UserService.Instance.myUserInfo && "pointer"
} d-inline-block float-right text-muted fw-bold`}
data-tippy-content={I18NextService.i18n.t("upload_image")}
>
<Icon icon="image" classes="icon-inline" />
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
className="d-none"
disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, handleImageUpload)}
/>
</form>
{url && validURL(url) && (
<div>
<a
@ -404,56 +384,73 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</a>
</div>
)}
</div>
</div>
<div className="mb-3 row">
<label htmlFor="file-upload" className={"col-sm-2 col-form-label"}>
{capitalizeFirstLetter(I18NextService.i18n.t("image"))}
<Icon icon="image" classes="icon-inline ms-1" />
</label>
<div className="col-sm-10">
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
className="small col-sm-10 form-control"
disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, handleImageUpload)}
/>
{this.state.imageLoading && <Spinner />}
{url && isImage(url) && (
<img src={url} className="img-fluid" alt="" />
<img src={url} className="img-fluid mt-2" alt="" />
)}
{this.state.imageDeleteUrl && (
<button
className="btn btn-danger btn-sm mt-2"
onClick={linkEvent(this, handleImageDelete)}
aria-label={I18NextService.i18n.t("delete")}
data-tippy-content={I18NextService.i18n.t("delete")}
>
<Icon icon="x" classes="icon-inline me-1" />
{capitalizeFirstLetter(I18NextService.i18n.t("delete"))}
</button>
)}
{this.props.crossPosts && this.props.crossPosts.length > 0 && (
<>
<div className="my-1 text-muted small fw-bold">
{I18NextService.i18n.t("cross_posts")}
</div>
<PostListings
showCommunity
posts={this.props.crossPosts}
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
viewOnly
// All of these are unused, since its view only
onPostEdit={() => {}}
onPostVote={() => {}}
onPostReport={() => {}}
onBlockPerson={() => {}}
onLockPost={() => {}}
onDeletePost={() => {}}
onRemovePost={() => {}}
onSavePost={() => {}}
onFeaturePost={() => {}}
onPurgePerson={() => {}}
onPurgePost={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
onAddModToCommunity={() => {}}
onAddAdmin={() => {}}
onTransferCommunity={() => {}}
/>
</>
)}
</div>
{this.props.crossPosts && this.props.crossPosts.length > 0 && (
<>
<div className="my-1 text-muted small fw-bold">
{I18NextService.i18n.t("cross_posts")}
</div>
<PostListings
showCommunity
posts={this.props.crossPosts}
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
viewOnly
// All of these are unused, since its view only
onPostEdit={() => {}}
onPostVote={() => {}}
onPostReport={() => {}}
onBlockPerson={() => {}}
onLockPost={() => {}}
onDeletePost={() => {}}
onRemovePost={() => {}}
onSavePost={() => {}}
onFeaturePost={() => {}}
onPurgePerson={() => {}}
onPurgePost={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
onAddModToCommunity={() => {}}
onAddAdmin={() => {}}
onTransferCommunity={() => {}}
/>
</>
)}
</div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="post-title">
{I18NextService.i18n.t("title")}

View file

@ -1,4 +1,4 @@
import { getRoleLabelPill, myAuthRequired } from "@utils/app";
import { myAuthRequired } from "@utils/app";
import { canShare, share } from "@utils/browser";
import { getExternalHost, getHttpBase } from "@utils/env";
import {
@ -55,6 +55,7 @@ import { setupTippy } from "../../tippy";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { PictrsImage } from "../common/pictrs-image";
import { UserBadges } from "../common/user-badges";
import { VoteButtons, VoteButtonsCompact } from "../common/vote-buttons";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing";
@ -333,7 +334,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return (
<button
type="button"
className="thumbnail rounded overflow-hidden d-inline-block position-relative mb-2 p-0 border-0"
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0 bg-transparent"
data-tippy-content={I18NextService.i18n.t("expand_here")}
onClick={linkEvent(this, this.handleImageExpandClick)}
aria-label={I18NextService.i18n.t("expand_here")}
@ -348,7 +349,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
return (
<a
className="thumbnail rounded bg-light d-flex justify-content-center"
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0"
href={url}
rel={relTags}
title={url}
@ -403,28 +404,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
createdLine() {
const post_view = this.postView;
return (
<div className="small">
<span className="me-1">
<PersonListing person={post_view.creator} />
</span>
{this.creatorIsMod_ &&
getRoleLabelPill({
label: I18NextService.i18n.t("mod"),
tooltip: I18NextService.i18n.t("mod"),
classes: "text-bg-primary",
})}
{this.creatorIsAdmin_ &&
getRoleLabelPill({
label: I18NextService.i18n.t("admin"),
tooltip: I18NextService.i18n.t("admin"),
classes: "text-bg-danger",
})}
{post_view.creator.bot_account &&
getRoleLabelPill({
label: I18NextService.i18n.t("bot_account").toLowerCase(),
tooltip: I18NextService.i18n.t("bot_account"),
})}
<div className="small mb-1 mb-md-0">
<PersonListing person={post_view.creator} />
<UserBadges
classNames="ms-1"
isMod={this.creatorIsMod_}
isAdmin={this.creatorIsAdmin_}
isBot={post_view.creator.bot_account}
/>
{this.props.showCommunity && (
<>
{" "}
@ -476,8 +465,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return (
<>
<div className="post-title overflow-hidden">
<h5 className="d-inline">
<div className="post-title">
<h1 className="h5 d-inline text-break">
{url && this.props.showBody ? (
<a
className={
@ -493,15 +482,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
) : (
this.postLink
)}
</h5>
</h1>
{/**
* If there is a URL, an embed title, and we were not told to show the
* body by the parent component, show the MetadataCard/body toggle.
* If there is (a) a URL and an embed title, or (b) a post body, and
* we were not told to show the body by the parent component, show the
* MetadataCard/body toggle.
*/}
{!this.props.showBody &&
post.url &&
post.embed_title &&
((post.url && post.embed_title) || post.body) &&
this.showPreviewButton()}
{post.removed && (
@ -1426,6 +1415,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
UserService.Instance.myUserInfo?.local_user_view.person.id
);
}
handleEditClick(i: PostListing) {
i.setState({ showEdit: true });
}
@ -1549,6 +1539,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
post_id: i.postView.post.id,
removed: !i.postView.post.removed,
auth: myAuthRequired(),
reason: i.state.removeReason,
});
}
@ -1620,13 +1611,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
handlePurgeSubmit(i: PostListing, event: any) {
event.preventDefault();
i.setState({ purgeLoading: true });
if (i.state.purgeType == PurgeType.Person) {
if (i.state.purgeType === PurgeType.Person) {
i.props.onPurgePerson({
person_id: i.postView.creator.id,
reason: i.state.purgeReason,
auth: myAuthRequired(),
});
} else if (i.state.purgeType == PurgeType.Post) {
} else if (i.state.purgeType === PurgeType.Post) {
i.props.onPurgePost({
post_id: i.postView.post.id,
reason: i.state.purgeReason,

View file

@ -115,7 +115,7 @@ export class CreatePrivateMessage extends Component<
return (
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h1 className="h4">
<h1 className="h4 mb-4">
{I18NextService.i18n.t("create_private_message")}
</h1>
<PrivateMessageForm

View file

@ -284,7 +284,6 @@ export class PrivateMessage extends Component<
<div className="row">
<div className="col-sm-6">
<PrivateMessageForm
privateMessageView={message_view}
replyType={true}
recipient={otherPerson}
onCreate={this.props.onCreate}

View file

@ -181,8 +181,8 @@ const Filter = ({
loading: boolean;
}) => {
return (
<div className="mb-3 col-sm-6">
<label className="col-form-label me-2" htmlFor={`${filterType}-filter`}>
<div className="col-sm-6">
<label className="mb-1" htmlFor={`${filterType}-filter`}>
{capitalizeFirstLetter(I18NextService.i18n.t(filterType))}
</label>
<SearchableSelect
@ -467,7 +467,7 @@ export class Search extends Component<any, SearchState> {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5>{I18NextService.i18n.t("search")}</h5>
<h1 className="h4 mb-4">{I18NextService.i18n.t("search")}</h1>
{this.selects}
{this.searchForm}
{this.displayResults(type)}
@ -500,8 +500,11 @@ export class Search extends Component<any, SearchState> {
get searchForm() {
return (
<form className="row" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
<div className="col-auto">
<form
className="row gx-2 gy-3"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
<div className="col-auto flex-grow-1 flex-sm-grow-0">
<input
type="text"
className="form-control me-2 mb-2 col-sm-8"
@ -542,41 +545,45 @@ export class Search extends Component<any, SearchState> {
communitiesRes.data.communities.length > 0;
return (
<div className="mb-2">
<select
value={type}
onChange={linkEvent(this, this.handleTypeChange)}
className="form-select d-inline-block w-auto mb-2"
aria-label={I18NextService.i18n.t("type")}
>
<option disabled aria-hidden="true">
{I18NextService.i18n.t("type")}
</option>
{searchTypes.map(option => (
<option value={option} key={option}>
{I18NextService.i18n.t(
option.toString().toLowerCase() as NoOptionI18nKeys
)}
</option>
))}
</select>
<span className="ms-2">
<ListingTypeSelect
type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
/>
</span>
<span className="ms-2">
<SortSelect
sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
/>
</span>
<div className="row">
<>
<div className="row row-cols-auto g-2 g-sm-3 mb-2 mb-sm-3">
<div className="col">
<select
value={type}
onChange={linkEvent(this, this.handleTypeChange)}
className="form-select d-inline-block w-auto"
aria-label={I18NextService.i18n.t("type")}
>
<option disabled aria-hidden="true">
{I18NextService.i18n.t("type")}
</option>
{searchTypes.map(option => (
<option value={option} key={option}>
{I18NextService.i18n.t(
option.toString().toLowerCase() as NoOptionI18nKeys
)}
</option>
))}
</select>
</div>
<div className="col">
<ListingTypeSelect
type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
/>
</div>
<div className="col">
<SortSelect
sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
/>
</div>
</div>
<div className="row gy-2 gx-4 mb-3">
{hasCommunities && (
<Filter
filterType="community"
@ -596,7 +603,7 @@ export class Search extends Component<any, SearchState> {
loading={searchCreatorLoading}
/>
</div>
</div>
</>
);
}

View file

@ -1,5 +1,7 @@
export const favIconUrl = "/static/assets/icons/favicon.svg";
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
import { getStaticDir } from "@utils/env";
export const favIconUrl = `${getStaticDir()}/assets/icons/favicon.svg`;
export const favIconPngUrl = `${getStaticDir()}/assets/icons/apple-touch-icon.png`;
export const repoUrl = "https://github.com/LemmyNet";
export const joinLemmyUrl = "https://join-lemmy.org";

View file

@ -2,7 +2,7 @@
import { isAuthPath } from "@utils/app";
import { isBrowser } from "@utils/browser";
import { isHttps } from "@utils/env";
import IsomorphicCookie from "isomorphic-cookie";
import * as cookie from "cookie";
import jwt_decode from "jwt-decode";
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
import { toast } from "../toast";
@ -31,9 +31,15 @@ export class UserService {
public login(res: LoginResponse) {
const expires = new Date();
expires.setDate(expires.getDate() + 365);
if (res.jwt) {
if (isBrowser() && res.jwt) {
toast(I18NextService.i18n.t("logged_in"));
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps() });
document.cookie = cookie.serialize("jwt", res.jwt, {
expires,
secure: isHttps(),
domain: location.hostname,
sameSite: true,
path: "/",
});
this.#setJwtInfo();
}
}
@ -41,8 +47,14 @@ export class UserService {
public logout() {
this.jwtInfo = undefined;
this.myUserInfo = undefined;
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.hostname;
if (isBrowser()) {
document.cookie = cookie.serialize("jwt", "", {
maxAge: 0,
path: "/",
domain: location.hostname,
sameSite: true,
});
}
if (isAuthPath(location.pathname)) {
location.replace("/");
} else {
@ -66,10 +78,11 @@ export class UserService {
}
#setJwtInfo() {
const jwt: string | undefined = IsomorphicCookie.load("jwt");
if (jwt) {
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
if (isBrowser()) {
const { jwt } = cookie.parse(document.cookie);
if (jwt) {
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
}
}
}

View file

@ -1,21 +0,0 @@
export default function getRoleLabelPill({
label,
tooltip,
classes,
shrink = true,
}: {
label: string;
tooltip: string;
classes?: string;
shrink?: boolean;
}) {
return (
<span
className={`badge me-1 ${classes ?? "text-bg-light"}`}
aria-label={tooltip}
data-tippy-content={tooltip}
>
{shrink ? label[0].toUpperCase() : label}
</span>
);
}

View file

@ -29,7 +29,6 @@ import getDataTypeString from "./get-data-type-string";
import getDepthFromComment from "./get-depth-from-comment";
import getIdFromProps from "./get-id-from-props";
import getRecipientIdFromProps from "./get-recipient-id-from-props";
import getRoleLabelPill from "./get-role-label-pill";
import getUpdatedSearchId from "./get-updated-search-id";
import initializeSite from "./initialize-site";
import insertCommentIntoTree from "./insert-comment-into-tree";
@ -87,7 +86,6 @@ export {
getDepthFromComment,
getIdFromProps,
getRecipientIdFromProps,
getRoleLabelPill,
getUpdatedSearchId,
initializeSite,
insertCommentIntoTree,

View file

@ -1,5 +1,5 @@
export default function isAuthPath(pathname: string) {
return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
return /^\/create_.*|inbox|settings|admin|reports|registration_applications/g.test(
pathname
);
}

View file

@ -1,18 +1,44 @@
import setDefaultOptions from "date-fns/setDefaultOptions";
import { I18NextService } from "../../services";
const EN_US = "en-US";
export default async function () {
let lang = I18NextService.i18n.language;
if (lang === "en") {
lang = "en-US";
lang = EN_US;
}
const locale = (
await import(
/* webpackExclude: /\.js\.flow$/ */
`date-fns/locale/${lang}`
)
).default;
// if lang and country are the same, then date-fns expects only the lang
// eg: instead of "fr-FR", we should import just "fr"
if (lang.includes("-")) {
const parts = lang.split("-");
if (parts[0] === parts[1].toLowerCase()) {
lang = parts[0];
}
}
let locale;
try {
locale = (
await import(
/* webpackExclude: /\.js\.flow$/ */
`date-fns/locale/${lang}`
)
).default;
} catch (e) {
console.log(
`Could not load locale ${lang} from date-fns, falling back to ${EN_US}`
);
locale = (
await import(
/* webpackExclude: /\.js\.flow$/ */
`date-fns/locale/${EN_US}`
)
).default;
}
setDefaultOptions({
locale,
});

View file

@ -0,0 +1,11 @@
import isDark from "./is-dark";
export default function dataBsTheme(user) {
return (isDark() && user?.local_user_view.local_user.theme === "browser") ||
(user &&
["darkly", "darkly-red", "darkly-pureblack"].includes(
user.local_user_view.local_user.theme
))
? "dark"
: "light";
}

View file

@ -1,5 +1,7 @@
import canShare from "./can-share";
import dataBsTheme from "./data-bs-theme";
import isBrowser from "./is-browser";
import isDark from "./is-dark";
import loadCss from "./load-css";
import restoreScrollPosition from "./restore-scroll-position";
import saveScrollPosition from "./save-scroll-position";
@ -7,7 +9,9 @@ import share from "./share";
export {
canShare,
dataBsTheme,
isBrowser,
isDark,
loadCss,
restoreScrollPosition,
saveScrollPosition,

View file

@ -0,0 +1,7 @@
import isBrowser from "./is-browser";
export default function isDark() {
return (
isBrowser() && window.matchMedia("(prefers-color-scheme: dark)").matches
);
}

View file

@ -0,0 +1,5 @@
// Returns path to static directory, intended
// for cache-busting based on latest commit hash.
export default function getStaticDir() {
return `/static/${process.env.COMMIT_HASH}`;
}

View file

@ -6,6 +6,7 @@ import getHttpBaseExternal from "./get-http-base-external";
import getHttpBaseInternal from "./get-http-base-internal";
import getInternalHost from "./get-internal-host";
import getSecure from "./get-secure";
import getStaticDir from "./get-static-dir";
import httpExternalPath from "./http-external-path";
import isHttps from "./is-https";
@ -18,6 +19,7 @@ export {
getHttpBaseInternal,
getInternalHost,
getSecure,
getStaticDir,
httpExternalPath,
isHttps,
};

View file

@ -17,6 +17,7 @@ import isCakeDay from "./is-cake-day";
import numToSI from "./num-to-si";
import poll from "./poll";
import randomStr from "./random-str";
import removeAuthParam from "./remove-auth-param";
import sleep from "./sleep";
import validEmail from "./valid-email";
import validInstanceTLD from "./valid-instance-tld";
@ -43,6 +44,7 @@ export {
numToSI,
poll,
randomStr,
removeAuthParam,
sleep,
validEmail,
validInstanceTLD,

View file

@ -0,0 +1,6 @@
export default function (err: any) {
return err
.toString()
.replace(new RegExp("[?&]auth=[^&#]*(#.*)?$"), "$1")
.replace(new RegExp("([?&])auth=[^&]*&"), "$1");
}

View file

@ -14,56 +14,63 @@ const banner = `
@license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
`;
const base = {
output: {
filename: "js/server.js",
publicPath: "/",
hashFunction: "xxhash64",
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
alias: {
"@": path.resolve(__dirname, "src/"),
"@utils": path.resolve(__dirname, "src/shared/utils/"),
function getBase(env, mode) {
return {
output: {
filename: "js/server.js",
publicPath: "/",
hashFunction: "xxhash64",
},
},
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.(scss|css)$/i,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
alias: {
"@": path.resolve(__dirname, "src/"),
"@utils": path.resolve(__dirname, "src/shared/utils/"),
},
{
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
exclude: /node_modules/, // ignore node_modules
loader: "babel-loader",
},
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.(scss|css)$/i,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
},
{
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
exclude: /node_modules/, // ignore node_modules
loader: "babel-loader",
},
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
],
},
plugins: [
new webpack.DefinePlugin({
"process.env.COMMIT_HASH": `"${env.COMMIT_HASH}"`,
"process.env.NODE_ENV": `"${mode}"`,
}),
new MiniCssExtractPlugin({
filename: "styles/styles.css",
}),
new CopyPlugin({
patterns: [{ from: "./src/assets", to: "./assets" }],
}),
new webpack.BannerPlugin({
banner,
}),
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "styles/styles.css",
}),
new CopyPlugin({
patterns: [{ from: "./src/assets", to: "./assets" }],
}),
new webpack.BannerPlugin({
banner,
}),
],
};
};
}
const createServerConfig = (_env, mode) => {
const createServerConfig = (env, mode) => {
const base = getBase(env, mode);
const config = merge({}, base, {
mode,
entry: "./src/server/index.tsx",
@ -90,22 +97,20 @@ const createServerConfig = (_env, mode) => {
return config;
};
const createClientConfig = (_env, mode) => {
const createClientConfig = (env, mode) => {
const base = getBase(env, mode);
const config = merge({}, base, {
mode,
entry: "./src/client/index.tsx",
output: {
filename: "js/client.js",
publicPath: "/static/",
publicPath: `/static/${env.COMMIT_HASH}/`,
},
plugins: [
...base.plugins,
new ServiceWorkerPlugin({
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
workbox: {
modifyURLPrefix: {
"/": "/static/",
},
cacheId: "lemmy",
include: [/(assets|styles|js)\/.+\..+$/g],
inlineWorkboxRuntime: true,

2408
yarn.lock

File diff suppressed because it is too large Load diff