inbox2
Thibault Duplessis 2020-01-24 16:48:23 -06:00
parent a0f9e187b2
commit 4a7498e60d
26 changed files with 337 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,6 +52,7 @@
"ui/tournamentSchedule",
"ui/tournamentCalendar",
"ui/tree",
"ui/msg",
"ui/@build/cssProject",
"ui/@build/jsProject",
"ui/@build/tsProject",

View File

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

View File

View File

@ -0,0 +1,2 @@
@import '../../../common/css/plugin';
@import '../msg';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/dark';
@import 'msg';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/light';
@import 'msg';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/transp';
@import 'msg';

View File

@ -0,0 +1,2 @@
require('@build/tsProject')('LichessMsg', 'lichess.msg', __dirname);
require('@build/cssProject')(__dirname);

View File

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

12
ui/msg/src/ctrl.ts 100644
View File

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

View File

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

28
ui/msg/src/main.ts 100644
View File

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

65
ui/msg/src/view.ts 100644
View File

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

View File

@ -0,0 +1,5 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
}
}