Compare commits

..

16 Commits

Author SHA1 Message Date
Tom Moor bee7911bee Types cleanup 2024-11-20 19:12:43 -05:00
Tom Moor 86714a353f fix: Rare loop of storage events between tabs causing flickering UI 2024-11-20 18:51:58 -05:00
Tom Moor fd5391cbb6 Cache diff generation for email notifications (#7987)
* Cache diff generation, closes #7982

* Handle cannot acquire lock

* Refactor to guard
2024-11-20 14:45:12 -08:00
Hemachandar 6e685ee8d9 store pin location on mount (#7994) 2024-11-20 14:27:16 -08:00
Tom Moor b595a0d427 fix: No-op sending emails in self-hosted if configuration is unavailable rather than retrying
towards #7982
2024-11-19 20:45:32 -05:00
Tom Moor 1c86119065 fix: Cannot sort by role on member settings, closes #7986 2024-11-19 19:22:53 -05:00
Tom Moor c629006642 Add inline resolve action on comment threads (#7977)
* Add inline resolve action on comment threads

* perf refactor
2024-11-18 18:36:44 -08:00
Tom Moor 326f733d4c fix: Further improvements to diacritics matching in CMD+F 2024-11-18 18:04:10 -05:00
Tom Moor d4d683c046 fix: Missing space character in invite modal, related #7968 2024-11-18 17:51:49 -05:00
Tom Moor 8204ac343f chore: Upgrade Sentry/AWS 2024-11-18 17:48:36 -05:00
dependabot[bot] cae8de7c7a chore(deps): bump @octokit/auth-app from 6.1.2 to 6.1.3 (#7974)
Bumps [@octokit/auth-app](https://github.com/octokit/auth-app.js) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/octokit/auth-app.js/releases)
- [Commits](https://github.com/octokit/auth-app.js/compare/v6.1.2...v6.1.3)

---
updated-dependencies:
- dependency-name: "@octokit/auth-app"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 14:41:05 -08:00
dependabot[bot] 8efa601967 chore(deps-dev): bump @relative-ci/agent from 4.2.12 to 4.2.13 (#7975)
Bumps [@relative-ci/agent](https://github.com/relative-ci/agent) from 4.2.12 to 4.2.13.
- [Release notes](https://github.com/relative-ci/agent/releases)
- [Commits](https://github.com/relative-ci/agent/compare/v4.2.12...v4.2.13)

---
updated-dependencies:
- dependency-name: "@relative-ci/agent"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 14:40:53 -08:00
Tom Moor 86c3ea8e9d fix: Copy toolbar positioning 2024-11-17 10:00:52 -05:00
Tom Moor c222782534 Upgrade Mermaid script in exported HTML, related #7964 2024-11-17 09:51:46 -05:00
Tom Moor 19ea7ee52b Remove sourcemap generation in bundle size calc (#7966) 2024-11-16 16:57:19 -08:00
Tom Moor d1de84a07e Reduce build time (#7965)
* Test using xlarge

* wip

* wip
2024-11-16 10:45:14 -08:00
22 changed files with 1053 additions and 952 deletions
+3 -2
View File
@@ -39,6 +39,7 @@ function DocumentCard(props: Props) {
const { collections } = useStores();
const theme = useTheme();
const { document, pin, canUpdatePin, isDraggable } = props;
const pinnedToHome = React.useRef(!pin?.collectionId).current;
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -122,13 +123,13 @@ function DocumentCard(props: Props) {
<Squircle
color={
collection?.color ??
(!pin?.collectionId ? theme.slateLight : theme.slateDark)
(pinnedToHome ? theme.slateLight : theme.slateDark)
}
>
{collection?.icon &&
collection?.icon !== "letter" &&
collection?.icon !== "collection" &&
!pin?.collectionId ? (
pinnedToHome ? (
<CollectionIcon collection={collection} color="white" />
) : (
<DocumentIcon color="white" />
+4 -2
View File
@@ -47,14 +47,16 @@ export default function LanguagePrompt() {
<br />
<Link
onClick={async () => {
ui.setLanguagePromptDismissed();
ui.set({ languagePromptDismissed: true });
await user.save({ language });
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
<Link onClick={() => ui.set({ languagePromptDismissed: true })}>
{t("Dismiss")}
</Link>
</span>
</Flex>
</Wrapper>
+16 -13
View File
@@ -2,7 +2,6 @@ import { ReactionIcon } from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit";
import styled from "styled-components";
import EventBoundary from "@shared/components/EventBoundary";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
@@ -11,6 +10,7 @@ import Popover from "~/components/Popover";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = React.lazy(
() => import("~/components/IconPicker/components/EmojiPanel")
@@ -98,15 +98,22 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<PopoverButton
{...props}
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
size={size}
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
>
<ReactionIcon size={22} />
</PopoverButton>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
size={size}
>
<ReactionIcon size={22} />
</NudeButton>
</Tooltip>
)}
</PopoverDisclosure>
<Popover
@@ -151,8 +158,4 @@ const Placeholder = React.memo(
);
Placeholder.displayName = "ReactionPickerPlaceholder";
const PopoverButton = styled(NudeButton)`
border-radius: 50%;
`;
export default ReactionPicker;
+2 -2
View File
@@ -32,13 +32,13 @@ function Right({ children, border, className }: Props) {
Math.min(window.innerWidth - event.pageX, maxWidth),
minWidth
);
ui.setRightSidebarWidth(width);
ui.set({ sidebarRightWidth: width });
},
[minWidth, maxWidth, ui]
);
const handleReset = React.useCallback(() => {
ui.setRightSidebarWidth(theme.sidebarRightWidth);
ui.set({ sidebarRightWidth: theme.sidebarRightWidth });
}, [ui, theme.sidebarRightWidth]);
const handleStopDrag = React.useCallback(() => {
+12 -13
View File
@@ -46,7 +46,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isHovering, setHovering] = React.useState(false);
const [isAnimating, setAnimating] = React.useState(false);
@@ -62,13 +61,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
ui.set({
sidebarWidth: isSmallerThanCollapsePoint
? theme.sidebarCollapsedWidth
: width,
});
},
[theme, offset, minWidth, maxWidth, setWidth]
[ui, theme, offset, minWidth, maxWidth]
);
const handleStopDrag = React.useCallback(() => {
@@ -86,13 +85,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
setCollapsing(true);
ui.collapseSidebar();
} else {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setAnimating(true);
}
} else {
setWidth(width);
ui.set({ sidebarWidth: width });
}
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
}, [ui, isSmallerThanMinimum, minWidth, width]);
const handleBlur = React.useCallback(() => {
setHovering(false);
@@ -149,11 +148,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setCollapsing(false);
}, ANIMATION_MS);
}
}, [setWidth, minWidth, isCollapsing]);
}, [ui, minWidth, isCollapsing]);
React.useEffect(() => {
if (isResizing) {
@@ -174,7 +173,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
}, [isResizing, handleDrag, handleBlur, handleStopDrag]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
ui.set({ sidebarWidth: theme.sidebarWidth });
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
+8 -3
View File
@@ -248,14 +248,19 @@ export default class FindAndReplaceExtension extends Extension {
let m;
const search = this.findRegExp;
while ((m = search.exec(deburr(text)))) {
// We construct a string with the text stripped of diacritics plus the original text for
// search allowing to search for diacritics-insensitive matches easily.
while ((m = search.exec(deburr(text) + text))) {
if (m[0] === "") {
break;
}
// Reconstruct the correct match position
const i = m.index > text.length ? m.index - text.length : m.index;
this.results.push({
from: pos + m.index,
to: pos + m.index + m[0].length,
from: pos + i,
to: pos + i + m[0].length,
});
}
} catch (e) {
@@ -1,6 +1,7 @@
import { differenceInMilliseconds } from "date-fns";
import { action } from "mobx";
import { observer } from "mobx-react";
import { DoneIcon } from "outline-icons";
import { darken } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -16,10 +17,14 @@ import Comment from "~/models/Comment";
import { Avatar } from "~/components/Avatar";
import ButtonSmall from "~/components/ButtonSmall";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import ReactionList from "~/components/Reactions/ReactionList";
import ReactionPicker from "~/components/Reactions/ReactionPicker";
import Text from "~/components/Text";
import Time from "~/components/Time";
import Tooltip from "~/components/Tooltip";
import { resolveCommentFactory } from "~/actions/definitions/comments";
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import CommentMenu from "~/menus/CommentMenu";
@@ -242,11 +247,13 @@ function CommentThreadItem({
onRemoveReaction={handleRemoveReaction}
picker={
!comment.isResolved ? (
<StyledReactionPicker
<Action
as={ReactionPicker}
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
size={28}
rounded
/>
) : undefined
}
@@ -257,14 +264,20 @@ function CommentThreadItem({
<EventBoundary>
{!isEditing && (
<Actions gap={4} dir={dir}>
{firstOfThread && (
<ResolveButton onUpdate={handleUpdate} comment={comment} />
)}
{!comment.isResolved && (
<StyledReactionPicker
<Action
as={ReactionPicker}
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
rounded
/>
)}
<StyledMenu
<Action
as={CommentMenu}
comment={comment}
onEdit={setEditing}
onDelete={handleDelete}
@@ -278,6 +291,38 @@ function CommentThreadItem({
);
}
const ResolveButton = ({
comment,
onUpdate,
}: {
comment: Comment;
onUpdate: (attrs: { resolved: boolean }) => void;
}) => {
const context = useActionContext();
const { t } = useTranslation();
return (
<Tooltip
content={t("Mark as resolved")}
placement="top"
delay={500}
hideOnClick
>
<Action
as={NudeButton}
context={context}
action={resolveCommentFactory({
comment,
onResolve: () => onUpdate({ resolved: true }),
})}
rounded
>
<DoneIcon size={22} outline />
</Action>
</Tooltip>
);
};
const StyledCommentEditor = styled(CommentEditor)`
${(props) =>
!props.readOnly &&
@@ -308,25 +353,13 @@ const Body = styled.form`
border-radius: 2px;
`;
const StyledMenu = styled(CommentMenu)`
color: ${s("textSecondary")};
svg {
fill: currentColor;
opacity: 0.5;
}
&: ${hover}, &[aria-expanded= "true"] {
background: ${s("backgroundQuaternary")};
svg {
opacity: 0.75;
}
}
`;
const StyledReactionPicker = styled(ReactionPicker)`
const Action = styled.span<{ rounded?: boolean }>`
color: ${s("textSecondary")};
${(props) =>
props.rounded &&
css`
border-radius: 50%;
`}
svg {
fill: currentColor;
@@ -352,7 +385,7 @@ const Actions = styled(Flex)<{ dir?: "rtl" | "ltr" }>`
background: ${s("backgroundSecondary")};
padding-left: 4px;
&:has(${StyledReactionPicker}[aria-expanded="true"], ${StyledMenu}[aria-expanded="true"]) {
&:has(${Action}[aria-expanded="true"]) {
opacity: 1;
}
`;
+6 -2
View File
@@ -103,6 +103,10 @@ function DocumentHeader({
});
}, [onSave]);
const handleToggle = React.useCallback(() => {
ui.set({ tocVisible: !ui.tocVisible });
}, [ui]);
const context = useActionContext({
activeDocumentId: document?.id,
});
@@ -129,7 +133,7 @@ function DocumentHeader({
placement="bottom"
>
<Button
onClick={showContents ? ui.hideTableOfContents : ui.showTableOfContents}
onClick={handleToggle}
icon={<TableOfContentsIcon />}
borderOnHover
neutral
@@ -180,7 +184,7 @@ function DocumentHeader({
useKeyDown(
(event) => event.ctrlKey && event.altKey && event.key === "˙",
ui.tocVisible ? ui.hideTableOfContents : ui.showTableOfContents,
handleToggle,
{
allowInInput: true,
}
+1 -1
View File
@@ -127,7 +127,7 @@ function Invite({ onSubmit }: Props) {
<Trans>{{ collectionCount }} collections</Trans>
</strong>
</Tooltip>
.
.{" "}
</span>
) : undefined;
@@ -52,7 +52,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
),
},
{
id: "isAdmin",
id: "role",
Header: t("Role"),
accessor: "rank",
Cell: observer(({ row }: { row: { original: User } }) => (
+9 -20
View File
@@ -7,31 +7,19 @@ import { CustomTheme } from "@shared/types";
import Storage from "@shared/utils/Storage";
import { getCookieDomain, parseDomain } from "@shared/utils/domains";
import RootStore from "~/stores/RootStore";
import Policy from "~/models/Policy";
import Team from "~/models/Team";
import User from "~/models/User";
import env from "~/env";
import { setPostLoginPath } from "~/hooks/useLastVisitedPath";
import { PartialExcept } from "~/types";
import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted";
import Store from "./base/Store";
type PersistedData = {
user?: PartialExcept<User, "id">;
team?: PartialExcept<Team, "id">;
collaborationToken?: string;
availableTeams?: {
id: string;
name: string;
avatarUrl: string;
url: string;
isSignedIn: boolean;
}[];
policies?: Policy[];
};
type PersistedData = Pick<
AuthStore,
"user" | "team" | "collaborationToken" | "availableTeams" | "policies"
>;
type Provider = {
id: string;
@@ -165,9 +153,10 @@ export default class AuthStore extends Store<Team> {
/** The current team's policies */
@computed
get policies() {
return this.currentTeamId
? [this.rootStore.policies.get(this.currentTeamId)]
: [];
const policy = this.currentTeamId
? this.rootStore.policies.get(this.currentTeamId)
: undefined;
return policy ? [policy] : [];
}
/** Whether the user is signed in */
@@ -177,7 +166,7 @@ export default class AuthStore extends Store<Team> {
}
@computed
get asJson() {
get asJson(): PersistedData {
return {
user: this.user,
team: this.team,
+30 -43
View File
@@ -1,4 +1,4 @@
import { action, autorun, computed, observable } from "mobx";
import { action, computed, observable } from "mobx";
import { flushSync } from "react-dom";
import { light as defaultTheme } from "@shared/styles/theme";
import Storage from "@shared/utils/Storage";
@@ -23,15 +23,16 @@ export enum SystemTheme {
Dark = "dark",
}
type PersistedData = {
languagePromptDismissed: boolean | undefined;
theme: Theme;
sidebarCollapsed: boolean;
sidebarWidth: number;
sidebarRightWidth: number;
tocVisible: boolean | undefined;
commentsExpanded: string[];
};
type PersistedData = Pick<
UiStore,
| "languagePromptDismissed"
| "commentsExpanded"
| "theme"
| "sidebarWidth"
| "sidebarRightWidth"
| "sidebarCollapsed"
| "tocVisible"
>;
class UiStore {
// has the user seen the prompt to change the UI language and actioned it
@@ -134,10 +135,6 @@ class UiStore {
this.tocVisible = newData.tocVisible;
}
});
autorun(() => {
Storage.set(UI_STORE, this.asJson);
});
}
@action
@@ -147,12 +144,7 @@ class UiStore {
this.theme = theme;
});
});
Storage.set("theme", this.theme);
};
@action
setLanguagePromptDismissed = () => {
this.languagePromptDismissed = true;
this.persist();
};
@action
@@ -205,25 +197,24 @@ class UiStore {
this.activeCollectionId = undefined;
};
@action
setSidebarWidth = (width: number): void => {
this.sidebarWidth = width;
};
@action
setRightSidebarWidth = (width: number): void => {
this.sidebarRightWidth = width;
};
@action
collapseSidebar = () => {
this.sidebarCollapsed = true;
this.set({ sidebarCollapsed: true });
};
@action
expandSidebar = () => {
sidebarHidden = false;
this.sidebarCollapsed = false;
this.set({ sidebarCollapsed: false });
};
@action
set = (data: Partial<PersistedData>) => {
for (const key in data) {
// @ts-expect-error doesn't understand PersistedData is subset of keys
this[key] = data[key];
}
this.persist();
};
@action
@@ -231,6 +222,7 @@ class UiStore {
this.commentsExpanded = this.commentsExpanded.filter(
(id) => id !== documentId
);
this.persist();
};
@action
@@ -238,6 +230,7 @@ class UiStore {
if (!this.commentsExpanded.includes(documentId)) {
this.commentsExpanded.push(documentId);
}
this.persist();
};
@action
@@ -252,17 +245,7 @@ class UiStore {
@action
toggleCollapsedSidebar = () => {
sidebarHidden = false;
this.sidebarCollapsed = !this.sidebarCollapsed;
};
@action
showTableOfContents = () => {
this.tocVisible = true;
};
@action
hideTableOfContents = () => {
this.tocVisible = false;
this.set({ sidebarCollapsed: !this.sidebarCollapsed });
};
@action
@@ -324,6 +307,10 @@ class UiStore {
theme: this.theme,
};
}
private persist = () => {
Storage.set(UI_STORE, this.asJson);
};
}
export default UiStore;
+9 -8
View File
@@ -48,11 +48,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@aws-sdk/client-s3": "3.616.0",
"@aws-sdk/lib-storage": "3.616.0",
"@aws-sdk/s3-presigned-post": "3.616.0",
"@aws-sdk/s3-request-presigner": "3.616.0",
"@aws-sdk/signature-v4-crt": "^3.616.0",
"@aws-sdk/client-s3": "3.693.0",
"@aws-sdk/lib-storage": "3.693.0",
"@aws-sdk/s3-presigned-post": "3.693.0",
"@aws-sdk/s3-request-presigner": "3.693.0",
"@aws-sdk/signature-v4-crt": "^3.693.0",
"@babel/core": "^7.24.7",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
@@ -79,11 +79,11 @@
"@hocuspocus/server": "1.1.2",
"@joplin/turndown-plugin-gfm": "^1.0.49",
"@juggle/resize-observer": "^3.4.0",
"@octokit/auth-app": "^6.1.2",
"@octokit/auth-app": "^6.1.3",
"@outlinewiki/koa-passport": "^4.2.1",
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
"@renderlesskit/react": "^0.11.0",
"@sentry/node": "^7.117.0",
"@sentry/node": "^7.119.0",
"@sentry/react": "^7.119.0",
"@tippyjs/react": "^4.2.6",
"@types/form-data": "^2.5.0",
@@ -208,6 +208,7 @@
"react-waypoint": "^10.3.0",
"react-window": "^1.8.10",
"reakit": "^1.3.11",
"redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.2.2",
"refractor": "^3.6.0",
"request-filtering-agent": "^1.1.2",
@@ -254,7 +255,7 @@
"@babel/cli": "^7.25.9",
"@babel/preset-typescript": "^7.24.1",
"@faker-js/faker": "^8.4.1",
"@relative-ci/agent": "^4.2.12",
"@relative-ci/agent": "^4.2.13",
"@testing-library/react": "^12.0.0",
"@types/addressparser": "^1.0.3",
"@types/body-scroll-lock": "^3.1.2",
+9
View File
@@ -51,6 +51,15 @@ export default abstract class BaseEmail<
* @returns A promise that resolves once the email is placed on the task queue
*/
public schedule(options?: Bull.JobOptions) {
// No-op to schedule emails if SMTP is not configured
if (!env.SMTP_FROM_EMAIL) {
Logger.info(
"email",
`Email ${this.constructor.name} not sent due to missing SMTP configuration`
);
return;
}
const templateName = this.constructor.name;
Metrics.increment("email.scheduled", {
@@ -7,6 +7,7 @@ import HTMLHelper from "@server/models/helpers/HTMLHelper";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import SubscriptionHelper from "@server/models/helpers/SubscriptionHelper";
import { can } from "@server/policies";
import { CacheHelper } from "@server/utils/CacheHelper";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
@@ -68,21 +69,28 @@ export default class DocumentPublishedOrUpdatedEmail extends BaseEmail<
let body;
if (revisionId && team?.getPreference(TeamPreference.PreviewsInEmails)) {
// generate the diff html for the email
const revision = await Revision.findByPk(revisionId);
body = await CacheHelper.getDataOrSet<string>(
`diff:${revisionId}`,
async () => {
// generate the diff html for the email
const revision = await Revision.findByPk(revisionId);
if (revision) {
const before = await revision.before();
const content = await DocumentHelper.toEmailDiff(before, revision, {
includeTitle: false,
centered: false,
signedUrls: 4 * Day.seconds,
baseUrl: props.teamUrl,
});
if (revision) {
const before = await revision.before();
const content = await DocumentHelper.toEmailDiff(before, revision, {
includeTitle: false,
centered: false,
signedUrls: 4 * Day.seconds,
baseUrl: props.teamUrl,
});
// inline all css so that it works in as many email providers as possible.
body = content ? await HTMLHelper.inlineCSS(content) : undefined;
}
// inline all css so that it works in as many email providers as possible.
return content ? await HTMLHelper.inlineCSS(content) : undefined;
}
return;
},
30
);
}
return {
+1 -1
View File
@@ -558,7 +558,7 @@ export class ProsemirrorHelper {
// Inject Mermaid script
if (mermaidElements.length) {
element.innerHTML = `
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
fontFamily: "inherit",
+49
View File
@@ -1,6 +1,7 @@
import { Day } from "@shared/utils/time";
import Logger from "@server/logging/Logger";
import Redis from "@server/storage/redis";
import { MutexLock } from "./MutexLock";
/**
* A Helper class for server-side cache management
@@ -9,6 +10,54 @@ export class CacheHelper {
// Default expiry time for cache data in seconds
private static defaultDataExpiry = Day.seconds;
/**
* Given a key this method will attempt to get the data from cache store first
* If data is not found, it will call the callback to get the data and save it in cache
* using a distributed lock to prevent multiple writes.
*
* @param key Cache key
* @param callback Callback to get the data if not found in cache
* @param expiry Cache data expiry in seconds
*/
public static async getDataOrSet<T>(
key: string,
callback: () => Promise<T | undefined>,
expiry?: number
): Promise<T | undefined> {
let cache = await this.getData<T>(key);
if (cache) {
return cache;
}
// Nothing in the cache, acquire a lock to prevent multiple writes
let lock;
const lockKey = `lock:${key}`;
try {
try {
lock = await MutexLock.lock.acquire(
[lockKey],
MutexLock.defaultLockTimeout
);
} catch (err) {
Logger.error(`Could not acquire lock for ${key}`, err);
}
cache = await this.getData<T>(key);
if (cache) {
return cache;
}
// Get the data from the callback and save it in cache
const value = await callback();
if (value) {
await this.setData<T>(key, value, expiry);
}
return value;
} finally {
await lock?.release();
}
}
/**
* Given a key, gets the data from cache store
*
+20
View File
@@ -0,0 +1,20 @@
import Redlock from "redlock";
import Redis from "@server/storage/redis";
export class MutexLock {
// Default expiry time for qcuiring lock in milliseconds
public static defaultLockTimeout = 5000;
/**
* Returns the redlock instance
*/
public static get lock(): Redlock {
this.redlock ??= new Redlock([Redis.defaultClient], {
retryJitter: 10,
});
return this.redlock;
}
private static redlock: Redlock;
}
+3 -1
View File
@@ -1310,7 +1310,9 @@ mark {
}
.ProseMirror[contenteditable="false"] .code-block[data-language=mermaidjs] {
display: none;
height: 0;
overflow: hidden;
margin: -0.5em 0 0 0;
}
.code-block.with-line-numbers {
@@ -308,6 +308,7 @@
"{{ firstUsername }} and {{ secondUsername }} reacted with {{ emoji }}": "{{ firstUsername }} and {{ secondUsername }} reacted with {{ emoji }}",
"{{ firstUsername }} and {{ count }} others reacted with {{ emoji }}": "{{ firstUsername }} and {{ count }} other reacted with {{ emoji }}",
"{{ firstUsername }} and {{ count }} others reacted with {{ emoji }}_plural": "{{ firstUsername }} and {{ count }} others reacted with {{ emoji }}",
"Add reaction": "Add reaction",
"Reaction picker": "Reaction picker",
"Could not load reactions": "Could not load reactions",
"Reaction": "Reaction",
+1 -1
View File
@@ -159,7 +159,7 @@ export default () =>
build: {
outDir: "./build/app",
manifest: true,
sourcemap: true,
sourcemap: process.env.CI ? false : "hidden",
minify: "terser",
// Prevent asset inling as it does not conform to CSP rules
assetsInlineLimit: 0,
+792 -804
View File
File diff suppressed because it is too large Load Diff