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.