Files
boluo-app-main/src/pages/message-chat/index.tsx
2025-06-16 00:29:17 +08:00

434 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}