feat: widget link
This commit is contained in:
+12
-4
@@ -1,16 +1,24 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import { WidgetProvider } from './widget/WidgetContext';
|
||||
import Widget from "./widget/index";
|
||||
import './assets/index.css';
|
||||
import store from "./app/store";
|
||||
import './i18n';
|
||||
import { isDarkMode } from "./common/utils";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
const hostId = new URLSearchParams(location.search).get("host");
|
||||
const themeColor = new URLSearchParams(location.search).get("themeColor") || "#1fe1f9";
|
||||
const hostId = new URLSearchParams(location.search).get("host") || 1;
|
||||
// dark mode
|
||||
if (isDarkMode()) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
root.render(
|
||||
hostId ? <Provider store={store}>
|
||||
<Widget hostId={Number(hostId)} themeColor={themeColor} />
|
||||
<WidgetProvider>
|
||||
<Widget hostId={Number(hostId)} />
|
||||
</WidgetProvider>
|
||||
</Provider> : null
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import { useWidget } from '../WidgetContext';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { useTheme } from '../ThemeContext';
|
||||
import IconClose from './close.svg';
|
||||
|
||||
type Props = {
|
||||
@@ -8,20 +8,19 @@ type Props = {
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ handleClose }) => {
|
||||
const { color, fgColor } = useTheme();
|
||||
const { color, fgColor, embed } = useWidget();
|
||||
const { name, logo } = useAppSelector(store => store.server);
|
||||
return (
|
||||
<header className="relative flex justify-between items-center h-14 px-4" style={{ backgroundColor: color }}>
|
||||
<header className="relative flex justify-between items-center h-14 px-4 py-2" style={{ backgroundColor: color }}>
|
||||
<div className="relative w-8 h-8">
|
||||
<img src={logo} alt="logo" className="w-full h-full rounded-full" />
|
||||
</div>
|
||||
<div className="flex-1 px-4 pr-2 text-lg">
|
||||
<span className="text-lg font-bold truncate text-gray-50" style={{ color: fgColor }}>{name}</span>
|
||||
</div>
|
||||
<button type='button' className='w-6 h-6'>
|
||||
|
||||
{embed && <button type='button' className='w-6 h-6'>
|
||||
<IconClose onClick={handleClose} className={fgColor == "black" ? `fill-black` : "fill-white"} />
|
||||
</button>
|
||||
</button>}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTheme } from '../../ThemeContext';
|
||||
import { useWidget } from '../../WidgetContext';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
@@ -10,10 +10,10 @@ type Props = {
|
||||
}
|
||||
|
||||
const Text = ({ content, host, sending }: Props) => {
|
||||
const { color, fgColor } = useTheme();
|
||||
const { color, fgColor } = useWidget();
|
||||
|
||||
return host ?
|
||||
<div className="text-md text-gray-900 bg-gray-100 rounded-lg px-3 py-1.5 break-words" style={{ maxWidth: 'min(((100vw - 56px) - 20px) - 64px, 360px)' }}>
|
||||
<div className="text-md text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-900 rounded-lg px-3 py-1.5 break-words" style={{ maxWidth: 'min(((100vw - 56px) - 20px) - 64px, 360px)' }}>
|
||||
{content}
|
||||
</div>
|
||||
:
|
||||
|
||||
@@ -34,7 +34,7 @@ const Index = (props: IWidgetMessage) => {
|
||||
}
|
||||
return <div className={clsx('group flex mb-3', host ? 'relative justify-start items-start' : 'items-center justify-end px-3')}>
|
||||
{host ? <><div className="w-9 h-9 absolute top-0 left-3">
|
||||
<img src={logo} alt="avatar" className='rounded-full bg-gray-100 w-9 h-9' />
|
||||
<img src={logo} alt="avatar" className='rounded-full w-9 h-9' />
|
||||
</div>
|
||||
<div className="pl-14 flex items-center">
|
||||
{contentContainer}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState, memo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useSendMessage from '../../common/hook/useSendMessage';
|
||||
import { useTheme } from '../ThemeContext';
|
||||
import { useWidget } from '../WidgetContext';
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
}
|
||||
let isComposing = false;
|
||||
const MessageInput = (props: Props) => {
|
||||
const { color } = useTheme();
|
||||
const { color } = useWidget();
|
||||
const { from, to } = props;
|
||||
const { sendMessage } = useSendMessage({
|
||||
from,
|
||||
@@ -22,13 +22,13 @@ const MessageInput = (props: Props) => {
|
||||
const [content, setContent] = useState('');
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
const textareaClassName = clsx(
|
||||
'px-2.5 py-1.5 text-sm rounded-md w-full block',
|
||||
'px-2.5 py-1.5 text-sm rounded-md w-full block dark:bg-gray-700 dark:text-gray-100',
|
||||
'min-h-[32px] max-h-[92px] h-8 resize-none overflow-y-auto',
|
||||
`ring-1 ring-gray-200 focus:ring-2 focus:ring-[${color}]`,
|
||||
`ring-1 ring-gray-200 dark:ring-gray-800 focus:ring-2 focus:ring-[${color}]`,
|
||||
'focus:outline-none',
|
||||
);
|
||||
return (
|
||||
<div className="relative border-t border-gray-300 w-full">
|
||||
<div className="relative border-t border-gray-300 dark:border-gray-600 w-full">
|
||||
<div className={'px-3 py-2 min-h-[48px]'}>
|
||||
<textarea
|
||||
// disabled={isSending}
|
||||
|
||||
@@ -16,7 +16,7 @@ const Index = ({ needLogin = false }: Props) => {
|
||||
</div>
|
||||
<div className="pl-14">
|
||||
<div className="rounded-lg" style={{ maxWidth: 'min(((100vw - 56px) - 20px) - 64px, 360px)' }}>
|
||||
<div className="text-md text-gray-900 px-3 py-1.5 bg-gray-100 rounded-lg mb-1.5">
|
||||
<div className="text-md text-gray-900 dark:text-gray-100 px-3 py-1.5 bg-gray-100 dark:bg-gray-900 rounded-lg mb-1.5">
|
||||
{needLogin ?
|
||||
`👋 Hi there, Nice to meet you! Please login before we have a nice talk 👇`
|
||||
:
|
||||
|
||||
@@ -6,6 +6,8 @@ import MessageFeed from './MessageFeed';
|
||||
import MessageInput from './MessageInput';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import useSSE from '../useSSE';
|
||||
import { useWidget } from '../WidgetContext';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = {
|
||||
hostId: number,
|
||||
@@ -13,6 +15,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const Index = ({ handleClose, hostId }: Props) => {
|
||||
const { embed } = useWidget();
|
||||
// 建立SSE连接
|
||||
useSSE();
|
||||
const { user: loginUser, token, guest: isGuest } = useAppSelector(store => store.authData);
|
||||
@@ -20,7 +23,7 @@ const Index = ({ handleClose, hostId }: Props) => {
|
||||
// no token or guest login
|
||||
const notLogin = !token || isGuest;
|
||||
return (
|
||||
<aside className="flex flex-col bg-white w-full h-full rounded-md overflow-hidden">
|
||||
<aside className={clsx("flex flex-col bg-white dark:bg-gray-700 rounded-md overflow-hidden", embed ? "w-full h-full" : `w-full h-full md:max-w-lg md:h-[calc(100vh_-_20px)] m-auto md:my-2 md:shadow-lg rounded-none md:rounded-md`)}>
|
||||
<Header handleClose={handleClose} />
|
||||
{/* message list */}
|
||||
<main id='MESSAGE_LIST_CONTAINER' className="relative flex-1 overflow-y-auto scroll-smooth">
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createContext, useContext, ReactNode } from 'react';
|
||||
import { getContrastColor } from '../common/utils';
|
||||
|
||||
const color = decodeURIComponent(new URLSearchParams(location.search).get("themeColor") || "#1fe1f9");
|
||||
const fgColor = getContrastColor(color);
|
||||
const ThemeContext = createContext({ color, fgColor });
|
||||
|
||||
|
||||
function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return <ThemeContext.Provider value={{ color, fgColor }} >{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { ThemeProvider, useTheme };
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createContext, useContext, ReactNode } from 'react';
|
||||
import { getContrastColor } from '../common/utils';
|
||||
|
||||
const color = decodeURIComponent(new URLSearchParams(location.search).get("themeColor") || "#1fe1f9");
|
||||
const fgColor = getContrastColor(color);
|
||||
// 判断是否是iframe上下文
|
||||
const embed = window.location !== window.parent.location;
|
||||
const WidgetContext = createContext({ color, fgColor, embed });
|
||||
|
||||
|
||||
function WidgetProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return <WidgetContext.Provider value={{ color, fgColor, embed }} >{children}</WidgetContext.Provider>;
|
||||
}
|
||||
|
||||
function useWidget() {
|
||||
const context = useContext(WidgetContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useWidget must be used within a WidgetProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { WidgetProvider, useWidget };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useGetServerQuery } from "../app/services/server";
|
||||
import { ThemeProvider } from "./ThemeContext";
|
||||
import { useWidget } from "./WidgetContext";
|
||||
import Icon from "./Icon";
|
||||
import Popup from "./Popup";
|
||||
import useCache from "./useCache";
|
||||
@@ -9,6 +9,7 @@ type Props = {
|
||||
};
|
||||
|
||||
function Widget({ hostId }: Props) {
|
||||
const { embed } = useWidget();
|
||||
const { rehydrated } = useCache();
|
||||
const [visible, setVisible] = useState(!!new URLSearchParams(location.search).get("open"));
|
||||
const { isLoading, isError } = useGetServerQuery();
|
||||
@@ -21,7 +22,8 @@ function Widget({ hostId }: Props) {
|
||||
setVisible((prev) => !prev);
|
||||
};
|
||||
if (isLoading || isError || !rehydrated) return null;
|
||||
return <ThemeProvider> {visible ? <Popup handleClose={toggleVisible} hostId={hostId} /> : <Icon handleClick={toggleVisible} />}</ThemeProvider>;
|
||||
if (!embed) return <Popup handleClose={toggleVisible} hostId={hostId} />;
|
||||
return visible ? <Popup handleClose={toggleVisible} hostId={hostId} /> : <Icon handleClick={toggleVisible} />;
|
||||
}
|
||||
|
||||
export default Widget;
|
||||
|
||||
Reference in New Issue
Block a user