diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala index ddf071371c..14e4cb5a60 100644 --- a/app/controllers/Tournament.scala +++ b/app/controllers/Tournament.scala @@ -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)) diff --git a/app/templating/UserHelper.scala b/app/templating/UserHelper.scala index ff776b41ee..288287e098 100644 --- a/app/templating/UserHelper.scala +++ b/app/templating/UserHelper.scala @@ -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) diff --git a/app/views/game/side.scala.html b/app/views/game/side.scala.html index 0e6e73ae62..d467739018 100644 --- a/app/views/game/side.scala.html +++ b/app/views/game/side.scala.html @@ -82,7 +82,6 @@
@m.tour.clockStatus
- @tournament.gameStanding(m) }.getOrElse { @game.tournamentId.map { tourId => diff --git a/app/views/round/jsI18n.scala.html b/app/views/round/jsI18n.scala.html index f6e76395d8..fcbbe08443 100644 --- a/app/views/round/jsI18n.scala.html +++ b/app/views/round/jsI18n.scala.html @@ -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()))) diff --git a/app/views/round/player.scala.html b/app/views/round/player.scala.html index ff4744f767..1e3e670931 100644 --- a/app/views/round/player.scala.html +++ b/app/views/round/player.scala.html @@ -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) diff --git a/app/views/round/watcher.scala.html b/app/views/round/watcher.scala.html index 5fea4a3c95..7b6e168c15 100644 --- a/app/views/round/watcher.scala.html +++ b/app/views/round/watcher.scala.html @@ -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) }) diff --git a/app/views/tournament/gameStanding.scala.html b/app/views/tournament/gameStanding.scala.html deleted file mode 100644 index abbd9bc4a8..0000000000 --- a/app/views/tournament/gameStanding.scala.html +++ /dev/null @@ -1,28 +0,0 @@ -@(m: lila.tournament.MiniStanding)(implicit ctx: Context) -@m.standing.map { standing => - - - @standing.map { - case lila.tournament.RankedPlayer(rank, player) => { - - - - - } - } - -
- @if(player.withdraw) { - - } else { - @if(m.tour.isFinished && rank == 1) { - - } else { - @rank - } - } - @userInfosLink(player.userId, none, withOnline = false, withPowerTip = true) - - @player.score -
-} diff --git a/app/views/tv/index.scala.html b/app/views/tv/index.scala.html index 2f37aea97c..59a2015f2e 100644 --- a/app/views/tv/index.scala.html +++ b/app/views/tv/index.scala.html @@ -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')); }; } diff --git a/conf/routes b/conf/routes index bfdeefc1e6..838bc89604 100644 --- a/conf/routes +++ b/conf/routes @@ -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) diff --git a/modules/tournament/src/main/Env.scala b/modules/tournament/src/main/Env.scala index 42338816d9..973a9198b1 100644 --- a/modules/tournament/src/main/Env.scala +++ b/modules/tournament/src/main/Env.scala @@ -93,6 +93,7 @@ final class Env( indexLeaderboard = leaderboardIndexer.indexOne _, roundMap = roundMap, asyncCache = asyncCache, + lightUserApi = lightUserApi, standingChannel = standingChannel ) diff --git a/modules/tournament/src/main/JsonView.scala b/modules/tournament/src/main/JsonView.scala index 0474ee69d9..898aa5eca3 100644 --- a/modules/tournament/src/main/JsonView.scala +++ b/modules/tournament/src/main/JsonView.scala @@ -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( diff --git a/modules/tournament/src/main/TournamentApi.scala b/modules/tournament/src/main/TournamentApi.scala index e9db0e1ebd..38a1e5c27f 100644 --- a/modules/tournament/src/main/TournamentApi.scala +++ b/modules/tournament/src/main/TournamentApi.scala @@ -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))) diff --git a/public/stylesheets/board.css b/public/stylesheets/board.css index 80cce4ab59..6237560d62 100644 --- a/public/stylesheets/board.css +++ b/public/stylesheets/board.css @@ -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 { diff --git a/public/stylesheets/chat.css b/public/stylesheets/chat.css index d44f12f68b..9863a51733 100644 --- a/public/stylesheets/chat.css +++ b/public/stylesheets/chat.css @@ -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; diff --git a/ui/chat/src/ctrl.ts b/ui/chat/src/ctrl.ts index 2daea11810..f453ed6950 100644 --- a/ui/chat/src/ctrl.ts +++ b/ui/chat/src/ctrl.ts @@ -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(); diff --git a/ui/chat/src/interfaces.ts b/ui/chat/src/interfaces.ts index 20b554c25e..b7e62d8579 100644 --- a/ui/chat/src/interfaces.ts +++ b/ui/chat/src/interfaces.ts @@ -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 { diff --git a/ui/chat/src/main.ts b/ui/chat/src/main.ts index 830e420f61..ac1aced678 100644 --- a/ui/chat/src/main.ts +++ b/ui/chat/src/main.ts @@ -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 } { diff --git a/ui/chat/src/view.ts b/ui/chat/src/view.ts index 99a93af8c4..f809cd9dd0 100644 --- a/ui/chat/src/view.ts +++ b/ui/chat/src/view.ts @@ -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 = ['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; } diff --git a/ui/round/src/boot.ts b/ui/round/src/boot.ts index ce97bb9469..9f417cea85 100644 --- a/ui/round/src/boot.ts +++ b/ui/round/src/boot.ts @@ -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; }); diff --git a/ui/round/src/interfaces.ts b/ui/round/src/interfaces.ts index e01183f494..b9b4afc8d6 100644 --- a/ui/round/src/interfaces.ts +++ b/ui/round/src/interfaces.ts @@ -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 { diff --git a/ui/round/src/tourStanding.ts b/ui/round/src/tourStanding.ts new file mode 100644 index 0000000000..717121166e --- /dev/null +++ b/ui/round/src/tourStanding.ts @@ -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) + ]) + })) + ); + } + }; +}