import './styles/global.css'; import { BoostProvider } from '@8base/boost'; import get from 'lodash/get'; import React from 'react'; import { ApolloProvider } from 'react-apollo'; import invariant, { InvariantError } from 'ts-invariant'; import { createApolloClient } from './apollo'; import css from './chat.module.css'; import { AddToChannelDialog, AlertDialog, ChatWindow, EditChannelDialog, LeaveChannelDialog, Sidebar, } from './components'; import DeleteChannelDialog from './components/dialogs/delete-channel'; import { ChatContext, IChatContext, SetChannel, SetTabs } from './context'; import { DocumentPreview } from './shared/components'; import { CHAT_MESSAGES_LAST, CHAT_TAB_MARGIN_R, CHAT_TAB_WIDTH, } from './shared/constants'; import { ChannelIdentityCommonFragment, ChannelIdentityCommonFragmentDoc, ChannelMessagesDocument, ChannelMessagesQuery, ChannelMessagesQueryVariables, ChannelPreviewCommonFragment, ChannelPreviewCommonFragmentDoc, MutationType, UserChannelsMessagesSubDocument, UserChannelsMessagesSubSubscription, UserChannelsPreviewDocument, UserChannelsPreviewQuery, UserChannelsPreviewQueryVariables, UserDmsPreviewDocument, UserDmsPreviewQuery, UserDmsPreviewQueryVariables, } from './shared/graphql/__generated__'; import * as icons from './shared/icons'; import theme from './shared/theme'; import { isDevEnv } from './shared/utils'; import { IUser, SectionsConfig } from './types'; // -- TYPES export interface IChatProps { currentUser: IUser; uri: string; authToken: string; workspaceId: string; maxTabsCount: number; top: string; isSidebarVisible: boolean; sections: SectionsConfig; onChangeSidebar?: IChatContext['setSidebarVisibility']; usersFilter?: {}; channelMembersFilter?: {}; } export interface IChatState extends Pick {} type SubscriptionMessageNode = NonNullable< NonNullable['node'] >; // -- MAIN class Chat extends React.Component { static defaultProps = { top: '0px', maxTabsCount: 2, isSidebarVisible: true, sections: { channels: true, dm: true, contacts: true, }, }; private client: ReturnType; constructor(props: IChatProps) { super(props); const { currentUser, uri, authToken, workspaceId, usersFilter } = this.props; if (!currentUser || !uri || !authToken) { throw new InvariantError( "You need to specify 'currentUser', 'authToken', 'workspaceId' and 'uri' to .", ); } invariant(typeof uri === 'string', "'uri' must be string."); invariant(typeof authToken === 'string', "'authToken' must be string."); invariant(typeof workspaceId === 'string', "'workspaceId' must be string."); this.state = { openedChannel: null, tabs: [], }; this.client = createApolloClient({ uri, token: authToken, workspaceId }); if (isDevEnv()) { // @ts-ignore window.__CHAT_APOLLO_CLIENT__ = this.client; } } componentDidMount() { this.subscribeToMessages(); } render() { const { tabs, openedChannel } = this.state; const inlineStyles = this.getInlineStyles(); const contextValue = this.getContextValue(); const { onChangeSidebar } = this.props; const closeSidebar = onChangeSidebar && (() => { onChangeSidebar(false); }); return (
{tabs.map((tab, inx) => ( ))} {this.props.isSidebarVisible && ( <> {openedChannel && ( )} )}
); } private getInlineStyles() { return { top: this.props.top, height: `calc(100vh - ${this.props.top})`, }; } private getTabInlineStyles(inx: number) { const nth = inx + 1; const offset = nth * CHAT_TAB_WIDTH + nth * CHAT_TAB_MARGIN_R; return { position: 'absolute', left: `-${offset}px`, } as const; } private getContextValue(): IChatContext { const user = this.props.currentUser; const usersFilter = this.props.usersFilter ? this.props.usersFilter : { id: { not_in: [user ? user.id : ''], }, }; const channelMembersFilter = this.props.channelMembersFilter; return { ...this.state, user: this.props.currentUser, setChannel: this.setChannel, setTabs: this.setTabs, setSidebarVisibility: this.props.onChangeSidebar, isSidebarVisible: this.props.isSidebarVisible, usersFilter, channelMembersFilter, }; } private setChannel: SetChannel = (newChannel, callback) => { this.setState(state => { const newValue = typeof newChannel === 'function' ? newChannel(state.openedChannel) : newChannel; const newChannelIdentity = newValue && newValue.channelIdentityId; if ( state.openedChannel && state.openedChannel.channelIdentityId === newChannelIdentity ) { return null; } return { ...state, openedChannel: newValue, tabs: state.tabs.filter(tab => tab.channelIdentityId !== newChannelIdentity), }; }, callback); }; private setTabs: SetTabs = (newTabs, callback) => { this.setState(state => { const newValue = typeof newTabs === 'function' ? newTabs(state.tabs) : newTabs; const windowChannelIdentityId = state.openedChannel && state.openedChannel.channelIdentityId; const shouldCloseWindow = newValue.some( tab => tab.channelIdentityId === windowChannelIdentityId, ); return { ...state, openedChannel: shouldCloseWindow ? null : state.openedChannel, tabs: newValue.length > this.props.maxTabsCount ? newValue.slice(0, this.props.maxTabsCount) : newValue, }; }, callback); }; private subscribeToMessages() { if (!this.props.currentUser) { return; } const userId = this.props.currentUser.id; this.client .subscribe({ query: UserChannelsMessagesSubDocument, variables: { userId, }, fetchPolicy: 'no-cache', }) .subscribe({ next: ({ data }: { data: UserChannelsMessagesSubSubscription }) => { const response = data && data.Message && data.Message; if (!response || !response.node) { return; } this.updateChannelMessages(response.node, response.mutation); this.checkAndMarkUnread(response.node); }, }); } private updateChannelMessages( node: SubscriptionMessageNode, mutationType?: MutationType, ) { if (!node.channel) { return; } let cachedChannel: ChannelMessagesQuery | undefined | null; try { cachedChannel = this.client.cache.readQuery< ChannelMessagesQuery, ChannelMessagesQueryVariables >({ query: ChannelMessagesDocument, variables: { id: node.channel.id as string, last: CHAT_MESSAGES_LAST, }, }); // tslint:disable-next-line: no-empty } catch (e) {} const { channel, ...message } = node; if (cachedChannel) { const oldItems = cachedChannel.channel!.messages!.items; let items = oldItems; let count = cachedChannel.channel!.messages!.count; switch (mutationType) { case 'create': { items = [...oldItems, message]; count += 1; break; } case 'update': { if (message.isDeleted) { items = oldItems.filter(el => el.id !== message.id); count -= 1; } else { items = oldItems.map(el => (el.id === message.id ? message : el)); } break; } } this.client.cache.writeQuery({ query: ChannelMessagesDocument, variables: { id: channel.id as string, last: CHAT_MESSAGES_LAST, }, data: { channel: { __typename: cachedChannel.channel!.__typename, id: channel.id, messages: { __typename: cachedChannel.channel!.messages!.__typename, count, items, }, }, }, }); this.client.query({ query: ChannelMessagesDocument, variables: { id: channel.id as string, last: CHAT_MESSAGES_LAST }, }); return; } const { channelMembersFilter } = this.getContextValue(); this.client.query({ query: channel.type === 'channel' ? UserChannelsPreviewDocument : UserDmsPreviewDocument, variables: { channelMembersFilter }, fetchPolicy: 'network-only', }); } private checkAndMarkUnread(node: SubscriptionMessageNode) { if (!node.channel) { return; } const userId = this.props.currentUser && this.props.currentUser.id; const { channel, ...message } = node; if (!message.createdBy || userId === message.createdBy.id) { return; } let cachedChannelPreview: ChannelPreviewCommonFragment | undefined | null; try { cachedChannelPreview = this.client.cache.readFragment( { fragment: ChannelPreviewCommonFragmentDoc, fragmentName: 'ChannelPreviewCommon', id: `Channel:${channel.id}`, }, ); // tslint:disable-next-line: no-empty } catch (e) {} const lastChannelMsg = get(cachedChannelPreview, 'messages.items[0]'); const isReceivedNewMessage = !lastChannelMsg || new Date(lastChannelMsg.createdAt).getTime() <= new Date(message.createdAt).getTime(); if (!message.createdAt || !isReceivedNewMessage || !cachedChannelPreview) { return; } this.client.cache.writeFragment({ fragment: ChannelPreviewCommonFragmentDoc, fragmentName: 'ChannelPreviewCommon', id: `Channel:${channel.id}`, data: { __typename: 'Channel', ...cachedChannelPreview, messages: { __typename: 'MessageListResponse', items: [ { __typename: 'Message', createdAt: message.createdAt, createdBy: message.createdBy, }, ], }, }, }); const { channelMembersFilter } = this.props; const cachedUserChannelsPreview = this.client.cache.readQuery< UserChannelsPreviewQuery, UserChannelsPreviewQueryVariables >({ query: UserChannelsPreviewDocument, variables: { channelMembersFilter }, }); const cachedUserDmsPreview = this.client.cache.readQuery< UserDmsPreviewQuery, UserDmsPreviewQueryVariables >({ query: UserDmsPreviewDocument, variables: { channelMembersFilter }, }); const identitiesPath = 'user.channelIdentities.items'; const channelIdentities = get(cachedUserChannelsPreview, identitiesPath, []); const dmIdentities = get(cachedUserDmsPreview, identitiesPath, []); const identities = [...channelIdentities, ...dmIdentities]; const channelIdentity = identities.find((el: any) => el.channel.id === channel.id); if (!channelIdentity) { return; } this.client.cache.writeFragment({ fragment: ChannelIdentityCommonFragmentDoc, fragmentName: 'ChannelIdentityCommon', id: `ChannelMember:${channelIdentity.id}`, data: { ...channelIdentity, hasUnreads: true, }, }); } } export default Chat;