UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

493 lines (484 loc) • 26.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const styles_1 = require("@mui/material/styles"); const api_services_1 = require("@selfcommunity/api-services"); const react_core_1 = require("@selfcommunity/react-core"); const types_1 = require("@selfcommunity/types"); const PrivateMessageThreadItem_1 = tslib_1.__importStar(require("../PrivateMessageThreadItem")); const pubsub_js_1 = tslib_1.__importDefault(require("pubsub-js")); const react_intl_1 = require("react-intl"); const material_1 = require("@mui/material"); const PrivateMessageEditor_1 = tslib_1.__importDefault(require("../PrivateMessageEditor")); const Autocomplete_1 = tslib_1.__importDefault(require("@mui/material/Autocomplete")); const classnames_1 = tslib_1.__importDefault(require("classnames")); const system_1 = require("@mui/system"); const Icon_1 = tslib_1.__importDefault(require("@mui/material/Icon")); const Skeleton_1 = tslib_1.__importDefault(require("./Skeleton")); const Errors_1 = require("../../constants/Errors"); const utils_1 = require("@selfcommunity/utils"); const notistack_1 = require("notistack"); const ConfirmDialog_1 = tslib_1.__importDefault(require("../../shared/ConfirmDialog/ConfirmDialog")); const InfiniteScroll_1 = tslib_1.__importDefault(require("../../shared/InfiniteScroll")); const constants_1 = require("./constants"); const translMessages = (0, react_intl_1.defineMessages)({ placeholder: { id: 'ui.privateMessage.thread.newMessage.autocomplete.placeholder', defaultMessage: 'ui.privateMessage.thread.newMessage.autocomplete.placeholder' }, messageDeleted: { id: 'ui.privateMessage.thread.message.deleted', defaultMessage: 'ui.privateMessage.thread.message.deleted' } }); const classes = { root: `${constants_1.PREFIX}-root`, subHeader: `${constants_1.PREFIX}-subheader`, section: `${constants_1.PREFIX}-section`, emptyMessage: `${constants_1.PREFIX}-empty-message`, newMessageHeader: `${constants_1.PREFIX}-new-message-header`, newMessageHeaderContent: `${constants_1.PREFIX}-new-message-header-content`, newMessageHeaderIcon: `${constants_1.PREFIX}-new-message-header-icon`, newMessageContent: `${constants_1.PREFIX}-new-message-content`, sender: `${constants_1.PREFIX}-sender`, receiver: `${constants_1.PREFIX}-receiver`, avatar: `${constants_1.PREFIX}-avatar`, item: `${constants_1.PREFIX}-item`, autocomplete: `${constants_1.PREFIX}-autocomplete`, autocompleteDialog: `${constants_1.PREFIX}-autocomplete-dialog`, editor: `${constants_1.PREFIX}-editor` }; const Root = (0, styles_1.styled)(material_1.Card, { name: constants_1.PREFIX, slot: 'Root' })(() => ({})); /** * > API documentation for the Community-JS PrivateMessage Thread component. Learn about the available props and the CSS API. * * * This component renders the conversation between two users. * Take a look at our <strong>demo</strong> component [here](/docs/sdk/community-js/react-ui/Components/Thread) #### Import ```jsx import {PrivateMessageThread} from '@selfcommunity/react-ui'; ``` #### Component Name The name `SCPrivateMessageThread` can be used when providing style overrides in the theme. #### CSS |Rule Name|Global class|Description| |---|---|---| |root|.SCPrivateMessageThread-root|Styles applied to the root element.| |subHeader|.SCPrivateMessageThread-subheader|Styles applied to thread list subheader element.| |section|.SCPrivateMessageThread-section|Styles applied to the list section| |emptyMessage|.SCPrivateMessageThread-empty-message|Styles applied to the empty message element.| |newMessageHeader|.SCPrivateMessageThread-new-message-header|Styles applied to the new message header section.| |newMessageHeaderContent|.SCPrivateMessageThread-new-message-header-content|Styles applied to the new message header content.| |newMessageHeaderIcon|.SCPrivateMessageThread-new-message-header-icon|Styles applied to the new message header icon element.| |newMessageContent|.SCPrivateMessageThread-new-message-content|Styles applied to the new message content.| |sender|.SCPrivateMessageThread-sender|Styles applied to the sender element.| |receiver|.SCPrivateMessageThread-receiver|Styles applied to the receiver element.| |autocomplete|.SCPrivateMessageThread-autocomplete|Styles applied to autocomplete element.| |autocompleteDialog|.SCPrivateMessageThread-autocomplete-dialog|Styles applied to autocomplete dialog element.| |editor|.SCPrivateMessageThread-editor|Styles applied to the editor element.| * @param inProps */ function PrivateMessageThread(inProps) { // PROPS const props = (0, system_1.useThemeProps)({ props: inProps, name: constants_1.PREFIX }); const { threadObj, openNewMessage = false, onNewMessageClose = null, onNewMessageSent = null, onSingleMessageOpen = null, className, type } = props, rest = tslib_1.__rest(props, ["threadObj", "openNewMessage", "onNewMessageClose", "onNewMessageSent", "onSingleMessageOpen", "className", "type"]); // CONTEXT const scUserContext = (0, react_1.useContext)(react_core_1.SCUserContext); // STATE const [value, setValue] = (0, react_1.useState)(''); const [previous, setPrevious] = (0, react_1.useState)(null); const [messageObjs, setMessageObjs] = (0, react_1.useState)([]); const [loadingMessageObjs, setLoadingMessageObjs] = (0, react_1.useState)(true); const [loading, setLoading] = (0, react_1.useState)(false); const [isHovered, setIsHovered] = (0, react_1.useState)({}); const [followers, setFollowers] = (0, react_1.useState)([]); const isNew = threadObj && threadObj === types_1.SCPrivateMessageStatusType.NEW; const authUserId = scUserContext.user ? scUserContext.user.id : null; const [singleMessageUser, setSingleMessageUser] = (0, react_1.useState)(null); const [receiver, setReceiver] = (0, react_1.useState)(null); const [deletingMsg, setDeletingMsg] = (0, react_1.useState)(null); const [singleMessageThread, setSingleMessageThread] = (0, react_1.useState)(false); const [openDeleteMessageDialog, setOpenDeleteMessageDialog] = (0, react_1.useState)(false); const [recipients, setRecipients] = (0, react_1.useState)([]); const { enqueueSnackbar, closeSnackbar } = (0, notistack_1.useSnackbar)(); const isNumber = typeof threadObj === 'number'; const messageReceiver = (item, loggedUserId) => { var _a, _b, _c; return ((_a = item === null || item === void 0 ? void 0 : item.receiver) === null || _a === void 0 ? void 0 : _a.id) !== loggedUserId ? (_b = item === null || item === void 0 ? void 0 : item.receiver) === null || _b === void 0 ? void 0 : _b.id : (_c = item === null || item === void 0 ? void 0 : item.sender) === null || _c === void 0 ? void 0 : _c.id; }; const [error, setError] = (0, react_1.useState)(false); // REFS const refreshSubscription = (0, react_1.useRef)(null); // INTL const intl = (0, react_intl_1.useIntl)(); // HOOKS // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore const { scUser } = (0, react_core_1.useSCFetchUser)({ id: isNumber && threadObj, threadObj }); const messagesEndRef = (0, react_1.useRef)(null); const scrollToBottom = () => { var _a; (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'end', behavior: 'instant' }); }; // UTILS const format = (item) => intl.formatDate(item.created_at, { year: 'numeric', day: 'numeric', month: 'long' }); // CONST const formattedMessages = (0, react_1.useMemo)(() => { const _messages = [...messageObjs]; return (0, utils_1.groupBy)(_messages, format); }, [messageObjs]); // HANDLERS const handleMouseEnter = (index) => { setIsHovered((prevState) => { return Object.assign(Object.assign({}, prevState), { [index]: true }); }); }; const handleMouseLeave = (index) => { setIsHovered((prevState) => { return Object.assign(Object.assign({}, prevState), { [index]: false }); }); }; /** * Handles delete message dialog opening * @param msg */ const handleOpenDeleteMessageDialog = (msg) => { setOpenDeleteMessageDialog(true); setDeletingMsg(msg); }; /** * Handles delete message dialog close */ const handleCloseDeleteMessageDialog = () => { setOpenDeleteMessageDialog(false); }; /** * Memoized message recipients ids */ const ids = (0, react_1.useMemo)(() => { if (!recipients) return []; return Array.isArray(recipients) ? recipients.map((u) => parseInt(u.id, 10)) : [parseInt(recipients.id || recipients, 10)]; }, [recipients, openNewMessage]); function fetchResults() { setLoading(true); api_services_1.PrivateMessageService.searchUser(value) .then((data) => { setLoading(false); setFollowers(data.results.filter((user) => user.id !== authUserId)); }) .catch((error) => { setLoading(false); console.log(error); }); } const handleInputChange = (event, value, reason) => { switch (reason) { case 'input': setValue(value); !value && setFollowers([]); break; case 'reset': setValue(value); break; } }; const handleChange = (event, value, reason) => { event.preventDefault(); event.stopPropagation(); switch (reason) { case 'selectOption': handleClear(event); setRecipients(value); break; case 'removeOption': handleClear(event, value); break; } return false; }; const handleClear = (event, value) => { setValue(''); setFollowers([]); setRecipients(value !== null && value !== void 0 ? value : []); }; const handleNewMessageClose = () => { handleClear(); // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore onNewMessageClose && onNewMessageClose(); }; function updateAndDeleteURLParameters(url, paramToUpdate, newValue, paramToDelete) { const urlObj = new URL(url); urlObj.searchParams.set(paramToUpdate, newValue); urlObj.searchParams.delete(paramToDelete); return urlObj.toString(); } const handlePrevious = (0, react_1.useMemo)(() => () => { if (!previous) { return; } return api_services_1.http .request({ url: previous, method: api_services_1.Endpoints.GetAThread.method }) .then((res) => { const _prev = [...messageObjs]; _prev.unshift(...res.data.results); setMessageObjs(_prev); setPrevious(res.data.next && updateAndDeleteURLParameters(res.data.next, 'before_message', res.data.results[0].id, 'offset')); }) .catch((error) => console.log(error)); }, [previous, messageObjs]); /** * Fetches thread */ function fetchThread() { if (threadObj && typeof threadObj !== 'string' && type !== types_1.SCPrivateMessageType.GROUP) { setLoadingMessageObjs(true); const _userObjId = isNumber ? threadObj : messageReceiver(threadObj, authUserId); api_services_1.PrivateMessageService.getAThread({ user: _userObjId, limit: 10 }) .then((res) => { setMessageObjs(res.results); setPrevious(res.next && updateAndDeleteURLParameters(res.next, 'before_message', res.results[0].id, 'offset')); if (res.results.length) { if (res.results[0].receiver.id !== authUserId) { setReceiver(res.results[0].receiver); } else { setReceiver(res.results[0].sender); } setSingleMessageThread(false); } else { if (scUser === null || scUser === void 0 ? void 0 : scUser.can_send_pm_to) { setSingleMessageThread(true); setRecipients(_userObjId); onSingleMessageOpen(true); setSingleMessageUser(scUser); } else { setSingleMessageThread(false); } } setLoadingMessageObjs(false); }) .catch((error) => { setLoadingMessageObjs(false); console.log(error); utils_1.Logger.error(Errors_1.SCOPE_SC_UI, { error }); }); } else if (type === types_1.SCPrivateMessageType.GROUP) { api_services_1.PrivateMessageService.getAThread({ group: isNumber ? threadObj : threadObj.group.id, limit: 10 }) .then((res) => { setMessageObjs(res.results); setPrevious(res.next && updateAndDeleteURLParameters(res.next, 'before_message', res.results[0].id, 'offset')); setLoadingMessageObjs(false); setSingleMessageThread(false); }) .catch((error) => { setLoadingMessageObjs(false); console.log(error); utils_1.Logger.error(Errors_1.SCOPE_SC_UI, { error }); }); } } const isNewerThan60Seconds = (creationTime) => { const date = new Date(creationTime); const now = new Date(); return now.getTime() - date.getTime() < 60000; }; function updateMessageAfterDeletion(id) { const newMessageObjects = [...messageObjs]; const index = newMessageObjects.findIndex((s) => s.id === id); if (index !== -1) { newMessageObjects[index].message = `${intl.formatMessage(translMessages.messageDeleted)}`; newMessageObjects[index].file = null; newMessageObjects[index].status = types_1.SCPrivateMessageStatusType.HIDDEN; setMessageObjs(newMessageObjects); } } /** * Handles the deletion of a single message */ function handleDeleteMessage() { const toHide = isNewerThan60Seconds(deletingMsg.created_at); api_services_1.PrivateMessageService.deleteAMessage(deletingMsg.id) .then(() => { const result = messageObjs.filter((m) => m.id !== deletingMsg.id); toHide ? setMessageObjs(result) : updateMessageAfterDeletion(deletingMsg.id); handleSnippetsUpdate((result.length >= 1 && toHide) || (!toHide && deletingMsg.id !== messageObjs.slice(-1)[0].id) ? result.slice(-1) : [deletingMsg]); handleCloseDeleteMessageDialog(); }) .catch((error) => { console.log(error); let _snackBar = enqueueSnackbar((0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.privateMessage.thread.error.delete.msg", defaultMessage: "ui.privateMessage.thread.error.delete.msg" }), { variant: 'error', SnackbarProps: { onClick: () => { closeSnackbar(_snackBar); } } }); }); } /** * Updates snippets list when a new message(or more) are sent or another one is deleted * @param message, it can be a single object or an array of objects */ function handleSnippetsUpdate(message) { pubsub_js_1.default.publish('snippetsChannel', message); } /** * Handles message sending * @param message * @param file */ function handleSend(message, file) { if (react_core_1.UserUtils.isBlocked(scUserContext.user)) { enqueueSnackbar((0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.common.userBlocked", defaultMessage: "ui.common.userBlocked" }), { variant: 'warning', autoHideDuration: 3000 }); } else { api_services_1.http .request({ url: api_services_1.Endpoints.SendMessage.url(), method: api_services_1.Endpoints.SendMessage.method, data: Object.assign(Object.assign({}, (type === types_1.SCPrivateMessageType.GROUP ? { group: isNumber ? threadObj : threadObj.group.id } : { recipients: openNewMessage || isNew || singleMessageThread ? ids : [isNumber && threadObj ? threadObj : messageReceiver(threadObj, authUserId)] })), { // recipients: openNewMessage || isNew || singleMessageThread ? ids : [isNumber && threadObj ? threadObj : messageReceiver(threadObj, authUserId)], message: message, file_uuid: file && !message ? file : null }) }) .then((res) => { const isOne = res.data.length <= 1; isOne && setMessageObjs((prev) => [...prev, res.data[0]]); handleSnippetsUpdate(res.data); if (openNewMessage || singleMessageThread) { setSingleMessageThread(false); onSingleMessageOpen(false); setRecipients([]); onNewMessageSent(res.data[0], isOne); } scrollToBottom(); }) .catch((error) => { console.log(error); setError(true); }); } } /** * Reset component state */ const reset = (0, react_1.useCallback)(() => () => { setLoadingMessageObjs(true); setMessageObjs([]); setPrevious(null); setReceiver(null); setSingleMessageThread(false); }, [setLoadingMessageObjs, setMessageObjs, setPrevious, setReceiver, setSingleMessageThread]); // EFFECTS /** * If a value is entered in new message field, it fetches user followers */ (0, react_1.useEffect)(() => { if (value) { fetchResults(); } }, [value]); /** * On mount, if obj, fetches thread */ (0, react_1.useEffect)(() => { if (!authUserId) { return; } if (threadObj) { fetchThread(); } else { reset(); } }, [threadObj, authUserId, scUser]); /** * Notification subscriber */ const subscriber = (msg, data) => { const res = data.data; const newMessages = [...messageObjs]; const index = newMessages.findIndex((m) => m.thread_id === res.thread_id); const _message = res.notification_obj.message; _message.receiver = res.notification_obj.snippet.receiver; _message.thread_status = res.notification_obj.snippet.thread_status; handleSnippetsUpdate([_message]); if (index !== -1) { setMessageObjs((prev) => [...prev, res.notification_obj.message]); } if (isNumber ? threadObj === res.thread_id : threadObj.id === res.thread_id) { scrollToBottom(); } }; /** * When a ws notification arrives, updates thread and snippets data */ (0, react_1.useEffect)(() => { refreshSubscription.current = pubsub_js_1.default.subscribe(`${types_1.SCNotificationTopicType.INTERACTION}.${types_1.SCNotificationTypologyType.PRIVATE_MESSAGE}`, subscriber); return () => { pubsub_js_1.default.unsubscribe(refreshSubscription.current); }; }, [messageObjs]); /** * Renders thread component * @return {JSX.Element} */ function renderThread() { if (loadingMessageObjs) { return (0, jsx_runtime_1.jsx)(Skeleton_1.default, {}); } return ((0, jsx_runtime_1.jsxs)(material_1.CardContent, { children: [(0, jsx_runtime_1.jsx)(InfiniteScroll_1.default, Object.assign({ height: '100%', dataLength: messageObjs.length, previous: handlePrevious, inverse: true, hasMorePrevious: Boolean(previous), loaderPrevious: (0, jsx_runtime_1.jsx)(PrivateMessageThreadItem_1.PrivateMessageThreadItemSkeleton, {}) }, { children: (0, jsx_runtime_1.jsx)(material_1.List, Object.assign({ ref: messagesEndRef }, { children: Object.keys(formattedMessages).map((key) => ((0, jsx_runtime_1.jsx)("li", Object.assign({ className: classes.section }, { children: (0, jsx_runtime_1.jsxs)("ul", { children: [(0, jsx_runtime_1.jsx)(material_1.ListSubheader, { children: (0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ align: "center", className: classes.subHeader }, { children: key })) }), formattedMessages[key].map((msg) => { var _a; return ((0, jsx_runtime_1.jsxs)(material_1.Box, Object.assign({ className: classes.item }, { children: [msg.group && ((_a = scUserContext === null || scUserContext === void 0 ? void 0 : scUserContext.user) === null || _a === void 0 ? void 0 : _a.username) !== msg.sender.username && ((0, jsx_runtime_1.jsx)(material_1.ListItemAvatar, { children: (0, jsx_runtime_1.jsx)(material_1.Avatar, { alt: msg.sender.username, src: msg.sender.avatar, className: classes.avatar }) })), (0, jsx_runtime_1.jsx)(PrivateMessageThreadItem_1.default, { className: authUserId === msg.sender.id ? classes.sender : classes.receiver, message: msg, mouseEvents: { onMouseEnter: () => handleMouseEnter(msg.id), onMouseLeave: () => handleMouseLeave(msg.id) }, isHovering: isHovered[msg.id], showMenuIcon: authUserId === msg.sender.id, onMenuIconClick: () => handleOpenDeleteMessageDialog(msg) }, msg.id)] }), msg.id)); })] }) }), key))) })) })), (0, jsx_runtime_1.jsx)(PrivateMessageEditor_1.default, { className: classes.editor, send: handleSend, autoHide: type !== types_1.SCPrivateMessageType.GROUP && !(scUser === null || scUser === void 0 ? void 0 : scUser.can_send_pm_to), autoHideDeletion: type === types_1.SCPrivateMessageType.USER && ((receiver === null || receiver === void 0 ? void 0 : receiver.deleted) || (scUser === null || scUser === void 0 ? void 0 : scUser.deleted)), onThreadChangeId: isNumber ? threadObj : type === types_1.SCPrivateMessageType.USER ? threadObj.receiver.id : threadObj.group.id, error: error, onErrorRemove: () => setError(false) }), openDeleteMessageDialog && ((0, jsx_runtime_1.jsx)(ConfirmDialog_1.default, { open: openDeleteMessageDialog, title: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.privateMessage.component.delete.message.dialog.msg", defaultMessage: "ui.privateMessage.component.delete.message.dialog.msg" }), btnConfirm: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.privateMessage.component.delete.message.dialog.confirm", defaultMessage: "ui.privateMessage.component.delete.message.dialog.confirm" }), onConfirm: handleDeleteMessage, onClose: handleCloseDeleteMessageDialog }))] })); } /** * Renders empty box (when there is no thread open) or new message box * @return {JSX.Element} */ function renderNewOrNoMessageBox() { return ((0, jsx_runtime_1.jsx)(material_1.CardContent, { children: isNew || singleMessageThread ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(material_1.Box, Object.assign({ className: classes.newMessageHeader }, { children: [(0, jsx_runtime_1.jsxs)(material_1.Box, Object.assign({ className: classes.newMessageHeaderContent }, { children: [(0, jsx_runtime_1.jsx)(Icon_1.default, Object.assign({ className: classes.newMessageHeaderIcon }, { children: "person" })), (0, jsx_runtime_1.jsx)(material_1.Typography, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { defaultMessage: "ui.privateMessage.thread.newMessage.to", id: "ui.privateMessage.thread.newMessage.to" }) }), (0, jsx_runtime_1.jsx)(Autocomplete_1.default, { className: classes.autocomplete, loading: loading, multiple: !singleMessageThread, limitTags: 3, freeSolo: true, disableClearable: true, options: followers, onChange: handleChange, onInputChange: handleInputChange, inputValue: value, value: singleMessageThread ? singleMessageUser : recipients, getOptionLabel: (option) => (option ? option.username : '...'), isOptionEqualToValue: (option, value) => (option ? value.id === option.id : false), renderInput: (params) => ((0, jsx_runtime_1.jsx)(material_1.TextField, Object.assign({}, params, { placeholder: singleMessageThread ? '...' : `${intl.formatMessage(translMessages.placeholder)}`, variant: "standard", InputProps: Object.assign(Object.assign({}, params.InputProps), { disableUnderline: true }) }))), classes: { popper: classes.autocompleteDialog }, disabled: Boolean(singleMessageUser) })] })), (0, jsx_runtime_1.jsx)(material_1.IconButton, Object.assign({ size: "small", onClick: handleNewMessageClose }, { children: (0, jsx_runtime_1.jsx)(Icon_1.default, { children: "close" }) }))] })), (0, jsx_runtime_1.jsx)(material_1.List, { className: classes.newMessageContent }), (0, jsx_runtime_1.jsx)(PrivateMessageEditor_1.default, { className: classes.editor, send: handleSend, autoHide: !followers, error: error, onErrorRemove: () => setError(false) })] })) : ((0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ className: classes.emptyMessage }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.privateMessage.thread.emptyBox.message", defaultMessage: "ui.privateMessage.thread.emptyBox.message" }) }))) })); } // Anonymous if (!authUserId) { return null; } /** * Renders the component */ return ((0, jsx_runtime_1.jsx)(Root, Object.assign({}, rest, { className: (0, classnames_1.default)(classes.root, className) }, { children: threadObj !== null && !isNew && !singleMessageThread ? renderThread() : renderNewOrNoMessageBox() }))); } exports.default = PrivateMessageThread;