not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

343 lines
16 KiB

  1. [Go back to tutorial home](tutorial.md)
  2. # Step 6: handle multi-user with presence
  3. *(Estimated time: 1h30mn)*
  4. What if the Pawn activity could be played by multiple players at the same time? After all, it's a logical feature for a game. That's what we will doing in this step.
  5. ### Connect to a server
  6. From the begining of this tutorial we use Sugarizer stand alone. All HTML and JavaScript are run locallly and do not depend of external code. For this step however, because we need to have communication between multiple clients, we will need a Sugarizer Server too. The Sugarizer Server is a backend that provide connection features to Sugarizer Apps.
  7. You could install a Sugarizer Server locally following instruction [here](https://github.com/llaske/sugarizer-server/blob/master/README.md) or use the test server available on [https://dev.sugarizer.org](https://dev.sugarizer.org).
  8. To see if you're connected to a server, click on the Neighborhood view - the icon with multiple dot - on the Sugarizer toolbar. Because you're not connected yet, here is the message you will see:
  9. ![](images/tutorial_step6_1.png)
  10. To connect to a server, click on the button to access to settings, then to "About my server" icon to display the server settings window.
  11. ![](images/tutorial_step6_2.png)
  12. Check the connected box and type the URL of your server, may be `http://localhost:8080` (if you've installed your own server) or `https://dev.sugarizer.org`. You will have to choose few images as password, then click on restart.
  13. If everything is right, you will see now the full Neighborhood view.
  14. ![](images/tutorial_step6_3.png)
  15. And you're now connected.
  16. ### What is meant by sharing an instance
  17. Suppose that Michaël, a user connected on the same server as you wants to Chat. He will launch the Chat activity.
  18. ![](images/tutorial_step6_4.png)
  19. ***Tip***: *To connect to Sugarizer with two users on the same computer, open a new "in private" browser window. So you will be able to create a new user. It's what we've done here and what we'll do below to simulate the user Michaël.*
  20. Then, once the activity is open, he can share the activity by clicking on the toolbar Neighborhood button in the Network palette. It's a way for the user to say: *"I want to share my activity on the network"*.
  21. ![](images/tutorial_step6_5.png)
  22. From your Neighborhood view you will see the icon of the activity suddenly appear near the Michaël's icon. Just pass the mouse on the activity icon and click the Join menu and you will be able to join Michaël.
  23. ![](images/tutorial_step6_6.png).
  24. The Chat activity will open on your side using Michaël colors, and the Chat can start between users.
  25. ![](images/tutorial_step6_7.png)
  26. Easy, isn't it ? And thanks to the unique Sugarizer presence framework, do the same thing for our Pawn activity will not be so complex.
  27. ### Add the presence palette
  28. All activities that could be shared have to include the Network palette in its toolbar. Like in the Chat activity it's the way for user to share its current work and allow other users to join. Let's do it in Pawn activity.
  29. First start by adding a button for the network in the `index.html` file. We add it just after the existing `add` button.
  30. <button class="toolbutton" id="network-button" title="Network"></button>
  31. We will now define this new button in the `css/activity.css` file. We define also the two buttons included in the palette. Note that all icons are already included in the `lib/sugar-web/graphics/icons/actions` directory.
  32. #main-toolbar #network-button {
  33. background-image: url(../lib/sugar-web/graphics/icons/actions/zoom-home.svg);
  34. }
  35. #private-button {
  36. background-image: url(../lib/sugar-web/graphics/icons/actions/zoom-home.svg);
  37. width: 47px;
  38. height: 47px;
  39. margin: 4px 2px;
  40. color: white;
  41. color: transparent;
  42. background-color: transparent;
  43. background-position: center;
  44. background-repeat: no-repeat;
  45. background-size: contain;
  46. border: 0;
  47. border-radius: 5.5px;
  48. }
  49. #shared-button {
  50. background-image: url(../lib/sugar-web/graphics/icons/actions/zoom-neighborhood.svg);
  51. width: 47px;
  52. height: 47px;
  53. margin: 4px 2px;
  54. color: white;
  55. color: transparent;
  56. background-color: transparent;
  57. background-position: center;
  58. background-repeat: no-repeat;
  59. background-size: contain;
  60. border: 0;
  61. border-radius: 5.5px;
  62. }
  63. The name "palette" refer to a popup menu in the toolbar. When the user click on the toolbar icon, the popup appear and display items inside - most often other buttons. To handle this feature Sugar-Web expose a Palette library and, more precisely, a PresencePalette too.
  64. As usual, to integrate this library, we will update the dependancies list at the first line of `activity/activity.js`.
  65. define(["sugar-web/activity/activity", "sugar-web/env", "sugar-web/graphics/icon", "webL10n","sugar-web/graphics/presencepalette"], function (activity, env, icon, webL10n, presencepalette) {
  66. This palette must be initialized in the code. You just have to call the PresencePalette constructor with the toolbar element. So add this line in the end of the require function of `activity/activity.js`:
  67. // Link presence palette
  68. var palette = new presencepalette.PresencePalette(document.getElementById("network-button"), undefined);
  69. Let's test the result by launching our Pawn activity.
  70. ![](images/tutorial_step6_8.png)
  71. The new Network button and palette is here. We now have to implement the magic inside.
  72. ### How presence works
  73. Before further implementation let's pause to explain what exactly Sugarizer presence framework is and what it does.
  74. The presence framework provide a real time communication between a set of clients. To do that the framework is based on the [publish/subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) pattern. Every client could create one or more **topics**. Other clients could **subscribe** to these topics and everyone could **publish** messages on a topic. When a message is published on a topic, only clients connected to this topic receive the message.
  75. In the context of Sugarizer, clients are Sugarizer App/WebApp connected to the Server. One topic is a shared activity. The Sugarizer Server is responsible to keep the list of topics and clients and dispatch messages to clients subscribed to topics. So the server is the central point and in fact, clients communicate only with the server.
  76. Let's take an example. Michaël, Lionel and Bastien are three users connected to the same Sugarizer Server. Michaël share a Chat activity. Lionel decide to join the activity. Bastien share its own Paint activity but he's alone on the activity.
  77. ![](images/tutorial_step6_9.png)
  78. The server know that two activities are shared: one Chat activity with Michaël and Lionel as subscribers, one Paint activity with only Bastien as subscriber.
  79. If Michaël post a message for the Chat activity, the server will automatically send back the message to Michaël and Lionel but not to Bastien.
  80. Easy to understand isn't it?
  81. ### Share the instance
  82. Now, let's update our Pawn activity to integrate presence. Start first by handling the click on the Share button. We will add a listener to handle `shared` event of the palette in the `activity/activity.js` file.
  83. // Link presence palette
  84. var presence = null;
  85. var palette = new presencepalette.PresencePalette(document.getElementById("network-button"), undefined);
  86. palette.addEventListener('shared', function() {
  87. palette.popDown();
  88. console.log("Want to share");
  89. presence = activity.getPresenceObject(function(error, network) {
  90. if (error) {
  91. console.log("Sharing error");
  92. return;
  93. }
  94. network.createSharedActivity('org.sugarlabs.Pawn', function(groupId) {
  95. console.log("Activity shared");
  96. });
  97. network.onDataReceived(onNetworkDataReceived);
  98. });
  99. });
  100. In this listener, we have to retrieve the presence object. As usual it's exposed by the `activity` object. So you just need a call to `activity.getPresenceObject` method. If everything goes well, you will retrieve a presence object. That's a way to indicate that you're connected to a server.
  101. The `createSharedActivity` on this object allow you to create a new shared activity. You must pass as parameter the type of the activity so Sugarizer could display the right icon in the neighborhood view. Then you receive the unique identifier of the share in the `groupId` parameter of the callback.
  102. Finally we register a callback to handle message received, with a call to `onDataReceived` method. We will have a look on this callback later.
  103. Now that our activity is shared, we have to slightly update the Plus button listener. Because now we should notify other users when a new pawn is played. Here's how the new listener will look like:
  104. // Handle click on add
  105. document.getElementById("add-button").addEventListener('click', function (event) {
  106. pawns.push(currentenv.user.colorvalue);
  107. drawPawns();
  108. document.getElementById("user").innerHTML = "<h1>"+webL10n.get("Played", {name:currentenv.user.name})+"</h1>";
  109. if (presence) {
  110. presence.sendMessage(presence.getSharedInfo().id, {
  111. user: presence.getUserInfo(),
  112. content: currentenv.user.colorvalue
  113. });
  114. }
  115. });
  116. If the activity is connected (i.e. presence is not null), we call the `sendMessage` method. As its name implies, `sendMessage` is the method to send a message to the server. The first parameter of this method is the id of the share. We could retrieve this id from the presence object by `getSharedInfo().id`. The second parameter is just the message. We decided to split the message in two parts: informations about `user` that sent the message and the `content`, the user color. The user info is get from presence object using the `getUserInfo()` call: it will retrieve an object with `name`, `networkId` and `colorvalue`.
  117. That's all we need to create the shared activity and let it appear on the Neighborhood view of other users. We have now to handle what happens when a user clicks on the Join menu. In that case, your activity is automatically open by Sugarizer with a specific parameter in the environment. So we will update the `getEnvironment` call in the `activity/activity.js` file to handle this case:
  118. env.getEnvironment(function(err, environment) {
  119. currentenv = environment;
  120. // Set current language to Sugarizer
  121. ...
  122. // Load from datatore
  123. if (!environment.objectId) {
  124. ...
  125. }
  126. // Shared instances
  127. if (environment.sharedId) {
  128. console.log("Shared instance");
  129. presence = activity.getPresenceObject(function(error, network) {
  130. network.onDataReceived(onNetworkDataReceived);
  131. });
  132. }
  133. });
  134. What we've added in this source code is: if `environment.sharedId` is not null - i.e. the activity was launched from the Join menu - we get the presence object and declare the callback to process data received.
  135. The `onNetworkDataReceived` callback is the same one we used previously. So it's a good idea now to see what this callback should do.
  136. var onNetworkDataReceived = function(msg) {
  137. if (presence.getUserInfo().networkId === msg.user.networkId) {
  138. return;
  139. }
  140. pawns.push(msg.content);
  141. drawPawns();
  142. document.getElementById("user").innerHTML = "<h1>"+webL10n.get("Played", {name:msg.user.name})+"</h1>";
  143. };
  144. This callback is call each time a message is received from the server. The message is the parameter for the callback. We first test the `networkId` in this message to ignore message that we sent ourself. Then we add the message `content` (i.e. colors for the user sending the message) to our `pawns` array and redraw the board with the `drawPawns()` call. Finally we update the welcome message to give the player's name.
  145. Let's try if everything works. From the Michaël browser, launch a new Pawn activity and share it with the network menu.
  146. ![](images/tutorial_step6_10.png)
  147. Open the Lionel browser on the neighborhood view. You should see the shared Pawn activity.
  148. ![](images/tutorial_step6_11.png)
  149. Join the activity by clicking on the Join menu. The activity should open with the Michaël colors.
  150. ![](images/tutorial_step6_12.png)
  151. Click on the Plus button on the Lionel or Michaël browser: each time a pawn with the right color should be added to both windows.
  152. ![](images/tutorial_step6_13.png)
  153. Great isn't it?
  154. And it works for any number of users connected at the same time on the shared activity!
  155. ### Handling subscribers change
  156. Let's go a bit further or more precisely, let's fix a small issue in the previous implementation.
  157. If Michaël start to play some pawns on the board before Lionel join the activity, there will be a difference between the boards:
  158. ![](images/tutorial_step6_14.png)
  159. It's like initial plays from Michaël was lost.
  160. This issue is related to the way of handling users that join the activity. Currently nothing is done to give them the initial board state. So they only seen new changes on the board. It could make sense for a chat activity: users who join a chat could not be able to see past discussions. But for our activity, it's not a good thing.
  161. To fix it, let's subscribe to a new presence callback named `onSharedActivityUserChanged`. So we will add two times - in `shared` listener and in `getEnvironment` under `network.onDataReceived` call - the following line in `activity/activity.js` file:
  162. network.onSharedActivityUserChanged(onNetworkUserChanged);
  163. Here's a first simple implementation for this new callback:
  164. var onNetworkUserChanged = function(msg) {
  165. console.log("User "+msg.user.name+" "+(msg.move == 1 ? "join": "leave"));
  166. };
  167. The `onSharedActivityUserChanged` message is send automatically by the server when the subscribers list for a shared activity has changed. You will receive in the message a `move` field telling if the user has joined (the `move` value is `1`) or left (the `move` value is `-1`). And you will receive in the `user` field of the message informations (`name`, `networkId` and `colorvalue`) about the user.
  168. The `onSharedActivityUserChanged` message is useful to display a list of users currently connected, and for example displaying this list. Thanks to this message we will be able too to fix our current issue.
  169. The idea is to identify the host for the share (Michaël in our sample). When a new subscriber join the share, the host - and only the host - send to the new subscriber a message with the current board state.
  170. But because current message contains only the color for the added pawn, we have to create a new type of message for that. Here's the suggested implementation to do that.
  171. First let's modify the current send message call to integrate the 'update' message type to keep compatibility with current implementation:
  172. presence.sendMessage(presence.getSharedInfo().id, {
  173. user: presence.getUserInfo(),
  174. content: {
  175. action: 'update',
  176. data: currentenv.user.colorvalue
  177. }
  178. });
  179. Let's tell the host to send the new 'init' message type when the subscriber list change:
  180. var onNetworkUserChanged = function(msg) {
  181. if (isHost) {
  182. presence.sendMessage(presence.getSharedInfo().id, {
  183. user: presence.getUserInfo(),
  184. content: {
  185. action: 'init',
  186. data: pawns
  187. }
  188. });
  189. }
  190. console.log("User "+msg.user.name+" "+(msg.move == 1 ? "join": "leave"));
  191. };
  192. Then we need to update the `onNetworkDataReceived` callback to handle the new message structure:
  193. var onNetworkDataReceived = function(msg) {
  194. if (presence.getUserInfo().networkId === msg.user.networkId) {
  195. return;
  196. }
  197. switch (msg.content.action) {
  198. case 'init':
  199. pawns = msg.content.data;
  200. drawPawns();
  201. break;
  202. case 'update':
  203. pawns.push(msg.content.data);
  204. drawPawns();
  205. document.getElementById("user").innerHTML = "<h1>"+webL10n.get("Played", {name:msg.user.name})+"</h1>";
  206. break;
  207. }
  208. };
  209. Finally we will implement host detection with a new `isHost` variable. The host is the one who created the share:
  210. var isHost = false;
  211. palette.addEventListener('shared', function() {
  212. palette.popDown();
  213. console.log("Want to share");
  214. presence = activity.getPresenceObject(function(error, network) {
  215. if (error) {
  216. console.log("Sharing error");
  217. return;
  218. }
  219. network.createSharedActivity('org.sugarlabs.Pawn', function(groupId) {
  220. console.log("Activity shared");
  221. isHost = true;
  222. });
  223. network.onDataReceived(onNetworkDataReceived);
  224. network.onSharedActivityUserChanged(onNetworkUserChanged);
  225. });
  226. });
  227. Let's repeat the test by launching the activity from Michaël's browser with an initial content (for example from the Journal).
  228. ![](images/tutorial_step6_15.png)
  229. It fully works now!
  230. Implementing a multi-user application is not an easy task but with a nice framework like Sugarizer presence, I'm sure you're convince now that it's feasible!
  231. [Go to next step](tutorial_step7.md)