434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
import { BaseEventOrig, Button, Image, ScrollView, ScrollViewProps, Textarea, TextareaProps } from '@tarojs/components';
|
||
import Taro, { NodesRef, useDidHide, useDidShow, useLoad, useUnload } from '@tarojs/taro';
|
||
|
||
import classNames from 'classnames';
|
||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
||
import {
|
||
ContactMessage,
|
||
JobMessage,
|
||
LocationMessage,
|
||
MaterialMessage,
|
||
TextMessage,
|
||
TimeMessage,
|
||
} from '@/components/message-chat';
|
||
import PageLoading from '@/components/page-loading';
|
||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||
import { EventName } from '@/constants/app';
|
||
import { CollectEventName } from '@/constants/event';
|
||
import { ChatWatchType, MessageType, PULL_NEW_MESSAGES_TIME } from '@/constants/message';
|
||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||
import { RESPONSE_ERROR_CODE } from '@/http/constant';
|
||
import { HttpError } from '@/http/error';
|
||
import {
|
||
IChatUser,
|
||
IChatInfo,
|
||
IChatMessage,
|
||
IJobMessage,
|
||
ILocationMessage,
|
||
IMaterialMessage,
|
||
IMessageStatus,
|
||
PostMessageRequest,
|
||
} from '@/types/message';
|
||
import { isAnchorMode } from '@/utils/app';
|
||
import { getScrollItemId, last, logWithPrefix } from '@/utils/common';
|
||
import { collectEvent } from '@/utils/event';
|
||
import {
|
||
isExchangeMessage,
|
||
isJobMessage,
|
||
isLocationMessage,
|
||
isMaterialMessage,
|
||
isTextMessage,
|
||
isTimeMessage,
|
||
openLocationSelect,
|
||
postAddMessageTimes,
|
||
postChatRejectWatch,
|
||
postSendMessage,
|
||
requestActionDetail,
|
||
requestChatDetail,
|
||
requestChatWatch,
|
||
requestMessageStatusList,
|
||
requestNewChatMessages,
|
||
} from '@/utils/message';
|
||
import { getPageQuery, parseQuery } from '@/utils/route';
|
||
import Toast from '@/utils/toast';
|
||
import { getUserId } from '@/utils/user';
|
||
|
||
import './index.less';
|
||
|
||
const PREFIX = 'page-message-chat';
|
||
const LIST_CONTAINER_CLASS = `${PREFIX}__chat-list`;
|
||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||
selectors: [`.${LIST_CONTAINER_CLASS}`],
|
||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult]) => {
|
||
const [rect] = rects;
|
||
return rect.height;
|
||
},
|
||
};
|
||
const log = logWithPrefix(PREFIX);
|
||
const chooseLocation = Taro.requirePlugin('chooseLocation');
|
||
|
||
interface ILoadProps {
|
||
chatId: string;
|
||
jobId?: string;
|
||
job?: string;
|
||
material?: string;
|
||
}
|
||
|
||
const getHeaderLeftButtonText = (job?: IJobMessage, material?: IMaterialMessage) => {
|
||
if (job) {
|
||
return '不感兴趣';
|
||
}
|
||
if (material) {
|
||
return '标记为不合适';
|
||
}
|
||
return isAnchorMode() ? '不感兴趣' : '标记为不合适';
|
||
};
|
||
|
||
export default function MessageChat() {
|
||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||
const [input, setInput] = useState('');
|
||
const [showMore, setShowMore] = useState(false);
|
||
const [chat, setChat] = useState<IChatInfo | null>(null);
|
||
const [reject, setReject] = useState<boolean>(false);
|
||
const [receiver, setReceiver] = useState<IChatUser | null>(null);
|
||
const [messages, setMessages] = useState<IChatMessage[]>([]);
|
||
const [messageStatusList, setMessageStatusList] = useState<IMessageStatus[]>([]);
|
||
const [jobId, setJobId] = useState<string>();
|
||
const [resumeId, setResumeId] = useState<string>();
|
||
const [job, setJob] = useState<IJobMessage>();
|
||
const [material, setMaterial] = useState<IMaterialMessage>();
|
||
const [scrollItemId, setScrollItemId] = useState<string>();
|
||
const scrollToLowerRef = useRef(false);
|
||
const autoSendRef = useRef({ sendJob: false, sendMaterial: false });
|
||
const loadMoreRef = useRef(async (chatId: string, currentMessages: IChatMessage[], forceScroll?: boolean) => {
|
||
try {
|
||
const lastMsgId = last(currentMessages)?.msgId;
|
||
const newMessages = await requestNewChatMessages({ chatId: chatId, lastMsgId });
|
||
log('requestNewChatMessages', newMessages, forceScroll);
|
||
if (newMessages.length) {
|
||
setMessages([...currentMessages, ...newMessages]);
|
||
(forceScroll || scrollToLowerRef.current) && setScrollItemId(getScrollItemId(last(newMessages)?.msgId));
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
});
|
||
|
||
const handleInput = useCallback((e: BaseEventOrig<TextareaProps.onInputEventDetail>) => {
|
||
const value = e.detail.value || '';
|
||
setInput(value);
|
||
}, []);
|
||
|
||
const handleClickExpand = useCallback(() => setShowMore(true), []);
|
||
|
||
const handleScroll = useCallback(
|
||
(e: BaseEventOrig<ScrollViewProps.onScrollDetail>) => {
|
||
// log('handleScroll', e);
|
||
const { scrollTop, scrollHeight } = e.detail;
|
||
scrollToLowerRef.current = listHeight + scrollTop >= scrollHeight - 40;
|
||
},
|
||
[listHeight]
|
||
);
|
||
|
||
const handleClickSendLocation = useCallback((e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
openLocationSelect();
|
||
}, []);
|
||
|
||
const handleClickMoreOuter = () => showMore && setShowMore(false);
|
||
|
||
const handleClickContactButton = useCallback(async () => {
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
await loadMoreRef.current(chat.chatId, messages, true);
|
||
}, [chat, messages]);
|
||
|
||
const handleSendMessage = useCallback(
|
||
async (newMessage: Omit<PostMessageRequest, 'chatId' | 'bizId'>) => {
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
try {
|
||
Taro.showLoading();
|
||
await postSendMessage({ chatId: chat.chatId, bizId: jobId || chat.lastJobId, ...newMessage });
|
||
await loadMoreRef.current(chat.chatId, messages, true);
|
||
Taro.hideLoading();
|
||
} catch (error) {
|
||
const e = error as HttpError;
|
||
const errorCode = e.errorCode;
|
||
collectEvent(CollectEventName.MESSAGE_DEV_LOG, { action: 'send-message', e, message: newMessage });
|
||
let tips = '发送失败请重试';
|
||
let duration = 1500;
|
||
if (
|
||
errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE &&
|
||
newMessage.type === MessageType.RequestCompanyContact
|
||
) {
|
||
tips = '今日申请交换联系方式次数已用完,当前每日限制为5次';
|
||
duration = 3000;
|
||
}
|
||
tips.length > 7 ? Toast.info(tips, duration) : Toast.error(tips, duration);
|
||
}
|
||
},
|
||
[chat, jobId, messages]
|
||
);
|
||
|
||
const handleClickReject = useCallback(async () => {
|
||
if (!chat || !receiver || reject) {
|
||
return;
|
||
}
|
||
const watchType = isAnchorMode() ? ChatWatchType.AnchorReject : ChatWatchType.CompanyReject;
|
||
await postChatRejectWatch({
|
||
type: watchType,
|
||
toUserId: receiver.userId,
|
||
jobId: jobId || chat.lastJobId,
|
||
status: false,
|
||
});
|
||
setReject(true);
|
||
}, [jobId, chat, receiver, reject]);
|
||
|
||
const handleSendExchangeContact = useCallback(async () => {
|
||
postAddMessageTimes('click_request_exchange_contact');
|
||
const type = isAnchorMode() ? MessageType.RequestCompanyContact : MessageType.RequestAnchorContact;
|
||
handleSendMessage({ type, actionObject: '' });
|
||
}, [handleSendMessage]);
|
||
|
||
const handleSendJobMessage = useCallback(async () => {
|
||
if (!job || !receiver || autoSendRef.current.sendJob) {
|
||
return;
|
||
}
|
||
const detail = await requestActionDetail({ type: MessageType.Job, bizId: job.id, toUserId: receiver.userId });
|
||
if (!detail) {
|
||
handleSendMessage({ type: MessageType.Job, actionObject: JSON.stringify(job) });
|
||
}
|
||
autoSendRef.current.sendJob = true;
|
||
}, [job, receiver, handleSendMessage]);
|
||
|
||
const handleSendMaterialMessage = useCallback(async () => {
|
||
if (!material || !receiver || autoSendRef.current.sendMaterial) {
|
||
return;
|
||
}
|
||
const detail = await requestActionDetail({
|
||
type: MessageType.Material,
|
||
bizId: material.id,
|
||
toUserId: receiver.userId,
|
||
});
|
||
if (!detail) {
|
||
handleSendMessage({ type: MessageType.Material, actionObject: JSON.stringify(material) });
|
||
}
|
||
autoSendRef.current.sendMaterial = true;
|
||
}, [material, receiver, handleSendMessage]);
|
||
|
||
const handleSendLocationMessage = useCallback(
|
||
(location: Omit<ILocationMessage, 'id'>) => {
|
||
setShowMore(false);
|
||
handleSendMessage({ type: MessageType.Location, actionObject: JSON.stringify(location) });
|
||
},
|
||
[handleSendMessage]
|
||
);
|
||
|
||
const handleSendTextMessage = useCallback(async () => {
|
||
if (!input) {
|
||
return;
|
||
}
|
||
postAddMessageTimes('send_message_button');
|
||
await handleSendMessage({ type: MessageType.Text, content: input });
|
||
setInput('');
|
||
}, [input, handleSendMessage]);
|
||
|
||
// useEffect(() => {
|
||
// loadMoreRef.current = async (chatId: string, currentMessages: IChatMessage[], forceScroll: boolean) => {
|
||
// try {
|
||
// const lastMsgId = last(currentMessages)?.msgId;
|
||
// const newMessages = await requestNewChatMessages({ chatId: chatId, lastMsgId });
|
||
// log('requestNewChatMessages', newMessages);
|
||
// if (newMessages.length) {
|
||
// setMessages([...currentMessages, ...newMessages]);
|
||
// (forceScroll || scrollToLowerRef.current) && setScrollItemId(getScrollItemId(last(newMessages)?.msgId));
|
||
// }
|
||
// } catch (e) {
|
||
// console.error(e);
|
||
// }
|
||
// };
|
||
// }, []);
|
||
|
||
useEffect(() => {
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
const intervalId = setInterval(async () => {
|
||
loadMoreRef.current(chat.chatId, messages);
|
||
const statusList = await requestMessageStatusList(chat.chatId);
|
||
setMessageStatusList(statusList);
|
||
}, PULL_NEW_MESSAGES_TIME);
|
||
|
||
return () => {
|
||
clearInterval(intervalId);
|
||
};
|
||
}, [chat, messages]);
|
||
|
||
useEffect(() => {
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
job && handleSendJobMessage();
|
||
material && handleSendMaterialMessage();
|
||
}, [chat, job, material, handleSendJobMessage, handleSendMaterialMessage]);
|
||
|
||
useLoad(async () => {
|
||
const query = getPageQuery<ILoadProps>();
|
||
const chatId = query.chatId;
|
||
if (!chatId) {
|
||
return;
|
||
}
|
||
try {
|
||
const currentUserId = getUserId();
|
||
const watchType = isAnchorMode() ? ChatWatchType.AnchorReject : ChatWatchType.CompanyReject;
|
||
const chatDetail = await requestChatDetail(chatId);
|
||
const toUserInfo = chatDetail.participants.find(u => u.userId !== currentUserId);
|
||
if (!toUserInfo) {
|
||
throw new Error('not receiver');
|
||
}
|
||
const watchStatus = await requestChatWatch({
|
||
type: watchType,
|
||
toUserId: toUserInfo.userId,
|
||
jobId: query.jobId || chatDetail.lastJobId,
|
||
});
|
||
const parseJob = query.job ? parseQuery<IJobMessage>(query.job) : null;
|
||
const parseMaterial = query.material ? parseQuery<IMaterialMessage>(query.material) : null;
|
||
// log('requestChatDetail', chatDetail, parseJob, parseMaterial);
|
||
setChat(chatDetail);
|
||
setResumeId(chatDetail.participants.find(u => u.userId !== currentUserId)?.resumeId);
|
||
setJobId(query.jobId);
|
||
setMessages(chatDetail.messages);
|
||
setScrollItemId(getScrollItemId(last(chatDetail.messages)?.msgId));
|
||
parseJob && setJob(parseJob);
|
||
parseMaterial && setMaterial(parseMaterial);
|
||
Taro.setNavigationBarTitle({ title: toUserInfo.nickName });
|
||
setReceiver(toUserInfo);
|
||
setReject(!watchStatus);
|
||
} catch (e) {
|
||
console.error(e);
|
||
collectEvent(CollectEventName.MESSAGE_DEV_LOG, { action: 'init-chat-message', e });
|
||
Toast.error('加载失败请重试');
|
||
}
|
||
});
|
||
|
||
useDidShow(() => {
|
||
const location = chooseLocation?.getLocation() as Omit<ILocationMessage, 'id'>;
|
||
log('useDidShow', location);
|
||
if (!location) {
|
||
return;
|
||
}
|
||
// 发送定位消息
|
||
handleSendLocationMessage(location);
|
||
chooseLocation?.setLocation(null);
|
||
});
|
||
|
||
useDidHide(() => chooseLocation?.setLocation(null));
|
||
|
||
useUnload(() => {
|
||
chooseLocation?.setLocation(null);
|
||
Taro.eventCenter.trigger(EventName.EXIT_CHAT_PAGE);
|
||
});
|
||
|
||
log('render', scrollItemId, scrollToLowerRef.current);
|
||
|
||
return (
|
||
<div className={PREFIX}>
|
||
{!chat && <PageLoading className={`${PREFIX}__loading`} />}
|
||
<div className={`${PREFIX}__header`} onTouchStart={handleClickMoreOuter}>
|
||
<Button className={classNames(`${PREFIX}__header__reject`, { highlight: reject })} onClick={handleClickReject}>
|
||
{getHeaderLeftButtonText(job, material)}
|
||
</Button>
|
||
<Button className={`${PREFIX}__header__exchange`} onClick={handleSendExchangeContact}>
|
||
交换联系方式
|
||
</Button>
|
||
</div>
|
||
<div className={`${PREFIX}__chat`} onTouchStart={handleClickMoreOuter}>
|
||
<ScrollView className={LIST_CONTAINER_CLASS} scrollIntoView={scrollItemId} onScroll={handleScroll} scrollY>
|
||
{messages.map((message: IChatMessage) => {
|
||
if (isTextMessage(message)) {
|
||
return (
|
||
<TextMessage
|
||
id={message.msgId}
|
||
key={message.msgId}
|
||
message={message}
|
||
resumeId={resumeId}
|
||
isRead={messageStatusList.some(m => m.msgId === message.msgId && !!m.isRead)}
|
||
/>
|
||
);
|
||
} else if (isTimeMessage(message)) {
|
||
return <TimeMessage key={message.msgId} id={message.msgId} message={message} />;
|
||
} else if (isJobMessage(message)) {
|
||
return <JobMessage key={message.msgId} id={message.msgId} message={message} />;
|
||
} else if (isMaterialMessage(message)) {
|
||
return <MaterialMessage key={message.msgId} id={message.msgId} message={message} />;
|
||
} else if (isExchangeMessage(message)) {
|
||
return (
|
||
<ContactMessage
|
||
key={message.msgId}
|
||
id={message.msgId}
|
||
message={message}
|
||
onClick={handleClickContactButton}
|
||
/>
|
||
);
|
||
} else if (isLocationMessage(message)) {
|
||
return (
|
||
<LocationMessage
|
||
id={message.msgId}
|
||
key={message.msgId}
|
||
message={message}
|
||
isRead={messageStatusList.some(m => m.msgId === message.msgId && !!m.isRead)}
|
||
/>
|
||
);
|
||
}
|
||
})}
|
||
</ScrollView>
|
||
</div>
|
||
<div className={`${PREFIX}__footer`}>
|
||
<div className={`${PREFIX}__input-container`} onTouchStart={handleClickMoreOuter}>
|
||
<Image
|
||
mode="aspectFit"
|
||
className={`${PREFIX}__expand-icon`}
|
||
src={require('@/statics/svg/chat_expand.svg')}
|
||
onTouchStart={e => e.stopPropagation()}
|
||
onClick={handleClickExpand}
|
||
/>
|
||
<Textarea
|
||
fixed
|
||
autoHeight
|
||
value={input}
|
||
maxlength={100}
|
||
cursorSpacing={20}
|
||
confirmType="return"
|
||
onInput={handleInput}
|
||
showConfirmBar={false}
|
||
className={`${PREFIX}__input`}
|
||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||
/>
|
||
<Button className={`${PREFIX}__send-button`} onClick={handleSendTextMessage}>
|
||
发送
|
||
</Button>
|
||
</div>
|
||
{showMore && (
|
||
<div className={`${PREFIX}__more`}>
|
||
<div className={`${PREFIX}__more__item`} onClick={handleClickSendLocation}>
|
||
<div className={`${PREFIX}__more__item__icon-wrapper`}>
|
||
<Image
|
||
mode="aspectFit"
|
||
className={`${PREFIX}__more__item__icon`}
|
||
src={require('@/statics/svg/location_black.svg')}
|
||
/>
|
||
</div>
|
||
<div className={`${PREFIX}__more__item__text`}>定位</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<SafeBottomPadding />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|