diff --git a/modules/chat/src/main/ChatApi.scala b/modules/chat/src/main/ChatApi.scala index 717d1d5bd9..b0b27de6a8 100644 --- a/modules/chat/src/main/ChatApi.scala +++ b/modules/chat/src/main/ChatApi.scala @@ -6,7 +6,7 @@ import reactivemongo.bson.BSONDocument import lila.db.Types.Coll import lila.user.{ User, UserRepo } -private[chat] final class ChatApi( +final class ChatApi( coll: Coll, flood: lila.security.Flood, maxLinesPerChat: Int, @@ -61,9 +61,9 @@ private[chat] final class ChatApi( private def pushLine(chatId: ChatId, line: Line) = coll.update( BSONDocument("_id" -> chatId), BSONDocument("$push" -> BSONDocument( - "messages" -> BSONDocument( - "$each" -> line, - "$slice" -> maxLinesPerChat) + Chat.BSONFields.lines -> BSONDocument( + "$each" -> List(line), + "$slice" -> -maxLinesPerChat) )), upsert = true) diff --git a/modules/chat/src/main/FrontActor.scala b/modules/chat/src/main/FrontActor.scala index 18527d5109..cb2c5fa8fe 100644 --- a/modules/chat/src/main/FrontActor.scala +++ b/modules/chat/src/main/FrontActor.scala @@ -3,7 +3,7 @@ package lila.chat import akka.actor._ import chess.Color -import lila.hub.actorApi.chat._ +import actorApi._ private[chat] final class FrontActor(api: ChatApi) extends Actor { @@ -13,18 +13,19 @@ private[chat] final class FrontActor(api: ChatApi) extends Actor { def receive = { - case UserTalk(chatId, userId, text) ⇒ api.userChat.write(chatId, userId, text) foreach publish(chatId) + case UserTalk(chatId, userId, text, replyTo) ⇒ + api.userChat.write(chatId, userId, text) foreach publish(chatId, replyTo) - case PlayerTalk(chatId, color, text) ⇒ api.playerChat.write(chatId, Color(color), text) foreach publish(chatId) + case PlayerTalk(chatId, color, text, replyTo) ⇒ + api.playerChat.write(chatId, Color(color), text) foreach publish(chatId, replyTo) - case SystemTalk(chatId, text) ⇒ api.userChat.system(chatId, text) foreach publish(chatId) - - case msg: NotifyLine ⇒ bus.publish(msg, 'chatOut) + case SystemTalk(chatId, text, replyTo) ⇒ + api.userChat.system(chatId, text) foreach publish(chatId, replyTo) } - def publish(chatId: String)(lineOption: Option[Line]) { + def publish(chatId: String, replyTo: ActorRef)(lineOption: Option[Line]) { lineOption foreach { line ⇒ - self ! NotifyLine(chatId, Line toJson line) + replyTo ! ChatLine(chatId, line) } } } diff --git a/modules/chat/src/main/Line.scala b/modules/chat/src/main/Line.scala index add210e4ce..5ce1badd68 100644 --- a/modules/chat/src/main/Line.scala +++ b/modules/chat/src/main/Line.scala @@ -29,7 +29,7 @@ object Line { def write(x: Line) = BSONString(lineToStr(x)) } - private val UserLineRegex = """^([\w-]{2,})[\s!](.+)$""".r + private val UserLineRegex = """^([\w-]{2,})(\s|\!)(.+)$""".r def strToUserLine(str: String): Option[UserLine] = str match { case UserLineRegex(username, " ", text) ⇒ UserLine(username, text, false).some case UserLineRegex(username, "!", text) ⇒ UserLine(username, text, true).some diff --git a/modules/chat/src/main/actorApi.scala b/modules/chat/src/main/actorApi.scala new file mode 100644 index 0000000000..7564fdcb8b --- /dev/null +++ b/modules/chat/src/main/actorApi.scala @@ -0,0 +1,9 @@ +package lila.chat +package actorApi + +import akka.actor.ActorRef + +case class UserTalk(chatId: String, userId: String, text: String, replyTo: ActorRef) +case class PlayerTalk(chatId: String, white: Boolean, text: String, replyTo: ActorRef) +case class SystemTalk(chatId: String, text: String, replyTo: ActorRef) +case class ChatLine(chatId: String, line: Line) diff --git a/modules/game/src/main/Event.scala b/modules/game/src/main/Event.scala index f2f0037cd5..648024a5d5 100644 --- a/modules/game/src/main/Event.scala +++ b/modules/game/src/main/Event.scala @@ -1,8 +1,8 @@ package lila.game -import org.apache.commons.lang3.StringEscapeUtils.escapeXml import play.api.libs.json._ +import lila.chat.{ Line, UserLine, PlayerLine } import chess.Pos.{ piotr, allPiotrs } import chess.{ PromotableRole, Pos, Color, Situation, Move ⇒ ChessMove, Clock ⇒ ChessClock } @@ -107,20 +107,20 @@ object Event { def data = JsString(pos.key) } - case class Message(author: String, text: String, t: Boolean) extends Event { + case class PlayerMessage(line: PlayerLine) extends Event { def typ = "message" - def data = Json.obj("u" -> author, "t" -> escapeXml(text)) + def data = Line toJson line override def owner = true - override def troll = t + override def troll = false } // it *IS* a username, and not a user ID // immediately used for rendering - case class WatcherMessage(username: Option[String], text: String, t: Boolean) extends Event { + case class UserMessage(line: UserLine, w: Boolean) extends Event { def typ = "message" - def data = Json.obj("u" -> username, "t" -> escapeXml(text)) - override def watcher = true - override def troll = t + def data = Line toJson line + override def troll = line.troll + override def watcher = w } object End extends Empty { diff --git a/modules/hub/src/main/actorApi.scala b/modules/hub/src/main/actorApi.scala index 772a70f391..592cb51504 100644 --- a/modules/hub/src/main/actorApi.scala +++ b/modules/hub/src/main/actorApi.scala @@ -35,13 +35,6 @@ case class WithUserIds(f: Iterable[String] ⇒ Unit) case object GetUids -package chat { -case class UserTalk(chatId: String, userId: String, text: String) -case class PlayerTalk(chatId: String, white: Boolean, text: String) -case class SystemTalk(chatId: String, text: String) -case class NotifyLine(chatId: String, json: JsObject) -} - package report { case class Cheater(userId: String, text: String) case class Check(userId: String) diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index 97eb14663d..b5553af276 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -59,7 +59,7 @@ final class Env( }), name = ActorMapName) private val socketHub = system.actorOf( - Props(new lila.socket.SocketHubActor.Default[Socket] { + Props(new lila.socket.SocketHubActor[Socket] { def mkActor(id: String) = new Socket( gameId = id, history = history(), @@ -68,6 +68,10 @@ final class Env( socketTimeout = SocketTimeout, disconnectTimeout = PlayerDisconnectTimeout, ragequitTimeout = PlayerRagequitTimeout) + def receive: Receive = ({ + case msg@lila.chat.actorApi.ChatLine(id, line) ⇒ + self ! lila.hub.actorApi.map.Tell(id take 8, msg).pp + }: Receive) orElse socketHubReceive }), name = SocketName) @@ -113,6 +117,7 @@ final class Env( lazy val messenger = new Messenger( bus = system.lilaBus, + socketHub = socketHub, i18nKeys = i18nKeys) lazy val fenUrlWatch = new FenUrlWatch( diff --git a/modules/round/src/main/Messenger.scala b/modules/round/src/main/Messenger.scala index df9d47c9a8..0f5b8d37eb 100644 --- a/modules/round/src/main/Messenger.scala +++ b/modules/round/src/main/Messenger.scala @@ -2,17 +2,18 @@ package lila.round import lila.common.Bus import lila.game.Game -import lila.hub.actorApi.chat._ +import lila.chat.actorApi._ import lila.i18n.I18nKey.{ Select ⇒ SelectI18nKey } import lila.i18n.I18nKeys final class Messenger( bus: Bus, + socketHub: akka.actor.ActorRef, i18nKeys: I18nKeys) { def apply(game: Game, message: SelectI18nKey, args: Any*) { val translated = message(i18nKeys).en(args: _*) - bus.publish(SystemTalk(game.id + "/w", translated), 'chatIn) - if (game.nonAi) bus.publish(SystemTalk(game.id, translated), 'chatIn) + bus.publish(SystemTalk(game.id + "/w", translated, socketHub), 'chatIn) + if (game.nonAi) bus.publish(SystemTalk(game.id, translated, socketHub), 'chatIn) } } diff --git a/modules/round/src/main/Socket.scala b/modules/round/src/main/Socket.scala index a82f66901d..2d0832c23b 100644 --- a/modules/round/src/main/Socket.scala +++ b/modules/round/src/main/Socket.scala @@ -26,6 +26,8 @@ private[round] final class Socket( disconnectTimeout: Duration, ragequitTimeout: Duration) extends SocketActor[Member](uidTimeout) { + context.system.lilaBus.subscribe(self, 'chatOut) + private val timeBomb = new TimeBomb(socketTimeout) private final class Player(color: Color) { @@ -87,8 +89,13 @@ private[round] final class Socket( sender ! Connected(enumerator, member) } - case Nil ⇒ - case events: Events ⇒ notify(events) + case Nil ⇒ + case events: Events ⇒ notify(events) + + case lila.chat.actorApi.ChatLine(chatId, line) ⇒ notify(List(line match { + case l: lila.chat.UserLine ⇒ Event.UserMessage(l, chatId endsWith "/w") + case l: lila.chat.PlayerLine ⇒ Event.PlayerMessage(l) + })) case AnalysisAvailable ⇒ notifyAll("analysisAvailable", true) diff --git a/modules/round/src/main/SocketHandler.scala b/modules/round/src/main/SocketHandler.scala index 4829778187..828e6aa674 100644 --- a/modules/round/src/main/SocketHandler.scala +++ b/modules/round/src/main/SocketHandler.scala @@ -8,6 +8,7 @@ import chess.Color import play.api.libs.json.{ JsObject, Json } import actorApi._, round._ +import lila.chat.actorApi._ import lila.common.PimpedJson._ import lila.game.{ Game, Pov, PovRef, PlayerRef, GameRepo } import lila.hub.actorApi.map._ @@ -37,6 +38,11 @@ private[round] final class SocketHandler( case ("liveGames", o) ⇒ o str "d" foreach { ids ⇒ socket ! LiveGames(uid, ids.split(' ').toList) } + case ("talk", o) ⇒ o str "d" foreach { text ⇒ + member.userId foreach { userId => + bus.publish(UserTalk(gameId + "/w", userId, text, socket), 'chatIn) + } + } }) { playerId ⇒ { case ("p", o) ⇒ o int "v" foreach { v ⇒ socket ! PingVersion(uid, v) } @@ -70,8 +76,8 @@ private[round] final class SocketHandler( } case ("talk", o) ⇒ o str "d" foreach { text ⇒ bus.publish(member.userId match { - case Some(userId) ⇒ lila.hub.actorApi.chat.UserTalk(uid, userId, text) - case None ⇒ lila.hub.actorApi.chat.PlayerTalk(uid, member.color.white, text) + case Some(userId) ⇒ UserTalk(gameId, userId, text, socket) + case None ⇒ PlayerTalk(gameId, member.color.white, text, socket) }, 'chatIn) } } diff --git a/project/Build.scala b/project/Build.scala index 14573768d8..70dd5c1b23 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -90,7 +90,7 @@ object ApplicationBuild extends Build { play.api, play.test, RM, PRM, hasher) ) - lazy val game = project("game", Seq(common, memo, db, hub, user, chess)).settings( + lazy val game = project("game", Seq(common, memo, db, hub, user, chess, chat)).settings( libraryDependencies ++= provided( play.api, RM, PRM) ) @@ -106,7 +106,7 @@ object ApplicationBuild extends Build { ) lazy val round = project("round", Seq( - common, db, memo, hub, socket, chess, game, user, i18n, ai, pref)).settings( + common, db, memo, hub, socket, chess, game, user, i18n, ai, pref, chat)).settings( libraryDependencies ++= provided(play.api, RM, PRM) ) diff --git a/public/javascripts/big.js b/public/javascripts/big.js index d376901bd5..b6c859bcd7 100644 --- a/public/javascripts/big.js +++ b/public/javascripts/big.js @@ -348,6 +348,11 @@ var storage = { }); } }, + message: function(msg) { + $('div.lichess_chat').each(function() { + $(this).chat("append", msg); + }); + }, nbm: function(e) { $('#nb_messages').text(e || "0").toggleClass("unread", e > 0); }, @@ -1584,11 +1589,12 @@ var storage = { if (!$toggle[0].checked) { self.element.addClass('hidden'); } + if (self.options.messages.length > 0) self._appendMany(self.options.messages); }, append: function(msg) { this._appendHtml(this._render(msg)); }, - appendMany: function(objs) { + _appendMany: function(objs) { var self = this, html = ""; $.each(objs, function() { @@ -1605,7 +1611,7 @@ var storage = { } else { user = '' + $.userLinkLimit(msg.u, 14) + ''; } - return '
  • ' + urlToLink(msg.t) + '
  • '; + return '
  • ' + user + urlToLink(msg.t) + '
  • '; }, _appendHtml: function(html) { this.$msgs.append(html); diff --git a/public/stylesheets/board.css b/public/stylesheets/board.css index 19bc3cc432..3776b6fdca 100644 --- a/public/stylesheets/board.css +++ b/public/stylesheets/board.css @@ -2,7 +2,6 @@ div.lichess_board_wrap { float: left; border: 1px solid #ccc; position: relative; - display: none; } span.board_mark { position: absolute;