完成 IM Web Android 端产品页面开发
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import React from 'react';
|
||||
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
|
||||
const navigate = view => HashNavigation.navigateTo(view);
|
||||
|
||||
const VIEW_CONFIG = {
|
||||
'chat-inbound-request': {relationship: 'pending_inbound', variant: 'inbound-request'},
|
||||
'chat-inbound-message': {relationship: 'pending_inbound', variant: 'inbound-message'},
|
||||
'chat-outbound-request': {relationship: 'pending_outbound', variant: 'outbound-request'},
|
||||
'chat-status': {relationship: 'friend', variant: 'status'},
|
||||
'chat-friend': {relationship: 'friend', variant: 'friend'},
|
||||
'chat-media': {relationship: 'friend', variant: 'media'},
|
||||
'chat-blocked': {relationship: 'blocked_by_peer', variant: 'blocked'},
|
||||
'chat-deleted': {relationship: 'none', variant: 'deleted'},
|
||||
'chat-standard': {relationship: 'friend', variant: 'standard'},
|
||||
'chat-attachments': {relationship: 'friend', variant: 'attachments'},
|
||||
'chat-mixed': {relationship: 'friend', variant: 'mixed'},
|
||||
'chat-contact-card': {relationship: 'friend', variant: 'contact-card'}
|
||||
};
|
||||
|
||||
const ATTACHMENTS = [
|
||||
{id: 'photo', icon: 'image', label: '照片'},
|
||||
{id: 'camera', icon: 'photo_camera', label: '拍摄'},
|
||||
{id: 'call', icon: 'call', label: '视频语音'},
|
||||
{id: 'card', icon: 'contact_page', label: '名片'},
|
||||
{id: 'file', icon: 'folder', label: '文件'},
|
||||
{id: 'favorite', icon: 'bookmark', label: '收藏'},
|
||||
{id: 'video', icon: 'videocam', label: '视频通话'}
|
||||
];
|
||||
|
||||
function Icon({children}) {
|
||||
return <i className="material-icons" aria-hidden="true">{children}</i>;
|
||||
}
|
||||
|
||||
function Toast({message, onDone}) {
|
||||
React.useEffect(() => {
|
||||
if (!message) return undefined;
|
||||
const timer = window.setTimeout(onDone, 2200);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [message, onDone]);
|
||||
return message ? <div className="chat-toast" role="status">{message}</div> : null;
|
||||
}
|
||||
|
||||
function ChatAvatar({mine = false, peer = false}) {
|
||||
return <div className="chat-avatar"><img src={mine ? 'img/bkg/p05.jpg' : peer ? 'img/bkg/p09.jpg' : 'img/bkg/p14.jpg'} alt="" /></div>;
|
||||
}
|
||||
|
||||
function Status({value, failedReason}) {
|
||||
if (!value && !failedReason) return null;
|
||||
return <div className={'chat-message-status' + (failedReason ? ' failed' : '')}>
|
||||
{failedReason ? <Icon>error</Icon> : null}<span>{failedReason || value}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function FileCard() {
|
||||
return <div className="chat-file-card">
|
||||
<div><strong>365天艾宾浩斯记忆曲线复习计划.pdf</strong><small>115KB</small></div>
|
||||
<span className="chat-file-icon">P</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ContactCard({onOpen}) {
|
||||
return <button type="button" className="chat-contact-card" onClick={onOpen}>
|
||||
<span><img src="img/bkg/p15.jpg" alt="" /><strong>栗子来了</strong></span>
|
||||
<small>个人名片 · 账号 QL1024</small>
|
||||
</button>;
|
||||
}
|
||||
|
||||
function VoiceMessage({duration = 6, playing = false}) {
|
||||
return <div className="chat-voice-message"><Icon>{playing ? 'graphic_eq' : 'volume_up'}</Icon><strong>{duration}″</strong><span /></div>;
|
||||
}
|
||||
|
||||
function ImageMessage({progress}) {
|
||||
return <div className="chat-image-message">
|
||||
<img src="img/bkg/p22.jpg" alt="会话图片" />
|
||||
{progress < 100 ? <div className="chat-upload-progress"><progress value={progress} max="100" /><strong>{progress}%</strong></div> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Bubble({mine = false, children, status, failedReason, type = 'text', onOpenCard}) {
|
||||
return <div className={'chat-message-row' + (mine ? ' mine' : '')}>
|
||||
{!mine ? <ChatAvatar peer /> : null}
|
||||
<div className="chat-message-stack">
|
||||
{type === 'file' ? <FileCard /> : type === 'card' ? <ContactCard onOpen={onOpenCard} /> :
|
||||
<div className={'chat-bubble ' + type + (mine ? ' mine' : '')}>{children}</div>}
|
||||
<Status value={status} failedReason={failedReason} />
|
||||
</div>
|
||||
{mine ? <ChatAvatar mine /> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RelationNotice({children, action, onAction}) {
|
||||
return <div className="chat-relation-notice"><span>{children}</span>{action ? <button type="button" onClick={onAction}>{action}</button> : null}</div>;
|
||||
}
|
||||
|
||||
function ChatTimeline({variant, uploadProgress, sendStatus, onRetry, onOpenCard, onFriendAction}) {
|
||||
if (variant === 'inbound-request' || variant === 'inbound-message') {
|
||||
return <>
|
||||
<p className="chat-time">7月30日 11:40</p>
|
||||
<Bubble>{variant === 'inbound-request' ? '请求添加你为好友' : '我是祈万事安好'}</Bubble>
|
||||
<RelationNotice>以上是打招呼的消息</RelationNotice>
|
||||
<div className="chat-request-actions"><button type="button" onClick={() => onFriendAction('declined')}>拒绝</button><button type="button" className="primary" onClick={() => onFriendAction('accepted')}>通过申请</button></div>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'outbound-request') {
|
||||
return <>
|
||||
<p className="chat-time">7月30日 11:40</p>
|
||||
<Bubble mine status="已发送">请求添加你为好友</Bubble>
|
||||
<RelationNotice>好友申请已发送,等待对方处理</RelationNotice>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'status') {
|
||||
return <>
|
||||
<p className="chat-time">7月30日 11:40</p>
|
||||
<Bubble mine status={sendStatus}>我是祈万事安好</Bubble>
|
||||
<RelationNotice>你已添加安妮,现在可以开始聊天。</RelationNotice>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'friend') {
|
||||
return <>
|
||||
<p className="chat-time">8月27日 11:11</p>
|
||||
<RelationNotice>你们已成为好友,现在可以开始聊天</RelationNotice>
|
||||
<Bubble>我们已成为好友,现在我们可以开始聊天</Bubble>
|
||||
<Bubble mine status="已读">你还好吗?</Bubble>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'media') {
|
||||
return <>
|
||||
<p className="chat-time">8月27日 11:11</p>
|
||||
<RelationNotice>接雨彤创建群聊</RelationNotice>
|
||||
<Bubble mine status="已读">你还好吗?</Bubble>
|
||||
<Bubble mine type="image"><ImageMessage progress={uploadProgress} /></Bubble>
|
||||
<Bubble mine type="voice"><VoiceMessage duration={6} playing /></Bubble>
|
||||
<Bubble type="voice"><VoiceMessage duration={6} /></Bubble>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'blocked') {
|
||||
return <>
|
||||
<p className="chat-time">7月30日 11:40</p>
|
||||
<Bubble mine status="已读">请求添加你为好友</Bubble>
|
||||
<RelationNotice>你已添加了安妮,现在可以开始聊天。</RelationNotice>
|
||||
<p className="chat-time">7月30日 13:00</p>
|
||||
<Bubble mine status="已读">这边在网上看见你的留言,你想了解什么课程了</Bubble>
|
||||
<Bubble>有课程辅导的老师?对新手友好吗?</Bubble>
|
||||
<Bubble mine failedReason="消息已发送,但被对方拒收了">这个不用担心特别好上手,学起来特别简单</Bubble>
|
||||
<Bubble mine failedReason="消息已发送,但被对方拒收了">拉黑我这么速度,真的牛</Bubble>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (variant === 'deleted') {
|
||||
return <>
|
||||
<p className="chat-time">7月30日 13:00</p>
|
||||
<Bubble mine status="已读">你好我是XXX推荐过来的,这边想了解课程是吗</Bubble>
|
||||
<Bubble>这边目前找到更好的课程,不考虑你们家,谢谢</Bubble>
|
||||
<Bubble mine status="已读">这是我们导师名片,你可以加一下,对比一下价格</Bubble>
|
||||
<RelationNotice action="发送朋友验证" onAction={() => onFriendAction('resent')}>你还不是他(她)朋友。</RelationNotice>
|
||||
<Bubble mine failedReason="发送失败,需重新添加好友">这么突然把我删了,真心累了</Bubble>
|
||||
<RelationNotice action="发送朋友验证" onAction={() => onFriendAction('resent')}>你还不是他(她)朋友。</RelationNotice>
|
||||
</>;
|
||||
}
|
||||
|
||||
const base = <>
|
||||
<p className="chat-time">7月30日 11:40</p>
|
||||
<Bubble mine status="已读">请求添加你为好友</Bubble>
|
||||
<RelationNotice>你已添加了安妮,现在可以开始聊天。</RelationNotice>
|
||||
<p className="chat-time">7月30日 13:00</p>
|
||||
<Bubble mine status="已读">你好,我是王珊珊,很高兴认识你,你怎么称呼</Bubble>
|
||||
<Bubble>你好,我是安妮,想跟你咨询一下设计课程,方便?</Bubble>
|
||||
</>;
|
||||
|
||||
if (variant === 'standard') {
|
||||
return <>{base}<Bubble mine status="未读">非常方便,这是我们的课程目录,你可以看看了解一下</Bubble><Bubble mine type="file" status="未读" /></>;
|
||||
}
|
||||
if (variant === 'mixed') {
|
||||
return <>{base}<Bubble mine type="file" status="已送达" /><Bubble type="card" onOpenCard={onOpenCard} /></>;
|
||||
}
|
||||
if (variant === 'contact-card') {
|
||||
return <>{base}<Bubble mine status="已读">这是我们导师名片,你可以加一下,有问题随时联系他</Bubble><Bubble mine type="card" status="已送达" onOpenCard={onOpenCard} /><Bubble>好的,非常感谢,那我这边就直接联系他吧</Bubble><Bubble mine status="已读">没问题的,有疑问随时沟通</Bubble></>;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function AttachmentPanel({onChoose}) {
|
||||
return <div className="chat-attachment-panel">
|
||||
{ATTACHMENTS.map(item => <button type="button" key={item.id} onClick={() => onChoose(item)}><span><Icon>{item.icon}</Icon></span><small>{item.label}</small></button>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Composer({relationship, attachmentOpen, setAttachmentOpen, onSend}) {
|
||||
const [draft, setDraft] = React.useState(sessionStorage.getItem('im-chat-draft') || '');
|
||||
const canSend = relationship === 'friend';
|
||||
const updateDraft = value => {
|
||||
setDraft(value);
|
||||
sessionStorage.setItem('im-chat-draft', value);
|
||||
};
|
||||
const submit = e => {
|
||||
e.preventDefault();
|
||||
if (!canSend || !draft.trim()) return;
|
||||
onSend(draft.trim());
|
||||
updateDraft('');
|
||||
};
|
||||
return <form className={'chat-composer' + (!canSend ? ' locked' : '')} onSubmit={submit}>
|
||||
<button type="button" aria-label="语音" disabled={!canSend} onClick={() => navigate('voice-ready')}><Icon>record_voice_over</Icon></button>
|
||||
<input value={draft} onChange={e => updateDraft(e.target.value)} placeholder={canSend ? '输入消息' : relationship === 'pending_inbound' ? '请先处理好友申请' : relationship === 'pending_outbound' ? '等待对方通过好友申请' : '当前无法发送消息'} disabled={!canSend} />
|
||||
<button type="button" aria-label="表情" disabled={!canSend} onClick={() => navigate('emoji-panel')}><Icon>sentiment_satisfied_alt</Icon></button>
|
||||
{draft.trim() ? <button type="submit" className="chat-send-button">发送</button> : <button type="button" aria-label="更多" disabled={!canSend} onClick={() => setAttachmentOpen(!attachmentOpen)}><Icon>{attachmentOpen ? 'keyboard' : 'add_circle_outline'}</Icon></button>}
|
||||
</form>;
|
||||
}
|
||||
|
||||
export default function DirectMessageView({view}) {
|
||||
const config = VIEW_CONFIG[view] || VIEW_CONFIG['chat-standard'];
|
||||
const [relationship, setRelationship] = React.useState(config.relationship);
|
||||
const [variant, setVariant] = React.useState(config.variant);
|
||||
const [attachmentOpen, setAttachmentOpen] = React.useState(config.variant === 'attachments');
|
||||
const [uploadProgress, setUploadProgress] = React.useState(config.variant === 'media' ? 76 : 100);
|
||||
const [sendStatus, setSendStatus] = React.useState('发送中');
|
||||
const [extraMessages, setExtraMessages] = React.useState([]);
|
||||
const [toast, setToast] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (variant !== 'status') return undefined;
|
||||
const stages = ['已发送', '已送达', '已读'];
|
||||
const timers = stages.map((status, index) => window.setTimeout(() => setSendStatus(status), 600 * (index + 1)));
|
||||
return () => timers.forEach(timer => window.clearTimeout(timer));
|
||||
}, [variant]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (variant !== 'media' || uploadProgress >= 100) return undefined;
|
||||
const timer = window.setTimeout(() => setUploadProgress(value => Math.min(value + 4, 100)), 350);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [variant, uploadProgress]);
|
||||
|
||||
const friendAction = action => {
|
||||
if (action === 'declined') {
|
||||
setRelationship('none');
|
||||
setToast('已拒绝好友申请');
|
||||
} else {
|
||||
setRelationship(action === 'accepted' ? 'friend' : 'pending_outbound');
|
||||
if (action === 'accepted') setVariant('friend');
|
||||
setToast(action === 'accepted' ? '好友申请已通过' : '好友验证已发送');
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="chat-product-page">
|
||||
<header className="chat-product-header">
|
||||
<button type="button" onClick={() => navigate('messages-home')} aria-label="返回消息"><Icon>arrow_back_ios_new</Icon></button>
|
||||
<div><h1>接语彤</h1><span><i />在线</span></div>
|
||||
<button type="button" onClick={() => navigate('friend-settings')} aria-label="会话设置"><Icon>more_vert</Icon></button>
|
||||
</header>
|
||||
<main className="chat-product-timeline">
|
||||
<ChatTimeline variant={variant} uploadProgress={uploadProgress} sendStatus={sendStatus}
|
||||
onRetry={() => setToast('正在重试发送')} onOpenCard={() => navigate('friend-profile')} onFriendAction={friendAction} />
|
||||
{extraMessages.map(item => <Bubble mine status={item.status} key={item.id}>{item.text}</Bubble>)}
|
||||
</main>
|
||||
{relationship === 'friend' ? null : <div className="chat-permission-banner"><Icon>info_outline</Icon>{relationship === 'pending_inbound' ? '通过好友申请后即可发送消息' : relationship === 'pending_outbound' ? '好友申请正在等待处理' : relationship === 'none' ? '对方已不是你的好友,发送消息前需重新验证' : '当前关系状态下无法发送消息'}</div>}
|
||||
<Composer relationship={relationship} attachmentOpen={attachmentOpen} setAttachmentOpen={setAttachmentOpen}
|
||||
onSend={text => {
|
||||
const id = Date.now();
|
||||
setExtraMessages(items => items.concat({id, text, status: '发送中'}));
|
||||
window.setTimeout(() => setExtraMessages(items => items.map(item => item.id === id ? {...item, status: '已发送'} : item)), 600);
|
||||
}} />
|
||||
{attachmentOpen && relationship === 'friend' ? <AttachmentPanel onChoose={item => {
|
||||
setToast(`已选择${item.label},上传接口将在联调阶段接入`);
|
||||
if (item.id === 'card') navigate('chat-contact-card');
|
||||
if (item.id === 'photo' || item.id === 'camera') navigate('media-uploading');
|
||||
if (item.id === 'call' || item.id === 'video') navigate('call-history');
|
||||
if (item.id === 'file') navigate('upload-file');
|
||||
if (item.id === 'favorite') navigate('favorites');
|
||||
}} /> : null}
|
||||
<Toast message={toast} onDone={() => setToast('')} />
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user