Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ object I18nKey:
val `tournamentShields`: I18nKey = "arena:tournamentShields"
val `onlyTitled`: I18nKey = "arena:onlyTitled"
val `onlyTitledHelp`: I18nKey = "arena:onlyTitledHelp"
val `tournamentPairingsAreNowClosed`: I18nKey = "arena:tournamentPairingsAreNowClosed"
val `berserkRate`: I18nKey = "arena:berserkRate"
val `drawingWithinNbMoves`: I18nKey = "arena:drawingWithinNbMoves"
val `viewAllXTeams`: I18nKey = "arena:viewAllXTeams"

Expand Down Expand Up @@ -1748,7 +1750,6 @@ object I18nKey:
val `blackWins`: I18nKey = "blackWins"
val `drawRate`: I18nKey = "drawRate"
val `draws`: I18nKey = "draws"
val `nextXTournament`: I18nKey = "nextXTournament"
val `averageOpponent`: I18nKey = "averageOpponent"
val `boardEditor`: I18nKey = "boardEditor"
val `setTheBoard`: I18nKey = "setTheBoard"
Expand Down
1 change: 1 addition & 0 deletions translation/source/broadcast.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,5 @@
<string name="allBroadcastsByMonth">View all broadcasts by month</string>
<string name="backToLiveMove">Back to live move</string>
<string name="sinceHideResults">Since you chose to hide the results, all the preview boards are empty to avoid spoilers.</string>
<string name="liveboard">Live board</string>
</resources>
2 changes: 2 additions & 0 deletions ui/@types/lichess/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ interface I18n {
howToUseLichessBroadcasts: string;
/** More options on the %s */
iframeHelp: I18nFormat;
/** Live board */
liveboard: string;
/** Live tournament broadcasts */
liveBroadcasts: string;
/** My broadcasts */
Expand Down
17 changes: 17 additions & 0 deletions ui/analyse/css/study/relay/_back-to-live.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@
display: none;
}
}

.chat-liveboard {
display: flex;
position: relative;
width: 100%;
aspect-ratio: 1;
}

.mchat:has(.chat-liveboard) {
box-shadow: none;
}
.mchat .liveboard {
background-color: transparent;
cg-board {
border-top-right-radius: 0;
}
}
11 changes: 9 additions & 2 deletions ui/analyse/src/ctrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { debounce, throttle } from 'lib/async';
import type GamebookPlayCtrl from './study/gamebook/gamebookPlayCtrl';
import type StudyCtrl from './study/studyCtrl';
import { isTouchDevice } from 'lib/device';
import type { AnalyseOpts, AnalyseData, ServerEvalData, JustCaptured, NvuiPlugin } from './interfaces';
import type {
AnalyseOpts,
AnalyseData,
ServerEvalData,
JustCaptured,
NvuiPlugin,
MultiRedraw,
} from './interfaces';
import type { Api as ChessgroundApi } from 'chessground/api';
import { Autoplay, AutoplayDelay } from './autoplay';
import { build as makeTree, path as treePath, ops as treeOps, type TreeWrapper } from 'lib/tree/tree';
Expand Down Expand Up @@ -124,7 +131,7 @@ export default class AnalyseCtrl {

constructor(
readonly opts: AnalyseOpts,
readonly redraw: Redraw,
readonly redraw: MultiRedraw,
makeStudy?: typeof StudyCtrl,
) {
this.data = opts.data;
Expand Down
7 changes: 6 additions & 1 deletion ui/analyse/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Player, Status, Source, Clock } from 'lib/game/game';
import type { ForecastData } from './forecast/interfaces';
import type { StudyPracticeData, Goal as PracticeGoal } from './study/practice/interfaces';
import type { RelayData } from './study/relay/interfaces';
import type { ChatCtrl } from 'lib/chat/chat';
import type { ChatCtrl, ChatPlugin } from 'lib/chat/chat';
import type { ExplorerOpts } from './explorer/interfaces';
import type { StudyDataFromServer } from './study/interfaces';
import type { AnalyseSocketSend } from './socket';
Expand Down Expand Up @@ -149,6 +149,7 @@ export interface AnalyseOpts {
$side?: Cash;
$underboard?: Cash;
chat: {
plugin: ChatPlugin;
enhance: EnhanceOpts;
instance?: ChatCtrl;
};
Expand Down Expand Up @@ -184,3 +185,7 @@ export interface AnalyseState {
path: Tree.Path | undefined;
flipped: boolean;
}

export type MultiRedraw = Redraw & {
add: (redraw: Redraw) => void;
};
17 changes: 15 additions & 2 deletions ui/analyse/src/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import makeCtrl from './ctrl';
import menuHover from 'lib/menuHover';
import makeView from './view/main';
import type { AnalyseApi, AnalyseOpts } from './interfaces';
import type { AnalyseApi, AnalyseOpts, MultiRedraw } from './interfaces';
import type { VNode } from 'snabbdom';
import type * as studyDeps from './study/studyDeps';

Expand All @@ -12,7 +12,7 @@ export default function (
return function (opts: AnalyseOpts): AnalyseApi {
opts.element = document.querySelector('main.analyse') as HTMLElement;

const ctrl = (site.analysis = new makeCtrl(opts, redraw, deps?.StudyCtrl));
const ctrl = (site.analysis = new makeCtrl(opts, multiRedraw(redraw), deps?.StudyCtrl));
const view = makeView(deps);

const blueprint = view(ctrl);
Expand All @@ -34,3 +34,16 @@ export default function (
};
};
}

function multiRedraw(base: Redraw): MultiRedraw {
// to build redraws involving multiple patch functions.
const redraws: Redraw[] = [base];
const multi: MultiRedraw = () => {
for (const r of redraws) r();
};

multi.add = (r: Redraw) => {
redraws.push(r);
};
return multi;
}
1 change: 1 addition & 0 deletions ui/analyse/src/study/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,5 @@ export type WithWhoAndChap = WithWho & WithChapterId;
export interface ChapterSelect {
is: (idOrNumber: ChapterId | number) => boolean;
set: (idOrNumber: ChapterId | number, force?: boolean) => Promise<boolean>;
get: () => ChapterId;
}
72 changes: 72 additions & 0 deletions ui/analyse/src/study/relay/relayChat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { type RelayViewContext } from '../../view/components';
import type { StudyChapters } from '../studyChapters';
import { spinnerVdom } from 'lib/controls';
import { looseH as h, VNode, onInsert } from 'lib/snabbdom';
import { getChessground, initMiniBoardWith } from 'lib/miniBoard';
import { type ChatPlugin, makeChat } from 'lib/chat/chat';
import { watchers } from 'lib/watchers';
import { uciToMove } from 'chessground/util';
import { frag } from 'lib';
import { ChapterId } from '../interfaces';

export function relayChatView({ ctrl, relay }: RelayViewContext): VNode | undefined {
if (ctrl.isEmbed || !ctrl.opts.chat) return undefined;
return h('section.mchat.mchat-optional', {
hook: onInsert(el => {
ctrl.opts.chat.instance?.destroy();
ctrl.opts.chat.instance = makeChat({
...ctrl.opts.chat,
plugin: relay.chatCtrl,
enhance: { plies: true, boards: true },
});
const members = frag<HTMLElement>('<div class="chat__members">');
el.parentElement?.append(members);
watchers(members);
}),
});
}

export class RelayChatPlugin implements ChatPlugin {
private chapter: ChapterId | undefined;
private animate = false;

key = 'liveboard';
name = i18n.broadcast.liveboard;
kidSafe = true;
redraw: Redraw;

constructor(
readonly chapters: StudyChapters,
readonly isDisabled: () => boolean,
) {}

set chapterId(id: ChapterId) {
if (id === this.chapter) return;
this.chapter = id;
this.animate = false;
}

get hidden(): boolean {
return this.isDisabled() || !this.chapter;
}

view(): VNode {
const preview = this.chapters.get(this.chapter || 0);
return preview
? h('div.chat-liveboard', {
hook: {
insert: (vn: VNode) =>
initMiniBoardWith(vn.elm as HTMLElement, preview.fen, 'white', preview.lastMove),
update: (_, vn: VNode) => {
getChessground(vn.elm as HTMLElement)?.set({
fen: preview.fen,
lastMove: uciToMove(preview.lastMove),
animation: { enabled: this.animate },
});
this.animate = true;
},
},
})
: spinnerVdom();
}
}
32 changes: 22 additions & 10 deletions ui/analyse/src/study/relay/relayCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import type { StudyChapters } from '../studyChapters';
import type { MultiCloudEval } from '../multiCloudEval';
import { VideoPlayer } from './videoPlayer';
import RelayStats from './relayStats';
import { RelayChatPlugin } from './relayChat';
import { pubsub } from 'lib/pubsub';
import type { MultiRedraw } from '../../interfaces';

export const relayTabs = ['overview', 'boards', 'teams', 'players', 'stats'] as const;
export type RelayTab = (typeof relayTabs)[number];
Expand All @@ -27,18 +29,20 @@ export default class RelayCtrl {
streams: [string, string][] = [];
showStreamerMenu = toggle(false);
videoPlayer?: VideoPlayer;
chatCtrl: RelayChatPlugin;

constructor(
readonly id: RoundId,
public data: RelayData,
readonly send: AnalyseSocketSend,
readonly redraw: (redrawOnly?: boolean) => void,
readonly baseRedraw: MultiRedraw,
readonly isEmbed: boolean,
readonly members: StudyMemberCtrl,
private readonly chapters: StudyChapters,
readonly chapters: StudyChapters,
private readonly multiCloudEval: MultiCloudEval | undefined,
private readonly federations: () => Federations | undefined,
chapterSelect: ChapterSelect,
private readonly updateHistoryAndAddressBar: () => void,
) {
this.tourShow = toggle((location.pathname.split('/broadcast/')[1].match(/\//g) || []).length < 3);
const locationTab = location.hash.replace(/^#(\w+).*$/, '$1') as RelayTab;
Expand All @@ -49,16 +53,16 @@ export default class RelayCtrl {
: 'boards';
this.tab = prop<RelayTab>(initialTab);
this.teams = data.tour.teamTable
? new RelayTeams(id, this.multiCloudEval, chapterSelect, this.roundPath, redraw)
? new RelayTeams(id, this.multiCloudEval, chapterSelect, this.roundPath, this.redraw)
: undefined;
this.players = new RelayPlayers(
data.tour.id,
() => this.openTab('players'),
this.isEmbed,
this.federations,
redraw,
this.redraw,
);
this.stats = new RelayStats(this.currentRound(), redraw);
this.stats = new RelayStats(this.currentRound(), this.redraw);
if (data.videoUrls?.[0] || this.isPinnedStreamOngoing())
this.videoPlayer = new VideoPlayer(
{
Expand All @@ -67,33 +71,41 @@ export default class RelayCtrl {
image: this.data.tour.image,
text: this.data.pinned?.text,
},
redraw,
this.redraw,
);
const pinnedName = this.isPinnedStreamOngoing() && data.pinned?.name;
if (pinnedName) this.streams.push(['ps', pinnedName]);

this.chatCtrl = new RelayChatPlugin(this.chapters, this.tourShow);
this.chatCtrl.chapterId = chapterSelect.get();
this.baseRedraw.add(() => this.chatCtrl.redraw?.());
pubsub.on('socket.in.crowd', d => {
const s = (d.streams as [string, string][]) ?? [];
if (pinnedName) s.unshift(['ps', pinnedName]);
if (this.streams.length === s.length && this.streams.every(([id], i) => id === s[i][0])) return;
this.streams = s;
this.redraw();
});
setInterval(() => this.redraw(true), 1000);
setInterval(this.baseRedraw, 1000);
}

redraw = () => {
this.baseRedraw();
this.updateHistoryAndAddressBar();
};

openTab = (t: RelayTab) => {
this.players.closePlayer();
this.tab(t);
this.tourShow(true);
this.redraw();
};

onChapterChange = () => {
onChapterChange = (id: ChapterId) => {
if (this.tourShow()) {
this.tourShow(false);
this.redraw();
}
this.chatCtrl.chapterId = id;
this.redraw();
};

lastMoveAt = (id: ChapterId): number | undefined => this.chapters.get(id)?.lastMoveAt;
Expand Down
12 changes: 3 additions & 9 deletions ui/analyse/src/study/relay/relayTourView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { toggle, copyMeInput } from 'lib/controls';
import { text as xhrText } from 'lib/xhr';
import { teamsView } from './relayTeams';
import { statsView } from './relayStats';
import { makeChatEl, type RelayViewContext } from '../../view/components';
import { type RelayViewContext } from '../../view/components';
import { gamesList } from './relayGames';
import { renderStreamerMenu } from './relayView';
import { playersView } from './relayPlayers';
import { gameLinksListener } from '../studyChapters';
import { baseUrl } from '../../view/util';
import { commonDateFormat, timeago } from 'lib/i18n';
import { watchers } from 'lib/watchers';
import { relayChatView } from './relayChat';

export function renderRelayTour(ctx: RelayViewContext): VNode | undefined {
const tab = ctx.relay.tab();
Expand Down Expand Up @@ -79,13 +79,7 @@ export const tourSide = (ctx: RelayViewContext) => {
]),
!ctrl.isEmbed && relay.showStreamerMenu() && renderStreamerMenu(relay),
!empty && gamesList(study, relay),
!ctrl.isEmbed &&
h('div.chat__members', {
hook: onInsert(el => {
makeChatEl(ctrl, chat => el.parentNode!.insertBefore(chat, el));
watchers(el);
}),
}),
relayChatView(ctx),
],
);
};
Expand Down
Loading
Loading