I am currently trying to migrate a mobile react-native application (working with expo) from twilio-chat to twilio-conversations. Already did it with the browser based agent-application which works fine.
The react-native app is able to send messages, which appear in the browser based app, but not in the message list of the mobile app itself. Also my "messageAdded" listener never gets called.
Here is the hook I wrote to handle the interaction with the conversation client (version 2.1.0):
// useTwilioConversation.tsx
import { Client, Conversation, Message } from '@twilio/conversations';
import { useEffect, useState } from 'react';
import { ConversationType, Session } from '../interfaces/Session';
import useConversationToken from './useConversationToken';
import { useSocket } from './useSocket';
interface ChatOptions {
session: Session;
}
const useTwilioConversation = (options: ChatOptions) => {
const [chatReady, setChatReady] = useState(false);
const [chatConversation, setChatConversation] = useState<Conversation>();
const [messages, setMessages] = useState<Message[]>([]);
const [newMessageAvailable, setNewMessageAvailable] = useState<boolean>(
false
);
useConversationToken({
session: options.session,
type: ConversationType.CHAT,
callback: async (data) => {
await createClient(data.token);
},
});
useEffect(() => {
return () => {
if (chatConversation) {
chatConversation.removeAllListeners();
chatConversation.leave();
}
};
}, []);
// socket context for handling other application communication
const socket = useSocket();
const onMessageAdded = (message: Message) => {
// never gets called
console.log(
'-------------------------------------------------------------->NEW MESSAGE: ',
message
);
setMessages((prevMessages) => {
return [...prevMessages, message];
});
setNewMessageAvailable(true);
};
const registerListeners = (chatConversation: Conversation) => {
chatConversation.on('messageAdded', onMessageAdded);
console.log(chatConversation.listenerCount('messageAdded')); // returns '2'
};
const onChatConnected = () => {
socket.emit('deviceConnect', { sessionId: options.session.id });
};
const createClient = async (token: string) => {
const client = await new Client(token, { logLevel: 'debug' });
// works fine
client.on('stateChanged', (state) => {
if (state === 'initialized') {
onInit(client);
}
});
client.on('messageAdded', onMessageAdded);
};
const onInit = async (client: Client) => {
const conversation = await client.getConversationByUniqueName(
options.session.room
);
if (conversation) {
if (conversation.status !== 'joined') {
client.on('conversationJoined', () => {
registerListeners(conversation);
});
await conversation.join();
} else {
registerListeners(conversation);
}
if (options.session.description !== '')
await conversation.sendMessage(options.session.description);
const messages = await conversation.getMessages();
setMessages(messages.items);
onChatConnected();
setChatConversation(conversation);
setChatReady(true);
}
};
const submitMessage = (message: string) => {
chatConversation?.sendMessage(message);
};
const readMessages = () => {
setNewMessageAvailable(false);
};
return {
messages,
submitMessage,
chatReady,
newMessageAvailable,
readMessages,
} as const;
};
export default useTwilioConversation;
EDIT: Added UseConversationToken.tsx for clarity
// useConversationToken.tsx
import { useEffect, useState } from 'react';
import { ConversationType, Session } from '../interfaces/Session';
import { useSocket } from './useSocket';
interface ConversationOptions {
type: ConversationType;
session: Session;
callback?: (data: ConversationReadyData) => void;
timeout?: number;
}
interface ConversationReadyData {
sessionId: string;
type: ConversationType;
token: string;
}
const useConversationToken = (options: ConversationOptions) => {
const [token, setToken] = useState<string | undefined>(undefined);
const socket = useSocket();
useEffect(() => {
const message = `${options.type}ConversationReady`;
if (socket) {
socket.on(message, (data: ConversationReadyData) => {
if (data.sessionId === options.session.id) {
if (options.timeout && options.timeout > 0) {
setTimeout(() => {
setToken(data.token);
if (options.callback) options.callback(data);
}, options.timeout);
} else {
setToken(data.token);
if (options.callback) options.callback(data);
}
}
});
}
return () => {
if (socket) socket.off(message);
};
}, [socket]);
return token;
};
export default useConversationToken;
I also tried this code directly in place instead of using it as a hook which makes no difference.
After sending a few messages to the mobile client, I receive an error, that indicates that there was a problem sending log messages to the devlopment environment. "RangeError: Invalid string length"
Twilio debug logs can be found here: Logs
Updated useTwilioConversation
hook code:
import { Client, Conversation, Message } from '@twilio/conversations';
import { useEffect, useState } from 'react';
import { ConversationType, Session } from '../interfaces/Session';
import useConversationToken from './useConversationToken';
import { useSocket } from './useSocket';
interface ChatOptions {
session: Session;
}
const useTwilioConversation = (options: ChatOptions) => {
const [client, setClient] = useState<Client>();
const [chatReady, setChatReady] = useState(false);
const [conversation, setConversation] = useState<Conversation>();
const [messages, setMessages] = useState<Message[]>([]);
const [newMessageAvailable, setNewMessageAvailable] = useState<boolean>(
false
);
const token = useConversationToken({
session: options.session,
type: ConversationType.CHAT,
});
useEffect(() => {
(async () => {
if (token) {
const client = new Client(token, { logLevel: 'debug' });
setClient(client);
client.on('stateChanged', async (state) => {
if (state === 'initialized') {
const conversation = await client.getConversationByUniqueName(
options.session.room
);
setConversation(conversation);
}
});
}
})();
return () => {
if (client) {
client.removeAllListeners();
client.shutdown();
}
};
}, [token]);
useEffect(() => {
(async () => {
if (client && conversation) {
const registerListeners = () => {
conversation.on('messageAdded', onMessageAdded);
// DOES NOT GET CALLED TOO!
conversation.on('participantLeft', () => {
console.log('PARTICIPANT LEFT')
);
});
};
if (conversation.status !== 'joined') {
client.on('conversationJoined', () => {
registerListeners();
});
await conversation.join();
} else {
registerListeners();
}
if (options.session.description !== '')
await conversation.sendMessage(options.session.description);
const messages = await conversation.getMessages();
setMessages(messages.items);
onChatConnected();
setChatReady(true);
}
})();
return () => {
if (conversation) {
conversation.removeAllListeners();
conversation.leave();
}
};
}, [conversation]);
// socket context for handling other application communication
const socket = useSocket();
const onMessageAdded = (message: Message) => {
// never gets called
console.log(
'-------------------------------------------------------------->NEW MESSAGE: ',
message
);
setMessages((prevMessages) => {
return [...prevMessages, message];
});
setNewMessageAvailable(true);
};
const onChatConnected = () => {
socket.emit('deviceConnect', { sessionId: options.session.id });
};
const submitMessage = (message: string) => {
conversation?.sendMessage(message);
};
const readMessages = () => {
setNewMessageAvailable(false);
};
return {
messages,
submitMessage,
chatReady,
newMessageAvailable,
readMessages,
} as const;
};
export default useTwilioConversation;