Add embeddable posts
authorEgor Kislitsyn <egor@kislitsyn.com>
Fri, 20 Mar 2020 17:19:34 +0000 (21:19 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Fri, 20 Mar 2020 17:46:26 +0000 (21:46 +0400)
lib/pleroma/web/embed_controller.ex [new file with mode: 0644]
lib/pleroma/web/endpoint.ex
lib/pleroma/web/router.ex
lib/pleroma/web/templates/embed/_attachment.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/embed/show.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/layout/embed.html.eex [new file with mode: 0644]
lib/pleroma/web/views/embed_view.ex [new file with mode: 0644]
priv/static/embed.css [new file with mode: 0644]
priv/static/embed.js [new file with mode: 0644]

diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
new file mode 100644 (file)
index 0000000..f6b8a5e
--- /dev/null
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.EmbedController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.User
+
+  alias Pleroma.Web.ActivityPub.Visibility
+
+  plug(:put_layout, :embed)
+
+  def show(conn, %{"id" => id}) do
+    with %Activity{local: true} = activity <-
+           Activity.get_by_id_with_object(id),
+         true <- Visibility.is_public?(activity.object) do
+      {:ok, author} = User.get_or_fetch(activity.object.data["actor"])
+
+      conn
+      |> delete_resp_header("x-frame-options")
+      |> delete_resp_header("content-security-policy")
+      |> render("show.html",
+        activity: activity,
+        author: User.sanitize_html(author),
+        counts: get_counts(activity)
+      )
+    end
+  end
+
+  defp get_counts(%Activity{} = activity) do
+    %Object{data: data} = Object.normalize(activity)
+
+    %{
+      likes: Map.get(data, "like_count", 0),
+      replies: Map.get(data, "repliesCount", 0),
+      announces: Map.get(data, "announcement_count", 0)
+    }
+  end
+end
index 72cb3ee279536954b9aed4cb8b10df93817fb6a9..4f665db12ce825c9cb8c064f42b9b4470987a286 100644 (file)
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.Endpoint do
     at: "/",
     from: :pleroma,
     only:
-      ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
+      ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css),
     # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
     gzip: true,
     cache_control_for_etags: @static_cache_control,
index 3f36f6c1a8d000b7e3f7de3f554a77dd02a2888d..eef0a80231e4bd6f2d9ca0aa3b3f97bcc1b9e9b3 100644 (file)
@@ -637,6 +637,8 @@ defmodule Pleroma.Web.Router do
     post("/auth/password", MastodonAPI.AuthController, :password_reset)
 
     get("/web/*path", MastoFEController, :index)
+
+    get("/embed/:id", EmbedController, :show)
   end
 
   pipeline :remote_media do
diff --git a/lib/pleroma/web/templates/embed/_attachment.html.eex b/lib/pleroma/web/templates/embed/_attachment.html.eex
new file mode 100644 (file)
index 0000000..7e04e95
--- /dev/null
@@ -0,0 +1,8 @@
+<%= case @mediaType do %>
+<% "audio" -> %>
+<audio src="<%= @url %>" controls="controls"></audio>
+<% "video" -> %>
+<video src="<%= @url %>" controls="controls"></video>
+<% _ -> %>
+<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<% end %>
diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex
new file mode 100644 (file)
index 0000000..6bf8fac
--- /dev/null
@@ -0,0 +1,76 @@
+<div>
+  <div class="p-author h-card">
+    <a class="u-url" rel="author noopener" href="<%= User.profile_url(@author) %>">
+      <div class="avatar">
+        <img src="<%= User.avatar_url(@author) |> MediaProxy.url %>" width="48" height="48" alt="">
+      </div>
+      <span class="display-name" style="padding-left: 0.5em;">
+        <bdi><%= raw (@author.name |> Formatter.emojify(emoji_for_user(@author))) %></bdi>
+        <span class="nickname"><%= full_nickname(@author) %></span>
+      </span>
+    </a>
+  </div>
+
+  <div class="activity-content" >
+    <%= if status_title(@activity) != "" do %>
+      <details <%= if open_content?() do %>open<% end %>>
+        <summary><%= raw status_title(@activity) %></summary>
+        <div><%= activity_content(@activity) %></div>
+      </details>
+    <% else %>
+      <div><%= activity_content(@activity) %></div>
+    <% end %>
+    <%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %>
+      <div class="attachment">
+      <%= if sensitive?(@activity) do %>
+        <details class="nsfw">
+          <summary onClick="updateHeight()"><%= Gettext.gettext("sensitive media") %></summary>
+          <div class="nsfw-content">
+            <%= render("_attachment.html", %{name: name, url: url["href"],
+                                             mediaType: fetch_media_type(url)}) %>
+          </div>
+        </details>
+      <% else %>
+        <%= render("_attachment.html", %{name: name, url: url["href"],
+                                         mediaType: fetch_media_type(url)}) %>
+      <% end %>
+      </div>
+    <% end %>
+  </div>
+
+  <dl class="counts pull-right">
+    <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
+    <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
+    <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
+  </dl>
+
+  <p class="date pull-left">
+    <%= link published(@activity), to: activity_url(@author, @activity) %>
+  </p>
+</div>
+
+<script>
+function updateHeight() {
+  window.requestAnimationFrame(function(){
+    var height = document.getElementsByTagName('html')[0].scrollHeight;
+
+    window.parent.postMessage({
+      type: 'setHeightPleromaEmbed',
+      id: window.parentId,
+      height: height,
+    }, '*');
+  })
+}
+
+window.addEventListener('message', function(e){
+  var data = e.data || {};
+
+  if (!window.parent || data.type !== 'setHeightPleromaEmbed') {
+    return;
+  }
+
+  window.parentId = data.id
+
+  updateHeight()
+});
+</script>
diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex
new file mode 100644 (file)
index 0000000..57ae4f8
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
+    <title><%= Pleroma.Config.get([:instance, :name]) %></title>
+    <meta content='noindex' name='robots'>
+    <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
+    <link rel="stylesheet" href="/embed.css">
+  </head>
+  <body>
+    <%= render @view_module, @view_template, assigns %>
+  </body>
+</html>
diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex
new file mode 100644 (file)
index 0000000..7753683
--- /dev/null
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.EmbedView do
+  use Pleroma.Web, :view
+
+  alias Calendar.Strftime
+  alias Pleroma.Activity
+  alias Pleroma.Emoji.Formatter
+  alias Pleroma.Object
+  alias Pleroma.User
+  alias Pleroma.Web.Gettext
+  alias Pleroma.Web.MediaProxy
+  alias Pleroma.Web.Metadata.Utils
+  alias Pleroma.Web.Router.Helpers
+
+  use Phoenix.HTML
+
+  @media_types ["image", "audio", "video"]
+
+  defp emoji_for_user(%User{} = user) do
+    user.source_data
+    |> Map.get("tag", [])
+    |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+    |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+      {String.trim(name, ":"), url}
+    end)
+  end
+
+  defp fetch_media_type(%{"mediaType" => mediaType}) do
+    Utils.fetch_media_type(@media_types, mediaType)
+  end
+
+  defp open_content? do
+    Pleroma.Config.get(
+      [:frontend_configurations, :collapse_message_with_subjects],
+      true
+    )
+  end
+
+  defp full_nickname(user) do
+    %{host: host} = URI.parse(user.ap_id)
+    "@" <> user.nickname <> "@" <> host
+  end
+
+  defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name),
+    do: name
+
+  defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}})
+       when is_binary(summary),
+       do: summary
+
+  defp status_title(_), do: nil
+
+  defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do
+    content |> Pleroma.HTML.filter_tags() |> raw()
+  end
+
+  defp activity_content(_), do: nil
+
+  defp activity_url(%User{local: true}, activity) do
+    Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+  end
+
+  defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do
+    data["url"] || data["external_url"] || data["id"]
+  end
+
+  defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do
+    attachments
+  end
+
+  defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do
+    sensitive
+  end
+
+  defp published(%Activity{object: %Object{data: %{"published" => published}}}) do
+    published
+    |> NaiveDateTime.from_iso8601!()
+    |> Strftime.strftime!("%B %d, %Y, %l:%M %p")
+  end
+end
diff --git a/priv/static/embed.css b/priv/static/embed.css
new file mode 100644 (file)
index 0000000..cc79ee7
--- /dev/null
@@ -0,0 +1,115 @@
+body {
+  background-color: #282c37;
+  font-family: sans-serif;
+  color: white;
+  margin: 0;
+  padding: 1em;
+  padding-bottom: 0;
+}
+
+.avatar {
+  cursor: pointer;
+}
+
+.avatar img {
+  float: left;
+  border-radius: 4px;
+  margin-right: 4px;
+}
+
+.activity-content {
+  padding-top: 1em;
+}
+
+.attachment {
+  margin-top: 1em;
+}
+
+.attachment img {
+  max-width: 100%;
+}
+
+.date a {
+  text-decoration: none;
+}
+
+.date a:hover {
+  text-decoration: underline;
+}
+
+.date a,
+.counts {
+  color: #666;
+  font-size: 0.9em;
+}
+
+.counts dt,
+.counts dd {
+  float: left;
+  margin-left: 1em;
+}
+
+a {
+  color: white;
+}
+
+.h-card {
+  min-height: 48px;
+  margin-bottom: 8px;
+}
+
+.h-card a {
+  text-decoration: none;
+}
+
+.h-card a:hover {
+  text-decoration: underline;
+}
+
+.display-name {
+  padding-top: 4px;
+  display: block;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  color: white;
+}
+
+/* keep emoji from being hilariously huge */
+.display-name img {
+  max-height: 1em;
+}
+
+.display-name .nickname {
+  padding-top: 4px;
+  display: block;
+}
+
+.nickname:hover {
+  text-decoration: none;
+}
+
+.pull-right {
+  float: right;
+}
+
+.collapse {
+  margin: 0;
+  width: auto;
+}
+
+a.button {
+  box-sizing: border-box;
+  display: inline-block;
+  color: white;
+  background-color: #419bdd;
+  border-radius: 4px;
+  border: none;
+  padding: 10px;
+  font-weight: 500;
+  font-size: 0.9em;
+}
+
+a.button:hover {
+  text-decoration: none;
+  background-color: #61a6d9;
+}
diff --git a/priv/static/embed.js b/priv/static/embed.js
new file mode 100644 (file)
index 0000000..f675f64
--- /dev/null
@@ -0,0 +1,43 @@
+(function () {
+  'use strict'
+
+  var ready = function (loaded) {
+    if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
+      loaded()
+    } else {
+      document.addEventListener('DOMContentLoaded', loaded)
+    }
+  }
+
+  ready(function () {
+    var iframes = []
+
+    window.addEventListener('message', function (e) {
+      var data = e.data || {}
+
+      if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) {
+        return
+      }
+
+      iframes[data.id].height = data.height
+    });
+
+    [].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) {
+      iframe.scrolling = 'no'
+      iframe.style.overflow = 'hidden'
+
+      iframes.push(iframe)
+
+      var id = iframes.length - 1
+
+      iframe.onload = function () {
+        iframe.contentWindow.postMessage({
+          type: 'setHeightPleromaEmbed',
+          id: id
+        }, '*')
+      }
+
+      iframe.onload()
+    })
+  })
+})()