make notifications behave more like challenges

pull/1964/head
Thibault Duplessis 2016-06-02 20:42:53 +02:00
parent bbde53588e
commit efed169648
24 changed files with 134 additions and 201 deletions

View File

@ -87,7 +87,8 @@ final class Env(
Env.slack, // required to load the actor
Env.challenge, // required to load the actor
Env.explorer, // required to load the actor
Env.fishnet // required to schedule the cleaner
Env.fishnet, // required to schedule the cleaner
Env.notifyModule // required to load the actor
)) { lap =>
lila.log("boot").info(s"${lap.millis}ms Preloading complete")
}

View File

@ -302,13 +302,12 @@ private[controllers] trait LilaController
case OnlineFriends(users) => users
} recover { case _ => Nil }) zip
Env.team.api.nbRequests(me.id) zip
Env.message.api.unreadIds(me.id) zip
Env.challenge.api.countInFor(me.id) zip
Env.notifyModule.notifyApi.countUnread(Notifies(me.id))
Env.notifyModule.api.unreadCount(Notifies(me.id)).map(_.value)
}
} map {
case (pref, ((((friends, teamNbRequests), messageIds), nbChallenges), nbNotifications)) =>
PageData(friends, teamNbRequests, messageIds.size, nbChallenges, nbNotifications, pref,
case (pref, (((friends, teamNbRequests), nbChallenges), nbNotifications)) =>
PageData(friends, teamNbRequests, nbChallenges, nbNotifications, pref,
blindMode = blindMode(ctx),
hasFingerprint = hasFingerprint)
}

View File

@ -13,24 +13,11 @@ object Notify extends LilaController {
import env.jsonHandlers._
val appMaxNotifications = 10
def recent(page: Int) = Auth { implicit ctx =>
me =>
val notifies = Notifies(me.id)
env.notifyApi.getNotifications(notifies, page, appMaxNotifications) map {
notifications => Ok(PaginatorJson(notifications)) as JSON
env.api.getNotificationsAndCount(notifies, page) map { res =>
Ok(Json toJson res) as JSON
}
}
def markAllAsRead = Auth {
implicit ctx =>
me =>
val userId = Notifies(me.id)
env.notifyApi.getNotifications(userId, 1, appMaxNotifications) flatMap { notifications =>
env.notifyApi.markAllRead(userId) inject {
Ok(PaginatorJson(notifications)) as JSON
}
}
}
}

View File

@ -598,6 +598,7 @@ insight {
}
notify {
collection.notify = notify
actor.name = notify
}
simulation {
enabled = false
@ -646,6 +647,7 @@ hub {
move_broadcast = ${socket.move_broadcast.name}
user_register = ${socket.user_register.name}
simul = ${simul.actor.name}
notify = ${notify.actor.name}
}
socket {
lobby = ${lobby.socket.name}

View File

@ -267,7 +267,6 @@ POST /challenge/rematch-of/$id<\w{8}> controllers.Challenge.rematchOf(id: Strin
# Notify
GET /notify controllers.Notify.recent(page: Int ?= 1)
POST /notify controllers.Notify.markAllAsRead
# Video
GET /video controllers.Video.index

View File

@ -13,4 +13,6 @@ object PaginatorJson {
"previousPage" -> p.previousPage,
"nextPage" -> p.nextPage,
"nbPages" -> p.nbPages)
implicit def paginatorWrites[A: Writes]: Writes[Paginator[A]] = Writes[Paginator[A]](apply)
}

View File

@ -85,7 +85,7 @@ object Env {
shutup = lila.hub.Env.current.actor.shutup,
hub = lila.hub.Env.current,
detectLanguage = DetectLanguage(lila.common.PlayApp loadConfig "detectlanguage"),
notifyApi = lila.notify.Env.current.notifyApi,
notifyApi = lila.notify.Env.current.api,
relationApi = lila.relation.Env.current.api,
system = lila.common.PlayApp.system)
}

View File

@ -29,6 +29,7 @@ final class Env(config: Config, system: ActorSystem) {
val analyser = select("actor.analyser")
val moveBroadcast = select("actor.move_broadcast")
val userRegister = select("actor.user_register")
val notification = select("actor.notify")
}
object channel {

View File

@ -146,6 +146,10 @@ case class LichessThread(
notification: Boolean = false)
}
package notify {
case class Notified(userId: String)
}
package router {
case class Abs(route: Any)
case class Nolang(route: Any)

View File

@ -1,6 +1,6 @@
package lila.notify
import akka.actor.ActorSystem
import akka.actor._
import com.typesafe.config.Config
final class Env(
@ -9,21 +9,25 @@ final class Env(
getLightUser: lila.common.LightUser.Getter,
system: ActorSystem) {
val settings = new {
val collectionNotifications = config getString "collection.notify"
}
import settings._
private val CollectionNotifications = config getString "collection.notify"
private val ActorName = config getString "actor.name"
val jsonHandlers = new JSONHandlers(getLightUser)
private lazy val repo = new NotificationRepo(coll = db(collectionNotifications))
private lazy val repo = new NotificationRepo(coll = db(CollectionNotifications))
lazy val notifyApi = new NotifyApi(
lazy val api = new NotifyApi(
bus = system.lilaBus,
jsonHandlers = jsonHandlers,
repo = repo)
// api actor
system.actorOf(Props(new Actor {
def receive = {
case lila.hub.actorApi.notify.Notified(userId) =>
api markAllRead Notification.Notifies(userId)
}
}), name = ActorName)
}
object Env {

View File

@ -1,8 +1,7 @@
package lila.notify
import lila.common.LightUser
import lila.user.User
import play.api.libs.json.{ JsValue, Json, Writes }
import play.api.libs.json._
final class JSONHandlers(
getLightUser: LightUser.Getter) {
@ -11,11 +10,11 @@ final class JSONHandlers(
def writeBody(notificationContent: NotificationContent) = {
notificationContent match {
case MentionedInThread(mentionedBy, topic, _, category, postId) =>
Json.obj("mentionedBy" -> lila.user.Env.current.lightUser(mentionedBy.value),
Json.obj("mentionedBy" -> getLightUser(mentionedBy.value),
"topic" -> topic.value, "category" -> category.value,
"postId" -> postId.value)
case InvitedToStudy(invitedBy, studyName, studyId) =>
Json.obj("invitedBy" -> lila.user.Env.current.lightUser(invitedBy.value),
Json.obj("invitedBy" -> getLightUser(invitedBy.value),
"studyName" -> studyName.value,
"studyId" -> studyId.value)
}
@ -36,6 +35,10 @@ final class JSONHandlers(
}
}
import lila.common.paginator.PaginatorJson._
implicit val unreadWrites = Writes[Notification.UnreadCount] { v => JsNumber(v.value) }
implicit val andUnreadWrites: Writes[Notification.AndUnread] = Json.writes[Notification.AndUnread]
implicit val newNotificationWrites: Writes[NewNotification] = new Writes[NewNotification] {
def writes(newNotification: NewNotification) = {

View File

@ -1,5 +1,6 @@
package lila.notify
import lila.common.paginator.Paginator
import lila.notify.MentionedInThread.PostId
import org.joda.time.DateTime
import ornicar.scalalib.Random
@ -19,6 +20,8 @@ case class Notification(
object Notification {
case class UnreadCount(value: Int) extends AnyVal
case class AndUnread(pager: Paginator[Notification], unread: UnreadCount)
case class Notifies(value: String) extends AnyVal with StringValue
case class NotificationRead(value: Boolean) extends AnyVal

View File

@ -15,15 +15,19 @@ final class NotifyApi(
import BSONHandlers.NotificationBSONHandler
import jsonHandlers._
def getNotifications(userId: Notification.Notifies, page: Int, perPage: Int): Fu[Paginator[Notification]] = Paginator(
val perPage = 7
def getNotifications(userId: Notification.Notifies, page: Int): Fu[Paginator[Notification]] = Paginator(
adapter = new Adapter(
collection = repo.coll,
selector = repo.userNotificationsQuery(userId),
projection = $empty,
sort = repo.recentSort),
currentPage = page,
maxPerPage = perPage
)
maxPerPage = perPage)
def getNotificationsAndCount(userId: Notification.Notifies, page: Int): Fu[Notification.AndUnread] =
getNotifications(userId, page) zip unreadCount(userId) map (Notification.AndUnread.apply _).tupled
def markAllRead(userId: Notification.Notifies) =
repo.markAllRead(userId) >> unreadCountCache.remove(userId)
@ -31,20 +35,16 @@ final class NotifyApi(
private val unreadCountCache =
AsyncCache(repo.unreadNotificationsCount, maxCapacity = 20000)
def countUnread(userId: Notification.Notifies) = unreadCountCache(userId)
def addNotification(notification: Notification): Funit = {
def unreadCount(userId: Notification.Notifies): Fu[Notification.UnreadCount] =
unreadCountCache(userId) map Notification.UnreadCount.apply
def addNotification(notification: Notification): Funit =
// Add to database and then notify any connected clients of the new notification
insertOrDiscardNotification(notification) map {
_ ?? {
notif =>
unreadCountCache(notif.notifies).
map(NewNotification(notif, _)).
foreach(notifyConnectedClients)
_ foreach { notif =>
notifyUser(notif.notifies)
}
}
}
def addNotifications(notifications: List[Notification]): Funit = {
notifications.map(addNotification).sequenceFu.void
@ -71,9 +71,9 @@ final class NotifyApi(
notification.some
}
private def notifyConnectedClients(newNotification: NewNotification): Unit = {
val notificationsEventKey = "new_notification"
val notificationEvent = SendTo(newNotification.notification.notifies.value, notificationsEventKey, newNotification)
bus.publish(notificationEvent, 'users)
}
private def notifyUser(notifies: Notification.Notifies): Funit =
getNotificationsAndCount(notifies, 1) map { msg =>
import play.api.libs.json.Json
bus.publish(SendTo(notifies.value, "notifications", Json toJson msg), 'users)
}
}

View File

@ -79,6 +79,9 @@ object Handler {
member push lila.socket.Socket.makeMessage("destsFailure", "Bad dests request")
}
}
case ("notified", _) => member.userId foreach { userId =>
hub.actor.notification ! lila.hub.actorApi.notify.Notified(userId)
}
case _ => // logwarn("Unhandled msg: " + msg)
}

View File

@ -79,7 +79,7 @@ final class Env(
notifier = new StudyNotifier(
messageActor = hub.actor.messenger,
netBaseUrl = NetBaseUrl,
notifyApi = lila.notify.Env.current.notifyApi,
notifyApi = lila.notify.Env.current.api,
relationApi = lila.relation.Env.current.api
),
lightUser = getLightUser,

View File

@ -36,32 +36,42 @@ lichess.challengeApp = (function() {
lichess.notifyApp = (function() {
var instance;
var $element = $('#notify_app');
var $toggle = $('#site_notifications_tag');
var readPending = false;
$toggle.one('mouseover', function() {
var isVisible = function() {
return $element.is(':visible');
};
$toggle.one('mouseover click', function() {
if (!instance) load();
}).on('click', function() {
if (instance) instance.updateAndMarkAsRead();
else readPending = true;
}).click(function() {
setTimeout(function() {
if (instance && isVisible()) instance.setVisible();
}, 200);
});
var load = function() {
var load = function(data) {
var isDev = $('body').data('dev');
lichess.loadCss('/assets/stylesheets/notifyApp.css');
lichess.loadScript("/assets/compiled/lichess.notify" + (isDev ? '' : '.min') + '.js').done(function() {
instance = LichessNotify(document.getElementById('notify_app'), {
instance = LichessNotify($element[0], {
data: data,
isVisible: isVisible,
setCount: function(nb) {
$toggle.attr('data-count', nb);
},
setNotified: function() {
lichess.socket.send('notified');
}
});
if (readPending) {
instance.updateAndMarkAsRead();
readPending = false;
}
});
};
return {};
return {
update: function(data) {
if (!instance) load(data);
else instance.update(data);
}
};
})();
(function() {
@ -169,18 +179,6 @@ lichess.notifyApp = (function() {
message: function(msg) {
$('#chat').chat("append", msg);
},
nbm: function(e) {
$('#message_notifications_tag').attr('data-count', e || 0).parent().toggle(e > 0);
if (e) {
$.sound.newPM();
var inboxDesktopNotification = lichess.storage.get("inboxDesktopNotification") || "0";
var s = e.toString();
if (inboxDesktopNotification !== s) {
lichess.desktopNotification("New inbox message!");
lichess.storage.set("inboxDesktopNotification", s);
}
}
},
new_notification: function(e) {
var notification = e.notification;
@ -484,27 +482,6 @@ lichess.notifyApp = (function() {
setTimeout(updatePowertips, 600);
$('body').on('lichess.content_loaded', updatePowertips);
$('#message_notifications_tag').on('click', function() {
lichess.storage.remove("inboxDesktopNotification");
$.ajax({
url: $(this).data('href'),
success: function(html) {
$('#message_notifications_display').html(html)
.find('a.mark_as_read').click(function() {
$.ajax({
url: $(this).attr('href'),
method: 'post'
});
$(this).parents('.notification').remove();
if ($('#message_notifications_display').children().length === 0)
$('#message_notifications_tag').click();
return false;
});
$('body').trigger('lichess.content_loaded');
}
});
});
function setMoment() {
$("time.moment").removeClass('moment').each(function() {
var parsed = moment(this.getAttribute('datetime'));

View File

@ -276,6 +276,9 @@ lichess.StrongSocket.defaults = {
},
challenges: function(d) {
lichess.challengeApp.update(d);
},
notifications: function(d) {
lichess.notifyApp.update(d);
}
},
params: {

View File

@ -877,12 +877,9 @@ body.offline #reconnecting,
#top .shown a.toggle {
background: #fff;
}
#top .shown #challenge_notifications_tag.toggle {
height: 29px;
}
#top .shown #challenge_notifications_tag.toggle,
#top .shown #site_notifications_tag.toggle {
height: 29px;
height: 29px;
}
#ham-plate {
@ -1156,14 +1153,12 @@ body.fpmenu #fpmenu {
#user_tag {
font-weight: bold;
}
#top div.message_notifications,
#top div.challenge_notifications,
#top div.auth,
#top #themepicker,
#top div.lichess_language {
position: relative;
}
#top div.message_notifications.shown .links,
#top div.challenge_notifications.shown .links,
#top div.site_notifications.shown .links,
#top div.auth.shown .links {
@ -1220,28 +1215,10 @@ body.fpmenu #fpmenu {
#challenge_notifications_tag.none {
display: none!important;
}
#message_notifications_tag span:before,
#challenge_notifications_tag span:before {
font-size: 1.45em;
padding-left: 3px;
}
#top div.message_notifications div.title {
padding: 5px 0;
text-align: center;
}
#message_notifications {
width: 250px;
z-index: 2;
left: -95px;
}
#message_notifications div.notification {
border-bottom: 1px solid #c0c0c0;
transition: background-color 0.13s;
position: relative;
}
#message_notifications div.notification:hover {
background-color: #f0f0f0;
}
#notifications {
position: absolute;
top: -20px;
@ -1265,44 +1242,6 @@ body.fpmenu #fpmenu {
display: flex;
justify-content: space-between;
}
#message_notifications .user_link,
#message_notifications .block .title {
font-weight: bold;
}
#message_notifications_display .content {
display: block;
overflow: hidden;
}
#message_notifications .actions {
position: absolute;
top: 5px;
right: 5px;
z-index: 1;
opacity: 0;
transition: opacity 0.13s;
margin-top: 5px;
text-align: right;
}
#message_notifications a.block {
display: block;
padding: 6px 8px 7px 8px;
}
#message_notifications .actions a {
font-weight: bold;
margin-right: 5px;
margin-left: 10px;
}
#message_notifications .notification:hover .actions {
opacity: 0.7;
}
#message_notifications .actions a:hover {
color: #d85000;
}
#site_notifications_tag .content {
display: block;
overflow: hidden;
}
#site_notifications_tag span:before {
font-size: 1.45em;

View File

@ -164,7 +164,6 @@ 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 #message_notifications div.notification,
body.dark #video .content_box_top,
body.dark #simul .half,
body.dark .donation .links a,
@ -209,8 +208,7 @@ body.dark div.search_status,
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 #message_notifications div.notification:hover {
body.dark #top .language_links button:hover {
background-color: #3e3e3e;
color: #b0b0b0;
}

View File

@ -43,12 +43,16 @@ body.dark #notify_app .site_notification.new:hover {
flex: 0 1 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.site_notification .content span {
.site_notification .content span:first-child {
display: flex;
justify-content: space-between;
}
.site_notification .content span:last-child {
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
.site_notification .content time {
font-size: 0.9em;
font-family: 'Roboto';

View File

@ -3,45 +3,55 @@ var m = require('mithril');
module.exports = function(env) {
this.pager;
this.data;
this.vm = {
initiating: true,
reloading: false
initiating: true
};
this.setPager = function(p) {
this.update = function(data) {
this.data = data;
if (this.data.pager.currentPage === 1 && this.data.unread && env.isVisible()) {
env.setNotified();
this.data.unread = 0;
} else this.notifyNew();
if (data.i18n) this.trans = lichess.trans(data.i18n);
this.vm.initiating = false;
this.vm.reloading = false;
this.pager = p;
env.setCount(this.data.unread);
m.redraw();
}.bind(this);
this.updatePager = function() {
this.vm.reloading = true;
return xhr.load().then(this.setPager);
this.notifyNew = function() {
// if (this.data.unread)
// this.data.pager.currentPageResults.forEach(function(n) {
// if (n.unread) {
// if (!lichess.quietMode) {
// env.show();
// $.sound.newPM();
// }
// lichess.desktopNotification("New notification! <more details here>");
// }
// });
}.bind(this);
this.updateAndMarkAsRead = function() {
this.vm.reloading = true;
return xhr.markAllRead().then(function(p) {
this.setPager(p);
env.setCount(0);
});
this.loadFirstPage = function() {
xhr.load().then(this.update);
}.bind(this);
this.nextPage = function() {
if (!this.pager.nextPage) return;
this.vm.reloading = true;
xhr.load(this.pager.nextPage).then(this.setPager);
if (!this.data.pager.nextPage) return;
xhr.load(this.data.pager.nextPage).then(this.update);
}.bind(this);
this.previousPage = function() {
if (!this.pager.previousPage) return;
this.vm.reloading = true;
xhr.load(this.pager.previousPage).then(this.setPager);
if (!this.data.pager.previousPage) return;
xhr.load(this.data.pager.previousPage).then(this.update);
}.bind(this);
this.updatePager();
this.setVisible = function() {
if (!this.data || this.data.pager.currentPage === 1) this.loadFirstPage();
}.bind(this);
if (env.data) this.update(env.data)
else this.loadFirstPage();
};

View File

@ -13,6 +13,7 @@ module.exports = function(element, opts) {
});
return {
updateAndMarkAsRead: controller.updateAndMarkAsRead
update: controller.update,
setVisible: controller.setVisible
}
}

View File

@ -69,7 +69,7 @@ function recentNotifications(ctrl) {
config: function() {
$('body').trigger('lichess.content_loaded');
}
}, ctrl.pager.currentPageResults.map(drawNotification));
}, ctrl.data.pager.currentPageResults.map(drawNotification));
}
function empty() {
@ -83,15 +83,15 @@ module.exports = function(ctrl) {
if (ctrl.vm.initiating) return m('div.initiating', m.trust(lichess.spinnerHtml));
var nb = ctrl.pager.currentPageResults.length;
var nb = ctrl.data.pager.currentPageResults.length;
return [
ctrl.pager.previousPage ? m('div.pager.prev', {
ctrl.data.pager.previousPage ? m('div.pager.prev', {
'data-icon': 'S',
onclick: ctrl.previousPage
}) : null,
nb ? recentNotifications(ctrl) : empty(),
ctrl.pager.nextPage ? m('div.pager.next', {
ctrl.data.pager.nextPage ? m('div.pager.next', {
'data-icon': 'R',
onclick: ctrl.nextPage
}) : null

View File

@ -19,12 +19,5 @@ module.exports = {
},
config: xhrConfig
});
},
markAllRead: function() {
return m.request({
method: 'POST',
url: '/notify',
config: xhrConfig
});
}
};