much progress on chat

This commit is contained in:
Thibault Duplessis 2013-12-28 18:12:17 +01:00
parent 9600b2383f
commit 3a4647fede
24 changed files with 192 additions and 68 deletions

View file

@ -175,11 +175,11 @@ private[controllers] trait LilaController
chatAndPrefs(ctx) map { case (chat, pref) Context(ctx, chat, pref) }
}
import lila.chat.Chat
import lila.chat.NamedChat
import lila.pref.Pref
private def chatAndPrefs(ctx: lila.user.UserContext): Fu[(Option[Chat], Option[Pref])] =
ctx.me.fold(fuccess(none[Chat] -> none[Pref])) { me
(HTTPRequest.isSynchronousHttp(ctx.req) ?? (Env.chat.api.get(me, Nil) map (_.some))) zip (Env.pref.api getPref me) map {
private def chatAndPrefs(ctx: lila.user.UserContext): Fu[(Option[NamedChat], Option[Pref])] =
ctx.me.fold(fuccess(none[NamedChat] -> none[Pref])) { me
(HTTPRequest.isSynchronousHttp(ctx.req) ?? (Env.chat.api.getNamed(me, Nil) map (_.some))) zip (Env.pref.api getPref me) map {
case (chat, pref) chat -> pref.some
}
}

View file

@ -0,0 +1,13 @@
package lila.app
package templating
import lila.chat.Env.{ current chatEnv }
import lila.user.User
trait ChatHelper {
def chatNameChans(chans: List[lila.chat.Chan], as: User): List[lila.chat.NamedChan] = chans match {
case Nil Nil
case cs chatEnv.namer.chans(cs, as).await
}
}

View file

@ -1,11 +1,11 @@
package lila.app
package templating
import lila.api.Env.{ current apiEnv }
import ornicar.scalalib
import play.api.templates.Html
import lila.api.Env.{ current apiEnv }
object Environment
extends scalaz.syntax.ToIdOps
with scalaz.std.OptionInstances
@ -41,7 +41,8 @@ object Environment
with AnalysisHelper
with RelationHelper
with TournamentHelper
with IRCHelper {
with IRCHelper
with ChatHelper {
implicit val LilaHtmlMonoid = scalaz.Monoid.instance[Html](
(a, b) Html(a.body + b.body),

View file

@ -176,8 +176,8 @@ data-accept-languages="@acceptLanguages.mkString(",")">
<div class="lines"></div>
<div class="controls">
<form action="#">
<span class="mainChan"></span>
<input class="input" value="" placeholder="@trans.talkInChat()" />
<span class="invite"></span>
</form>
</div>
</div>
@ -188,7 +188,7 @@ data-accept-languages="@acceptLanguages.mkString(",")">
@jsTag("deps.min.js")
@signedJs.fold(jsTagCompiled("big.js"))(js => jsAt(js, false))
@ctx.chat.map { c =>
@embedJs("var _lc_ = " + toJson(c.addChans(chans).toJson))
@embedJs("var _lc_ = " + toJson(c.addChans(chatNameChans(chans, c.chat.user)).toJson))
}
@moreJs
@if(lang.language != "en") {

View file

@ -15,7 +15,8 @@ title = title,
goodies = views.html.game.infoBox(pov, tour),
chat = roomHtml.map(round.room(_, false)),
underchat = underchat.some,
signedJs = pov.game.rated option routes.Round.signedJs(pov.gameId) map (_.toString)) {
signedJs = pov.game.rated option routes.Round.signedJs(pov.gameId) map (_.toString),
chans = List(if (pov.game.hasAi) lila.chat.GameWatcherChan(pov.gameId) else lila.chat.GamePlayerChan(pov.gameId))) {
<div class="lichess_game clearfix lichess_player_@color not_spectator"
data-socket-url="@routes.Round.websocketPlayer(fullId)"
data-table-url="@routes.Round.tablePlayer(fullId)"

View file

@ -14,7 +14,8 @@
title = title,
goodies = views.html.game.infoBox(pov, tour),
chat = round.room(roomHtml, true).some,
underchat = underchat.some) {
underchat = underchat.some,
chans = List(lila.chat.GameWatcherChan(pov.gameId))) {
@watcherGame(pov)
@embedJs("var _ld_ = " + roundWatcherJsData(pov, version, false, ctx.pref))
@analyse.link(pov, analysed)

View file

@ -14,7 +14,8 @@
@tournament.layout(
title = trans.tournaments.str(),
goodies = goodies.some) {
goodies = goodies.some,
chans = List(lila.chat.TournamentLobbyChan)) {
<div id="tournament" data-href="@routes.Tournament.homeReload()">
@tournament.homeInner(createds, starteds, finisheds)
</div>

View file

@ -1,4 +1,4 @@
@(title: String, moreJs: Html = Html(""), chat: Option[Html] = None, goodies: Option[Html] = None, underchat: Option[Html] = None)(body: Html)(implicit ctx: Context)
@(title: String, moreJs: Html = Html(""), chat: Option[Html] = None, goodies: Option[Html] = None, underchat: Option[Html] = None, chans: List[lila.chat.Chan] = Nil)(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("tournament.css")
@ -11,6 +11,7 @@ moreJs = moreJs,
chat = chat,
goodies = goodies,
active = siteMenu.tournament.some,
underchat = underchat) {
underchat = underchat,
chans = chans) {
@body
}

View file

@ -18,7 +18,8 @@ cssClass = "tournament_chat")(tournament.show.room(messages))
title = title,
goodies = goodies.some,
chat = chat.some,
underchat = underchat.some) {
underchat = underchat.some,
chans = List(lila.chat.TournamentChan(tour.id))) {
<div
id="tournament"
data-href="@routes.Tournament.reload(tour.id)"

View file

@ -2,14 +2,14 @@ package lila.api
import play.api.mvc.{ Request, RequestHeader }
import lila.chat.Chat
import lila.chat.NamedChat
import lila.pref.Pref
import lila.user.{ UserContext, HeaderUserContext, BodyUserContext }
sealed trait Context extends lila.user.UserContextWrapper {
val userContext: UserContext
val chat: Option[Chat]
val chat: Option[NamedChat]
val pref: Pref
def currentTheme =
@ -23,15 +23,15 @@ sealed trait Context extends lila.user.UserContextWrapper {
sealed abstract class BaseContext(
val userContext: lila.user.UserContext,
val chat: Option[Chat],
val chat: Option[NamedChat],
val pref: Pref) extends Context
final class BodyContext(val bodyContext: BodyUserContext, chatOption: Option[Chat], p: Pref)
final class BodyContext(val bodyContext: BodyUserContext, chatOption: Option[NamedChat], p: Pref)
extends BaseContext(bodyContext, chatOption, p) {
def body = bodyContext.body
}
final class HeaderContext(val headerContext: HeaderUserContext, chatOption: Option[Chat], p: Pref)
final class HeaderContext(val headerContext: HeaderUserContext, chatOption: Option[NamedChat], p: Pref)
extends BaseContext(headerContext, chatOption, p)
object Context {
@ -39,9 +39,9 @@ object Context {
def apply(req: RequestHeader): HeaderContext =
new HeaderContext(UserContext(req, none), none, Pref.default)
def apply(userContext: HeaderUserContext, chat: Option[Chat], pref: Option[Pref]): HeaderContext =
def apply(userContext: HeaderUserContext, chat: Option[NamedChat], pref: Option[Pref]): HeaderContext =
new HeaderContext(userContext, chat, pref | Pref.default)
def apply(userContext: BodyUserContext, chat: Option[Chat], pref: Option[Pref]): BodyContext =
def apply(userContext: BodyUserContext, chat: Option[NamedChat], pref: Option[Pref]): BodyContext =
new BodyContext(userContext, chat, pref | Pref.default)
}

View file

@ -12,6 +12,7 @@ import lila.user.{ User, UserRepo }
import tube.lineTube
private[chat] final class Api(
namer: Namer,
flood: lila.security.Flood,
prefApi: lila.pref.PrefApi,
netDomain: String) {
@ -30,6 +31,13 @@ private[chat] final class Api(
get(u, extraChans.map(Chan.parse).flatten)
}
def getNamed(user: User, extraChans: List[Chan]): Fu[NamedChat] =
get(user, extraChans) flatMap namer.chat
def getNamed(userId: String, extraChans: List[String]): Fu[NamedChat] =
(UserRepo byId userId) flatten s"No such user: $userId" flatMap { u
getNamed(u, extraChans.map(Chan.parse).flatten)
}
def write(chan: String, userId: String, text: String): Fu[Option[Line]] = {
import Writer._
UserRepo byId userId flatMap {

View file

@ -5,15 +5,8 @@ import play.api.libs.json._
sealed trait Chan {
def typ: String
def key: String
def name: String
def idOption: Option[String]
def toJson = Json.obj(
"typ" -> typ,
"id" -> idOption,
"key" -> key,
"name" -> name)
override def toString = key
}
@ -22,7 +15,7 @@ sealed abstract class StaticChan(val typ: String, val name: String) extends Chan
val idOption = none
}
sealed abstract class IdChan(val typ: String, val id: String, val name: String) extends Chan {
sealed abstract class IdChan(val typ: String, val id: String) extends Chan {
val key = s"${typ}_$id"
val idOption = id.some
}
@ -30,9 +23,17 @@ sealed abstract class IdChan(val typ: String, val id: String, val name: String)
object LichessChan extends StaticChan(Chan.typ.lichess, "Lichess")
object LobbyChan extends StaticChan(Chan.typ.lobby, "Lobby")
object TvChan extends StaticChan(Chan.typ.tv, "TV")
case class GameChan(i: String, n: String) extends IdChan(Chan.typ.game, i, n)
case class TournamentChan(i: String, n: String) extends IdChan(Chan.typ.game, i, n)
case class UserChan(i: String, n: String) extends IdChan(Chan.typ.game, i, n)
case class GameWatcherChan(i: String) extends IdChan(Chan.typ.gameWatcher, i)
case class GamePlayerChan(i: String) extends IdChan(Chan.typ.gamePlayer, i)
object TournamentLobbyChan extends StaticChan(Chan.typ.tournamentLobby, "Tournament Lobby")
case class TournamentChan(i: String) extends IdChan(Chan.typ.tournament, i)
case class NamedChan(chan: Chan, name: String) {
def toJson = Json.obj(
"key" -> chan.key,
"name" -> name)
}
object Chan {
@ -40,15 +41,21 @@ object Chan {
val lichess = "lichess"
val lobby = "lobby"
val tv = "tv"
val game = "game"
val gameWatcher = "gameWatcher"
val gamePlayer = "gamePlayer"
val tournamentLobby = "tournamentLobby"
val tournament = "tournament"
}
def apply(typ: String, idOption: Option[String]): Option[Chan] = typ match {
case Chan.typ.lichess LichessChan.some
case Chan.typ.lobby LobbyChan.some
case Chan.typ.tv TvChan.some
case Chan.typ.game idOption map { GameChan(_, "Some game") }
case _ none
case Chan.typ.lichess LichessChan.some
case Chan.typ.lobby LobbyChan.some
case Chan.typ.tv TvChan.some
case Chan.typ.gameWatcher idOption map GameWatcherChan
case Chan.typ.gamePlayer idOption map GamePlayerChan
case Chan.typ.tournamentLobby TournamentLobbyChan.some
case Chan.typ.tournament idOption map TournamentChan
case _ none
}
def parse(str: String): Option[Chan] = str.split('_') match {

View file

@ -1,5 +1,7 @@
package lila.chat
import scala.concurrent.Future
import play.api.libs.json._
import lila.user.User
@ -14,13 +16,18 @@ case class Chat(
def chanKeys = chans map (_.key)
def addChans(cs: List[Chan]) = copy(chans = (chans ::: cs).distinct)
}
case class NamedChat(chat: Chat, namedChans: List[NamedChan]) {
def toJson = Json.obj(
"user" -> user.username,
"lines" -> (lines map (_.toJson)),
"chans" -> JsObject(chans map { c c.key -> c.toJson }),
"activeChans" -> activeChanKeys,
"mainChan" -> mainChanKey.filter(activeChanKeys.contains))
"user" -> chat.user.username,
"lines" -> (chat.lines map (_.toJson)),
"chans" -> JsObject(namedChans map { c c.chan.key -> c.toJson }),
"activeChans" -> chat.activeChanKeys,
"mainChan" -> chat.mainChanKey.filter(chat.activeChanKeys.contains))
def addChans(chans: List[NamedChan]) = copy(namedChans = (namedChans ::: chans).distinct)
}
object Chat {

View file

@ -13,6 +13,7 @@ import lila.user.UserRepo
private[chat] final class ChatActor(
api: Api,
namer: Namer,
bus: Bus,
prefApi: PrefApi) extends Actor {
@ -36,7 +37,7 @@ private[chat] final class ChatActor(
_.withChan(chan, value)
))) andThen {
case _ {
member setActiveChan(chan, value)
member setActiveChan (chan, value)
reloadChat(member)
}
}
@ -57,7 +58,7 @@ private[chat] final class ChatActor(
}
}
case SocketEnter(uid, member, socket) member.userId foreach { userId
case SocketEnter(uid, member) member.userId foreach { userId
members += (uid -> ChatMember(uid, userId, member.troll, member.channel))
}
@ -72,8 +73,8 @@ private[chat] final class ChatActor(
}
private def reloadChat(member: ChatMember) {
api.get(member.userId, member.extraChans) foreach {
case chat: Chat member.channel push Socket.makeMessage("chat.reload", chat.toJson)
api.getNamed(member.userId, member.extraChans) foreach { chat
member.channel push Socket.makeMessage("chat.reload", chat.toJson)
}
}
}

View file

@ -12,7 +12,7 @@ case class ChatMember(
private var privateActiveChans = Set[String]()
def wants(line: Line) =
(troll || !line.troll) && (privateActiveChans.pp contains line.chan.key.pp pp)
(troll || !line.troll) && (privateActiveChans contains line.chan.key)
def extraChans = privateExtraChans

View file

@ -8,6 +8,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
db: lila.db.Env,
getUsername: String Fu[String],
flood: lila.security.Flood,
prefApi: lila.pref.PrefApi,
system: ActorSystem) {
@ -21,12 +22,16 @@ final class Env(
private[chat] lazy val lineColl = db(CollectionLine)
lazy val api = new Api(
namer = namer,
flood = flood,
prefApi = prefApi,
netDomain = NetDomain)
lazy val namer = new Namer(getUsername)
system.actorOf(Props(new ChatActor(
api = api,
api = api,
namer = namer,
bus = system.lilaBus,
prefApi = prefApi)))
}
@ -36,6 +41,7 @@ object Env {
lazy val current: Env = "[boot] chat" describes new Env(
config = lila.common.PlayApp loadConfig "chat",
db = lila.db.Env.current,
getUsername = lila.user.Env.current.usernameOrAnonymous,
flood = lila.security.Env.current.flood,
prefApi = lila.pref.Env.current.api,
system = lila.common.PlayApp.system)

View file

@ -0,0 +1,50 @@
package lila.chat
import scala.concurrent.duration._
import scala.concurrent.Future
import lila.game.{ GameRepo, Game }
import lila.memo.AsyncCache
import lila.tournament.TournamentRepo
import lila.user.User
private[chat] final class Namer(getUsername: String Fu[String]) {
def chat(c: Chat): Fu[NamedChat] =
chans(c.chans, c.user) map { NamedChat(c, _) }
def chan(c: Chan, as: User): Fu[NamedChan] =
chanCache(c -> as) map { NamedChan(c, _) }
def chans(cs: List[Chan], as: User): Fu[List[NamedChan]] =
Future.traverse(cs) { c chan(c, as) }
private val chanCache = AsyncCache(nameChan, timeToLive = 30 minutes)
private def nameChan(data: (Chan, User)): Fu[String] = data match {
case (static: StaticChan, _) fuccess(static.name)
case (c@GameWatcherChan(id), _)
GameRepo game id flatten s"No game for chan $c" flatMap nameWatcherChan
case (c@GamePlayerChan(id), user)
GameRepo game id flatten s"No game for chan $c" flatMap { game
(game player user).fold(nameWatcherChan(game)) { player
lila.game.Namer.player(game opponent player, false)(getUsername) map { opponent
s"Game: $opponent"
}
}
}
case (c@TournamentChan(id), _)
TournamentRepo nameById id flatten s"No tournament for chan $c" map {
_ + " tournament"
}
case (c, _) fuccess(c.toString)
}
private def nameWatcherChan(game: Game) =
lila.game.Namer.players(game, false)(getUsername) map { case (p1, p2) s"$p1 vs $p2" }
}

View file

@ -1,16 +1,20 @@
package lila.game
import chess.Clock
import lila.user.User
object Namer {
def player(player: Player, withRating: Boolean = true)(getUsername: String Fu[String]): Fu[String] =
def players(game: Game, withRatings: Boolean = true)(implicit getUsername: String Fu[String]): Fu[(String, String)] =
player(game.firstPlayer, withRatings) zip player(game.secondPlayer, withRatings)
def player(player: Player, withRating: Boolean = true)(implicit getUsername: String Fu[String]): Fu[String] =
player.aiLevel.fold(
player.userId.fold(fuccess(User.anonymous)) { id
getUsername(id) map { username
withRating.fold(
"%s (%s)".format(username, player.rating getOrElse "?"),
s"$username (${player.rating getOrElse "?"})",
username)
}
}) { level fuccess("A.I. level " + level) }

View file

@ -130,7 +130,7 @@ abstract class SocketActor[M <: SocketMember](uidTtl: Duration) extends Socket w
eject(uid)
members = members + (uid -> member)
setAlive(uid)
lilaBus.publish(SocketEnter(uid, member, self), 'socketDoor)
lilaBus.publish(SocketEnter(uid, member), 'socketDoor)
}
def setAlive(uid: String) { aliveUids put uid }

View file

@ -13,7 +13,7 @@ case class PingVersion(uid: String, version: Int)
case object Broom
case class Quit(uid: String)
case class SocketEnter[M <: SocketMember](uid: String, member: M, socket: ActorRef)
case class SocketEnter[M <: SocketMember](uid: String, member: M)
case class SocketLeave(uid: String)
case class LiveGames(uid: String, gameIds: List[String])

View file

@ -21,6 +21,9 @@ object TournamentRepo {
def byId(id: String): Fu[Option[Tournament]] = $find byId id
def nameById(id: String): Fu[Option[String]] =
$primitive.one($select(id), "name")(_.asOpt[String])
def createdById(id: String): Fu[Option[Created]] = byIdAs(id, asCreated)
def startedById(id: String): Fu[Option[Started]] = byIdAs(id, asStarted)

View file

@ -64,7 +64,8 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, scalastic)
)
lazy val chat = project("chat", Seq(common, db, hub, socket, user, security, pref)).settings(
lazy val chat = project("chat", Seq(
common, db, hub, socket, user, security, pref, game, tournament)).settings(
libraryDependencies ++= provided(
play.api, RM, PRM)
)

View file

@ -841,6 +841,7 @@ var storage = {
self.$lines = self.element.find('.lines');
self.$chans = self.element.find('.chans');
self.$form = self.element.find('.controls form');
self.$invite = self.$form.find('.invite');
self.$input = self.$form.find('input');
self.reload(_lc_);
@ -858,6 +859,9 @@ var storage = {
self.$chans.on('click', 'input', function(e) {
self._setActive($(this).attr('name'), $(this).prop('checked'));
});
$(window).resize(function() {
self._renderForm();
});
$('body').on('socket.open', function() {
lichess.socket.send('chat.register', {
@ -885,6 +889,9 @@ var storage = {
self.lines.push(l);
self.$lines.append(self._renderLine(l)).scrollTop(999999);
},
_disablePlaceholder: _.once(function() {
this.$input.attr('placeholder', '');
}),
_tell: function() {
var self = this;
if (!self.mainChan) return false;
@ -903,6 +910,7 @@ var storage = {
_setActive: function(chan, value, setMain) {
var self = this;
if (!self._exists(chan)) return;
self._disablePlaceholder();
setMain = setMain | false;
if (value) self.activeChans.push(chan);
else self.activeChans = _.without(self.activeChans, chan);
@ -919,6 +927,7 @@ var storage = {
// chan can be null, then there is no main chan
_setMain: function(chan) {
var self = this;
self._disablePlaceholder();
if (chan !== null && !self._isActive(chan)) {
self._setActive(chan, true, true);
} else {
@ -982,9 +991,14 @@ var storage = {
if (self.mainChan) {
var index = self._chanIndex(self.mainChan);
var mainChan = self.chans[self.mainChan];
self.$form.show().find('.mainChan')
self.$form.show();
self.$invite
.text(self.user + ' ● ' + mainChan.name)
.attr('class', 'mainChan ' + self._colorClass(index));
.attr('class', 'invite ' + self._colorClass(index));
self.$input.css({
width: (self.$form.width() - self.$invite.width() - 50) + 'px',
paddingLeft: (self.$invite.width() + 40) + 'px'
});
} else {
self.$form.hide();
}

View file

@ -455,12 +455,13 @@ div.footer div.right {
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
}
#chat .chan:hover, #chat .chan.main {
#chat .chan.main {
background: #030303;
}
#chat .chan > .name {
display: block;
margin-right: 20px;
white-space: nowrap;
}
#chat div.check {
float: right;
@ -509,12 +510,11 @@ div.footer div.right {
}
#chat > .room {
margin-left: 201px;
width: 100%;
}
#chat > .room > .lines {
height: 171px;
overflow: hidden;
overflow-y: auto;
/* overflow-y: auto; */
border-bottom: 1px solid #444;
}
#chat .line {
@ -522,6 +522,7 @@ div.footer div.right {
padding: 2px 10px;
position: relative;
opacity: 0.7;
color: #b0b0b0;
}
#chat .line.main {
opacity: 1;
@ -539,21 +540,19 @@ div.footer div.right {
display: inline-block;
}
#chat .controls > form {
width: 100%;
background: #1a1a1a;
display: none;
position: relative;
}
#chat .controls > form .mainChan {
#chat .controls > form .invite {
padding: 0 10px;
display: block;
float: left;
display: inline-block;
height: 28px;
line-height: 28px;
position: relative;
margin-right: 12px;
background: #0a0a0a;
}
#chat .controls > form .mainChan:after {
#chat .controls > form .invite:after {
content:"";
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
@ -563,11 +562,16 @@ div.footer div.right {
top: 0;
}
#chat .controls > form .input {
width: 80%;
position: absolute;
top: 0;
left: 0;
height: 20px;
padding: 4px 10px;
border: none;
background: none;
}
#chat .controls > form .input:focus {
background: #2a2a2a;
color: #d0d0d0;
}
#chat .color0 {
/* green */