aboutsummaryrefslogtreecommitdiff
path: root/shared
diff options
context:
space:
mode:
authorJakob Odersky <jakob@inpher.io>2019-10-09 17:10:43 -0400
committerJakob Odersky <jakob@inpher.io>2019-10-09 20:33:16 -0400
commit0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7 (patch)
tree2df0258f81050e6fed51d38e217c4f6256518e12 /shared
parentfaed28c54900fc0b359700873367095f51425794 (diff)
downloadscala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.tar.gz
scala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.tar.bz2
scala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.zip
Migrate build to mill
Diffstat (limited to 'shared')
-rw-r--r--shared/ApiProtocol.scala12
-rw-r--r--shared/Message.scala42
-rw-r--r--shared/Templates.scala54
-rw-r--r--shared/TextTemplates.scala64
-rw-r--r--shared/http/Backend.scala8
-rw-r--r--shared/http/Request.scala9
-rw-r--r--shared/http/Response.scala6
7 files changed, 195 insertions, 0 deletions
diff --git a/shared/ApiProtocol.scala b/shared/ApiProtocol.scala
new file mode 100644
index 0000000..5d0e00a
--- /dev/null
+++ b/shared/ApiProtocol.scala
@@ -0,0 +1,12 @@
+package triad
+
+import java.time.Instant
+import spray.json.{DerivedJsonProtocol, JsNumber, JsValue, JsonFormat}
+
+object ApiProtocol extends DerivedJsonProtocol {
+ implicit val timestampFormat: JsonFormat[Instant] = new JsonFormat[Instant] {
+ def read(js: JsValue) = Instant.ofEpochMilli(js.convertTo[Long])
+ def write(i: Instant) = JsNumber(i.toEpochMilli)
+ }
+ implicit val messageFormat = jsonFormat[Message]
+}
diff --git a/shared/Message.scala b/shared/Message.scala
new file mode 100644
index 0000000..84c733e
--- /dev/null
+++ b/shared/Message.scala
@@ -0,0 +1,42 @@
+package triad
+
+import java.security.MessageDigest
+import java.time.Instant
+
+case class Message(content: String,
+ author: String,
+ timestamp: Instant = Instant.now()) {
+
+ lazy val id: String = {
+ val digest = MessageDigest.getInstance("SHA-256")
+ digest.update(content.getBytes)
+ digest.update(author.getBytes)
+ digest.update((timestamp.getEpochSecond & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 8) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 16) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 24) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 32) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 40) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 48) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 56) & 0xff).toByte)
+ Message.bytesToHex(digest.digest())
+ }
+
+ def hashTags: Seq[String] =
+ content.split("\\s").filter(_.startsWith("#")).map(_.drop(1))
+
+}
+
+object Message {
+ private def bytesToHex(hash: Array[Byte]): String = {
+ val hexString = new StringBuffer(hash.length * 2)
+ var i = 0
+ while (i < hash.length) {
+ val hex = Integer.toHexString(0xff & hash(i))
+ if (hex.length == 1) hexString.append('0')
+ hexString.append(hex)
+ i += 1
+ }
+ hexString.toString
+ }
+}
diff --git a/shared/Templates.scala b/shared/Templates.scala
new file mode 100644
index 0000000..10ee116
--- /dev/null
+++ b/shared/Templates.scala
@@ -0,0 +1,54 @@
+package triad
+
+class Templates[Builder, Output <: FragT, FragT](
+ val bundle: scalatags.generic.Bundle[Builder, Output, FragT]) {
+ import bundle.all._
+
+ val colorStyles = List(
+ "bg-primary",
+ "bg-secondary",
+ "bg-success",
+ "bg-danger",
+ "bg-warning",
+ "bg-info",
+ "bg-dark"
+ )
+ // pick a "random" style by computing a hash of arbitrary data
+ def dataStyle(data: String) = {
+ val dataHash = data.foldLeft(7) {
+ case (hash, char) =>
+ (hash * 31 + char.toInt)
+ }
+ colorStyles(
+ ((dataHash % colorStyles.length) + colorStyles.length) % colorStyles.length)
+ }
+
+ def message(msg: Message) = {
+ val tags = msg.hashTags.map(
+ hashTag =>
+ span(`class` := "badge badge-light float-right ml-1")(
+ hashTag
+ ))
+ div(`class` := "col-xs-12 col-sm-6 col-md-3 col-lg-2")(
+ div(`class` := s"card text-white mb-3 ${dataStyle(msg.author)}")(
+ div(`class` := "card-header")(
+ msg.author,
+ tags
+ ),
+ div(`class` := "card-body")(
+ div(`class` := "card-text")(
+ msg.content
+ )
+ )
+ )
+ )
+ }
+
+ def conversation(messages: Seq[Message]): Tag =
+ div(`class` := "container-fluid")(
+ div(id := "conversation", `class` := "row")(
+ for (msg <- messages.sortBy(_.timestamp)) yield message(msg)
+ )
+ )
+
+}
diff --git a/shared/TextTemplates.scala b/shared/TextTemplates.scala
new file mode 100644
index 0000000..0dd45c5
--- /dev/null
+++ b/shared/TextTemplates.scala
@@ -0,0 +1,64 @@
+package triad
+
+object TextTemplates extends Templates(scalatags.Text) {
+ import bundle.all._
+
+ def scripts(js: Boolean = true) =
+ if (js)
+ Seq(
+ div(id := "scalajs-error", style := "display: none;")(
+ "ScalaJS raised an exception. See the log for more information."
+ ),
+ script(`type` := "text/javascript",
+ src := "/out.js"),
+ script(`type` := "text/javascript")(
+ raw(
+ """|document.addEventListener("DOMContentLoaded", function(event) {
+ | try {
+ | // root element that will contain the ScalaJS application
+ | var root = document.getElementById("conversation");
+ |
+ | // clear any existing content
+ | while (root.firstChild) {
+ | root.removeChild(root.firstChild);
+ | }
+ |
+ | // run ScalaJS application
+ | console.info("Starting ScalaJS application...")
+ | triad.Main().main(root)
+ | } catch(ex) {
+ | // display warning message in case of exception
+ | document.getElementById("scalajs-error").style.display = "block";
+ | throw ex;
+ | }
+ |});
+ |""".stripMargin
+ )
+ )
+ )
+ else Seq.empty
+
+ def page(messages: Seq[Message], js: Boolean = true) = html(
+ head(
+ link(rel := "stylesheet",
+ `type` := "text/css",
+ href := "/assets/lib/bootstrap-4.1.0/css/bootstrap-reboot.min.css"),
+ link(rel := "stylesheet",
+ `type` := "text/css",
+ href := "/assets/lib/bootstrap-4.1.0/css/bootstrap-grid.min.css"),
+ link(rel := "stylesheet",
+ `type` := "text/css",
+ href := "/assets/lib/bootstrap-4.1.0/css/bootstrap.min.css"),
+ link(rel := "stylesheet",
+ `type` := "text/css",
+ href := "/assets/main.css"),
+ meta(name := "viewport",
+ content := "width=device-width, initial-scale=1, shrink-to-fit=no")
+ ),
+ body(
+ conversation(messages),
+ scripts(js)
+ )
+ )
+
+}
diff --git a/shared/http/Backend.scala b/shared/http/Backend.scala
new file mode 100644
index 0000000..f3ce5f8
--- /dev/null
+++ b/shared/http/Backend.scala
@@ -0,0 +1,8 @@
+package triad
+package http
+
+import scala.concurrent.Future
+
+trait Backend {
+ def send(request: Request): Future[Response]
+}
diff --git a/shared/http/Request.scala b/shared/http/Request.scala
new file mode 100644
index 0000000..ec7d28d
--- /dev/null
+++ b/shared/http/Request.scala
@@ -0,0 +1,9 @@
+package triad
+package http
+
+case class Request(
+ method: String,
+ url: String,
+ headers: Map[String, String] = Map.empty,
+ body: Array[Byte] = Array.empty
+)
diff --git a/shared/http/Response.scala b/shared/http/Response.scala
new file mode 100644
index 0000000..4ba2342
--- /dev/null
+++ b/shared/http/Response.scala
@@ -0,0 +1,6 @@
+package triad
+package http
+
+case class Response(statusCode: Int,
+ headers: Map[String, String],
+ body: Array[Byte])