in-game tournament chat POC

pull/3468/head
Thibault Duplessis 2017-08-17 17:07:43 -05:00
parent 2b9c7c1dab
commit 5a22a3abe9
21 changed files with 175 additions and 161 deletions

View File

@ -5,12 +5,12 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
import lila.chat.Chat
import lila.common.PimpedJson._
import lila.common.{ HTTPRequest, ApiVersion }
import lila.game.{ Pov, GameRepo, Game => GameModel, PgnDump }
import lila.tournament.MiniStanding
import lila.tournament.TourMiniView
import lila.user.{ User => UserModel }
import lila.chat.Chat
import views._
object Round extends LilaController with TheftPrevention {
@ -194,10 +194,8 @@ object Round extends LilaController with TheftPrevention {
) map NoCache
}
private def myTour(tourId: Option[String], withStanding: Boolean)(implicit ctx: Context): Fu[Option[MiniStanding]] =
tourId ?? { tid =>
Env.tournament.api.miniStanding(tid, ctx.userId, withStanding)
}
private def myTour(tourId: Option[String], withTop: Boolean): Fu[Option[TourMiniView]] =
tourId ?? { Env.tournament.api.miniView(_, withTop) }
private[controllers] def getWatcherChat(game: GameModel)(implicit ctx: Context): Fu[Option[lila.chat.UserChat.Mine]] = ctx.noKid ?? {
Env.chat.api.userChat.findMineIf(Chat.Id(s"${game.id}/w"), ctx.me, !game.justCreated) flatMap { chat =>
@ -234,12 +232,17 @@ object Round extends LilaController with TheftPrevention {
}
}
def sidesWatcher(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color)) { sides(_, false) }
}
def sidesPlayer(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color)) { sides(_, true) }
def sides(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
(pov.game.tournamentId ?? lila.tournament.TournamentRepo.byId) zip
(pov.game.simulId ?? Env.simul.repo.find) zip
GameRepo.initialFen(pov.game) zip
Env.game.crosstableApi.withMatchup(pov.game) zip
Env.bookmark.api.exists(pov.game, ctx.me) map {
case tour ~ simul ~ initialFen ~ crosstable ~ bookmarked =>
Ok(html.game.sides(pov, initialFen, tour, crosstable, simul, bookmarked = bookmarked))
}
}
}
def writeNote(gameId: String) = AuthBody { implicit ctx => me =>
@ -258,16 +261,6 @@ object Round extends LilaController with TheftPrevention {
}
}
private def sides(pov: Pov, isPlayer: Boolean)(implicit ctx: Context) =
myTour(pov.game.tournamentId, isPlayer) zip
(pov.game.simulId ?? Env.simul.repo.find) zip
GameRepo.initialFen(pov.game) zip
Env.game.crosstableApi.withMatchup(pov.game) zip
Env.bookmark.api.exists(pov.game, ctx.me) map {
case tour ~ simul ~ initialFen ~ crosstable ~ bookmarked =>
Ok(html.game.sides(pov, initialFen, tour, crosstable, simul, bookmarked = bookmarked))
}
def continue(id: String, mode: String) = Open { implicit ctx =>
OptionResult(GameRepo game id) { game =>
Redirect("%s?fen=%s#%s".format(

View File

@ -1,4 +1,4 @@
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], userTv: Option[User] = None, bookmarked: Boolean)(implicit ctx: Context)
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.Tournament], simul: Option[lila.simul.Simul], userTv: Option[User] = None, bookmarked: Boolean)(implicit ctx: Context)
@import pov._
@ -76,11 +76,11 @@
</div>
}
@tour.map { m =>
@tour.map { t =>
<div class="game_tournament side_box no_padding scroll-shadow-soft">
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(m.tour.id)">@m.tour.fullName</a></p>
<div class="clock" data-time="@m.tour.secondsToFinish">
<div class="time">@m.tour.clockStatus</div>
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(t.id)">@t.fullName</a></p>
<div class="clock" data-time="@t.secondsToFinish">
<div class="time">@t.clockStatus</div>
</div>
</div>
}.getOrElse {

View File

@ -1,4 +1,4 @@
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.MiniStanding], cross: Option[lila.game.Crosstable.WithMatchup], simul: Option[lila.simul.Simul], userTv: Option[User] = None, bookmarked: Boolean)(implicit ctx: Context)
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable.WithMatchup], simul: Option[lila.simul.Simul], userTv: Option[User] = None, bookmarked: Boolean)(implicit ctx: Context)
<div class="sides">
@side(pov, initialFen, tour, simul, userTv, bookmarked = bookmarked)

View File

@ -1,4 +1,4 @@
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable.WithMatchup], playing: List[Pov], chatOption: Option[lila.chat.Chat.GameOrEvent], bookmarked: Boolean)(implicit ctx: Context)
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.TourMiniView], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable.WithMatchup], playing: List[Pov], chatOption: Option[lila.chat.Chat.GameOrEvent], bookmarked: Boolean)(implicit ctx: Context)
@import pov._
@ -13,7 +13,7 @@ LichessRound.boot({
data: @Html(J.stringify(data)),
i18n: @jsI18n(pov.game),
userId: @jsUserId,
tour: @jsOrNull(tour.map(m => lila.tournament.JsonView.miniStanding(m, lightUser))),
tour: @jsOrNull(tour.flatMap(_.top).map(lila.tournament.JsonView.top(_, lightUser))),
chat: @jsOrNull(chatOption.map(_.either).map {
case Left(c) => {
chat.ChatJsData.restricted(c, name = trans.chatRoom.txt(), timeout = false, withNote = true, public = false)
@ -33,7 +33,7 @@ chat.ChatJsData.json(c.chat, name = trans.chatRoom.txt(), timeout = c.timeout, p
@round.layout(
title = title,
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, simul, bookmarked = bookmarked),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour.map(_.tour), simul, bookmarked = bookmarked),
chat = chatOption.isDefined.option(chat.dom()),
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View File

@ -1,4 +1,4 @@
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable.WithMatchup], userTv: Option[User] = None, chatOption: Option[lila.chat.UserChat.Mine], bookmarked: Boolean)(implicit ctx: Context)
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.TourMiniView], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable.WithMatchup], userTv: Option[User] = None, chatOption: Option[lila.chat.UserChat.Mine], bookmarked: Boolean)(implicit ctx: Context)
@title = @{ s"${gameVsText(pov.game, withRatings = true)} in ${pov.gameId}" }
@ -24,7 +24,7 @@ chat.ChatJsData.json(c.chat, name = trans.spectatorRoom.txt(), timeout = c.timeo
@round.layout(
title = title,
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, simul = simul, userTv = userTv, bookmarked = bookmarked),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour.map(_.tour), simul = simul, userTv = userTv, bookmarked = bookmarked),
chat = chat.dom().some,
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View File

@ -183,8 +183,7 @@ GET /$gameId<\w{8}>/$color<white|black> controllers.Round.watcher(gameI
GET /$fullId<\w{12}> controllers.Round.player(fullId: String)
GET /$gameId<\w{8}>/$color<white|black>/socket controllers.Round.websocketWatcher(gameId: String, color: String)
GET /$fullId<\w{12}>/socket/v:apiVersion controllers.Round.websocketPlayer(fullId: String, apiVersion: Int)
GET /$gameId<\w{8}>/$color<white|black>/sides/watcher controllers.Round.sidesWatcher(gameId: String, color: String)
GET /$gameId<\w{8}>/$color<white|black>/sides/player controllers.Round.sidesPlayer(gameId: String, color: String)
GET /$gameId<\w{8}>/$color<white|black>/sides controllers.Round.sides(gameId: String, color: String)
GET /$gameId<\w{8}>/others controllers.Round.others(gameId: String)
GET /$gameId<\w{8}>/continue/:mode controllers.Round.continue(gameId: String, mode: String)
GET /$gameId<\w{8}>/note controllers.Round.readNote(gameId: String)

View File

@ -5,7 +5,7 @@ import reactivemongo.bson._
import lila.db.dsl._
import lila.game.{ Pov, GameRepo }
import lila.tournament.{ Tournament, RankedPlayer }
import lila.tournament.{ Tournament, TournamentTop }
import lila.user.User
final class IrwinApi(
@ -91,10 +91,10 @@ final class IrwinApi(
}
}
private[irwin] def fromTournamentLeaders(leaders: Map[Tournament, List[RankedPlayer]]): Funit =
private[irwin] def fromTournamentLeaders(leaders: Map[Tournament, TournamentTop]): Funit =
lila.common.Future.applySequentially(leaders.toList) {
case (tour, rps) =>
val userIds = rps.filter(_.rank <= tour.nbPlayers * 2 / 100).map(_.player.userId)
case (tour, top) =>
val userIds = top.value.zipWithIndex.filter(_._2 <= tour.nbPlayers * 2 / 100).map(_._1.userId)
lila.common.Future.applySequentially(userIds) { userId =>
insert(userId, _.Tournament, none)
}

View File

@ -17,19 +17,19 @@ final class Messenger(val chat: ActorSelection) {
if (game.nonAi) chat ! SystemTalk(Chat.Id(game.id), translated)
}
def systemForOwners(gameId: String, message: SelectI18nKey, args: Any*) {
def systemForOwners(gameId: Game.ID, message: SelectI18nKey, args: Any*) {
val translated = message(I18nKeys).literalTxtTo(enLang, args)
chat ! SystemTalk(Chat.Id(gameId), translated)
}
def watcher(gameId: String, member: Member, text: String) =
def watcher(gameId: Game.ID, member: Member, text: String) =
member.userId foreach { userId =>
chat ! UserTalk(Chat.Id(watcherId(gameId)), userId, text)
}
private val whisperCommands = List("/whisper ", "/w ")
def owner(gameId: String, member: Member, text: String) = (member.userId match {
def owner(gameId: Game.ID, member: Member, text: String) = (member.userId match {
case Some(userId) =>
whisperCommands.collectFirst {
case command if text startsWith command =>
@ -42,5 +42,5 @@ final class Messenger(val chat: ActorSelection) {
PlayerTalk(Chat.Id(gameId), member.color.white, text).some
}) foreach chat.!
private def watcherId(gameId: String) = s"$gameId/w"
private def watcherId(gameId: Game.ID) = s"$gameId/w"
}

View File

@ -9,9 +9,10 @@ import play.api.libs.iteratee._
import play.api.libs.json._
import actorApi._
import lila.chat.Chat
import lila.common.LightUser
import lila.game.actorApi.{ StartGame, UserStartGame }
import lila.game.Event
import lila.game.{ Game, GameRepo, Event }
import lila.hub.actorApi.Deploy
import lila.hub.actorApi.game.ChangeFeatured
import lila.hub.actorApi.round.IsOnGame
@ -22,7 +23,7 @@ import lila.socket.actorApi.{ Connected => _, _ }
import makeTimeout.short
private[round] final class Socket(
gameId: String,
gameId: Game.ID,
history: History,
lightUser: LightUser.Getter,
uidTimeout: Duration,
@ -71,10 +72,15 @@ private[round] final class Socket(
private val whitePlayer = new Player(White)
private val blackPlayer = new Player(Black)
private var chatIds = Socket.ChatIds(
priv = Chat.Id(gameId),
pub = Chat.Id(s"$gameId/w")
)
override def preStart() {
super.preStart()
refreshSubscriptions
lila.game.GameRepo game gameId map SetGame.apply pipeTo self
GameRepo game gameId map SetGame.apply pipeTo self
}
override def postStop() {
@ -87,7 +93,12 @@ private[round] final class Socket(
members.flatMap { case (_, m) => m.userTv }.toList.distinct foreach { userId =>
lilaBus.subscribe(self, Symbol(s"userStartGame:$userId"))
}
lilaBus.subscribe(self, Symbol(s"chat-$gameId"), Symbol(s"chat-$gameId/w"))
refreshChatSubscriptions
}
private def refreshChatSubscriptions {
println(s"$gameId refreshChatSubscriptions $chatIds")
lilaBus.subscribe(self, Symbol(s"chat-${chatIds.priv}"), Symbol(s"chat-${chatIds.pub}"))
}
def receiveSpecific = ({
@ -97,6 +108,10 @@ private[round] final class Socket(
whitePlayer.userId = game.player(White).userId
blackPlayer.userId = game.player(Black).userId
mightBeSimul = game.isSimul
game.tournamentId orElse game.simulId map Chat.Id.apply foreach { chatId =>
chatIds = chatIds.copy(priv = chatId)
refreshChatSubscriptions
}
// from lilaBus 'startGame
// sets definitive user ids
@ -156,7 +171,7 @@ private[round] final class Socket(
case eventList: EventList => notify(eventList.events)
case lila.chat.actorApi.ChatLine(chatId, line) => notify(List(line match {
case l: lila.chat.UserLine => Event.UserMessage(l, chatId.value endsWith "/w")
case l: lila.chat.UserLine => Event.UserMessage(l, chatId == chatIds.pub)
case l: lila.chat.PlayerLine => Event.PlayerMessage(l)
}))
@ -258,3 +273,13 @@ private[round] final class Socket(
effect(color.fold(whitePlayer, blackPlayer))
}
}
object Socket {
case class ChatIds(priv: Chat.Id, pub: Chat.Id) {
def update(g: Game) =
g.tournamentId.map { id => copy(priv = Chat.Id(id)) } orElse
g.simulId.map { id => copy(priv = Chat.Id(id)) } getOrElse
this
}
}

View File

@ -13,7 +13,7 @@ import play.api.libs.json.{ JsObject, Json }
import actorApi._, round._
import lila.common.PimpedJson._
import lila.common.IpAddress
import lila.game.{ Pov, PovRef, GameRepo }
import lila.game.{ Pov, PovRef, GameRepo, Game }
import lila.hub.actorApi.map._
import lila.hub.actorApi.round.Berserk
import lila.socket.actorApi.{ Connected => _, _ }
@ -34,7 +34,8 @@ private[round] final class SocketHandler(
) {
private def controller(
gameId: String,
gameId: Game.ID,
chatId: Option[Chat.Id], // if using a non-game chat (tournament, simul, ...)
socket: ActorRef,
uid: Uid,
ref: PovRef,
@ -90,7 +91,7 @@ private[round] final class SocketHandler(
case ("outoftime", _) => send(QuietFlag) // mobile app BC
case ("flag", o) => clientFlag(o, playerId.some) foreach send
case ("bye2", _) => socket ! Bye(ref.color)
case ("talk", o) => o str "d" foreach { messenger.owner(gameId, member, _) }
case ("talk", o) if chatId.isEmpty => o str "d" foreach { messenger.owner(gameId, member, _) }
case ("hold", o) => for {
d o obj "d"
mean d int "mean"
@ -105,7 +106,7 @@ private[round] final class SocketHandler(
name d str "n"
} selfReport(member.userId, member.ip, s"$gameId$playerId", name)
}: Handler.Controller) orElse lila.chat.Socket.in(
chatId = Chat.Id(gameId),
chatId = chatId | Chat.Id(gameId),
member = member,
socket = socket,
chat = messenger.chat
@ -149,6 +150,10 @@ private[round] final class SocketHandler(
ip = ip,
userTv = userTv
)
// non-game chat, for tournament or simul games; only for players
val publicChatId = playerId.isDefined ?? {
pov.game.tournamentId.map(Chat.Id) orElse pov.game.simulId.map(Chat.Id)
}
socketHub ? Get(pov.gameId) mapTo manifest[ActorRef] flatMap { socket =>
Handler(hub, socket, uid, join) {
case Connected(enum, member) =>
@ -158,7 +163,7 @@ private[round] final class SocketHandler(
// register to the tournament standing channel when playing a tournament game
if (playerId.isDefined && pov.game.isTournament)
hub.channel.tournamentStanding ! lila.socket.Channel.Sub(member)
(controller(pov.gameId, socket, uid, pov.ref, member, user), enum, member)
(controller(pov.gameId, publicChatId, socket, uid, pov.ref, member, user), enum, member)
}
}
}

View File

@ -324,17 +324,15 @@ final class JsonView(
object JsonView {
def miniStanding(m: MiniStanding, getLightUser: LightUser.GetterSync): JsObject = Json.obj(
"standing" -> (~m.standing).map {
case RankedPlayer(rank, player) =>
val light = getLightUser(player.userId)
Json.obj(
"name" -> light.fold(player.userId)(_.name),
"rank" -> rank,
"score" -> player.score
).add("title" -> light.flatMap(_.title))
.add("fire" -> scala.util.Random.nextBoolean) //player.fire)
.add("withdraw" -> scala.util.Random.nextBoolean) //player.withdraw)
def top(t: TournamentTop, getLightUser: LightUser.GetterSync): JsObject = Json.obj(
"top" -> t.value.map { p =>
val light = getLightUser(p.userId)
Json.obj(
"n" -> light.fold(p.userId)(_.name),
"s" -> p.score
).add("t" -> light.flatMap(_.title))
.add("f" -> p.fire)
.add("w" -> p.withdraw)
}
)

View File

@ -329,26 +329,19 @@ final class TournamentApi(
}
}
private val miniStandingCache = asyncCache.multi[String, List[RankedPlayer]](
name = "tournament.miniStanding",
id => PlayerRepo.bestByTourWithRank(id, 20),
private val tournamentTopCache = asyncCache.multi[Tournament.ID, TournamentTop](
name = "tournament.top",
id => PlayerRepo.bestByTour(id, 20) map TournamentTop.apply,
expireAfter = _.ExpireAfterWrite(3 second)
)
def miniStanding(tourId: String, withStanding: Boolean): Fu[Option[MiniStanding]] =
def tournamentTop(tourId: Tournament.ID): Fu[TournamentTop] =
tournamentTopCache get tourId
def miniView(tourId: Tournament.ID, withTop: Boolean): Fu[Option[TourMiniView]] =
TournamentRepo byId tourId flatMap {
_ ?? { tour =>
if (withStanding) miniStandingCache get tour.id map { rps =>
MiniStanding(tour, rps.some).some
}
else fuccess(MiniStanding(tour, none).some)
}
}
def miniStanding(tourId: String, userId: Option[String], withStanding: Boolean): Fu[Option[MiniStanding]] =
userId ?? { uid =>
PlayerRepo.exists(tourId, uid) flatMap {
_ ?? miniStanding(tourId, withStanding)
withTop ?? { tournamentTop(tour.id) map some } map { TourMiniView(tour, _).some }
}
}
@ -377,10 +370,10 @@ final class TournamentApi(
}
}
def allCurrentLeadersInStandard: Fu[Map[Tournament, List[RankedPlayer]]] =
def allCurrentLeadersInStandard: Fu[Map[Tournament, TournamentTop]] =
TournamentRepo.standardPublicStartedFromSecondary.flatMap { tours =>
tours.map { tour =>
miniStandingCache get tour.id map (tour -> _)
tournamentTop(tour.id) map (tour -> _)
}.sequenceFu.map(_.toMap)
}
@ -431,12 +424,10 @@ final class TournamentApi(
import lila.hub.EarlyMultiThrottler
private def publishNow(tourId: Tournament.ID) = miniStanding(tourId, true) map {
_ ?? { m =>
standingChannel ! lila.socket.Channel.Publish(
lila.socket.Socket.makeMessage("tourStanding", JsonView.miniStanding(m, lightUserApi.sync))
)
}
private def publishNow(tourId: Tournament.ID) = tournamentTop(tourId) map { top =>
standingChannel ! lila.socket.Channel.Publish(
lila.socket.Socket.makeMessage("tourStanding", JsonView.top(top, lightUserApi.sync))
)
}
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))

View File

@ -1,9 +1,8 @@
package lila.tournament
case class MiniStanding(
tour: Tournament,
standing: Option[RankedPlayers]
)
case class TournamentTop(value: List[Player]) extends AnyVal
case class TourMiniView(tour: Tournament, top: Option[TournamentTop])
case class PlayerInfo(rank: Int, withdraw: Boolean) {
def page = {

View File

@ -14,8 +14,7 @@ export default function(opts: ChatOpts, redraw: Redraw): Ctrl {
let moderation: ModerationCtrl | undefined;
const vm: ViewModel = {
// tab: 'discussion',
tab: 'tourStanding',
tab: 'discussion',
enabled: opts.alwaysEnabled || !li.storage.get('nochat'),
placeholderKey: 'talkInChat',
loading: false,
@ -121,6 +120,7 @@ export default function(opts: ChatOpts, redraw: Redraw): Ctrl {
if (!v) li.storage.set('nochat', '1');
else li.storage.remove('nochat');
redraw();
}
},
redraw
};
};

View File

@ -1,4 +1,5 @@
import { VNode } from 'snabbdom/vnode'
import { PresetCtrl } from './preset'
export interface ChatOpts {
data: ChatData
@ -62,6 +63,7 @@ export interface Ctrl {
setTab(tab: Tab): void
setEnabled(v: boolean): void
plugin?: ChatPlugin
redraw: Redraw
}
export interface ViewModel {
@ -73,33 +75,6 @@ export interface ViewModel {
writeable: boolean
}
export interface PresetCtrl {
group(): string | undefined
said(): string[]
setGroup(group: string): void
post(preset: Preset): void
}
export type PresetKey = string
export type PresetText = string
export interface Preset {
key: PresetKey
text: PresetText
}
export interface PresetGroups {
start: Preset[]
end: Preset[]
[key: string]: Preset[]
}
export interface PresetOpts {
initialGroup?: string
redraw: Redraw
post(text: string): boolean
}
export interface NoteOpts {
id: string
trans: Trans

View File

@ -5,12 +5,13 @@ import { VNode } from 'snabbdom/vnode'
import makeCtrl from './ctrl';
import view from './view';
import { ChatOpts, Ctrl, PresetCtrl } from './interfaces'
import { ChatOpts, Ctrl } from './interfaces'
import { PresetCtrl } from './preset'
import klass from 'snabbdom/modules/class';
import attributes from 'snabbdom/modules/attributes';
export { ChatPlugin } from './interfaces';
export { Ctrl as ChatCtrl, ChatPlugin } from './interfaces';
export default function LichessChat(element: Element, opts: ChatOpts): {
preset: PresetCtrl
@ -37,7 +38,5 @@ export default function LichessChat(element: Element, opts: ChatOpts): {
return false;
});
return {
preset: ctrl.preset
};
return ctrl;
};

View File

@ -1,14 +1,33 @@
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { PresetCtrl, PresetOpts, Preset, PresetGroups } from './interfaces'
import { bind } from './util'
import { Redraw } from './interfaces'
function splitIt(s: string): Preset {
const parts = s.split('/');
return {
key: parts[0],
text: parts[1]
}
export interface PresetCtrl {
group(): string | undefined
said(): string[]
setGroup(group: string | undefined): void
post(preset: Preset): void
}
export type PresetKey = string
export type PresetText = string
export interface Preset {
key: PresetKey
text: PresetText
}
export interface PresetGroups {
start: Preset[]
end: Preset[]
[key: string]: Preset[]
}
export interface PresetOpts {
initialGroup?: string
redraw: Redraw
post(text: string): boolean
}
const groups: PresetGroups = {
@ -22,14 +41,14 @@ const groups: PresetGroups = {
export function presetCtrl(opts: PresetOpts): PresetCtrl {
let group = opts.initialGroup;
let group: string | undefined = opts.initialGroup;
let said: string[] = [];
return {
group: () => group,
said: () => said,
setGroup(p) {
setGroup(p: string | undefined) {
if (p !== group) {
group = p;
if (!p) said = [];
@ -66,3 +85,11 @@ export function presetView(ctrl: PresetCtrl): VNode | undefined {
}, p.key);
})) : undefined;
}
function splitIt(s: string): Preset {
const parts = s.split('/');
return {
key: parts[0],
text: parts[1]
}
}

View File

@ -10,8 +10,6 @@ export default function(ctrl: Ctrl): VNode {
const mod = ctrl.moderation();
console.log(ctrl);
return h('div#chat.side_box.mchat' + (ctrl.opts.alwaysEnabled ? '' : '.optional'), {
class: {
mod: !!mod

View File

@ -1,13 +1,14 @@
import { RoundOpts, RoundData } from './interfaces';
import { RoundApi, RoundMain } from './main';
import { tourStandingCtrl, TourStandingData } from './tourStanding';
import { ChatCtrl } from 'chat';
import { tourStandingCtrl, TourStandingCtrl, TourStandingData } from './tourStanding';
const li = window.lichess;
export default function(opts: RoundOpts, element: HTMLElement): void {
const data = opts.data;
li.openInMobileApp(data.game.id);
let round: RoundApi, chat: any;
let round: RoundApi, chat: ChatCtrl | undefined;
if (data.tournament) $('body').data('tournament-id', data.tournament.id);
li.socket = li.StrongSocket(
data.url.socket,
@ -30,7 +31,7 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
},
end() {
$.ajax({
url: '/' + (data.tv ? ['tv', data.tv.channel, data.game.id, data.player.color, 'sides'] : [data.game.id, data.player.color, 'sides', data.player.spectator ? 'watcher' : 'player']).join('/'),
url: [(data.tv ? '/tv/' + data.tv.channel : ''), data.game.id, data.player.color, 'sides'].join('/'),
success: function(html) {
const $html = $(html);
$('#site_header div.side').replaceWith($html.find('.side'));
@ -41,7 +42,8 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
});
},
tourStanding(data: TourStandingData) {
console.log(data);
if (opts.chat && opts.chat.plugin) (opts.chat.plugin as TourStandingCtrl).set(data);
chat && chat.redraw();
}
}
});
@ -54,10 +56,10 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
});
};
function getPresetGroup(d: RoundData) {
if (d.player.spectator) return null;
if (d.player.spectator) return;
if (d.steps.length < 4) return 'start';
else if (d.game.status.id >= 30) return 'end';
return null;
return;
};
opts.element = element.querySelector('.round') as HTMLElement;
opts.socketSend = li.socket.send;
@ -72,7 +74,6 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
if (opts.chat) {
if (opts.tour) {
opts.chat.plugin = tourStandingCtrl(opts.tour, opts.i18n.standing);
console.log(opts);
opts.chat.alwaysEnabled = true;
} else {
opts.chat.preset = getPresetGroup(opts.data);

View File

@ -86,7 +86,7 @@ export interface RoundOpts {
}
export interface Chat {
preset: 'start' | 'end' | null;
preset: 'start' | 'end' | undefined;
parseMoves?: boolean;
plugin?: ChatPlugin;
alwaysEnabled: boolean;

View File

@ -3,39 +3,43 @@ import { VNode } from 'snabbdom/vnode'
import { ChatPlugin } from 'chat'
import { justIcon } from './util'
export interface TourStandingCtrl extends ChatPlugin {
set(data: TourStandingData): void;
}
export interface TourStandingData {
standing: RankedPlayer[]
top: TourPlayer[];
}
interface RankedPlayer {
name: string;
rank: number;
score: number;
title?: string;
fire: boolean;
withdraw: boolean;
interface TourPlayer {
n: string; // name
s: number; // score
t?: string; // title
f: boolean; // fire
w: boolean; // withdraw
}
export function tourStandingCtrl(data: TourStandingData, name: string): ChatPlugin {
export function tourStandingCtrl(data: TourStandingData, name: string): TourStandingCtrl {
return {
set(d: TourStandingData) { data = d },
tab: {
key: 'tourStanding',
name: name
},
view(): VNode {
return h('table.slist',
h('tbody', data.standing.map(p => {
return h('tr.' + p.name, [
h('tbody', data.top.map((p: TourPlayer, i: number) => {
return h('tr.' + p.n, [
h('td.name', [
p.withdraw ? h('span', justIcon('Z')) : h('span.rank', '' + p.rank),
p.w ? h('span', justIcon('Z')) : h('span.rank', '' + (i + 1)),
h('a.user_link.ulpt', {
attrs: { href: `/@/${p.name}` }
}, (p.title ? p.title + ' ' : '') + p.name)
attrs: { href: `/@/${p.n}` }
}, (p.t ? p.t + ' ' : '') + p.n)
]),
h('td.total', p.fire ? {
h('td.total', p.f ? {
class: { 'is-gold': true },
attrs: { 'data-icon': 'Q' }
} : {}, '' + p.score)
} : {}, '' + p.s)
])
}))
);