refactor: new widget UX
This commit is contained in:
@@ -48,7 +48,7 @@ export const onMessageSendStarted = async (
|
||||
// console.log("message server mid", server_mid);
|
||||
batch(() => {
|
||||
dispatch(removeContextMessage({ id, mid: ts }));
|
||||
dispatch(addMessage({ mid: server_mid, ...tmpMsg }));
|
||||
dispatch(addMessage({ mid: server_mid, ...tmpMsg, sending: false }));
|
||||
dispatch(addContextMessage({ id, mid: server_mid }));
|
||||
});
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import Button from "./styled/Button";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,11 +10,10 @@ import { useLoginMutation } from "../../app/services/auth";
|
||||
const StyledSocialButton = styled(Button)`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
background: white !important;
|
||||
border: 1px solid #d0d5dd;
|
||||
color: #344054;
|
||||
height: 42px;
|
||||
height: 46px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
background: inherit;
|
||||
|
||||
@@ -156,11 +156,14 @@ export default function LoginPage() {
|
||||
</Button>
|
||||
</form>
|
||||
{hasDivider && <hr className="or" />}
|
||||
{enableMagicLink && <MagicLinkLogin />}
|
||||
{googleLogin && <GoogleLoginButton clientId={clientId} />}
|
||||
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} />}
|
||||
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
|
||||
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
|
||||
<div className="btns">
|
||||
|
||||
{enableMagicLink && <MagicLinkLogin />}
|
||||
{googleLogin && <GoogleLoginButton clientId={clientId} />}
|
||||
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} />}
|
||||
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
|
||||
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
|
||||
</div>
|
||||
{whoCanSignUp === "EveryOne" && <SignUpLink />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -76,6 +76,11 @@ const StyledWrapper = styled.div`
|
||||
color: #667085;
|
||||
}
|
||||
}
|
||||
>.btns{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Widget() {
|
||||
Code Example:
|
||||
</label>
|
||||
<SyntaxHighlighter id="code" language="html" style={vscDarkPlus} className="rounded">
|
||||
{`<!-- put this code snippet into your html file -->\n<script \n data-host-id="4" \n data-origin="${location.origin}" \n data-close-width="52" \n data-close-height="52" \n data-open-width="600" \n data-open-height="800" \n src="${location.origin}/widget.js" \n async \n/>`}
|
||||
{`<!-- put this code snippet into your html file -->\n<script \n data-host-id="4" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n src="${location.origin}/widget.js" \n async \n/>`}
|
||||
</SyntaxHighlighter>
|
||||
<div className="text-gray-500 text-sm mt-5 mb-2">
|
||||
Configuration Description:
|
||||
@@ -46,25 +46,21 @@ export default function Widget() {
|
||||
paramKey: "host-id",
|
||||
paramDefault: 1,
|
||||
remarks: "Assign the user chatting with visitor"
|
||||
}, {
|
||||
paramKey: "origin",
|
||||
paramDefault: 'location.origin',
|
||||
remarks: "The domain (with protocol) script load from"
|
||||
}, {
|
||||
paramKey: "close-width",
|
||||
paramDefault: `52(px)`,
|
||||
paramDefault: `48(px)`,
|
||||
remarks: "The width while widget closed"
|
||||
}, {
|
||||
paramKey: "close-height",
|
||||
paramDefault: `52(px)`,
|
||||
paramDefault: `48(px)`,
|
||||
remarks: "The height while widget closed"
|
||||
}, {
|
||||
paramKey: "open-width",
|
||||
paramDefault: `600(px)`,
|
||||
paramDefault: `380(px)`,
|
||||
remarks: "The width while widget opened"
|
||||
}, {
|
||||
paramKey: "open-height",
|
||||
paramDefault: `800(px)`,
|
||||
paramDefault: `680(px)`,
|
||||
remarks: "The height while widget opened"
|
||||
}
|
||||
].map(row => <Row key={row.paramKey} {...row} />)}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
type Props = {
|
||||
|
||||
};
|
||||
|
||||
const Index: FC<Props> = () => {
|
||||
return (
|
||||
<footer className="text-xs text-gray-300 text-center pb-2">Powered by <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400">voce.chat</a></footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -7,18 +7,20 @@ type Props = {
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ handleClose }) => {
|
||||
const { name, description, logo } = useAppSelector(store => store.server);
|
||||
const { name, logo } = useAppSelector(store => store.server);
|
||||
return (
|
||||
<div className="flex gap-2 justify-between items-center p-2">
|
||||
<div className="relative w-12 h-12">
|
||||
<header className="relative flex justify-between items-center h-14 px-4 bg-[#1fe1f9]">
|
||||
<div className="relative w-8 h-8">
|
||||
<img src={logo} alt="logo" className="w-full h-full rounded-full" />
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
<span className="text-gray-900 text-lg font-bold">{name}</span>
|
||||
{description && <span className="text-gray-400 text-sm">{description}</span>}
|
||||
<div className="flex-1 px-4 pr-2 text-lg">
|
||||
<span className="text-lg font-bold truncate text-gray-50">{name}</span>
|
||||
</div>
|
||||
<IconClose className="w-5 h-5 mr-2 cursor-pointer" onClick={handleClose} />
|
||||
</div>
|
||||
<button type='button' className='w-6 h-6'>
|
||||
|
||||
<IconClose onClick={handleClose} />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = {
|
||||
thin?: boolean
|
||||
};
|
||||
|
||||
const Index = ({ thin = false }: Props) => {
|
||||
const h = thin ? 'h-[1px]' : 'h-0.5';
|
||||
return (
|
||||
<hr className={clsx('w-full bg-gray-300 border-none', h)} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -20,7 +20,7 @@ const Login = () => {
|
||||
} = loginConfig;
|
||||
const googleLogin = enableGoogleLogin && clientId;
|
||||
return (
|
||||
<div className="w-60 flex flex-col py-2">
|
||||
<div className="w-60 flex flex-col gap-2 mt-4">
|
||||
{googleLogin && <GoogleLoginButton clientId={clientId} />}
|
||||
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} source="widget" />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
host?: boolean,
|
||||
content: string,
|
||||
sending: boolean
|
||||
}
|
||||
|
||||
const Text = ({ content, host, sending }: Props) => {
|
||||
|
||||
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)' }}>
|
||||
{content}
|
||||
</div>
|
||||
:
|
||||
<div className={clsx("text-md text-white bg-[#1fe1f9] rounded-lg px-3 py-1.5 transition-all break-words", sending ? 'opacity-70' : "")} style={{ maxWidth: 'min(((100vw - 56px) - 20px) - 64px, 360px)' }}>
|
||||
{content}
|
||||
</div>
|
||||
;
|
||||
};
|
||||
|
||||
export default Text;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { memo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import clsx from 'clsx';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
dayjs.extend(localizedFormat);
|
||||
import Text from './Text';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
|
||||
export interface IWidgetMessage {
|
||||
mid: number,
|
||||
uid: number,
|
||||
host?: boolean,
|
||||
type?: "text",
|
||||
content: string,
|
||||
create_time: number,
|
||||
sending: boolean
|
||||
}
|
||||
const Time = ({ time }: { time: number }) => {
|
||||
return <time itemProp='dateCreated' dateTime={new Date(time).toISOString()} className='hidden group-hover:inline px-1.5 text-gray-500 text-xs'>
|
||||
{dayjs.unix(time / 1000).format('LT')}
|
||||
</time>;
|
||||
};
|
||||
const Index = (props: IWidgetMessage) => {
|
||||
const { logo } = useAppSelector(store => store.server);
|
||||
const { host = false, type = "text", content, uid, create_time, sending } = props;
|
||||
let contentContainer = null;
|
||||
switch (type) {
|
||||
case "text":
|
||||
contentContainer = <Text sending={sending} content={content} host={host} uid={uid} />;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
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' />
|
||||
</div>
|
||||
<div className="pl-14 flex items-center">
|
||||
{contentContainer}
|
||||
<Time time={create_time} />
|
||||
</div>
|
||||
</> : <>
|
||||
<Time time={create_time} />
|
||||
{contentContainer}
|
||||
</>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default memo(Index, (prev, next) => {
|
||||
return prev.mid === next.mid;
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable @typescript-eslint/indent */
|
||||
import { useEffect, memo, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import Message from './Message';
|
||||
|
||||
type Props = {
|
||||
hostId: number
|
||||
};
|
||||
const triggerScrollHeight = 1200;
|
||||
const MessageFeed = ({ hostId }: Props) => {
|
||||
const firstRender = useRef(true);
|
||||
const [hasNewMsg, setHasNewMsg] = useState(false);
|
||||
const { mids, messageMap } = useAppSelector(store => { return { mids: store.userMessage.byId[hostId], messageMap: store.message }; });
|
||||
console.log("mids", mids, hostId);
|
||||
useEffect(() => {
|
||||
const container = document.querySelector("#MESSAGE_LIST_CONTAINER");
|
||||
if (container && mids && mids.length > 0) {
|
||||
const lastMsg = messageMap[mids[mids.length - 1]];
|
||||
const lastIsMyself = lastMsg.from_uid != hostId;
|
||||
const { scrollHeight, scrollTop } = container;
|
||||
const deltaHeight = scrollHeight - scrollTop;
|
||||
console.log("ttt", deltaHeight, lastIsMyself, firstRender.current);
|
||||
// 自己发送的消息
|
||||
if (lastIsMyself && lastMsg.sending) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
// 在可视区域附近
|
||||
if (deltaHeight <= triggerScrollHeight) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
setHasNewMsg(false);
|
||||
} else if (!lastIsMyself && !firstRender.current) {
|
||||
console.log("new msg");
|
||||
setHasNewMsg(true);
|
||||
}
|
||||
}
|
||||
firstRender.current = false;
|
||||
}, [mids, messageMap, hostId]);
|
||||
const handleNewMessageScroll = () => {
|
||||
const container = document.querySelector("#MESSAGE_LIST_CONTAINER");
|
||||
if (container) {
|
||||
setHasNewMsg(false);
|
||||
setTimeout(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, 30);
|
||||
}
|
||||
};
|
||||
if (!mids) return null;
|
||||
return <>
|
||||
{mids.map((mid) => {
|
||||
const currMsg = messageMap[mid];
|
||||
if (!currMsg) return null;
|
||||
const lastMsg = messageMap[mids[mids.length - 1]];
|
||||
console.log("lll", lastMsg);
|
||||
|
||||
const { content, created_at = 0, from_uid = 0, sending = false } = currMsg;
|
||||
return <Message uid={from_uid} host={hostId == from_uid} sending={sending} key={mid} mid={mid} type="text" content={content} create_time={created_at} />;
|
||||
})}
|
||||
|
||||
{hasNewMsg && <button type='button' className="sticky bottom-4 left-1/2 -translate-x-1/2 bg-white border border-orange-300 px-2 py-1.5 text-xs text-orange-500 rounded-lg" onClick={handleNewMessageScroll} >New Message</button>}
|
||||
</>;
|
||||
};
|
||||
|
||||
// export default MessageFeed;
|
||||
export default memo(MessageFeed, (prev, next) => {
|
||||
return prev.hostId === next.hostId;
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { FC, memo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { MessagePayload } from '../../../../app/slices/message';
|
||||
// import { Message } from '../../../../types/chatbot';
|
||||
// import Image from 'next/image';
|
||||
import { useAppSelector } from '../../../../app/store';
|
||||
import { getInitials, getInitialsAvatar } from '../../../../common/utils';
|
||||
|
||||
type TimeProps = {
|
||||
time: number;
|
||||
};
|
||||
|
||||
const Time: FC<TimeProps> = ({ time }) => {
|
||||
return (
|
||||
<time className="text-gray-300 text-sm">{`${new Date(time).toLocaleTimeString('en-US', {
|
||||
minute: 'numeric',
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
})}`}</time>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageProps = {
|
||||
hostId: number;
|
||||
compact?: boolean;
|
||||
isFirst?: boolean;
|
||||
} & MessagePayload;
|
||||
|
||||
const Index: FC<MessageProps> = (props) => {
|
||||
const { server: { name, logo }, loginUser } = useAppSelector(store => { return { server: store.server, loginUser: store.authData.user }; });
|
||||
const { hostId, from_uid, content, created_at = 0, compact, isFirst = false } = props;
|
||||
const isHost = from_uid == hostId;
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col relative pl-14 py-2 hover:bg-gray-100 rounded-sm',
|
||||
compact ? 'py-1' : 'mt-4',
|
||||
isFirst ? 'mt-0' : ''
|
||||
)}
|
||||
>
|
||||
{!compact && (
|
||||
<div className="absolute left-1 top-2 w-12 h-12 rounded-full overflow-hidden">
|
||||
{isHost ? (
|
||||
<img src={logo} alt="logo" className="w-full h-full" />
|
||||
) : (
|
||||
<img className="w-12 h-12" src={getInitialsAvatar({ initials: getInitials(loginUser?.name ?? "Unkown") })} alt="avatar" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!compact && (
|
||||
<span className="flex items-center gap-2 text-md">
|
||||
<em className="text-black not-italic font-bold">{isHost ? name : loginUser?.name}</em>
|
||||
<Time time={created_at} />
|
||||
</span>
|
||||
)}
|
||||
<p
|
||||
className={clsx(
|
||||
'text-gray-600 text-md whitespace-normal break-words w-[80%]',
|
||||
compact ? 'group relative ' : 'pt-1'
|
||||
)}
|
||||
>
|
||||
{compact && (
|
||||
<time className="absolute -left-2 top-1/2 -translate-x-full -translate-y-1/2 text-gray-300 text-xs invisible group-hover:visible">{`${new Date(created_at)
|
||||
.toLocaleTimeString('en-US', {
|
||||
second: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
})
|
||||
.split(' ')[0]
|
||||
}`}</time>
|
||||
)}
|
||||
{content}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index, (prev, next) => {
|
||||
return prev.mid === next.mid;
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/indent */
|
||||
import { FC, memo, useEffect } from 'react';
|
||||
// import { MessagePayload } from '../../../app/slices/message';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import TextMessage from './Message/Text';
|
||||
|
||||
type Props = {
|
||||
uid: number
|
||||
};
|
||||
|
||||
// 间隔10秒
|
||||
const interval = 10 * 1000;
|
||||
const Index: FC<Props> = ({ uid }) => {
|
||||
const { mids, messageMap } = useAppSelector(store => { return { mids: store.userMessage.byId[uid], messageMap: store.message }; });
|
||||
console.log("mids", mids, uid);
|
||||
useEffect(() => {
|
||||
const container = document.querySelector("#MESSAGE_LIST_CONTAINER");
|
||||
if (mids && mids.length > 0 && container) {
|
||||
setTimeout(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, 30);
|
||||
}
|
||||
}, [mids]);
|
||||
|
||||
if (!mids) return null;
|
||||
return mids.map((mid, idx) => {
|
||||
const currMsg = messageMap[mid];
|
||||
const prevMsg = messageMap[mids[idx - 1]];
|
||||
const compact = !prevMsg
|
||||
? false
|
||||
: prevMsg.from_uid !== currMsg.from_uid
|
||||
? false
|
||||
: (currMsg.created_at ?? 0) - (prevMsg.created_at ?? 0) < interval;
|
||||
return <TextMessage hostId={uid} key={currMsg.mid} {...currMsg} compact={compact} isFirst={idx === 0} />;
|
||||
});
|
||||
};
|
||||
|
||||
export default memo(Index, (prev, next) => {
|
||||
return JSON.stringify(prev.uid) === JSON.stringify(next.uid);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useRef, useState, memo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useSendMessage from '../../common/hook/useSendMessage';
|
||||
|
||||
|
||||
const textareaClassName = clsx(
|
||||
'px-2.5 py-1.5 text-sm rounded-md w-full block',
|
||||
'min-h-[32px] max-h-[92px] h-8 resize-none overflow-y-auto',
|
||||
'ring-1 ring-gray-200 focus:ring-2 focus:ring-[#1fe1f9]',
|
||||
'focus:outline-none',
|
||||
);
|
||||
type Props = {
|
||||
from: number,
|
||||
to: number
|
||||
}
|
||||
const MessageInput = (props: Props) => {
|
||||
const { from, to } = props;
|
||||
const { sendMessage } = useSendMessage({
|
||||
from,
|
||||
to,
|
||||
context: "user"
|
||||
});
|
||||
|
||||
const [content, setContent] = useState('');
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
return (
|
||||
<div className="relative border-t border-gray-300 w-full">
|
||||
<div className={'px-3 py-2 min-h-[48px]'}>
|
||||
<textarea
|
||||
// disabled={isSending}
|
||||
ref={ref}
|
||||
maxLength={4096}
|
||||
className={textareaClassName}
|
||||
value={content}
|
||||
placeholder="Type and press enter"
|
||||
onChange={e => setContent(e.target.value)}
|
||||
onInput={() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
element.style.height = '32px';
|
||||
// borderTop + borderBottom = 2px
|
||||
element.style.height = `${element.scrollHeight + 2}px`;
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (!e.shiftKey && e.key === 'Enter') {
|
||||
// e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (content.trim().length === 0) return;
|
||||
sendMessage({
|
||||
type: "text",
|
||||
content
|
||||
});
|
||||
setContent("");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageInput);
|
||||
@@ -3,37 +3,29 @@ import { memo } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import Login from './Login';
|
||||
|
||||
const TextMessage = ({ text, animate = '' }: { text: string; animate?: string }) => (
|
||||
<p className={`text-gray-600 text-md mb-1 ${animate}`}>{text}</p>
|
||||
);
|
||||
type Props = {
|
||||
needLogin?: boolean
|
||||
}
|
||||
const Index = ({ needLogin = false }: Props) => {
|
||||
const { name, logo } = useAppSelector(store => store.server);
|
||||
return (
|
||||
<div className="flex gap-1 pl-1">
|
||||
<div className="w-12 h-12">
|
||||
<img src={logo} alt="logo" className="w-full h-full rounded-full" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-2 leading-6">
|
||||
<em className="text-black not-italic text-lg font-bold">{name}</em>
|
||||
<time className="text-gray-300 text-sm">{`${new Date().toLocaleTimeString('en-US', {
|
||||
minute: 'numeric',
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
})}`}</time>
|
||||
</span>
|
||||
<TextMessage text="Hi there, Nice to meet you! 👋👋👋" animate={needLogin ? "animate-[fadeInUp_.5s_ease-in-out_both]" : ''} />
|
||||
{needLogin && <>
|
||||
<TextMessage text="You need login before we have a nice talk 👇" animate="animate-[fadeInUp_.5s_.8s_ease-in-out_both]" />
|
||||
<div className="animate-[fadeInUp_.5s_1.2s_ease-in-out_both]">
|
||||
<Login />
|
||||
<>
|
||||
<div className="group relative flex justify-start items-start mb-3">
|
||||
<div className="w-9 h-9 absolute top-0 left-3">
|
||||
<img src={logo} alt="logo" className="rounded-full bg-transparent w-9 h-9" />
|
||||
</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">
|
||||
👋 Hi there, Nice to meet you! Please login before we have a nice talk 👇
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{needLogin && <div className="pl-14 animate-[fadeInUp_.5s_1.2s_ease-in-out_both]">
|
||||
<Login />
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 5.75L5.75 16.25M5.75 5.75L16.25 16.25" stroke="#667085" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="18" viewBox="0 0 24 24" width="18" fill="#fff" class="w-6 h-6"><path d="M0 0h24v24H0z" fill="none"></path><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 279 B |
+28
-50
@@ -1,9 +1,9 @@
|
||||
import { useState, ChangeEvent } from 'react';
|
||||
// import { useState, ChangeEvent } from 'react';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Welcome from './Welcome';
|
||||
import MessageFeed from './MessageFeed';
|
||||
import Line from './Line';
|
||||
import useSendMessage from '../../common/hook/useSendMessage';
|
||||
import MessageInput from './MessageInput';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
type Props = {
|
||||
hostId: number,
|
||||
@@ -12,61 +12,39 @@ type Props = {
|
||||
|
||||
const Index = ({ handleClose, hostId }: Props) => {
|
||||
const { user: loginUser, token, guest: isGuest } = useAppSelector(store => store.authData);
|
||||
const { sendMessage } = useSendMessage({
|
||||
from: loginUser?.uid,
|
||||
to: hostId,
|
||||
context: "user"
|
||||
});
|
||||
const [input, setInput] = useState('');
|
||||
const handleInput = (evt: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInput(evt.target.value);
|
||||
};
|
||||
const handleSend = () => {
|
||||
if (!input) return;
|
||||
sendMessage({
|
||||
type: "text",
|
||||
content: input
|
||||
});
|
||||
setInput("");
|
||||
};
|
||||
// const { sendMessage } = useSendMessage({
|
||||
// from: loginUser?.uid,
|
||||
// to: hostId,
|
||||
// context: "user"
|
||||
// });
|
||||
// const [input, setInput] = useState('');
|
||||
// const handleInput = (evt: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
// setInput(evt.target.value);
|
||||
// };
|
||||
// const handleSend = () => {
|
||||
// if (!input) return;
|
||||
// sendMessage({
|
||||
// type: "text",
|
||||
// content: input
|
||||
// });
|
||||
// setInput("");
|
||||
// };
|
||||
|
||||
// no token or guest login
|
||||
const notLogin = !token || isGuest;
|
||||
return (
|
||||
<aside className="flex flex-col justify-between bg-white w-[600px] h-[800px] rounded-md overflow-hidden">
|
||||
<aside className="flex flex-col bg-white w-full h-full rounded-md overflow-hidden">
|
||||
<Header handleClose={handleClose} />
|
||||
<Line />
|
||||
{/* message list */}
|
||||
<section id='MESSAGE_LIST_CONTAINER' className="px-2 py-3 flex-1 overflow-y-auto scroll-smooth">
|
||||
<main id='MESSAGE_LIST_CONTAINER' className="relative flex-1 overflow-y-auto scroll-smooth">
|
||||
{/* placeholder */}
|
||||
<div className="flex items-center h-10 justify-center"></div>
|
||||
<Welcome needLogin={notLogin} />
|
||||
{notLogin ? null : <MessageFeed uid={hostId} />}
|
||||
</section>
|
||||
<Line />
|
||||
{notLogin ? null : <MessageFeed hostId={hostId} />}
|
||||
</main>
|
||||
{/* message input */}
|
||||
<div className="w-full px-2 py-3">
|
||||
<textarea
|
||||
disabled={notLogin}
|
||||
value={input}
|
||||
onChange={handleInput}
|
||||
className="w-full h-full text-sm p-2 rounded-lg bg-gray-200 resize-none text-black outline-none"
|
||||
placeholder={notLogin ? "Login first..." : "Write a message..."}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<Line thin />
|
||||
{/* operation area */}
|
||||
<div className="w-full flex px-3 py-2 justify-between">
|
||||
<div className="opts">{/* opts placeholder */}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSend}
|
||||
disabled={input.trim() === ''}
|
||||
className="rounded-full bg-[#FA491D] disabled:bg-gray-200 text-white disabled:text-gray-400 text-xs leading-4 px-2 py-1.5 "
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-300 text-center pb-2">Powered by <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400">voce.chat</a></span>
|
||||
{notLogin ? null : <MessageInput from={loginUser?.uid || 0} to={hostId} />}
|
||||
<Footer />
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user