feat: first commit
This commit is contained in:
430
src/pages/message-chat/index.tsx
Normal file
430
src/pages/message-chat/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user