palantir WIP

palantir
Thibault Duplessis 2019-08-07 17:10:30 +02:00
parent 9a9add8d03
commit 99bd6bb539
17 changed files with 300 additions and 24 deletions

View File

@ -110,7 +110,7 @@ object home {
ctx.blind option h2("Timeline"),
views.html.timeline entries userTimeline,
// userTimeline.size >= 8 option
a(cls := "more", href := routes.Timeline.home)(trans.more(), " »")
if (userTimeline.size > 0) a(cls := "more", href := routes.Timeline.home)(trans.more(), " »")
)
} getOrElse div(cls := "about-side")(
ctx.blind option h2("About"),

View File

@ -34,7 +34,8 @@ object bits {
playing = playing,
robots = robots,
deferJs = true,
zoomable = true
zoomable = true,
csp = defaultCsp.withPeer.some
)(body)
def crosstable(cross: Option[lila.game.Crosstable.WithMatchup], game: Game)(implicit ctx: Context) =

View File

@ -45,11 +45,13 @@ final class Env(
val panic = new ChatPanic
private val palantir = new Palantir
system.scheduler.schedule(TimeoutCheckEvery, TimeoutCheckEvery) {
timeout.checkExpired foreach api.userChat.reinstate
}
system.actorOf(Props(new FrontActor(api)), name = ActorName)
system.actorOf(Props(new FrontActor(api, palantir)), name = ActorName)
private[chat] lazy val chatColl = db(CollectionChat)
private[chat] lazy val timeoutColl = db(CollectionTimeout)

View File

@ -5,7 +5,10 @@ import chess.Color
import actorApi._
private[chat] final class FrontActor(api: ChatApi) extends Actor {
private[chat] final class FrontActor(
api: ChatApi,
palantir: Palantir
) extends Actor {
def receive = {
@ -20,5 +23,7 @@ private[chat] final class FrontActor(api: ChatApi) extends Actor {
case Remove(chatId) => api remove chatId
case RemoveAll(chatIds) => api removeAll chatIds
case Palantir.Toggle(chatId, userId, sri, on) => palantir.toggle(chatId, userId, sri, on)
}
}

View File

@ -0,0 +1,43 @@
package lila.chat
import com.github.blemale.scaffeine.{ Cache, Scaffeine }
import scala.concurrent.duration._
import lila.socket.Socket.makeMessage
import lila.socket.SocketMember
import lila.user.User
private final class Palantir {
import Palantir._
private val stones: Cache[Chat.Id, Stone] = Scaffeine()
.expireAfterWrite(1 minute)
.build[Chat.Id, Stone]
def toggle(chatId: Chat.Id, userId: User.ID, member: SocketMember, on: Boolean): Unit = {
val stone = stones.getIfPresent(chatId).getOrElse(emptyStone) |> { stone =>
if (on) stone.add(userId, member)
else stone remove userId
}
stones.put(chatId, stone)
member.push(makeMessage("palantir", stone.userIds.filter(userId !=)))
}
}
private object Palantir {
case class Stone(members: Map[User.ID, SocketMember]) {
def add(uid: User.ID, member: SocketMember) = copy(
members = members + (uid -> member)
)
def remove(uid: User.ID) = copy(
members = members - uid
)
def userIds = members.keys.toList
}
val emptyStone = Stone(Map.empty)
case class Toggle(chatId: Chat.Id, userId: User.ID, member: SocketMember, on: Boolean)
}

View File

@ -30,6 +30,12 @@ object Socket {
} canTimeout.??(_(userId)) foreach { localTimeout =>
chat ! actorApi.Timeout(chatId, modId, userId, reason, local = localTimeout)
}
case ("palantir", o) => for {
data o obj "d"
on <- data boolean "on"
userId <- member.userId
} chat ! Palantir.Toggle(chatId, userId, member, on)
}
type Send = (String, JsValue, Boolean) => Unit

View File

@ -61,6 +61,10 @@ case class ContentSecurityPolicy(
frameSrc = "https://www.google.com" :: frameSrc
)
def withPeer = copy(
connectSrc = "wss://0.peerjs.com" :: connectSrc
)
private def withPrismicEditor(maybe: Boolean): ContentSecurityPolicy = if (maybe) copy(
scriptSrc = "https://static.cdn.prismic.io" :: scriptSrc,
frameSrc = "https://lichess.prismic.io" :: "https://lichess.cdn.prismic.io" :: frameSrc,

File diff suppressed because one or more lines are too long

View File

@ -68,6 +68,10 @@ interface LichessSpeech {
step(s: { san?: San }, cut: boolean): void;
}
interface PalantirOpts {
uid: string;
redraw(): void;
}
interface Palantir {
button(): any;
}
@ -138,7 +142,7 @@ interface Window {
hopscotch: any;
LichessSpeech?: LichessSpeech;
palantir?: {
palantir(): Palantir
palantir(opts: PalantirOpts): Palantir
};
[key: string]: any; // TODO

View File

@ -13,7 +13,7 @@ mkdir -p public/compiled
ts_apps1="common chess"
ts_apps2="ceval game tree chat nvui"
apps="site chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul perfStat dasher speech"
apps="site chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul perfStat dasher speech palantir"
if [ $mode == "upgrade" ]; then
yarn upgrade --non-interactive

View File

@ -40,5 +40,26 @@
}
&__palantir {
padding: 0 .6em;
background: $c-primary;
color: $c-primary-over;
animation: glowing 1.5s ease-in-out infinite;
&:hover {
background: $c-bad !important;
color: $c-bad-over !important;
}
}
.pal-off {
background: transparent;
color: $c-font;
animation: none;
&:hover {
background: $c-primary !important;
color: $c-primary-over !important;
}
}
.pal-on {
background: $c-good;
color: $c-good-over;
animation: none;
}
}

View File

@ -121,10 +121,14 @@ export default function(opts: ChatOpts, redraw: Redraw): Ctrl {
const emitEnabled = () => li.pubsub.emit('chat.enabled', vm.enabled);
emitEnabled();
li.loadScript(li.compiledScript('palantir')).then(() => {
palantir.instance = window.Palantir!.palantir();
console.log(palantir.instance);
redraw();
data.userId && li.loadScript('javascripts/vendor/peerjs.min.js').then(() => {
li.loadScript(li.compiledScript('palantir')).then(() => {
palantir.instance = window.Palantir!.palantir({
uid: data.userId,
redraw
});
redraw();
});
});
return {

View File

@ -27,5 +27,6 @@
"typescript": "^3"
},
"dependencies": {
"@types/webrtc": "^0.0.25"
}
}

View File

@ -1,13 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const snabbdom_1 = require("snabbdom");
function instance() {
function palantir(opts) {
// const peerId = `lichess:${opts.uid}`;
console.log(opts);
console.log(window.Peer);
// const peer = new peerjs.Peer(peerId);
// peer.on('open', id => {
// console.log('My peer ID is: ' + id);
// navigator.getUserMedia({video: false, audio: true}, function(stream) {
// // var call = peer.call('another-peers-id', stream);
// // call.on('stream', function(remoteStream) {
// // // Show stream in some video/canvas element.
// // });
// }, function(err) {
// console.log('Failed to get local stream' ,err);
// });
// });
return {
button() {
return snabbdom_1.h('button.palantir', {
return snabbdom_1.h('button.mchat__palantir.fbt', {
attrs: { 'data-icon': '' }
});
}
};
}
exports.instance = instance;
exports.palantir = palantir;

View File

@ -1,21 +1,147 @@
import { h } from 'snabbdom'
import { h } from 'snabbdom';
export function palantir() {
const li = window.lichess;
navigator.getUserMedia({video: true, audio: true}, function(stream) {
var call = peer.call('another-peers-id', stream);
call.on('stream', function(remoteStream) {
// Show stream in some video/canvas element.
type State = 'off' | 'opening' | 'getting-media' | 'ready' | 'calling' | 'answering' | 'getting-stream' | 'on' | 'stopping';
export function palantir(opts: PalantirOpts) {
// const peerId = `lichess:${opts.uid}`;
let state: State = 'off',
peer: any | undefined,
myStream: any | undefined,
remoteStream: any | undefined;
function setState(s: State) {
console.log(s);
state = s;
opts.redraw();
}
function peerIdOf(uid: string) {
// return `org-lichess-${uid}`;
return `org-l-${uid}`;
}
function callStart(s: any) {
remoteStream = s;
setState('on');
}
function start() {
setState('opening');
peer = peer || new window['Peer'](peerIdOf(opts.uid));
window.peer = peer;
peer.on('open', () => {
setState('getting-media');
navigator.mediaDevices.getUserMedia({video: false, audio: true}).then((s: any) => {
myStream = s;
setState('on');
notifyLichess();
setInterval(notifyLichess, 10 * 1000);
setState('ready');
peer.on('call', (call: any) => {
if (!peer.connections[call.peer].find((c: any) => c.open)) {
setState('answering');
monitorCall(call);
call.answer(myStream);
}
});
}, function(err) {
console.log('Failed to get local stream' ,err);
}).catch(err => {
console.log(err);
});
});
}, function(err) {
console.log('Failed to get local stream' ,err);
peer.on('stream', s => {
console.log('stream', s);
});
peer.on('connection', function (c) {
console.log("Connected to: " + c.peer);
});
peer.on('disconnected', function() {
if (state == 'stopping') {
peer.destroy(); // 'off' means manual disconnect
peer = undefined; // 'off' means manual disconnect
setState('off');
}
else {
setState('opening');
peer.reconnect();
}
});
peer.on('close', function() {
console.log('Connection destroyed');
});
peer.on('error', function (err) {
console.log(err);
});
}
function monitorCall(call: any) {
console.log(call, 'monitor');
call
.on('stream', callStart)
.on('close', s => {
console.log('call close', s);
stop();
})
.on('error', s => {
console.log('call error', s);
stop();
});
}
function notifyLichess() {
li.pubsub.emit('socket.send', 'palantir', { on: true });
}
function call(uid: string) {
const peerId = peerIdOf(uid);
if (peer && myStream && peer.id < peerId && !peer.connections[peerId]) {
setState('calling');
monitorCall(peer.call(peerId, myStream));
}
}
function stop() {
if (peer && state != 'off') {
setState('stopping');
peer.disconnect();
}
}
let started = false;
function click() {
if (!started) {
start();
started = true;
} else if (state == 'off') start();
else stop();
}
li.pubsub.on('socket.in.palantir', uids => {
uids.forEach(call);
});
return {
button() {
return h('button.mchat__palantir.fbt', {
attrs: { 'data-icon': '' }
});
return h('button.mchat__palantir.fbt.pal-' + state, {
attrs: {
'data-icon': '',
title: state
},
hook: {
insert(vnode) { (vnode.elm as HTMLElement).addEventListener('click', click) }
}
}, [
state == 'on' ? h('audio.palantir__audio', {
attrs: { autoplay: true },
hook: {
insert(vnode) { (vnode.elm as HTMLAudioElement).srcObject = remoteStream }
}
}) : null
]);
}
};
}

View File

@ -2,5 +2,6 @@
"extends": "../tsconfig.base.json",
"include": ["src/*.ts"],
"compilerOptions": {
"noImplicitAny": false
}
}

View File

@ -50,6 +50,11 @@
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
"@types/webrtc@^0.0.25":
version "0.0.25"
resolved "https://registry.yarnpkg.com/@types/webrtc/-/webrtc-0.0.25.tgz#bd2b4e7b4c13250b3d58439623f2b9adfd7dee9e"
integrity sha512-ep/e+p2uUKV1h96GBgRhwomrBch/bPDHPOKbCHODLGRUDuuKe2s7sErlFVKw+5BYUzvpxSmUNqoadaZ44MePoQ==
JSONStream@^1.0.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"