| title | Multi-user Chat |
|---|---|
| description | Learn to track users, manage message history, and handle timeouts in a real-time chat application |
This tutorial demonstrates how to keep track of different users within the same session through a simple chat application. The app maintains a list of currently connected users, assigns random nicknames to new users, and includes automatic cleanup features.
<iframe src="https://codepen.io/multisynq/embed/gbbmdem?height=512&theme-id=37190&default-tab=result&editable=true" style={{ width: '100%', height: '512px', border: '2px solid #ccc', borderRadius: '8px', marginBottom: '24px' }} title="Multi-user Chat" allowFullScreen ></iframe>Click or scan the QR code above to launch a new CodePen instance. Typing a message in either window will post the text to the shared chat screen under a randomly assigned nickname. Other people reading this documentation can also join the conversation!
Use `"view-join"` and `"view-exit"` events to track connections Store user information using `viewId` as unique identifier Safely read from model without breaking synchronization Schedule actions with `future()` and `model.now()`The application uses a single Model subclass called ChatModel that handles four main responsibilities:
- User Management: Maps active views to their nicknames
- Message History: Maintains chat conversation history
- Event Handling: Processes chat posts and reset commands
- Cleanup: Automatically clears inactive chats
class ChatModel extends Multisynq.Model {
init() {
this.views = new Map();
this.participants = 0;
this.history = [];
this.inactivity_timeout_ms = 20 * 60 * 1000; // 20 minutes
this.lastPostTime = null;
// System event subscriptions
this.subscribe(this.sessionId, "view-join", this.viewJoin);
this.subscribe(this.sessionId, "view-exit", this.viewExit);
// User input event subscriptions
this.subscribe("input", "newPost", this.newPost);
this.subscribe("input", "reset", this.resetHistory);
}
}viewJoin(viewId) {
const existing = this.views.get(viewId);
if (!existing) {
const nickname = this.randomName();
this.views.set(viewId, nickname);
}
this.participants++;
this.publish("viewInfo", "refresh");
}When a user joins:
- Check if the
viewIdalready exists (for reconnections) - Generate a random nickname if it's a new user
- Increment the participant count
- Notify views to refresh their user information
viewExit(viewId) {
this.participants--;
this.views.delete(viewId);
this.publish("viewInfo", "refresh");
}newPost(post) {
const postingView = post.viewId;
const nickname = this.views.get(postingView);
const chatLine = `<b>${nickname}:</b> ${this.escape(post.text)}`;
this.addToHistory({ viewId: postingView, html: chatLine });
this.lastPostTime = this.now();
this.future(this.inactivity_timeout_ms).resetIfInactive();
}
addToHistory(item) {
this.history.push(item);
if (this.history.length > 100) this.history.shift();
this.publish("history", "refresh");
}The message processing flow:
- Extract the sender's
viewIdfrom the event data - Look up the user's nickname
- Build HTML chat line with nickname and escaped message
- Add to history with size limit (100 messages)
- Schedule inactivity timeout check
resetIfInactive() {
if (this.lastPostTime !== this.now() - this.inactivity_timeout_ms) return;
this.resetHistory("due to inactivity");
}This method verifies that exactly inactivity_timeout_ms milliseconds have passed since the last post. If another post arrived during the timeout period, lastPostTime will be different, and the reset is skipped.
resetHistory(reason) {
this.history = [{ html: `<i>chat reset ${reason}</i>` }];
this.lastPostTime = null;
this.publish("history", "refresh");
}Chat can be reset in three scenarios:
- Inactivity timeout: No posts for 20 minutes
- User command: Someone types
/reset - New user alone: Solo user with no previous messages
randomName() {
const names = ["Acorn", "Banana", "Cherry", /* ... */, "Zucchini"];
return names[Math.floor(Math.random() * names.length)];
}class ChatView extends Multisynq.View {
constructor(model) {
super(model);
this.model = model;
// Set up UI event handlers
sendButton.onclick = () => this.send();
// Subscribe to model updates
this.subscribe("history", "refresh", this.refreshHistory);
this.subscribe("viewInfo", "refresh", this.refreshViewInfo);
// Initialize display
this.refreshHistory();
this.refreshViewInfo();
// Reset chat if alone with no contributions
if (model.participants === 1 &&
!model.history.find(item => item.viewId === this.viewId)) {
this.publish("input", "reset", "for new participants");
}
}
}send() {
const text = textIn.value;
textIn.value = "";
if (text === "/reset") {
this.publish("input", "reset", "at user request");
} else {
this.publish("input", "newPost", {viewId: this.viewId, text});
}
}The send method handles both regular messages and the special /reset command. Note that this.viewId is automatically available in all View classes.
refreshViewInfo() {
nickname.innerHTML = "<b>Nickname:</b> " + this.model.views.get(this.viewId);
viewCount.innerHTML = "<b>Total Views:</b> " + this.model.participants;
}refreshHistory() {
textOut.innerHTML = "<b>Welcome to Multisynq Chat!</b><br><br>" +
this.model.history.map(item => item.html).join("<br>");
textOut.scrollTop = Math.max(10000, textOut.scrollHeight);
}Both methods directly read from the model to update the display. The history display automatically scrolls to show the latest messages.
To prevent accidental model modification, you can use explicit getter/setter methods:
class MyModel extends Multisynq.Model {
init() {
this.data = null;
}
getData() {
return this.data;
}
setData(newData) {
this.modelOnly(); // Throws error if called from view
this.data = newData;
}
}This tutorial demonstrates essential patterns for user management, message handling, and proper Model-View interaction in Multisynq applications. These concepts form the foundation for building more complex collaborative applications.