common/app/model/LiveBlogCurrentPage.scala (213 lines of code) (raw):
package model
import model.liveblog.BodyBlock.{KeyEvent, SummaryEvent}
import model.liveblog.{Blocks, BodyBlock}
case class LiveBlogCurrentPage(
currentPage: PageReference,
pagination: Option[N1Pagination],
pinnedBlock: Option[BodyBlock],
)
// Extends normal Pages due to the need for pagination and since-last-seen logic on
object LiveBlogCurrentPage {
def apply(
pageSize: Int,
blocks: Blocks,
range: BlockRange,
filterKeyEvents: Boolean,
): Option[LiveBlogCurrentPage] = {
range match {
case CanonicalLiveBlog => firstPage(pageSize, blocks, filterKeyEvents)
case PageWithBlock(isRequestedBlock) =>
findPageWithBlock(pageSize, blocks.body, isRequestedBlock, filterKeyEvents)
case SinceBlockId(blockId) => updates(blocks, SinceBlockId(blockId), filterKeyEvents)
case ArticleBlocks => None
case GenericFallback => None
case _ => None
}
}
// filters newer blocks out of the list
def updates(
blocks: Blocks,
sinceBlockId: SinceBlockId,
filterKeyEvents: Boolean,
): Option[LiveBlogCurrentPage] = {
val bodyBlocks = blocks.requestedBodyBlocks.get(sinceBlockId.around).toSeq.flatMap { bodyBlocks =>
val onlyBlocksAfterLastUpdated = bodyBlocks.takeWhile(_.id != sinceBlockId.lastUpdate)
applyFilters(onlyBlocksAfterLastUpdated, filterKeyEvents)
}
Some(
LiveBlogCurrentPage(FirstPage(bodyBlocks, filterKeyEvents), None, None),
) // just pretend to be the first page, it'll be ignored
}
// turns the slimmed down (to save bandwidth) capi response into a first page model object
def firstPage(
pageSize: Int,
blocks: Blocks,
filterKeyEvents: Boolean,
): Option[LiveBlogCurrentPage] = {
val (maybeRequestedBodyBlocks, blockCount, oldestPageBlockId) =
extractFirstPageBlocks(blocks, filterKeyEvents)
val remainder = blockCount % pageSize
val numPages = blockCount / pageSize
maybeRequestedBodyBlocks.map { requestedBodyBlocks =>
val (firstPageBlocks, startOfSecondPageBlocks) = requestedBodyBlocks.splitAt(remainder + pageSize)
val olderPage = startOfSecondPageBlocks.headOption.map { block =>
BlockPage(blocks = Nil, blockId = block.id, pageNumber = 2, filterKeyEvents)
}
val oldestPage = oldestPageBlockId map { blockId =>
BlockPage(blocks = Nil, blockId = blockId, pageNumber = numPages, filterKeyEvents)
}
val pinnedBlocks = blocks.requestedBodyBlocks.get(CanonicalLiveBlog.pinned)
val pinnedBlock = pinnedBlocks.flatMap(_.headOption)
val blocksToDisplay = removeFirstBlockIfPinned(firstPageBlocks, pinnedBlock)
val pinnedBlockRenamed = pinnedBlock.map(renamePinnedBlock)
val pagination = {
if (blockCount > firstPageBlocks.size)
Some(
N1Pagination(
newest = None,
newer = None,
oldest = oldestPage,
older = olderPage,
numberOfPages = numPages,
),
)
else None
}
LiveBlogCurrentPage(FirstPage(blocksToDisplay, filterKeyEvents), pagination, pinnedBlockRenamed)
}
}
private def extractFirstPageBlocks(
blocks: Blocks,
filterKeyEvents: Boolean,
) = {
if (filterKeyEvents) {
getKeyEventsBlocks(blocks)
} else {
getStandardBlocks(blocks)
}
}
private def getStandardBlocks(blocks: Blocks): (Option[Seq[BodyBlock]], Int, Option[String]) = {
val firstPageBlocks = blocks.requestedBodyBlocks.get(CanonicalLiveBlog.firstPage)
val oldestPageBlockId =
blocks.requestedBodyBlocks.get(CanonicalLiveBlog.oldestPage) flatMap (_.headOption.map(_.id))
(firstPageBlocks, blocks.totalBodyBlocks, oldestPageBlockId)
}
private def getKeyEventsBlocks(blocks: Blocks) = {
val keyEventsAndSummaries = for {
keyEvents <- blocks.requestedBodyBlocks.get(CanonicalLiveBlog.timeline)
summaries <- blocks.requestedBodyBlocks.get(CanonicalLiveBlog.summary)
} yield {
(keyEvents ++ summaries).sortBy(_.publishedCreatedTimestamp()).reverse
}
val keyEventsAndSummariesCount = keyEventsAndSummaries.getOrElse(Seq.empty).size
val oldestPageBlockId = keyEventsAndSummaries.flatMap(_.lastOption map (_.id))
(keyEventsAndSummaries, keyEventsAndSummariesCount, oldestPageBlockId)
}
private def removeFirstBlockIfPinned(firstPageBlocks: Seq[BodyBlock], pinnedBlock: Option[BodyBlock]) = {
firstPageBlocks match {
case firstBlock +: otherBlocks if pinnedBlock.contains(firstBlock) => otherBlocks
case _ => firstPageBlocks
}
}
// turns a full capi blocks list into a page model of the page with a specific block in it
def findPageWithBlock(
pageSize: Int,
blocks: Seq[BodyBlock],
requestedBlockId: String,
filterKeyEvents: Boolean,
): Option[LiveBlogCurrentPage] = {
val pages: Seq[PageReference] = pagesFromBodyBlocks(pageSize, blocks, filterKeyEvents)
val indexOfPageWithRequestedBlock = pages.indexWhere(_.blocks.exists(_.id == requestedBlockId))
if (indexOfPageWithRequestedBlock < 0) None
else
Some {
val currentPage = pages(indexOfPageWithRequestedBlock)
val isNewestPage = pages.head == currentPage
LiveBlogCurrentPage(
currentPage = currentPage,
pagination =
if (pages.size <= 1) None
else
Some(
N1Pagination(
newest = if (isNewestPage) None else Some(pages.head),
newer = pages.lift(indexOfPageWithRequestedBlock - 1),
oldest = if (pages.last == currentPage) None else Some(pages.last),
older = pages.lift(indexOfPageWithRequestedBlock + 1),
numberOfPages = pages.size,
),
),
pinnedBlock = if (isNewestPage) blocks.find(_.attributes.pinned).map(renamePinnedBlock) else None,
)
}
}
private def pagesFromBodyBlocks(
pageSize: Int,
blocks: Seq[BodyBlock],
filterKeyEvents: Boolean,
): Seq[PageReference] = {
val (mainPageBlocks, restPagesBlocks) = getPages(pageSize, applyFilters(blocks, filterKeyEvents))
FirstPage(mainPageBlocks, filterKeyEvents) :: restPagesBlocks.zipWithIndex.map { case (page, index) =>
// page number is index + 2 to account for first page and 0-based index of `zipWithIndex`
BlockPage(blocks = page, blockId = page.head.id, pageNumber = index + 2, filterKeyEvents)
}
}
private def renamePinnedBlock(pinnedBlock: BodyBlock): BodyBlock = {
pinnedBlock.copy(id = s"${pinnedBlock.id}-pinned")
}
private def applyFilters(
blocks: Seq[BodyBlock],
filterKeyEvents: Boolean,
) = {
if (filterKeyEvents) {
blocks.filter(block => block.eventType == KeyEvent || block.eventType == SummaryEvent)
} else {
blocks
}
}
// returns the pages, newest at the end, newest at the start
private def getPages[B](pageSize: Int, blocks: Seq[B]): (Seq[B], List[Seq[B]]) = {
val length = blocks.size
val remainder = length % pageSize
val (main, rest) = blocks.splitAt(remainder + pageSize)
(main, rest.grouped(pageSize).toList)
}
}
sealed trait PageReference {
def blocks: Seq[BodyBlock]
def suffix: String
def pageNumber: Int
def isArchivePage: Boolean
}
case class N1Pagination(
newest: Option[PageReference],
newer: Option[PageReference],
oldest: Option[PageReference],
older: Option[PageReference],
numberOfPages: Int,
)
case class FirstPage(blocks: Seq[BodyBlock], filterKeyEvents: Boolean) extends PageReference {
val suffix = s"?filterKeyEvents=$filterKeyEvents"
val pageNumber = 1
val isArchivePage = false
}
case class BlockPage(
blocks: Seq[BodyBlock],
blockId: String,
pageNumber: Int,
filterKeyEvents: Boolean,
) extends PageReference {
val suffix = s"?page=with:block-$blockId&filterKeyEvents=$filterKeyEvents"
val isArchivePage = true
}
object LatestBlock {
def apply(maybeBlocks: Option[Blocks]): Option[String] = {
maybeBlocks.flatMap { blocks =>
blocks.requestedBodyBlocks.getOrElse(CanonicalLiveBlog.firstPage, blocks.body).headOption.map(_.id)
}
}
}
object LatestKeyBlock {
def apply(maybeBlocks: Option[Blocks]): Option[String] = {
maybeBlocks.flatMap { blocks =>
blocks.requestedBodyBlocks
.getOrElse(CanonicalLiveBlog.firstPage, blocks.body)
.find(block => block.eventType == KeyEvent || block.eventType == SummaryEvent)
.map(_.id)
}
}
}