Skip to content

feat: select group avatars with the emoji picker #11395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions site/src/@types/emoji-mart.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ declare module "@emoji-mart/react" {
| { unified: undefined; src: string }
| { unified: string; src: undefined };

const EmojiPicker: React.FC<{
export interface EmojiMartProps {
set: "native" | "apple" | "facebook" | "google" | "twitter";
theme: "dark" | "light";
data: unknown;
custom: CustomCategory[];
emojiButtonSize?: number;
emojiSize?: number;
onEmojiSelect: (emoji: EmojiData) => void;
}>;
}

const EmojiMart: React.FC<EmojiMartProps>;

export default EmojiPicker;
export default EmojiMart;
}
40 changes: 40 additions & 0 deletions site/src/components/IconField/EmojiPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import EmojiMart, { type EmojiMartProps } from "@emoji-mart/react";
import data from "@emoji-mart/data/sets/14/twitter.json";
import { type FC } from "react";
import icons from "theme/icons.json";

const custom = [
{
id: "icons",
name: "Icons",
emojis: icons.map((icon) => {
const id = icon.split(".")[0];

return {
id,
name: id,
keywords: id.split("-"),
skins: [{ src: `/icon/${icon}` }],
};
}),
},
];

type EmojiPickerProps = Omit<
EmojiMartProps,
"custom" | "data" | "set" | "theme"
>;

const EmojiPicker: FC<EmojiPickerProps> = (props) => {
return (
<EmojiMart
theme="dark"
set="twitter"
data={data}
custom={custom}
{...props}
/>
);
};

export default EmojiPicker;
2 changes: 1 addition & 1 deletion site/src/components/IconField/IconField.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { action } from "@storybook/addon-actions";
import IconField from "./IconField";
import type { Meta, StoryObj } from "@storybook/react";
import { IconField } from "./IconField";

const meta: Meta<typeof IconField> = {
title: "components/IconField",
Expand Down
89 changes: 42 additions & 47 deletions site/src/components/IconField/IconField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { css, Global, useTheme } from "@emotion/react";
import Button from "@mui/material/Button";
import InputAdornment from "@mui/material/InputAdornment";
import TextField, { type TextFieldProps } from "@mui/material/TextField";
import Picker from "@emoji-mart/react";
import { type FC } from "react";
import { type FC, lazy, Suspense } from "react";
import { Loader } from "components/Loader/Loader";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { Stack } from "components/Stack/Stack";
import data from "@emoji-mart/data/sets/14/twitter.json";
import icons from "theme/icons.json";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { visuallyHidden } from "@mui/utils";

// See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222
const urlFromUnifiedCode = (unified: string) =>
Expand All @@ -22,24 +21,12 @@ type IconFieldProps = TextFieldProps & {
onPickEmoji: (value: string) => void;
};

const custom = [
{
id: "icons",
name: "Icons",
emojis: icons.map((icon) => {
const id = icon.split(".")[0];
const EmojiPicker = lazy(() => import("./EmojiPicker"));

return {
id,
name: id,
keywords: id.split("-"),
skins: [{ src: `/icon/${icon}` }],
};
}),
},
];

const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
export const IconField: FC<IconFieldProps> = ({
onPickEmoji,
...textFieldProps
}) => {
if (
typeof textFieldProps.value !== "string" &&
typeof textFieldProps.value !== "undefined"
Expand All @@ -53,9 +40,9 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
return (
<Stack spacing={1}>
<TextField
{...textFieldProps}
fullWidth
label="Icon"
{...textFieldProps}
InputProps={{
endAdornment: hasIcon ? (
<InputAdornment
Expand Down Expand Up @@ -86,6 +73,18 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
}}
/>

<Global
styles={css`
em-emoji-picker {
--rgb-background: ${theme.palette.background.paper};
--rgb-input: ${theme.palette.primary.main};
--rgb-color: ${theme.palette.text.primary};

// Hack to prevent the right side from being cut off
width: 350px;
}
`}
/>
<Popover>
{(popover) => (
<>
Expand All @@ -98,35 +97,31 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
id="emoji"
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
>
<Global
styles={css`
em-emoji-picker {
--rgb-background: ${theme.palette.background.paper};
--rgb-input: ${theme.palette.primary.main};
--rgb-color: ${theme.palette.text.primary};

// Hack to prevent the right side from being cut off
width: 350px;
}
`}
/>
<Picker
set="twitter"
theme="dark"
data={data}
custom={custom}
onEmojiSelect={(emoji) => {
const value = emoji.src ?? urlFromUnifiedCode(emoji.unified);
onPickEmoji(value);
popover.setIsOpen(false);
}}
/>
<Suspense fallback={<Loader />}>
<EmojiPicker
onEmojiSelect={(emoji) => {
const value =
emoji.src ?? urlFromUnifiedCode(emoji.unified);
onPickEmoji(value);
popover.setIsOpen(false);
}}
/>
</Suspense>
</PopoverContent>
</>
)}
</Popover>

{/* This component takes a long time to load (easily several seconds), so we
don't want to wait until the user actually clicks the button to start loading.
Unfortunately, React doesn't provide an API to start warming a lazy component,
so we just have to sneak it into the DOM, which is kind of annoying, but means
that users shouldn't ever spend time waiting for it to load. */}
<div css={{ ...visuallyHidden }}>
<Suspense>
<EmojiPicker onEmojiSelect={() => {}} />
</Suspense>
</div>
</Stack>
);
};

export default IconField;
11 changes: 0 additions & 11 deletions site/src/components/IconField/LazyIconField.tsx

This file was deleted.

5 changes: 2 additions & 3 deletions site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
HelpTooltipText,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import { LazyIconField } from "components/IconField/LazyIconField";
import { IconField } from "components/IconField/IconField";
import Link from "@mui/material/Link";
import {
HorizontalForm,
Expand Down Expand Up @@ -345,12 +345,11 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
label="Description"
/>

<LazyIconField
<IconField
{...getFieldHelpers("icon")}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label="Icon"
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
Expand Down
5 changes: 3 additions & 2 deletions site/src/pages/GroupsPage/CreateGroupPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import TextField from "@mui/material/TextField";
import { CreateGroupRequest } from "api/typesGenerated";
import { FormFooter } from "components/FormFooter/FormFooter";
import { FullPageForm } from "components/FullPageForm/FullPageForm";
import { IconField } from "components/IconField/IconField";
import { Margins } from "components/Margins/Margins";
import { Stack } from "components/Stack/Stack";
import { useFormik } from "formik";
Expand Down Expand Up @@ -58,12 +59,12 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
fullWidth
label="Display Name"
/>
<TextField
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
autoComplete="avatar url"
fullWidth
label="Avatar URL"
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
/>
</Stack>
<FormFooter onCancel={onCancel} isLoading={isLoading} />
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/GroupsPage/SettingsGroupPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Group } from "api/typesGenerated";
import { FormFooter } from "components/FormFooter/FormFooter";
import { FullPageForm } from "components/FullPageForm/FullPageForm";
import { Loader } from "components/Loader/Loader";
import { LazyIconField } from "components/IconField/LazyIconField";
import { IconField } from "components/IconField/IconField";
import { Margins } from "components/Margins/Margins";
import { useFormik } from "formik";
import { FC } from "react";
Expand Down Expand Up @@ -84,7 +84,7 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
label="Display Name"
disabled={isEveryoneGroup(group)}
/>
<LazyIconField
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
fullWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
iconValidator,
} from "utils/formUtils";
import * as Yup from "yup";
import { LazyIconField } from "components/IconField/LazyIconField";
import { IconField } from "components/IconField/IconField";
import {
FormFields,
FormSection,
Expand Down Expand Up @@ -126,7 +126,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
rows={2}
/>

<LazyIconField
<IconField
{...getFieldHelpers("icon")}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
Expand Down