opt-out from messages of a team
parent
1e1aeb4d9d
commit
c258d24a6c
|
@ -1,6 +1,7 @@
|
|||
package controllers
|
||||
|
||||
import play.api.data.Form
|
||||
import play.api.data.Forms._
|
||||
import play.api.libs.json._
|
||||
import play.api.mvc._
|
||||
import scala.concurrent.duration._
|
||||
|
@ -189,16 +190,18 @@ final class Team(
|
|||
AuthBody { implicit ctx => implicit me =>
|
||||
LimitPerWeek(me) {
|
||||
implicit val req = ctx.body
|
||||
forms.create.bindFromRequest().fold(
|
||||
err =>
|
||||
forms.anyCaptcha map { captcha =>
|
||||
BadRequest(html.team.form.create(err, captcha))
|
||||
},
|
||||
data =>
|
||||
api.create(data, me) map { team =>
|
||||
Redirect(routes.Team.show(team.id)).flashSuccess
|
||||
}
|
||||
)
|
||||
forms.create
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
err =>
|
||||
forms.anyCaptcha map { captcha =>
|
||||
BadRequest(html.team.form.create(err, captcha))
|
||||
},
|
||||
data =>
|
||||
api.create(data, me) map { team =>
|
||||
Redirect(routes.Team.show(team.id)).flashSuccess
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +238,8 @@ final class Team(
|
|||
},
|
||||
api = _ => {
|
||||
implicit val body = ctx.body
|
||||
forms.apiRequest.bindFromRequest()
|
||||
forms.apiRequest
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
newJsonFormError,
|
||||
msg =>
|
||||
|
@ -254,7 +258,8 @@ final class Team(
|
|||
scoped = implicit req =>
|
||||
me => {
|
||||
implicit val lang = reqLang
|
||||
forms.apiRequest.bindFromRequest()
|
||||
forms.apiRequest
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
newJsonFormError,
|
||||
msg =>
|
||||
|
@ -272,6 +277,17 @@ final class Team(
|
|||
}
|
||||
)
|
||||
|
||||
def subscribe(teamId: String) = {
|
||||
def doSub(req: Request[_], me: UserModel) =
|
||||
Form(single("v" -> boolean))
|
||||
.bindFromRequest()(req)
|
||||
.fold(_ => funit, v => api.subscribe(teamId, me.id, v))
|
||||
AuthOrScopedBody(_.Team.Write)(
|
||||
auth = ctx => me => doSub(ctx.body, me) inject Redirect(routes.Team.show(teamId)),
|
||||
scoped = req => me => doSub(req, me) inject jsonOkResult
|
||||
)
|
||||
}
|
||||
|
||||
def requests =
|
||||
Auth { implicit ctx => me =>
|
||||
import lila.memo.CacheApi._
|
||||
|
@ -290,14 +306,18 @@ final class Team(
|
|||
AuthBody { implicit ctx => me =>
|
||||
OptionFuResult(api.requestable(id, me)) { team =>
|
||||
implicit val req = ctx.body
|
||||
forms.request.bindFromRequest().fold(
|
||||
err =>
|
||||
forms.anyCaptcha map { captcha =>
|
||||
BadRequest(html.team.request.requestForm(team, err, captcha))
|
||||
},
|
||||
setup =>
|
||||
api.createRequest(team, me, setup.message) inject Redirect(routes.Team.show(team.id)).flashSuccess
|
||||
)
|
||||
forms.request
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
err =>
|
||||
forms.anyCaptcha map { captcha =>
|
||||
BadRequest(html.team.request.requestForm(team, err, captcha))
|
||||
},
|
||||
setup =>
|
||||
api.createRequest(team, me, setup.message) inject Redirect(
|
||||
routes.Team.show(team.id)
|
||||
).flashSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,13 +329,15 @@ final class Team(
|
|||
} yield (teamOption |@| requestOption).tupled) {
|
||||
case (team, request) =>
|
||||
implicit val req = ctx.body
|
||||
forms.processRequest.bindFromRequest().fold(
|
||||
_ => fuccess(routes.Team.show(team.id).toString),
|
||||
{
|
||||
case (decision, url) =>
|
||||
api.processRequest(team, request, (decision == "accept")) inject url
|
||||
}
|
||||
)
|
||||
forms.processRequest
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
_ => fuccess(routes.Team.show(team.id).toString),
|
||||
{
|
||||
case (decision, url) =>
|
||||
api.processRequest(team, request, (decision == "accept")) inject url
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -451,16 +473,18 @@ final class Team(
|
|||
}
|
||||
|
||||
private def doPmAll(team: TeamModel, me: UserModel)(implicit req: Request[_]): Either[Form[_], Funit] =
|
||||
forms.pmAll.bindFromRequest()
|
||||
forms.pmAll
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
err => Left(err),
|
||||
msg =>
|
||||
Right {
|
||||
PmAllLimitPerUser(me.id) {
|
||||
val url = s"${env.net.baseUrl}${routes.Team.show(team.id)}"
|
||||
val full = s"""$msg
|
||||
---
|
||||
You received this message because you are part of the team lichess.org${routes.Team.show(team.id)}."""
|
||||
env.msg.api.multiPost(me, env.team.memberStream.ids(team, MaxPerSecond(50)), full)
|
||||
You received this because you are subscribed to messages of the team $url."""
|
||||
env.msg.api.multiPost(me, env.team.memberStream.subscribedIds(team, MaxPerSecond(50)), full)
|
||||
funit // we don't wait for the stream to complete, it would make lichess time out
|
||||
}(funit)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ case class TeamInfo(
|
|||
mine: Boolean,
|
||||
ledByMe: Boolean,
|
||||
requestedByMe: Boolean,
|
||||
subscribed: Boolean,
|
||||
requests: List[RequestWithUser],
|
||||
forumPosts: List[MiniForumPost],
|
||||
tours: TeamInfo.PastAndNext
|
||||
|
@ -51,12 +52,14 @@ final class TeamInfoApi(
|
|||
requests <- (team.enabled && me.exists(m => team.leaders(m.id))) ?? api.requestsWithUsers(team)
|
||||
mine <- me.??(m => api.belongsTo(team.id, m.id))
|
||||
requestedByMe <- !mine ?? me.??(m => requestRepo.exists(team.id, m.id))
|
||||
subscribed <- me.ifTrue(mine) ?? { api.isSubscribed(team, _) }
|
||||
forumPosts <- forumRecent.team(team.id)
|
||||
tours <- tournaments(team, 5, 5)
|
||||
} yield TeamInfo(
|
||||
mine = mine,
|
||||
ledByMe = me.exists(m => team.leaders(m.id)),
|
||||
requestedByMe = requestedByMe,
|
||||
subscribed = subscribed,
|
||||
requests = requests,
|
||||
forumPosts = forumPosts,
|
||||
tours = tours
|
||||
|
|
|
@ -99,6 +99,17 @@ object show {
|
|||
if (info.requestedByMe) strong(beingReviewed())
|
||||
else ctx.isAuth option joinButton(t)
|
||||
),
|
||||
ctx.userId.ifTrue(t.enabled && info.mine) map { myId =>
|
||||
postForm(
|
||||
cls := "team-show__subscribe form3",
|
||||
action := routes.Team.subscribe(t.id)
|
||||
)(
|
||||
div(
|
||||
span(form3.cmnToggle("team-subscribe", "subscribe", checked = info.subscribed)),
|
||||
label(`for` := "team-subscribe")("Subscribe to team messages")
|
||||
)
|
||||
)
|
||||
},
|
||||
(info.mine && !info.ledByMe) option
|
||||
postForm(cls := "quit", action := routes.Team.quit(t.id))(
|
||||
submitButton(cls := "button button-empty button-red confirm")(quitTeam.txt())
|
||||
|
|
|
@ -304,6 +304,7 @@ GET /team/:id/users controllers.Team.legacyUsers(id: S
|
|||
GET /team/:id/tournaments controllers.Team.tournaments(id: String)
|
||||
GET /team/:id/pm-all controllers.Team.pmAll(id: String)
|
||||
POST /team/:id/pm-all controllers.Team.pmAllSubmit(id: String)
|
||||
POST /team/:id/subscribe controllers.Team.subscribe(id: String)
|
||||
# Team API
|
||||
GET /api/team/all controllers.Team.apiAll(page: Int ?= 1)
|
||||
GET /api/team/search controllers.Team.apiSearch(text: String ?= "", page: Int ?= 1)
|
||||
|
|
|
@ -10,38 +10,48 @@ final class MemberRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCo
|
|||
|
||||
import BSONHandlers._
|
||||
|
||||
type ID = String
|
||||
|
||||
// expensive with thousands of members!
|
||||
def userIdsByTeam(teamId: ID): Fu[Set[ID]] =
|
||||
def userIdsByTeam(teamId: Team.ID): Fu[Set[Team.ID]] =
|
||||
coll.secondaryPreferred.distinctEasy[String, Set]("user", $doc("team" -> teamId))
|
||||
|
||||
def teamIdsByUser(userId: User.ID): Fu[Set[ID]] =
|
||||
coll.distinctEasy[ID, Set]("team", $doc("user" -> userId))
|
||||
def teamIdsByUser(userId: User.ID): Fu[Set[Team.ID]] =
|
||||
coll.distinctEasy[Team.ID, Set]("team", $doc("user" -> userId))
|
||||
|
||||
def removeByteam(teamId: ID): Funit =
|
||||
def removeByteam(teamId: Team.ID): Funit =
|
||||
coll.delete.one(teamQuery(teamId)).void
|
||||
|
||||
def removeByUser(userId: User.ID): Funit =
|
||||
coll.delete.one(userQuery(userId)).void
|
||||
|
||||
def exists(teamId: ID, userId: User.ID): Fu[Boolean] =
|
||||
def exists(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
|
||||
coll.exists(selectId(teamId, userId))
|
||||
|
||||
def add(teamId: ID, userId: User.ID): Funit =
|
||||
def add(teamId: Team.ID, userId: User.ID): Funit =
|
||||
coll.insert.one(Member.make(team = teamId, user = userId)).void
|
||||
|
||||
def remove(teamId: ID, userId: User.ID): Fu[WriteResult] =
|
||||
def remove(teamId: Team.ID, userId: User.ID): Fu[WriteResult] =
|
||||
coll.delete.one(selectId(teamId, userId))
|
||||
|
||||
def countByTeam(teamId: ID): Fu[Int] =
|
||||
def countByTeam(teamId: Team.ID): Fu[Int] =
|
||||
coll.countSel(teamQuery(teamId))
|
||||
|
||||
def filterUserIdsInTeam(teamId: ID, userIds: Set[User.ID]): Fu[Set[User.ID]] =
|
||||
def filterUserIdsInTeam(teamId: Team.ID, userIds: Set[User.ID]): Fu[Set[User.ID]] =
|
||||
userIds.nonEmpty ??
|
||||
coll.distinctEasy[User.ID, Set]("user", $inIds(userIds.map { Member.makeId(teamId, _) }))
|
||||
|
||||
def teamQuery(teamId: ID) = $doc("team" -> teamId)
|
||||
private def selectId(teamId: ID, userId: ID) = $id(Member.makeId(teamId, userId))
|
||||
private def userQuery(userId: ID) = $doc("user" -> userId)
|
||||
def isSubscribed(team: Team, user: User): Fu[Boolean] =
|
||||
!coll.exists(selectId(team.id, user.id) ++ $doc("unsub" -> true))
|
||||
|
||||
def subscribe(teamId: Team.ID, userId: User.ID, v: Boolean): Funit =
|
||||
coll.update
|
||||
.one(
|
||||
selectId(teamId, userId),
|
||||
if (v) $unset("unsub")
|
||||
else $set("unsub" -> true)
|
||||
)
|
||||
.void
|
||||
|
||||
def teamQuery(teamId: Team.ID) = $doc("team" -> teamId)
|
||||
private def selectId(teamId: Team.ID, userId: User.ID) = $id(Member.makeId(teamId, userId))
|
||||
private def userQuery(userId: User.ID) = $doc("user" -> userId)
|
||||
}
|
||||
|
|
|
@ -75,6 +75,10 @@ final class TeamApi(
|
|||
def mine(me: User): Fu[List[Team]] =
|
||||
cached teamIdsList me.id flatMap teamRepo.byIdsSortPopular
|
||||
|
||||
def isSubscribed = memberRepo.isSubscribed _
|
||||
|
||||
def subscribe = memberRepo.subscribe _
|
||||
|
||||
def countTeamsOf(me: User) =
|
||||
cached teamIdsList me.id dmap (_.size)
|
||||
|
||||
|
@ -177,7 +181,7 @@ final class TeamApi(
|
|||
timeline ! Propagate(TeamJoin(user.id, team.id)).toFollowersOf(user.id)
|
||||
Bus.publish(JoinTeam(id = team.id, userId = user.id), "team")
|
||||
}
|
||||
} recover lila.db.recoverDuplicateKey(_ => ())
|
||||
} recover lila.db.ignoreDuplicateKey
|
||||
}
|
||||
|
||||
def quit(teamId: Team.ID, me: User): Fu[Option[Team]] =
|
||||
|
|
|
@ -22,13 +22,17 @@ final class TeamMemberStream(
|
|||
.mapAsync(1)(userRepo.usersFromSecondary)
|
||||
.mapConcat(identity)
|
||||
|
||||
def ids(team: Team, perSecond: MaxPerSecond): Source[User.ID, _] =
|
||||
idsBatches(team, perSecond)
|
||||
def subscribedIds(team: Team, perSecond: MaxPerSecond): Source[User.ID, _] =
|
||||
idsBatches(team, perSecond, $doc("unsub" $ne true))
|
||||
.mapConcat(identity)
|
||||
|
||||
private def idsBatches(team: Team, perSecond: MaxPerSecond): Source[Seq[User.ID], _] =
|
||||
private def idsBatches(
|
||||
team: Team,
|
||||
perSecond: MaxPerSecond,
|
||||
selector: Bdoc = $empty
|
||||
): Source[Seq[User.ID], _] =
|
||||
memberRepo.coll.ext
|
||||
.find($doc("team" -> team.id), $doc("user" -> true))
|
||||
.find($doc("team" -> team.id) ++ selector, $doc("user" -> true))
|
||||
.sort($sort desc "date")
|
||||
.batchSize(perSecond.value)
|
||||
.cursor[Bdoc](ReadPreference.secondaryPreferred)
|
||||
|
|
|
@ -131,4 +131,13 @@ $section-margin-more: 5vh;
|
|||
margin: 10px 10px 0 0;
|
||||
}
|
||||
}
|
||||
&__subscribe {
|
||||
div {
|
||||
@extend %flex-center;
|
||||
label {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -874,6 +874,12 @@
|
|||
function startTeam(cfg) {
|
||||
lichess.socket = lichess.StrongSocket('/team/' + cfg.id, cfg.socketVersion);
|
||||
cfg.chat && lichess.makeChat(cfg.chat);
|
||||
$('#team-subscribe').on('change', function() {
|
||||
const v = this.checked;
|
||||
$(this).parents('form').each(function() {
|
||||
$.post($(this).attr('action'), { v: v });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
////////////////
|
||||
|
|
Loading…
Reference in New Issue