I'm trying to make a list of connected users using key and value pairs. The key will be the connected user, when I received a message I want to emit this to the receiver and sender (to and from)
Now I'm stuck on the part where I want to add the new connected user. How do I add the user to the user to the array?
Second issue is the foreach is not a function
let connectedUsers: { [key: string]: Socket }
io.on('connection', (socket: Socket) => {
socket.on('USER_CONNECTED', (profile: Profile) => {
connectedUsers !== undefined &&
Object.keys(connectedUsers).includes(profile.userId) &&
Object.assign(connectedUsers, { [profile.userId]: socket })
console.log(connectedUsers)
})
socket.on('MESSAGE_SENT', (message: ChatMessage) => {
connectedUsers.forEach(cli => {
if (cli.key = message.to || cli.key = message.from) {
cli.emit('MESSAGE_RECIEVED', message)
}
});
})
connectedUsers is not an array but an object, so you can't use forEach to iterate it.
The good news is that you don't need it!
When connectedUsers is populated you can refactor on message function like
socket.on('MESSAGE_SENT', (message: ChatMessage) => {
connectedUsers[message.to].emit('MESSAGE_RECIEVED', message);
connectedUsers[message.from].emit('MESSAGE_RECIEVED', message);
})
To add a user to the object connectedUsers simply
socket.on('USER_CONNECTED', (profile: Profile) => {
connectedUsers = Object.assign(connectedUsers, { [profile.userId]: socket })
console.log(connectedUsers)
})
Related
I am building a chat in my app. I am using socket.io for this. When a user sends a message I send an api request to my server. The api stores the message in the database and only then emits, with a socket service to everyone in the room that there is a new message. I have a SocketService class with this method:
private async broadcast({ type, data, chatId, senderId }: { type: string; data: any; chatId: string; senderId: string }) {
const excludedSocket = await this.getUserSocket(senderId);
if (chatId && excludedSocket) {
excludedSocket.emit;
} else if (excludedSocket) {
excludedSocket.emit(type, data);
} else if (room) {
gIo.to(room).emit(type, data);
} else {
gIo.emit(type, data);
}
}
The problem I have is that getUserSocket returns a RemoteSocket object that doesn't have the broadcast or methods on it. So how can I achieve this?
private async getUserSocket(userId: string) {
const sockets = await this.getAllSockets();
const socket = sockets.find((s) => s.data.uid === userId);
return socket;
}
private async getAllSockets() {
const sockets = await this.io.fetchSockets();
return sockets
}
socket.broadcast.emit equates to a BroadcastOperator that sends to an empty Set of rooms (meaning "all rooms"), excluding the socket id "room" of the sender Socket.
The same BroadcastOperator can be created from the server instance with server.except()
const sockets = await this.io.except(socketIdString).emit('blah', x)
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.
Reading this docs https://www.twilio.com/blog/implementing-programmable-chat-php-laravel-vue-js
I try to add in my Laravel 8 / jQuery 3.5.1 / vue 2.6 / Bootstrap 4.5
chat when logged user select another user I run axios request to check if channel was
created priorly(or create a new channel). On client part
connectClientWithUsername(){
this.tc.username = this.loggedUser.name
let vm = this;
axios.post('/admin/team-chat/check_channel', {
sender_id : vm.loggedUser.id,
receiver_id : vm.selectedTeamUser.id
})
.then(({data}) => {
console.log('check_channel data::')
console.log(data)
vm.newChartCreated= data.newChartCreated // Flag if new chat was created
vm.currentChatChannelName= data.currentChatChannelName // Name of requested channel
vm.fetchAccessToken(vm.tc.username, vm.connectMessagingClient);
})
.catch(error => {
console.error(error)
popupAlert('Team Chat', error.response.data.message, 'warn')
vm.is_page_loaded = true
})
},
fetchAccessToken(username, handler) {
let vm = this;
axios.post('/token', {
identity: this.tc.username,
device: 'browser'
})
.then(function (response) {
handler(response.data);
})
.catch(function (error) {
console.log(error);
});
},
And on server part in app/Http/Controllers/Admin/TeamChatController.php:
public function check_channel(Request $request)
{
$requestData = $request->all();
$chatName= "public_oo_team_chat_" . $requestData['sender_id'] . '_' . $requestData['receiver_id'];
$newChartCreated = false;
$chatChannel = null;
try {
$channel = $this->twilio->chat->v2->services(config('app.TWILIO_SERVICE_SID'))
->channels($chatName)
->fetch();
} catch(RestException $e) {
$channel = $this->twilio->chat->v2->services(config('app.TWILIO_SERVICE_SID'))
->channels->create([
'uniqueName' => $chatName,
'friendlyName' => $chatName,
'type' => 'public' // New channel was created
]);
if($channel->sid) {
$chatChannel= new ChatChannel(); // Ref to newly created channel was saved in db
$chatChannel->sender_id= $requestData['sender_id'];
$chatChannel->receiver_id= $requestData['receiver_id'];
$chatChannel->channel_name= $chatName;
$chatChannel->last_chat_at= Carbon::now(config('app.timezone'));
$chatChannel->save();
}
$newChartCreated= true;
}
return response()->json([
'message' => '',
'chatChannel' => $chatChannel,
'newChartCreated' => $newChartCreated, // return name of current Channel
'currentChatChannelName' => $chatName], HTTP_RESPONSE_OK);
} // check_channel
public function getToken(Request $request)
{
$this->identity = $request->identity;
$token = new AccessToken(
$this->twilio_account_sid,
$this->twilio_api_key,
$this->twilio_api_secret,
3600,
$this->identity
);
// Create Chat grant
$chat_grant = new ChatGrant();
$chat_grant->setServiceSid($this->service_sid);
// Add grant to token
$token->addGrant($chat_grant);
// render token to string
echo $token->toJWT();
}
and I when I get token from server I create client and try to conect to channel
connectMessagingClient(token) { // connects the user to the Twilio Chat client.
// Initialize the Chat messaging client
let vm = this;
this.tc.accessManager = new Twilio.AccessManager(token);
new Twilio.Chat.Client.create(token).then(function(client) {
vm.tc.messagingClient = client;
vm.updateConnectedUI();
vm.connectToActiveChannel(client) // I try to connect to channel I need
// vm.tc.messagingClient.on('channelAdded', _.throttle(vm.loadChannelList));
// vm.tc.messagingClient.on('channelRemoved', _.throttle(vm.loadChannelList));
// vm.tc.messagingClient.on('tokenExpired', vm.refreshToken);
});
},
connectToActiveChannel(messagingClient) {
let vm = this
// Get all public channels
messagingClient.getPublicChannelDescriptors().then(function(channels) {
for (let i = 0; i < channels.items.length; i++) {
const channel = channels.items[i];
}
vm.tc.channelArray = channels.items;
vm.tc.channelArray.forEach(vm.addChannel); // Check for selected channel
});
},
addChannel(channel){
console.log('addChannel channel::')
console.log(typeof channel)
if (channel.uniqueName === this.currentChatChannelName) {
this.tc.generalChannel = channel;
console.log('FOUND this.tc.generalChannel!!!')
console.log( this.tc.generalChannel )
return this.joinChannel(channel);
}
},
joinChannel(_channel) { // the member joins the channel (general or a personally created channel)
console.log(" joinChannel _channel");
console.log(_channel);
let vm = this;
return _channel.join()
.then(function(joinedChannel) {
console.log('Joined channel ' + joinedChannel.friendlyName);
vm.updateChannelUI(_channel);
vm.tc.currentChannel = _channel;
vm.loadMessages();
return joinedChannel;
})
.catch(function(err) {
alert("Couldn't join channel " + _channel.friendlyName + ' because ' + err);
});
},
and in joinChannel I got error :
vue.common.dev.js?4650:630 [Vue warn]: Error in render: "TypeError: Converting circular structure to JSON
--> starting at object with constructor 'l'
| property '_fsm' -> object with constructor 'o'
--- property 'context' closes the circle"
on line:
return _channel.join()
I see in the browser's console :
https://prnt.sc/wekctp
and
https://prnt.sc/wekspu
Looks like there is an error in my flow, but it seems to me that I passwed valid object to line :
_channel.join
Why error and how it can be fixed?
Thanks!
Twilio developer evangelist here.
In this code:
connectToActiveChannel(messagingClient) {
let vm = this
// Get all public channels
messagingClient.getPublicChannelDescriptors().then(function(channels) {
for (let i = 0; i < channels.items.length; i++) {
const channel = channels.items[i];
}
vm.tc.channelArray = channels.items;
vm.tc.channelArray.forEach(vm.addChannel); // Check for selected channel
});
}
This line doesn't seem to do anything (it iterates over the list of channel descriptors, creating a new channel const for each of them, but then immediately discarding it.
Then, you set the vm.tc.channelArray to the list of channel descriptors and call vm.addChannel for each of the descriptors.
Note here that getPublicChannelDescriptors() returns a list of ChannelDescriptor objects. ChannelDescriptors cannot be joined and contain a snapshot of data about a channel at the time it was requested.
To join a channel, you would need to first call getChannel() on the channel descriptor first, then perform the rest of your code.
Let me know if that helps.
I want to implement a simple notification system. When user1 likes user2's post, user2 should get a real-time notification from user1.
Here is a part of the client function (Redux action) where someone likes a post:
.then(() => {
const socket = require("socket.io-client")(
"http://localhost:5000"
);
socket.emit("like", user, post);
});
Here is the server socket function where a notification is created after user1 likes user2's post:
io.on("connection", (socket) => {
socket.on("like", async (user, post) => {
if (!post.post.likedBy.includes(user.user._id)) {
const Notification = require("./models/Notification");
let newNotification = new Notification({
notification: `${user.user.username} liked your post!`,
sender: user.user._id,
recipient: post.post.by._id,
read: false,
});
await newNotification.save();
io.emit("notification");
}
});
});
Here is the client function after the notification is created:
socket.on("notification", () => {
console.log("liked");
});
Now the problem with this is the console.log('liked') appears for both user1 and user2. How can I emit to only that user that receives the notification? How can socket.io find this specific user2 that receives the notification from user1?
You should store a list (array or object) of all users like this :
(note that the list has to be updated when a user connects or leaves the socket server)
// an example of structure in order to store the users
const users = [
{
id: 1,
socket: socket
},
// ...
];
And then you can target the post owner and send him a notification like this :
// assuming the the 'post' object contains the id of the owner
const user = users.find(user => user.id == post.user.id);
// (or depending of the storage structure)
// const user = users[post.user.id]
user.socket.emit('notification');
Here an example :
const withObject = {};
const withArray = [];
io.on('connection', socket => {
const user = { socket : socket };
socket.on('data', id => {
// here you do as you want, if you want to store just their socket or another data, in this example I store their id and socket
user.id = id;
withObject[id] = user;
withArray[id] = user;
// or withArray.push(user);
});
socket.on('disconnect', () => {
delete withObject[user.id];
delete withArray[user.id];
// or let index = users.indexOf(user);
// if(index !=== -1) users.splice(index, 1);
});
});
There is plenty way of achieving what i'm trying to explain but the main idea is to link the socket with an index (user id for example) in other to retrieve it later in the code.
I'm trying out some dynamic options for storing environment variables throughout a distributed app. I'm struggling with combining a simple Socket.io with chat page with connection based on a URL from a config.json file. I thought it would be clever on page load to fetch the json file and feed the URL to the socket object. However, I keep running into a chicken and egg issue with the async processing. With what I have right now it starts the connection with the startSocket() function but I would like to have the socket variable available for other functions such as the event listener for the message form. Is there a better way to handle the Socket object get me out of this mess? Obviously new to web dev.
const messageContainer = document.getElementById('message-container')
const messageForm = document.getElementById('send-container')
const messageInput = document.getElementById('message-input')
const name = document.getElementById('chatUserField').getAttribute('data-chatUsername');
fetch('./assets/config.json')
.then(results => results.json())
.then(data => {
startSocket(JSON.stringify(data.SocketIO_URL).replace(/['"]+/g, ''))
})
.catch(err=>console.log(err))
function startSocket(url) {
const socket = io(url)
appendMessage('you joined')
socket.emit('new-user', name)
socket.on('chat-message', data => {
appendMessage(`${data.name}: ${data.message}`)
})
socket.on('user-connected', name => {
appendMessage(`${name} connected`)
})
socket.on('user-disconnected', name => {
appendMessage(`${name} disconnected`)
})
}
messageForm.addEventListener('submit', e => {
e.preventDefault()
const message = messageInput.value
appendMessage(`You: ${message}`)
socket.emit('send-chat-message', message)
messageInput.value = ''
})
function appendMessage(message) {
const messageElement = document.createElement('div')
messageElement.innerText = message
messageContainer.append(messageElement)
}
Error:
ReferenceError: socket is not defined
There's a few things going on here. The reason why you're getting the error, I presume, is not because of the async question you describe—rather it's because in the event listener, you have the following line:
socket.emit('send-chat-message', message)
However, socket isn't defined here within that function. You defined it in the startSocket function but it's locally scoped, meaning that outside of the code in startSocket, you can't access it.
Generally you want to add event listeners and all the stuff that depends on the loaded config in the promise .then handler, since JavaScript will then only run that when your config file is loaded (which is what you want!).
So I would return the socket object from startSocket:
function startSocket(url) {
const socket = io(url)
appendMessage('you joined')
socket.emit('new-user', name)
socket.on('chat-message', data => {
appendMessage(`${data.name}: ${data.message}`)
})
socket.on('user-connected', name => {
appendMessage(`${name} connected`)
})
socket.on('user-disconnected', name => {
appendMessage(`${name} disconnected`)
})
return socket;
}
Then I would assign it to a variable in the .then handler.
fetch('./assets/config.json')
.then(results => results.json())
.then(data => {
// do all processing related to setting stuff up now here
const mySocket = startSocket(JSON.stringify(data.SocketIO_URL).replace(/['"]+/g, ''));
// now that we have the socket object, let's set up the event listeners
messageForm.addEventListener('submit', e => {
e.preventDefault()
const message = messageInput.value
appendMessage(`You: ${message}`)
mySocket.emit('send-chat-message', message)
messageInput.value = ''
})
})