From ffed3f041fe6c45e01a331417f70e48dab671206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Stepanovi=C4=87?= Date: Fri, 3 Nov 2023 08:59:20 +0000 Subject: [PATCH 01/12] feat: Add `` component --- README.md | 15 ++++++++ example/App.tsx | 21 ++++++++++- example/panel.html | 19 ++++++++++ lib/HtmlPanel.tsx | 94 ++++++++++++++++++++++++++++++++++++++++++++++ lib/MountedBox.tsx | 14 +++++-- lib/main.tsx | 1 + lib/ui/Chatbox.tsx | 3 ++ 7 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 example/panel.html create mode 100644 lib/HtmlPanel.tsx diff --git a/README.md b/README.md index 22683a7..3d12865 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,7 @@ Accepted props: loading - `chatboxRef` (resp. `inboxRef`, `popupRef`) - Pass a ref (created with `useRef`) and it'll be set to the vanilla JS [Chatbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Chatbox/) (resp. [Inbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/), [Popup](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Popup/)) instance. See [above](#using-refs) for an example. - All [Talk.ChatboxOptions](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Session/#ChatboxOptions) +- `children?: ReactNode` - Optional. You can provide an `` component as a child to use [HTML Panels](https://talkjs.com/docs/Features/Customizations/HTML_Panels/). Accepted events (props that start with "on"): @@ -281,6 +282,20 @@ Accepted events (props that start with "on"): Note: For `` and ``, you must provide exactly one of `conversationId` and `syncConversation`. For ``, leaving both unset selects the latest conversation this user participates in (if any). See [Inbox.select](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/#Inbox__select) for more information. +### `` + +Accepted props: + +- `url: string` - The URL you want to load inside the HTML panel. The URL can be absolute or relative. We recommend using same origin pages to have better control of the page. Learn more about HTML Panels and same origin pages [here](https://talkjs.com/docs/Features/Customizations/HTML_Panels/) + +- `height?: number` - Optional. The panel height in pixels. Defaults to `100px`. + +- `show?: boolean` - Optional. Sets the visibility of the panel. Defaults to `true`. + +- `conversationId?: string` - Optional. If given, the panel will only show up for the conversation that has an `id` matching the one given. + +- `children: React.ReactNode` - The content that gets rendered inside the `` of the panel. + ## Contributing diff --git a/example/App.tsx b/example/App.tsx index cdc480e..1edf6cc 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -3,6 +3,7 @@ import "./App.css"; import { Session, Chatbox } from "../lib/main"; import Talk from "talkjs"; import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react"; +import { HtmlPanel } from "../lib/HtmlPanel"; const convIds = ["talk-react-94872948u429843", "talk-react-194872948u429843"]; const users = [ @@ -104,6 +105,9 @@ function App() { setDn(JSON.parse(event.target!.value)); }, []); + const [panelHeight, setPanelHeight] = useState(100); + const [panelVisible, setPanelVisible] = useState(true); + if (typeof import.meta.env.VITE_APP_ID !== "string") { return (
@@ -150,8 +154,23 @@ function App() { loadingComponent={LOADING....} {...(blur ? { onBlur } : {})} style={{ width: 500, height: 600 }} - /> + > + + I am an HTML panel. + + + + +
- - + I am an HTML panel. + + + + )} +

diff --git a/example/main.tsx b/example/main.tsx index 07f7e17..27481e0 100644 --- a/example/main.tsx +++ b/example/main.tsx @@ -8,20 +8,3 @@ ReactDOM.createRoot(document.getElementById("root")!).render( , ); - -// import Talk from "talkjs"; - -// await Talk.ready; - -// const me = new Talk.User({ id: "alice", name: "Alice" }); -// const session = new Talk.Session({ appId: "Hku1c4Pt", me }); -// const chatbox = session.createChatbox(); -// const conversation = session.getOrCreateConversation("abc"); -// conversation.setParticipant(me); - -// chatbox.select(conversation); -// chatbox.mount(document.getElementById("root")!); - -// chatbox.createHtmlPanel({ url: "./example/panel.html" }); - -// document.getElementById("root")!.style.height = "100vh"; \ No newline at end of file diff --git a/example/panel.html b/example/panel.html index 70e2e08..a451a48 100644 --- a/example/panel.html +++ b/example/panel.html @@ -15,7 +15,5 @@ } - - Default content here - + diff --git a/lib/HtmlPanel.tsx b/lib/HtmlPanel.tsx index 561a093..778cbd6 100644 --- a/lib/HtmlPanel.tsx +++ b/lib/HtmlPanel.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import Talk from "talkjs"; import { BoxContext } from "./MountedBox"; @@ -31,47 +31,38 @@ export function HtmlPanel({ conversationId, children, }: HtmlPanelProps) { - const panelPromise = useRef>(undefined); const [panel, setPanel] = useState(undefined); const box = useContext(BoxContext); useEffect(() => { - function run() { - console.log("@@trying"); - if (!box || panelPromise.current) return; - console.log("@@initializing"); + async function run() { + if (!box || panel) return Promise.resolve(panel); - // const old = await panelPromise; - // old?.destroy(); + const newPanel = await box.createHtmlPanel({ + url, + conversation: conversationId, + height, + show: false, + }); - const panel = box - .createHtmlPanel({ url, conversation: conversationId, height, show }) - .then(async (panel) => { - await panel.windowLoadedPromise; - console.log("@@window loaded"); - // setPanel(panel); - return panel; - }); + await newPanel.DOMContentLoadedPromise; + if (show) { + newPanel.show(); + } - panelPromise.current = panel; + setPanel(newPanel); + return newPanel; } - run(); + const panelPromise = run(); - // return () => { - // console.log("@@cleanup", panelPromise); - // if (panelPromise) { - // panelPromise.current?.then((panel) => { - // panelPromise.current = undefined; - // panel.destroy().then(() => { - // console.log("@@deleted"); - // setPanel(undefined); - // }); - // }); - // } else { - // setPanel(undefined); - // } - // }; + return () => { + panelPromise.then((panel) => { + panel?.destroy().then(() => { + setPanel(undefined); + }); + }); + }; // We intentionally exclude `height` and `show` from the dependency array so // that we update them later via methods instead of by re-creating the // entire panel from scratch each time. @@ -91,6 +82,5 @@ export function HtmlPanel({ } }, [panel, show]); - // return <>{panel && createPortal(children, panel.window.document.body)}; - return null; + return <>{panel && createPortal(children, panel.window.document.body)}; } diff --git a/lib/hooks.tsx b/lib/hooks.tsx index ea07478..4b6f19b 100644 --- a/lib/hooks.tsx +++ b/lib/hooks.tsx @@ -84,14 +84,11 @@ export function useUIBox< if (session?.isAlive) { const uibox = session[create](options) as R; setBox(uibox); - (window as any).ui = uibox; - console.log("@@create"); if (ref) { ref.current = uibox; } return () => { - console.log("@@destroy"); uibox.destroy(); setBox(undefined); }; From cbdf35b5741edc79f831b76ce7dddbfa153f12a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Stepanovi=C4=87?= Date: Wed, 29 Nov 2023 10:00:45 +0000 Subject: [PATCH 06/12] feat: Validate children of UIBox components --- README.md | 2 +- lib/ui/Chatbox.tsx | 14 ++++++++++++-- lib/ui/Inbox.tsx | 13 ++++++++++++- lib/ui/Popup.tsx | 13 ++++++++++++- lib/util.ts | 11 +++++++++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3d12865..cb536c7 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ Accepted props: loading - `chatboxRef` (resp. `inboxRef`, `popupRef`) - Pass a ref (created with `useRef`) and it'll be set to the vanilla JS [Chatbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Chatbox/) (resp. [Inbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/), [Popup](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Popup/)) instance. See [above](#using-refs) for an example. - All [Talk.ChatboxOptions](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Session/#ChatboxOptions) -- `children?: ReactNode` - Optional. You can provide an `` component as a child to use [HTML Panels](https://talkjs.com/docs/Features/Customizations/HTML_Panels/). +- `children?: ReactNode` - Optional. If provided, only [``](https://talkjs.com/docs/Features/Customizations/HTML_Panels/) components are allowed as children. Accepted events (props that start with "on"): diff --git a/lib/ui/Chatbox.tsx b/lib/ui/Chatbox.tsx index ab8c6b7..f29215c 100644 --- a/lib/ui/Chatbox.tsx +++ b/lib/ui/Chatbox.tsx @@ -1,7 +1,11 @@ -import { CSSProperties, ReactNode } from "react"; +import React, { CSSProperties, ReactNode } from "react"; import type Talk from "talkjs"; import { useSession } from "../SessionContext"; -import { getKeyForObject, splitObjectByPrefix } from "../util"; +import { + validateChildrenAreHtmlPanels, + getKeyForObject, + splitObjectByPrefix, +} from "../util"; import { useSetter, useConversation, useUIBox } from "../hooks"; import { FirstParameter, UIBoxProps } from "../types"; import { MountedBox } from "../MountedBox"; @@ -19,6 +23,12 @@ type ChatboxProps = UIBoxProps & export function Chatbox(props: ChatboxProps) { const session = useSession(); + if (!validateChildrenAreHtmlPanels(props.children)) { + throw new Error( + " may only have components as direct children.", + ); + } + if (session) { const key = getKeyForObject(session); return ; diff --git a/lib/ui/Inbox.tsx b/lib/ui/Inbox.tsx index 411a086..a536cf9 100644 --- a/lib/ui/Inbox.tsx +++ b/lib/ui/Inbox.tsx @@ -1,7 +1,11 @@ import { CSSProperties, ReactNode } from "react"; import type Talk from "talkjs"; import { useSession } from "../SessionContext"; -import { getKeyForObject, splitObjectByPrefix } from "../util"; +import { + getKeyForObject, + splitObjectByPrefix, + validateChildrenAreHtmlPanels, +} from "../util"; import { useSetter, useConversation, useUIBox } from "../hooks"; import { FirstParameter, UIBoxProps } from "../types"; import { MountedBox } from "../MountedBox"; @@ -13,11 +17,18 @@ type InboxProps = Partial> & loadingComponent?: ReactNode; style?: CSSProperties; className?: string; + children?: React.ReactNode; }; export function Inbox(props: InboxProps) { const session = useSession(); + if (!validateChildrenAreHtmlPanels(props.children)) { + throw new Error( + " may only have components as direct children.", + ); + } + if (session) { const key = getKeyForObject(session); return ; diff --git a/lib/ui/Popup.tsx b/lib/ui/Popup.tsx index 6ac1bc5..17de344 100644 --- a/lib/ui/Popup.tsx +++ b/lib/ui/Popup.tsx @@ -1,6 +1,10 @@ import Talk from "talkjs"; import { useSession } from "../SessionContext"; -import { getKeyForObject, splitObjectByPrefix } from "../util"; +import { + getKeyForObject, + splitObjectByPrefix, + validateChildrenAreHtmlPanels, +} from "../util"; import { useSetter, useConversation, useUIBox, useMountBox } from "../hooks"; import { EventListeners } from "../EventListeners"; import { UIBoxProps } from "../types"; @@ -9,11 +13,18 @@ type PopupProps = UIBoxProps & Talk.PopupOptions & { highlightedWords?: Parameters[0]; popupRef?: React.MutableRefObject; + children?: React.ReactNode; }; export function Popup(props: PopupProps) { const session = useSession(); + if (!validateChildrenAreHtmlPanels(props.children)) { + throw new Error( + " may only have components as direct children.", + ); + } + if (session) { const key = getKeyForObject(session); return ; diff --git a/lib/util.ts b/lib/util.ts index b9f5de5..95d793f 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -1,3 +1,6 @@ +import React from "react"; +import { HtmlPanel } from "./HtmlPanel"; + export type Func = (...args: any) => any; export interface Mountable { @@ -63,3 +66,11 @@ export function splitObjectByPrefix

( } return [prefixed, unprefixed]; } + +export function validateChildrenAreHtmlPanels(children?: React.ReactNode) { + if (!children) return true; + + return React.Children.toArray(children).every((x) => { + return typeof x === "object" && (x as any).type === HtmlPanel; + }); +} From faf5bdc81547bd06f3eece3698aa6596a7cd9e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Stepanovi=C4=87?= Date: Wed, 29 Nov 2023 11:03:14 +0000 Subject: [PATCH 07/12] refactor: Apply review comments --- README.md | 6 +++--- example/App.tsx | 8 +++++++- example/panel.html | 14 +++++++++++++- lib/HtmlPanel.tsx | 29 +++++++++++++++++++++++------ lib/util.ts | 17 +++++++++++++++-- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cb536c7..9edcf51 100644 --- a/README.md +++ b/README.md @@ -286,15 +286,15 @@ Note: For `` and ``, you must provide exactly one of `conversati Accepted props: -- `url: string` - The URL you want to load inside the HTML panel. The URL can be absolute or relative. We recommend using same origin pages to have better control of the page. Learn more about HTML Panels and same origin pages [here](https://talkjs.com/docs/Features/Customizations/HTML_Panels/) +- `url: string` - The URL you want to load inside the HTML panel. The URL can be absolute or relative. Any child components provided to this component will only be rendered if `url` has the same origin as the parent page. Learn more about HTML Panels and same origin pages [here](https://talkjs.com/docs/Features/Customizations/HTML_Panels/) - `height?: number` - Optional. The panel height in pixels. Defaults to `100px`. -- `show?: boolean` - Optional. Sets the visibility of the panel. Defaults to `true`. +- `show?: boolean` - Optional. Sets the visibility of the panel. Defaults to `true`. Changing this prop is equivalent to calling [`HtmlPanel.show()`](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/HtmlPanel/#HtmlPanel__show) and [`HtmlPanel.hide()`](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/HtmlPanel/#HtmlPanel__hide), while re-rendering the component calls [`HtmlPanel.destroy()`](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/HtmlPanel/#HtmlPanel__destroy) and [`createHtmlPanel()`](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Chatbox/#Chatbox__createHtmlPanel) in the background. - `conversationId?: string` - Optional. If given, the panel will only show up for the conversation that has an `id` matching the one given. -- `children: React.ReactNode` - The content that gets rendered inside the `` of the panel. +- `children?: React.ReactNode` - Optional. The content that gets rendered inside the `` of the panel. ## Contributing diff --git a/example/App.tsx b/example/App.tsx index 51b33a5..403f0a5 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -2,7 +2,13 @@ import "./App.css"; import { Session, Chatbox, HtmlPanel } from "../lib/main"; import Talk from "talkjs"; -import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react"; +import React, { + ChangeEvent, + useCallback, + useMemo, + useRef, + useState, +} from "react"; const convIds = ["talk-react-94872948u429843", "talk-react-194872948u429843"]; const users = [ diff --git a/example/panel.html b/example/panel.html index a451a48..8d9f2f9 100644 --- a/example/panel.html +++ b/example/panel.html @@ -3,8 +3,20 @@ - Document + + + +