Compare commits

..

3 Commits

Author SHA1 Message Date
Tom Moor a508ec8380 wip 2024-11-16 13:34:04 -05:00
Tom Moor c7dde8fbd7 wip 2024-11-16 13:30:39 -05:00
Tom Moor 827d4e5ad9 Test using xlarge 2024-11-16 13:27:46 -05:00
22 changed files with 952 additions and 1053 deletions
+2 -3
View File
@@ -39,7 +39,6 @@ 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;
@@ -123,13 +122,13 @@ function DocumentCard(props: Props) {
<Squircle
color={
collection?.color ??
(pinnedToHome ? theme.slateLight : theme.slateDark)
(!pin?.collectionId ? theme.slateLight : theme.slateDark)
}
>
{collection?.icon &&
collection?.icon !== "letter" &&
collection?.icon !== "collection" &&
pinnedToHome ? (
!pin?.collectionId ? (
<CollectionIcon collection={collection} color="white" />
) : (
<DocumentIcon color="white" />
+2 -4
View File
@@ -47,16 +47,14 @@ export default function LanguagePrompt() {
<br />
<Link
onClick={async () => {
ui.set({ languagePromptDismissed: true });
ui.setLanguagePromptDismissed();
await user.save({ language });
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={() => ui.set({ languagePromptDismissed: true })}>
{t("Dismiss")}
</Link>
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
</span>
</Flex>
</Wrapper>
+13 -16
View File
@@ -2,6 +2,7 @@ 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";
@@ -10,7 +11,6 @@ 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,22 +98,15 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
<PopoverButton
{...props}
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
size={size}
>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
size={size}
>
<ReactionIcon size={22} />
</NudeButton>
</Tooltip>
<ReactionIcon size={22} />
</PopoverButton>
)}
</PopoverDisclosure>
<Popover
@@ -158,4 +151,8 @@ 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.set({ sidebarRightWidth: width });
ui.setRightSidebarWidth(width);
},
[minWidth, maxWidth, ui]
);
const handleReset = React.useCallback(() => {
ui.set({ sidebarRightWidth: theme.sidebarRightWidth });
ui.setRightSidebarWidth(theme.sidebarRightWidth);
}, [ui, theme.sidebarRightWidth]);
const handleStopDrag = React.useCallback(() => {
+13 -12
View File
@@ -46,6 +46,7 @@ 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);
@@ -61,13 +62,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
ui.set({
sidebarWidth: isSmallerThanCollapsePoint
? theme.sidebarCollapsedWidth
: width,
});
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
},
[ui, theme, offset, minWidth, maxWidth]
[theme, offset, minWidth, maxWidth, setWidth]
);
const handleStopDrag = React.useCallback(() => {
@@ -85,13 +86,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
setCollapsing(true);
ui.collapseSidebar();
} else {
ui.set({ sidebarWidth: minWidth });
setWidth(minWidth);
setAnimating(true);
}
} else {
ui.set({ sidebarWidth: width });
setWidth(width);
}
}, [ui, isSmallerThanMinimum, minWidth, width]);
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
const handleBlur = React.useCallback(() => {
setHovering(false);
@@ -148,11 +149,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
ui.set({ sidebarWidth: minWidth });
setWidth(minWidth);
setCollapsing(false);
}, ANIMATION_MS);
}
}, [ui, minWidth, isCollapsing]);
}, [setWidth, minWidth, isCollapsing]);
React.useEffect(() => {
if (isResizing) {
@@ -173,7 +174,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
}, [isResizing, handleDrag, handleBlur, handleStopDrag]);
const handleReset = React.useCallback(() => {
ui.set({ sidebarWidth: theme.sidebarWidth });
ui.setSidebarWidth(theme.sidebarWidth);
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
+3 -8
View File
@@ -248,19 +248,14 @@ export default class FindAndReplaceExtension extends Extension {
let m;
const search = this.findRegExp;
// 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))) {
while ((m = search.exec(deburr(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 + i,
to: pos + i + m[0].length,
from: pos + m.index,
to: pos + m.index + m[0].length,
});
}
} catch (e) {
@@ -1,7 +1,6 @@
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";
@@ -17,14 +16,10 @@ 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";
@@ -247,13 +242,11 @@ function CommentThreadItem({
onRemoveReaction={handleRemoveReaction}
picker={
!comment.isResolved ? (
<Action
as={ReactionPicker}
<StyledReactionPicker
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
size={28}
rounded
/>
) : undefined
}
@@ -264,20 +257,14 @@ function CommentThreadItem({
<EventBoundary>
{!isEditing && (
<Actions gap={4} dir={dir}>
{firstOfThread && (
<ResolveButton onUpdate={handleUpdate} comment={comment} />
)}
{!comment.isResolved && (
<Action
as={ReactionPicker}
<StyledReactionPicker
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
rounded
/>
)}
<Action
as={CommentMenu}
<StyledMenu
comment={comment}
onEdit={setEditing}
onDelete={handleDelete}
@@ -291,38 +278,6 @@ 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 &&
@@ -353,13 +308,25 @@ const Body = styled.form`
border-radius: 2px;
`;
const Action = styled.span<{ rounded?: boolean }>`
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)`
color: ${s("textSecondary")};
${(props) =>
props.rounded &&
css`
border-radius: 50%;
`}
svg {
fill: currentColor;
@@ -385,7 +352,7 @@ const Actions = styled(Flex)<{ dir?: "rtl" | "ltr" }>`
background: ${s("backgroundSecondary")};
padding-left: 4px;
&:has(${Action}[aria-expanded="true"]) {
&:has(${StyledReactionPicker}[aria-expanded="true"], ${StyledMenu}[aria-expanded="true"]) {
opacity: 1;
}
`;
+2 -6
View File
@@ -103,10 +103,6 @@ function DocumentHeader({
});
}, [onSave]);
const handleToggle = React.useCallback(() => {
ui.set({ tocVisible: !ui.tocVisible });
}, [ui]);
const context = useActionContext({
activeDocumentId: document?.id,
});
@@ -133,7 +129,7 @@ function DocumentHeader({
placement="bottom"
>
<Button
onClick={handleToggle}
onClick={showContents ? ui.hideTableOfContents : ui.showTableOfContents}
icon={<TableOfContentsIcon />}
borderOnHover
neutral
@@ -184,7 +180,7 @@ function DocumentHeader({
useKeyDown(
(event) => event.ctrlKey && event.altKey && event.key === "˙",
handleToggle,
ui.tocVisible ? ui.hideTableOfContents : ui.showTableOfContents,
{
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: "role",
id: "isAdmin",
Header: t("Role"),
accessor: "rank",
Cell: observer(({ row }: { row: { original: User } }) => (
+20 -9
View File
@@ -7,19 +7,31 @@ 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 = Pick<
AuthStore,
"user" | "team" | "collaborationToken" | "availableTeams" | "policies"
>;
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 Provider = {
id: string;
@@ -153,10 +165,9 @@ export default class AuthStore extends Store<Team> {
/** The current team's policies */
@computed
get policies() {
const policy = this.currentTeamId
? this.rootStore.policies.get(this.currentTeamId)
: undefined;
return policy ? [policy] : [];
return this.currentTeamId
? [this.rootStore.policies.get(this.currentTeamId)]
: [];
}
/** Whether the user is signed in */
@@ -166,7 +177,7 @@ export default class AuthStore extends Store<Team> {
}
@computed
get asJson(): PersistedData {
get asJson() {
return {
user: this.user,
team: this.team,
+43 -30
View File
@@ -1,4 +1,4 @@
import { action, computed, observable } from "mobx";
import { action, autorun, computed, observable } from "mobx";
import { flushSync } from "react-dom";
import { light as defaultTheme } from "@shared/styles/theme";
import Storage from "@shared/utils/Storage";
@@ -23,16 +23,15 @@ export enum SystemTheme {
Dark = "dark",
}
type PersistedData = Pick<
UiStore,
| "languagePromptDismissed"
| "commentsExpanded"
| "theme"
| "sidebarWidth"
| "sidebarRightWidth"
| "sidebarCollapsed"
| "tocVisible"
>;
type PersistedData = {
languagePromptDismissed: boolean | undefined;
theme: Theme;
sidebarCollapsed: boolean;
sidebarWidth: number;
sidebarRightWidth: number;
tocVisible: boolean | undefined;
commentsExpanded: string[];
};
class UiStore {
// has the user seen the prompt to change the UI language and actioned it
@@ -135,6 +134,10 @@ class UiStore {
this.tocVisible = newData.tocVisible;
}
});
autorun(() => {
Storage.set(UI_STORE, this.asJson);
});
}
@action
@@ -144,7 +147,12 @@ class UiStore {
this.theme = theme;
});
});
this.persist();
Storage.set("theme", this.theme);
};
@action
setLanguagePromptDismissed = () => {
this.languagePromptDismissed = true;
};
@action
@@ -197,24 +205,25 @@ class UiStore {
this.activeCollectionId = undefined;
};
@action
setSidebarWidth = (width: number): void => {
this.sidebarWidth = width;
};
@action
setRightSidebarWidth = (width: number): void => {
this.sidebarRightWidth = width;
};
@action
collapseSidebar = () => {
this.set({ sidebarCollapsed: true });
this.sidebarCollapsed = true;
};
@action
expandSidebar = () => {
sidebarHidden = 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();
this.sidebarCollapsed = false;
};
@action
@@ -222,7 +231,6 @@ class UiStore {
this.commentsExpanded = this.commentsExpanded.filter(
(id) => id !== documentId
);
this.persist();
};
@action
@@ -230,7 +238,6 @@ class UiStore {
if (!this.commentsExpanded.includes(documentId)) {
this.commentsExpanded.push(documentId);
}
this.persist();
};
@action
@@ -245,7 +252,17 @@ class UiStore {
@action
toggleCollapsedSidebar = () => {
sidebarHidden = false;
this.set({ sidebarCollapsed: !this.sidebarCollapsed });
this.sidebarCollapsed = !this.sidebarCollapsed;
};
@action
showTableOfContents = () => {
this.tocVisible = true;
};
@action
hideTableOfContents = () => {
this.tocVisible = false;
};
@action
@@ -307,10 +324,6 @@ class UiStore {
theme: this.theme,
};
}
private persist = () => {
Storage.set(UI_STORE, this.asJson);
};
}
export default UiStore;
+8 -9
View File
@@ -48,11 +48,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@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",
"@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",
"@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.3",
"@octokit/auth-app": "^6.1.2",
"@outlinewiki/koa-passport": "^4.2.1",
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
"@renderlesskit/react": "^0.11.0",
"@sentry/node": "^7.119.0",
"@sentry/node": "^7.117.0",
"@sentry/react": "^7.119.0",
"@tippyjs/react": "^4.2.6",
"@types/form-data": "^2.5.0",
@@ -208,7 +208,6 @@
"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",
@@ -255,7 +254,7 @@
"@babel/cli": "^7.25.9",
"@babel/preset-typescript": "^7.24.1",
"@faker-js/faker": "^8.4.1",
"@relative-ci/agent": "^4.2.13",
"@relative-ci/agent": "^4.2.12",
"@testing-library/react": "^12.0.0",
"@types/addressparser": "^1.0.3",
"@types/body-scroll-lock": "^3.1.2",
-9
View File
@@ -51,15 +51,6 @@ 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,7 +7,6 @@ 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";
@@ -69,28 +68,21 @@ export default class DocumentPublishedOrUpdatedEmail extends BaseEmail<
let body;
if (revisionId && team?.getPreference(TeamPreference.PreviewsInEmails)) {
body = await CacheHelper.getDataOrSet<string>(
`diff:${revisionId}`,
async () => {
// generate the diff html for the email
const revision = await Revision.findByPk(revisionId);
// 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.
return content ? await HTMLHelper.inlineCSS(content) : undefined;
}
return;
},
30
);
// inline all css so that it works in as many email providers as possible.
body = content ? await HTMLHelper.inlineCSS(content) : undefined;
}
}
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@11/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
fontFamily: "inherit",
-49
View File
@@ -1,7 +1,6 @@
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
@@ -10,54 +9,6 @@ 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
@@ -1,20 +0,0 @@
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;
}
+1 -3
View File
@@ -1310,9 +1310,7 @@ mark {
}
.ProseMirror[contenteditable="false"] .code-block[data-language=mermaidjs] {
height: 0;
overflow: hidden;
margin: -0.5em 0 0 0;
display: none;
}
.code-block.with-line-numbers {
@@ -308,7 +308,6 @@
"{{ 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: process.env.CI ? false : "hidden",
sourcemap: true,
minify: "terser",
// Prevent asset inling as it does not conform to CSP rules
assetsInlineLimit: 0,
+804 -792
View File
File diff suppressed because it is too large Load Diff