pusher-websocket-react-native Freezes UI During Message Regeneration

When regenerating messages using pusher-websocket-react-native, I am unable to interact with anything and stop the message regeneration.
When I manage to stop it, the next message regenerates as two messages in one.
Is there any equivalent solution considering that a similar library is used on the backend (Python) while the frontend is developed in React Native with Expo?

*The backend directly fetches the response from OpenAI as a stream.
While the option to rewrite both the backend and frontend exists, it is a last resort.

*

But sometimes, it’s possible to press the stop button, and everything stops, although this is rare, and occasionally, the next message contains two messages in one.

import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
import {
  Animated,
  FlatList,
  ImageBackground,
  Keyboard,
  KeyboardAvoidingView,
  ListRenderItem,
  NativeSyntheticEvent,
  Platform,
  SafeAreaView,
  Text,
  TextInput,
  TextInputKeyPressEventData,
  TouchableOpacity,
  View,
} from "react-native";
import { PayloadAction } from "@reduxjs/toolkit";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { Pusher, PusherEvent } from "@pusher/pusher-websocket-react-native";
import Tts from "react-native-tts";

// types
import { ChatInfoType, ChatMessageType } from "../../store/store.types";
import { RootStackCatalogParamList } from "../../../Navigation/Navigation.types";

// hooks
import {
  useAppDispatch,
  useAppNavigation,
  useAppSelector,
} from "../../store/hooks";

// redux
import {
  addTempName,
  addUserMessage,
  addWordToMessage,
  createChat,
  deleteChat,
  getFavoritesChat,
  sendMessage,
  setMessage,
} from "../../store/slices/chatSlice";
import { fetchChatHistoryWithUID } from "../../API/chat/chat";

// components
import { ChatMessage } from "../../components/ChatMessages/ChatMessages";
import { StyledButton } from "../../components/StyledButtons/StyledButton";
import { ModalBottomSheet } from "../../components/ModalBottomSheet/ModalBottomSheet";
import CenterOfIdeas from "../../components/CenterOfIdeas";
import { SoundBar } from "./components/SoundBar/SoundBar";

// icons
import ReturnButton from "../../../assets/icons/chatScreen/back.svg";
import MuteButton from "../../../assets/icons/chatScreen/soundMute.svg";
import StarButton from "../../../assets/icons/chatScreen/starButton.svg";
import StarEmptyButton from "../../../assets/icons/chatScreen/starEmptyButton.svg";
import ChatLogo from "../../../assets/icons/avatars/avatar.svg";
import IconButton from "../../../assets/icons/chatScreen/button.svg";
import IconBulb from "../../../assets/icons/chatScreen/icon_bulb.svg";
import IconSendMessage from "../../../assets/icons/chatScreen/icon_send_message.svg";
import IconAdd from "../../../assets/icons/landingScreen/add.svg";

// styles
import { styles } from "./Chat.styles";
import { NameChat } from "./components/NameChat/NameChat";

interface IChatScreenProps
  extends NativeStackScreenProps<RootStackCatalogParamList, "Chat"> {}

export const ChatScreen: FunctionComponent<IChatScreenProps> = ({ route }) => {
  const { uuid_chat, title_idea, isIdea } = route.params;
  const dispatch = useAppDispatch();
  const navigation = useAppNavigation();
  const chatData = useAppSelector((state) => state.chat.messages);
  const reversedChatData = [...chatData].reverse();
  const listOfChats = useAppSelector((state) => state.chat.listOfChats);
  const [selectChat, setSelectChat] = useState<ChatInfoType | null>(null);
  const [isShowOnlyFavorites, setIsShowOnlyFavorites] = useState(false);
  const [showKeyboard, setShowKeyboard] = useState(false);
  const [isShowIdeas, setIsShowIdeas] = useState(false);
  // TODO Later deleting or added logics
  const [_updating, setUpdating] = useState<boolean>(false);
  const [_isLoading, setIsLoading] = useState(false);
  const [inputText, setInputText] = useState("");
  const uuidChat = uuid_chat || selectChat?.uuid_chat || "";
  const [isPusher, setIsPusher] = useState(false);
  const pusher = useMemo(() => {
    return Pusher.getInstance();
  }, []);
  //SoundBar
  const [textVoice, setTextVoice] = useState("");
  const [tempTextVoice, setTempTextVoice] = useState("");
  const [isShowBar, setIsShowBar] = useState(false);
  const [isPlay, setIsPlay] = useState(false);
  const [isPause, setIsPause] = useState(false);
  const [isSpeed, setIsSpeed] = useState(false);
  Tts.setDefaultRate(isSpeed ? 0.6 : 0.5);
  Tts.setDefaultLanguage("ru");

  const setSpeedBar = () => {
    setIsSpeed((state) => !state);
  };

  const onCloseBar = async () => {
    await Tts.stop();
    setIsPlay(false);
    setIsPause(false);
    setIsShowBar(false);
    setTextVoice("");
    setTempTextVoice("");
  };

  const onPause = async () => {
    setIsPause((state) => !state);
    setTextVoice(tempTextVoice);
  };

  const onClear = async () => {
    await pusher.unsubscribe({ channelName: uuidChat });
    dispatch(setMessage([]));
    setIsShowIdeas(false);
    setSelectChat(null);
    if (reversedChatData.length <= 1 && uuid_chat)
      await dispatch(deleteChat(route.params.uuid_chat || ""));
  };

  const onBackPress = async () => {
    await onClear();
    navigation.goBack();
  };

  const handleKeyPress = (
    event: NativeSyntheticEvent<TextInputKeyPressEventData>
  ) => {
    if (event.nativeEvent.key === "Enter") {
      event.preventDefault();
    }
  };

  const onPressSendMessage = async () => {
    if (inputText.length === 0) return;

    //setIsPusher(true);
    if (uuidChat) {
      await pusher.subscribe({
        channelName: uuidChat,
        onEvent: async (event) => await handlePusherEvent(event),
      });
      dispatch(
        sendMessage({
          chatId: uuidChat,
          message: inputText,
        })
      );
    } else {
      const { payload } = (await dispatch(
        createChat()
      )) as PayloadAction<ChatInfoType>;
      setSelectChat(payload);
      dispatch(
        sendMessage({
          chatId: payload.uuid_chat,
          message: inputText,
        })
      );
    }

    setInputText("");
    Keyboard.dismiss();
  };

  const createNewChatIdea = async () => {
    await onClear();
    navigation.navigate("Chat", {
      isIdea: true,
    });
  };

  const renderItemChat: ListRenderItem<ChatMessageType> = ({ item }) => {
    return (
      <ChatMessage
        key={Math.random() * 1000}
        text={item.content.toString()}
        isReply={item.role === "assistant"}
        message_id={item.message_id}
        uuid_chat={uuidChat}
        isShowOnlyFavorites={isShowOnlyFavorites}
        // SoundBar
        isPlay={isPlay}
        setIsPlay={(value) => {
          setTextVoice(item.content.toString());
          setIsPlay(value);
        }}
      />
    );
  };

  const onSelectIdea = async () => {
    const { payload } = (await dispatch(
      createChat()
    )) as PayloadAction<ChatInfoType>;
    setSelectChat(payload);
    await dispatch(
      sendMessage({
        chatId: payload.uuid_chat,
        message: title_idea || "",
      })
    );
  };

  const addWordToMessageTest = async (
    parsed: any,
    create_chat: boolean,
    finish_reason: boolean
  ) => {
    setIsPusher(true);
    if (finish_reason) {
      await setIsPusher(false);
    }
    if (!create_chat && !finish_reason)
      await dispatch(addWordToMessage(parsed?.message?.toString()));
  };

  const handlePusherEvent = async (event: PusherEvent) => {
    const parsed = JSON.parse(event.data);
    await setIsPusher(true);
    if (parsed?.new_chat_name && selectChat)
      dispatch(
        addTempName({ id: selectChat.id, chat_name: parsed.new_chat_name })
      );
    await addWordToMessageTest(
      parsed,
      parsed?.create_chat,
      parsed?.finish_reason
    );
  };

  const setupPusher = async () => {
    try {
      if (pusher.connectionState === "DISCONNECTED") {
        await pusher.init({
          apiKey: "e03b3bc72e86741ea3c9",
          cluster: "eu",
        });
        await pusher.connect();
      }

      if (pusher.channels) {
        for (const channelName in pusher.channels) {
          await pusher.unsubscribe({ channelName });
        }
      }

      await pusher.subscribe({
        channelName: uuidChat,
        onEvent: async (event) => await handlePusherEvent(event),
      });
    } catch (error) {
      console.error("Error setting up Pusher:", error);
    }
  };

  const getData = async () => {
    try {
      const response = await dispatch(fetchChatHistoryWithUID(uuidChat));
      if (response.payload?.results.length)
        dispatch(setMessage(response.payload.results));
      await setupPusher();
    } catch (error) {
      console.error("Error fetching chat history:", error);
    }
  };

  const stopRegeneration = async () => {
    await pusher.unsubscribe({
      channelName: uuidChat,
    });
    setIsPusher(false);
  };

  useEffect(() => {
    if (isIdea && reversedChatData.length === 0) {
      dispatch(
        addUserMessage({
          content: "Чем могу помочь",
          role: "assistant",
          message_id: "",
        })
      );
    }
    if (title_idea && reversedChatData.length === 0) onSelectIdea();
    if (uuid_chat && reversedChatData.length === 0) {
      const findChat =
        listOfChats.find((chat) => chat.uuid_chat === uuid_chat) || null;
      if (findChat) setSelectChat(findChat);
      else navigation.navigate("Landing");
    }
  }, [isIdea]);

  useEffect(() => {
    if (selectChat !== null)
      (async () => {
        await dispatch(getFavoritesChat(uuidChat));
        await getData();
      })();
  }, [selectChat]);

  // SoundBar
  Tts.addEventListener("tts-progress", (event) =>
    setTempTextVoice(() => {
      const otherText = textVoice.slice(event.location, textVoice.length);
      return otherText;
    })
  );

  useEffect(() => {
    Tts.addEventListener("tts-start", (_event) => {
      setIsShowBar(true);
    });
    Tts.addEventListener("tts-finish", (_event) => {});
  }, []);

  useEffect(() => {
    (async () => {
      if (isPlay && textVoice.length) {
        await Tts.stop();
        if (textVoice != null) Tts.speak(textVoice);
      } else await onCloseBar();
    })();
  }, [isPlay]);

  useEffect(() => {
    if (isPlay)
      (async () => {
        if (isPause) {
          await Tts.stop();
        } else if (!isPause && textVoice.length) {
          if (textVoice != null) {
            Tts.speak(textVoice);
          }
        }
      })();
  }, [isPause]);

  return (
    <ImageBackground
      source={require("../../../assets/images/backgrounds/chatBackground.jpg")}
      style={styles.background}
    >
      <KeyboardAvoidingView
        style={[
          styles.keyboardAvoidingView,
          {
            marginBottom: showKeyboard && Platform.OS === "ios" ? 20 : 0,
          },
        ]}
        behavior={Platform.OS === "ios" ? "padding" : undefined}
      >
        <SafeAreaView style={{ flex: 1 }}>
          <View style={[styles.containerCol]}>
            <View style={[styles.containerRow]}>
              <View
                style={{
                  flexDirection: "row",
                  alignItems: "center",
                  gap: 12,
                }}
              >
                <StyledButton onPress={onBackPress}>
                  <ReturnButton />
                </StyledButton>
                <NameChat selectChat={selectChat} />
              </View>
              <View style={[styles.headerManipulation]}>
                <StyledButton>
                  <MuteButton />
                </StyledButton>
                <StyledButton
                  onPress={() => setIsShowOnlyFavorites((state) => !state)}
                >
                  {isShowOnlyFavorites ? (
                    <StarButton fill={"gold"} />
                  ) : (
                    <StarEmptyButton />
                  )}
                </StyledButton>
                <ChatLogo width={40} height={40} />
              </View>
            </View>
            <SoundBar
              isSpeed={isSpeed}
              setSpeed={setSpeedBar}
              onClose={onCloseBar}
              onPause={onPause}
              isShow={isShowBar}
              isPause={isPause}
            />
          </View>
          <FlatList
            inverted={true}
            onStartReached={() => {
              setUpdating(true);
            }}
            showsVerticalScrollIndicator={false}
            style={{
              marginHorizontal: 16,
              marginTop: 16,
              display: "flex",
              flexDirection: "column",
            }}
            data={reversedChatData}
            renderItem={renderItemChat}
          />
          <Animated.View
            style={{
              flexDirection: "row",
              gap: 12,
              marginHorizontal: 16,
            }}
          >
            {inputText.length > 0 && (
              <TouchableOpacity
                onPress={() => {
                  setInputText("");
                  Keyboard.dismiss();
                }}
                style={{
                  borderRadius: 8,
                  alignSelf: "flex-end",
                }}
              >
                <IconButton />
              </TouchableOpacity>
            )}

            <View
              style={{
                flex: 1,
                borderRadius: 6,
                backgroundColor: "white",
                paddingHorizontal: 16,
                justifyContent: "space-between",
                flexDirection: "row",
                alignItems: "center",
              }}
            >
              <TextInput
                multiline
                blurOnSubmit
                onFocus={() => setShowKeyboard(true)}
                onBlur={() => setShowKeyboard(false)}
                style={styles.textInput}
                placeholderTextColor={"rgba(120, 166, 185, 1)"}
                onChangeText={(text) => {
                  const sanitizedText = text.replace(/[\r\n]/g, "");
                  setInputText(sanitizedText);
                }}
                onKeyPress={handleKeyPress}
                value={inputText}
                placeholder={"Спросите что-нибудь"}
              />
              {inputText.length === 0 && (
                <TouchableOpacity onPress={() => setIsShowIdeas(true)}>
                  <IconBulb />
                </TouchableOpacity>
              )}
            </View>
            {isPusher ? (
              <TouchableOpacity
                style={{
                  backgroundColor: "#0A7AB2",
                  borderRadius: 8,
                  paddingHorizontal: 16,
                  justifyContent: "center",
                }}
                onPress={stopRegeneration}
              >
                <View style={styles.stopIcon} />
              </TouchableOpacity>
            ) : (
              <TouchableOpacity
                style={{
                  backgroundColor: "#0A7AB2",
                  borderRadius: 8,
                  justifyContent: "center",
                }}
                onPress={onPressSendMessage}
              >
                <IconSendMessage />
              </TouchableOpacity>
            )}
          </Animated.View>
        </SafeAreaView>

        <ModalBottomSheet
          isOpen={isShowIdeas}
          onClose={() => setIsShowIdeas(false)}
          title="Центр идей"
          childrenTitle={
            <TouchableOpacity
              style={{
                flexDirection: "row",
                alignItems: "center",
                backgroundColor: "rgba(8, 103, 152, 0.30)",
                padding: 8,
                borderRadius: 6,
                gap: 6,
              }}
              onPress={createNewChatIdea}
            >
              <IconAdd />
              <Text style={styles.titleNewChatButton}>Новый чат</Text>
            </TouchableOpacity>
          }
          styleTitle={{ fontSize: 24 }}
          footer={
            <TouchableOpacity
              style={{
                alignItems: "center",
                backgroundColor: "#0A7AB2",
                paddingVertical: 16,
                borderRadius: 12,
              }}
            >
              <Text style={styles.titleNewChatButton}>Купить пакет</Text>
            </TouchableOpacity>
          }
          snapPoints={["90%"]}
          children={
            <CenterOfIdeas
              setLoading={setIsLoading}
              setToggle={setIsShowIdeas}
              chatId={uuidChat}
            />
          }
        />
      </KeyboardAvoidingView>
    </ImageBackground>
  );
};

I’ve tried implementing this logic within the provider, as well as in Redux Toolkit. It works, but it locks up the interface and behaves improperly.

Leave a Comment