From e12f7824e88542e2dfb427cc475cb423c9a14d24 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 15:53:25 +0900 Subject: [PATCH 01/12] Support carousel template with simple templates --- src/ui/TemplateMessageItemBody/index.tsx | 152 +++++++++++++++-------- src/ui/TemplateMessageItemBody/types.ts | 16 +-- 2 files changed, 111 insertions(+), 57 deletions(-) diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index f5adc9852..70f732cda 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -3,7 +3,14 @@ import React, { ReactElement, useEffect, useState } from 'react'; import type { BaseMessage } from '@sendbird/chat/message'; import { getClassName, removeAtAndBraces, startsWithAtAndEndsWithBraces } from '../../utils'; import MessageTemplateWrapper from '../../modules/GroupChannel/components/MessageTemplateWrapper'; -import { CarouselItem, CarouselType, MessageTemplateData, MessageTemplateItem, SimpleTemplateData } from './types'; +import { + CarouselItem, + CarouselType, + MessageTemplateData, + MessageTemplateItem, + SendbirdUiTemplate, + SimpleTemplateData +} from './types'; import restoreNumbersFromMessageTemplateObject from './utils/restoreNumbersFromMessageTemplateObject'; import mapData from './utils/mapData'; import selectColorVariablesByTheme from './utils/selectColorVariablesByTheme'; @@ -33,19 +40,28 @@ interface TemplateMessageItemBodyProps { /** * Returns copied message template object filled with given template data and color variables. */ -const getFilledMessageTemplateWithData = ( +const getFilledMessageTemplateWithData = ({ + template, + templateData = {}, + colorVariables, + theme, +}: { template: MessageTemplateItem[], - templateData: Record, - colorVariables: Record, - theme: SendbirdTheme, -): MessageTemplateItem[] => { - const selectedThemeColorVariables = selectColorVariablesByTheme({ - colorVariables, - theme, - }); + templateData?: Record, + colorVariables?: Record, + theme?: SendbirdTheme, +}): MessageTemplateItem[] => { + let selectedThemeColorVariables = {}; + if (colorVariables && theme) { + selectedThemeColorVariables = selectColorVariablesByTheme({ + colorVariables, + theme, + }); + } + const source = { ...templateData, ...selectedThemeColorVariables }; const parsedTemplate: MessageTemplateItem[] = mapData({ template: restoreNumbersFromMessageTemplateObject(template) as any, - source: { ...templateData, ...selectedThemeColorVariables }, + source, }) as any; return parsedTemplate; }; @@ -86,9 +102,13 @@ export function TemplateMessageItemBody({ setRenderData, ] = useState(getFilledMessageTemplateItems()); - function getFilledMessageTemplateItemsForCarouselTemplate(simpleTemplateDataList: SimpleTemplateData[]) { + function getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload(simpleTemplateDataList: SimpleTemplateData[]): { + maxVersion: number, + filledTemplates: MessageTemplateItem[][], + } { const cachedSimpleTemplates: ProcessedMessageTemplate[] = []; const simpleTemplatesVariables: Array | undefined> = []; + let maxVersion = 0; simpleTemplateDataList.forEach((simpleTemplateData: SimpleTemplateData) => { const simpleTemplateKey = simpleTemplateData.key; if (!simpleTemplateKey) { @@ -98,31 +118,51 @@ export function TemplateMessageItemBody({ const simpleCachedTemplate = getCachedTemplate(simpleTemplateKey); cachedSimpleTemplates.push(simpleCachedTemplate); simpleTemplatesVariables.push(simpleTemplateData.variables); + maxVersion = Math.max(maxVersion, simpleCachedTemplate.version); }); const filledMessageTemplateItemsList: MessageTemplateItem[][] = cachedSimpleTemplates .map((cachedSimpleTemplate, index) => { const templateItems: MessageTemplateItem[] = JSON.parse(cachedSimpleTemplate.uiTemplate); - const filledMessageTemplateItems: MessageTemplateItem[] = getFilledMessageTemplateWithData( - templateItems, - simpleTemplatesVariables[index] ?? {}, - cachedSimpleTemplate.colorVariables, + const filledMessageTemplateItems: MessageTemplateItem[] = getFilledMessageTemplateWithData({ + template: templateItems, + templateData: simpleTemplatesVariables[index], + colorVariables: cachedSimpleTemplate.colorVariables, theme, - ); + }); return filledMessageTemplateItems; }); - return filledMessageTemplateItemsList; + return { + maxVersion, + filledTemplates: filledMessageTemplateItemsList, + }; + } + + function getFilledMessageTemplateItemsForCarouselTemplate(uiTemplates: SendbirdUiTemplate[]): { + maxVersion: number, + filledTemplates: MessageTemplateItem[][], + } { + let maxVersion = 0; + const filledTemplates: MessageTemplateItem[][] = []; + uiTemplates.forEach((uiTemplate: SendbirdUiTemplate) => { + maxVersion = Math.max(maxVersion, uiTemplate.version); + filledTemplates.push(uiTemplate.body.items); + }); + return { + maxVersion, + filledTemplates, + }; } function getFilledMessageTemplateItemsForSimpleTemplate( templateItems: MessageTemplateItem[], - colorVariables: Record, + colorVariables?: Record ): MessageTemplateItem[] { - const filledMessageTemplateItems: MessageTemplateItem[] = getFilledMessageTemplateWithData( - templateItems, - templateData.variables ?? {}, + const filledMessageTemplateItems: MessageTemplateItem[] = getFilledMessageTemplateWithData({ + template: templateItems, + templateData: templateData?.variables ?? {}, colorVariables, theme, - ); + }); return filledMessageTemplateItems; } @@ -167,47 +207,59 @@ export function TemplateMessageItemBody({ logger.error('TemplateMessageItemBody | parsed template is missing ui_template: ', parsedUiTemplate); throw new Error(); } - if ( - templateData.view_variables - || parsedUiTemplate[0].type === CarouselType - || typeof parsedUiTemplate[0]['items'] === 'string' - || parsedUiTemplate[0]['spacing'] - ) { + /** + * Composite template validation + */ + if (parsedUiTemplate[0].type === CarouselType) { if (!templateData.view_variables) { logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData); throw new Error(); } const carouselItem = parsedUiTemplate[0] as unknown as CarouselItem; - if (carouselItem.type !== CarouselType - || typeof carouselItem.items !== 'string' - || !startsWithAtAndEndsWithBraces(carouselItem.items) - || !carouselItem.spacing - ) { - logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem); - throw new Error(); - } if (parsedUiTemplate.length > 1) { // TODO: in future, support multiple templates logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate); throw new Error(); } - const reservationKey = removeAtAndBraces(carouselItem.items); - const simpleTemplateDataList: SimpleTemplateData[] | undefined = templateData.view_variables[reservationKey]; - if (!simpleTemplateDataList) { - logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables); + if (typeof carouselItem.items === 'string') { + if (!startsWithAtAndEndsWithBraces(carouselItem.items)) { + logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem); + throw new Error(); + } + const reservationKey = removeAtAndBraces(carouselItem.items); + const simpleTemplateDataList: SimpleTemplateData[] | undefined = templateData.view_variables[reservationKey]; + if (!simpleTemplateDataList) { + logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables); + throw new Error(); + } + const { maxVersion, filledTemplates } = + getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload( + simpleTemplateDataList + ); + result.templateVersion = Math.max(cachedTemplate.version, maxVersion); + result.filledMessageTemplateItemsList = [{ + type: carouselItem.type as any, + spacing: carouselItem.spacing, + items: filledTemplates, + }]; + } else if (Array.isArray(carouselItem.items)) { + const { maxVersion, filledTemplates } = + getFilledMessageTemplateItemsForCarouselTemplate( + carouselItem.items + ); + result.templateVersion = Math.max(cachedTemplate.version, maxVersion); + result.filledMessageTemplateItemsList = [{ + type: carouselItem.type as any, + spacing: carouselItem.spacing, + items: filledTemplates, + }]; + } else { + logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem); throw new Error(); } - result.templateVersion = cachedTemplate.version; - result.filledMessageTemplateItemsList = [{ - type: carouselItem.type as any, - spacing: carouselItem.spacing, - items: getFilledMessageTemplateItemsForCarouselTemplate( - simpleTemplateDataList, - ), - }]; } else { result.filledMessageTemplateItemsList = getFilledMessageTemplateItemsForSimpleTemplate( parsedUiTemplate, - cachedTemplate.colorVariables, + cachedTemplate.colorVariables ); } } diff --git a/src/ui/TemplateMessageItemBody/types.ts b/src/ui/TemplateMessageItemBody/types.ts index ab3425b13..09931b181 100644 --- a/src/ui/TemplateMessageItemBody/types.ts +++ b/src/ui/TemplateMessageItemBody/types.ts @@ -86,7 +86,7 @@ export const CarouselType = 'carouselView'; export interface CarouselItem { type: string; spacing: number; - items: string; // Reservation key. ex. "{@some_key}" + items: string | SendbirdUiTemplate[]; // Reservation key. ex. "{@some_key}" } // FIXME: This needs to be updated in the future. @@ -99,16 +99,18 @@ export type SimpleTemplateData = { variables?: Record; }; +export interface SendbirdUiTemplate { + version: number; + body: { + items: MessageTemplateItem[]; + }; +} + export type SendbirdMessageTemplate = { key: string; created_at: number; updated_at: number; - ui_template: { - version: number; - body: { - items: MessageTemplateItem[]; - }; - }; + ui_template: SendbirdUiTemplate; name?: string; color_variables?: Record; }; From b132fffcee9c21bd01f93314c77b1394bb705f65 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:05:30 +0900 Subject: [PATCH 02/12] Fix carousel timestamp location for fallback message --- src/ui/MessageContent/MessageBody/index.tsx | 4 ++ src/ui/MessageContent/index.tsx | 20 ++++-- .../FallbackTemplateMessageItemBody.tsx | 5 ++ src/ui/TemplateMessageItemBody/index.tsx | 67 +++++++++++++------ src/utils/index.ts | 11 --- 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/ui/MessageContent/MessageBody/index.tsx b/src/ui/MessageContent/MessageBody/index.tsx index 3a9e370ca..07b1537c7 100644 --- a/src/ui/MessageContent/MessageBody/index.tsx +++ b/src/ui/MessageContent/MessageBody/index.tsx @@ -23,12 +23,14 @@ import { match } from 'ts-pattern'; import TemplateMessageItemBody from '../../TemplateMessageItemBody'; const MESSAGE_ITEM_BODY_CLASSNAME = 'sendbird-message-content__middle__message-item-body'; +export type RenderedTemplateBodyType = 'failed' | 'composite' | 'simple'; export interface MessageBodyProps { channel: Nullable; message: CoreMessageType; showFileViewer?: (bool: boolean) => void; onMessageHeightChange?: (isBottomMessageAffected?: boolean) => void; + onTemplateMessageRenderedCallback?: (renderedTemplateBodyType: RenderedTemplateBodyType) => void; mouseHover: boolean; isMobile: boolean; @@ -43,6 +45,7 @@ export default function MessageBody(props: MessageBodyProps): ReactElement { channel, showFileViewer, onMessageHeightChange, + onTemplateMessageRenderedCallback, mouseHover, isMobile, @@ -66,6 +69,7 @@ export default function MessageBody(props: MessageBodyProps): ReactElement { message={message as BaseMessage} isByMe={isByMe} theme={config?.theme as SendbirdTheme} + onTemplateMessageRenderedCallback={onTemplateMessageRenderedCallback} /> )) .when((message) => isOgMessageEnabledInGroupChannel diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 24d1581e1..ad3c54fc1 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -44,6 +44,7 @@ import MessageFeedbackModal from '../../modules/Channel/components/MessageFeedba import { SbFeedbackStatus } from './types'; import MessageFeedbackFailedModal from '../../modules/Channel/components/MessageFeedbackFailedModal'; import { MobileBottomSheetProps } from '../MobileMenu/types'; +import { CarouselType, MessageTemplateData, MessageTemplateItem } from '../TemplateMessageItemBody/types'; export interface MessageContentProps { className?: string | Array; @@ -136,7 +137,7 @@ export default function MessageContent(props: MessageContentProps): ReactElement } = props; const { dateLocale } = useLocalization(); - const { config, eventHandlers } = useSendbirdStateContext?.() || {}; + const { config, eventHandlers, utils } = useSendbirdStateContext?.() || {}; const onPressUserProfileHandler = eventHandlers?.reaction?.onPressUserProfile; const contentRef = useRef(null); const timestampRef = useRef(null); @@ -152,11 +153,21 @@ export default function MessageContent(props: MessageContentProps): ReactElement const [showFeedbackModal, setShowFeedbackModal] = useState(false); const [feedbackFailedText, setFeedbackFailedText] = useState(''); const [totalBottom, setTotalBottom] = useState(0); - - const uiContainerType: UI_CONTAINER_TYPES = getMessageContentMiddleClassNameByContainerType({ + const [uiContainerType, setUiContainerType] = useState(getMessageContentMiddleClassNameByContainerType({ message, isMobile, - }); + })); + + const onTemplateMessageRenderedCallback = (renderedTemplateType: 'failed' | 'composite' | 'simple') => { + if (renderedTemplateType === 'failed') { + setUiContainerType(UI_CONTAINER_TYPES.DEFAULT); + } else if (renderedTemplateType === 'composite') { + /** + * Composite templates must have default carousel view irregardless of given containerType. + */ + setUiContainerType(UI_CONTAINER_TYPES.DEFAULT_CAROUSEL); + } + }; const { stringSet } = useContext(LocalizationContext); @@ -382,6 +393,7 @@ export default function MessageContent(props: MessageContentProps): ReactElement config, isReactionEnabledInChannel, isByMe, + onTemplateMessageRenderedCallback, }) } {/* reactions */} diff --git a/src/ui/TemplateMessageItemBody/FallbackTemplateMessageItemBody.tsx b/src/ui/TemplateMessageItemBody/FallbackTemplateMessageItemBody.tsx index e940f1528..87cd2ce45 100644 --- a/src/ui/TemplateMessageItemBody/FallbackTemplateMessageItemBody.tsx +++ b/src/ui/TemplateMessageItemBody/FallbackTemplateMessageItemBody.tsx @@ -3,20 +3,25 @@ import React, { ReactElement, useContext } from 'react'; import { LocalizationContext } from '../../lib/LocalizationContext'; import { getClassName } from '../../utils'; import Label, { LabelColors, LabelTypography } from '../Label'; +import { RenderedTemplateBodyType } from '../MessageContent/MessageBody'; export interface FallbackTemplateMessageItemBodyProps { className?: string | Array; message: BaseMessage; isByMe?: boolean; + onTemplateMessageRenderedCallback?: (renderedTemplateBodyType: RenderedTemplateBodyType) => void; } export function FallbackTemplateMessageItemBody({ className, message, isByMe, + onTemplateMessageRenderedCallback = () => { /* noop */ }, }: FallbackTemplateMessageItemBodyProps): ReactElement { const { stringSet } = useContext(LocalizationContext); const text = message['message']; + onTemplateMessageRenderedCallback('failed'); + return (
void; + onTemplateMessageRenderedCallback?: (renderedTemplateBodyType: RenderedTemplateBodyType) => void; } /** @@ -71,16 +73,27 @@ export function TemplateMessageItemBody({ message, isByMe = false, theme = 'light', + onTemplateMessageRenderedCallback = () => { /* noop */ }, }: TemplateMessageItemBodyProps): ReactElement { const templateData: MessageTemplateData | undefined = message.extendedMessagePayload?.['template'] as MessageTemplateData; if (!templateData?.key) { - return ; + return ; } const templateKey = templateData.key; const globalState = useSendbirdStateContext(); if (!globalState) { - return ; + return ; } const { @@ -155,7 +168,7 @@ export function TemplateMessageItemBody({ function getFilledMessageTemplateItemsForSimpleTemplate( templateItems: MessageTemplateItem[], - colorVariables?: Record + colorVariables?: Record, ): MessageTemplateItem[] { const filledMessageTemplateItems: MessageTemplateItem[] = getFilledMessageTemplateWithData({ template: templateItems, @@ -211,10 +224,6 @@ export function TemplateMessageItemBody({ * Composite template validation */ if (parsedUiTemplate[0].type === CarouselType) { - if (!templateData.view_variables) { - logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData); - throw new Error(); - } const carouselItem = parsedUiTemplate[0] as unknown as CarouselItem; if (parsedUiTemplate.length > 1) { // TODO: in future, support multiple templates logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate); @@ -225,16 +234,20 @@ export function TemplateMessageItemBody({ logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem); throw new Error(); } + if (!templateData.view_variables) { + logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData); + throw new Error(); + } const reservationKey = removeAtAndBraces(carouselItem.items); const simpleTemplateDataList: SimpleTemplateData[] | undefined = templateData.view_variables[reservationKey]; if (!simpleTemplateDataList) { logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables); throw new Error(); } - const { maxVersion, filledTemplates } = - getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload( - simpleTemplateDataList - ); + const { maxVersion, filledTemplates } = getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload( + simpleTemplateDataList, + ); + result.isComposite = true; result.templateVersion = Math.max(cachedTemplate.version, maxVersion); result.filledMessageTemplateItemsList = [{ type: carouselItem.type as any, @@ -242,10 +255,10 @@ export function TemplateMessageItemBody({ items: filledTemplates, }]; } else if (Array.isArray(carouselItem.items)) { - const { maxVersion, filledTemplates } = - getFilledMessageTemplateItemsForCarouselTemplate( - carouselItem.items - ); + const { maxVersion, filledTemplates } = getFilledMessageTemplateItemsForCarouselTemplate( + carouselItem.items, + ); + result.isComposite = true; result.templateVersion = Math.max(cachedTemplate.version, maxVersion); result.filledMessageTemplateItemsList = [{ type: carouselItem.type as any, @@ -259,7 +272,7 @@ export function TemplateMessageItemBody({ } else { result.filledMessageTemplateItemsList = getFilledMessageTemplateItemsForSimpleTemplate( parsedUiTemplate, - cachedTemplate.colorVariables + cachedTemplate.colorVariables, ); } } @@ -311,13 +324,20 @@ export function TemplateMessageItemBody({ } if (renderData.isErrored) { - return ; + return ; } if (renderData.filledMessageTemplateItemsList.length === 0) { return ; } + onTemplateMessageRenderedCallback(renderData.isComposite ? 'composite' : 'simple'); + return (
} + fallbackMessage={ + + } logger={logger} > ( export const isTemplateMessage = (message: CoreMessageType): boolean => !!( message && message.extendedMessagePayload?.['template'] ); - -export const isCompositeTemplateMessage = (message: CoreMessageType): boolean => !!( - message && message.extendedMessagePayload?.['template']?.['view_variables'] -); - export enum UI_CONTAINER_TYPES { DEFAULT = '', WIDE = 'ui_container_type__wide', @@ -299,12 +294,6 @@ export const getMessageContentMiddleClassNameByContainerType = ({ * WIDE: all message types. */ const containerType: string | undefined = message.extendedMessagePayload?.['ui']?.['container_type']; - if (isCompositeTemplateMessage(message)) { - /** - * Composite templates must have default carousel view irregardless of given containerType. - */ - return UI_CONTAINER_TYPES.DEFAULT_CAROUSEL; - } if (!isMobile) return UI_CONTAINER_TYPES.DEFAULT; if (containerType === MessageContentMiddleContainerType.WIDE) { return UI_CONTAINER_TYPES.WIDE; From a60126e9636bc989d5b457363bdc1e9e96dfc604 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:06:58 +0900 Subject: [PATCH 03/12] minor fix --- src/ui/TemplateMessageItemBody/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index ca3aea5b7..8f7486f37 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -270,6 +270,7 @@ export function TemplateMessageItemBody({ throw new Error(); } } else { + result.templateVersion = cachedTemplate.version; result.filledMessageTemplateItemsList = getFilledMessageTemplateItemsForSimpleTemplate( parsedUiTemplate, cachedTemplate.colorVariables, From ac604b53ce5736f3e48fcd6801dd83f72a13dc64 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:15:53 +0900 Subject: [PATCH 04/12] minor fix --- src/ui/MessageContent/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index ad3c54fc1..35068a8e5 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -44,7 +44,6 @@ import MessageFeedbackModal from '../../modules/Channel/components/MessageFeedba import { SbFeedbackStatus } from './types'; import MessageFeedbackFailedModal from '../../modules/Channel/components/MessageFeedbackFailedModal'; import { MobileBottomSheetProps } from '../MobileMenu/types'; -import { CarouselType, MessageTemplateData, MessageTemplateItem } from '../TemplateMessageItemBody/types'; export interface MessageContentProps { className?: string | Array; @@ -137,7 +136,7 @@ export default function MessageContent(props: MessageContentProps): ReactElement } = props; const { dateLocale } = useLocalization(); - const { config, eventHandlers, utils } = useSendbirdStateContext?.() || {}; + const { config, eventHandlers } = useSendbirdStateContext?.() || {}; const onPressUserProfileHandler = eventHandlers?.reaction?.onPressUserProfile; const contentRef = useRef(null); const timestampRef = useRef(null); From 77462a25d51870c17af08b104273e91f962f85f8 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:36:42 +0900 Subject: [PATCH 05/12] apply feeds --- src/ui/TemplateMessageItemBody/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index 8f7486f37..cc7a0a290 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -125,7 +125,7 @@ export function TemplateMessageItemBody({ simpleTemplateDataList.forEach((simpleTemplateData: SimpleTemplateData) => { const simpleTemplateKey = simpleTemplateData.key; if (!simpleTemplateKey) { - logger.error('TemplateMessageItemBody | simple template keys are not found in view_variables: ', simpleTemplateDataList); + logger.error('TemplateMessageItemBody | simple template keys are not found in view_variables: ', simpleTemplateDataList, message); throw new Error(); } const simpleCachedTemplate = getCachedTemplate(simpleTemplateKey); @@ -206,7 +206,7 @@ export function TemplateMessageItemBody({ }); }); } catch (e) { - logger.error('TemplateMessageItemBody | received view_variables is malformed: ', templateData); + logger.error('TemplateMessageItemBody | received view_variables is malformed: ', templateData, message); result.isErrored = true; return result; } @@ -217,7 +217,7 @@ export function TemplateMessageItemBody({ } else { const parsedUiTemplate: MessageTemplateItem[] = JSON.parse(cachedTemplate.uiTemplate); if (!Array.isArray(parsedUiTemplate) || parsedUiTemplate.length === 0) { - logger.error('TemplateMessageItemBody | parsed template is missing ui_template: ', parsedUiTemplate); + logger.error('TemplateMessageItemBody | parsed template is missing ui_template: ', parsedUiTemplate, message); throw new Error(); } /** @@ -226,22 +226,22 @@ export function TemplateMessageItemBody({ if (parsedUiTemplate[0].type === CarouselType) { const carouselItem = parsedUiTemplate[0] as unknown as CarouselItem; if (parsedUiTemplate.length > 1) { // TODO: in future, support multiple templates - logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate); + logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate, message); throw new Error(); } if (typeof carouselItem.items === 'string') { if (!startsWithAtAndEndsWithBraces(carouselItem.items)) { - logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem); + logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem, message); throw new Error(); } if (!templateData.view_variables) { - logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData); + logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData, message); throw new Error(); } const reservationKey = removeAtAndBraces(carouselItem.items); const simpleTemplateDataList: SimpleTemplateData[] | undefined = templateData.view_variables[reservationKey]; if (!simpleTemplateDataList) { - logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables); + logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables, message); throw new Error(); } const { maxVersion, filledTemplates } = getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload( @@ -266,7 +266,7 @@ export function TemplateMessageItemBody({ items: filledTemplates, }]; } else { - logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem); + logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem, message); throw new Error(); } } else { From 8ca77f32e0f8959508e738cfbdc2ed3d064498f7 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:45:34 +0900 Subject: [PATCH 06/12] add error messages --- src/ui/TemplateMessageItemBody/index.tsx | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index cc7a0a290..afb609e5d 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -125,8 +125,8 @@ export function TemplateMessageItemBody({ simpleTemplateDataList.forEach((simpleTemplateData: SimpleTemplateData) => { const simpleTemplateKey = simpleTemplateData.key; if (!simpleTemplateKey) { - logger.error('TemplateMessageItemBody | simple template keys are not found in view_variables: ', simpleTemplateDataList, message); - throw new Error(); + logger.error('TemplateMessageItemBody | simple template keys are not found in view_variables: ', simpleTemplateDataList); + throw new Error('TemplateMessageItemBody | simple template keys are not found in view_variables.'); } const simpleCachedTemplate = getCachedTemplate(simpleTemplateKey); cachedSimpleTemplates.push(simpleCachedTemplate); @@ -206,7 +206,7 @@ export function TemplateMessageItemBody({ }); }); } catch (e) { - logger.error('TemplateMessageItemBody | received view_variables is malformed: ', templateData, message); + logger.error('TemplateMessageItemBody | received view_variables is malformed: ', templateData); result.isErrored = true; return result; } @@ -217,8 +217,8 @@ export function TemplateMessageItemBody({ } else { const parsedUiTemplate: MessageTemplateItem[] = JSON.parse(cachedTemplate.uiTemplate); if (!Array.isArray(parsedUiTemplate) || parsedUiTemplate.length === 0) { - logger.error('TemplateMessageItemBody | parsed template is missing ui_template: ', parsedUiTemplate, message); - throw new Error(); + logger.error('TemplateMessageItemBody | parsed template is missing ui_template: ', parsedUiTemplate); + throw new Error('TemplateMessageItemBody | parsed template is missing ui_template. See error log in console for details'); } /** * Composite template validation @@ -226,23 +226,23 @@ export function TemplateMessageItemBody({ if (parsedUiTemplate[0].type === CarouselType) { const carouselItem = parsedUiTemplate[0] as unknown as CarouselItem; if (parsedUiTemplate.length > 1) { // TODO: in future, support multiple templates - logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate, message); - throw new Error(); + logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate); + throw new Error('TemplateMessageItemBody | composite template currently does not support multiple items. See error log in console for details'); } if (typeof carouselItem.items === 'string') { if (!startsWithAtAndEndsWithBraces(carouselItem.items)) { - logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem, message); - throw new Error(); + logger.error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}": ', templateKey, carouselItem); + throw new Error('TemplateMessageItemBody | composite template with reservation key must follow the following string format "{@your-reservation-key}". See error log in console for details'); } if (!templateData.view_variables) { - logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData, message); - throw new Error(); + logger.error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables: ', templateKey, templateData); + throw new Error('TemplateMessageItemBody | template key suggests composite template but template data is missing view_variables. See error log in console for details'); } const reservationKey = removeAtAndBraces(carouselItem.items); const simpleTemplateDataList: SimpleTemplateData[] | undefined = templateData.view_variables[reservationKey]; if (!simpleTemplateDataList) { - logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables, message); - throw new Error(); + logger.error('TemplateMessageItemBody | no reservation key found in view_variables: ', reservationKey, templateData.view_variables); + throw new Error('TemplateMessageItemBody | no reservation key found in view_variables. See error log in console for details'); } const { maxVersion, filledTemplates } = getFilledMessageTemplateItemsForCarouselTemplateByMessagePayload( simpleTemplateDataList, @@ -266,8 +266,8 @@ export function TemplateMessageItemBody({ items: filledTemplates, }]; } else { - logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem, message); - throw new Error(); + logger.error('TemplateMessageItemBody | composite template is malformed: ', templateKey, carouselItem); + throw new Error('TemplateMessageItemBody | composite template is malformed. See error log in console for details'); } } else { result.templateVersion = cachedTemplate.version; @@ -315,7 +315,7 @@ export function TemplateMessageItemBody({ ) { keysToUpdate.push(templateKey); } else if (waitingTemplateKeyData.erroredMessageIds.indexOf(message.messageId) > -1) { - throw new Error(); + throw new Error(`TemplateMessageItemBody | fetching template key ${templateKey} for messageId: ${message.messageId} has failed.`); } }); if (keysToUpdate.length > 0) { From 3422154252648278e939b1e86924989d993e257f Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 17:52:22 +0900 Subject: [PATCH 07/12] use memo --- src/ui/MessageContent/index.tsx | 36 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 35068a8e5..15aca7c34 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'; +import React, { ReactElement, ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'; import format from 'date-fns/format'; import './index.scss'; @@ -151,7 +151,6 @@ export default function MessageContent(props: MessageContentProps): ReactElement const [showFeedbackOptionsMenu, setShowFeedbackOptionsMenu] = useState(false); const [showFeedbackModal, setShowFeedbackModal] = useState(false); const [feedbackFailedText, setFeedbackFailedText] = useState(''); - const [totalBottom, setTotalBottom] = useState(0); const [uiContainerType, setUiContainerType] = useState(getMessageContentMiddleClassNameByContainerType({ message, isMobile, @@ -210,6 +209,22 @@ export default function MessageContent(props: MessageContentProps): ReactElement const isTimestampBottom = !!uiContainerType; + const getTotalBottom = (): number => { + let sum = 2; + if (timestampRef.current && isTimestampBottom) { + sum += 4 + timestampRef.current.clientHeight; + } + if (threadRepliesRef.current) { + sum += 4 + threadRepliesRef.current.clientHeight; + } + if (feedbackButtonsRef.current) { + sum += 4 + feedbackButtonsRef.current.clientHeight; + } + return sum; + }; + + const totalBottom = useMemo(() => getTotalBottom(), [isTimestampBottom]); + const onCloseFeedbackForm = () => { setShowFeedbackModal(false); }; @@ -243,23 +258,6 @@ export default function MessageContent(props: MessageContentProps): ReactElement return (); } - useEffect(() => { - const getTotalBottom = (): number => { - let sum = 2; - if (timestampRef.current && isTimestampBottom) { - sum += 4 + timestampRef.current.clientHeight; - } - if (threadRepliesRef.current) { - sum += 4 + threadRepliesRef.current.clientHeight; - } - if (feedbackButtonsRef.current) { - sum += 4 + feedbackButtonsRef.current.clientHeight; - } - return sum; - }; - setTotalBottom(getTotalBottom()); - }, [isTimestampBottom]); - return (
Date: Mon, 1 Apr 2024 18:03:12 +0900 Subject: [PATCH 08/12] lint fix --- src/ui/MessageContent/index.tsx | 4 ++-- src/ui/TemplateMessageItemBody/index.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 15aca7c34..b9fd33bf2 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { ReactElement, ReactNode, useContext, useMemo, useRef, useState } from 'react'; import format from 'date-fns/format'; import './index.scss'; @@ -222,7 +222,7 @@ export default function MessageContent(props: MessageContentProps): ReactElement } return sum; }; - + const totalBottom = useMemo(() => getTotalBottom(), [isTimestampBottom]); const onCloseFeedbackForm = () => { diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index afb609e5d..d696c243d 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -21,6 +21,7 @@ import FallbackTemplateMessageItemBody from './FallbackTemplateMessageItemBody'; import LoadingTemplateMessageItemBody from './LoadingTemplateMessageItemBody'; import MessageTemplateErrorBoundary from '../MessageTemplate/messageTemplateErrorBoundary'; import { RenderedTemplateBodyType } from '../MessageContent/MessageBody'; +import { CompositeComponentType } from '@sendbird/uikit-message-template'; const TEMPLATE_FETCH_RETRY_BUFFER_TIME_IN_MILLIES = 500; // It takes about 450ms for isError update @@ -250,7 +251,7 @@ export function TemplateMessageItemBody({ result.isComposite = true; result.templateVersion = Math.max(cachedTemplate.version, maxVersion); result.filledMessageTemplateItemsList = [{ - type: carouselItem.type as any, + type: carouselItem.type as CompositeComponentType, spacing: carouselItem.spacing, items: filledTemplates, }]; @@ -261,7 +262,7 @@ export function TemplateMessageItemBody({ result.isComposite = true; result.templateVersion = Math.max(cachedTemplate.version, maxVersion); result.filledMessageTemplateItemsList = [{ - type: carouselItem.type as any, + type: carouselItem.type as CompositeComponentType, spacing: carouselItem.spacing, items: filledTemplates, }]; From aa01637fdf9b49d2195fc1eaf73d7b9a2d8678c6 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 18:15:31 +0900 Subject: [PATCH 09/12] apply feeds --- .../messageTemplateErrorBoundary.tsx | 5 ++++ .../FallbackTemplateMessageItemBody.tsx | 5 ---- src/ui/TemplateMessageItemBody/index.tsx | 29 +++++++------------ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/ui/MessageTemplate/messageTemplateErrorBoundary.tsx b/src/ui/MessageTemplate/messageTemplateErrorBoundary.tsx index cae0522d6..5394c77de 100644 --- a/src/ui/MessageTemplate/messageTemplateErrorBoundary.tsx +++ b/src/ui/MessageTemplate/messageTemplateErrorBoundary.tsx @@ -1,9 +1,12 @@ import { Component, ErrorInfo, ReactNode } from 'react'; import { LoggerInterface } from '../../lib/Logger'; +import { RenderedTemplateBodyType } from '../MessageContent/MessageBody'; interface ErrorBoundaryProps { children: ReactNode; fallbackMessage: ReactNode; + onTemplateMessageRenderedCallback: (renderedTemplateBodyType: RenderedTemplateBodyType) => void; + isComposite?: boolean; logger?: LoggerInterface; } @@ -27,8 +30,10 @@ export class MessageTemplateErrorBoundary extends Component; message: BaseMessage; isByMe?: boolean; - onTemplateMessageRenderedCallback?: (renderedTemplateBodyType: RenderedTemplateBodyType) => void; } export function FallbackTemplateMessageItemBody({ className, message, isByMe, - onTemplateMessageRenderedCallback = () => { /* noop */ }, }: FallbackTemplateMessageItemBodyProps): ReactElement { const { stringSet } = useContext(LocalizationContext); const text = message['message']; - onTemplateMessageRenderedCallback('failed'); - return (
{ /* noop */ }, }: TemplateMessageItemBodyProps): ReactElement { const templateData: MessageTemplateData | undefined = message.extendedMessagePayload?.['template'] as MessageTemplateData; - if (!templateData?.key) { + + const getFailedBody = () => { + onTemplateMessageRenderedCallback('failed'); return ; + }; + + if (!templateData?.key) { + return getFailedBody(); } const templateKey = templateData.key; const globalState = useSendbirdStateContext(); if (!globalState) { - return ; + return getFailedBody(); } const { @@ -326,20 +326,12 @@ export function TemplateMessageItemBody({ } if (renderData.isErrored) { - return ; + return getFailedBody(); } if (renderData.filledMessageTemplateItemsList.length === 0) { return ; } - - onTemplateMessageRenderedCallback(renderData.isComposite ? 'composite' : 'simple'); - return (
} + onTemplateMessageRenderedCallback={onTemplateMessageRenderedCallback} + isComposite={renderData.isComposite} logger={logger} > Date: Mon, 1 Apr 2024 18:17:19 +0900 Subject: [PATCH 10/12] apply feeds --- src/ui/TemplateMessageItemBody/index.tsx | 3 +-- src/ui/TemplateMessageItemBody/types.ts | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ui/TemplateMessageItemBody/index.tsx b/src/ui/TemplateMessageItemBody/index.tsx index 9ba268b3f..7b381fa0c 100644 --- a/src/ui/TemplateMessageItemBody/index.tsx +++ b/src/ui/TemplateMessageItemBody/index.tsx @@ -5,7 +5,6 @@ import { getClassName, removeAtAndBraces, startsWithAtAndEndsWithBraces } from ' import MessageTemplateWrapper from '../../modules/GroupChannel/components/MessageTemplateWrapper'; import { CarouselItem, - CarouselType, MessageTemplateData, MessageTemplateItem, SendbirdUiTemplate, @@ -224,7 +223,7 @@ export function TemplateMessageItemBody({ /** * Composite template validation */ - if (parsedUiTemplate[0].type === CarouselType) { + if (parsedUiTemplate[0].type === CompositeComponentType.Carousel) { const carouselItem = parsedUiTemplate[0] as unknown as CarouselItem; if (parsedUiTemplate.length > 1) { // TODO: in future, support multiple templates logger.error('TemplateMessageItemBody | composite template currently does not support multiple items: ', parsedUiTemplate); diff --git a/src/ui/TemplateMessageItemBody/types.ts b/src/ui/TemplateMessageItemBody/types.ts index 09931b181..8d03c2b33 100644 --- a/src/ui/TemplateMessageItemBody/types.ts +++ b/src/ui/TemplateMessageItemBody/types.ts @@ -1,4 +1,4 @@ -import type { ComponentsUnion } from '@sendbird/uikit-message-template'; +import {ComponentsUnion, CompositeComponentType} from '@sendbird/uikit-message-template'; type SendbirdFontWeight = 'bold' | 'normal'; @@ -81,10 +81,8 @@ export type MessageTemplateTheme = { export type MessageTemplateItem = ComponentsUnion['properties']; -export const CarouselType = 'carouselView'; - export interface CarouselItem { - type: string; + type: CompositeComponentType.Carousel; spacing: number; items: string | SendbirdUiTemplate[]; // Reservation key. ex. "{@some_key}" } From e2124e0cd0f1d65d565762a1e04bda8c30dfc224 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 18:39:25 +0900 Subject: [PATCH 11/12] fix error --- src/lib/dux/appInfo/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/dux/appInfo/utils.ts b/src/lib/dux/appInfo/utils.ts index cc568d5d2..ea1ce6f4c 100644 --- a/src/lib/dux/appInfo/utils.ts +++ b/src/lib/dux/appInfo/utils.ts @@ -6,7 +6,7 @@ import { SendbirdMessageTemplate } from '../../../ui/TemplateMessageItemBody/typ */ export const getProcessedTemplate = (parsedTemplate: SendbirdMessageTemplate): ProcessedMessageTemplate => { return { - version: parsedTemplate.ui_template.version, + version: Number(parsedTemplate.ui_template.version), uiTemplate: JSON.stringify(parsedTemplate.ui_template.body.items), colorVariables: parsedTemplate.color_variables, }; From b6d9ca2cadc1629ee068c3b0d13694dabb979fc1 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 1 Apr 2024 18:42:25 +0900 Subject: [PATCH 12/12] lint fix --- src/ui/TemplateMessageItemBody/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/TemplateMessageItemBody/types.ts b/src/ui/TemplateMessageItemBody/types.ts index 8d03c2b33..4a8dbce9d 100644 --- a/src/ui/TemplateMessageItemBody/types.ts +++ b/src/ui/TemplateMessageItemBody/types.ts @@ -1,4 +1,4 @@ -import {ComponentsUnion, CompositeComponentType} from '@sendbird/uikit-message-template'; +import { ComponentsUnion, CompositeComponentType } from '@sendbird/uikit-message-template'; type SendbirdFontWeight = 'bold' | 'normal';