more work on challenges

pull/1558/head
Thibault Duplessis 2016-01-31 20:44:12 +07:00
parent feea61f5f0
commit 6ca7360a6a
15 changed files with 210 additions and 72 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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>

View File

@ -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

View File

@ -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]
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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')),
])
};

View File

@ -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,
});
}
};