]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'LemmyNet:main' into added-darkly-compact-552
authordankxiaobong <137083062+dankxiaobong@users.noreply.github.com>
Fri, 23 Jun 2023 19:33:39 +0000 (21:33 +0200)
committerGitHub <noreply@github.com>
Fri, 23 Jun 2023 19:33:39 +0000 (21:33 +0200)
46 files changed:
.github/CODEOWNERS
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
.github/ISSUE_TEMPLATE/QUESTION.yml [deleted file]
.github/ISSUE_TEMPLATE/config.yml [new file with mode: 0644]
.github/ISSUE_TEMPLATE/hexbear.yml [deleted file]
.gitignore
.prettierignore
package.json
src/client/index.tsx
src/server/index.tsx
src/server/utils/create-ssr-html.tsx
src/shared/components/comment/comment-node.tsx
src/shared/components/common/moment-time.tsx
src/shared/components/common/pictrs-image.tsx
src/shared/components/common/sort-select.tsx
src/shared/components/community/community.tsx
src/shared/components/community/create-community.tsx
src/shared/components/home/home.tsx
src/shared/components/home/legal.tsx
src/shared/components/home/login.tsx
src/shared/components/home/setup.tsx
src/shared/components/home/signup.tsx
src/shared/components/modlog.tsx
src/shared/components/person/inbox.tsx
src/shared/components/person/password-change.tsx
src/shared/components/person/person-details.tsx
src/shared/components/person/profile.tsx
src/shared/components/person/settings.tsx
src/shared/components/person/verify-email.tsx
src/shared/components/post/post-listing.tsx
src/shared/config.ts
src/shared/markdown.ts
src/shared/services/FirstLoadService.ts
src/shared/services/HomeCacheService.ts [new file with mode: 0644]
src/shared/services/index.ts
src/shared/utils/app/convert-comment-sort-type.ts
src/shared/utils/app/index.ts
src/shared/utils/app/setup-date-fns.ts [new file with mode: 0644]
src/shared/utils/browser/restore-scroll-position.ts
src/shared/utils/browser/save-scroll-position.ts
src/shared/utils/helpers/format-past-date.ts [new file with mode: 0644]
src/shared/utils/helpers/index.ts
src/shared/utils/helpers/is-cake-day.ts
webpack.config.js
yarn.lock

index 76916e604aeda28ef62a8e3bbd4ab7f8d13a8e82..ee3d7a5420401eed580da61fa6ffd41ea9154255 100644 (file)
@@ -1 +1 @@
-* @dessalines @SleeplessOne1917 @alectrocute
+* @dessalines @SleeplessOne1917 @alectrocute @jsit
index a43a5a5582a3da7372b251bce481d3e080759033..a0b16005c8b26dd286a1c1adabba6b94aa1bfb59 100644 (file)
@@ -1,32 +1,29 @@
-name: "\U0001F41E Bug Report"
-description: Create a report to help us improve lemmy-ui
-title: "[Bug]: "
+name: "\U0001F41E Bug report"
+description: Report a bug to help us improve Lemmy-UI.
 labels: ["bug", "triage"]
 body:
   - type: markdown
     attributes:
       value: |
-        Found a bug? Please fill out the sections below. 👍
-        Thanks for taking the time to fill out this bug report!
-        For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
+        Thanks for taking the time to help improve Lemmy-UI by reporting a bug!
   - type: checkboxes
     attributes:
       label: Requirements
-      description: Before you create a bug report please do the following.
+      description: Before you create a bug report, please carefully check the following –
       options:
-        - label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support
+        - label: This is a bug report, and if not, please post to https://lemmy.ml/c/lemmy_support instead.
           required: true
-        - label: Did you check to see if this issue already exists?
+        - label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this issue already exists.
           required: true
-        - label: Is this only a single bug? Do not put multiple bugs in one issue.
+        - label: It's a single bug. Do not report multiple bugs in one issue.
+          required: true
+        - label: It's a frontend issue, not a backend issue; Otherwise please create an issue on the [backend repo](https://github.com/LemmyNet/lemmy) instead.
           required: true
-        - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
-          required: false
   - type: textarea
     id: summary
     attributes:
       label: Summary
-      description: A summary of the bug.
+      description: Explain the bug and upload images, screenshots or videos if possible.
     validations:
       required: true
   - type: textarea
@@ -34,12 +31,13 @@ body:
     attributes:
       label: Steps to Reproduce
       description: |
-        Describe the steps to reproduce the bug.
-        The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution.
+        In a numbered list, walk us through the steps needed to reproduce the bug.
+        The better your description is _(go 'here', click 'there'...)_, the quicker we can fix it.
       value: |
         1.
         2.
         3.
+        4.
     validations:
       required: true
   - type: textarea
@@ -47,20 +45,21 @@ body:
     attributes:
       label: Technical Details
       description: |
-        - Any browser console errors?
+        Describe your environment (OS, browser, model of smartphone, etc)
+        If relevant, also share any console errors and/or screenshots here.
     validations:
       required: true
   - type: input
     id: lemmy-ui-version
     attributes:
-      label: Version
-      description: Which Lemmy UI version do you use? Displayed in the footer.
-      placeholder: ex. 0.17.4-rc.4
+      label: Lemmy Instance Version
+      description: What's the version of the Lemmy instance where the bug can be reproduced?
+      placeholder: ex. 0.18-rc.6
     validations:
       required: true
   - type: input
     id: lemmy-instance
     attributes:
       label: Lemmy Instance URL
-      description: Which Lemmy instance do you use? The address
-      placeholder: lemmy.ml, lemmy.world, etc
+      description: What's the URL of the Lemmy instance where the bug can be reproduced?
+      placeholder: https://lemmy.ml
index 2d656819d6ee569b0855e1052e5af832271b9df9..ac7d8dc6e82b508fd06c06fb87b43c4e7d4a4cef 100644 (file)
@@ -1,54 +1,27 @@
 name: "\U0001F680 Feature request"
-description: Suggest an idea for improving Lemmy's UI
+description: Suggest an idea for Lemmy-UI.
 labels: ["enhancement"]
 body:
   - type: markdown
     attributes:
       value: |
-        Have a suggestion about Lemmy's UI?
-        For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
+        Thanks for taking the time to help improve Lemmy-UI by suggesting a feature!
   - type: checkboxes
     attributes:
       label: Requirements
-      description: Before you create a feature request please do the following.
+      description: Before you create a feature request, please carefully check the following –
       options:
-        - label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
+        - label: This is a feature request and not a bug report. Otherwise, please create a new [bug report](https://github.com/LemmyNet/lemmy-ui/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=BUG_REPORT.yml) instead.
           required: true
-        - label: Did you check to see if this issue already exists?
+        - label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this request (or a similar one) already exists.
           required: true
-        - label: Is this only a feature request? Do not put multiple feature requests in one issue.
+        - label: It's a single feature. Please don't request multiple features in one issue.
           required: true
-        - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
-          required: false
-  - type: textarea
-    id: problem
-    attributes:
-      label: Is your proposal related to a problem?
-      description: |
-        Provide a clear and concise description of what the problem is.
-        For example, "I'm always frustrated when..."
-    validations:
-      required: true
   - type: textarea
     id: solution
     attributes:
-      label: Describe the solution you'd like.
+      label: Describe the feature you'd like
       description: |
-        Provide a clear and concise description of what you want to happen.
+        Provide a clear and concise description of the feature. Explain why it's needed.
     validations:
       required: true
-  - type: textarea
-    id: alternatives
-    attributes:
-      label: Describe alternatives you've considered.
-      description: |
-        Let us know about other solutions you've tried or researched.
-    validations:
-      required: true
-  - type: textarea
-    id: context
-    attributes:
-      label: Additional context
-      description: |
-        Is there anything else you can add about the proposal?
-        You might want to link to related issues here, if you haven't already.
diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml
deleted file mode 100644 (file)
index 734937e..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-name: "? Question"
-description: General questions about Lemmy
-title: "Question: "
-labels: ["question", "triage"]
-body:
-  - type: markdown
-    attributes:
-      value: |
-        Have a question about Lemmy's UI?
-        Please check the docs first: https://join-lemmy.org/docs/en/index.html
-  - type: textarea
-    id: question
-    attributes:
-      label: Question
-      description: What's the question you have about Lemmy's UI?
-    validations:
-      required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644 (file)
index 0000000..5908570
--- /dev/null
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Question
+    url: https://lemmy.ml/c/lemmy_support
+    about: Please ask and answer general questions here.
+  - name: Technical Discussion
+    url: https://github.com/LemmyNet/lemmy-ui/discussions
+    about: Please discuss technical topics with other contributors here.
diff --git a/.github/ISSUE_TEMPLATE/hexbear.yml b/.github/ISSUE_TEMPLATE/hexbear.yml
deleted file mode 100644 (file)
index 73ef548..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-name: "Hexbear"
-description: For hexbear issues
-labels: ["hexbear", "triage"]
-body:
-  - type: textarea
-    id: question
-    attributes:
-      label: Question
-      description: What's the question you have about hexbear?
-    validations:
-      required: true
index 34454f3f03512cb58213b5a3054b04286fd41bbb..3234d3dfdb9022f547715c503bf0ad38fbfda65b 100644 (file)
@@ -27,3 +27,5 @@ package-lock.json
 
 src/shared/translations
 
+stats.json
+
index 98eb0d9b885d8e110d58cc1f7bd4da6b1aa2bd2a..004c815f9b04377fd9e27304462ec7ead6646fec 100644 (file)
@@ -1,3 +1,4 @@
 src/shared/translations
 lemmy-translations
 src/assets/css/themes/*.css
+stats.json
index 9e7a0f1b2f79b369b609f63f0bcb1b36b9e4f2e1..f01c907b5ec562e064b6abf803894266fe70ed12 100644 (file)
@@ -1,20 +1,20 @@
 {
   "name": "lemmy-ui",
-  "version": "0.18.0-rc.6",
+  "version": "0.18.0",
   "description": "An isomorphic UI for lemmy",
   "repository": "https://github.com/LemmyNet/lemmy-ui",
   "license": "AGPL-3.0",
   "author": "Dessalines <tyhou13@gmx.com>",
   "scripts": {
+    "analyze": "webpack --mode=none",
     "prebuild:dev": "yarn clean && node generate_translations.js",
     "build:dev": "webpack --mode=development",
     "prebuild:prod": "yarn clean && node generate_translations.js",
     "build:prod": "webpack --mode=production",
     "clean": "yarn run rimraf dist",
-    "dev": "yarn start",
+    "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}\"",
     "prepare": "husky install",
-    "start": "yarn build:dev --watch",
     "themes:build": "sass src/assets/css/themes/:src/assets/css/themes",
     "themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes",
     "translations:generate": "node generate_translations.js",
@@ -51,6 +51,7 @@
     "copy-webpack-plugin": "^11.0.0",
     "cross-fetch": "^3.1.5",
     "css-loader": "^6.7.3",
+    "date-fns": "^2.30.0",
     "emoji-mart": "^5.4.0",
     "emoji-short-name": "^2.0.0",
     "express": "~4.18.2",
@@ -76,7 +77,6 @@
     "markdown-it-sub": "^1.0.0",
     "markdown-it-sup": "^1.0.0",
     "mini-css-extract-plugin": "^2.7.5",
-    "moment": "^2.29.4",
     "register-service-worker": "^1.7.2",
     "run-node-webpack-plugin": "^1.3.0",
     "sanitize-html": "^2.10.0",
     "@types/markdown-it": "^12.2.3",
     "@types/markdown-it-container": "^2.0.5",
     "@types/node": "^20.1.2",
+    "@types/path-browserify": "^1.0.0",
     "@types/sanitize-html": "^2.9.0",
     "@types/serialize-javascript": "^5.0.1",
     "@types/toastify-js": "^1.11.1",
     "style-loader": "^3.3.2",
     "terser": "^5.17.3",
     "typescript": "^5.0.4",
+    "webpack-bundle-analyzer": "^4.9.0",
     "webpack-dev-server": "4.15.0"
   },
   "packageManager": "yarn@1.22.19",
index eb2bb80fe2e515221d127d6a5a2f62c179884b5a..36059f97bb39830d07692c38d5c545ce8e338a45 100644 (file)
@@ -1,4 +1,4 @@
-import { initializeSite } from "@utils/app";
+import { initializeSite, setupDateFns } from "@utils/app";
 import { hydrate } from "inferno-hydrate";
 import { Router } from "inferno-router";
 import { App } from "../shared/components/app/app";
@@ -7,16 +7,22 @@ import { HistoryService } from "../shared/services";
 import "bootstrap/js/dist/collapse";
 import "bootstrap/js/dist/dropdown";
 
-initializeSite(window.isoData.site_res);
+async function startClient() {
+  initializeSite(window.isoData.site_res);
 
-const wrapper = (
-  <Router history={HistoryService.history}>
-    <App />
-  </Router>
-);
+  await setupDateFns();
 
-const root = document.getElementById("root");
+  const wrapper = (
+    <Router history={HistoryService.history}>
+      <App />
+    </Router>
+  );
 
-if (root) {
-  hydrate(wrapper, root);
+  const root = document.getElementById("root");
+
+  if (root) {
+    hydrate(wrapper, root);
+  }
 }
+
+startClient();
index b65506b0e6c4a3709e9b7ccd2dfd12f173bfaea3..25a1be6440597abc80aa39d8b9e44e4f036fa3ed 100644 (file)
@@ -1,3 +1,4 @@
+import { setupDateFns } from "@utils/app";
 import express from "express";
 import path from "path";
 import process from "process";
@@ -31,6 +32,7 @@ server.get("/css/themelist", ThemesListHandler);
 server.get("/*", CatchAllHandler);
 
 server.listen(Number(port), hostname, () => {
+  setupDateFns();
   console.log(`http://${hostname}:${port}`);
 });
 
index ae766b3a0e73294f6e019875ab6056575903a16e..13775981b2972d97ff1561b264bbd0093b6d7ee2 100644 (file)
@@ -4,6 +4,7 @@ import serialize from "serialize-javascript";
 import sharp from "sharp";
 import { favIconPngUrl, favIconUrl } from "../../shared/config";
 import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
+import { buildThemeList } from "./build-themes-list";
 import { fetchIconPng } from "./fetch-icon-png";
 
 const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
@@ -16,6 +17,10 @@ export async function createSsrHtml(
 ) {
   const site = isoData.site_res;
 
+  const fallbackTheme = `<link rel="stylesheet" type="text/css" href="/css/themes/${
+    (await buildThemeList())[0]
+  }.css" />`;
+
   if (!appleTouchIcon) {
     appleTouchIcon = site?.site_view.site.icon
       ? `data:image/png;base64,${sharp(
@@ -68,7 +73,7 @@ export async function createSsrHtml(
     <!-- Required meta tags -->
     <meta name="Description" content="Lemmy">
     <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <link
        id="favicon"
        rel="shortcut icon"
@@ -85,7 +90,7 @@ export async function createSsrHtml(
     <link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
   
     <!-- Current theme and more -->
-    ${helmet.link.toString()}
+    ${helmet.link.toString() || fallbackTheme}
     
     </head>
   
index b558d142ce9f10646f8cfe30ebe84f68a90e1609..b4d3d662641940f98f7a49e12adc9241f8f7d24f 100644 (file)
@@ -16,6 +16,9 @@ import {
   isMod,
 } from "@utils/roles";
 import classNames from "classnames";
+import isBefore from "date-fns/isBefore";
+import parseISO from "date-fns/parseISO";
+import subMinutes from "date-fns/subMinutes";
 import { Component, InfernoNode, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import {
@@ -46,7 +49,6 @@ import {
   SaveComment,
   TransferCommunity,
 } from "lemmy-js-client";
-import moment from "moment";
 import { commentTreeMaxDepth } from "../../config";
 import {
   BanType,
@@ -1451,9 +1453,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   get isCommentNew(): boolean {
-    const now = moment.utc().subtract(10, "minutes");
-    const then = moment.utc(this.commentView.comment.published);
-    return now.isBefore(then);
+    const now = subMinutes(new Date(), 10);
+    const then = parseISO(this.commentView.comment.published);
+    return isBefore(now, then);
   }
 
   handleCommentCollapse(i: CommentNode) {
index 1857a0070c3cd133b598bc814172de2ef24f5aff..7c5693ed061e512421284a36305caf3963803892 100644 (file)
@@ -1,6 +1,7 @@
-import { capitalizeFirstLetter } from "@utils/helpers";
+import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
+import format from "date-fns/format";
+import parseISO from "date-fns/parseISO";
 import { Component } from "inferno";
-import moment from "moment";
 import { I18NextService } from "../../services";
 import { Icon } from "./icon";
 
@@ -11,22 +12,24 @@ interface MomentTimeProps {
   ignoreUpdated?: boolean;
 }
 
+function formatDate(input: string) {
+  return format(parseISO(input), "PPPPpppp");
+}
+
 export class MomentTime extends Component<MomentTimeProps, any> {
   constructor(props: any, context: any) {
     super(props, context);
-
-    moment.locale([...I18NextService.i18n.languages]);
   }
 
   createdAndModifiedTimes() {
     const updated = this.props.updated;
     let line = `${capitalizeFirstLetter(
       I18NextService.i18n.t("created")
-    )}: ${this.format(this.props.published)}`;
+    )}: ${formatDate(this.props.published)}`;
     if (updated) {
       line += `\n\n\n${capitalizeFirstLetter(
         I18NextService.i18n.t("modified")
-      )} ${this.format(updated)}`;
+      )} ${formatDate(updated)}`;
     }
     return line;
   }
@@ -39,7 +42,7 @@ export class MomentTime extends Component<MomentTimeProps, any> {
           className="moment-time font-italics pointer unselectable"
         >
           <Icon icon="edit-2" classes="icon-inline me-1" />
-          {moment.utc(this.props.updated).fromNow(!this.props.showAgo)}
+          {formatPastDate(this.props.updated)}
         </span>
       );
     } else {
@@ -47,15 +50,11 @@ export class MomentTime extends Component<MomentTimeProps, any> {
       return (
         <span
           className="moment-time pointer unselectable"
-          data-tippy-content={this.format(published)}
+          data-tippy-content={formatDate(published)}
         >
-          {moment.utc(published).fromNow(!this.props.showAgo)}
+          {formatPastDate(published)}
         </span>
       );
     }
   }
-
-  format(input: string): string {
-    return moment.utc(input).local().format("LLLL");
-  }
 }
index 7443749094970cd427bf9ea8cc30a07349d413cc..31fb12299663f27ec9df5d43dec9cf0966783d1e 100644 (file)
@@ -22,7 +22,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
 
   render() {
     return (
-      <picture className="pictrs-image d-inline-block overflow-hidden">
+      <picture>
         <source srcSet={this.src("webp")} type="image/webp" />
         <source srcSet={this.props.src} />
         <source srcSet={this.src("jpg")} type="image/jpeg" />
@@ -31,7 +31,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
           alt={this.alt()}
           title={this.alt()}
           loading="lazy"
-          className={classNames({
+          className={classNames("overflow-hidden pictrs-image", {
             "img-fluid": !this.props.icon && !this.props.iconOverlay,
             banner: this.props.banner,
             "thumbnail rounded":
index 90515d5648992452114a0af158eb5d70ecdbfc5d..4d03ab5d662fcfade85a738f5a67867a43f68f03 100644 (file)
@@ -67,6 +67,13 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
           <option disabled aria-hidden="true">
             ─────
           </option>
+          <option value={"TopHour"}>{I18NextService.i18n.t("top_hour")}</option>
+          <option value={"TopSixHour"}>
+            {I18NextService.i18n.t("top_six_hours")}
+          </option>
+          <option value={"TopTwelveHour"}>
+            {I18NextService.i18n.t("top_twelve_hours")}
+          </option>
           <option value={"TopDay"}>{I18NextService.i18n.t("top_day")}</option>
           <option value={"TopWeek"}>{I18NextService.i18n.t("top_week")}</option>
           <option value={"TopMonth"}>
index 7eefe0eb96ea0c8a2932e5b9f94d0bef371adf1f..111b47cd9d4747be7da88e94fa729413096456ec 100644 (file)
@@ -15,7 +15,6 @@ import {
   updateCommunityBlock,
   updatePersonBlock,
 } from "@utils/app";
-import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
 import {
   getPageFromString,
   getQueryParams,
@@ -229,10 +228,6 @@ export class Community extends Component<
     setupTippy();
   }
 
-  componentWillUnmount() {
-    saveScrollPosition(this.context);
-  }
-
   static async fetchInitialData({
     client,
     path,
@@ -609,7 +604,6 @@ export class Community extends Component<
       });
     }
 
-    restoreScrollPosition(this.context);
     setupTippy();
   }
 
index 2ce5af5a67cf01037872e9693b2e6290d34273bd..aad6630eb8dcf682f7c179ebd67351f65899955c 100644 (file)
@@ -4,7 +4,7 @@ import {
   CreateCommunity as CreateCommunityI,
   GetSiteResponse,
 } from "lemmy-js-client";
-import { HttpService, I18NextService } from "../../services";
+import { FirstLoadService, HttpService, I18NextService } from "../../services";
 import { HtmlTags } from "../common/html-tags";
 import { CommunityForm } from "./community-form";
 
@@ -22,6 +22,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
   constructor(props: any, context: any) {
     super(props, context);
     this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
+
+    FirstLoadService.isFirstLoad;
   }
 
   get documentTitle(): string {
index 0d91bdbcc28815b0aaa427a2bc44c64e8ed33dff..7fa942af34c24e2633349229b3f7a900ac9fedc2 100644 (file)
@@ -13,7 +13,6 @@ import {
   showLocal,
   updatePersonBlock,
 } from "@utils/app";
-import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
 import {
   getPageFromString,
   getQueryParams,
@@ -79,7 +78,12 @@ import {
   InitialFetchRequest,
 } from "../../interfaces";
 import { mdToHtml } from "../../markdown";
-import { FirstLoadService, I18NextService, UserService } from "../../services";
+import {
+  FirstLoadService,
+  HomeCacheService,
+  I18NextService,
+  UserService,
+} from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { setupTippy } from "../../tippy";
 import { toast } from "../../toast";
@@ -102,6 +106,7 @@ interface HomeState {
   showTrendingMobile: boolean;
   showSidebarMobile: boolean;
   subscribedCollapsed: boolean;
+  scrolled: boolean;
   tagline?: string;
   siteRes: GetSiteResponse;
   finished: Map<CommentId, boolean | undefined>;
@@ -218,6 +223,7 @@ export class Home extends Component<any, HomeState> {
     postsRes: { state: "empty" },
     commentsRes: { state: "empty" },
     trendingCommunitiesRes: { state: "empty" },
+    scrolled: true,
     siteRes: this.isoData.site_res,
     showSubscribedMobile: false,
     showTrendingMobile: false,
@@ -277,9 +283,15 @@ export class Home extends Component<any, HomeState> {
           ?.content,
         isIsomorphic: true,
       };
+
+      HomeCacheService.postsRes = postsRes;
     }
   }
 
+  componentWillUnmount() {
+    HomeCacheService.activate();
+  }
+
   async componentDidMount() {
     if (
       !this.state.isIsomorphic ||
@@ -293,10 +305,6 @@ export class Home extends Component<any, HomeState> {
     setupTippy();
   }
 
-  componentWillUnmount() {
-    saveScrollPosition(this.context);
-  }
-
   static async fetchInitialData({
     client,
     auth,
@@ -625,6 +633,11 @@ export class Home extends Component<any, HomeState> {
       search: getQueryString(queryParams),
     });
 
+    if (!this.state.scrolled) {
+      this.setState({ scrolled: true });
+      setTimeout(() => window.scrollTo(0, 0), 0);
+    }
+
     await this.fetchData();
   }
 
@@ -647,7 +660,9 @@ export class Home extends Component<any, HomeState> {
     const siteRes = this.state.siteRes;
 
     if (dataType === DataType.Post) {
-      switch (this.state.postsRes.state) {
+      switch (this.state.postsRes?.state) {
+        case "empty":
+          return <div style="min-height: 20000px;"></div>;
         case "loading":
           return (
             <h5>
@@ -775,17 +790,30 @@ export class Home extends Component<any, HomeState> {
     const { dataType, page, listingType, sort } = getHomeQueryParams();
 
     if (dataType === DataType.Post) {
-      this.setState({ postsRes: { state: "loading" } });
-      this.setState({
-        postsRes: await HttpService.client.getPosts({
-          page,
-          limit: fetchLimit,
-          sort,
-          saved_only: false,
-          type_: listingType,
-          auth,
-        }),
-      });
+      if (HomeCacheService.active) {
+        const { postsRes, scrollY } = HomeCacheService;
+        HomeCacheService.deactivate();
+        this.setState({ postsRes });
+        window.scrollTo({
+          left: 0,
+          top: scrollY,
+          behavior: "instant",
+        });
+      } else {
+        this.setState({ postsRes: { state: "loading" } });
+        this.setState({
+          postsRes: await HttpService.client.getPosts({
+            page,
+            limit: fetchLimit,
+            sort,
+            saved_only: false,
+            type_: listingType,
+            auth,
+          }),
+        });
+
+        HomeCacheService.postsRes = this.state.postsRes;
+      }
     } else {
       this.setState({ commentsRes: { state: "loading" } });
       this.setState({
@@ -800,7 +828,6 @@ export class Home extends Component<any, HomeState> {
       });
     }
 
-    restoreScrollPosition(this.context);
     setupTippy();
   }
 
@@ -821,23 +848,23 @@ export class Home extends Component<any, HomeState> {
   }
 
   handlePageChange(page: number) {
+    this.setState({ scrolled: false });
     this.updateUrl({ page });
-    window.scrollTo(0, 0);
   }
 
   handleSortChange(val: SortType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ sort: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   handleListingTypeChange(val: ListingType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ listingType: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
+    this.setState({ scrolled: false });
     this.updateUrl({ dataType: val, page: 1 });
-    window.scrollTo(0, 0);
   }
 
   async handleAddModToCommunity(form: AddModToCommunity) {
index 85a413ebb9d4a5edecbbde00021dd079196a12a2..750a91e911b5ef1a3540f13e987fa57914886a1a 100644 (file)
@@ -2,7 +2,7 @@ import { setIsoData } from "@utils/app";
 import { Component } from "inferno";
 import { GetSiteResponse } from "lemmy-js-client";
 import { mdToHtml } from "../../markdown";
-import { I18NextService } from "../../services";
+import { FirstLoadService, I18NextService } from "../../services";
 import { HtmlTags } from "../common/html-tags";
 
 interface LegalState {
@@ -17,6 +17,8 @@ export class Legal extends Component<any, LegalState> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    FirstLoadService.isFirstLoad;
   }
 
   get documentTitle(): string {
index 1853a82b92d3c1b096ab7265c126d50a5a40e36e..d5dd4d2949b21c16d358a1038d71e8e8ebead1ca 100644 (file)
@@ -3,7 +3,7 @@ import { isBrowser } from "@utils/browser";
 import { validEmail } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
 import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
-import { I18NextService, UserService } from "../../services";
+import { FirstLoadService, I18NextService, UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { toast } from "../../toast";
 import { HtmlTags } from "../common/html-tags";
@@ -32,6 +32,8 @@ export class Login extends Component<any, State> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    FirstLoadService.isFirstLoad;
   }
 
   componentDidMount() {
index bed1262038eb2a098724e07405c0ece852e1a520..fb4c710f3dd4c3a8dab8adf0d8450819445cadf6 100644 (file)
@@ -7,7 +7,7 @@ import {
   LoginResponse,
   Register,
 } from "lemmy-js-client";
-import { I18NextService, UserService } from "../../services";
+import { FirstLoadService, I18NextService, UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { Spinner } from "../common/icon";
 import { SiteForm } from "./site-form";
@@ -47,6 +47,8 @@ export class Setup extends Component<any, State> {
     super(props, context);
 
     this.handleCreateSite = this.handleCreateSite.bind(this);
+
+    FirstLoadService.isFirstLoad;
   }
 
   async componentDidMount() {
index a2d960dc675a5ba10f973e44e1673e9c5d73cb83..3bb7ea0e5f16367b18f24b7f1bc226aa0786c1ee 100644 (file)
@@ -14,7 +14,7 @@ import {
 } from "lemmy-js-client";
 import { joinLemmyUrl } from "../../config";
 import { mdToHtml } from "../../markdown";
-import { I18NextService, UserService } from "../../services";
+import { FirstLoadService, I18NextService, UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { toast } from "../../toast";
 import { HtmlTags } from "../common/html-tags";
@@ -84,6 +84,8 @@ export class Signup extends Component<any, State> {
     super(props, context);
 
     this.handleAnswerChange = this.handleAnswerChange.bind(this);
+
+    FirstLoadService.isFirstLoad;
   }
 
   async componentDidMount() {
index e705bac86694affe85e7f72d7dbe71ece7b0e841..f12f5b63cec8eebbd9de4411e1cce080d3b44d9b 100644 (file)
@@ -7,6 +7,7 @@ import {
 } from "@utils/app";
 import {
   debounce,
+  formatPastDate,
   getIdFromString,
   getPageFromString,
   getQueryParams,
@@ -44,7 +45,6 @@ import {
   ModlogActionType,
   Person,
 } from "lemmy-js-client";
-import moment from "moment";
 import { fetchLimit } from "../config";
 import { InitialFetchRequest } from "../interfaces";
 import { FirstLoadService, I18NextService } from "../services";
@@ -371,7 +371,7 @@ function renderModlogType({ type_, view }: ModlogType) {
           )}
           {expires && (
             <span>
-              <div>expires: {moment.utc(expires).fromNow()}</div>
+              <div>expires: {formatPastDate(expires)}</div>
             </span>
           )}
         </>
@@ -403,7 +403,7 @@ function renderModlogType({ type_, view }: ModlogType) {
           )}
           {expires && (
             <span>
-              <div>expires: {moment.utc(expires).fromNow()}</div>
+              <div>expires: {formatPastDate(expires)}</div>
             </span>
           )}
         </>
@@ -467,7 +467,7 @@ function renderModlogType({ type_, view }: ModlogType) {
           )}
           {expires && (
             <span>
-              <div>expires: {moment.utc(expires).fromNow()}</div>
+              <div>expires: {formatPastDate(expires)}</div>
             </span>
           )}
         </>
@@ -686,6 +686,10 @@ export class Modlog extends Component<
     }
   }
 
+  async componentDidMount() {
+    await this.refetch();
+  }
+
   get combined() {
     const res = this.state.res;
     const combined = res.state == "success" ? buildCombined(res.data) : [];
index 062fc01c5e7bdb70b17a16f613b9ac498f646d2d..395875be4c49b975a3c3364ee99ee7c120115d95 100644 (file)
@@ -449,7 +449,6 @@ export class Inbox extends Component<any, InboxState> {
             ]}
             viewType={CommentViewType.Flat}
             finished={this.state.finished}
-            noIndent
             markable
             showCommunity
             showContext
@@ -489,7 +488,6 @@ export class Inbox extends Component<any, InboxState> {
             ]}
             finished={this.state.finished}
             viewType={CommentViewType.Flat}
-            noIndent
             markable
             showCommunity
             showContext
@@ -567,7 +565,6 @@ export class Inbox extends Component<any, InboxState> {
               nodes={commentsToFlatNodes(replies)}
               viewType={CommentViewType.Flat}
               finished={this.state.finished}
-              noIndent
               markable
               showCommunity
               showContext
@@ -617,7 +614,6 @@ export class Inbox extends Component<any, InboxState> {
                 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
                 viewType={CommentViewType.Flat}
                 finished={this.state.finished}
-                noIndent
                 markable
                 showCommunity
                 showContext
index dd85e05b3a1961e9f91975ac74185f8db8489291..0403c553616d8dc91a6b78263720e85d7d3f13c8 100644 (file)
@@ -2,7 +2,12 @@ import { myAuth, setIsoData } from "@utils/app";
 import { capitalizeFirstLetter } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
 import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
-import { HttpService, I18NextService, UserService } from "../../services";
+import {
+  FirstLoadService,
+  HttpService,
+  I18NextService,
+  UserService,
+} from "../../services";
 import { RequestState } from "../../services/HttpService";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -30,6 +35,8 @@ export class PasswordChange extends Component<any, State> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    FirstLoadService.isFirstLoad;
   }
 
   get documentTitle(): string {
index 3771b844a18f7a0040a7d9a05bcd60c83b3a596d..b2b74b6e4ad0ea95b2b4d410923f355456befa47 100644 (file)
@@ -145,7 +145,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
             finished={this.props.finished}
             admins={this.props.admins}
             noBorder
-            noIndent
             showCommunity
             showContext
             enableDownvotes={this.props.enableDownvotes}
index d00036871a04c9ec922c93a56e5c9b6eb43fec8b..70bada821c950adf659b496dd9af95407c85ad12 100644 (file)
@@ -23,6 +23,8 @@ import { canMod, isAdmin, isBanned } from "@utils/roles";
 import type { QueryParams } from "@utils/types";
 import { RouteDataResponse } from "@utils/types";
 import classNames from "classnames";
+import format from "date-fns/format";
+import parseISO from "date-fns/parseISO";
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
@@ -70,7 +72,6 @@ import {
   SortType,
   TransferCommunity,
 } from "lemmy-js-client";
-import moment from "moment";
 import { fetchLimit, relTags } from "../../config";
 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
 import { mdToHtml } from "../../markdown";
@@ -613,10 +614,7 @@ export class Profile extends Component<
                 <Icon icon="cake" />
                 <span className="ms-2">
                   {I18NextService.i18n.t("cake_day_title")}{" "}
-                  {moment
-                    .utc(pv.person.published)
-                    .local()
-                    .format("MMM DD, YYYY")}
+                  {format(parseISO(pv.person.published), "PPP")}
                 </span>
               </div>
               {!UserService.Instance.myUserInfo && (
index dc542e72594518b49c5efd3f39c8be1431ea1722..1ef95f5a5dd02cc3e29c5d30ebdd54d1a4a2c9a3 100644 (file)
@@ -29,7 +29,7 @@ import {
   SortType,
 } from "lemmy-js-client";
 import { elementUrl, emDash, relTags } from "../../config";
-import { UserService } from "../../services";
+import { FirstLoadService, UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { I18NextService, languages } from "../../services/I18NextService";
 import { setupTippy } from "../../tippy";
@@ -170,6 +170,8 @@ export class Settings extends Component<any, SettingsState> {
     this.handleBlockPerson = this.handleBlockPerson.bind(this);
     this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
 
+    FirstLoadService.isFirstLoad;
+
     const mui = UserService.Instance.myUserInfo;
     if (mui) {
       const {
@@ -1177,7 +1179,6 @@ export class Settings extends Component<any, SettingsState> {
     });
     if (saveRes.state === "success") {
       UserService.Instance.login(saveRes.data);
-      location.reload();
       toast(I18NextService.i18n.t("saved"));
       window.scrollTo(0, 0);
     }
index 1800c3f25753fc255b3807591cde390ea34941a5..a13df2d94e8d69309626b751134cb7ff4c0d8e0a 100644 (file)
@@ -1,7 +1,7 @@
 import { setIsoData } from "@utils/app";
 import { Component } from "inferno";
 import { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client";
-import { I18NextService } from "../../services";
+import { FirstLoadService, I18NextService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
 import { toast } from "../../toast";
 import { HtmlTags } from "../common/html-tags";
@@ -22,6 +22,8 @@ export class VerifyEmail extends Component<any, State> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    FirstLoadService.isFirstLoad;
   }
 
   async verify() {
index 4d0951bb60ae4619da1347b0ead74fc2a7f71b32..d5ddc2f20e5f285d42d00bf23930120e7ac34b10 100644 (file)
@@ -239,25 +239,40 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get img() {
-    return this.imageSrc ? (
-      <>
-        <div className="offset-sm-3 my-2 d-none d-sm-block">
-          <a href={this.imageSrc} className="d-inline-block">
-            <PictrsImage src={this.imageSrc} />
-          </a>
-        </div>
-        <div className="my-2 d-block d-sm-none">
-          <a
-            className="d-inline-block"
-            onClick={linkEvent(this, this.handleImageExpandClick)}
-          >
-            <PictrsImage src={this.imageSrc} />
-          </a>
+    if (this.imageSrc) {
+      return (
+        <>
+          <div className="offset-sm-3 my-2 d-none d-sm-block">
+            <a href={this.imageSrc} className="d-inline-block">
+              <PictrsImage src={this.imageSrc} />
+            </a>
+          </div>
+          <div className="my-2 d-block d-sm-none">
+            <a
+              className="d-inline-block"
+              onClick={linkEvent(this, this.handleImageExpandClick)}
+            >
+              <PictrsImage src={this.imageSrc} />
+            </a>
+          </div>
+        </>
+      );
+    }
+
+    const { post } = this.postView;
+    const { url } = post;
+
+    if (url && isVideo(url)) {
+      return (
+        <div className="embed-responsive mt-3">
+          <video muted controls className="embed-responsive-item col-12">
+            <source src={url} type="video/mp4" />
+          </video>
         </div>
-      </>
-    ) : (
-      <></>
-    );
+      );
+    }
+
+    return <></>;
   }
 
   imgThumb(src: string) {
@@ -325,17 +340,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     } else if (url) {
       if (!this.props.hideImage && isVideo(url)) {
         return (
-          <div className="embed-responsive embed-responsive-16by9">
-            <video
-              playsInline
-              muted
-              loop
-              controls
-              className="embed-responsive-item"
-            >
-              <source src={url} type="video/mp4" />
-            </video>
-          </div>
+          <a
+            className="text-body"
+            href={url}
+            title={url}
+            rel={relTags}
+            data-tippy-content={I18NextService.i18n.t("expand_here")}
+            onClick={linkEvent(this, this.handleImageExpandClick)}
+            aria-label={I18NextService.i18n.t("expand_here")}
+          >
+            <div className="thumbnail rounded bg-light d-flex justify-content-center">
+              <Icon icon="play" classes="d-flex align-items-center" />
+            </div>
+          </a>
         );
       } else {
         return (
@@ -364,33 +381,30 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   createdLine() {
     const post_view = this.postView;
     return (
-      <ul className="list-inline mb-1 text-muted small mt-2">
-        <li className="list-inline-item">
-          <PersonListing person={post_view.creator} />
-
-          {this.creatorIsMod_ && (
-            <span className="mx-1 badge text-bg-light">
-              {I18NextService.i18n.t("mod")}
-            </span>
-          )}
-          {this.creatorIsAdmin_ && (
-            <span className="mx-1 badge text-bg-light">
-              {I18NextService.i18n.t("admin")}
-            </span>
-          )}
-          {post_view.creator.bot_account && (
-            <span className="mx-1 badge text-bg-light">
-              {I18NextService.i18n.t("bot_account").toLowerCase()}
-            </span>
-          )}
-          {this.props.showCommunity && (
-            <>
-              {" "}
-              {I18NextService.i18n.t("to")}{" "}
-              <CommunityLink community={post_view.community} />
-            </>
-          )}
-        </li>
+      <span className="small">
+        <PersonListing person={post_view.creator} />
+        {this.creatorIsMod_ && (
+          <span className="mx-1 badge text-bg-light">
+            {I18NextService.i18n.t("mod")}
+          </span>
+        )}
+        {this.creatorIsAdmin_ && (
+          <span className="mx-1 badge text-bg-light">
+            {I18NextService.i18n.t("admin")}
+          </span>
+        )}
+        {post_view.creator.bot_account && (
+          <span className="mx-1 badge text-bg-light">
+            {I18NextService.i18n.t("bot_account").toLowerCase()}
+          </span>
+        )}
+        {this.props.showCommunity && (
+          <>
+            {" "}
+            {I18NextService.i18n.t("to")}{" "}
+            <CommunityLink community={post_view.community} />
+          </>
+        )}
         {post_view.post.language_id !== 0 && (
           <span className="mx-1 badge text-bg-light">
             {
@@ -399,17 +413,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               )?.name
             }
           </span>
-        )}
-        <li className="list-inline-item">•</li>
-        <li className="list-inline-item">
-          <span>
-            <MomentTime
-              published={post_view.post.published}
-              updated={post_view.post.updated}
-            />
-          </span>
-        </li>
-      </ul>
+        )}{" "}
+        •{" "}
+        <MomentTime
+          published={post_view.post.published}
+          updated={post_view.post.updated}
+        />
+      </span>
     );
   }
 
@@ -750,10 +760,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         to={`/post/${post_view.post.id}?scrollToComments=true`}
         data-tippy-content={title}
       >
-        <span className="me-1">
-          <Icon icon="message-square" classes="me-1" inline />
-          {post_view.counts.comments}
-        </span>
+        <Icon icon="message-square" classes="me-1" inline />
+        {post_view.counts.comments}
         {this.unreadCount && (
           <span className="text-muted fst-italic">
             ({this.unreadCount} {I18NextService.i18n.t("new")})
@@ -1087,7 +1095,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     const post_view = this.postView;
     return (
       this.state.showAdvanced && (
-        <>
+        <div className="mt-3">
           {this.canMod_ && (
             <>
               {!this.creatorIsMod_ &&
@@ -1248,7 +1256,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               )}
             </>
           )}
-        </>
+        </div>
       )
     );
   }
@@ -1443,11 +1451,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  showMobilePreview() {
+  showBodyPreview() {
     const { body, id } = this.postView.post;
 
     return !this.showBody && body ? (
-      <Link className="text-body" to={`/post/${id}`}>
+      <Link className="text-body mt-2 d-block" to={`/post/${id}`}>
         <div className="md-div mb-1 preview-lines">{body}</div>
       </Link>
     ) : (
@@ -1468,7 +1476,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               {this.mobileThumbnail()}
 
               {/* Show a preview of the post body */}
-              {this.showMobilePreview()}
+              {this.showBodyPreview()}
 
               {this.commentsLine(true)}
               {this.userActionsLine()}
@@ -1490,6 +1498,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 <div className="col-12">
                   {this.postTitleLine()}
                   {this.createdLine()}
+                  {this.showBodyPreview()}
                   {this.commentsLine()}
                   {this.duplicatesLine()}
                   {this.userActionsLine()}
index 28e8ce5119a5046125e62c4f9b0e98f088926188..c56c64b0c2c3f1afce182f3598d1afebf432f1c5 100644 (file)
@@ -25,4 +25,14 @@ export const fetchLimit = 40;
 export const relTags = "noopener nofollow";
 export const emDash = "\u2014";
 
+/**
+ * Accepted formats:
+ * !community@server.com
+ * /c/community@server.com
+ * /m/community@server.com
+ * /u/username@server.com
+ */
+export const instanceLinkRegex =
+  /(\/[cmu]\/|!)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
+
 export const testHost = "0.0.0.0:8536";
index 8f4d5c238ac5bf8a71015ece3520deedc50ac399..9f1ec733f74c52236f4b32cb03f24193ebf55009 100644 (file)
@@ -14,6 +14,7 @@ import markdown_it_sub from "markdown-it-sub";
 import markdown_it_sup from "markdown-it-sup";
 import Renderer from "markdown-it/lib/renderer";
 import Token from "markdown-it/lib/token";
+import { instanceLinkRegex } from "./config";
 
 export let Tribute: any;
 
@@ -72,6 +73,75 @@ const html5EmbedConfig = {
   },
 };
 
+function localInstanceLinkParser(md: MarkdownIt) {
+  md.core.ruler.push("replace-text", state => {
+    for (let i = 0; i < state.tokens.length; i++) {
+      if (state.tokens[i].type !== "inline") {
+        continue;
+      }
+      const inlineTokens: Token[] = state.tokens[i].children || [];
+      for (let j = inlineTokens.length - 1; j >= 0; j--) {
+        if (
+          inlineTokens[j].type === "text" &&
+          new RegExp(instanceLinkRegex).test(inlineTokens[j].content)
+        ) {
+          const text = inlineTokens[j].content;
+          const matches = Array.from(text.matchAll(instanceLinkRegex));
+
+          let lastIndex = 0;
+          const newTokens: Token[] = [];
+
+          let linkClass = "community-link";
+
+          for (const match of matches) {
+            // If there is plain text before the match, add it as a separate token
+            if (match.index !== undefined && match.index > lastIndex) {
+              const textToken = new state.Token("text", "", 0);
+              textToken.content = text.slice(lastIndex, match.index);
+              newTokens.push(textToken);
+            }
+
+            let href;
+            if (match[0].startsWith("!")) {
+              href = "/c/" + match[0].substring(1);
+            } else if (match[0].startsWith("/m/")) {
+              href = "/c/" + match[0].substring(3);
+            } else {
+              href = match[0];
+              if (match[0].startsWith("/u/")) {
+                linkClass = "user-link";
+              }
+            }
+
+            const linkOpenToken = new state.Token("link_open", "a", 1);
+            linkOpenToken.attrs = [
+              ["href", href],
+              ["class", linkClass],
+            ];
+            const textToken = new state.Token("text", "", 0);
+            textToken.content = match[0];
+            const linkCloseToken = new state.Token("link_close", "a", -1);
+
+            newTokens.push(linkOpenToken, textToken, linkCloseToken);
+
+            lastIndex =
+              (match.index !== undefined ? match.index : 0) + match[0].length;
+          }
+
+          // If there is plain text after the last match, add it as a separate token
+          if (lastIndex < text.length) {
+            const textToken = new state.Token("text", "", 0);
+            textToken.content = text.slice(lastIndex);
+            newTokens.push(textToken);
+          }
+
+          inlineTokens.splice(j, 1, ...newTokens);
+        }
+      }
+    }
+  });
+}
+
 export function setupMarkdown() {
   const markdownItConfig: MarkdownIt.Options = {
     html: false,
@@ -88,7 +158,8 @@ export function setupMarkdown() {
     .use(markdown_it_sup)
     .use(markdown_it_footnote)
     .use(markdown_it_html5_embed, html5EmbedConfig)
-    .use(markdown_it_container, "spoiler", spoilerConfig);
+    .use(markdown_it_container, "spoiler", spoilerConfig)
+    .use(localInstanceLinkParser);
   // .use(markdown_it_emoji, {
   //   defs: emojiDefs,
   // });
@@ -99,6 +170,7 @@ export function setupMarkdown() {
     .use(markdown_it_footnote)
     .use(markdown_it_html5_embed, html5EmbedConfig)
     .use(markdown_it_container, "spoiler", spoilerConfig)
+    .use(localInstanceLinkParser)
     // .use(markdown_it_emoji, {
     //   defs: emojiDefs,
     // })
index b7558efff07150f9325b21f3e9832184980043d0..926e3acd6eed44e8e3021797355e78d10349b786 100644 (file)
@@ -1,3 +1,5 @@
+import { isBrowser } from "@utils/browser";
+
 export class FirstLoadService {
   #isFirstLoad: boolean;
   static #instance: FirstLoadService;
@@ -20,6 +22,6 @@ export class FirstLoadService {
   }
 
   static get isFirstLoad() {
-    return this.#Instance.isFirstLoad;
+    return !isBrowser() || this.#Instance.isFirstLoad;
   }
 }
diff --git a/src/shared/services/HomeCacheService.ts b/src/shared/services/HomeCacheService.ts
new file mode 100644 (file)
index 0000000..9f33dc4
--- /dev/null
@@ -0,0 +1,60 @@
+import { GetPostsResponse } from "lemmy-js-client";
+import { RequestState } from "./HttpService.js";
+
+/**
+ * Service to cache home post listings and restore home state when user uses the browser back buttons.
+ */
+export class HomeCacheService {
+  static #_instance: HomeCacheService;
+  historyIdx = 0;
+  scrollY = 0;
+  posts: RequestState<GetPostsResponse> = { state: "empty" };
+
+  get active() {
+    return (
+      this.historyIdx === window.history.state.idx + 1 &&
+      this.posts.state === "success"
+    );
+  }
+
+  deactivate() {
+    this.historyIdx = 0;
+  }
+
+  activate() {
+    this.scrollY = window.scrollY;
+    this.historyIdx = window.history.state.idx;
+  }
+
+  static get #Instance() {
+    return this.#_instance ?? (this.#_instance = new this());
+  }
+
+  public static get scrollY() {
+    return this.#Instance.scrollY;
+  }
+
+  public static get historyIdx() {
+    return this.#Instance.historyIdx;
+  }
+
+  public static set postsRes(posts: RequestState<GetPostsResponse>) {
+    this.#Instance.posts = posts;
+  }
+
+  public static get postsRes() {
+    return this.#Instance.posts;
+  }
+
+  public static get active() {
+    return this.#Instance.active;
+  }
+
+  public static deactivate() {
+    this.#Instance.deactivate();
+  }
+
+  public static activate() {
+    this.#Instance.activate();
+  }
+}
index 5856245ac08a02475956c97d287a72be6abded88..620293c4a4c54f3e2459771080dd0fe5f2bdea0b 100644 (file)
@@ -1,5 +1,6 @@
 export { FirstLoadService } from "./FirstLoadService";
 export { HistoryService } from "./HistoryService";
+export { HomeCacheService } from "./HomeCacheService";
 export { HttpService } from "./HttpService";
 export { I18NextService } from "./I18NextService";
 export { UserService } from "./UserService";
index 3a89a23c774a7c4fd34d9f175cf93add02da45b9..665ab46269aa99451b2a0e823829f8106a206f25 100644 (file)
@@ -5,6 +5,9 @@ export default function convertCommentSortType(
 ): CommentSortType {
   switch (sort) {
     case "TopAll":
+    case "TopHour":
+    case "TopSixHour":
+    case "TopTwelveHour":
     case "TopDay":
     case "TopWeek":
     case "TopMonth":
index cdae267786be2bb37e40caa4146615dcd842f873..9993ac7230dc18c0c4f6034d1df43df74960d151 100644 (file)
@@ -46,6 +46,7 @@ import searchCommentTree from "./search-comment-tree";
 import selectableLanguages from "./selectable-languages";
 import setIsoData from "./set-iso-data";
 import setTheme from "./set-theme";
+import setupDateFns from "./setup-date-fns";
 import showAvatars from "./show-avatars";
 import showLocal from "./show-local";
 import showScores from "./show-scores";
@@ -102,6 +103,7 @@ export {
   selectableLanguages,
   setIsoData,
   setTheme,
+  setupDateFns,
   showAvatars,
   showLocal,
   showScores,
diff --git a/src/shared/utils/app/setup-date-fns.ts b/src/shared/utils/app/setup-date-fns.ts
new file mode 100644 (file)
index 0000000..fe95720
--- /dev/null
@@ -0,0 +1,19 @@
+import setDefaultOptions from "date-fns/setDefaultOptions";
+import { I18NextService } from "../../services";
+
+export default async function () {
+  let lang = I18NextService.i18n.language;
+  if (lang === "en") {
+    lang = "en-US";
+  }
+
+  const locale = (
+    await import(
+      /* webpackExclude: /\.js\.flow$/ */
+      `date-fns/locale/${lang}`
+    )
+  ).default;
+  setDefaultOptions({
+    locale,
+  });
+}
index f1534644d9c24a0cfc78ef9e2468d0068576c06f..6986ae597f1a6086784010e71beee7dc8ad01b44 100644 (file)
@@ -1,5 +1,6 @@
 export default function restoreScrollPosition(context: any) {
   const path: string = context.router.route.location.pathname;
   const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
+
   window.scrollTo(0, y);
 }
index 48353287dccfb4c1a7928ffb47713b9e3282590e..596d5d9f529eb0f378b2a8cbc9897990f0266eb4 100644 (file)
@@ -1,5 +1,6 @@
 export default function saveScrollPosition(context: any) {
   const path: string = context.router.route.location.pathname;
   const y = window.scrollY;
+
   sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
 }
diff --git a/src/shared/utils/helpers/format-past-date.ts b/src/shared/utils/helpers/format-past-date.ts
new file mode 100644 (file)
index 0000000..78bc2a2
--- /dev/null
@@ -0,0 +1,12 @@
+import formatDistanceStrict from "date-fns/formatDistanceStrict";
+import parseISO from "date-fns/parseISO";
+
+export default function (dateString?: string) {
+  return formatDistanceStrict(
+    parseISO(dateString ?? Date.now().toString()),
+    new Date(),
+    {
+      addSuffix: true,
+    }
+  );
+}
index 36ae83fa36e50ac9a50bbb847c933072568ca8f6..3420adbc431f3aeb830cbb6a49a3473da00bc03d 100644 (file)
@@ -1,6 +1,7 @@
 import capitalizeFirstLetter from "./capitalize-first-letter";
 import debounce from "./debounce";
 import editListImmutable from "./edit-list-immutable";
+import formatPastDate from "./format-past-date";
 import futureDaysToUnixTime from "./future-days-to-unix-time";
 import getIdFromString from "./get-id-from-string";
 import getPageFromString from "./get-page-from-string";
@@ -26,6 +27,7 @@ export {
   capitalizeFirstLetter,
   debounce,
   editListImmutable,
+  formatPastDate,
   futureDaysToUnixTime,
   getIdFromString,
   getPageFromString,
index 694be17026b522dc50726800b8b1c80a19f4c7fd..f1e1c3d7f02de7a4cc28fc359bd97ba284ab0040 100644 (file)
@@ -1,33 +1,13 @@
-import moment from "moment";
-
-moment.updateLocale("en", {
-  relativeTime: {
-    future: "in %s",
-    past: "%s ago",
-    s: "<1m",
-    ss: "%ds",
-    m: "1m",
-    mm: "%dm",
-    h: "1h",
-    hh: "%dh",
-    d: "1d",
-    dd: "%dd",
-    w: "1w",
-    ww: "%dw",
-    M: "1M",
-    MM: "%dM",
-    y: "1Y",
-    yy: "%dY",
-  },
-});
+import getDayOfYear from "date-fns/getDayOfYear";
+import getYear from "date-fns/getYear";
+import parseISO from "date-fns/parseISO";
 
 export default function isCakeDay(published: string): boolean {
-  const createDate = moment.utc(published).local();
-  const currentDate = moment(new Date());
+  const createDate = parseISO(published);
+  const currentDate = new Date();
 
   return (
-    createDate.date() === currentDate.date() &&
-    createDate.month() === currentDate.month() &&
-    createDate.year() !== currentDate.year()
+    getDayOfYear(createDate) === getDayOfYear(currentDate) &&
+    getYear(createDate) !== getYear(currentDate)
   );
 }
index ba3ea60d8e984d2934f5ab57ec61559f4f6aee97..f14707a1a00ae738b92f60a9ce4731dc4795d514 100644 (file)
@@ -6,6 +6,8 @@ const CopyPlugin = require("copy-webpack-plugin");
 const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
 const merge = require("lodash/merge");
 const { ServiceWorkerPlugin } = require("service-worker-webpack");
+const BundleAnalyzerPlugin =
+  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
 const banner = `
   hash:[contentHash], chunkhash:[chunkhash], name:[name], filebase:[base], query:[query], file:[file]
   Source code: https://github.com/LemmyNet/lemmy-ui
@@ -153,11 +155,8 @@ const createClientConfig = (_env, mode) => {
     ],
   });
 
-  if (mode === "development") {
-    // config.cache = {
-    //   type: "filesystem",
-    //   name: "client",
-    // };
+  if (mode === "none") {
+    config.plugins.push(new BundleAnalyzerPlugin());
   }
 
   return config;
index f874cb53757fb616e8e0c932a3995e5ca4ead4e6..76d9355f859b44fb41e88cf7f110aaa435757b6d 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     regenerator-runtime "^0.13.11"
 
-"@babel/runtime@^7.20.7":
+"@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0":
   version "7.22.5"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
   integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
     "@babel/helper-validator-identifier" "^7.19.1"
     to-fast-properties "^2.0.0"
 
-"@discoveryjs/json-ext@^0.5.0":
+"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0":
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
   integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
     picocolors "^1.0.0"
     tslib "^2.5.0"
 
+"@polka/url@^1.0.0-next.20":
+  version "1.0.0-next.21"
+  resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
+  integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
+
 "@popperjs/core@^2.9.0", "@popperjs/core@^2.9.2":
   version "2.11.8"
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
   integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
 
+"@types/path-browserify@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.0.tgz#294ec6e88b6b0d340a3897b7120e5b393f16690e"
+  integrity sha512-XMCcyhSvxcch8b7rZAtFAaierBYdeHXVvg2iYnxOV0MCQHmPuRRmGZPFDRzPayxcGiiSL1Te9UIO+f3cuj0tfw==
+
 "@types/qs@*":
   version "6.9.7"
   resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@@ -1977,6 +1987,16 @@ acorn-jsx@^5.3.2:
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
+acorn-walk@^8.0.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+  integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
+acorn@^8.0.4:
+  version "8.9.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
+  integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==
+
 acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2:
   version "8.8.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
@@ -2714,7 +2734,7 @@ chalk@^2.0.0, chalk@^2.0.1:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.0.0, chalk@^4.0.2:
+chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
   integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -2959,6 +2979,11 @@ commander@^2.20.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
+commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
 common-tags@^1.8.0:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
@@ -3200,6 +3225,13 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
+date-fns@^2.30.0:
+  version "2.30.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+  dependencies:
+    "@babel/runtime" "^7.21.0"
+
 debug@2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3481,6 +3513,11 @@ duplexer3@^0.1.4:
   resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e"
   integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==
 
+duplexer@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
+  integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
+
 duplexify@^3.4.2, duplexify@^3.6.0:
   version "3.7.1"
   resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
@@ -4576,6 +4613,13 @@ graphemer@^1.4.0:
   resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
   integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
 
+gzip-size@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
+  integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==
+  dependencies:
+    duplexer "^0.1.2"
+
 handle-thing@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
@@ -6302,11 +6346,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
   dependencies:
     minimist "^1.2.6"
 
-moment@^2.29.4:
-  version "2.29.4"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
-  integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
-
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -6319,6 +6358,11 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     run-queue "^1.0.3"
 
+mrmime@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
+  integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -6916,6 +6960,11 @@ opencollective-postinstall@^2.0.3:
   resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
   integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
 
+opener@^1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
+  integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
+
 opener@~1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
@@ -8247,6 +8296,15 @@ simple-swizzle@^0.2.2:
   dependencies:
     is-arrayish "^0.3.1"
 
+sirv@^1.0.7:
+  version "1.0.19"
+  resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
+  integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==
+  dependencies:
+    "@polka/url" "^1.0.0-next.20"
+    mrmime "^1.0.0"
+    totalist "^1.0.0"
+
 slash@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -8913,6 +8971,11 @@ toidentifier@1.0.1:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
+totalist@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
+  integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
+
 tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -9258,6 +9321,22 @@ webidl-conversions@^4.0.2:
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
   integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
 
+webpack-bundle-analyzer@^4.9.0:
+  version "4.9.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz#fc093c4ab174fd3dcbd1c30b763f56d10141209d"
+  integrity sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==
+  dependencies:
+    "@discoveryjs/json-ext" "0.5.7"
+    acorn "^8.0.4"
+    acorn-walk "^8.0.0"
+    chalk "^4.1.0"
+    commander "^7.2.0"
+    gzip-size "^6.0.0"
+    lodash "^4.17.20"
+    opener "^1.5.2"
+    sirv "^1.0.7"
+    ws "^7.3.1"
+
 webpack-cli@^5.1.1:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.3.tgz#6b6186270efec62394f6fefeebed0872a779f345"
@@ -9710,6 +9789,11 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
     imurmurhash "^0.1.4"
     signal-exit "^3.0.2"
 
+ws@^7.3.1:
+  version "7.5.9"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
+  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+
 ws@^8.13.0:
   version "8.13.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"