diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx
index 06039e0c34..e2a850823d 100644
--- a/app/actions/definitions/collections.tsx
+++ b/app/actions/definitions/collections.tsx
@@ -22,7 +22,6 @@ import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
-import SharePopover from "~/components/Sharing/Collection/SharePopover";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import {
createAction,
@@ -37,10 +36,14 @@ import {
searchPath,
} from "~/utils/routeHelpers";
import ExportDialog from "~/components/ExportDialog";
+import lazyWithRetry from "~/utils/lazyWithRetry";
const ColorCollectionIcon = ({ collection }: { collection: Collection }) => (
);
+const SharePopover = lazyWithRetry(
+ () => import("~/components/Sharing/Collection/SharePopover")
+);
export const openCollection = createAction({
name: ({ t }) => t("Open collection"),
diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx
index 9bdb5dae8d..eb4b5e08dd 100644
--- a/app/actions/definitions/documents.tsx
+++ b/app/actions/definitions/documents.tsx
@@ -50,7 +50,6 @@ import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInT
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DocumentCopy from "~/components/DocumentCopy";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
-import SharePopover from "~/components/Sharing/Document";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import {
@@ -82,7 +81,14 @@ import {
import capitalize from "lodash/capitalize";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { ActionV2, ActionV2Group, ActionV2Separator } from "~/types";
-import Insights from "~/scenes/Document/components/Insights";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const Insights = lazyWithRetry(
+ () => import("~/scenes/Document/components/Insights")
+);
+const SharePopover = lazyWithRetry(
+ () => import("~/components/Sharing/Document/SharePopover")
+);
export const openDocument = createAction({
name: ({ t }) => t("Open document"),
@@ -593,12 +599,15 @@ export const copyDocumentAsMarkdown = createActionV2({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
- perform: ({ stores, activeDocumentId, t }) => {
+ perform: async ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
- copy(document.toMarkdown());
+ const { ProsemirrorHelper } = await import(
+ "~/models/helpers/ProsemirrorHelper"
+ );
+ copy(ProsemirrorHelper.toMarkdown(document));
toast.success(t("Markdown copied to clipboard"));
}
},
@@ -612,12 +621,15 @@ export const copyDocumentAsPlainText = createActionV2({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
- perform: ({ stores, activeDocumentId, t }) => {
+ perform: async ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
- copy(document.toPlainText());
+ const { ProsemirrorHelper } = await import(
+ "~/models/helpers/ProsemirrorHelper"
+ );
+ copy(ProsemirrorHelper.toPlainText(document));
toast.success(t("Text copied to clipboard"));
}
},
diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx
index 2289b74242..5cd459dcac 100644
--- a/app/components/AuthenticatedLayout.tsx
+++ b/app/components/AuthenticatedLayout.tsx
@@ -13,7 +13,6 @@ import ErrorSuspended from "~/scenes/Errors/ErrorSuspended";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Sidebar from "~/components/Sidebar";
-import SettingsSidebar from "~/components/Sidebar/Settings";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { usePostLoginPath } from "~/hooks/useLastVisitedPath";
import usePolicy from "~/hooks/usePolicy";
@@ -30,6 +29,7 @@ import {
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
import { PortalContext } from "./Portal";
+import CommandBar from "./CommandBar";
const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments")
@@ -37,8 +37,9 @@ const DocumentComments = lazyWithRetry(
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
);
-
-const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
+const SettingsSidebar = lazyWithRetry(
+ () => import("~/components/Sidebar/Settings")
+);
type Props = {
children?: React.ReactNode;
@@ -130,9 +131,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
{children}
-
-
-
+
diff --git a/app/components/Branding.tsx b/app/components/Branding.tsx
index b7f70dca06..96f03fa74a 100644
--- a/app/components/Branding.tsx
+++ b/app/components/Branding.tsx
@@ -1,3 +1,4 @@
+import * as React from "react";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import env from "~/env";
@@ -44,4 +45,4 @@ const Link = styled.a`
}
`;
-export default Branding;
+export default React.memo(Branding);
diff --git a/app/components/Dialogs.tsx b/app/components/Dialogs.tsx
index b136566ca5..8306906312 100644
--- a/app/components/Dialogs.tsx
+++ b/app/components/Dialogs.tsx
@@ -1,7 +1,10 @@
import { observer } from "mobx-react";
-import Guide from "~/components/Guide";
-import Modal from "~/components/Modal";
+import { Suspense } from "react";
import useStores from "~/hooks/useStores";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const Guide = lazyWithRetry(() => import("~/components/Guide"));
+const Modal = lazyWithRetry(() => import("~/components/Modal"));
function Dialogs() {
const { dialogs } = useStores();
@@ -9,7 +12,7 @@ function Dialogs() {
const modals = [...modalStack];
return (
- <>
+
{guide ? (
))}
- >
+
);
}
diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx
index ac470336ea..ec904c4dd5 100644
--- a/app/components/DocumentCard.tsx
+++ b/app/components/DocumentCard.tsx
@@ -3,8 +3,8 @@ import { CSS } from "@dnd-kit/utilities";
import { subDays } from "date-fns";
import { m } from "framer-motion";
import { observer } from "mobx-react";
-import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons";
-import { useRef, useCallback, useMemo } from "react";
+import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons";
+import { useRef, useCallback, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { useTheme } from "styled-components";
@@ -19,10 +19,12 @@ import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
-import { useTextStats } from "~/hooks/useTextStats";
import CollectionIcon from "./Icons/CollectionIcon";
import Text from "./Text";
import Tooltip from "./Tooltip";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const ReadingTime = lazyWithRetry(() => import("./ReadingTime"));
type Props = {
/** The pin record */
@@ -76,6 +78,13 @@ function DocumentCard(props: Props) {
const isRecentlyUpdated =
new Date(document.updatedAt) > subDays(new Date(), 7);
+ const updatedAt = (
+ <>
+
+
+ >
+ );
+
return (
{isRecentlyUpdated ? (
- <>
-
-
- >
+ updatedAt
) : (
-
+
+
+
)}
@@ -177,21 +185,6 @@ function DocumentCard(props: Props) {
);
}
-const ReadingTime = ({ document }: { document: Document }) => {
- const { t } = useTranslation();
- const markdown = useMemo(() => document.toMarkdown(), [document]);
- const stats = useTextStats(markdown);
-
- return (
- <>
-
- {t(`{{ minutes }}m read`, {
- minutes: stats.total.readingTime,
- })}
- >
- );
-};
-
const DocumentSquircle = ({
icon,
color,
diff --git a/app/components/DocumentTasks.tsx b/app/components/DocumentTasks.tsx
index a9463c2fc1..5b16d19b3b 100644
--- a/app/components/DocumentTasks.tsx
+++ b/app/components/DocumentTasks.tsx
@@ -39,6 +39,7 @@ function DocumentTasks({ document }: Props) {
const done = completed === total;
const previousDone = usePrevious(done);
const message = getMessage(t, total, completed);
+
return (
<>
{completed === total ? (
diff --git a/app/components/Notifications/NotificationListItem.tsx b/app/components/Notifications/NotificationListItem.tsx
index 53724b1e08..2641436d73 100644
--- a/app/components/Notifications/NotificationListItem.tsx
+++ b/app/components/Notifications/NotificationListItem.tsx
@@ -6,13 +6,17 @@ import { Link } from "react-router-dom";
import styled from "styled-components";
import { s, hover, truncateMultiline } from "@shared/styles";
import Notification from "~/models/Notification";
-import CommentEditor from "~/scenes/Document/components/CommentEditor";
import useStores from "~/hooks/useStores";
import { Avatar, AvatarSize, AvatarVariant } from "../Avatar";
import Flex from "../Flex";
import Text from "../Text";
import Time from "../Time";
import { UnreadBadge } from "../UnreadBadge";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const CommentEditor = lazyWithRetry(
+ () => import("~/scenes/Document/components/CommentEditor")
+);
type Props = {
notification: Notification;
diff --git a/app/components/Notifications/NotificationsPopover.tsx b/app/components/Notifications/NotificationsPopover.tsx
index 7a9abacd5f..ec3104a2cc 100644
--- a/app/components/Notifications/NotificationsPopover.tsx
+++ b/app/components/Notifications/NotificationsPopover.tsx
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
-import * as React from "react";
+import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Popover,
@@ -7,7 +7,9 @@ import {
PopoverContent,
} from "~/components/primitives/Popover";
import useStores from "~/hooks/useStores";
-import Notifications from "./Notifications";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const Notifications = lazyWithRetry(() => import("./Notifications"));
type Props = {
children?: React.ReactNode;
@@ -16,18 +18,18 @@ type Props = {
const NotificationsPopover: React.FC = ({ children }: Props) => {
const { t } = useTranslation();
const { notifications } = useStores();
- const [open, setOpen] = React.useState(false);
- const scrollableRef = React.useRef(null);
+ const [open, setOpen] = useState(false);
+ const scrollableRef = useRef(null);
- React.useEffect(() => {
+ useEffect(() => {
void notifications.fetchPage({ archived: false });
}, [notifications]);
- const handleRequestClose = React.useCallback(() => {
+ const handleRequestClose = useCallback(() => {
setOpen(false);
}, []);
- const handleAutoFocus = React.useCallback((event: Event) => {
+ const handleAutoFocus = useCallback((event: Event) => {
// Prevent focus from moving to the popover content
event.preventDefault();
@@ -48,10 +50,12 @@ const NotificationsPopover: React.FC = ({ children }: Props) => {
onOpenAutoFocus={handleAutoFocus}
shrink
>
-
+
+
+
);
diff --git a/app/components/ReadingTime.tsx b/app/components/ReadingTime.tsx
new file mode 100644
index 0000000000..034204c071
--- /dev/null
+++ b/app/components/ReadingTime.tsx
@@ -0,0 +1,26 @@
+import { EyeIcon } from "outline-icons";
+import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useTextStats } from "~/hooks/useTextStats";
+import type Document from "~/models/Document";
+import { ProsemirrorHelper } from "~/models/helpers/ProsemirrorHelper";
+
+const ReadingTime = ({ document }: { document: Document }) => {
+ const { t } = useTranslation();
+ const markdown = useMemo(
+ () => ProsemirrorHelper.toMarkdown(document),
+ [document]
+ );
+ const stats = useTextStats(markdown);
+
+ return (
+ <>
+
+ {t(`{{ minutes }}m read`, {
+ minutes: stats.total.readingTime,
+ })}
+ >
+ );
+};
+
+export default ReadingTime;
diff --git a/app/components/Theme.tsx b/app/components/Theme.tsx
index 5f3d615837..3b2d215b12 100644
--- a/app/components/Theme.tsx
+++ b/app/components/Theme.tsx
@@ -5,7 +5,6 @@ import GlobalStyles from "@shared/styles/globals";
import { TeamPreference, UserPreference } from "@shared/types";
import useBuildTheme from "~/hooks/useBuildTheme";
import useStores from "~/hooks/useStores";
-import { TooltipStyles } from "./Tooltip";
type Props = {
children?: React.ReactNode;
@@ -30,7 +29,6 @@ const Theme: React.FC = ({ children }: Props) => {
return (
<>
-
import("~/scenes/Settings/Export"));
const Features = lazy(() => import("~/scenes/Settings/Features"));
const Groups = lazy(() => import("~/scenes/Settings/Groups"));
const Import = lazy(() => import("~/scenes/Settings/Import"));
+const Integrations = lazy(() => import("~/scenes/Settings/Integrations"));
const Members = lazy(() => import("~/scenes/Settings/Members"));
const Notifications = lazy(() => import("~/scenes/Settings/Notifications"));
const Preferences = lazy(() => import("~/scenes/Settings/Preferences"));
@@ -211,7 +211,8 @@ const useSettingsConfig = () => {
{
name: `${t("Install")}…`,
path: settingsPath("integrations"),
- component: Integrations,
+ component: Integrations.Component,
+ preload: Integrations.preload,
enabled: can.update,
group: t("Integrations"),
icon: PlusIcon,
diff --git a/app/index.tsx b/app/index.tsx
index ee6c0a9635..b4f8394a12 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -55,11 +55,11 @@ if (element) {
-
-
-
-
-
+
+
+
+
+
@@ -69,11 +69,11 @@ if (element) {
-
-
-
-
-
+
+
+
+
+
diff --git a/app/models/Document.ts b/app/models/Document.ts
index e8ee8d6abd..8f04e7e2e7 100644
--- a/app/models/Document.ts
+++ b/app/models/Document.ts
@@ -3,9 +3,6 @@ import i18n, { t } from "i18next";
import capitalize from "lodash/capitalize";
import floor from "lodash/floor";
import { action, autorun, computed, observable, set } from "mobx";
-import { Node, Schema } from "prosemirror-model";
-import ExtensionManager from "@shared/editor/lib/ExtensionManager";
-import { richExtensions, withComments } from "@shared/editor/nodes";
import type {
JSONObject,
NavigationNode,
@@ -17,7 +14,6 @@ import {
NavigationNodeType,
NotificationEventType,
} from "@shared/types";
-import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import Storage from "@shared/utils/Storage";
import { isRTL } from "@shared/utils/rtl";
import slugify from "@shared/utils/slugify";
@@ -687,47 +683,6 @@ export default class Document extends ArchivableModel implements Searchable {
);
}
- /**
- * Returns the markdown representation of the document derived from the ProseMirror data.
- *
- * @returns The markdown representation of the document as a string.
- */
- toMarkdown = () => {
- const extensionManager = new ExtensionManager(withComments(richExtensions));
- const serializer = extensionManager.serializer();
- const schema = new Schema({
- nodes: extensionManager.nodes,
- marks: extensionManager.marks,
- });
-
- const doc = Node.fromJSON(
- schema,
- ProsemirrorHelper.attachmentsToAbsoluteUrls(this.data)
- );
-
- const markdown = serializer.serialize(doc, {
- softBreak: true,
- });
- return markdown;
- };
-
- /**
- * Returns the plain text representation of the document derived from the ProseMirror data.
- *
- * @returns The plain text representation of the document as a string.
- */
- toPlainText = () => {
- const extensionManager = new ExtensionManager(withComments(richExtensions));
- const schema = new Schema({
- nodes: extensionManager.nodes,
- marks: extensionManager.marks,
- });
- const text = ProsemirrorHelper.toPlainText(
- Node.fromJSON(schema, this.data)
- );
- return text;
- };
-
download = (contentType: ExportContentType) =>
client.post(
`/documents.export`,
diff --git a/app/models/helpers/ProsemirrorHelper.ts b/app/models/helpers/ProsemirrorHelper.ts
new file mode 100644
index 0000000000..75f3964205
--- /dev/null
+++ b/app/models/helpers/ProsemirrorHelper.ts
@@ -0,0 +1,49 @@
+import ExtensionManager from "@shared/editor/lib/ExtensionManager";
+import { richExtensions, withComments } from "@shared/editor/nodes";
+import { ProsemirrorHelper as SharedProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
+import type Document from "../Document";
+import { Schema } from "prosemirror-model";
+import { Node } from "prosemirror-model";
+
+export class ProsemirrorHelper {
+ /**
+ * Returns the markdown representation of the document derived from the ProseMirror data.
+ *
+ * @returns The markdown representation of the document as a string.
+ */
+ static toMarkdown = (document: Document) => {
+ const extensionManager = new ExtensionManager(withComments(richExtensions));
+ const serializer = extensionManager.serializer();
+ const schema = new Schema({
+ nodes: extensionManager.nodes,
+ marks: extensionManager.marks,
+ });
+
+ const doc = Node.fromJSON(
+ schema,
+ SharedProsemirrorHelper.attachmentsToAbsoluteUrls(document.data)
+ );
+
+ const markdown = serializer.serialize(doc, {
+ softBreak: true,
+ });
+ return markdown;
+ };
+
+ /**
+ * Returns the plain text representation of the document derived from the ProseMirror data.
+ *
+ * @returns The plain text representation of the document as a string.
+ */
+ static toPlainText = (document: Document) => {
+ const extensionManager = new ExtensionManager(withComments(richExtensions));
+ const schema = new Schema({
+ nodes: extensionManager.nodes,
+ marks: extensionManager.marks,
+ });
+ const text = SharedProsemirrorHelper.toPlainText(
+ Node.fromJSON(schema, document.data)
+ );
+ return text;
+ };
+}
diff --git a/app/scenes/Collection/components/ShareButton.tsx b/app/scenes/Collection/components/ShareButton.tsx
index 770066f852..9aa5abf383 100644
--- a/app/scenes/Collection/components/ShareButton.tsx
+++ b/app/scenes/Collection/components/ShareButton.tsx
@@ -1,10 +1,9 @@
import { observer } from "mobx-react";
import { GlobeIcon, PadlockIcon } from "outline-icons";
-import { useCallback, useState } from "react";
+import { Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
-import SharePopover from "~/components/Sharing/Collection/SharePopover";
import {
Popover,
PopoverTrigger,
@@ -13,6 +12,11 @@ import {
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const SharePopover = lazyWithRetry(
+ () => import("~/components/Sharing/Collection/SharePopover")
+);
type Props = {
/** Collection being shared */
@@ -56,11 +60,13 @@ function ShareButton({ collection }: Props) {
side="bottom"
align="end"
>
-
+
+
+
);
diff --git a/app/scenes/Collection/index.tsx b/app/scenes/Collection/index.tsx
index 55870c294f..64cab2847a 100644
--- a/app/scenes/Collection/index.tsx
+++ b/app/scenes/Collection/index.tsx
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
-import { lazy, useState, useCallback, useEffect, Suspense } from "react";
+import { useState, useCallback, useEffect, Suspense } from "react";
import { useTranslation } from "react-i18next";
import {
useParams,
@@ -47,10 +47,12 @@ import Empty from "./components/Empty";
import MembershipPreview from "./components/MembershipPreview";
import Notices from "./components/Notices";
import Overview from "./components/Overview";
-import ShareButton from "./components/ShareButton";
import first from "lodash/first";
+import lazyWithRetry from "~/utils/lazyWithRetry";
-const IconPicker = lazy(() => import("~/components/IconPicker"));
+const IconPicker = lazyWithRetry(() => import("~/components/IconPicker"));
+
+const ShareButton = lazyWithRetry(() => import("./components/ShareButton"));
enum CollectionPath {
Overview = "overview",
diff --git a/app/scenes/Document/components/CommentForm.tsx b/app/scenes/Document/components/CommentForm.tsx
index aa86bb8d62..8e053456c4 100644
--- a/app/scenes/Document/components/CommentForm.tsx
+++ b/app/scenes/Document/components/CommentForm.tsx
@@ -22,9 +22,11 @@ import type { Editor as SharedEditor } from "~/editor";
import useCurrentUser from "~/hooks/useCurrentUser";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import useStores from "~/hooks/useStores";
-import CommentEditor from "./CommentEditor";
import { Bubble } from "./CommentThreadItem";
import { HighlightedText } from "./HighlightText";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const CommentEditor = lazyWithRetry(() => import("./CommentEditor"));
type Props = {
/** Callback when the form is submitted. */
diff --git a/app/scenes/Document/components/Contents.tsx b/app/scenes/Document/components/Contents.tsx
index 0a165e69f0..7b699b5aaa 100644
--- a/app/scenes/Document/components/Contents.tsx
+++ b/app/scenes/Document/components/Contents.tsx
@@ -37,7 +37,9 @@ function Contents() {
}
}
- setActiveSlug(activeId);
+ if (activeSlug !== activeId) {
+ setActiveSlug(activeId);
+ }
}, [scrollPosition, headings]);
// calculate the minimum heading level and adjust all the headings to make
diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx
index 2233580a61..ad797be969 100644
--- a/app/scenes/Document/components/Document.tsx
+++ b/app/scenes/Document/components/Document.tsx
@@ -27,16 +27,13 @@ import {
} from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { TextHelper } from "@shared/utils/TextHelper";
-import { parseDomain } from "@shared/utils/domains";
import { determineIconType } from "@shared/utils/icon";
import { isModKey } from "@shared/utils/keyboard";
import RootStore from "~/stores/RootStore";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
-import ConnectionStatus from "~/scenes/Document/components/ConnectionStatus";
import DocumentMove from "~/scenes/DocumentMove";
import DocumentPublish from "~/scenes/DocumentPublish";
-import Branding from "~/components/Branding";
import ErrorBoundary from "~/components/ErrorBoundary";
import LoadingIndicator from "~/components/LoadingIndicator";
import PageTitle from "~/components/PageTitle";
@@ -57,13 +54,11 @@ import Container from "./Container";
import Contents from "./Contents";
import Editor from "./Editor";
import Header from "./Header";
-import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
import { MeasuredContainer } from "./MeasuredContainer";
import Notices from "./Notices";
import PublicReferences from "./PublicReferences";
import References from "./References";
import RevisionViewer from "./RevisionViewer";
-import { SizeWarning } from "./SizeWarning";
const AUTOSAVE_DELAY = 3000;
@@ -433,6 +428,7 @@ class DocumentScene extends React.Component {
render() {
const {
+ children,
document,
revision,
readOnly,
@@ -633,19 +629,8 @@ class DocumentScene extends React.Component {
)}
- {isShare &&
- !parseDomain(window.location.origin).custom &&
- !auth.user && (
-
- )}
+ {children}
- {!isShare && (
-
- )}
);
@@ -754,16 +739,6 @@ const RevisionContainer = styled.div`
`}
`;
-const Footer = styled.div`
- position: fixed;
- bottom: 12px;
- right: 20px;
- text-align: right;
- display: flex;
- justify-content: flex-end;
- gap: 20px;
-`;
-
const Background = styled(Container)`
position: relative;
background: ${s("background")};
diff --git a/app/scenes/Document/components/DocumentTitle.tsx b/app/scenes/Document/components/DocumentTitle.tsx
index dd94bc7541..df8c3975c3 100644
--- a/app/scenes/Document/components/DocumentTitle.tsx
+++ b/app/scenes/Document/components/DocumentTitle.tsx
@@ -24,8 +24,9 @@ import { PopoverButton } from "~/components/IconPicker/components/PopoverButton"
import useBoolean from "~/hooks/useBoolean";
import usePolicy from "~/hooks/usePolicy";
import { useTranslation } from "react-i18next";
+import lazyWithRetry from "~/utils/lazyWithRetry";
-const IconPicker = React.lazy(() => import("~/components/IconPicker"));
+const IconPicker = lazyWithRetry(() => import("~/components/IconPicker"));
type Props = {
/** ID of the associated document */
diff --git a/app/scenes/Document/components/Footer.tsx b/app/scenes/Document/components/Footer.tsx
new file mode 100644
index 0000000000..f34ac40d83
--- /dev/null
+++ b/app/scenes/Document/components/Footer.tsx
@@ -0,0 +1,27 @@
+import styled from "styled-components";
+import type Document from "~/models/Document";
+import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
+import ConnectionStatus from "./ConnectionStatus";
+import { SizeWarning } from "./SizeWarning";
+
+type Props = {
+ document: Document;
+};
+
+export const Footer = ({ document }: Props) => (
+
+
+
+
+
+);
+
+const FooterWrapper = styled.div`
+ position: fixed;
+ bottom: 12px;
+ right: 20px;
+ text-align: right;
+ display: flex;
+ justify-content: flex-end;
+ gap: 20px;
+`;
diff --git a/app/scenes/Document/components/Insights.tsx b/app/scenes/Document/components/Insights.tsx
index 6ed581cffb..07ed27c5a1 100644
--- a/app/scenes/Document/components/Insights.tsx
+++ b/app/scenes/Document/components/Insights.tsx
@@ -14,6 +14,7 @@ import useTextSelection from "~/hooks/useTextSelection";
import { useTextStats } from "~/hooks/useTextStats";
import type Document from "~/models/Document";
import { useFormatNumber } from "~/hooks/useFormatNumber";
+import { ProsemirrorHelper } from "~/models/helpers/ProsemirrorHelper";
type Props = {
document: Document;
@@ -22,7 +23,7 @@ type Props = {
function Insights({ document }: Props) {
const { t } = useTranslation();
const selectedText = useTextSelection();
- const text = document.toPlainText();
+ const text = ProsemirrorHelper.toPlainText(document);
const stats = useTextStats(text ?? "", selectedText);
const formatNumber = useFormatNumber();
diff --git a/app/scenes/Document/components/ShareButton.tsx b/app/scenes/Document/components/ShareButton.tsx
index 8d523fb0d1..ed6632d7dc 100644
--- a/app/scenes/Document/components/ShareButton.tsx
+++ b/app/scenes/Document/components/ShareButton.tsx
@@ -1,10 +1,9 @@
import { observer } from "mobx-react";
import { GlobeIcon } from "outline-icons";
-import { useCallback, useState } from "react";
+import { Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import Document from "~/models/Document";
import Button from "~/components/Button";
-import SharePopover from "~/components/Sharing/Document";
import {
Popover,
PopoverTrigger,
@@ -12,6 +11,11 @@ import {
} from "~/components/primitives/Popover";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const SharePopover = lazyWithRetry(
+ () => import("~/components/Sharing/Document")
+);
type Props = {
/** Document being shared */
@@ -50,11 +54,13 @@ function ShareButton({ document }: Props) {
side="bottom"
align="end"
>
-
+
+
+
);
diff --git a/app/scenes/Document/components/SizeWarning.tsx b/app/scenes/Document/components/SizeWarning.tsx
index 99224904d1..f3b8570bfe 100644
--- a/app/scenes/Document/components/SizeWarning.tsx
+++ b/app/scenes/Document/components/SizeWarning.tsx
@@ -7,6 +7,7 @@ import type Document from "~/models/Document";
import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
+import { ProsemirrorHelper } from "~/models/helpers/ProsemirrorHelper";
type Props = {
document: Document;
@@ -14,7 +15,7 @@ type Props = {
export const SizeWarning = ({ document }: Props) => {
const { t } = useTranslation();
- const length = document.toPlainText().length;
+ const length = ProsemirrorHelper.toPlainText(document).length;
if (length < DocumentValidation.maxRecommendedLength) {
return null;
diff --git a/app/scenes/Document/index.tsx b/app/scenes/Document/index.tsx
index 8a5e50054d..7e1287878d 100644
--- a/app/scenes/Document/index.tsx
+++ b/app/scenes/Document/index.tsx
@@ -6,6 +6,7 @@ import { useLastVisitedPath } from "~/hooks/useLastVisitedPath";
import useStores from "~/hooks/useStores";
import DataLoader from "./components/DataLoader";
import Document from "./components/Document";
+import { Footer } from "./components/Footer";
type Params = {
documentSlug: string;
@@ -65,7 +66,11 @@ export default function DocumentScene(props: Props) {
history={props.history}
location={props.location}
>
- {(rest) => }
+ {(rest) => (
+
+
+
+ )}
);
}
diff --git a/app/scenes/Login/Login.tsx b/app/scenes/Login/Login.tsx
index 0e5eedadb9..e0e38d61d3 100644
--- a/app/scenes/Login/Login.tsx
+++ b/app/scenes/Login/Login.tsx
@@ -40,8 +40,12 @@ import { BackButton } from "./components/BackButton";
import { Background } from "./components/Background";
import { Centered } from "./components/Centered";
import { Notices } from "./components/Notices";
-import WorkspaceSetup from "./components/WorkspaceSetup";
import { getRedirectUrl, navigateToSubdomain } from "./urls";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const WorkspaceSetup = lazyWithRetry(
+ () => import("./components/WorkspaceSetup")
+);
type Props = {
children?: (config?: Config) => React.ReactNode;
@@ -205,7 +209,11 @@ function Login({ children, onBack }: Props) {
const preferOTP = isPWA;
if (firstRun) {
- return ;
+ return (
+
+
+
+ );
}
if (emailLinkSentTo) {
diff --git a/app/scenes/Settings/Integrations.tsx b/app/scenes/Settings/Integrations.tsx
index 8c9fd508d7..46b6141765 100644
--- a/app/scenes/Settings/Integrations.tsx
+++ b/app/scenes/Settings/Integrations.tsx
@@ -12,8 +12,9 @@ import useStores from "~/hooks/useStores";
import { settingsPath } from "~/utils/routeHelpers";
import IntegrationCard from "./components/IntegrationCard";
import { StickyFilters } from "./components/StickyFilters";
+import { observer } from "mobx-react";
-export function Integrations() {
+function Integrations() {
const { t } = useTranslation();
const { integrations } = useStores();
const items = useSettingsConfig();
@@ -70,3 +71,5 @@ const Cards = styled(Flex)`
margin-top: 20px;
width: "100%";
`;
+
+export default observer(Integrations);
diff --git a/app/scenes/Shared/Document.tsx b/app/scenes/Shared/Document.tsx
index 21f2065101..0f3b22fb72 100644
--- a/app/scenes/Shared/Document.tsx
+++ b/app/scenes/Shared/Document.tsx
@@ -5,6 +5,9 @@ import DocumentComponent from "~/scenes/Document/components/Document";
import { useDocumentContext } from "~/components/DocumentContext";
import { useTeamContext } from "~/components/TeamContext";
import { useMemo } from "react";
+import { parseDomain } from "@shared/utils/domains";
+import useCurrentUser from "~/hooks/useCurrentUser";
+import Branding from "~/components/Branding";
type Props = {
document: DocumentModel;
@@ -14,8 +17,14 @@ type Props = {
function SharedDocument({ document, shareId, sharedTree }: Props) {
const team = useTeamContext() as PublicTeam | undefined;
+ const user = useCurrentUser({ rejectOnEmpty: false });
const { hasHeadings, setDocument } = useDocumentContext();
const abilities = useMemo(() => ({}), []);
+ const isCustomDomain = useMemo(
+ () => parseDomain(window.location.origin).custom,
+ []
+ );
+ const showBranding = !isCustomDomain && !user;
const tocPosition = hasHeadings
? (team?.tocPosition ?? TOCPosition.Left)
@@ -23,14 +32,19 @@ function SharedDocument({ document, shareId, sharedTree }: Props) {
setDocument(document);
return (
-
+ <>
+
+ {showBranding ? (
+
+ ) : null}
+ >
);
}
diff --git a/app/scenes/Shared/index.tsx b/app/scenes/Shared/index.tsx
index 952b03412f..b3fc20106b 100644
--- a/app/scenes/Shared/index.tsx
+++ b/app/scenes/Shared/index.tsx
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
-import { useCallback, useEffect } from "react";
+import { Suspense, useCallback, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useLocation, useParams } from "react-router-dom";
@@ -28,10 +28,12 @@ import isCloudHosted from "~/utils/isCloudHosted";
import { changeLanguage, detectLanguage } from "~/utils/language";
import Loading from "../Document/components/Loading";
import ErrorOffline from "../Errors/ErrorOffline";
-import Login from "../Login";
import { Collection as CollectionScene } from "./Collection";
import { Document as DocumentScene } from "./Document";
import DelayedMount from "~/components/DelayedMount";
+import lazyWithRetry from "~/utils/lazyWithRetry";
+
+const Login = lazyWithRetry(() => import("../Login"));
// Parse the canonical origin from the SSR HTML, only needs to be done once.
const canonicalUrl = document
@@ -194,21 +196,23 @@ function SharedScene() {
if (error instanceof AuthorizationError) {
setPostLoginPath(location.pathname);
return (
-
- {(config) =>
- config?.name && isCloudHosted ? (
-
- {t(
- "{{ teamName }} is using {{ appName }} to share documents, please login to continue.",
- {
- teamName: config.name,
- appName: env.APP_NAME,
- }
- )}
-
- ) : null
- }
-
+
+
+ {(config) =>
+ config?.name && isCloudHosted ? (
+
+ {t(
+ "{{ teamName }} is using {{ appName }} to share documents, please login to continue.",
+ {
+ teamName: config.name,
+ appName: env.APP_NAME,
+ }
+ )}
+
+ ) : null
+ }
+
+
);
}
return ;
diff --git a/server/models/helpers/ProsemirrorHelper.tsx b/server/models/helpers/ProsemirrorHelper.tsx
index 7bbeaf0be8..000c3a9257 100644
--- a/server/models/helpers/ProsemirrorHelper.tsx
+++ b/server/models/helpers/ProsemirrorHelper.tsx
@@ -4,7 +4,6 @@ import flatten from "lodash/flatten";
import isMatch from "lodash/isMatch";
import uniq from "lodash/uniq";
import { Node, DOMSerializer, Fragment } from "prosemirror-model";
-import * as React from "react";
import { renderToString } from "react-dom/server";
import styled, { ServerStyleSheet, ThemeProvider } from "styled-components";
import { prosemirrorToYDoc } from "y-prosemirror";
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 2ada642587..8c728feb50 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -216,7 +216,6 @@
"Deleted Collection": "Deleted Collection",
"Untitled": "Untitled",
"Unpin": "Unpin",
- "{{ minutes }}m read": "{{ minutes }}m read",
"Select a location to copy": "Select a location to copy",
"Document copied": "Document copied",
"Couldn’t copy the document, try again?": "Couldn’t copy the document, try again?",
@@ -345,6 +344,7 @@
"Reaction picker": "Reaction picker",
"Could not load reactions": "Could not load reactions",
"Reaction": "Reaction",
+ "{{ minutes }}m read": "{{ minutes }}m read",
"Revision deleted": "Revision deleted",
"Current version": "Current version",
"{{userName}} edited": "{{userName}} edited",