feat: widget link

This commit is contained in:
Tristan Yang
2023-02-20 10:07:56 +08:00
parent 12ae089c6b
commit 7e4833594f
13 changed files with 261 additions and 101 deletions
+12 -4
View File
@@ -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
);
+5 -6
View File
@@ -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>
);
};
+3 -3
View File
@@ -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>
:
+1 -1
View File
@@ -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}
+5 -5
View File
@@ -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}
+1 -1
View File
@@ -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 👇`
:
+4 -1
View File
@@ -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">
-22
View File
@@ -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 };
+24
View File
@@ -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 };
+4 -2
View File
@@ -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;