import { h, Component } from 'preact'; import PropTypes from 'prop-types'; import { setupPusher } from '../utilities/connect'; import { notifyUser } from '../utilities/connect/newMessageNotify'; import { debounceAction } from '../utilities/debounceAction'; import { addSnackbarItem } from '../Snackbar'; import { processImageUpload } from '../article-form/actions'; import { conductModeration, getAllMessages, sendMessage, sendOpen, getChannels, getUnopenedChannelIds, getContent, deleteMessage, editMessage, } from './actions/actions'; import { CreateChatModal } from './components/CreateChatModal'; import { ChannelFilterButton } from './components/ChannelFilterButton'; import { sendChannelRequest, rejectJoiningRequest, acceptJoiningRequest, getChannelRequestInfo, } from './actions/requestActions'; import { hideMessages, scrollToBottom, setupObserver, getCurrentUser, } from './util'; import { Alert } from './alert'; import { Channels } from './channels'; import { Compose } from './compose'; import { Message } from './message'; import { ActionMessage } from './actionMessage'; import { Content } from './content'; import { DragAndDropZone } from '@utilities/dragAndDrop'; import { dragAndUpload } from '@utilities/dragAndUpload'; import { Button } from '@crayons'; const NARROW_WIDTH_LIMIT = 767; const WIDE_WIDTH_LIMIT = 1600; export class Chat extends Component { static propTypes = { pusherKey: PropTypes.number.isRequired, chatChannels: PropTypes.string.isRequired, chatOptions: PropTypes.string.isRequired, githubToken: PropTypes.string.isRequired, tagModerator: PropTypes.shape({ isTagModerator: PropTypes.bool.isRequired }) .isRequired, }; constructor(props) { super(props); const chatChannels = JSON.parse(props.chatChannels); const chatOptions = JSON.parse(props.chatOptions); this.debouncedChannelFilter = debounceAction( this.triggerChannelFilter.bind(this), ); this.state = { appDomain: document.body.dataset.appDomain, messages: [], scrolled: false, showAlert: false, chatChannels, unopenedChannelIds: [], filterQuery: '', channelTypeFilter: 'all', channelsLoaded: false, channelPaginationNum: 0, fetchingPaginatedChannels: false, activeChannelId: chatOptions.activeChannelId, activeChannel: null, showChannelsList: chatOptions.showChannelsList, showTimestamp: chatOptions.showTimestamp, currentUserId: chatOptions.currentUserId, notificationsPermission: null, activeContent: {}, fullscreenContent: null, expanded: window.innerWidth > NARROW_WIDTH_LIMIT, isMobileDevice: typeof window.orientation !== 'undefined', subscribedPusherChannels: [], messageOffset: 0, showDeleteModal: false, messageDeleteId: null, allMessagesLoaded: false, currentMessageLocation: 0, startEditing: false, activeEditMessage: {}, markdownEdited: false, searchShowing: false, channelUsers: [], showMemberlist: false, memberFilterQuery: null, rerenderIfUnchangedCheck: null, userRequestCount: 0, openModal: false, isTagModerator: JSON.parse(props.tagModerator).isTagModerator, }; if (chatOptions.activeChannelId) { getAllMessages(chatOptions.activeChannelId, 0, this.receiveAllMessages); } } componentDidMount() { const { chatChannels, activeChannelId, showChannelsList, channelTypeFilter, isMobileDevice, channelPaginationNum, currentUserId, appDomain, } = this.state; this.setupChannels(chatChannels); const channelsForPusherSub = chatChannels.filter( this.channelTypeFilterFn('open'), ); this.subscribeChannelsToPusher( channelsForPusherSub, (channel) => `open-channel--${appDomain}-${channel.chat_channel_id}`, ); setupObserver(this.observerCallback); this.subscribePusher( `private-message-notifications--${appDomain}-${currentUserId}`, ); if (activeChannelId) { sendOpen(activeChannelId, this.handleChannelOpenSuccess, null); } if (showChannelsList) { const filters = channelTypeFilter === 'all' ? {} : { filters: `channel_type:${channelTypeFilter}` }; const searchParams = { query: '', retrievalID: activeChannelId, searchType: '', paginationNumber: channelPaginationNum, }; if (activeChannelId !== null) { searchParams.searchType = 'discoverable'; } getChannels(searchParams, filters, this.loadChannels); getUnopenedChannelIds(this.markUnopenedChannelIds); } if (!isMobileDevice) { document.getElementById('messageform').focus(); } if (document.getElementById('chatchannels__channelslist')) { document .getElementById('chatchannels__channelslist') .addEventListener('scroll', this.handleChannelScroll); } this.handleRequestCount(); } shouldComponentUpdate(nextProps, nextState) { if ( this.state.rerenderIfUnchangedCheck !== nextState.rerenderIfUnchangedCheck ) { return false; } } componentDidUpdate() { const { scrolled, currentMessageLocation } = this.state; const messageList = document.getElementById('messagelist'); if (messageList) { if (!scrolled) { scrollToBottom(); } } if (currentMessageLocation && messageList.scrollTop === 0) { messageList.scrollTop = messageList.scrollHeight - (currentMessageLocation + 30); } } handleRequestCount = () => { getChannelRequestInfo().then((response) => { const { result } = response; const { user_joining_requests, channel_joining_memberships } = result; const totalRequest = user_joining_requests?.length + channel_joining_memberships?.length; this.setState({ userRequestCount: totalRequest, }); }); }; filterForActiveChannel = (channels, id, currentUserId) => channels.filter( (channel) => channel.chat_channel_id === parseInt(id, 10) && channel.viewable_by === parseInt(currentUserId, 10), )[0]; subscribePusher = (channelName) => { const { subscribedPusherChannels } = this.state; const { pusherKey } = this.props; if (!subscribedPusherChannels.includes(channelName)) { setupPusher(pusherKey, { channelId: channelName, messageCreated: this.receiveNewMessage, messageDeleted: this.removeMessage, messageEdited: this.updateMessage, channelCleared: this.clearChannel, redactUserMessages: this.redactUserMessages, channelError: this.channelError, mentioned: this.mentioned, messageOpened: this.messageOpened, }); const subscriptions = subscribedPusherChannels; subscriptions.push(channelName); this.setState({ subscribedPusherChannels: subscriptions }); } }; mentioned = () => {}; messageOpened = () => {}; loadChannels = (channels, query) => { const { activeChannelId, appDomain } = this.state; const activeChannel = this.state.activeChannel || channels.filter( (channel) => channel.chat_channel_id === activeChannelId, )[0]; if (activeChannelId && query.length === 0) { this.setState({ chatChannels: channels, scrolled: false, channelsLoaded: true, channelPaginationNum: 0, filterQuery: '', activeChannel: activeChannel || this.filterForActiveChannel(channels, activeChannelId), }); this.setupChannel(activeChannelId, activeChannel); } else if (activeChannelId) { this.setState({ scrolled: false, chatChannels: channels, channelsLoaded: true, channelPaginationNum: 0, filterQuery: query, activeChannel: activeChannel || this.filterForActiveChannel(channels, activeChannelId), }); this.setupChannel(activeChannelId, activeChannel); } else if (channels.length > 0) { this.setState({ chatChannels: channels, channelsLoaded: true, channelPaginationNum: 0, filterQuery: query || '', scrolled: false, }); this.triggerSwitchChannel( channels[0].chat_channel_id, channels[0].channel_modified_slug, channels, ); this.setupChannels(channels); } else { this.setState({ channelsLoaded: true }); } this.subscribeChannelsToPusher( channels.filter(this.channelTypeFilterFn('open')), (channel) => `open-channel--${appDomain}-${channel.chat_channel_id}`, ); this.subscribeChannelsToPusher( channels.filter(this.channelTypeFilterFn('invite_only')), (channel) => `private-channel--${appDomain}-${channel.chat_channel_id}`, ); const chatChannelsList = document.getElementById( 'chatchannels__channelslist', ); if (chatChannelsList) { chatChannelsList.scrollTop = 0; } }; markUnopenedChannelIds = (ids) => { this.setState({ unopenedChannelIds: ids }); }; subscribeChannelsToPusher = (channels, channelNameFn) => { channels.forEach((channel) => { this.subscribePusher(channelNameFn(channel)); }); }; channelTypeFilterFn = (type) => (channel) => { return channel.channel_type === type; }; setupChannels = (channels) => { const { activeChannel } = this.state; channels.forEach((channel, index) => { if (index < 3) { this.setupChannel(channel.chat_channel_id, activeChannel); } }); }; loadPaginatedChannels = (channels) => { const { state } = this; const currentChannels = state.chatChannels; const currentChannelIds = currentChannels.map((channel) => channel.id); const newChannels = currentChannels; channels.forEach((channel) => { if (!currentChannelIds.includes(channel.id)) { newChannels.push(channel); } }); if ( currentChannels.length === newChannels.length && state.channelPaginationNum > 3 ) { return; } this.setState({ chatChannels: newChannels, fetchingPaginatedChannels: false, channelPaginationNum: state.channelPaginationNum + 1, }); }; setupChannel = (channelId, activeChannel) => { const { messages, messageOffset, appDomain } = this.state; if ( !messages[channelId] || messages[channelId].length === 0 || messages[channelId][0].reception_method === 'pushed' ) { getAllMessages(channelId, messageOffset, this.receiveAllMessages); } if ( activeChannel && activeChannel.channel_type !== 'direct' && activeChannel.chat_channel_id === channelId ) { getContent( `/chat_channels/${channelId}/channel_info`, this.setOpenChannelUsers, null, ); if (activeChannel.channel_type === 'open') this.subscribePusher(`open-channel--${appDomain}-${channelId}`); } this.subscribePusher(`private-channel--${appDomain}-${channelId}`); }; setOpenChannelUsers = (res) => { const { activeChannelId, activeChannel } = this.state; Object.filter = (obj, predicate) => Object.fromEntries(Object.entries(obj).filter(predicate)); const leftUser = Object.filter( res.channel_users, ([username]) => username !== window.currentUser.username, ); if (activeChannel && activeChannel.channel_type === 'open') { this.setState({ channelUsers: { [activeChannelId]: leftUser, }, }); } else { this.setState({ channelUsers: { [activeChannelId]: { all: { username: 'all', name: 'To notify everyone here' }, ...leftUser, }, }, }); } }; observerCallback = (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && this.state.scrolled === true) { this.setState({ scrolled: false, showAlert: false }); } else if (this.state.scrolled === false) { this.setState({ scrolled: true, rerenderIfUnchangedCheck: Math.random(), }); } }); }; channelError = (_error) => { this.setState({ subscribedPusherChannels: [], }); }; receiveAllMessages = (res) => { const { chatChannelId, messages } = res; this.setState((prevState) => ({ messages: { ...prevState.messages, [chatChannelId]: messages }, scrolled: false, })); }; removeMessage = (message) => { const { activeChannelId } = this.state; this.setState((prevState) => ({ messages: { [activeChannelId]: [ ...prevState.messages[activeChannelId].filter( (oldmessage) => oldmessage.id !== message.id, ), ], }, })); }; updateMessage = (message) => { const { activeChannelId } = this.state; if (message.chat_channel_id === activeChannelId) { this.setState(({ messages }) => { const newMessages = messages; const foundIndex = messages[activeChannelId].findIndex( (oldMessage) => oldMessage.id === message.id, ); newMessages[activeChannelId][foundIndex] = message; return { messages: newMessages }; }); } }; receiveNewMessage = (message) => { const { messages, activeChannelId, chatChannels, currentUserId, unopenedChannelIds, } = this.state; const receivedChatChannelId = message.chat_channel_id; const messageList = document.getElementById('messagelist'); let newMessages = []; const nearBottom = messageList.scrollTop + messageList.offsetHeight + 400 > messageList.scrollHeight; if (nearBottom) { scrollToBottom(); } // If I'm not sender and tab is not active if (message.user_id !== currentUserId && document.hidden) { notifyUser(); } if ( message.temp_id && messages[receivedChatChannelId] && messages[receivedChatChannelId].findIndex( (oldmessage) => oldmessage.temp_id === message.temp_id, ) > -1 ) { // Remove reduntant messages return; } if (messages[receivedChatChannelId]) { newMessages = messages[receivedChatChannelId].slice(); newMessages.push(message); if (newMessages.length > 150) { newMessages.shift(); } } // Show alert if message received and you have scrolled up const newShowAlert = activeChannelId === receivedChatChannelId ? { showAlert: !nearBottom } : {}; let newMessageChannelIndex = 0; let newMessageChannel = null; const newChannelsObj = chatChannels.map((channel, index) => { if (receivedChatChannelId === channel.chat_channel_id) { newMessageChannelIndex = index; newMessageChannel = channel; return { ...channel, channel_last_message_at: new Date() }; } return channel; }); if (newMessageChannelIndex > 0) { newChannelsObj.splice(newMessageChannelIndex, 1); newChannelsObj.unshift(newMessageChannel); } // Mark messages read if (receivedChatChannelId === activeChannelId) { sendOpen(receivedChatChannelId, this.handleChannelOpenSuccess, null); } else { const newUnopenedChannels = unopenedChannelIds; if (!unopenedChannelIds.includes(receivedChatChannelId)) { newUnopenedChannels.push(receivedChatChannelId); } this.setState({ unopenedChannelIds: newUnopenedChannels, }); } // Updating the messages this.setState((prevState) => ({ ...newShowAlert, chatChannels: newChannelsObj, messages: { ...prevState.messages, [receivedChatChannelId]: newMessages, }, })); }; redactUserMessages = (res) => { const { messages } = this.state; const newMessages = hideMessages(messages, res.userId); this.setState({ messages: newMessages }); }; clearChannel = (res) => { this.setState((prevState) => ({ messages: { ...prevState.messages, [res.chat_channel_id]: [] }, })); }; handleChannelScroll = (e) => { const { fetchingPaginatedChannels, chatChannels, channelTypeFilter, filterQuery, activeChannelId, channelPaginationNum, } = this.state; if (fetchingPaginatedChannels || chatChannels.length < 30) { return; } const { target } = e; if (target.scrollTop + target.offsetHeight + 1800 > target.scrollHeight) { this.setState({ fetchingPaginatedChannels: true }); const filters = channelTypeFilter === 'all' ? {} : { filters: `channel_type:${channelTypeFilter}` }; const searchParams = { query: filterQuery, retrievalID: activeChannelId, searchType: '', paginationNumber: channelPaginationNum, }; getChannels(searchParams, filters, this.loadPaginatedChannels); } }; handleKeyDown = (e) => { const { showMemberlist, activeContent, activeChannelId, messages, currentUserId, } = this.state; const enterPressed = e.keyCode === 13; const leftPressed = e.keyCode === 37; const rightPressed = e.keyCode === 39; const escPressed = e.keyCode === 27; const targetValue = e.target.value; const messageIsEmpty = targetValue.length === 0; const shiftPressed = e.shiftKey; const upArrowPressed = e.keyCode === 38; const deletePressed = e.keyCode === 46; if (enterPressed) { if (showMemberlist) { e.preventDefault(); const selectedUser = document.getElementsByClassName( 'active__message__list', )[0]; this.addUserName({ target: selectedUser }); } else if (messageIsEmpty) { e.preventDefault(); } else if (!messageIsEmpty && !shiftPressed) { e.preventDefault(); this.handleMessageSubmit(e.target.value); } } if (e.target.value.includes('@')) { if (e.keyCode === 40 || e.keyCode === 38) { e.preventDefault(); } } if ( leftPressed && activeContent[activeChannelId] && e.target.value === '' && document.getElementById('activecontent-iframe') ) { e.preventDefault(); try { e.target.value = document.getElementById( 'activecontent-iframe', ).contentWindow.location.href; } catch (err) { e.target.value = activeContent[activeChannelId].path; } } if ( rightPressed && !activeContent[activeChannelId] && e.target.value === '' ) { e.preventDefault(); const richLinks = document.getElementsByClassName( 'chatchannels__richlink', ); if (richLinks.length === 0) { return; } this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: richLinks[richLinks.length - 1].href, type_of: 'article', }); } if (escPressed && activeContent[activeChannelId]) { this.setActiveContentState(activeChannelId, null); this.setState({ fullscreenContent: null, expanded: window.innerWidth > NARROW_WIDTH_LIMIT, }); } if (messageIsEmpty) { const messagesByCurrentUser = messages[activeChannelId]?.filter( (message) => message.user_id === currentUserId, ); const lastMessage = messagesByCurrentUser?.[messagesByCurrentUser.length - 1]; if (lastMessage) { if (upArrowPressed) { this.triggerEditMessage(lastMessage.id); } else if (deletePressed) { this.triggerDeleteMessage(lastMessage.id); } } } }; handleKeyDownEdit = (e) => { const enterPressed = e.keyCode === 13; const targetValue = e.target.value; const messageIsEmpty = targetValue.length === 0; const shiftPressed = e.shiftKey; if (enterPressed) { if (messageIsEmpty) { e.preventDefault(); } else if (!messageIsEmpty && !shiftPressed) { e.preventDefault(); this.handleMessageSubmitEdit(e.target.value); } } }; handleMessageSubmitEdit = (message) => { const { activeChannelId, activeEditMessage } = this.state; const editedMessage = { activeChannelId, id: activeEditMessage.id, message, }; editMessage(editedMessage, this.handleSuccess, this.handleFailure); this.handleEditMessageClose(); }; handleMessageSubmit = (message) => { const { activeChannelId } = this.state; scrollToBottom(); // should check if user has the privilege if (message.startsWith('/code')) { this.setActiveContentState(activeChannelId, { type_of: 'code_editor' }); } else if (message.startsWith('/play ')) { const messageObject = { activeChannelId, message, mentionedUsersId: this.getMentionedUsers(message), }; sendMessage(messageObject, this.handleSuccess, this.handleFailure); } else if (message.startsWith('/new')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: '/new', type_of: 'article', }); } else if (message.startsWith('/search')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: `/search?q=${message.replace('/search ', '')}`, type_of: 'article', }); } else if (message.startsWith('/s ')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: `/search?q=${message.replace('/s ', '')}`, type_of: 'article', }); } else if (message.startsWith('/ban ') || message.startsWith('/unban ')) { conductModeration( activeChannelId, message, this.handleSuccess, this.handleFailure, ); } else if (message.startsWith('/draw')) { this.setActiveContent({ sendCanvasImage: this.sendCanvasImage, type_of: 'draw', }); } else if (message.startsWith('/')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: message, type_of: 'article', }); } else if (message.startsWith('/github')) { const args = message.split('/github ')[1].trim(); this.setActiveContentState(activeChannelId, { type_of: 'github', args }); } else { const messageObject = { activeChannelId, message, mentionedUsersId: this.getMentionedUsers(message), }; this.setState({ scrolled: false, showAlert: false }); sendMessage(messageObject, this.handleSuccess, this.handleFailure); } }; hideChannelList = () => { const chatContainer = document.getElementsByClassName('chat__activechat')[0]; chatContainer.classList.remove('chat__activechat--hidden'); }; handleSwitchChannel = (e) => { e.preventDefault(); let { target } = e; this.hideChannelList(); if (!target.dataset.channelId) { target = target.parentElement; } this.triggerSwitchChannel( parseInt(target.dataset.channelId, 10), target.dataset.channelSlug, ); }; triggerSwitchChannel = (id, slug, channels) => { const { chatChannels, isMobileDevice, unopenedChannelIds, activeChannelId, currentUserId, } = this.state; const channelList = channels || chatChannels; const newUnopenedChannelIds = unopenedChannelIds; const index = newUnopenedChannelIds.indexOf(id); if (index > -1) { newUnopenedChannelIds.splice(index, 1); } const updatedActiveChannel = this.filterForActiveChannel( channelList, id, currentUserId, ); this.setState({ activeChannel: updatedActiveChannel, activeChannelId: parseInt(id, 10), scrolled: false, showAlert: false, allMessagesLoaded: false, showMemberlist: false, unopenedChannelIds: unopenedChannelIds.filter( (unopenedId) => unopenedId !== id, ), }); this.setupChannel(id, updatedActiveChannel); const params = new URLSearchParams(window.location.search); if (params.get('ref') === 'group_invite') { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: '/chat_channel_memberships', type_of: 'article', }); } window.history.replaceState(null, null, `/connect/${slug}`); if (!isMobileDevice) { document.getElementById('messageform').focus(); } if (window.ga && ga.create) { ga('send', 'pageview', window.location.pathname + window.location.search); } sendOpen(id, this.handleChannelOpenSuccess, null); }; handleSubmitOnClick = (e) => { e.preventDefault(); const message = document.getElementById('messageform').value; if (message.length > 0) { this.handleMessageSubmit(message); } }; handleSubmitOnClickEdit = (e) => { e.preventDefault(); const message = document.getElementById('messageform').value; if (message.length > 0) { this.handleMessageSubmitEdit(message); } }; triggerDeleteMessage = (messageId) => { this.setState({ messageDeleteId: messageId }); this.setState({ showDeleteModal: true }); }; triggerEditMessage = (messageId) => { const { messages, activeChannelId } = this.state; this.setState({ activeEditMessage: messages[activeChannelId].filter( (message) => message.id === messageId, )[0], }); this.setState({ startEditing: true }); }; handleSuccess = (response) => { const { activeChannelId } = this.state; scrollToBottom(); if (response.status === 'success') { if (response.message.temp_id) { this.setState(({ messages }) => { const newMessages = messages; const foundIndex = messages[activeChannelId].findIndex( (message) => message.temp_id === response.message.temp_id, ); if (foundIndex > 0) { newMessages[activeChannelId][foundIndex].id = response.message.id; } return { messages: newMessages }; }); } } else if (response.status === 'moderation-success') { addSnackbarItem({ message: response.message, addCloseButton: true }); } else if (response.status === 'error') { addSnackbarItem({ message: response.message, addCloseButton: true }); } }; handleRequestRejection = (e) => { rejectJoiningRequest( e.target.dataset.channelId, e.target.dataset.membershipId, this.handleJoiningManagerSuccess(e.target.dataset.membershipId), null, ); }; handleRequestApproval = (e) => { acceptJoiningRequest( e.target.dataset.channelId, e.target.dataset.membershipId, this.handleJoiningManagerSuccess(e.target.dataset.membershipId), null, ); }; handleUpdateRequestCount = (isAccepted = false, acceptedInfo) => { if (isAccepted) { const searchParams = { query: '', retrievalID: null, searchType: '', paginationNumber: 0, }; getChannels(searchParams, 'all', this.loadChannels); this.triggerSwitchChannel( parseInt(acceptedInfo.channelId, 10), acceptedInfo.channelSlug, this.state.chatChannels, ); } this.setState((prevState) => { return { userRequestCount: prevState.userRequestCount - 1, }; }); }; triggerActiveContent = (e) => { if ( // Trying to open in new tab e.ctrlKey || e.shiftKey || e.metaKey || // apple (e.button && e.button === 1) // middle click, >IE9 + everyone else ) { return false; } const { target } = e; const content = target.dataset.content || target.parentElement.dataset.content; if (content) { e.preventDefault(); e.stopPropagation(); this.hideChannelList(); const { activeChannelId, activeChannel } = this.state; if (content.startsWith('chat_channels/')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-user', }); getContent(`/${content}/channel_info`, this.setActiveContent, null); } else if (content === 'sidecar-channel-request') { this.setActiveContent({ data: { user: getCurrentUser(), channel: { id: target.dataset.channelId, name: target.dataset.channelName, status: target.dataset.channelStatus, }, }, handleJoiningRequest: this.handleJoiningRequest, type_of: 'channel-request', }); } else if (content === 'sidecar-joining-request-manager') { this.setActiveContent({ data: {}, type_of: 'channel-request-manager', updateRequestCount: this.handleUpdateRequestCount, }); } else if (content === 'sidecar_all') { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: `/chat_channel_memberships/${activeChannel.id}/edit`, type_of: 'article', }); } else if ( content.startsWith('sidecar') || content.startsWith('article') ) { // article is legacy which can be removed shortly this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); this.setActiveContent({ path: target.href || target.parentElement.href, type_of: 'article', }); } else if (target.dataset.content === 'exit') { this.setActiveContentState(activeChannelId, null); this.setState({ fullscreenContent: null, expanded: window.innerWidth > NARROW_WIDTH_LIMIT, }); } else if (target.dataset.content === 'fullscreen') { const mode = this.state.fullscreenContent === 'sidecar' ? null : 'sidecar'; this.setState({ fullscreenContent: mode, expanded: mode === null || window.innerWidth > WIDE_WIDTH_LIMIT, }); } else if (target.dataset.content === 'chat_channel_setting') { this.setActiveContent({ data: {}, type_of: 'chat-channel-setting', activeMembershipId: activeChannel.id, handleLeavingChannel: this.handleLeavingChannel, }); } } return false; }; setActiveContentState = (channelId, state) => { this.setState((prevState) => ({ activeContent: { ...prevState.activeContent, [channelId]: state, }, })); }; closeReportAbuseForm = () => { const { activeChannelId } = this.state; this.setActiveContentState(activeChannelId, null); this.setState({ fullscreenContent: null, expanded: window.innerWidth > NARROW_WIDTH_LIMIT, }); }; setActiveContent = (response) => { const { activeChannelId } = this.state; this.setActiveContentState(activeChannelId, response); setTimeout(() => { document.getElementById('chat_activecontent').scrollTop = 0; document.getElementById('chat').scrollLeft = 1000; }, 3); setTimeout(() => { document.getElementById('chat_activecontent').scrollTop = 0; document.getElementById('chat').scrollLeft = 1000; }, 10); }; handleChannelOpenSuccess = (response) => { this.setState(({ chatChannels }) => { const newChannelsObj = chatChannels.map((channel) => { if (parseInt(response.channel, 10) === channel.chat_channel_id) { return { ...channel, last_opened_at: new Date() }; } return channel; }); return { chatChannels: newChannelsObj }; }); }; handleLeavingChannel = (leftChannelId) => { const { chatChannels } = this.state; this.triggerSwitchChannel( chatChannels[1].chat_channel_id, chatChannels[1].channel_modified_slug, chatChannels, ); this.setState((prevState) => ({ chatChannels: prevState.chatChannels.filter( (channel) => channel.id !== leftChannelId, ), })); this.setActiveContentState(chatChannels[1].chat_channel_id, null); }; triggerChannelTypeFilter = (e) => { const { filterQuery } = this.state; const type = e.target.dataset.channelType; this.setState({ channelTypeFilter: type, fetchingPaginatedChannels: false, }); const filters = type === 'all' ? {} : { filters: `channel_type:${type}` }; const searchParams = { query: filterQuery, retrievalID: null, searchType: '', paginationNumber: 0, }; if (filterQuery && type !== 'direct') { searchParams.searchType = 'discoverable'; getChannels(searchParams, filters, this.loadChannels); } else { getChannels(searchParams, filters, this.loadChannels); } }; handleFailure = (err) => { // eslint-disable-next-line no-console console.error(err); addSnackbarItem({ message: err, addCloseButton: true }); }; renderMessages = () => { const { activeChannelId, messages, showTimestamp, activeChannel, currentUserId, } = this.state; if (!messages[activeChannelId]) { return ''; } if (messages[activeChannelId].length === 0 && activeChannel) { if (activeChannel.channel_type === 'direct') { return (
You and{' '} {activeChannel.channel_modified_slug} {' '} are connected because you both follow each other. All interactions{' '} must {' '} abide by the code of conduct.
); } if (activeChannel.channel_type === 'open') { return (
You have joined {activeChannel.channel_name}! All interactions{' '} must {' '} abide by the code of conduct.
); } } return messages[activeChannelId].map((message) => message.action ? ( ) : ( ), ); }; triggerReportMessage = (messageId) => { const { activeChannelId, messages } = this.state; this.setActiveContent({ data: messages[activeChannelId].find( (message) => message.id === messageId, ), type_of: 'message-report-abuse', }); }; triggerChannelFilter = (e) => { const { channelTypeFilter } = this.state; const filters = channelTypeFilter === 'all' ? {} : { filters: `channel_type:${channelTypeFilter}` }; const searchParams = { query: e.target.value, retrievalID: null, searchType: '', paginationNumber: 0, }; if (e.target.value) { searchParams.searchType = 'discoverable'; getChannels(searchParams, filters, this.loadChannels); } else { getChannels(searchParams, filters, this.loadChannels); } }; toggleExpand = () => { this.setState((prevState) => ({ expanded: !prevState.expanded })); }; toggleSearchShowing = () => { if (!this.state.searchShowing) { setTimeout(() => { document.getElementById('chatchannelsearchbar').focus(); }, 100); } else { const searchParams = { query: '', retrievalID: null, searchType: '', paginationNumber: 0, }; getChannels(searchParams, 'all', this.loadChannels); this.setState({ filterQuery: '' }); } this.setState({ searchShowing: !this.state.searchShowing }); }; renderChatChannels = () => { const { state } = this; if (state.showChannelsList) { const { notificationsPermission } = state; const notificationsButton = ''; let notificationsState = ''; const invitesButton = ''; const joiningRequestButton = ''; if (notificationsPermission === 'granted') { notificationsState = (
Notifications On
); } else if (notificationsPermission === 'denied') { notificationsState = (
Notifications Off
); } return (
{notificationsButton} {invitesButton} {joiningRequestButton}
{this.state.isTagModerator ? ( ) : null} {this.state.openModal ? ( ) : ( '' )}
{notificationsState}
); } return ''; }; toggleModalCreateChannel = () => { const { openModal } = this.state; this.setState({ openModal: !openModal }); }; navigateToChannelsList = () => { const chatContainer = document.getElementsByClassName('chat__activechat')[0]; chatContainer.classList.add('chat__activechat--hidden'); }; handleCreateChannelSuccess = () => { this.toggleModalCreateChannel(); const searchParams = { query: '', retrievalID: null, searchType: '', paginationNumber: 0, }; getChannels(searchParams, {}, this.loadChannels); }; handleMessageScroll = () => { const { allMessagesLoaded, messages, activeChannelId, messageOffset } = this.state; if (!messages[activeChannelId]) { return; } const jumpbackButton = document.getElementById('jumpback_button'); if (this.scroller) { const scrolledRatio = (this.scroller.scrollTop + this.scroller.clientHeight) / this.scroller.scrollHeight; if (scrolledRatio < 0.5) { jumpbackButton.classList.remove('chatchanneljumpback__hide'); } else if (scrolledRatio > 0.6) { jumpbackButton.classList.add('chatchanneljumpback__hide'); } if (this.scroller.scrollTop === 0 && !allMessagesLoaded) { getAllMessages( activeChannelId, messageOffset + messages[activeChannelId].length, this.addMoreMessages, ); const curretPosition = this.scroller.scrollHeight; this.setState({ currentMessageLocation: curretPosition }); } } }; addMoreMessages = (res) => { const { chatChannelId, messages } = res; if (messages.length > 0) { this.setState((prevState) => ({ messages: { [chatChannelId]: [...messages, ...prevState.messages[chatChannelId]], }, })); } else { this.setState({ allMessagesLoaded: true }); } }; jumpBacktoBottom = () => { scrollToBottom(); document .getElementById('jumpback_button') .classList.remove('chatchanneljumpback__hide'); }; handleDragOver = (event) => { event.preventDefault(); event.currentTarget.classList.add('opacity-25'); }; handleDragExit = (event) => { event.preventDefault(); event.currentTarget.classList.remove('opacity-25'); }; handleImageDrop = (event) => { event.preventDefault(); const { files } = event.dataTransfer; event.currentTarget.classList.remove('opacity-25'); processImageUpload(files, this.handleImageSuccess, this.handleImageFailure); }; sendCanvasImage = (files) => { dragAndUpload([files], this.handleImageSuccess, this.handleImageFailure); }; handleImageSuccess = (res) => { const { links, image } = res; const mLink = `![${image[0].name}](${links[0]})`; const el = document.getElementById('messageform'); const start = el.selectionStart; const end = el.selectionEnd; const text = el.value; let before = text.substring(0, start); before = text.substring(0, before.lastIndexOf('@') + 1); const after = text.substring(end, text.length); el.value = `${before + mLink} ${after}`; el.selectionStart = start + mLink.length + 1; el.selectionEnd = el.selectionStart; el.focus(); }; handleImageFailure = (e) => { addSnackbarItem({ message: e.message, addCloseButton: true }); }; handleDragHover(e) { e.preventDefault(); const messageArea = document.getElementById('messagelist'); messageArea.classList.add('opacity-25'); } handleDragExit(e) { e.preventDefault(); const messageArea = document.getElementById('messagelist'); messageArea.classList.remove('opacity-25'); } renderActiveChatChannel = (channelHeader) => { const { state } = this; const channelName = state.activeChannel ? state.activeChannel.channel_name : ' '; const connectAnnouncement = (
We have made the decision to deprecate Connect as core functionality from the application.   Read the announcement to learn more » .
); return (
{connectAnnouncement} {channelHeader}
{ this.scroller = scroller; }} id="messagelist" > {this.renderMessages()}
{this.renderDeleteModal()}
{this.renderChannelMembersList()}
); }; handleFilePaste = (e) => { if (!e.clipboardData || !e.clipboardData.items) { return; } const items = []; for (let i = 0; i < e.clipboardData.items.length; i++) { const item = e.clipboardData.items[i]; if (item.kind !== 'file') { continue; } items.push(item); } if (items && items.length > 0) { processImageUpload( [items[0].getAsFile()], this.handleImageSuccess, this.handleImageFailure, ); } }; handleMention = (e) => { const { activeChannel } = this.state; const mention = e.keyCode === 64; if (mention && activeChannel.channel_type !== 'direct') { const memberListElement = document.getElementById('mentionList'); memberListElement.focus(); this.setState({ showMemberlist: true }); } }; handleKeyUp = (e) => { const { startEditing, activeChannel, showMemberlist } = this.state; const enterPressed = e.keyCode === 13; if (enterPressed && showMemberlist) this.setState({ showMemberlist: false }); if (activeChannel?.channel_type !== 'direct') { if (startEditing) { this.setState({ markdownEdited: true }); } if (!e.target.value.includes('@') && showMemberlist) { this.setState({ showMemberlist: false }); } else { this.setQuery(e.target); this.listHighlightManager(e.keyCode); } } }; setQuery = (e) => { const { showMemberlist } = this.state; if (showMemberlist) { const before = e.value.substring(0, e.selectionStart); const query = before.substring( before.lastIndexOf('@') + 1, e.selectionStart, ); if (query.includes(' ') || before.lastIndexOf('@') < 0) this.setState({ showMemberlist: false }); else { this.setState({ showMemberlist: true }); this.setState({ memberFilterQuery: query }); } } }; addUserName = (e) => { const name = e.target.dataset.content || e.target.parentElement.dataset.content; const el = document.getElementById('messageform'); const start = el.selectionStart; const end = el.selectionEnd; const text = el.value; let before = text.substring(0, start); before = text.substring(0, before.lastIndexOf('@') + 1); const after = text.substring(end, text.length); el.value = `${before + name} ${after}`; el.selectionStart = start + name.length + 1; el.selectionEnd = start + name.length + 1; el.dispatchEvent(new Event('input')); el.focus(); this.setState({ showMemberlist: false }); }; listHighlightManager = (keyCode) => { const mentionList = document.getElementById('mentionList'); const activeElement = document.getElementsByClassName( 'active__message__list', )[0]; if (mentionList.children.length > 0) { if (keyCode === 40 && activeElement) { if (activeElement.nextElementSibling) { activeElement.classList.remove('active__message__list'); activeElement.nextElementSibling.classList.add( 'active__message__list', ); } } else if (keyCode === 38 && activeElement) { if (activeElement.previousElementSibling) { activeElement.classList.remove('active__message__list'); activeElement.previousElementSibling.classList.add( 'active__message__list', ); } } else { mentionList.children[0].classList.add('active__message__list'); } } }; getMentionedUsers = (message) => { const { channelUsers, activeChannelId, activeChannel } = this.state; if (channelUsers[activeChannelId]) { if (message.includes('@all') && activeChannel.channel_type !== 'open') { return Array.from( Object.values(channelUsers[activeChannelId]).filter( (user) => user.id, ), (user) => user.id, ); } return Array.from( Object.values(channelUsers[activeChannelId]).filter((user) => message.includes(user.username), ), (user) => user.id, ); } return null; }; renderChannelMembersList = () => { const { showMemberlist, activeChannelId, channelUsers, memberFilterQuery } = this.state; const filterRegx = new RegExp(memberFilterQuery, 'gi'); return (
{showMemberlist ? Object.values(channelUsers[activeChannelId]) .filter((user) => user.username.match(filterRegx)) .map((user) => (
{ if (e.keyCode === 13) this.addUserName(); }} > {user.name} {'@'} {user.username}

{user.name}

)) : ' '}
); }; handleEditMessageClose = () => { this.setState({ startEditing: false, markdownEdited: false, activeEditMessage: { message: '', markdown: '' }, }); }; renderDeleteModal = () => { const { showDeleteModal } = this.state; return (