Team Chat Powered by PubNub

Team Chat Powered by PubNub

    ›Tutorial

    Quickstart

    • Introduction
    • Run Locally
    • Advanced Features

    Tutorial

    • Overview
    • Setup Theming
    • Add PubNub Keys
    • Populate Data
    • Login
    • Show User Details
    • Show Conversations
    • Send Messages
    • Join Conversation
    • Leave Conversation
    • Show Members
    • Send Typing Indicators
    • Profanity Filter

    Typing Indicators

    When someone is typing, the app displays a message showing the name of the user who is typing. If more than one person is typing, the message changes to reflect that multiple users are typing.

    typing indicators

    The typingIndicator/TypingIndicatorDisplay/TypingIndicatorDisplay.tsx component adds logic to display typing indicators in the app. The typing indicators are displayed to all users in a conversation as a user is typing a message.

    The component calls the getTypingIndicatorsById selector to get the typing indicator signals for a conversation from the local store. If the store has typing indicator signals for the conversation and the signal was triggered less than 10 seconds ago, “User is typing ...” text is displayed on the screen. If typing indicators are present from multiple users, “Multiple users typing …” text is displayed.

    export interface TypingIndicatorFragment {
      sender: {
        id: string;
        name: string;
      };
      timetoken: string;
      message: TypingIndicator;
    }
    export const getCurrentConversationTypingIndicators = createSelector(
      [getTypingIndicatorsById, getCurrentConversationId, getUsersById, getLoggedInUserId],
      (typingIndicators, conversationId, users, loggedInUserId): TypingIndicatorFragment[] => {
        return typingIndicators[conversationId]
          ? Object.values(
              Object.values(typingIndicators[conversationId] || [])
                .filter(typingIndicator => typingIndicator.channel === conversationId )
                .reduce((grouped: {[key:string]: TypingIndicatorEnvelope}, typingIndicator) => {
                  grouped[typingIndicator.publisher] = typingIndicator;
                  return grouped;
                }, {})
              )
              .filter(typingIndicator => (Date.now() - (typingIndicator.timetoken / 10000)) < (TYPING_INDICATOR_DURATION_SECONDS * 1000))
              .map(
                typingIndicator => {
                  return {
                    ...typingIndicator,
                    timetoken: String(typingIndicator.timetoken),
                    sender:
                      users[typingIndicator.publisher || ''] ||
                      (typingIndicator.publisher
                        ? {
                            id: typingIndicator.publisher,
                            name: typingIndicator.publisher
                          }
                        : {
                            id: "unknown",
                            name: "unknown"
                          })
                  };
                }
              )
          : [];
      }
    );
    /**
     * Display a Message based on its type
     */
    export const TypingIndicatorDisplay = () => {
      const typingIndicators: TypingIndicatorFragment[] = useSelector(
        getCurrentConversationTypingIndicators
      );
      if (typingIndicators.length === 0) {
        return <Wrapper> </Wrapper>;
      } else if (typingIndicators.length === 1) {
        return <Wrapper>{typingIndicators[0].sender.name} is typing ...</Wrapper>
      } else {
        return <Wrapper>Multiple users typing ...</Wrapper>;
      }
    };
    

    Receive a Typing Signal

    The features/typingIndicator/typingIndicatorModel.ts file calls the createSignalReducer() reducer from the Redux framework. This responds to actions dispatched when signals are received by the app. The reducer automatically updates state in the local store when it receives a signal.

    The code then either stores the "typing on" signal in the store, or removes this signal from the store if an actual message was received from the sender. The signal is also removed if the sender stopped typing, which sends a "typing off" signal.

    It also initiates a 10-second timer to expire each typing signal, and dispatch an action that removes the “User is typing ...” text from the screen. The expectation is that the sender of the message will trigger another typing signal if they continue typing once 10 second window elapses. In such a case, the app will continue to show that the user is typing.

    const signalReducer = createSignalReducer<TypingIndicatorEnvelope>();
    const defaultState = { byId: {} };
    const removeTypingIndicator = (state: SignalState<TypingIndicatorEnvelope>, channel: string, userId: string, timetoken?: number): SignalState<TypingIndicatorEnvelope> => {
      let newState = {
        byId: { ...state.byId },
      };
      newState.byId[channel] = newState.byId[channel].filter(
        (signal) => timetoken ? !(signal.publisher === userId && signal.timetoken === timetoken) : !(signal.publisher === userId)
      );
      return newState;
    };
    /**
     * create a reducer which holds all typing indicator signal objects in a normalized form
     */
    export const TypingIndicatorStateReducer = (state: SignalState<TypingIndicatorEnvelope>, action: AppActions): SignalState<TypingIndicatorEnvelope> => {
      switch (action.type) {
        case SignalActionType.SIGNAL_RECEIVED:
          if (action.payload.message.type === TypingIndicatorType.ShowTypingIndicator) {
            // we only want to store the show typing indicator signals
            // the hide signal is handled by the listener below
            return signalReducer(state, action);
          }
          
          return state || defaultState;
        case TypingIndicatorActionType.REMOVE_TYPING_INDICATOR:
          return removeTypingIndicator(state, action.payload.channel, action.payload.userId, action.payload.timetoken);
        case TypingIndicatorActionType.REMOVE_TYPING_INDICATOR_ALL:
          return removeTypingIndicator(state, action.payload.channel, action.payload.userId);
        case MessageActionType.MESSAGE_RECEIVED:
          return removeTypingIndicator(state, action.payload.channel, action.payload.message.senderId);
        default:
          return state || defaultState;
      }
    };
    export const typingIndicatorRemoved = (
      payload: RemoveTypingIndicatorPayload
    ): RemoveTypingIndicatorAction => ({
      type: TypingIndicatorActionType.REMOVE_TYPING_INDICATOR,
      payload,
    });
    export const typingIndicatorRemovedAll = (
      payload: RemoveTypingIndicatorAllPayload
    ): RemoveTypingIndicatorAllAction => ({
      type: TypingIndicatorActionType.REMOVE_TYPING_INDICATOR_ALL,
      payload,
    });
    /**
     * This listener will initiate a timer to dispatch a RemoveTypingIndicatorAction once the 
     * TYPING_INDICATOR_DURATION_SECONDS time is passed
     */
    export const createTypingIndicatorsListener = (
      dispatch: Dispatch<AppActions>
    ): any => ({
      signal: (payload: TypingIndicatorEnvelope) => {
        if (payload.message.type === TypingIndicatorType.ShowTypingIndicator) {
          // hide indicator after display seconds
          setTimeout(() => {
            dispatch(typingIndicatorRemoved({
              userId: payload.publisher,
              channel: payload.channel,
              timetoken: payload.timetoken,
            }));
          }, TYPING_INDICATOR_DURATION_SECONDS * 1000);
        } else if (payload.message.type === TypingIndicatorType.HideTypingIndicator) {
          // hide indicator now, removes all for user regardless of time token
          dispatch(typingIndicatorRemovedAll({
            userId: payload.publisher,
            channel: payload.channel,
          }));
        }
      }
    });
    

    Send a Typing Signal

    The currentConversation/MessageInput/MessageInput.tsx component dispatches actions that trigger signals as a user is typing a message. The code automatically dispatches the "typing on" signal in a conversation when a user starts typing in that conversation. If the user is still typing the message after 10 seconds, another signal is sent to indicate that the user is still typing. If the user stops typing and clears off the message from the screen, a "typing off" signal is triggered to remove the typing indicator from the screen.

    const notifyTyping = () => {
      if (!typingIndicators[conversationId]) {
        typingIndicators[conversationId] = true;
        dispatch(sendTypingIndicator(TypingIndicatorType.ShowTypingIndicator));
        // allow sending additional typing indicators 1 seconds before display duration ends
        setTimeout(() => {
          typingIndicators[conversationId] = false;
        }, (TYPING_INDICATOR_DURATION_SECONDS - 1) * 1000);
      }
    };
    const notifyStopTyping = () => {
      if (typingIndicators[conversationId]) {
        typingIndicators[conversationId] = false;
        dispatch(sendTypingIndicator(TypingIndicatorType.HideTypingIndicator));
      }
    };
    const send = (appMessage: DraftMessage) => {
      dispatch(sendMessage(appMessage));
      dispatch(discardMessageDraft(conversationId));
      typingIndicators[conversationId] = false;
    };
    const update = (appMessage: DraftMessage) => {
      dispatch(updateMessageDraft(conversationId, appMessage));
      if (appMessage.text.length > 0) {
        notifyTyping();
      } else {
        notifyStopTyping();
      }
    };
    

    SendSignal Command

    The typingIndicator/sendTypingIndicator.ts file calls the sendSignal command from the Redux framework to trigger typing on/off signals. These signals are sent to all users in a channel and can be used to display typing indicators in the app.

    /**
     * Send a typing indicator to the current conversation
     *
     * This command does not handle failure and leaves the error to the caller
     */
    export const sendTypingIndicator = (typingIndicatorType: TypingIndicatorType): ThunkAction => {
      return (dispatch, getState) => {
        const state = getState();
        return dispatch(
          sendSignal({
            channel: getCurrentConversationId(state),
            message: { type: typingIndicatorType }
          })
        );
      };
    };
    
    ← Show MembersProfanity Filter →
    • Receive a Typing Signal
    • Send a Typing Signal
    • SendSignal Command
    Docs
    QuickstartTutorialThemeingPubNub Chat
    Source
    GitHubStar
    PubNub
    Copyright © 2020 PubNub