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(react): Display <Button> components as normal buttons in whatsapp #1699

Merged
merged 11 commits into from
Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions packages/botonic-react/src/components/multichannel/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface MultichannelTextProps extends MultichannelViewOptions {
indexMode?: IndexMode
/** Defaults to no separator between lines*/
newline?: string
buttonsAsText?: boolean
buttonsTextSeparator?: string
}

export const MultichannelText: React.FunctionComponent<MultichannelTextProps>
Expand All @@ -39,6 +41,7 @@ export interface MultichannelButtonProps {
payload?: string
url?: string
webview?: string
asText?: boolean
}
export const MultichannelButton: React.FunctionComponent<MultichannelButtonProps>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useContext } from 'react'
import { RequestContext } from '../../contexts'
import { Button } from '../button'
import { MultichannelContext } from './multichannel-context'
import { isWhatsapp } from './multichannel-utils'
import { isWhatsapp, WHATSAPP_MAX_BUTTON_CHARS } from './multichannel-utils'

export const MultichannelButton = props => {
const requestContext = useContext(RequestContext)
Expand Down Expand Up @@ -56,15 +56,30 @@ export const MultichannelButton = props => {
return text
}

const truncateText = (text, maxLength, ellipsis = '...') => {
if (text.length > maxLength) {
return text.substring(0, maxLength - ellipsis.length) + ellipsis
}
return text
}

if (isWhatsapp(requestContext)) {
if (hasUrl()) {
return `${getText()}: ${getUrl()}`
} else if (hasPath() || hasPayload()) {
const text = getText()
increaseCurrentIndex()
return `${text}`
} else if (hasWebview()) return <Button {...props}>{getText()}</Button>
else return <Button {...props}>{props.children}</Button>
const asText = props.asText == null ? true : props.asText
if (asText) {
if (hasUrl()) {
return `${getText()}: ${getUrl()}`
} else if (hasPath() || hasPayload()) {
const text = getText()
increaseCurrentIndex()
return `${text}`
} else if (hasWebview()) return <Button {...props}>{getText()}</Button>
} else {
return (
<Button {...props}>
{truncateText(props.children, WHATSAPP_MAX_BUTTON_CHARS)}
</Button>
)
}
}
return <Button {...props}>{props.children}</Button>
}
160 changes: 133 additions & 27 deletions packages/botonic-react/src/components/multichannel/multichannel-text.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@ import { MultichannelFacebook } from './facebook/facebook'
import { MultichannelButton } from './multichannel-button'
import { MultichannelContext } from './multichannel-context'
import {
DEFAULT_WHATSAPP_MAX_BUTTON_SEPARATOR,
elementHasPostback,
elementHasUrl,
elementHasWebview,
getMultichannelButtons,
getMultichannelReplies,
isFacebook,
isMultichannelButton,
isMultichannelReply,
isWhatsapp,
MULTICHANNEL_WHATSAPP_PROPS,
WHATSAPP_MAX_BUTTONS,
} from './multichannel-utils'

const buttonTypes = {
POSTBACK: 'postback',
URL: 'url',
WEBVIEW: 'webview',
}

export const MultichannelText = props => {
const requestContext = useContext(RequestContext)
const multichannelContext = useContext(MultichannelContext)
const postbackButtonsAsText =
props.buttonsAsText == null ? true : props.buttonsAsText

let elements = []

Expand Down Expand Up @@ -47,11 +56,13 @@ export const MultichannelText = props => {
const getWhatsappButtons = () => {
const postbackButtons = []
const urlButtons = []
const webviewButtons = []
for (const button of getButtonsAndReplies()) {
if (elementHasUrl(button)) urlButtons.push(button)
if (elementHasPostback(button)) postbackButtons.push(button)
if (elementHasWebview(button)) webviewButtons.push(button)
}
return { postbackButtons, urlButtons }
return { postbackButtons, urlButtons, webviewButtons }
}

const getDefaultIndex = () => {
Expand All @@ -64,36 +75,131 @@ export const MultichannelText = props => {
return props.indexMode === 'letter' ? 'a' : 1
}

const generateMultichannelButtons = (type, newLineFirstButton = true) => {
const asText = type === buttonTypes.POSTBACK ? postbackButtonsAsText : true
const generator = (element, i) => {
const newline =
multichannelContext.messageSeparator == null &&
!newLineFirstButton &&
i === 0
? ''
: '\n'
return (
<MultichannelButton
key={`${type}${i}`}
newline={newline}
asText={asText}
{...element.props}
>
{element.props.children}
</MultichannelButton>
)
}
return generator
}

const splitPostbackButtons = postbackButtons => {
const messages = []
for (let i = 0; i < postbackButtons.length; i += WHATSAPP_MAX_BUTTONS) {
messages.push(postbackButtons.slice(i, i + WHATSAPP_MAX_BUTTONS))
}
return messages
}

if (isWhatsapp(requestContext)) {
const text = getText(props.children)
const { postbackButtons, urlButtons } = getWhatsappButtons()
const texts = getText(props.children)
const { postbackButtons, urlButtons, webviewButtons } = getWhatsappButtons()

elements = [].concat([...text], [...postbackButtons], [...urlButtons])
multichannelContext.currentIndex = getDefaultIndex()
elements = elements.map((element, i) => {
const newline =
multichannelContext.messageSeparator == null && i === 0 ? '' : '\n'
if (isMultichannelButton(element) || isMultichannelReply(element)) {
return (
<MultichannelButton key={i} newline={newline} {...element.props}>
{element.props.children}
</MultichannelButton>
const textElements = texts.map(text => {
return (props.newline || '') + text
})

const webviewButtonElements = webviewButtons.map(
generateMultichannelButtons('webview', false)
)

const buttonsTextSeparator =
props.buttonsTextSeparator || DEFAULT_WHATSAPP_MAX_BUTTON_SEPARATOR

if (
!postbackButtonsAsText &&
postbackButtons.length > WHATSAPP_MAX_BUTTONS
) {
const urlButtonElements = urlButtons.map(
generateMultichannelButtons('url', !!texts.length)
)
const postbackButtonElements = postbackButtons.map(
generateMultichannelButtons(
'postback',
!!texts.length || !!urlButtons.length
)
} else if (typeof element === 'string') {
return (props.newline || '') + element
} else {
return element
)
const messagesPostbackButtons = splitPostbackButtons(
postbackButtonElements
)

const messages = messagesPostbackButtons.map((postbackButtons, i) => {
if (i === 0) {
return [].concat(
...textElements,
...urlButtonElements,
...postbackButtons
)
} else {
return [].concat(buttonsTextSeparator, ...postbackButtons)
}
})
if (webviewButtonElements) {
messages.push([buttonsTextSeparator, ...webviewButtonElements])
}
})
if (multichannelContext.messageSeparator != null) {
return elements

return (
<>
{messages.map((message, i) => (
<Text key={i} {...MULTICHANNEL_WHATSAPP_PROPS} {...props}>
{message}
</Text>
))}
</>
)
} else {
multichannelContext.currentIndex = getDefaultIndex()
const postbackButtonElements = postbackButtons.map(
generateMultichannelButtons('postback', !!texts.length)
)
const urlButtonElements = urlButtons.map(
generateMultichannelButtons(
'url',
!!texts.length || !!postbackButtons.length
)
)

elements = [].concat(
[...textElements],
[...postbackButtonElements],
[...urlButtonElements]
)
if (multichannelContext.messageSeparator != null) {
return elements
}
const messages = [
<Text key={0} {...MULTICHANNEL_WHATSAPP_PROPS} {...props}>
{elements}
</Text>,
]
if (webviewButtonElements.length) {
messages.push(
<Text key={1} {...MULTICHANNEL_WHATSAPP_PROPS} {...props}>
{buttonsTextSeparator}
{webviewButtonElements}
</Text>
)
}

return <>{messages}</>
}
return (
<Text {...MULTICHANNEL_WHATSAPP_PROPS} {...props}>
{elements}
</Text>
)
}

if (isFacebook(requestContext)) {
const text = getText(props.children)
const multichannelFacebook = new MultichannelFacebook()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { Providers } from '@botonic/core'
*/
export const MULTICHANNEL_WHATSAPP_PROPS = { markdown: false }

export const WHATSAPP_MAX_BUTTONS = 3
export const WHATSAPP_MAX_BUTTON_CHARS = 20
export const DEFAULT_WHATSAPP_MAX_BUTTON_SEPARATOR = 'More options:'

export function isMultichannelButton(node) {
return isNodeKind(node, 'MultichannelButton')
}
Expand All @@ -31,6 +35,9 @@ export function elementHasPostback(element) {
(element.props && element.props.path)
)
}
export function elementHasWebview(element) {
return element.props && element.props.webview
}

export function getFilteredElements(node, filter) {
const elements = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Multichannel buttons: truncate text if more than 20 chars in whatsapp buttons 1`] = `
<button
payload="payload1"
>
button text with ...
</button>
`;

exports[`Multichannel buttons: with URL 1`] = `"- button text with URL: https://docs.botonic.io/"`;

exports[`Multichannel buttons: with path 1`] = `"1.- button text with path1"`;

exports[`Multichannel buttons: with payload 1`] = `"1- button text with payload1"`;

exports[`Multichannel buttons: with webview 1`] = `
<button
url="/webviews/undefined?"
>
button text with Webview
</button>
`;
Loading