msg wip
parent
a0f9e187b2
commit
4a7498e60d
|
@ -93,6 +93,7 @@ final class LilaComponents(ctx: ApplicationLoader.Context)
|
|||
lazy val lobby: Lobby = wire[Lobby]
|
||||
lazy val main: Main = wire[Main]
|
||||
lazy val message: Message = wire[Message]
|
||||
lazy val msg: Msg = wire[Msg]
|
||||
lazy val mod: Mod = wire[Mod]
|
||||
lazy val notifyC: Notify = wire[Notify]
|
||||
lazy val oAuthApp: OAuthApp = wire[OAuthApp]
|
||||
|
|
|
@ -22,6 +22,7 @@ final class Env(
|
|||
val socket: lila.socket.Env,
|
||||
val memo: lila.memo.Env,
|
||||
val message: lila.message.Env,
|
||||
val msg: lila.msg.Env,
|
||||
val i18n: lila.i18n.Env,
|
||||
val game: lila.game.Env,
|
||||
val bookmark: lila.bookmark.Env,
|
||||
|
@ -179,6 +180,7 @@ final class EnvBoot(
|
|||
lazy val hub: lila.hub.Env = wire[lila.hub.Env]
|
||||
lazy val socket: lila.socket.Env = wire[lila.socket.Env]
|
||||
lazy val message: lila.message.Env = wire[lila.message.Env]
|
||||
lazy val msg: lila.msg.Env = wire[lila.msg.Env]
|
||||
lazy val i18n: lila.i18n.Env = wire[lila.i18n.Env]
|
||||
lazy val game: lila.game.Env = wire[lila.game.Env]
|
||||
lazy val bookmark: lila.bookmark.Env = wire[lila.bookmark.Env]
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package controllers
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.app._
|
||||
import lila.common.LightUser.lightUserWrites
|
||||
|
||||
final class Msg(
|
||||
env: Env
|
||||
) extends LilaController(env) {
|
||||
|
||||
def home = Auth { implicit ctx => me =>
|
||||
env.msg.api.threads(me) flatMap env.msg.json.threads(me) map { threads =>
|
||||
Ok(
|
||||
views.html.msg.home(
|
||||
Json.obj(
|
||||
"me" -> me.light,
|
||||
"threads" -> threads
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import play.api.mvc._
|
|||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.config.MaxPerSecond
|
||||
import lila.common.HTTPRequest
|
||||
import lila.hub.LightTeam
|
||||
import lila.security.Granter
|
||||
import lila.team.{ Joined, Motivate, Team => TeamModel }
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package views.html
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.common.String.html.safeJsonValue
|
||||
|
||||
object msg {
|
||||
|
||||
def home(json: JsObject)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
moreCss = frag(cssTag("msg")),
|
||||
moreJs = frag(
|
||||
jsAt(s"compiled/lichess.msg${isProd ?? (".min")}.js"),
|
||||
embedJsUnsafe(
|
||||
s"""LichessMsg.app(document.querySelector('.msg-app'), ${safeJsonValue(
|
||||
Json.obj(
|
||||
"data" -> json,
|
||||
"i18n" -> jsI18n
|
||||
)
|
||||
)})"""
|
||||
)
|
||||
),
|
||||
title = "Lichess Inbox"
|
||||
) {
|
||||
main(cls := "page box msg-app")(
|
||||
"loading"
|
||||
)
|
||||
}
|
||||
|
||||
def jsI18n(implicit ctx: Context) = i18nJsObject(translations)
|
||||
|
||||
private val translations = List(
|
||||
trans.inbox
|
||||
)
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
db.msg_msg.remove({});
|
||||
db.msg_thread.remove({});
|
||||
|
||||
print("Create db.m_thread_sorted");
|
||||
if (true) {
|
||||
if (false) {
|
||||
print("Create db.m_thread_sorted");
|
||||
db.m_thread_sorted.drop();
|
||||
db.m_thread.find({visibleByUserIds:{$size:2}}).forEach(t => {
|
||||
t.visibleByUserIds.sort();
|
||||
|
@ -17,25 +17,35 @@ db.m_thread_sorted.aggregate([
|
|||
|
||||
let first = o.threads[0];
|
||||
|
||||
let posts = [];
|
||||
let msgs = [];
|
||||
|
||||
o.posts.forEach(ps => {
|
||||
ps.forEach(p => posts.push);
|
||||
o.threads.forEach(t => {
|
||||
t.posts.forEach(p => {
|
||||
msgs.push({
|
||||
_id: p.id,
|
||||
thread: first._id,
|
||||
text: p.text,
|
||||
user: p.isByCreator ? t.creatorId : t.invitedId,
|
||||
date: p.createdAt
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
posts.sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
msgs.sort((a,b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
let msgs = posts.map(p => ({
|
||||
_id: p.id,
|
||||
text: p.text,
|
||||
date: p.createdAt,
|
||||
read: p.isRead
|
||||
}));
|
||||
let last = msgs[msgs.length - 1];
|
||||
|
||||
let thread = {
|
||||
_id: first._id,
|
||||
users: o._id.sort(),
|
||||
lastMsg: lastMsg
|
||||
lastMsg: {
|
||||
text: last.text.slice(0, 50),
|
||||
user: last.user,
|
||||
date: last.date,
|
||||
read: !o.threads.find(t => t.posts.find(p => p.isRead))
|
||||
}
|
||||
}
|
||||
|
||||
db.msg_thread.insert(thread);
|
||||
db.msg_msg.insertMany(msgs, {ordered: false});
|
||||
});
|
||||
|
|
19
conf/routes
19
conf/routes
|
@ -415,14 +415,17 @@ POST /forum/post/:id controllers.ForumPost.edit(id: String)
|
|||
GET /forum/redirect/post/:id controllers.ForumPost.redirect(id: String)
|
||||
|
||||
# Message
|
||||
GET /inbox controllers.Message.inbox(page: Int ?= 1)
|
||||
GET /inbox/new controllers.Message.form
|
||||
GET /inbox/unread-count controllers.Message.unreadCount
|
||||
POST /inbox/new controllers.Message.create
|
||||
POST /inbox/batch controllers.Message.batch
|
||||
GET /inbox/$id<\w{8}> controllers.Message.thread(id: String)
|
||||
POST /inbox/$id<\w{8}> controllers.Message.answer(id: String)
|
||||
POST /inbox/$id<\w{8}>/delete controllers.Message.delete(id: String)
|
||||
GET /old/inbox controllers.Message.inbox(page: Int ?= 1)
|
||||
GET /old/inbox/new controllers.Message.form
|
||||
GET /old/inbox/unread-count controllers.Message.unreadCount
|
||||
POST /old/inbox/new controllers.Message.create
|
||||
POST /old/inbox/batch controllers.Message.batch
|
||||
GET /old/inbox/$id<\w{8}> controllers.Message.thread(id: String)
|
||||
POST /old/inbox/$id<\w{8}> controllers.Message.answer(id: String)
|
||||
POST /old/inbox/$id<\w{8}>/delete controllers.Message.delete(id: String)
|
||||
|
||||
# Message
|
||||
GET /inbox controllers.Msg.home
|
||||
|
||||
# Coach
|
||||
GET /coach controllers.Coach.allDefault(page: Int ?= 1)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lila.msg
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.db.BSON
|
||||
import reactivemongo.api.bson._
|
||||
|
||||
private[msg] object BsonHandlers {
|
||||
|
@ -9,7 +10,24 @@ private[msg] object BsonHandlers {
|
|||
implicit val msgContentBSONHandler = Macros.handler[Last]
|
||||
|
||||
implicit val threadIdBSONHandler = stringAnyValHandler[MsgThread.Id](_.value, MsgThread.Id.apply)
|
||||
implicit val threadBSONHandler = Macros.handler[MsgThread]
|
||||
|
||||
implicit val threadBSONHandler = new BSON[MsgThread] {
|
||||
def reads(r: BSON.Reader) = r.strsD("users") match {
|
||||
case List(u1, u2) =>
|
||||
MsgThread(
|
||||
id = r.get[MsgThread.Id]("_id"),
|
||||
user1 = u1,
|
||||
user2 = u2,
|
||||
lastMsg = r.get[Last]("lastMsg")
|
||||
)
|
||||
case x => sys error s"Invalid MsgThread users: $x"
|
||||
}
|
||||
def writes(w: BSON.Writer, t: MsgThread) = $doc(
|
||||
"_id" -> t.id,
|
||||
"users" -> t.users.sorted,
|
||||
"lastMsg" -> t.lastMsg
|
||||
)
|
||||
}
|
||||
|
||||
implicit val msgIdBSONHandler = stringAnyValHandler[Msg.Id](_.value, Msg.Id.apply)
|
||||
implicit val msgBSONHandler = Macros.handler[Msg]
|
||||
|
|
|
@ -7,12 +7,16 @@ import lila.common.config._
|
|||
@Module
|
||||
final class Env(
|
||||
db: lila.db.Db,
|
||||
userRepo: lila.user.UserRepo
|
||||
userRepo: lila.user.UserRepo,
|
||||
lightUserApi: lila.user.LightUserApi,
|
||||
isOnline: lila.socket.IsOnline
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
private val colls = wire[MsgColls]
|
||||
|
||||
lazy val api: MsgApi = wire[MsgApi]
|
||||
|
||||
lazy val json = wire[MsgJson]
|
||||
}
|
||||
|
||||
private class MsgColls(db: lila.db.Db) {
|
||||
|
|
|
@ -14,7 +14,7 @@ final class MsgApi(
|
|||
|
||||
import BsonHandlers._
|
||||
|
||||
def inbox(me: User): Fu[List[MsgThread]] =
|
||||
def threads(me: User): Fu[List[MsgThread]] =
|
||||
colls.thread.ext
|
||||
.find(
|
||||
$doc(
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package lila.msg
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.user.User
|
||||
import lila.common.Json._
|
||||
import lila.common.LightUser
|
||||
|
||||
final class MsgJson(
|
||||
lightUserApi: lila.user.LightUserApi,
|
||||
isOnline: lila.socket.IsOnline
|
||||
) {
|
||||
|
||||
implicit val threadIdWrites: Writes[MsgThread.Id] = Writes.of[String].contramap[MsgThread.Id](_.value)
|
||||
implicit val lastMsgWrites: OWrites[Msg.Last] = Json.writes[Msg.Last]
|
||||
|
||||
def threads(me: User)(threads: List[MsgThread]): Fu[JsArray] =
|
||||
lightUserApi.preloadMany(threads.map(_ other me)) inject Json.arr(
|
||||
threads.map { t =>
|
||||
Json.obj(
|
||||
"id" -> t.id,
|
||||
"contact" -> contact(t other me),
|
||||
"lastMsg" -> t.lastMsg
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private def contact(userId: User.ID): JsObject =
|
||||
LightUser.lightUserWrites
|
||||
.writes(lightUserApi.sync(userId) | LightUser.fallback(userId))
|
||||
.add("online" -> isOnline(userId))
|
||||
}
|
|
@ -5,10 +5,16 @@ import org.joda.time.DateTime
|
|||
import lila.user.User
|
||||
|
||||
case class MsgThread(
|
||||
_id: MsgThread.Id, // random
|
||||
users: List[User.ID], // unique
|
||||
id: MsgThread.Id, // random
|
||||
user1: User.ID,
|
||||
user2: User.ID,
|
||||
lastMsg: Msg.Last
|
||||
)
|
||||
) {
|
||||
|
||||
def users = List(user1, user2)
|
||||
|
||||
def other(user: User) = if (user1 == user.id) user2 else user1
|
||||
}
|
||||
|
||||
object MsgThread {
|
||||
|
||||
|
@ -16,11 +22,12 @@ object MsgThread {
|
|||
|
||||
def make(
|
||||
msg: Msg.Last,
|
||||
orig: User.ID,
|
||||
dest: User.ID
|
||||
user1: User.ID,
|
||||
user2: User.ID
|
||||
): MsgThread = MsgThread(
|
||||
_id = Id(ornicar.scalalib.Random nextString 8),
|
||||
users = List(orig, dest).sorted,
|
||||
id = Id(ornicar.scalalib.Random nextString 8),
|
||||
user1 = user1,
|
||||
user2 = user2,
|
||||
lastMsg = msg
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"ui/tournamentSchedule",
|
||||
"ui/tournamentCalendar",
|
||||
"ui/tree",
|
||||
"ui/msg",
|
||||
"ui/@build/cssProject",
|
||||
"ui/@build/jsProject",
|
||||
"ui/@build/tsProject",
|
||||
|
|
2
ui/build
2
ui/build
|
@ -15,7 +15,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 dasher speech palantir serviceWorker"
|
||||
apps="site msg chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul dasher speech palantir serviceWorker"
|
||||
|
||||
if [ $mode == "upgrade" ]; then
|
||||
yarn upgrade --non-interactive
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/plugin';
|
||||
@import '../msg';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/dark';
|
||||
@import 'msg';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/light';
|
||||
@import 'msg';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/transp';
|
||||
@import 'msg';
|
|
@ -0,0 +1,2 @@
|
|||
require('@build/tsProject')('LichessMsg', 'lichess.msg', __dirname);
|
||||
require('@build/cssProject')(__dirname);
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "msg",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "lichess.org msg",
|
||||
"author": "Thibault Duplessis",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@build/tsProject": "2.0.0",
|
||||
"@types/jquery": "^2.0",
|
||||
"@types/lichess": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"snabbdom": "ornicar/snabbdom#0.7.1-lichess"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { MsgData, MsgOpts } from './interfaces';
|
||||
|
||||
export default class MsgCtrl {
|
||||
|
||||
data: MsgData;
|
||||
trans: Trans;
|
||||
|
||||
constructor(opts: MsgOpts, readonly redraw: () => void) {
|
||||
this.data = opts.data;
|
||||
this.trans = window.lichess.trans(opts.i18n);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
export interface MsgOpts {
|
||||
data: MsgData;
|
||||
i18n: any;
|
||||
}
|
||||
export interface MsgData {
|
||||
me: User;
|
||||
threads: Thread[];
|
||||
thread?: Thread;
|
||||
}
|
||||
export interface Thread {
|
||||
id: string;
|
||||
contact: User;
|
||||
lastMsg: LastMsg;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
patron: boolean;
|
||||
online: boolean;
|
||||
}
|
||||
export interface LastMsg extends BaseMsg {
|
||||
read: boolean;
|
||||
}
|
||||
export interface BaseMsg {
|
||||
user: string;
|
||||
text: string;
|
||||
date: number;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import view from './view';
|
||||
|
||||
import { init } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode'
|
||||
import klass from 'snabbdom/modules/class';
|
||||
import attributes from 'snabbdom/modules/attributes';
|
||||
|
||||
import { MsgOpts } from './interfaces'
|
||||
import MsgCtrl from './ctrl';
|
||||
|
||||
const patch = init([klass, attributes]);
|
||||
|
||||
export function app(element: HTMLElement, opts: MsgOpts) {
|
||||
|
||||
let vnode: VNode, ctrl: MsgCtrl;
|
||||
|
||||
function redraw() {
|
||||
vnode = patch(vnode, view(ctrl));
|
||||
}
|
||||
|
||||
ctrl = new MsgCtrl(opts, redraw);
|
||||
|
||||
const blueprint = view(ctrl);
|
||||
element.innerHTML = '';
|
||||
vnode = patch(element, blueprint);
|
||||
|
||||
redraw();
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import { h } from 'snabbdom'
|
||||
import { VNode } from 'snabbdom/vnode'
|
||||
import { Thread, BaseMsg, User } from './interfaces'
|
||||
import MsgCtrl from './ctrl';
|
||||
|
||||
function userIcon(user: User): VNode {
|
||||
return h('i.line' + (user.patron ? '.patron' : ''));
|
||||
}
|
||||
|
||||
function userName(user: User): Array<string | VNode> {
|
||||
return user.title ? [
|
||||
h(
|
||||
'span.title',
|
||||
user.title == 'BOT' ? { attrs: {'data-bot': true } } : {},
|
||||
user.title
|
||||
), ' ', user.name
|
||||
] : [user.name];
|
||||
}
|
||||
|
||||
function renderDate(msg: BaseMsg) {
|
||||
var date = new Date(msg.date);
|
||||
return h('time.timeago', {
|
||||
attrs: {
|
||||
title: date.toLocaleString(),
|
||||
datetime: msg.date
|
||||
}
|
||||
}, window.lichess.timeago.format(date));
|
||||
}
|
||||
|
||||
function sideThread(thread: Thread) {
|
||||
return h('div.msg-app__threads__thread', [
|
||||
h('a.msg-app__threads__thread__icon.user-link.ulpt', {
|
||||
class: {
|
||||
online: thread.contact.online,
|
||||
offline: !thread.contact.online
|
||||
},
|
||||
attrs: {
|
||||
href: '/@/' + thread.contact.name
|
||||
}
|
||||
}, [userIcon(thread.contact)]),
|
||||
h('div.msg-app__threads__thread__contact', [
|
||||
h('div.msg-app__threads__thread__head', [
|
||||
h('a.msg-app__threads__thread__name', userName(thread.contact)),
|
||||
h('a.msg-app__threads__thread__date', renderDate(thread.lastMsg))
|
||||
]),
|
||||
h('a.msg-app__threads__thread__msg', thread.lastMsg.text)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
export default function(ctrl: MsgCtrl) {
|
||||
return h('div.msg-app', [
|
||||
h('div.msg-app__side', [
|
||||
h('div.msg-app__side__search', [
|
||||
h('input')
|
||||
]),
|
||||
h('div.msg-app__threads', [
|
||||
ctrl.data.threads.map(sideThread)
|
||||
])
|
||||
]),
|
||||
h('div.msg-app__main', [
|
||||
'main'
|
||||
])
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue