more work on challenges
parent
feea61f5f0
commit
6ca7360a6a
|
@ -24,9 +24,9 @@ object Challenge extends LilaController {
|
|||
}
|
||||
|
||||
private[controllers] def renderAll(me: lila.user.User)(implicit ctx: Context) =
|
||||
env.api.findByDestId(me.id) zip
|
||||
env.api.findByChallengerId(me.id) map {
|
||||
case (out, in) => Ok(env.jsonView.all(in, out)) as JSON
|
||||
env.api.createdByDestId(me.id) zip
|
||||
env.api.createdByChallengerId(me.id) map {
|
||||
case (in, out) => Ok(env.jsonView.all(in, out)) as JSON
|
||||
}
|
||||
private[controllers] def renderOne(challenge: ChallengeModel)(implicit ctx: Context) =
|
||||
Ok(env.jsonView.one(challenge)) as JSON
|
||||
|
@ -43,6 +43,24 @@ object Challenge extends LilaController {
|
|||
case Right(user) => ctx.userId contains user.id
|
||||
}
|
||||
|
||||
private def isForMe(challenge: ChallengeModel)(implicit ctx: Context) =
|
||||
challenge.destUserId.fold(true)(ctx.userId.contains)
|
||||
|
||||
def accept(id: String) = Open { implicit ctx =>
|
||||
OptionFuResult(env.api byId id) { challenge =>
|
||||
if (isForMe(challenge)) ???
|
||||
else notFound
|
||||
}
|
||||
}
|
||||
|
||||
def decline(id: String) = Auth { implicit ctx =>
|
||||
me =>
|
||||
OptionFuResult(env.api byId id) { challenge =>
|
||||
if (isForMe(challenge)) (env.api decline challenge) >> renderAll(me)
|
||||
else notFound
|
||||
}
|
||||
}
|
||||
|
||||
def cancel(id: String) = Open { implicit ctx =>
|
||||
OptionFuResult(env.api byId id) { challenge =>
|
||||
if (isMine(challenge)) env.api remove challenge inject Redirect(routes.Lobby.home)
|
||||
|
|
|
@ -82,6 +82,7 @@ object Setup extends LilaController with TheftPrevention {
|
|||
import lila.challenge.Challenge._
|
||||
val challenge = lila.challenge.Challenge.make(
|
||||
variant = config.variant,
|
||||
initialFen = config.fen,
|
||||
timeControl = config.makeClock map { c =>
|
||||
TimeControl.Clock(c.limit, c.increment)
|
||||
} orElse config.makeDaysPerTurn.map {
|
||||
|
|
|
@ -106,7 +106,7 @@ withLangAnnotations: Boolean = true)(body: Html)(implicit ctx: Context)
|
|||
<span data-icon="U"></span>
|
||||
</span>
|
||||
</a>
|
||||
<div id="challenge_notifications" class="links dropdown">
|
||||
<div id="challenge_app" class="links dropdown">
|
||||
<div class="square-wrap"><div class="square-spin"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -232,7 +232,8 @@ GET /challenge controllers.Challenge.all
|
|||
GET /challenge controllers.Challenge.all
|
||||
# GET /challenge/$fullId<\w{12}>/await controllers.Setup.await(fullId: String, user: Option[String] ?= None)
|
||||
# GET /challenge/$fullId<\w{12}>/cancel controllers.Setup.cancel(fullId: String)
|
||||
# POST /challenge/$id<\w{8}>/join controllers.Setup.join(id: String)
|
||||
POST /challenge/$id<\w{8}>/accept controllers.Challenge.accept(id: String)
|
||||
POST /challenge/$id<\w{8}>/decline controllers.Challenge.decline(id: String)
|
||||
POST /challenge/$id<\w{8}>/cancel controllers.Challenge.cancel(id: String)
|
||||
|
||||
# Video
|
||||
|
|
|
@ -42,6 +42,10 @@ private object BSONHandlers {
|
|||
def read(b: BSONInteger): Variant = Variant(b.value) err s"No such variant: ${b.value}"
|
||||
def write(x: Variant) = BSONInteger(x.id)
|
||||
}
|
||||
implicit val StateBSONHandler = new BSONHandler[BSONInteger, State] {
|
||||
def read(b: BSONInteger): State = State(b.value) err s"No such state: ${b.value}"
|
||||
def write(x: State) = BSONInteger(x.id)
|
||||
}
|
||||
implicit val ModeBSONHandler = new BSONHandler[BSONBoolean, Mode] {
|
||||
def read(b: BSONBoolean) = Mode(b.value)
|
||||
def write(m: Mode) = BSONBoolean(m.rated)
|
||||
|
@ -65,4 +69,3 @@ private object BSONHandlers {
|
|||
|
||||
implicit val ChallengeBSONHandler = Macros.handler[Challenge]
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ import lila.rating.PerfType
|
|||
|
||||
case class Challenge(
|
||||
_id: String,
|
||||
state: Challenge.State,
|
||||
variant: Variant,
|
||||
initialFen: Option[String],
|
||||
timeControl: Challenge.TimeControl,
|
||||
mode: Mode,
|
||||
color: Challenge.ColorChoice,
|
||||
|
@ -35,6 +37,15 @@ case class Challenge(
|
|||
|
||||
object Challenge {
|
||||
|
||||
sealed abstract class State(val id: Int)
|
||||
object State {
|
||||
case object Created extends State(10)
|
||||
case object Accepted extends State(20)
|
||||
case object Declined extends State(30)
|
||||
val all = List(Created, Accepted, Declined)
|
||||
def apply(id: Int): Option[State] = all.find(_.id == id)
|
||||
}
|
||||
|
||||
case class Rating(int: Int, provisional: Boolean)
|
||||
object Rating {
|
||||
def apply(p: lila.rating.Perf): Rating = Rating(p.intRating, p.provisional)
|
||||
|
@ -78,13 +89,16 @@ object Challenge {
|
|||
|
||||
def make(
|
||||
variant: Variant,
|
||||
initialFen: Option[String],
|
||||
timeControl: TimeControl,
|
||||
mode: Mode,
|
||||
color: String,
|
||||
challenger: Option[lila.user.User],
|
||||
destUserId: Option[String]): Challenge = new Challenge(
|
||||
_id = randomId,
|
||||
state = State.Created,
|
||||
variant = variant,
|
||||
initialFen = initialFen.ifTrue(variant == chess.variant.FromPosition),
|
||||
timeControl = timeControl,
|
||||
mode = mode,
|
||||
color = color match {
|
||||
|
|
|
@ -11,32 +11,40 @@ final class ChallengeApi(
|
|||
maxPerUser: Int) {
|
||||
|
||||
import BSONHandlers._
|
||||
import Challenge._
|
||||
|
||||
def byId(id: String) = coll.find(BSONDocument("_id" -> id)).one[Challenge]
|
||||
|
||||
def insert(c: Challenge): Funit =
|
||||
coll.insert(c) >> c.challenger.right.toOption.?? { challenger =>
|
||||
findByChallengerId(challenger.id).flatMap {
|
||||
createdByChallengerId(challenger.id).flatMap {
|
||||
case challenges if challenges.size <= maxPerUser => funit
|
||||
case challenges => challenges.drop(maxPerUser).map(remove).sequenceFu.void
|
||||
}
|
||||
}
|
||||
|
||||
def findByChallengerId(userId: String): Fu[List[Challenge]] =
|
||||
coll.find(BSONDocument("challenger.id" -> userId))
|
||||
def createdByChallengerId(userId: String): Fu[List[Challenge]] =
|
||||
coll.find(selectCreated ++ BSONDocument("challenger.id" -> userId))
|
||||
.sort(BSONDocument("createdAt" -> -1))
|
||||
.cursor[Challenge]().collect[List]()
|
||||
|
||||
def findByDestId(userId: String): Fu[List[Challenge]] =
|
||||
coll.find(BSONDocument("destUserId" -> userId))
|
||||
def createdByDestId(userId: String): Fu[List[Challenge]] =
|
||||
coll.find(selectCreated ++ BSONDocument("destUserId" -> userId))
|
||||
.sort(BSONDocument("createdAt" -> -1))
|
||||
.cursor[Challenge]().collect[List]()
|
||||
|
||||
def findByDestIds(userIds: List[String]): Fu[Map[String, Seq[Challenge]]] =
|
||||
coll.find(BSONDocument("destUserId" -> BSONDocument("$in" -> userIds)))
|
||||
def createdByDestIds(userIds: List[String]): Fu[Map[String, Seq[Challenge]]] =
|
||||
coll.find(selectCreated ++ BSONDocument("destUserId" -> BSONDocument("$in" -> userIds)))
|
||||
.sort(BSONDocument("createdAt" -> -1))
|
||||
.cursor[Challenge]().collect[Stream]().map { _.groupBy(~_.destUserId) }
|
||||
|
||||
def remove(challenge: Challenge) =
|
||||
coll.remove(BSONDocument("_id" -> challenge.id)).void
|
||||
|
||||
def decline(challenge: Challenge) = coll.update(
|
||||
selectCreated ++ BSONDocument("_id" -> challenge.id),
|
||||
BSONDocument("$set" -> BSONDocument("state" -> State.Declined.id))
|
||||
).void
|
||||
|
||||
private val selectCreated = BSONDocument("state" -> State.Created.id)
|
||||
}
|
||||
|
|
|
@ -340,15 +340,15 @@ lichess.hopscotch = function(f) {
|
|||
.attr('href', baseUrl + '/assets/vendor/hopscotch/dist/css/hopscotch.min.css'));
|
||||
$.getScript(baseUrl + "/assets/vendor/hopscotch/dist/js/hopscotch.min.js").done(f);
|
||||
}
|
||||
lichess.challengeBox = (function() {
|
||||
lichess.challengeApp = (function() {
|
||||
var instance;
|
||||
var load = function(then) {
|
||||
var baseUrl = $('body').data('asset-url');
|
||||
var isDev = $('body').data('dev');
|
||||
$('head').append($('<link rel="stylesheet" type="text/css" />')
|
||||
.attr('href', baseUrl + '/assets/stylesheet/challengeBox.css'));
|
||||
.attr('href', baseUrl + '/assets/stylesheets/challengeApp.css'));
|
||||
$.getScript(baseUrl + "/assets/compiled/lichess.challenge" + (isDev ? '' : '.min') + '.js').done(function() {
|
||||
instance = LichessChallenge(document.getElementById('challenge_notifications'), {
|
||||
instance = LichessChallenge(document.getElementById('challenge_app'), {
|
||||
setCount: function(nb) {
|
||||
$('#challenge_notifications_tag').attr('data-count', nb).toggleClass('none', !nb);
|
||||
}
|
||||
|
@ -1114,7 +1114,7 @@ lichess.unique = function(xs) {
|
|||
});
|
||||
});
|
||||
$('#challenge_notifications_tag').one('mouseover click', function() {
|
||||
lichess.challengeBox.load();
|
||||
lichess.challengeApp.load();
|
||||
}).trigger('click');
|
||||
|
||||
$('#translation_call .close').click(function() {
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
#challenge_app {
|
||||
width: 250px;
|
||||
z-index: 2;
|
||||
left: -95px;
|
||||
}
|
||||
#challenge_app .challenge {
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
position: relative;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
transition: 0.13s;
|
||||
}
|
||||
#challenge_app .challenge:hover {
|
||||
/* #challenge_app .challenge { */
|
||||
background: rgba(191, 231, 255, 0.7);
|
||||
height: 80px;
|
||||
}
|
||||
#challenge_app .challenge.declined {
|
||||
height: 0px;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
#challenge_app .challenge .buttons {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
transition: 0.13s;
|
||||
}
|
||||
#challenge_app .challenge form {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
}
|
||||
#challenge_app .challenge button {
|
||||
color: #759900;
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: 50px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
#challenge_app .challenge button::before {
|
||||
line-height: 50px;
|
||||
}
|
||||
#challenge_app .challenge button.decline {
|
||||
color: #dc322f;
|
||||
}
|
||||
#challenge_app .challenge button:hover {
|
||||
background: #759900;
|
||||
color: #fff;
|
||||
}
|
||||
#challenge_app .challenge button.decline:hover {
|
||||
background: #dc322f;
|
||||
}
|
||||
#challenge_app .challenge:hover .buttons {
|
||||
/* #challenge_app .challenge .buttons { */
|
||||
opacity: 1;
|
||||
}
|
||||
#challenge_app .challenge:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
#challenge_app .challenge i {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
}
|
||||
#challenge_app .challenge .content {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50px;
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 30px;
|
||||
}
|
||||
#challenge_app .challenge .title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1308,20 +1308,17 @@ body.fpmenu #fpmenu {
|
|||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
#message_notifications,
|
||||
#challenge_notifications {
|
||||
#message_notifications {
|
||||
width: 250px;
|
||||
z-index: 2;
|
||||
left: -95px;
|
||||
}
|
||||
#message_notifications div.notification,
|
||||
#challenge_notifications div.notification {
|
||||
#message_notifications div.notification {
|
||||
border-bottom: 1px solid #c0c0c0;
|
||||
transition: background-color 0.13s;
|
||||
position: relative;
|
||||
}
|
||||
#message_notifications div.notification:hover,
|
||||
#challenge_notifications div.notification:hover {
|
||||
#message_notifications div.notification:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
#notifications {
|
||||
|
@ -1343,18 +1340,13 @@ body.fpmenu #fpmenu {
|
|||
transition: box-shadow 1.5s;
|
||||
}
|
||||
#message_notifications .user_link,
|
||||
#challenge_notifications .user_link,
|
||||
#message_notifications .block .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
#message_notifications_display .content,
|
||||
#challenge_notifications .setup {
|
||||
#message_notifications_display .content {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
#challenge_notifications .setup {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#message_notifications .actions {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
|
@ -1365,8 +1357,7 @@ body.fpmenu #fpmenu {
|
|||
margin-top: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
#message_notifications a.block,
|
||||
#challenge_notifications div.notification {
|
||||
#message_notifications a.block {
|
||||
display: block;
|
||||
padding: 6px 8px 7px 8px;
|
||||
}
|
||||
|
@ -1381,27 +1372,6 @@ body.fpmenu #fpmenu {
|
|||
#message_notifications .actions a:hover {
|
||||
color: #d85000;
|
||||
}
|
||||
#challenge_notifications .buttons {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.13s;
|
||||
}
|
||||
#challenge_notifications .notification:hover .buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
#challenge_notifications .buttons form {
|
||||
display: inline;
|
||||
}
|
||||
#challenge_notifications .buttons .button {
|
||||
color: #759900;
|
||||
font-size: 1.6em;
|
||||
padding: 6px 30px;
|
||||
}
|
||||
#challenge_notifications .buttons .decline {
|
||||
color: #dc322f;
|
||||
margin-left: 1em;
|
||||
}
|
||||
form.wide input[type="text"],
|
||||
form.wide textarea {
|
||||
padding: 0.5em;
|
||||
|
|
|
@ -151,7 +151,7 @@ body.dark div.training div.box,
|
|||
body.dark div.force_resign_zone,
|
||||
body.dark div.negotiation,
|
||||
body.dark div.side_box .game_infos,
|
||||
body.dark #challenge_notifications div.notification,
|
||||
body.dark #challenge_app div.notification,
|
||||
body.dark #message_notifications div.notification,
|
||||
body.dark #video .content_box_top,
|
||||
body.dark #simul .half,
|
||||
|
@ -188,7 +188,7 @@ body.dark #opening .meter .step,
|
|||
body.dark #hooks_wrap div.tabs a,
|
||||
body.dark #top div.auth .links a:hover,
|
||||
body.dark #top .language_links button:hover,
|
||||
body.dark #challenge_notifications > div.notification:hover,
|
||||
body.dark #challenge_app > div.notification:hover,
|
||||
body.dark #message_notifications div.notification:hover {
|
||||
background-color: #3e3e3e;
|
||||
color: #b0b0b0;
|
||||
|
|
|
@ -19,6 +19,15 @@ module.exports = function(env) {
|
|||
this.vm.reloading = false;
|
||||
}.bind(this);
|
||||
|
||||
this.decline = function(id) {
|
||||
this.data.in.forEach(function(c) {
|
||||
if (c.id === id) {
|
||||
xhr.decline(id).then(this.update);
|
||||
c.declined = true;
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
xhr.load().then(this.update);
|
||||
|
||||
this.trans = lichess.trans(env.i18n);
|
||||
|
|
|
@ -11,7 +11,7 @@ function user(u) {
|
|||
},
|
||||
children: [
|
||||
fullName,
|
||||
m('span.progress', rating)
|
||||
' (' + rating + ')'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
@ -27,29 +27,52 @@ function timeControl(c) {
|
|||
}
|
||||
}
|
||||
|
||||
function challenge(c) {
|
||||
return m('div.challenge', [
|
||||
m('i', {
|
||||
'data-icon': c.perf.icon
|
||||
}),
|
||||
m('div.content', [
|
||||
m('span.title', user(c.challenger)),
|
||||
m('span.desc', [
|
||||
c.rated ? 'Rated' : 'Casual',
|
||||
timeControl(c.timeControl),
|
||||
c.variant.name
|
||||
].join(' '))
|
||||
])
|
||||
]);
|
||||
function challenge(ctrl, dir) {
|
||||
return function(c) {
|
||||
return m('div', {
|
||||
class: 'challenge' + (c.declined ? ' declined' : ''),
|
||||
}, [
|
||||
m('i', {
|
||||
'data-icon': c.perf.icon
|
||||
}),
|
||||
m('div.content', [
|
||||
m('span.title', user(c.challenger)),
|
||||
m('span.desc', [
|
||||
c.rated ? 'Rated' : 'Casual',
|
||||
timeControl(c.timeControl),
|
||||
c.variant.name
|
||||
].join(' '))
|
||||
]),
|
||||
m('div.buttons', [
|
||||
m('form', {
|
||||
method: 'post',
|
||||
action: '/challenge/' + c.id + '/accept'
|
||||
}, m('button', {
|
||||
'type': 'submit',
|
||||
class: 'submit button accept',
|
||||
'data-icon': 'E'
|
||||
})),
|
||||
m('form', m('button', {
|
||||
'type': 'submit',
|
||||
class: 'submit button decline',
|
||||
'data-icon': 'L',
|
||||
onclick: function(e) {
|
||||
ctrl.decline(c.id);
|
||||
return false;
|
||||
}
|
||||
}))
|
||||
])
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(ctrl) {
|
||||
if (ctrl.vm.initiating) return m('div.square-wrap', m('div.square-spin'));
|
||||
var d = ctrl.data;
|
||||
return m('div', {
|
||||
class: 'challenges ' + (ctrl.vm.reloading ? ' reloading' : '')
|
||||
class: 'challenges' + (ctrl.vm.reloading ? ' reloading' : '')
|
||||
}, [
|
||||
d.in.map(challenge),
|
||||
d.out.map(challenge),
|
||||
d.in.map(challenge(ctrl, 'in')),
|
||||
d.out.map(challenge(ctrl, 'out')),
|
||||
])
|
||||
};
|
||||
|
|
|
@ -16,5 +16,12 @@ module.exports = {
|
|||
url: uncache('/challenge'),
|
||||
config: xhrConfig,
|
||||
});
|
||||
},
|
||||
decline: function(id) {
|
||||
return m.request({
|
||||
method: 'POST',
|
||||
url: '/challenge/' + id + '/decline',
|
||||
config: xhrConfig,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue