Skip to content
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

feat(web): reusable sms preview component #5173

Merged
merged 6 commits into from
Feb 13, 2024

Conversation

LetItRock
Copy link
Contributor

What change does this PR introduce?

Reusable SMS preview component based on the new designs from the Translation Management project.

Why was this change needed?

Other information (Screenshots)

Screenshot 2024-02-08 at 23 51 37
Screenshot 2024-02-08 at 23 51 49
Screenshot 2024-02-08 at 23 51 58
Screenshot 2024-02-08 at 23 52 04
Screenshot 2024-02-08 at 23 52 18
Screenshot 2024-02-08 at 23 52 31
Screenshot 2024-02-08 at 23 52 44

Screen.Recording.2024-02-08.at.23.53.01.mov

@LetItRock LetItRock self-assigned this Feb 8, 2024
Copy link

linear bot commented Feb 8, 2024


type ErrorType = { error: string; message: string; statusCode: number };

export const usePreviewSms = (options: UseMutationOptions<ResultType, ErrorType, PayloadType> = {}) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook to compile SMS template.

Comment on lines +9 to +23
export function LocaleSelect({
locales,
value,
isLoading,
onLocaleChange,
className,
dropdownPosition,
}: {
locales: { langName: string; langIso: string }[];
value?: string;
isLoading?: boolean;
onLocaleChange: (val: string) => void;
className?: string;
dropdownPosition?: ISelectProps['dropdownPosition'];
}) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added missing types and a new property dropdownPosition

Comment on lines 23 to 45
&::before {
content: '';
position: absolute;
bottom: -2px;
right: -7px;
height: 20px;
border-right: 20px solid ${({ isError }) => (isError ? colors.error : '#51ba52')};
border-bottom-left-radius: 16px 14px;
transform: translate(0, -2px);
}

&::after {
content: '';
position: absolute;
z-index: 1;
bottom: -2px;
right: -46px;
width: 16px;
height: 20px;
background: ${({ theme }) => (theme.colorScheme === 'dark' ? '#4b4b51' : colors.white)};
border-bottom-left-radius: 10px;
transform: translate(-30px, -2px);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the CSS magic to render the message bubble tail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 suggestion: Could you please add some comments to the code to explain the purpose / use case of this magic?‏

Comment on lines 13 to 14
min-width: 160px;
min-height: 36px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set the min-width because on the hover we show blurred background and edit button which needs some space

min-height: 36px;
padding: 8px 12px;
border-radius: 20px;
background: ${({ isError }) => (isError ? colors.error : '#51ba52')};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#51ba52 this is the light green color unfortunately not from the color theme we have

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 suggestion: Could we please check with Nik to make sure this will be part of our next color palette then? (I think we already have the base palette defined somewhere)‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will talk to him

Comment on lines 42 to 43
const [previewContent, setPreviewContent] = useState(templateContent as string);
const [selectedLocale, setSelectedLocale] = useState('');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previewContent is a compiled template content shown as the text
selectedLocale is the locale to which the template would be compiled

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 thought: These would be helpful in code too!‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refactor this code so additional comments won't be needed.

Comment on lines 56 to 60
useEffect(() => {
getLocalesFromContent({
content: previewData.current.templateContent,
});
}, [getLocalesFromContent, previewData]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch locales from the template, will be called only once during the initial mount

Comment on lines 62 to 68
useEffect(() => {
getSmsPreview({
content: previewData.current.templateContent,
payload: '',
locale: selectedLocale,
});
}, [selectedLocale, previewData, getSmsPreview]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compile the template for the preview

Comment on lines 79 to 101
export const MobileSimulator: React.FC = ({ children }) => {
const [isIOS, setIsIOS] = useState(true);
const { colorScheme } = useMantineColorScheme();

return (
<MobileSimulatorBody>
{isIOS ? <Notch /> : <Camera />}
<IndicatorsContainer>
<TimeIconStyled isVisible={isIOS} />
{isIOS ? <IOSIndicatorsIcon /> : <AndroidIndicatorsIcon />}
</IndicatorsContainer>
<SwitchContainer>
<PhonePlatformSwitch checked={isIOS} onChange={() => setIsIOS((old) => !old)} />
</SwitchContainer>
{children}
{isIOS ? (
<IOSKeyboard isDarkMode={colorScheme === 'dark'} />
) : (
<AndroidKeyboard isDarkMode={colorScheme === 'dark'} />
)}
</MobileSimulatorBody>
);
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried a few approaches here, exported phone svg and webp images, but the problem that I faced is with changing it for our needs, like for ex. in the Push preview we would need to have a background... also there are different themes and platforms, so in total would be 4 images just for sms, and 4 for push then...
that's why I decided to code it with css and only have some parts included with svg... it's less scalable, but for now we don't need this...

Comment on lines 87 to 88
<SwitchInput id="phonePlatformSwitch" ref={ref} type="checkbox" checked={checked} onChange={onChange} />
<ButtonsContainer htmlFor="phonePlatformSwitch">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old good css trick to use input + label

Copy link
Contributor

@antonjoel82 antonjoel82 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks pretty good overall! Will do behavior validation tomorrow morning in another pass.

Some high-level thoughts:

  • We should aim for rem where possible IMO
  • The assets related to the Mobile views looked not insignificantly large... when we begin chunking the application, I think we will want to make sure those are not in the main bundle

}: {
content?: string | IEmailBlock[];
payload: string;
locale?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: Do we have a more restricted type for Locale that could be used here?‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately, we don't :/ might be hard to introduce now without refactoring

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const getSmsPreview = useCallback(
({ content, payload, locale }: PayloadType) => {
if (IS_DOCKER_HOSTED) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 suggestion: Could you please add a comment in-code explaining why we do this?‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a general pattern we use when something should not be available for the self-hosted users

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it reads logically, it might be good to extract this structure to a single helper function to ensure we apply it consistently and with documentation / comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's have a tech debt ticket for this as we have many places around the code ;)

apps/web/src/api/hooks/usePreviewSms.ts Outdated Show resolved Hide resolved
apps/web/src/pages/templates/components/SmsBubble.tsx Outdated Show resolved Hide resolved
Comment on lines -1 to +4
import { errorMessage, successMessage } from '@novu/design-system';
import { errorMessage as errorMessageRoot, successMessage as successMessageRoot } from '@novu/design-system';

export { errorMessage, successMessage };
export const errorMessage = errorMessageRoot;
export const successMessage = successMessageRoot;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: Why make this abstraction here?‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an error in the console that somehow it wasn't able to find these methods when doing imports, it was weird and only this helped

const isVariant = !!variantUuid;
let navigatePath = `${basePath}/${channel}/${stepUuid}`;
if (isVariant) {
navigatePath += `/variants/${variantUuid}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤓 nitpick: Is there a constant we can derive this from?‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately, we don't, because it's a dynamic path

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, by that I meant something more like /${VARIANTS_PATH}/${variantUuid}

Base automatically changed from nv-2995-reusable-email-preview-component to translation-management-beta February 9, 2024 13:06
@antonjoel82
Copy link
Contributor

antonjoel82 commented Feb 9, 2024

Looks and works great overall! Nice job.

Screenshot 2024-02-09 at 9 31 26 AM

A couple UX things:

  • I think on phones, the message bubbles do have text left-aligned, but generally are sized to fit content, so we may want to shrink the minimum width
  • It may be worth the discussion with Nik first, but incoming messages (both Android and iOS) are always on the left and gray instead, so it might fit better with the real world if it looks like a received message
  • Lastly, in mock-ups it looks like the "Edit message" + blur are centered in the entire screen rather than within the message bubble; did we decide to deviate from that?

@LetItRock
Copy link
Contributor Author

LetItRock commented Feb 12, 2024

  • I think on phones, the message bubbles do have text left-aligned, but generally are sized to fit content, so we may want to shrink the minimum width
  • It may be worth the discussion with Nik first, but incoming messages (both Android and iOS) are always on the left and gray instead, so it might fit better with the real world if it looks like a received message
  • Lastly, in mock-ups it looks like the "Edit message" + blur are centered in the entire screen rather than within the message bubble; did we decide to deviate from that?

These are the things I discussed with Nikolay and we came up with this implementation. Mainly because of the issues we had with the overlay showing on the entire phone (it doesn't allow change language, frustration on hover, etc.).

Copy link
Contributor

@antonjoel82 antonjoel82 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes and responses!

}
);
} = useMutation<ILocale[], IResponseError, Payload>(({ content }) => getLocalesFromContent({ content }), {
onError: (e: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤓 nitpick: unknown is technically recommended by TypeScript here (introduced first in 4.4) ‏

Suggested change
onError: (e: any) => {
onError: (e: unknown) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be defined tho IResponseError, I will remove that :)


const getSmsPreview = useCallback(
({ content, payload, locale }: PayloadType) => {
if (IS_DOCKER_HOSTED) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it reads logically, it might be good to extract this structure to a single helper function to ensure we apply it consistently and with documentation / comments

Comment on lines +22 to +25
}

export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => {
const [isHovered, setIsHovered] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => {
const [isHovered, setIsHovered] = useState(false);
}
export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => {
const [isHovered, setIsHovered] = useState<boolean>(false);

In my previous position, we ended up with typing issues everywhere we didn't have explicit type params, so my preference is to always use them to be safe. Especially because in theory this could be false | null or false | undefined instead of boolean

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type is derived here from the value and it won't allow setting the state for undefined or null. But I'm not sure whether you mean something else.

Comment on lines +37 to +38
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 thought: Sorry it didn't catch my eye the first time around, but this could be useful to extract to its own hook since we may have to re-use useHover behavior elsewhere‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, will do that 🙌

`;

export const TimeIconStyled = styled(({ isVisible }: { isVisible: boolean }) => (
<TimeIcon style={{ opacity: isVisible ? 1 : 0 }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: Do we specifically want this to still "occupy" its space even when not visible?‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, otherwise we would have to position the indicators component differently in the flex container

@LetItRock LetItRock merged commit 24526c2 into translation-management-beta Feb 13, 2024
23 checks passed
@LetItRock LetItRock deleted the nv-2998-reusable-sms-preview branch February 13, 2024 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants