Skip to content

Commit ffa7722

Browse files
authored
feat: select group avatars with the emoji picker (#11395)
1 parent ee2daed commit ffa7722

File tree

10 files changed

+112
-76
lines changed

10 files changed

+112
-76
lines changed

site/src/@types/emoji-mart.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ declare module "@emoji-mart/react" {
2828
| { unified: undefined; src: string }
2929
| { unified: string; src: undefined };
3030

31-
const EmojiPicker: React.FC<{
31+
export interface EmojiMartProps {
3232
set: "native" | "apple" | "facebook" | "google" | "twitter";
3333
theme: "dark" | "light";
3434
data: unknown;
3535
custom: CustomCategory[];
3636
emojiButtonSize?: number;
3737
emojiSize?: number;
3838
onEmojiSelect: (emoji: EmojiData) => void;
39-
}>;
39+
}
40+
41+
const EmojiMart: React.FC<EmojiMartProps>;
4042

41-
export default EmojiPicker;
43+
export default EmojiMart;
4244
}

site/src/components/ErrorBoundary/ErrorBoundary.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { Component, ReactNode, PropsWithChildren } from "react";
1+
import { Component, type ReactNode } from "react";
22
import { RuntimeErrorState } from "./RuntimeErrorState";
33

4-
type ErrorBoundaryProps = PropsWithChildren<unknown>;
4+
interface ErrorBoundaryProps {
5+
fallback?: ReactNode;
6+
children: ReactNode;
7+
}
58

69
interface ErrorBoundaryState {
710
error: Error | null;
811
}
912

1013
/**
1114
* Our app's Error Boundary
12-
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html
15+
* Read more about React Error Boundaries: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
1316
*/
1417
export class ErrorBoundary extends Component<
1518
ErrorBoundaryProps,
@@ -20,13 +23,15 @@ export class ErrorBoundary extends Component<
2023
this.state = { error: null };
2124
}
2225

23-
static getDerivedStateFromError(error: Error): { error: Error } {
26+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
2427
return { error };
2528
}
2629

2730
render(): ReactNode {
2831
if (this.state.error) {
29-
return <RuntimeErrorState error={this.state.error} />;
32+
return (
33+
this.props.fallback ?? <RuntimeErrorState error={this.state.error} />
34+
);
3035
}
3136

3237
return this.props.children;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import EmojiMart, { type EmojiMartProps } from "@emoji-mart/react";
2+
import data from "@emoji-mart/data/sets/14/twitter.json";
3+
import { type FC } from "react";
4+
import icons from "theme/icons.json";
5+
6+
const custom = [
7+
{
8+
id: "icons",
9+
name: "Icons",
10+
emojis: icons.map((icon) => {
11+
const id = icon.split(".")[0];
12+
13+
return {
14+
id,
15+
name: id,
16+
keywords: id.split("-"),
17+
skins: [{ src: `/icon/${icon}` }],
18+
};
19+
}),
20+
},
21+
];
22+
23+
type EmojiPickerProps = Omit<
24+
EmojiMartProps,
25+
"custom" | "data" | "set" | "theme"
26+
>;
27+
28+
const EmojiPicker: FC<EmojiPickerProps> = (props) => {
29+
return (
30+
<EmojiMart
31+
theme="dark"
32+
set="twitter"
33+
data={data}
34+
custom={custom}
35+
{...props}
36+
/>
37+
);
38+
};
39+
40+
export default EmojiPicker;

site/src/components/IconField/IconField.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { action } from "@storybook/addon-actions";
2-
import IconField from "./IconField";
32
import type { Meta, StoryObj } from "@storybook/react";
3+
import { IconField } from "./IconField";
44

55
const meta: Meta<typeof IconField> = {
66
title: "components/IconField",

site/src/components/IconField/IconField.tsx

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { css, Global, useTheme } from "@emotion/react";
22
import Button from "@mui/material/Button";
33
import InputAdornment from "@mui/material/InputAdornment";
44
import TextField, { type TextFieldProps } from "@mui/material/TextField";
5-
import Picker from "@emoji-mart/react";
6-
import { type FC } from "react";
5+
import { visuallyHidden } from "@mui/utils";
6+
import { type FC, lazy, Suspense } from "react";
7+
import { Loader } from "components/Loader/Loader";
78
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
89
import { Stack } from "components/Stack/Stack";
9-
import data from "@emoji-mart/data/sets/14/twitter.json";
10-
import icons from "theme/icons.json";
1110
import {
1211
Popover,
1312
PopoverContent,
@@ -22,24 +21,12 @@ type IconFieldProps = TextFieldProps & {
2221
onPickEmoji: (value: string) => void;
2322
};
2423

25-
const custom = [
26-
{
27-
id: "icons",
28-
name: "Icons",
29-
emojis: icons.map((icon) => {
30-
const id = icon.split(".")[0];
24+
const EmojiPicker = lazy(() => import("./EmojiPicker"));
3125

32-
return {
33-
id,
34-
name: id,
35-
keywords: id.split("-"),
36-
skins: [{ src: `/icon/${icon}` }],
37-
};
38-
}),
39-
},
40-
];
41-
42-
const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
26+
export const IconField: FC<IconFieldProps> = ({
27+
onPickEmoji,
28+
...textFieldProps
29+
}) => {
4330
if (
4431
typeof textFieldProps.value !== "string" &&
4532
typeof textFieldProps.value !== "undefined"
@@ -53,9 +40,9 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
5340
return (
5441
<Stack spacing={1}>
5542
<TextField
56-
{...textFieldProps}
5743
fullWidth
5844
label="Icon"
45+
{...textFieldProps}
5946
InputProps={{
6047
endAdornment: hasIcon ? (
6148
<InputAdornment
@@ -86,6 +73,18 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
8673
}}
8774
/>
8875

76+
<Global
77+
styles={css`
78+
em-emoji-picker {
79+
--rgb-background: ${theme.palette.background.paper};
80+
--rgb-input: ${theme.palette.primary.main};
81+
--rgb-color: ${theme.palette.text.primary};
82+
83+
// Hack to prevent the right side from being cut off
84+
width: 350px;
85+
}
86+
`}
87+
/>
8988
<Popover>
9089
{(popover) => (
9190
<>
@@ -98,35 +97,36 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
9897
id="emoji"
9998
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
10099
>
101-
<Global
102-
styles={css`
103-
em-emoji-picker {
104-
--rgb-background: ${theme.palette.background.paper};
105-
--rgb-input: ${theme.palette.primary.main};
106-
--rgb-color: ${theme.palette.text.primary};
107-
108-
// Hack to prevent the right side from being cut off
109-
width: 350px;
110-
}
111-
`}
112-
/>
113-
<Picker
114-
set="twitter"
115-
theme="dark"
116-
data={data}
117-
custom={custom}
118-
onEmojiSelect={(emoji) => {
119-
const value = emoji.src ?? urlFromUnifiedCode(emoji.unified);
120-
onPickEmoji(value);
121-
popover.setIsOpen(false);
122-
}}
123-
/>
100+
<Suspense fallback={<Loader />}>
101+
<EmojiPicker
102+
onEmojiSelect={(emoji) => {
103+
const value =
104+
emoji.src ?? urlFromUnifiedCode(emoji.unified);
105+
onPickEmoji(value);
106+
popover.setIsOpen(false);
107+
}}
108+
/>
109+
</Suspense>
124110
</PopoverContent>
125111
</>
126112
)}
127113
</Popover>
114+
115+
{/*
116+
- This component takes a long time to load (easily several seconds), so we
117+
don't want to wait until the user actually clicks the button to start loading.
118+
Unfortunately, React doesn't provide an API to start warming a lazy component,
119+
so we just have to sneak it into the DOM, which is kind of annoying, but means
120+
that users shouldn't ever spend time waiting for it to load.
121+
- Except we don't do it when running tests, because Jest doesn't define
122+
`IntersectionObserver`, and it would make them slower anyway. */}
123+
{process.env.NODE_ENV !== "test" && (
124+
<div css={{ ...visuallyHidden }}>
125+
<Suspense>
126+
<EmojiPicker onEmojiSelect={() => {}} />
127+
</Suspense>
128+
</div>
129+
)}
128130
</Stack>
129131
);
130132
};
131-
132-
export default IconField;

site/src/components/IconField/LazyIconField.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
HelpTooltipText,
2828
HelpTooltipTrigger,
2929
} from "components/HelpTooltip/HelpTooltip";
30-
import { LazyIconField } from "components/IconField/LazyIconField";
30+
import { IconField } from "components/IconField/IconField";
3131
import Link from "@mui/material/Link";
3232
import {
3333
HorizontalForm,
@@ -345,12 +345,11 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
345345
label="Description"
346346
/>
347347

348-
<LazyIconField
348+
<IconField
349349
{...getFieldHelpers("icon")}
350350
disabled={isSubmitting}
351351
onChange={onChangeTrimmed(form)}
352352
fullWidth
353-
label="Icon"
354353
onPickEmoji={(value) => form.setFieldValue("icon", value)}
355354
/>
356355
</FormFields>

site/src/pages/GroupsPage/CreateGroupPageView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import TextField from "@mui/material/TextField";
22
import { CreateGroupRequest } from "api/typesGenerated";
33
import { FormFooter } from "components/FormFooter/FormFooter";
44
import { FullPageForm } from "components/FullPageForm/FullPageForm";
5+
import { IconField } from "components/IconField/IconField";
56
import { Margins } from "components/Margins/Margins";
67
import { Stack } from "components/Stack/Stack";
78
import { useFormik } from "formik";
@@ -58,12 +59,12 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
5859
fullWidth
5960
label="Display Name"
6061
/>
61-
<TextField
62+
<IconField
6263
{...getFieldHelpers("avatar_url")}
6364
onChange={onChangeTrimmed(form)}
64-
autoComplete="avatar url"
6565
fullWidth
6666
label="Avatar URL"
67+
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
6768
/>
6869
</Stack>
6970
<FormFooter onCancel={onCancel} isLoading={isLoading} />

site/src/pages/GroupsPage/SettingsGroupPageView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Group } from "api/typesGenerated";
33
import { FormFooter } from "components/FormFooter/FormFooter";
44
import { FullPageForm } from "components/FullPageForm/FullPageForm";
55
import { Loader } from "components/Loader/Loader";
6-
import { LazyIconField } from "components/IconField/LazyIconField";
6+
import { IconField } from "components/IconField/IconField";
77
import { Margins } from "components/Margins/Margins";
88
import { useFormik } from "formik";
99
import { FC } from "react";
@@ -84,7 +84,7 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
8484
label="Display Name"
8585
disabled={isEveryoneGroup(group)}
8686
/>
87-
<LazyIconField
87+
<IconField
8888
{...getFieldHelpers("avatar_url")}
8989
onChange={onChangeTrimmed(form)}
9090
fullWidth

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
iconValidator,
1212
} from "utils/formUtils";
1313
import * as Yup from "yup";
14-
import { LazyIconField } from "components/IconField/LazyIconField";
14+
import { IconField } from "components/IconField/IconField";
1515
import {
1616
FormFields,
1717
FormSection,
@@ -126,7 +126,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
126126
rows={2}
127127
/>
128128

129-
<LazyIconField
129+
<IconField
130130
{...getFieldHelpers("icon")}
131131
disabled={isSubmitting}
132132
onChange={onChangeTrimmed(form)}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy