TKur
TKur

Reputation: 29

Chat type indicator doesn’t appear for all clients

I'm working on a writing indicator, which shows up next to the name on the online liste, whenever the user writes something inside the textarea. Problem is, it only shows up for the person that writes but not for other tabs/clients.

Here is my code:

Online list template: imports/ui/components/chat/chat_onlinelist.html

<template name="onlineliste">
  <div  id="online-liste" class="onlineList">
    {{#each characters}}
  <div class="characterBasicInteraction" oncontextmenu="return false;">
    <span class="typeIndicator" id="typeInd_{{name}}">✎</span>
 <!-- TypeIndicator shows up here, code gets id with this.name 
and uses it to change jquery fadeIn/fadeOut --> 
    <a href="/c/{{name}}" target="_blank" class="{{name}}" {{checkIfFriend}}><li>{{name}}</li></a>
    <div id="panel_{{name}}" class="panelChat">
      <span><b>{{name}}</b></span>      
      <span id="whisp">Flüstern</span>      
      {{{checkIfFriendButton}}}
      <span>Bookmark</span>
      <span>Ignore</span>
      <span>Report</span>  
    </div>
  </div>
     {{/each}}
</div>
</template>

Now I tried 3 approaches so far, all had the same outcome as mentioned above.

First Approach, Event keydown textarea in imports/ui/components/chat/chat.js

 'keydown textarea'(event, template) {
      var character = Session.get("activeChar");
      var getIndicator = "#typeInd_"+character;

        //setup before functions
      var typingTimer;                //timer identifier
      var doneTypingInterval = 5000;  //time in ms (5 seconds)

        //on keyup, start the countdown
        template.$('textarea').keyup(function(){
            clearTimeout(typingTimer);
            if (template.$('textarea').val()) {
                typingTimer = setTimeout(doneTyping, doneTypingInterval);
            }
        });

        //user is "finished typing,"
        function doneTyping () {
          template.$(getIndicator).fadeOut(); 
        }

      template.$(getIndicator).fadeIn();
      template.$(".input-box_text").focusout(function() {
      template.$(getIndicator).fadeOut(); 
      })

    }

Approach 2: Writing the function in imports/api/chat/chat.js to have it on the server (?) and load it inside imports/ui/components/chat/chat.js

 typeIndicator = function typeIndicator (character, getIndicator) {
  var typingTimer;                //timer identifier
  var doneTypingInterval = 5000;  //time in ms (5 seconds)

  //on keyup, start the countdown
  $('textarea').keyup(function(){
      clearTimeout(typingTimer);
      if ($('textarea').val()) {
          typingTimer = setTimeout(doneTyping, doneTypingInterval);
      }
  });

  //user is "finished typing," do something
  function doneTyping () {
   $(getIndicator).fadeOut(); 
  }

//$(getIndicator).fadeIn();
$(getIndicator).fadeIn();

$(".input-box_text").focusout(function() {
$(getIndicator).fadeOut(); 
});

};

    'keydown textarea'(event, template) {
      var character = Session.get("activeChar");
      var getIndicator = "#typeInd_"+character;

     TypeIndicator(character, getIndicator); 
}

Approach 3: Basically the same as 2 but this time I didn't use the blaze-template.events helper

document.addEventListener("keydown", event => {
  var character = Session.get("activeChar");
  var getIndicator = "#typeInd_"+character;
  typeIndicator(character, getIndicator);

});

So it looks like those changes do nothing different. Can someone help? Thank you!

Upvotes: 0

Views: 35

Answers (2)

coagmano
coagmano

Reputation: 5671

From your different approaches, it seems there's a fundamental misunderstanding of web applications and isomorphic javascript.

Each client loads and runs the client version of your app individually. They run in an isolated environment (a browser tab). When you run code in that application, it is only able to affect itself. For example, your current approaches all look like this:

     ┌─────┐
┌────▼───┐ │ ┌────────┐  ┌────────┐
│ Client │ │ │ Client │  │ Client │
└────────┘ │ └────────┘  └────────┘
     └─────┘
┌────────┐
│ Server │
└────────┘

The client is only talking to itself. Which is why the other clients don't update the chat indicator. (Note that Session is also isolated to each client instance)

What we want is for the client to tell the server that the status has changed and then the server can tell the other clients. Those clients can then update their ui in response to the change:

┌────────┐  ┌────────┐  ┌────────┐
│ Client │  │ Client │  │ Client │
└────────┘  └────────┘  └────────┘
   │   ▲         ▲           ▲
   ▼   │         │           │
┌────────┐       │           │
│ Server │───────┴───────────┘
└────────┘

For that individual instance of your application to communicate to the server or other instances, it needs to make a network request. This is usually a HTTP request (eg XHR, fetch, jquery \$.http), though in Meteor's case, we use DDP over websockets. (Note, you can have the clients talk to each other directly, but true p2p is much more complicated)

In Meteor, the recommended way to communicate to the server is using Meteor.methods. And the recommended way to send data to clients in real time is using pub/sub with Mongo Collections. When a client subscribes to a data feed that the server is publishing, the server will send updates to the client through websockets.

To do this for your chat indicator problem, lets create a collection with chat statuses, set up a method, and pub/sub

import { Meteor } from "meteor/meteor";
import { Mongo } from "meteor/mongo";
// Create the collection
export const ChatStatus = new Mongo.Collection("chat-status");

// Set up the method
Meteor.methods({
  updateStatus(characterId, status) {
    ChatStatus.upsert({
      characterId: characterId,
      status: status,
    });
  },
});
// Publications are server-only
if (Meteor.isServer) {
  Meteor.publish("chat-status", function() {
    return ChatStatus.find();
  });
}
// Subscriptions are client only. Normally you would put this in template code, not here.
if (Meteor.isClient) {
  Meteor.subscribe("chat-status");
}

Because we want to access this data on the server (for long-term storage) and the client (so we can show the indicator), lets place it in: /both/chat-status.js And import that file (import '/both/chat-status.js') into /client/main.js and /server/main.js

This is what is meant by isomorphic. We write the code in one place, and load it on both the server and client.

Now lets give your template access to the collection by importing it and add a helper to check if the status is editing

// chat.js;
import { ChatStatus } from "/both/chat-status.js";

Template.chat_onlinelist.helpers({
  isEditing: function(characterId) {
    const document = ChatStatus.findOne({ characterId: characterId });
    if (document) {
      return document.status === "editing";
    } else {
      return false;
    }
  },
});

And update the template code to use the new helper:

{{#each characters}}
  <div class="characterBasicInteraction" oncontextmenu="return false;">
    {{#if isEditing _id }}
    <span class="typeIndicator" id="typeInd_{{name}}">✎</span>
    {{/if}}
 <!-- TypeIndicator shows up here, code gets id with this.name
and uses it to change jquery fadeIn/fadeOut -->
    <a href="/c/{{name}}" target="_blank" class="{{name}}" {{checkIfFriend}}><li>{{name}}</li></a>
    <div id="panel_{{name}}" class="panelChat">
      <span><b>{{name}}</b></span>
      <span id="whisp">Flüstern</span>
      {{{checkIfFriendButton}}}
      <span>Bookmark</span>
      <span>Ignore</span>
      <span>Report</span>
    </div>
  </div>
{{/each}}

Now the template code for all clients depends on the value inside the database. The last thing is to update that value in the database by calling the meteor method:

Template.chat_onlinelist.events({
  "keydown textarea"(event, templateInstance) {
    var character = Session.get("activeChar");
    var doneTypingInterval = 5000; //time in ms (5 seconds)
    // Assuming that you're storing the whole document?
    Meteor.call("update-status", character._id, "editing");
    // set the timer identifier on the template instance so we can get it later
    templateInstance.typingTimer = setTimeout(() => {
      // use empty string to denote no status
      Meteor.call("update-status", character._id, "");
    }, doneTypingInterval);
  },
  "focusout textarea"(event, templateInstance) {
    if (templateInstance.typingTimer) {
      clearTimeout(templateInstance.typingTimer);
    }
    Meteor.call("update-status", character._id, "");
  },
});

So now the value in the database is set by events, and the UI is a representation of the state in the database.


Note: I've oversimplified a few things, written the code from memory, and droped the fadeIn, fadeOut behaviour from before. Please edit the code to your needs and I recommend doing animations with a package like: gwendall:template-animations, or gwendall:ui-hooks

Upvotes: 2

ghybs
ghybs

Reputation: 53185

If I understand correctly, you would like an indicator to be shared in real time with all other connected clients. That is indeed the type of feature where Meteor can shine easily.

What you probably miss is that for such feature to work, the data / indicator to be shared must be made available in your MongoDB, so that your Server can then push it to all Clients in realtime (this requirement through db is due to how the Meteor server is designed, so even transient indicator like your "writing status" must still be recorded in db).

See publication and subscription in Meteor guide.

Upvotes: 1

Related Questions