chaskiq/chaskiq

View on GitHub
app/javascript/packages/messenger/src/client_messenger/conversations/conversation.tsx

Summary

Maintainability
F
4 days
Test Coverage
import React from 'react';
import { ThemeProvider } from 'emotion-theming';

import theme from '../textEditor/theme';
import themeDark from '../textEditor/darkTheme';
import DraftRenderer from '../textEditor/draftRenderer';
import DanteContainer from '../textEditor/editorStyles';
import styled from '@emotion/styled';
import Moment from 'react-moment';
import UnicornEditor from '../textEditor';
import { isEmpty } from 'lodash';
import {
  EditorSection,
  CommentsWrapper,
  Footer,
  MessageItem,
  UserAutoChatAvatar,
  ConversationSummaryAvatar,
  MessageSpinner,
  ConversationEventContainer,
  InlineConversationWrapper,
  FooterAckInline,
} from '../styles/styled';

import NewConversationBlock from './newConversationBlock';
import { MessengerContext } from '../context';
import AppPackageBlock from './appPackageBlock';
import MessageItemWrapper from './messageItemWrapper';

const DanteStylesExtend = styled(DanteContainer)`
  .graf--code {
    width: 242px;
    overflow: auto;
  }
`;
export function Conversation(props) {
  const wait_for_input = React.useRef(null);
  const {
    value: {
      kind,
      domain,
      i18n,
      updateHeader,
      conversation,
      setConversation,
      displayAppBlockFrame,
      insertComment,
      pushEvent,
      appData,
      setOverflow,
      setInlineOverflow,
      isMobile,
      inline_conversation,
      agent_typing,
      submitAppUserData,
      updatePackage,
      getPackage,
      visible,
      email,
      isUserAutoMessage,
    },
  } = React.useContext<any>(MessengerContext);

  const { footerClassName } = props;

  React.useEffect(() => {
    updateHeader({
      translateY: 0,
      opacity: 1,
      height: '0',
    });
  }, []);

  // TODO: skip on xhr progress
  function handleConversationScroll(e) {
    if (props.disablePagination) return;

    const element = e.target;
    if (element.scrollTop === 0) {
      // on top
      const meta = conversation.messages.meta;
      if (meta && meta.next_page) {
        setConversation(conversation.key);
      }
    } else {
      updateHeader({
        translateY: 0,
        opacity: 1,
        height: 0,
      });
    }
  }

  function appPackageBlockDisplay(message) {
    displayAppBlockFrame(message);
  }

  function appPackageClickHandler(item, message) {
    // for new_conversation blocks
    if (
      message.message.blocks.type === 'ask_option' &&
      conversation.key === 'volatile'
    ) {
      return insertComment(
        {
          conversationKey: conversation.key,
          message_key: message.key,
          trigger: message.message.triggerId,
          reply: item,
        },
        {
          before: () => {
            console.log('init conversation with ', item);
          },
          sent: () => {
            console.log('sent conversation', item);
          },
        }
      );
    }

    if (message.message.blocks.type === 'app_package') {
      return appPackageBlockDisplay(message);
    }

    pushEvent('trigger_step', {
      conversation_key: conversation.key,
      message_key: message.key,
      trigger: message.triggerId,
      step: item.nextStepUuid || item.next_step_uuid,
      reply: item,
    });
  }

  function appPackageSubmitHandler(data, message) {
    pushEvent('receive_conversation_part', {
      conversation_key: conversation.key,
      message_key: message.key,
      step: message.stepId,
      trigger: message.triggerId,
      ...data,
    });
  }

  function renderTyping() {
    return (
      <MessageItem>
        <div className="message-content-wrapper">
          <MessageSpinner>
            <div className={'bounce1'} />
            <div className={'bounce2'} />
            <div className={'bounce3'} />
          </MessageSpinner>
          <span
            style={{
              fontSize: '0.7rem',
              color: '#afabb3',
            }}
          >
            {i18n.t('messenger.is_typing', {
              name: agent_typing.author.name || 'agent',
            })}
          </span>
        </div>
      </MessageItem>
    );
  }

  function isInboundRepliesClosed() {
    const namespace = kind === 'AppUser' ? 'users' : 'visitors';

    const inboundSettings = appData.inboundSettings[namespace];
    // if this option is not enabled then replies are allowed
    if (!inboundSettings.close_conversations_enabled) return;

    // if this is not a number asume closed
    if (isNaN(inboundSettings.close_conversations_after)) return true;

    // if zero we asume closed
    if (inboundSettings.close_conversations_after === 0) return true;

    const now = new Date();
    const closedAtDate = new Date(conversation.closedAt);
    //@ts-ignore
    const diff = (now - new Date(closedAtDate)) / (1000 * 3600 * 24);

    // if diff is greather than setting assume closed
    if (Math.round(diff) >= inboundSettings.close_conversations_after)
      return true;
  }

  function isInputEnabled() {
    if (conversation.state === 'closed') {
      if (isInboundRepliesClosed()) {
        return false;
      }
    }

    if (isEmpty(conversation.messages)) return true;

    const messages = conversation.messages.collection;
    if (messages.length === 0) return true;

    const message = messages[0].message;
    if (isEmpty(message.blocks)) return true;
    if (message.blocks && message.blocks.type === 'wait_for_reply') return true;

    // strict comparison of false
    if (message.blocks && message.blocks.wait_for_input === false) return true;
    if (message.blocks && message.blocks.waitForInput === false) return true;

    return message.state === 'replied';
  }

  function renderInlineCommentWrapper() {
    return (
      <div
        ref={(comp) => setOverflow(comp)}
        onScroll={handleConversationScroll}
        style={{
          overflowY: 'auto',
          height: '86vh',
          position: 'absolute',
          width: '100%',
          zIndex: 20,
        }}
      >
        <CommentsWrapper
          isReverse={true}
          isInline={inline_conversation}
          ref={(comp) => setInlineOverflow(comp)}
        >
          {renderMessages()}
        </CommentsWrapper>
      </div>
    );
  }

  function renderCommentWrapper() {
    return (
      <CommentsWrapper isReverse={true}>{renderMessages()}</CommentsWrapper>
    );
  }

  function renderMessage(o, _i) {
    const userClass = o.appUser.kind === 'agent' ? 'admin' : 'user';
    const isAgent = o.appUser.kind === 'agent';
    const themeforMessage = o.privateNote || isAgent ? theme : themeDark;

    return (
      <MessageItemWrapper
        visible={visible}
        email={email}
        key={`conversation-${conversation.key}-item-${o.key}`}
        conversation={conversation}
        pushEvent={pushEvent}
        data={o}
      >
        <MessageItem
          className={userClass}
          messageSourceType={o.messageSource ? o.messageSource.type : ''}
          isInline={inline_conversation}
        >
          {!isUserAutoMessage(o) && isAgent && (
            <ConversationSummaryAvatar>
              <img alt={o.appUser.displayName} src={o.appUser.avatarUrl} />
            </ConversationSummaryAvatar>
          )}

          <div className="message-content-wrapper">
            {isUserAutoMessage(o) && (
              <UserAutoChatAvatar>
                <img alt={o.appUser.displayName} src={o.appUser.avatarUrl} />
                <span>{o.appUser.name || '^'}</span>
              </UserAutoChatAvatar>
            )}

            {/* render light theme on user or private note */}

            <ThemeProvider theme={themeforMessage}>
              <DanteStylesExtend>
                <DraftRenderer
                  message={o}
                  domain={domain}
                  raw={JSON.parse(o.message.serializedContent)}
                />

                <span className="status">
                  {o.readAt ? (
                    <Moment fromNow>{o.readAt}</Moment>
                  ) : (
                    <span>{i18n.t('messenger.not_seen')}</span>
                  )}
                </span>
              </DanteStylesExtend>
            </ThemeProvider>
          </div>
        </MessageItem>
      </MessageItemWrapper>
    );
  }

  function renderItemPackage(o, i) {
    return (
      <AppPackageBlock
        key={`package-${o.key}-${i}`}
        message={o}
        conversation={conversation}
        clickHandler={appPackageClickHandler}
        appPackageSubmitHandler={appPackageSubmitHandler}
        i18n={i18n}
        searcheableFields={appData.searcheableFields}
        displayAppBlockFrame={displayAppBlockFrame}
        getPackage={getPackage}
        triggerId={o.triggerId}
        stepId={o.stepId}
        // {...o}
      />
    );
  }

  function renderEventBlock(o, _i) {
    const { data, action } = o.message;
    return (
      <ConversationEventContainer isInline={inline_conversation}>
        <span>{i18n.t(`messenger.conversations.events.${action}`, data)}</span>
      </ConversationEventContainer>
    );
  }

  function renderMessages() {
    return (
      <React.Fragment>
        {agent_typing && renderTyping()}
        {isInputEnabled() &&
          conversation.messages &&
          conversation.messages.collection.length >= 3 && (
            <FooterAckInline>
              <a href="https://chaskiq.io" target="blank">
                <img alt="Chaskiq.io" src={`${domain}/logo-gray.png`} />{' '}
                {i18n.t('messenger.runon')}
              </a>
            </FooterAckInline>
          )}

        {conversation.messages &&
          conversation.messages.collection.map((o, i) => {
            if (o.message.blocks) return renderItemPackage(o, i);
            if (o.message.action) return renderEventBlock(o, i);
            return renderMessage(o, i);
          })}
      </React.Fragment>
    );
  }

  function renderReplyAbove() {
    if (inline_conversation) return null;
    return i18n.t('messenger.reply_above');
  }

  function renderNewConversationButton() {
    return (
      <NewConversationBlock
        styles={{
          bottom: '-8px',
          height: '93px',
          background: '#ffffff',
          boxShadow: '-2px 1px 9px 0px #a0a0a0',
        }}
      >
        <p style={{ margin: '0 0 9px 0px' }}>
          {i18n.t('messenger.closed_conversation')}
        </p>
      </NewConversationBlock>
    );
  }

  function handleBeforeSubmit() {
    const { messages } = conversation;
    if (isEmpty(messages)) return;
    const message = messages.collection[0];
    if (!message) return;
    if (!message.message) return;
    if (
      message.message.blocks &&
      message.message.blocks.type === 'wait_for_reply'
    ) {
      wait_for_input.current = message;
    }
  }

  function handleSent() {
    if (!wait_for_input.current) return;

    const message = wait_for_input.current;

    pushEvent('receive_conversation_part', {
      conversation_key: conversation.key,
      message_key: message.key,
      step: message.stepId,
      trigger: message.triggerId,
      // submit: data
    });

    wait_for_input.current = null;
  }

  function footerReplyIndicator() {
    if (conversation.state === 'closed') return renderNewConversationButton();
    return renderReplyAbove();
  }

  function allowedEditorFeature(feature_type) {
    switch (kind) {
      case 'AppUser':
        return resolveEditorSetting(appData?.userEditorSettings, feature_type);
      default:
        return resolveEditorSetting(appData?.leadEditorSettings, feature_type);
    }
  }

  function resolveEditorSetting(setting, feature_type) {
    return !setting ? true : setting[feature_type];
  }

  function renderFooter() {
    return (
      <Footer
        conversationState={conversation.state}
        isInputEnabled={isInputEnabled()}
        isInboundRepliesClosed={isInboundRepliesClosed()}
        className={footerClassName || ''}
      >
        {!isInputEnabled() ? (
          footerReplyIndicator()
        ) : (
          <UnicornEditor
            i18n={i18n}
            allowsGiphy={allowedEditorFeature('gif')}
            allowsAttachment={allowedEditorFeature('attachments')}
            allowsEmoji={allowedEditorFeature('emojis')}
            beforeSubmit={(data) => handleBeforeSubmit()}
            onSent={(data) => handleSent()}
            domain={domain}
            footerClassName={footerClassName}
            insertComment={insertComment}
          />
        )}
      </Footer>
    );
  }

  function renderInline() {
    return (
      <div>
        <EditorSection isInline={true}>
          {renderInlineCommentWrapper()}
          {renderFooter()}
        </EditorSection>
      </div>
    );
  }

  function renderDefault() {
    return (
      <div
        ref={(comp) => setOverflow(comp)}
        onScroll={handleConversationScroll}
        style={{ overflowY: 'auto', height: '100%' }}
      >
        <EditorSection>
          {renderCommentWrapper()}
          {renderFooter()}
        </EditorSection>
      </div>
    );
  }

  return (
    <main
      aria-labelledby="conversation"
      style={{
        position: 'absolute',
        top: '0',
        bottom: '0',
        left: '0',
        right: '0',
      }}
    >
      {inline_conversation ? renderInline() : renderDefault()}
    </main>
  );
}

export function InlineConversation({ conversation }) {
  return (
    <InlineConversationWrapper>
      hola {conversation.key}
    </InlineConversationWrapper>
  );
}