Skip to content

Commit

Permalink
Merge pull request #16629 from schlawg/streamer-approval-cleanup
Browse files Browse the repository at this point in the history
improve/ruin streamer approval experience
  • Loading branch information
ornicar authored Dec 20, 2024
2 parents a21fe82 + 2888ff9 commit c858bee
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 110 deletions.
39 changes: 19 additions & 20 deletions app/controllers/Streamer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,28 @@ final class Streamer(env: Env, apiC: => Api) extends LilaController(env):
modData(s.streamer).flatMap: forMod =>
BadRequest.page(views.streamer.edit(sws, error, forMod)),
data =>
api.update(sws.streamer, data, isGranted(_.Streamers)).flatMap { change =>
if change.decline then logApi.streamerDecline(s.user.id)
change.list.foreach { logApi.streamerList(s.user.id, _) }
change.tier.foreach { logApi.streamerTier(s.user.id, _) }
if data.approval.flatMap(_.quick).isDefined
then
env.streamer.pager.nextRequestId.map: nextId =>
Redirect:
nextId.fold(s"${routes.Streamer.index()}?requests=1"): id =>
s"${routes.Streamer.edit.url}?u=$id"
else
val next = if sws.streamer.is(me) then "" else s"?u=${sws.user.id}"
Redirect(s"${routes.Streamer.edit.url}$next")
}
api
.update(sws.streamer, data, isGranted(_.Streamers))
.flatMap:
case Some(change) =>
if change.decline then logApi.streamerDecline(s.user.id)
change.list.foreach { logApi.streamerList(s.user.id, _) }
change.tier.foreach { logApi.streamerTier(s.user.id, _) }
if data.approval.flatMap(_.quick).isDefined
then
env.streamer.pager.nextRequestId.map: nextId =>
Redirect:
nextId.fold(s"${routes.Streamer.index()}?requests=1"): id =>
s"${routes.Streamer.edit.url}?u=$id"
else
val next = if sws.streamer.is(me) then "" else s"?u=${sws.user.id}"
Redirect(s"${routes.Streamer.edit.url}$next")
case _ =>
Redirect(routes.Streamer.edit)
)
}
}

def approvalRequest = AuthBody { _ ?=> me ?=>
NoBot:
api.approval.request(me).inject(Redirect(routes.Streamer.edit))
}

def pictureApply = AuthBody(parse.multipartFormData) { ctx ?=> me ?=>
AsStreamer: s =>
ctx.body.body.file("picture") match
Expand All @@ -135,7 +134,7 @@ final class Streamer(env: Env, apiC: => Api) extends LilaController(env):
.recoverWith { case e: Exception =>
Redirect(routes.Streamer.edit).flashFailure
}
.inject(Redirect(routes.Streamer.edit))
.inject(Ok)
case None => Redirect(routes.Streamer.edit).flashFailure
}

Expand Down
1 change: 0 additions & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,6 @@ GET /streamer/live controllers.Main.movedPermanently(to = "/
GET /streamer/edit controllers.Streamer.edit
POST /streamer/new controllers.Streamer.create
POST /streamer/edit controllers.Streamer.editApply
POST /streamer/approval/request controllers.Streamer.approvalRequest
POST /streamer/subscribe/:streamer controllers.Streamer.subscribe(streamer: UserStr, set: Boolean ?= true)
POST /upload/image/streamer controllers.Streamer.pictureApply
GET /streamer/:username controllers.Streamer.show(username: UserStr)
Expand Down
5 changes: 3 additions & 2 deletions modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2331,10 +2331,11 @@ object I18nKey:
val `pendingReview`: I18nKey = "streamer:pendingReview"
val `pleaseFillIn`: I18nKey = "streamer:pleaseFillIn"
val `whenReady`: I18nKey = "streamer:whenReady"
val `requestReview`: I18nKey = "streamer:requestReview"
val `submitForReview`: I18nKey = "streamer:submitForReview"
val `streamerLanguageSettings`: I18nKey = "streamer:streamerLanguageSettings"
val `twitchUsername`: I18nKey = "streamer:twitchUsername"
val `optionalOrEmpty`: I18nKey = "streamer:optionalOrEmpty"
val `twitchOrYouTubeRequired`: I18nKey = "streamer:twitchOrYouTubeRequired"
val `twitchOrYouTubeMustBeVerified`: I18nKey = "streamer:twitchOrYouTubeMustBeVerified"
val `youTubeChannelId`: I18nKey = "streamer:youTubeChannelId"
val `streamerName`: I18nKey = "streamer:streamerName"
val `visibility`: I18nKey = "streamer:visibility"
Expand Down
2 changes: 1 addition & 1 deletion modules/streamer/src/main/Streamer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ case class Streamer(

def completeEnough = {
twitch.isDefined || youTube.isDefined
} && headline.isDefined && hasPicture
} && name.value.length > 2 && hasPicture

object Streamer:

Expand Down
36 changes: 24 additions & 12 deletions modules/streamer/src/main/StreamerApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,46 @@ final class StreamerApi(
candidateIds <- cache.candidateIds.getUnit
yield if streams.map(_.streamer.id).exists(candidateIds.contains) then cache.candidateIds.invalidateUnit()

def update(prev: Streamer, data: StreamerForm.UserData, asMod: Boolean): Fu[Streamer.ModChange] =
def update(prev: Streamer, data: StreamerForm.UserData, asMod: Boolean): Fu[Option[Streamer.ModChange]] =
val streamer = data(prev, asMod)
for
_ <- coll.update.one($id(streamer.id), streamer)
_ = cache.listedIds.invalidateUnit()
_ = streamer.youTube.foreach(tuber => ytApi.channelSubscribe(tuber.channelId, true))
yield modChange(prev, streamer)
coll.update
.one($id(streamer.id), streamer)
.map: _ =>
asMod.option:
cache.listedIds.invalidateUnit()
streamer.youTube
.foreach(tuber => ytApi.channelSubscribe(tuber.channelId, true))
modChange(prev, streamer)

def forceCheck(uid: UserId): Funit =
byId(uid.into(Streamer.Id)).map:
_.filter(_.approval.granted).so: s =>
s.youTube.foreach(ytApi.forceCheckWithHtmlScraping)

private def modChange(prev: Streamer, current: Streamer): Streamer.ModChange =
val list = (prev.approval.granted != current.approval.granted).option(current.approval.granted)
(~list).so(
if !current.approval.granted then
notifyApi.notifyOne(
current,
lila.core.notify.NotificationContent.GenericLink(
url = "/streamer/edit",
url = streamerPageActivationRoute.url,
title = "Streamer application denied".some,
text =
"Your streamer application was denied. Click here to read instructions before resubmitting.".some,
icon = lila.ui.Icon.Mic.value
)
)
else if !prev.approval.granted && current.approval.granted then
notifyApi.notifyOne(
current,
lila.core.notify.NotificationContent.GenericLink(
url = routes.Streamer.edit.url,
title = "Listed on /streamer".some,
text = "Your streamer page is public".some,
icon = lila.ui.Icon.Mic.value
)
)
)
Streamer.ModChange(
list = list,
list = (prev.approval.granted != current.approval.granted).option(current.approval.granted),
tier = (prev.approval.tier != current.approval.tier).option(current.approval.tier),
decline = !current.approval.granted && !current.approval.requested && prev.approval.requested
)
Expand Down Expand Up @@ -146,7 +158,7 @@ final class StreamerApi(
picfitApi
.uploadFile(s"streamer:${s.id}", picture, userId = by.id)
.flatMap { pic =>
coll.update.one($id(s.id), $set("picture" -> pic.id)).void
coll.update.one($id(s.id), $set("picture" -> pic.id, "approval.requested" -> true)).void
}

// unapprove after 6 weeks if you never streamed (was originally 1 week)
Expand Down
25 changes: 12 additions & 13 deletions modules/streamer/src/main/StreamerForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,34 +76,33 @@ object StreamerForm:
def apply(streamer: Streamer, asMod: Boolean) =
val liveVideoId = streamer.youTube.flatMap(_.liveVideoId)
val pubsubVideoId = streamer.youTube.flatMap(_.pubsubVideoId)
val newStreamer = streamer.copy(
val newTwitch = twitch.flatMap(Twitch.parseUserId).map(Twitch.apply)
val newYouTube =
youTube.flatMap(YouTube.parseChannelId).map(YouTube.apply(_, liveVideoId, pubsubVideoId))
val urlChanges = newTwitch != streamer.twitch || newYouTube != streamer.youTube
streamer.copy(
name = name,
headline = headline,
description = description,
twitch = twitch.flatMap(Twitch.parseUserId).map(Twitch.apply),
youTube = youTube.flatMap(YouTube.parseChannelId).map(YouTube.apply(_, liveVideoId, pubsubVideoId)),
twitch = newTwitch,
youTube = newYouTube,
listed = listed,
updatedAt = nowInstant
)
newStreamer.copy(
updatedAt = nowInstant,
approval = approval.map(_.resolve) match
case Some(m) if asMod =>
streamer.approval.copy(
granted = m.granted,
tier = m.tier | streamer.approval.tier,
requested = !m.granted && {
if streamer.approval.requested != m.requested then m.requested
else streamer.approval.requested || m.requested
},
requested = !m.granted && m.requested,
ignored = m.ignored && !m.granted,
chatEnabled = m.chat,
lastGrantedAt = m.granted.option(nowInstant).orElse(streamer.approval.lastGrantedAt)
)
case _ =>
streamer.approval.copy(
granted = streamer.approval.granted &&
newStreamer.twitch.forall(streamer.twitch.has) &&
newStreamer.youTube.forall(streamer.youTube.has)
requested = streamer.approval.requested || urlChanges || name != streamer.name
|| headline != streamer.headline || description != streamer.description,
granted = streamer.approval.granted && !urlChanges
)
)

Expand Down
3 changes: 3 additions & 0 deletions modules/streamer/src/main/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ export lila.core.lilaism.Lilaism.{ *, given }
export lila.common.extensions.*

private val logger = lila.log("streamer")

private val streamerPageActivationRoute =
routes.Cms.lonePage(lila.core.id.CmsPageKey("streamer-page-activation"))
2 changes: 1 addition & 1 deletion modules/streamer/src/main/ui/StreamerBits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ final class StreamerBits(helpers: Helpers)(picfitUrl: lila.core.misc.PicfitUrl):
a(href := routes.Cms.lonePage(CmsPageKey("streaming-fairplay-faq")))(trs.streamingFairplayFAQ())
)
),
li(a(href := routes.Cms.lonePage(CmsPageKey("streamer-page-activation")))(trs.rule3()))
li(a(href := streamerPageActivationRoute.url)(trs.rule3()))
),
h2(trs.perks()),
ul(
Expand Down
88 changes: 49 additions & 39 deletions modules/streamer/src/main/ui/StreamerEdit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ final class StreamerEdit(helpers: Helpers, bits: StreamerBits):
def apply(s: Streamer.WithUserAndStream, form: Form[?], modZone: Option[(Frag, List[Streamer])])(using
ctx: Context
) =
val wasListed = s.streamer.approval.lastGrantedAt.isDefined
Page(s"${s.user.titleUsername} ${trs.lichessStreamer.txt()}")
.css("bits.streamer.form")
.js(esmInitBit("streamer")):
.i18n(_.streamer)
.js(
esmInitObj(
"bits.streamer",
"youtube" -> wasListed.so(s.streamer.youTube).so[String](_.channelId),
"twitch" -> wasListed.so(s.streamer.twitch).so[String](_.userId)
)
):
main(cls := "page-menu")(
bits.menu("edit", s.some),
div(cls := "page-menu__content box streamer-edit")(
Expand All @@ -36,44 +44,42 @@ final class StreamerEdit(helpers: Helpers, bits: StreamerBits):
)
else bits.header(s, modZone.isDefined),
div(cls := "box-pad") {
val granted = s.streamer.approval.granted
val granted = s.streamer.approval.granted
val requested = s.streamer.approval.requested
val (clas, icon) = (granted, requested, wasListed) match
case (true, true, _) => ("status is-green", Icon.Search)
case (true, false, _) => ("status is-green", Icon.Checkmark)
case (false, true, _) => ("status is-gold", Icon.CautionTriangle)
case (false, false, true) => ("status is-red", Icon.X)
case (false, false, false) => ("status is", Icon.InfoCircle)
frag(
(ctx.is(s.user) && s.streamer.listed.value).option(
div(
cls := s"status is${granted.so("-green")}",
dataIcon := (if granted then Icon.Checkmark else Icon.InfoCircle)
)(
if granted then
frag(
trs.approved(),
(s.streamer.approval.tier > 0).option:
frag(
br,
strong("You have been selected for frontpage featuring!"),
p(
"Note that we can only show a limited number of streams on the homepage, ",
"so yours may not always appear."
)
)
)
else
frag(
if s.streamer.approval.requested then trs.pendingReview()
else
frag(
if s.streamer.completeEnough then
trs.whenReady(
postForm(action := routes.Streamer.approvalRequest)(
button(tpe := "submit", cls := "button", ctx.isnt(s.user).option(disabled))(
trs.requestReview()
)
)
(ctx.is(s.user) && s.streamer.listed.value)
.option(
div(cls := clas, dataIcon := icon)(
if granted then
frag(
if requested then "Changes are under review." else trs.approved(),
(s.streamer.approval.tier > 0).option:
frag(
br,
strong("You have been selected for frontpage featuring!"),
p(
"Note that we can only show a limited number of streams on the homepage, ",
"so yours may not always appear."
)
else trs.pleaseFillIn()
)
)
else if requested then trs.pendingReview()
else if wasListed then
frag(
"Your previous application was declined. ",
a(href := streamerPageActivationRoute)(
"See instructions before submitting again"
)
)
)
),
)
else trs.pleaseFillIn()
)
),
ctx.is(s.user).option(div(cls := "status")(trs.streamerLanguageSettings())),
modZone.map: (modFrag, same) =>
frag(
Expand Down Expand Up @@ -163,13 +169,13 @@ final class StreamerEdit(helpers: Helpers, bits: StreamerBits):
form3.group(
form("twitch"),
trs.twitchUsername(),
help = trs.optionalOrEmpty().some,
help = trs.twitchOrYouTubeRequired().some,
half = true
)(form3.input(_)),
form3.group(
form("youTube"),
trs.youTubeChannelId(),
help = trs.optionalOrEmpty().some,
help = trs.twitchOrYouTubeRequired().some,
half = true
)(form3.input(_))
),
Expand All @@ -195,7 +201,11 @@ final class StreamerEdit(helpers: Helpers, bits: StreamerBits):
form3.group(form("description"), trs.longDescription())(form3.textarea(_)(rows := 10)),
form3.actions(
a(href := routes.Streamer.show(s.user.username))(trans.site.cancel()),
form3.submit(trans.site.apply())
button(
tpe := "submit",
cls := "submit button text approval-request-submit",
title := "You must provide an image, a streamer name, and a Twitch or YouTube channel."
)(trs.submitForReview())
)
)
)
Expand Down
4 changes: 2 additions & 2 deletions modules/streamer/src/main/ui/StreamerUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final class StreamerUi(helpers: Helpers, bits: StreamerBits)(using netDomain: Ne
Page(title)
.css("bits.streamer.list")
.js(infiniteScrollEsmInit)
.js(esmInitBit("streamer")):
.js(esmInitBit("streamerSubscribe")):
main(cls := "page-menu")(
bits.menu(if requests then "requests" else "index", none)(cls := " page-menu__menu"),
div(cls := "page-menu__content box streamer-list")(
Expand Down Expand Up @@ -106,7 +106,7 @@ final class StreamerUi(helpers: Helpers, bits: StreamerBits)(using netDomain: Ne
Page(s"${s.titleName} streams chess")
.csp(csp)
.css("bits.streamer.show")
.js(esmInitBit("streamer"))
.js(esmInitBit("streamerSubscribe"))
.graph(
OpenGraph(
title = s"${s.titleName} streams chess",
Expand Down
7 changes: 5 additions & 2 deletions translation/source/streamer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
<string name="pendingReview">Your stream is being reviewed by moderators.</string>
<string name="pleaseFillIn">Please fill in your streamer information, and upload a picture.</string>
<string name="whenReady" comment="whenReady&#10;&#10;%s is &quot;request a moderator review&quot;">When you are ready to be listed as a Lichess streamer, %s</string>
<string name="requestReview" comment="requestReview&#10;&#10;Used in the sense of: &quot;When you are ready to be listed as a Lichess streamer, request a moderator review&quot;">request a moderator review</string>
<string name="submitForReview">Submit for review</string>
<string name="streamerLanguageSettings">The Lichess streamer page targets your audience with the language provided by your streaming platform. Set the correct default language for your chess streams in the app or service you use to broadcast.</string>
<string name="twitchUsername">Your Twitch username or URL</string>
<string name="optionalOrEmpty">Optional. Leave empty if none</string>
<string name="twitchOrYouTubeRequired">Either Twitch or YouTube is required</string>
<string name="twitchOrYouTubeMustBeVerified">Twitch and YouTube changes must be verified.

Your streamer badge and listing will be suspended while the review is in process. This can take up to 72 hours.</string>
<string name="youTubeChannelId">Your YouTube channel ID</string>
<string name="streamerName">Your streamer name on Lichess</string>
<plurals name="keepItShort">
Expand Down
Loading

0 comments on commit c858bee

Please sign in to comment.