in-game tournament chat WIP

pull/3468/head
Thibault Duplessis 2017-08-17 11:52:07 -05:00
parent 7f3cdc39f3
commit e2e1b524fc
21 changed files with 141 additions and 88 deletions

View File

@ -118,13 +118,6 @@ object Tournament extends LilaController {
}
}
def gameStanding(id: String) = Open { implicit ctx =>
env.api.miniStanding(id, true) map {
case Some(m) if !m.tour.isCreated => Ok(html.tournament.gameStanding(m))
case _ => NotFound
}
}
def userGameNbMini(id: String, user: String, nb: Int) = Open { implicit ctx =>
withUserGameNb(id, user, nb) { pov =>
Ok(html.tournament.miniGame(pov))

View File

@ -71,6 +71,8 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
def lightUser(userId: String): Option[LightUser] = Env.user lightUserSync userId
def lightUser(userId: Option[String]): Option[LightUser] = userId flatMap lightUser
// def lightUserSync: LightUser.SyncGetter(userId: String): Option[LightUser] = Env.user lightUserSync userId
def usernameOrId(userId: String) = lightUser(userId).fold(userId)(_.titleName)
def usernameOrAnon(userId: Option[String]) = lightUser(userId).fold(User.anonymous)(_.titleName)

View File

@ -82,7 +82,6 @@
<div class="clock" data-time="@m.tour.secondsToFinish">
<div class="time">@m.tour.clockStatus</div>
</div>
@tournament.gameStanding(m)
</div>
}.getOrElse {
@game.tournamentId.map { tourId =>

View File

@ -1,4 +1,4 @@
@()(implicit ctx: Context)
@(g: Game)(implicit ctx: Context)
@Html(J.stringify(i18nJsObject(
trans.flipBoard,
trans.aiNameLevelAiLevel,
@ -35,7 +35,6 @@ trans.blackIsVictorious,
trans.kingInTheCenter,
trans.threeChecks,
trans.variantEnding,
trans.backToTournament,
trans.withdraw,
trans.rematch,
trans.rematchOfferSent,
@ -45,7 +44,6 @@ trans.cancelRematchOffer,
trans.newOpponent,
trans.moveConfirmation,
trans.viewRematch,
trans.viewTournament,
trans.whitePlays,
trans.blackPlays,
trans.giveNbSeconds,
@ -56,4 +54,8 @@ trans.yourOpponentWantsToPlayANewGameWithYou,
trans.oneDay,
trans.nbDays,
trans.nbHours
)))
) ++ g.isTournament.fold(i18nJsObject(
trans.backToTournament,
trans.viewTournament,
trans.standing
), J.obj())))

View File

@ -11,8 +11,9 @@ window.customWS = true;
window.onload = function() {
LichessRound.boot({
data: @Html(J.stringify(data)),
i18n: @jsI18n(),
i18n: @jsI18n(pov.game),
userId: @jsUserId,
tour: @jsOrNull(tour.map(m => lila.tournament.JsonView.miniStanding(m, lightUser))),
chat: @jsOrNull(chatOption.map(_.either).map {
case Left(c) => {
chat.ChatJsData.restricted(c, name = trans.chatRoom.txt(), timeout = false, withNote = true, public = false)

View File

@ -9,7 +9,7 @@ window.customWS = true;
window.onload = function() {
LichessRound.boot({
data: @Html(J.stringify(data)),
i18n: @jsI18n(),
i18n: @jsI18n(pov.game),
chat: @jsOrNull(chatOption map { c =>
chat.ChatJsData.json(c.chat, name = trans.spectatorRoom.txt(), timeout = c.timeout, withNote = ctx.isAuth, public = true)
})

View File

@ -1,28 +0,0 @@
@(m: lila.tournament.MiniStanding)(implicit ctx: Context)
@m.standing.map { standing =>
<table class="slist standing">
<tbody>
@standing.map {
case lila.tournament.RankedPlayer(rank, player) => {
<tr @if(ctx.userId.exists(player.is)) { class="me" }>
<td class="name">
@if(player.withdraw) {
<span data-icon="Z" title="Pause"></span>
} else {
@if(m.tour.isFinished && rank == 1) {
<span data-icon="g" title="@trans.winner()"></span>
} else {
<span class="rank">@rank</span>
}
}
@userInfosLink(player.userId, none, withOnline = false, withPowerTip = true)
</td>
<td class="total">
<strong@if(player.fire) { class="is-gold" data-icon="Q" }>@player.score</strong>
</td>
</tr>
}
}
</tbody>
</table>
}

View File

@ -8,7 +8,7 @@
window.onload = function() {
LichessRound.boot({
data: @Html(J.stringify(data)),
i18n: @round.jsI18n()
i18n: @round.jsI18n(pov.game)
}, document.getElementById('lichess'));
};
}

View File

@ -220,7 +220,6 @@ POST /tournament/new controllers.Tournament.create
GET /tournament/$id<\w{8}> controllers.Tournament.show(id: String)
GET /tournament/$id<\w{8}>/standing/:page controllers.Tournament.standing(id: String, page: Int)
GET /tournament/$id<\w{8}>/socket/v:apiVersion controllers.Tournament.websocket(id: String, apiVersion: Int)
GET /tournament/$id<\w{8}>/game-standing controllers.Tournament.gameStanding(id: String)
POST /tournament/$id<\w{8}>/join controllers.Tournament.join(id: String)
POST /tournament/$id<\w{8}>/withdraw controllers.Tournament.withdraw(id: String)
GET /tournament/$id<\w{8}>/mini/:user/:nb controllers.Tournament.userGameNbMini(id: String, user: String, nb: Int)

View File

@ -93,6 +93,7 @@ final class Env(
indexLeaderboard = leaderboardIndexer.indexOne _,
roundMap = roundMap,
asyncCache = asyncCache,
lightUserApi = lightUserApi,
standingChannel = standingChannel
)

View File

@ -7,6 +7,7 @@ import scala.concurrent.duration._
import chess.Clock.{ Config => TournamentClock }
import lila.common.PimpedJson._
import lila.common.LightUser
import lila.game.{ GameRepo, Pov }
import lila.quote.Quote.quoteWriter
import lila.rating.PerfType
@ -323,6 +324,20 @@ 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)
}
)
private def formatDate(date: DateTime) = ISODateTimeFormat.dateTime print date
private[tournament] def scheduleJson(s: Schedule) = Json.obj(

View File

@ -36,6 +36,7 @@ final class TournamentApi(
verify: Condition.Verify,
indexLeaderboard: Tournament => Funit,
asyncCache: lila.memo.AsyncCache.Builder,
lightUserApi: lila.user.LightUserApi,
standingChannel: ActorRef
) {
@ -330,7 +331,7 @@ final class TournamentApi(
private val miniStandingCache = asyncCache.multi[String, List[RankedPlayer]](
name = "tournament.miniStanding",
id => PlayerRepo.bestByTourWithRank(id, 30),
id => PlayerRepo.bestByTourWithRank(id, 20),
expireAfter = _.ExpireAfterWrite(3 second)
)
@ -430,10 +431,12 @@ final class TournamentApi(
import lila.hub.EarlyMultiThrottler
private def publishNow(tourId: Tournament.ID) = fuccess {
standingChannel ! lila.socket.Channel.Publish(
lila.socket.Socket.makeMessage("tournamentStanding", tourId)
)
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 val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))

View File

@ -938,39 +938,40 @@ div.game_tournament {
max-height: 300px;
overflow: hidden;
}
div.game_tournament:hover {
overflow-y: auto;
}
div.game_tournament .clock {
text-align: center;
font-size: 20px;
font-family: 'Roboto Mono', 'Roboto';
margin: 10px 0;
}
div.game_tournament table.standing {
div.tourStanding table {
border-bottom: none;
}
div.game_tournament td {
div.tourStanding:hover {
overflow-y: auto!important;
}
div.tourStanding td {
padding: 0 10px;
text-align: left;
line-height: 2em;
line-height: 1.8em;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
div.game_tournament table.slist td:first-child {
div.tourStanding .slist td:first-child {
padding-left: 10px;
}
div.game_tournament tr.me td:first-child {
border-left: 10px solid #d59120;
padding-left: 5px;
}
div.game_tournament td.name span {
div.tourStanding .name span {
display: inline-block;
width: 20px;
}
div.game_tournament td.total {
div.tourStanding .name span::before {
font-size: 0.8em;
opacity: 0.4;
}
div.tourStanding .total {
font-weight: bold;
text-align: right;
}
span.inline_userlist {

View File

@ -37,7 +37,7 @@ div.mchat .chat_tabs .tab.active {
div.mchat .chat_tabs .tab input {
cursor: pointer;
}
div.mchat .chat_tabs .tab.discussion {
div.mchat.optional .chat_tabs .tab.discussion {
display: flex;
justify-content: space-between;
align-items: center;

View File

@ -14,8 +14,9 @@ export default function(opts: ChatOpts, redraw: Redraw): Ctrl {
let moderation: ModerationCtrl | undefined;
const vm: ViewModel = {
tab: 'discussion',
enabled: !li.storage.get('nochat'),
// tab: 'discussion',
tab: 'tourStanding',
enabled: opts.alwaysEnabled || !li.storage.get('nochat'),
placeholderKey: 'talkInChat',
loading: false,
timeout: opts.timeout,
@ -105,14 +106,15 @@ export default function(opts: ChatOpts, redraw: Redraw): Ctrl {
opts,
vm,
setTab(t: Tab) {
vm.tab = t
redraw()
vm.tab = t;
redraw()
},
moderation: () => moderation,
note,
preset,
post,
trans,
plugin: opts.plugin,
setEnabled(v: boolean) {
vm.enabled = v;
emitEnabled();

View File

@ -1,3 +1,5 @@
import { VNode } from 'snabbdom/vnode'
export interface ChatOpts {
data: ChatData
writeable: boolean
@ -11,12 +13,16 @@ export interface ChatOpts {
preset?: string
noteId?: string
loadCss: (url: string) => void
extra?: ExtraTab
plugin?: ChatPlugin
alwaysEnabled: boolean;
}
export interface ExtraTab {
name: string; // i18n key
content: string; // HTML
export interface ChatPlugin {
tab: {
key: string;
name: string;
}
view(): VNode;
}
export interface ChatData {
@ -42,7 +48,7 @@ export interface Permissions {
shadowban?: boolean
}
export type Tab = 'discussion' | 'note' | 'extra';
export type Tab = string;
export interface Ctrl {
data: ChatData
@ -55,6 +61,7 @@ export interface Ctrl {
trans: Trans
setTab(tab: Tab): void
setEnabled(v: boolean): void
plugin?: ChatPlugin
}
export interface ViewModel {

View File

@ -10,6 +10,8 @@ import { ChatOpts, Ctrl, PresetCtrl } from './interfaces'
import klass from 'snabbdom/modules/class';
import attributes from 'snabbdom/modules/attributes';
export { ChatPlugin } from './interfaces';
export default function LichessChat(element: Element, opts: ChatOpts): {
preset: PresetCtrl
} {

View File

@ -10,7 +10,9 @@ export default function(ctrl: Ctrl): VNode {
const mod = ctrl.moderation();
return h('div#chat.side_box.mchat', {
console.log(ctrl);
return h('div#chat.side_box.mchat' + (ctrl.opts.alwaysEnabled ? '' : '.optional'), {
class: {
mod: !!mod
}
@ -21,12 +23,12 @@ function normalView(ctrl: Ctrl) {
const active = ctrl.vm.tab;
const tabs: Array<Tab> = ['discussion'];
if (ctrl.note) tabs.push('note');
if (ctrl.opts.extra) tabs.push('extra');
if (ctrl.plugin) tabs.push(ctrl.plugin.tab.key);
return [
h('div.chat_tabs.nb_' + tabs.length, tabs.map(t => renderTab(ctrl, t, active))),
h('div.content.' + active,
(active === 'note' && ctrl.note) ? [noteView(ctrl.note)] : (
active === 'extra' ? [extraView(ctrl)] : discussionView(ctrl)
ctrl.plugin && active === ctrl.plugin.tab.key ? [ctrl.plugin.view()] : discussionView(ctrl)
))
]
}
@ -41,7 +43,7 @@ function renderTab(ctrl: Ctrl, tab: Tab, active: Tab) {
function tabName(ctrl: Ctrl, tab: Tab) {
if (tab === 'discussion') return [
h('span', ctrl.data.name),
h('input.toggle_chat', {
ctrl.opts.alwaysEnabled ? undefined : h('input.toggle_chat', {
attrs: {
type: 'checkbox',
title: ctrl.trans.noarg('toggleTheChat'),
@ -53,5 +55,5 @@ function tabName(ctrl: Ctrl, tab: Tab) {
})
];
if (tab === 'note') return ctrl.trans.noarg('notes');
if (tab === 'extra') return ctrl.trans.noarg(ctrl.opts.extra!.name);
if (ctrl.plugin && tab === ctrl.plugin.tab.key) return ctrl.plugin.tab.name;
}

View File

@ -1,5 +1,6 @@
import { RoundOpts, RoundData } from './interfaces';
import { RoundApi, RoundMain } from './main';
import { tourStandingCtrl, TourStandingData } from './tourStanding';
const li = window.lichess;
@ -39,14 +40,8 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
}
});
},
tournamentStanding(id: string) {
if (data.tournament && id === data.tournament.id) $.ajax({
url: '/tournament/' + id + '/game-standing',
success: function(html) {
$('#site_header div.game_tournament').replaceWith(html);
startTournamentClock();
}
});
tourStanding(data: TourStandingData) {
console.log(data);
}
}
});
@ -66,7 +61,7 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
};
opts.element = element.querySelector('.round') as HTMLElement;
opts.socketSend = li.socket.send;
opts.onChange = (d: RoundData) => {
if (!opts.tour) opts.onChange = (d: RoundData) => {
if (chat) chat.preset.setGroup(getPresetGroup(d));
};
opts.crosstableEl = element.querySelector('.crosstable') as HTMLElement;
@ -75,8 +70,14 @@ export default function(opts: RoundOpts, element: HTMLElement): void {
function letsGo() {
round = (window['LichessRound'] as RoundMain).app(opts);
if (opts.chat) {
opts.chat.preset = getPresetGroup(opts.data);
opts.chat.parseMoves = true;
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);
opts.chat.parseMoves = true;
}
li.makeChat('chat', opts.chat, function(c) {
chat = c;
});

View File

@ -2,6 +2,8 @@ import { VNode } from 'snabbdom/vnode';
import { GameData, Status } from 'game';
import { ClockData, Seconds, Centis } from './clock/clockCtrl';
import { CorresClockData } from './corresClock/corresClockCtrl';
import { TourStandingData } from './tourStanding';
import { ChatPlugin } from 'chat';
import * as cg from 'chessground/types';
export type MaybeVNode = VNode | null | undefined;
@ -80,11 +82,14 @@ export interface RoundOpts {
crosstableEl: HTMLElement;
i18n: any;
chat?: Chat;
tour?: TourStandingData;
}
export interface Chat {
preset: 'start' | 'end' | null;
parseMoves?: boolean;
plugin?: ChatPlugin;
alwaysEnabled: boolean;
}
export interface Step {

View File

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