feat: first commit

This commit is contained in:
eleanor.mao
2025-03-31 22:34:22 +08:00
commit d25187c9c8
390 changed files with 57031 additions and 0 deletions

View File

@ -0,0 +1,430 @@
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 [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);
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}
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>
);
}