in-game tournament chat WIP
parent
7f3cdc39f3
commit
e2e1b524fc
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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())))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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'));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -93,6 +93,7 @@ final class Env(
|
|||
indexLeaderboard = leaderboardIndexer.indexOne _,
|
||||
roundMap = roundMap,
|
||||
asyncCache = asyncCache,
|
||||
lightUserApi = lightUserApi,
|
||||
standingChannel = standingChannel
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
} {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
])
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue