From 7efed1380f6a6cfc8d0b95015f1fa167b4a7bc23 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sun, 13 Nov 2016 17:14:43 -0800 Subject: Implement chat room backend --- client/src/main/scala/chat/Main.scala | 16 ++++----- server/app/controllers/HomeController.scala | 23 +++++++++--- server/app/controllers/actors.scala | 56 +++++++++++++++++++++++++++++ server/app/views/index.scala.html | 6 ++-- server/conf/routes | 2 +- shared/src/main/scala/chat/Messages.scala | 3 -- shared/src/main/scala/chat/messages.scala | 10 ++++++ 7 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 server/app/controllers/actors.scala delete mode 100644 shared/src/main/scala/chat/Messages.scala create mode 100644 shared/src/main/scala/chat/messages.scala diff --git a/client/src/main/scala/chat/Main.scala b/client/src/main/scala/chat/Main.scala index c68e0d9..dbc2876 100644 --- a/client/src/main/scala/chat/Main.scala +++ b/client/src/main/scala/chat/Main.scala @@ -1,26 +1,22 @@ package chat +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue import scala.scalajs.js import org.scalajs.dom -import scala.util.{ Failure, Success } -import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue +import org.scalajs.dom.raw.MessageEvent import upickle.default._ object Main extends js.JSApp { def main(): Unit = { val root = dom.document.getElementById("root") + val sock = new dom.WebSocket("ws://localhost:9000/socket/john") - dom.ext.Ajax.get("/message").onComplete { - case Success(msg) if 200 <= msg.status && msg.status < 300 => - root.textContent = "OK, " + read[Message](msg.responseText).data - case Success(msg) => - root.textContent = msg.responseText - case Failure(err) => - root.textContent = "ERROR: " + err + sock.onmessage = (msg: MessageEvent) => { + val event = read[Event](msg.data.asInstanceOf[String]) + dom.console.log(event.toString()) } - //dom.document.getElementById("scalajsShoutOut").textContent = SharedMessages.itWorks } } diff --git a/server/app/controllers/HomeController.scala b/server/app/controllers/HomeController.scala index 13bc8dc..4d48c98 100644 --- a/server/app/controllers/HomeController.scala +++ b/server/app/controllers/HomeController.scala @@ -1,14 +1,24 @@ package controllers -import play.api._ +import akka.actor.{ActorRef, ActorSystem, Props} +import akka.stream.Materializer +import chat._ +import javax.inject._ +import play.api.libs.streams.ActorFlow import play.api.mvc._ +import play.api.mvc.WebSocket.MessageFlowTransformer import upickle.default._ /** * This controller creates an `Action` to handle HTTP requests to the * application's home page. */ -class HomeController extends Controller { +class HomeController @Inject() ( + implicit system: ActorSystem, + mat: Materializer +) extends Controller { + + lazy val room: ActorRef = system.actorOf(Props(classOf[RoomActor])) /** * Create an Action to render an HTML page. @@ -21,8 +31,13 @@ class HomeController extends Controller { Ok(views.html.index()) } - def message = Action { implicit request => - Ok(write(chat.Message("hello"))) + def socket(uid: String) = WebSocket.accept[Command, Event] { request => + ActorFlow.actorRef(out => Props(classOf[ClientActor], uid, room, out)) } + implicit val transformer = MessageFlowTransformer.stringMessageFlowTransformer.map( + in => read[Command](in), + (out: Event) => write(out) + ) + } diff --git a/server/app/controllers/actors.scala b/server/app/controllers/actors.scala new file mode 100644 index 0000000..f10f925 --- /dev/null +++ b/server/app/controllers/actors.scala @@ -0,0 +1,56 @@ +package controllers + +import akka.actor.{ Actor, ActorRef, Terminated } +import chat._ +import scala.collection.mutable.HashMap + + +/** An actor instantiated for every websocket connection. It represents a + * chat client and registers with a chat room. + * @param uid user id of the connecting client + * @param room chat room actor that this client will join + * @param socket websocket actor, any message sent to it will get transferred to the remote + * browser + */ +class ClientActor(uid: String, room: ActorRef, socket: ActorRef) extends Actor { + + override def preStart() = { + room ! (self, uid) + } + + def receive = { + case cmd: Command => room ! cmd + case ev: Event => socket ! ev + } + +} + +/** An actor that represents a chat room. + * Handles commands and events subclassing `chat.Command` and `chat.Event` + */ +class RoomActor extends Actor { + val clients = new HashMap[ActorRef, String] + + override def receive = { + + case (client: ActorRef, uid: String) => + context.watch(sender) + clients += client -> uid + for ( (client, _) <- clients ) { + client ! Joined(uid) + } + + case Terminated(client) => + clients -= client + for ( (cl, _) <- clients ) { + cl ! Left(clients(client)) + } + + case Broadcast(content) => + val origin = clients(sender) + for ( (client, _) <- clients ) { + client ! Message(origin, content) + } + } +} + diff --git a/server/app/views/index.scala.html b/server/app/views/index.scala.html index 911bdaf..4b71c59 100644 --- a/server/app/views/index.scala.html +++ b/server/app/views/index.scala.html @@ -1,7 +1,9 @@ @() -@main("Welcome to Play") { -
placeholder
+@main("Welcome to Chat") { +
+ hello +