* Feature 🚀 : Ability to delete messages in chat channels - Sending message ID to frontend - Deleting Message - Use pusher to delete message realtime * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Refactoring🛠: Message controller refactoring * Test Cases📝 : Specs for Delete message added * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Merge conflict resolved * 🐞 Fixes bug #9503 When joining a new room, user is not taken to the new room Co-authored-by: Narender Singh <narender2031@gmail.com>
1875 lines
59 KiB
JavaScript
1875 lines
59 KiB
JavaScript
/*
|
|
eslint-disable
|
|
consistent-return, no-unused-vars, react/destructuring-assignment,
|
|
react/no-access-state-in-setstate, react/button-has-type
|
|
*/
|
|
|
|
import { h, Component } from 'preact';
|
|
import PropTypes from 'prop-types';
|
|
import { setupPusher } from '../utilities/connect';
|
|
import debounceAction from '../utilities/debounceAction';
|
|
import { addSnackbarItem } from '../Snackbar';
|
|
import {
|
|
conductModeration,
|
|
getAllMessages,
|
|
sendMessage,
|
|
sendOpen,
|
|
getChannels,
|
|
getUnopenedChannelIds,
|
|
getContent,
|
|
deleteMessage,
|
|
editMessage,
|
|
} from './actions/actions';
|
|
import {
|
|
sendChannelRequest,
|
|
rejectJoiningRequest,
|
|
acceptJoiningRequest,
|
|
getChannelRequestInfo,
|
|
} from './actions/requestActions';
|
|
import {
|
|
hideMessages,
|
|
scrollToBottom,
|
|
setupObserver,
|
|
getCurrentUser,
|
|
channelSorter,
|
|
} 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 { VideoContent } from './videoContent';
|
|
|
|
const NARROW_WIDTH_LIMIT = 767;
|
|
const WIDE_WIDTH_LIMIT = 1600;
|
|
|
|
export default class Chat extends Component {
|
|
static propTypes = {
|
|
pusherKey: PropTypes.number.isRequired,
|
|
chatChannels: PropTypes.string.isRequired,
|
|
chatOptions: PropTypes.string.isRequired,
|
|
githubToken: PropTypes.string.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 = {
|
|
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,
|
|
videoPath: 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,
|
|
};
|
|
if (chatOptions.activeChannelId) {
|
|
getAllMessages(chatOptions.activeChannelId, 0, this.receiveAllMessages);
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
const {
|
|
chatChannels,
|
|
activeChannelId,
|
|
showChannelsList,
|
|
channelTypeFilter,
|
|
isMobileDevice,
|
|
channelPaginationNum,
|
|
currentUserId,
|
|
messageOffset,
|
|
} = this.state;
|
|
|
|
this.setupChannels(chatChannels);
|
|
|
|
const channelsForPusherSub = chatChannels.filter(
|
|
this.channelTypeFilterFn('open'),
|
|
);
|
|
this.subscribeChannelsToPusher(
|
|
channelsForPusherSub,
|
|
(channel) => `open-channel-${channel.chat_channel_id}`,
|
|
);
|
|
|
|
setupObserver(this.observerCallback);
|
|
|
|
this.subscribePusher(`private-message-notifications-${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;
|
|
let 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 } = 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);
|
|
} else if (activeChannelId) {
|
|
this.setState({
|
|
scrolled: false,
|
|
chatChannels: channels,
|
|
channelsLoaded: true,
|
|
channelPaginationNum: 0,
|
|
filterQuery: query,
|
|
activeChannel:
|
|
activeChannel ||
|
|
this.filterForActiveChannel(channels, activeChannelId),
|
|
});
|
|
this.setupChannel(activeChannelId);
|
|
} 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,
|
|
);
|
|
} else {
|
|
this.setState({ channelsLoaded: true });
|
|
}
|
|
this.subscribeChannelsToPusher(
|
|
channels.filter(this.channelTypeFilterFn('open')),
|
|
(channel) => `open-channel-${channel.chat_channel_id}`,
|
|
);
|
|
this.subscribeChannelsToPusher(
|
|
channels.filter(this.channelTypeFilterFn('invite_only')),
|
|
(channel) => `private-channel-${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) => {
|
|
channels.forEach((channel, index) => {
|
|
if (index < 3) {
|
|
this.setupChannel(channel.chat_channel_id);
|
|
}
|
|
});
|
|
};
|
|
|
|
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) => {
|
|
const {
|
|
messages,
|
|
messageOffset,
|
|
activeChannel,
|
|
activeChannelId,
|
|
} = 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') {
|
|
getContent(
|
|
`/chat_channels/${activeChannelId}/channel_info`,
|
|
this.setOpenChannelUsers,
|
|
null,
|
|
);
|
|
if (activeChannel.channel_type === 'open')
|
|
this.subscribePusher(`open-channel-${channelId}`);
|
|
}
|
|
this.subscribePusher(`private-channel-${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.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,
|
|
scrolled,
|
|
chatChannels,
|
|
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();
|
|
}
|
|
// Remove reduntant messages
|
|
if (
|
|
message.temp_id &&
|
|
messages[receivedChatChannelId] &&
|
|
messages[receivedChatChannelId].findIndex(
|
|
(oldmessage) => oldmessage.temp_id === message.temp_id,
|
|
) > -1
|
|
) {
|
|
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.querySelector('.active__message__list');
|
|
this.addUserName({ target: selectedUser });
|
|
} else if (messageIsEmpty) {
|
|
e.preventDefault();
|
|
} else if (!messageIsEmpty && !shiftPressed) {
|
|
e.preventDefault();
|
|
this.handleMessageSubmit(e.target.value);
|
|
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.querySelectorAll('.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);
|
|
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('/call')) {
|
|
const messageObject = {
|
|
activeChannelId,
|
|
message: '/call',
|
|
mentionedUsersId: this.getMentionedUsers(message),
|
|
};
|
|
this.setState({ videoPath: `/video_chats/${activeChannelId}` });
|
|
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
|
|
} 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('/')) {
|
|
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.querySelector('.chat__activechat');
|
|
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);
|
|
}
|
|
this.setState({
|
|
activeChannel: this.filterForActiveChannel(
|
|
channelList,
|
|
id,
|
|
currentUserId,
|
|
),
|
|
activeChannelId: parseInt(id, 10),
|
|
scrolled: false,
|
|
showAlert: false,
|
|
allMessagesLoaded: false,
|
|
unopenedChannelIds: unopenedChannelIds.filter(
|
|
(unopenedId) => unopenedId !== id,
|
|
),
|
|
});
|
|
this.setupChannel(id);
|
|
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);
|
|
document.getElementById('messageform').value = '';
|
|
}
|
|
};
|
|
|
|
handleSubmitOnClickEdit = (e) => {
|
|
e.preventDefault();
|
|
const message = document.getElementById('messageform').value;
|
|
if (message.length > 0) {
|
|
this.handleMessageSubmitEdit(message);
|
|
document.getElementById('messageform').value = '';
|
|
}
|
|
};
|
|
|
|
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.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-plus-video')) {
|
|
this.setActiveContentState(activeChannelId, {
|
|
type_of: 'loading-post',
|
|
});
|
|
this.setActiveContent({
|
|
path: target.href || target.parentElement.href,
|
|
type_of: 'article',
|
|
});
|
|
this.setState({ videoPath: `/video_chats/${activeChannelId}` });
|
|
} else if (content.startsWith('sidecar-video')) {
|
|
this.setState({ videoPath: target.href || target.parentElement.href });
|
|
} 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,
|
|
});
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
setActiveContentState = (channelId, state) => {
|
|
this.setState((prevState) => ({
|
|
activeContent: {
|
|
...prevState.activeContent,
|
|
[channelId]: state,
|
|
},
|
|
}));
|
|
};
|
|
|
|
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 };
|
|
});
|
|
};
|
|
|
|
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 (
|
|
<div className="chatmessage" style={{ color: 'grey' }}>
|
|
<div className="chatmessage__body">
|
|
You and{' '}
|
|
<a href={`/${activeChannel.channel_modified_slug}`}>
|
|
{activeChannel.channel_modified_slug}
|
|
</a>{' '}
|
|
are connected because you both follow each other. All interactions{' '}
|
|
<em>
|
|
<b>must</b>
|
|
</em>{' '}
|
|
abide by the <a href="/code-of-conduct">code of conduct</a>.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
if (activeChannel.channel_type === 'open') {
|
|
return (
|
|
<div className="chatmessage" style={{ color: 'grey' }}>
|
|
<div className="chatmessage__body">
|
|
You have joined {activeChannel.channel_name}! All interactions{' '}
|
|
<em>
|
|
<b>must</b>
|
|
</em>{' '}
|
|
abide by the <a href="/code-of-conduct">code of conduct</a>.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
return messages[activeChannelId].map((message) =>
|
|
message.action ? (
|
|
<ActionMessage
|
|
user={message.username}
|
|
profileImageUrl={message.profile_image_url}
|
|
message={message.message}
|
|
timestamp={showTimestamp ? message.timestamp : null}
|
|
color={message.color}
|
|
onContentTrigger={this.triggerActiveContent}
|
|
/>
|
|
) : (
|
|
<Message
|
|
currentUserId={currentUserId}
|
|
id={message.id}
|
|
user={message.username}
|
|
userID={message.user_id}
|
|
profileImageUrl={message.profile_image_url}
|
|
message={message.message}
|
|
timestamp={showTimestamp ? message.timestamp : null}
|
|
editedAt={message.edited_at}
|
|
color={message.color}
|
|
onContentTrigger={this.triggerActiveContent}
|
|
onDeleteMessageTrigger={this.triggerDeleteMessage}
|
|
onEditMessageTrigger={this.triggerEditMessage}
|
|
/>
|
|
),
|
|
);
|
|
};
|
|
|
|
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 }));
|
|
};
|
|
|
|
renderChannelFilterButton = (type, name, active) => (
|
|
<button
|
|
data-channel-type={type}
|
|
onClick={this.triggerChannelTypeFilter}
|
|
className={`chat__channeltypefilterbutton crayons-indicator crayons-indicator--${
|
|
type === active ? 'accent' : ''
|
|
}`}
|
|
type="button"
|
|
>
|
|
{name}
|
|
</button>
|
|
);
|
|
|
|
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 = '';
|
|
let invitesButton = '';
|
|
let joiningRequestButton = '';
|
|
if (notificationsPermission === 'granted') {
|
|
notificationsState = (
|
|
<div className="chat_chatconfig chat_chatconfig--on">
|
|
Notifications On
|
|
</div>
|
|
);
|
|
} else if (notificationsPermission === 'denied') {
|
|
notificationsState = (
|
|
<div className="chat_chatconfig chat_chatconfig--off">
|
|
Notifications Off
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="chat__channels">
|
|
{notificationsButton}
|
|
{state.searchShowing ? (
|
|
<input
|
|
placeholder="Search Channels"
|
|
onKeyUp={this.debouncedChannelFilter}
|
|
id="chatchannelsearchbar"
|
|
className="crayons-textfield"
|
|
aria-label="Search Channels"
|
|
/>
|
|
) : (
|
|
''
|
|
)}
|
|
{invitesButton}
|
|
{joiningRequestButton}
|
|
<div className="chat__channeltypefilter">
|
|
<button
|
|
className="chat__channelssearchtoggle"
|
|
onClick={this.toggleSearchShowing}
|
|
aria-label="Toggle channel search"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
width="17"
|
|
height="17"
|
|
>
|
|
<path fill="none" d="M0 0h24v24H0z" />
|
|
<path d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
|
|
</svg>
|
|
</button>
|
|
{this.renderChannelFilterButton(
|
|
'all',
|
|
'all',
|
|
state.channelTypeFilter,
|
|
)}
|
|
{this.renderChannelFilterButton(
|
|
'direct',
|
|
'direct',
|
|
state.channelTypeFilter,
|
|
)}
|
|
{this.renderChannelFilterButton(
|
|
'invite_only',
|
|
'group',
|
|
state.channelTypeFilter,
|
|
)}
|
|
<button
|
|
className="chat__channelssearchtoggle "
|
|
aria-label="Toggle request manager"
|
|
onClick={this.triggerActiveContent}
|
|
data-content="sidecar-joining-request-manager"
|
|
>
|
|
<span
|
|
onClick={this.triggerActiveContent}
|
|
data-content="sidecar-joining-request-manager"
|
|
role="button"
|
|
aria-hidden="true"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 477.869 477.869"
|
|
width="18"
|
|
height="18"
|
|
>
|
|
<path d="M387.415 233.496c48.976-44.03 52.987-119.424 8.958-168.4C355.99 20.177 288.4 12.546 239.02 47.332c-53.83-38-128.264-25.15-166.254 28.68-34.86 49.393-27.26 117.054 17.69 157.483C34.606 262.935-.25 320.976.002 384.108v51.2a17.07 17.07 0 0 0 17.067 17.067h443.733a17.07 17.07 0 0 0 17.067-17.067v-51.2c.252-63.132-34.605-121.173-90.454-150.612zM307.2 59.842c47.062-.052 85.256 38.057 85.31 85.12.037 33.564-19.63 64.023-50.237 77.8-1.314.597-2.628 1.143-3.96 1.707a83.66 83.66 0 0 1-12.988 4.045c-.853.188-1.707.3-2.577.46-4.952.95-9.977 1.457-15.02 1.52-2.27 0-4.557-.17-6.827-.375-.853 0-1.707 0-2.56-.17a86.22 86.22 0 0 1-27.904-8.226c-.324-.154-.7-.137-1.024-.273-1.707-.82-3.413-1.536-4.932-2.458.137-.17.222-.358.358-.53a119.72 119.72 0 0 0 18.278-33.297l.53-1.434a120.38 120.38 0 0 0 4.523-17.562c.154-.87.273-1.707.4-2.645.987-6.067 1.506-12.2 1.553-18.347a120.04 120.04 0 0 0-1.553-18.313l-.4-2.645c-1.064-5.96-2.576-11.83-4.523-17.562l-.53-1.434c-4.282-12-10.453-23.24-18.278-33.297-.137-.17-.222-.358-.358-.53C277.45 63.83 292.2 59.843 307.2 59.842zM85.335 145.176c-.12-47.006 37.886-85.2 84.892-85.33a85.11 85.11 0 0 1 59.134 23.686l2.918 2.9a87.75 87.75 0 0 1 8.09 9.813c.75 1.058 1.434 2.185 2.133 3.277a83.95 83.95 0 0 1 6.263 11.52c.427.973.75 1.963 1.126 2.935a83.42 83.42 0 0 1 4.233 13.653c.12.512.154 1.024.256 1.553a80.34 80.34 0 0 1 0 32.119c-.102.53-.137 1.04-.256 1.553a83.23 83.23 0 0 1-4.233 13.653c-.375.973-.7 1.963-1.126 2.935a84.25 84.25 0 0 1-6.263 11.503c-.7 1.092-1.382 2.22-2.133 3.277a87.55 87.55 0 0 1-8.09 9.813 117.37 117.37 0 0 1-2.918 2.901c-6.9 6.585-14.877 11.962-23.57 15.906a49.35 49.35 0 0 1-4.198 1.707 85.84 85.84 0 0 1-12.663 3.925c-1.075.24-2.185.375-3.277.563a84.67 84.67 0 0 1-14.046 1.417h-1.877c-4.713-.08-9.412-.554-14.046-1.417-1.092-.188-2.202-.324-3.277-.563a85.8 85.8 0 0 1-12.663-3.925l-4.198-1.707c-30.534-13.786-50.173-44.166-50.212-77.667zM307.2 418.242H34.135V384.11c-.25-57.833 36.188-109.468 90.76-128.614 29.296 12.197 62.25 12.197 91.546 0a137.14 137.14 0 0 1 16.623 7.356c3.55 1.826 6.827 3.908 10.24 6.007 2.22 1.382 4.47 2.73 6.605 4.25 3.294 2.338 6.4 4.88 9.455 7.492l5.75 5.12c2.816 2.662 5.46 5.478 8.004 8.363 1.826 2.082 3.6 4.198 5.29 6.383 2.236 2.867 4.37 5.803 6.35 8.823 1.707 2.56 3.226 5.222 4.727 7.885 1.707 2.935 3.277 5.87 4.7 8.926s2.697 6.4 3.925 9.66c1.075 2.833 2.22 5.65 3.106 8.533 1.195 3.96 2.03 8.055 2.867 12.15.512 2.423 1.178 4.796 1.553 7.253 1.01 6.757 1.53 13.58 1.553 20.412v34.133zm136.534 0h-102.4V384.11c0-5.342-.307-10.633-.785-15.872-.137-1.536-.375-3.055-.546-4.59-.46-3.772-1-7.51-1.707-11.213l-.973-4.762c-.82-3.8-1.77-7.566-2.85-11.298l-1.058-3.686c-4.78-15.277-11.704-29.797-20.565-43.127l-.666-.973a168.96 168.96 0 0 0-9.404-12.646l-.12-.154c-3.413-4.232-7.117-8.346-11.008-12.237h.7a120.8 120.8 0 0 0 14.524 1.024h.94c4.496-.04 8.985-.33 13.45-.87 1.4-.17 2.782-.427 4.18-.65a117.43 117.43 0 0 0 10.752-2.167l3.055-.785a116.21 116.21 0 0 0 13.653-4.642c54.612 19.127 91.083 70.785 90.83 128.65v34.132z" />
|
|
</svg>
|
|
{this.state.userRequestCount > 0 ? (
|
|
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet requests-badge">
|
|
{this.state.userRequestCount}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<Channels
|
|
activeChannelId={state.activeChannelId}
|
|
chatChannels={state.chatChannels}
|
|
unopenedChannelIds={state.unopenedChannelIds}
|
|
handleSwitchChannel={this.handleSwitchChannel}
|
|
channelsLoaded={state.channelsLoaded}
|
|
filterQuery={state.filterQuery}
|
|
expanded={state.expanded}
|
|
aria-expanded={state.expanded}
|
|
currentUserId={state.currentUserId}
|
|
triggerActiveContent={this.triggerActiveContent}
|
|
/>
|
|
{notificationsState}
|
|
</div>
|
|
);
|
|
}
|
|
return '';
|
|
};
|
|
|
|
navigateToChannelsList = () => {
|
|
const chatContainer = document.querySelector('.chat__activechat');
|
|
|
|
chatContainer.classList.add('chat__activechat--hidden');
|
|
};
|
|
|
|
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');
|
|
};
|
|
|
|
renderActiveChatChannel = (channelHeader) => {
|
|
const { state, props } = this;
|
|
|
|
return (
|
|
<div className="activechatchannel">
|
|
<div className="activechatchannel__conversation">
|
|
{channelHeader}
|
|
<div
|
|
className="activechatchannel__messages"
|
|
onScroll={this.handleMessageScroll}
|
|
ref={(scroller) => {
|
|
this.scroller = scroller;
|
|
}}
|
|
id="messagelist"
|
|
>
|
|
{this.renderMessages()}
|
|
<div className="messagelist__sentinel" id="messagelist__sentinel" />
|
|
</div>
|
|
<div
|
|
className="chatchanneljumpback chatchanneljumpback__hide"
|
|
id="jumpback_button"
|
|
>
|
|
<div
|
|
role="button"
|
|
className="chatchanneljumpback__messages"
|
|
onClick={this.jumpBacktoBottom}
|
|
tabIndex="0"
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.jumpBacktoBottom();
|
|
}}
|
|
>
|
|
Scroll to Bottom
|
|
</div>
|
|
</div>
|
|
{this.renderDeleteModal()}
|
|
<div className="activechatchannel__alerts">
|
|
<Alert showAlert={state.showAlert} />
|
|
</div>
|
|
{this.renderChannelMembersList()}
|
|
<div className="activechatchannel__form">
|
|
<Compose
|
|
handleSubmitOnClick={this.handleSubmitOnClick}
|
|
handleKeyDown={this.handleKeyDown}
|
|
handleSubmitOnClickEdit={this.handleSubmitOnClickEdit}
|
|
handleMention={this.handleMention}
|
|
handleKeyUp={this.handleKeyUp}
|
|
handleKeyDownEdit={this.handleKeyDownEdit}
|
|
activeChannelId={state.activeChannelId}
|
|
startEditing={state.startEditing}
|
|
markdownEdited={state.markdownEdited}
|
|
editMessageMarkdown={state.activeEditMessage.markdown}
|
|
handleEditMessageClose={this.handleEditMessageClose}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Content
|
|
onTriggerContent={this.triggerActiveContent}
|
|
resource={state.activeContent[state.activeChannelId]}
|
|
activeChannel={state.activeChannel}
|
|
fullscreen={state.fullscreenContent === 'sidecar'}
|
|
/>
|
|
<VideoContent
|
|
videoPath={state.videoPath}
|
|
onTriggerVideoContent={this.onTriggerVideoContent}
|
|
fullscreen={state.fullscreenContent === 'video'}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
onTriggerVideoContent = (e) => {
|
|
if (e.target.dataset.content === 'exit') {
|
|
this.setState({
|
|
videoPath: null,
|
|
fullscreenContent: null,
|
|
expanded: window.innerWidth > 600,
|
|
});
|
|
} else if (this.state.fullscreenContent === 'video') {
|
|
this.setState({ fullscreenContent: null });
|
|
} else {
|
|
this.setState({
|
|
fullscreenContent: 'video',
|
|
expanded: window.innerWidth > WIDE_WIDTH_LIMIT,
|
|
});
|
|
}
|
|
};
|
|
|
|
handleMention = (e) => {
|
|
const { activeChannel } = this.state;
|
|
const mention = e.keyCode === 64;
|
|
if (mention && activeChannel.channel_type !== 'direct') {
|
|
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.focus();
|
|
this.setState({ showMemberlist: false });
|
|
};
|
|
|
|
listHighlightManager = (keyCode) => {
|
|
const mentionList = document.getElementById('mentionList');
|
|
const activeElement = document.querySelector('.active__message__list');
|
|
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 (
|
|
<div
|
|
className={
|
|
showMemberlist ? 'mention__list mention__visible' : 'mention__list'
|
|
}
|
|
id="mentionList"
|
|
>
|
|
{showMemberlist
|
|
? Object.values(channelUsers[activeChannelId])
|
|
.filter((user) => user.username.match(filterRegx))
|
|
.map((user) => (
|
|
<div
|
|
key={user.username}
|
|
className="mention__user"
|
|
role="button"
|
|
onClick={this.addUserName}
|
|
tabIndex="0"
|
|
data-content={user.username}
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.addUserName();
|
|
}}
|
|
>
|
|
<img
|
|
className="mention__user__image"
|
|
src={user.profile_image}
|
|
alt={user.name}
|
|
style={!user.profile_image ? { display: 'none' } : ' '}
|
|
/>
|
|
<span
|
|
style={{
|
|
padding: '3px 0px',
|
|
'font-size': '16px',
|
|
}}
|
|
>
|
|
{'@'}
|
|
{user.username}
|
|
<p>{user.name}</p>
|
|
</span>
|
|
</div>
|
|
))
|
|
: ' '}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
handleEditMessageClose = () => {
|
|
const textarea = document.getElementById('messageform');
|
|
this.setState({
|
|
startEditing: false,
|
|
markdownEdited: false,
|
|
activeEditMessage: { message: '', markdown: '' },
|
|
});
|
|
textarea.value = '';
|
|
};
|
|
|
|
renderDeleteModal = () => {
|
|
const { showDeleteModal } = this.state;
|
|
return (
|
|
<div
|
|
id="message"
|
|
className={
|
|
showDeleteModal
|
|
? 'message__delete__modal crayons-modal crayons-modal--s absolute'
|
|
: 'message__delete__modal message__delete__modal__hide crayons-modal crayons-modal--s absolute'
|
|
}
|
|
aria-hidden={showDeleteModal}
|
|
role="dialog"
|
|
>
|
|
<div className="crayons-modal__box">
|
|
<div className="crayons-modal__box__body">
|
|
<h3>Are you sure, you want to delete this message?</h3>
|
|
<div className="delete-actions__container">
|
|
<div
|
|
role="button"
|
|
className="crayons-btn crayons-btn--danger message__delete__button"
|
|
onClick={this.handleMessageDelete}
|
|
tabIndex="0"
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.handleMessageDelete();
|
|
}}
|
|
>
|
|
{' '}
|
|
Delete
|
|
</div>
|
|
<div
|
|
role="button"
|
|
className="crayons-btn crayons-btn--secondary message__cancel__button"
|
|
onClick={this.handleCloseDeleteModal}
|
|
tabIndex="0"
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.handleCloseDeleteModal();
|
|
}}
|
|
>
|
|
{' '}
|
|
Cancel
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="crayons-modal__overlay" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
handleCloseDeleteModal = () => {
|
|
this.setState({ showDeleteModal: false, messageDeleteId: null });
|
|
};
|
|
|
|
handleMessageDelete = () => {
|
|
const { messageDeleteId } = this.state;
|
|
deleteMessage(messageDeleteId);
|
|
this.setState({ showDeleteModal: false });
|
|
};
|
|
|
|
handleJoiningRequest = (e) => {
|
|
sendChannelRequest(
|
|
e.target.dataset.channelId,
|
|
this.handleJoiningRequestSuccess,
|
|
this.handleFailure,
|
|
);
|
|
};
|
|
|
|
handleJoiningRequestSuccess = () => {
|
|
const { activeChannelId } = this.state;
|
|
this.setActiveContentState(activeChannelId, null);
|
|
this.setState({ fullscreenContent: null });
|
|
this.toggleSearchShowing();
|
|
};
|
|
|
|
renderChannelBackNav = () => {
|
|
return (
|
|
<button
|
|
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost active-channel__back-btn"
|
|
onClick={this.navigateToChannelsList}
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.navigateToChannelsList(e);
|
|
}}
|
|
tabIndex="0"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
height="24"
|
|
className="crayons-icon"
|
|
>
|
|
<path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z" />
|
|
</svg>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
renderChannelHeaderInner = () => {
|
|
const { activeChannel } = this.state;
|
|
if (activeChannel.channel_type === 'direct') {
|
|
return (
|
|
<a
|
|
href={`/${activeChannel.channel_username}`}
|
|
className="active-channel__title"
|
|
onClick={this.triggerActiveContent}
|
|
data-content="sidecar-user"
|
|
>
|
|
{activeChannel.channel_modified_slug}
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<a
|
|
href="#/"
|
|
onClick={this.triggerActiveContent}
|
|
data-content="chat_channel_setting"
|
|
>
|
|
{activeChannel.channel_name}
|
|
</a>
|
|
);
|
|
};
|
|
|
|
renderChannelConfigImage = () => {
|
|
const { activeContent, activeChannel, activeChannelId } = this.state;
|
|
if (
|
|
activeContent[activeChannelId] &&
|
|
activeContent[activeChannelId].type_of
|
|
) {
|
|
return '';
|
|
}
|
|
|
|
const path =
|
|
activeChannel.channel_type === 'direct'
|
|
? `/${activeChannel.channel_username}`
|
|
: `#`;
|
|
|
|
const dataContent =
|
|
activeChannel.channel_type === 'direct'
|
|
? 'sidecar-user'
|
|
: `chat_channel_setting`;
|
|
|
|
const contentLink =
|
|
activeChannel.channel_type === 'direct'
|
|
? `/${activeChannel.channel_username}`
|
|
: '#/';
|
|
|
|
return (
|
|
<a
|
|
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost"
|
|
onClick={this.triggerActiveContent}
|
|
onKeyUp={(e) => {
|
|
if (e.keyCode === 13) this.triggerActiveContent(e);
|
|
}}
|
|
tabIndex="0"
|
|
href={contentLink}
|
|
data-content={dataContent}
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
height="24"
|
|
className="crayons-icon"
|
|
data-content={dataContent}
|
|
>
|
|
<path
|
|
data-content={dataContent}
|
|
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const { state } = this;
|
|
const detectIOSSafariClass =
|
|
navigator.userAgent.match(/iPhone/i) &&
|
|
!navigator.userAgent.match('CriOS')
|
|
? ' chat--iossafari'
|
|
: '';
|
|
let channelHeader = <div className="active-channel__header"> </div>;
|
|
if (state.activeChannel) {
|
|
channelHeader = (
|
|
<div className="active-channel__header">
|
|
{this.renderChannelBackNav()}
|
|
{this.renderChannelHeaderInner()}
|
|
{this.renderChannelConfigImage()}
|
|
</div>
|
|
);
|
|
}
|
|
let fullscreenMode = '';
|
|
if (state.fullscreenContent === 'sidecar') {
|
|
fullscreenMode = 'chat--content-visible-full';
|
|
} else if (state.fullscreenContent === 'video') {
|
|
fullscreenMode = 'chat--video-visible-full';
|
|
}
|
|
return (
|
|
<div
|
|
data-testid="chat"
|
|
className={`chat chat--expanded
|
|
${detectIOSSafariClass} chat--${
|
|
state.videoPath ? 'video-visible' : 'video-not-visible'
|
|
} chat--${
|
|
state.activeContent[state.activeChannelId]
|
|
? 'content-visible'
|
|
: 'content-not-visible'
|
|
} ${fullscreenMode}`}
|
|
data-no-instant
|
|
aria-expanded={state.expanded}
|
|
>
|
|
{this.renderChatChannels()}
|
|
<div data-testid="active-chat" className="chat__activechat">
|
|
{this.renderActiveChatChannel(channelHeader)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|