aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--config/config.exs2
-rw-r--r--docs/configuration/cheatsheet.md7
-rw-r--r--lib/pleroma/plugs/static_fe_plug.ex26
-rw-r--r--lib/pleroma/web/router.ex1
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex124
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex47
-rw-r--r--lib/pleroma/web/templates/layout/static_fe.html.eex15
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex8
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex37
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex11
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex11
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/error.html.eex7
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex31
-rw-r--r--priv/static/static/static-fe.css176
-rw-r--r--test/web/static_fe/static_fe_controller_test.exs181
16 files changed, 685 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 727dde9be..945e7dc4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Refreshing poll results for remote polls
- Authentication: Added rate limit for password-authorized actions / login existence checks
+- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
<details>
diff --git a/config/config.exs b/config/config.exs
index 17d15256f..75f463797 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -605,6 +605,8 @@ config :pleroma, Pleroma.ActivityExpiration, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
+config :pleroma, :static_fe, enabled: false
+
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 61783cf3f..6c7f60203 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -798,3 +798,10 @@ config :auto_linker,
]
```
+## :static_fe
+
+Render profiles and posts using server-generated HTML that is viewable without using JavaScript.
+
+Available options:
+
+* `enabled` - Enables the rendering of static HTML. Defaults to `false`.
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex
new file mode 100644
index 000000000..b3fb3c582
--- /dev/null
+++ b/lib/pleroma/plugs/static_fe_plug.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.StaticFEPlug do
+ import Plug.Conn
+ alias Pleroma.Web.StaticFE.StaticFEController
+
+ def init(options), do: options
+
+ def call(conn, _) do
+ if enabled?() and accepts_html?(conn) do
+ conn
+ |> StaticFEController.call(:show)
+ |> halt()
+ else
+ conn
+ end
+ end
+
+ defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
+
+ defp accepts_html?(conn) do
+ conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html")
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 8fb4aec13..ecf5f744c 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -495,6 +495,7 @@ defmodule Pleroma.Web.Router do
pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
+ plug(Pleroma.Plugs.StaticFEPlug)
end
pipeline :oembed do
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
new file mode 100644
index 000000000..5e60c82b0
--- /dev/null
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -0,0 +1,124 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Metadata
+ alias Pleroma.Web.Router.Helpers
+
+ plug(:put_layout, :static_fe)
+ plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
+ plug(:assign_id)
+
+ @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
+
+ defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
+ do: name
+
+ defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
+ do: summary
+
+ defp get_title(_), do: nil
+
+ def get_counts(%Activity{} = activity) do
+ %Object{data: data} = Object.normalize(activity)
+
+ %{
+ likes: data["like_count"] || 0,
+ replies: data["repliesCount"] || 0,
+ announces: data["announcement_count"] || 0
+ }
+ end
+
+ def represent(%Activity{} = activity), do: represent(activity, false)
+
+ def represent(%Activity{object: %Object{data: data}} = activity, selected) do
+ {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
+
+ link =
+ case user.local do
+ true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+ _ -> data["url"] || data["external_url"] || data["id"]
+ end
+
+ %{
+ user: user,
+ title: get_title(activity.object),
+ content: data["content"] || nil,
+ attachment: data["attachment"],
+ link: link,
+ published: data["published"],
+ sensitive: data["sensitive"],
+ selected: selected,
+ counts: get_counts(activity),
+ id: activity.id
+ }
+ end
+
+ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
+ with %Activity{local: true} = activity <-
+ Activity.get_by_id_with_object(notice_id),
+ true <- Visibility.is_public?(activity.object),
+ %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
+ meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
+
+ timeline =
+ activity.object.data["context"]
+ |> ActivityPub.fetch_activities_for_context(%{})
+ |> Enum.reverse()
+ |> Enum.map(&represent(&1, &1.object.id == activity.object.id))
+
+ render(conn, "conversation.html", %{activities: timeline, meta: meta})
+ else
+ _ ->
+ conn
+ |> put_status(404)
+ |> render("error.html", %{message: "Post not found.", meta: ""})
+ end
+ end
+
+ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
+ case User.get_cached_by_nickname_or_id(username_or_id) do
+ %User{} = user ->
+ meta = Metadata.build_tags(%{user: user})
+
+ timeline =
+ ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
+ |> Enum.map(&represent/1)
+
+ prev_page_id =
+ (params["min_id"] || params["max_id"]) &&
+ List.first(timeline) && List.first(timeline).id
+
+ next_page_id = List.last(timeline) && List.last(timeline).id
+
+ render(conn, "profile.html", %{
+ user: user,
+ timeline: timeline,
+ prev_page_id: prev_page_id,
+ next_page_id: next_page_id,
+ meta: meta
+ })
+
+ _ ->
+ conn
+ |> put_status(404)
+ |> render("error.html", %{message: "User not found.", meta: ""})
+ end
+ end
+
+ def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
+ do: assign(conn, :notice_id, notice_id)
+
+ def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
+ do: assign(conn, :username_or_id, user_id)
+
+ def assign_id(conn, _opts), do: conn
+end
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
new file mode 100644
index 000000000..821ece9a9
--- /dev/null
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEView do
+ use Pleroma.Web, :view
+
+ alias Calendar.Strftime
+ alias Pleroma.Emoji.Formatter
+ alias Pleroma.User
+ alias Pleroma.Web.Endpoint
+ 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"]
+
+ def 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
+
+ def fetch_media_type(%{"mediaType" => mediaType}) do
+ Utils.fetch_media_type(@media_types, mediaType)
+ end
+
+ def format_date(date) do
+ {:ok, date, _} = DateTime.from_iso8601(date)
+ Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
+ end
+
+ def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
+
+ def open_content? do
+ Pleroma.Config.get(
+ [:frontend_configurations, :collapse_message_with_subjects],
+ true
+ )
+ end
+end
diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex
new file mode 100644
index 000000000..819632cec
--- /dev/null
+++ b/lib/pleroma/web/templates/layout/static_fe.html.eex
@@ -0,0 +1,15 @@
+<!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>
+ <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
+ <link rel="stylesheet" href="/static/static-fe.css">
+ </head>
+ <body>
+ <div class="container">
+ <%= render @view_module, @view_template, assigns %>
+ </div>
+ </body>
+</html>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
new file mode 100644
index 000000000..7e04e9550
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
@@ -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/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
new file mode 100644
index 000000000..df5e5eedd
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
@@ -0,0 +1,37 @@
+<div class="activity" <%= if @selected do %> id="selected" <% end %>>
+ <p class="pull-right">
+ <%= link format_date(@published), to: @link, class: "activity-link" %>
+ </p>
+ <%= render("_user_card.html", %{user: @user}) %>
+ <div class="activity-content">
+ <%= if @title != "" do %>
+ <details <%= if open_content?() do %>open<% end %>>
+ <summary><%= raw @title %></summary>
+ <div class="e-content"><%= raw @content %></div>
+ </details>
+ <% else %>
+ <div class="e-content"><%= raw @content %></div>
+ <% end %>
+ <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
+ <%= if @sensitive do %>
+ <details class="nsfw">
+ <summary><%= Gettext.gettext("sensitive media") %></summary>
+ <div>
+ <%= 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 %>
+ <% end %>
+ </div>
+ <%= if @selected do %>
+ <dl class="counts">
+ <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>
+ <% end %>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
new file mode 100644
index 000000000..c7789f9ac
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
@@ -0,0 +1,11 @@
+<div class="p-author h-card">
+ <a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
+ <div class="avatar">
+ <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
+ </div>
+ <span class="display-name">
+ <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
+ <span class="nickname"><%= @user.nickname %></span>
+ </span>
+ </a>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex
new file mode 100644
index 000000000..2acd84828
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex
@@ -0,0 +1,11 @@
+<header>
+ <h1><%= link instance_name(), to: "/" %></h1>
+</header>
+
+<main>
+ <div class="conversation">
+ <%= for activity <- @activities do %>
+ <%= render("_notice.html", activity) %>
+ <% end %>
+ </div>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex
new file mode 100644
index 000000000..d98a1eba7
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex
@@ -0,0 +1,7 @@
+<header>
+ <h1><%= gettext("Oops") %></h1>
+</header>
+
+<main>
+ <p><%= @message %></p>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
new file mode 100644
index 000000000..94063c92d
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
@@ -0,0 +1,31 @@
+<header>
+ <h1><%= link instance_name(), to: "/" %></h1>
+
+ <h3>
+ <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
+ <input type="hidden" name="nickname" value="<%= @user.nickname %>">
+ <input type="hidden" name="profile" value="">
+ <button type="submit" class="collapse">Remote follow</button>
+ </form>
+ <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
+ <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
+ </h3>
+ <p><%= raw @user.bio %></p>
+</header>
+
+<main>
+ <div class="activity-stream">
+ <%= for activity <- @timeline do %>
+ <%= render("_notice.html", Map.put(activity, :selected, false)) %>
+ <% end %>
+ <p id="pagination">
+ <%= if @prev_page_id do %>
+ <%= link "«", to: "?min_id=" <> @prev_page_id %>
+ <% end %>
+ <%= if @prev_page_id && @next_page_id, do: " | " %>
+ <%= if @next_page_id do %>
+ <%= link "»", to: "?max_id=" <> @next_page_id %>
+ <% end %>
+ </p>
+ </div>
+</main>
diff --git a/priv/static/static/static-fe.css b/priv/static/static/static-fe.css
new file mode 100644
index 000000000..19c56387b
--- /dev/null
+++ b/priv/static/static/static-fe.css
@@ -0,0 +1,176 @@
+body {
+ background-color: #282c37;
+ font-family: sans-serif;
+ color: white;
+}
+
+main {
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 40px;
+ background-color: #313543;
+ border-radius: 4px;
+}
+
+header {
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 40px;
+ background-color: #313543;
+ border-radius: 4px;
+}
+
+.activity {
+ border-radius: 4px;
+ padding: 1em;
+ padding-bottom: 2em;
+ margin-bottom: 1em;
+}
+
+.avatar {
+ cursor: pointer;
+}
+
+.avatar img {
+ float: left;
+ border-radius: 4px;
+ margin-right: 4px;
+}
+
+.activity-content img, video, audio {
+ padding: 1em;
+ max-width: 800px;
+ max-height: 800px;
+}
+
+#selected {
+ background-color: #1b2735;
+}
+
+.counts dt, .counts dd {
+ float: left;
+ margin-left: 1em;
+}
+
+a {
+ color: white;
+}
+
+.h-card {
+ min-height: 48px;
+ margin-bottom: 8px;
+}
+
+header a, .h-card a {
+ text-decoration: none;
+}
+
+header a:hover, .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;
+}
+
+h1 {
+ margin: 0;
+}
+
+h2 {
+ color: #9baec8;
+ font-weight: normal;
+ font-size: 20px;
+ margin-bottom: 40px;
+}
+
+form {
+ width: 100%;
+}
+
+input {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 10px;
+ margin-top: 20px;
+ background-color: rgba(0,0,0,.1);
+ color: white;
+ border: 0;
+ border-bottom: 2px solid #9baec8;
+ font-size: 14px;
+}
+
+input:focus {
+ border-bottom: 2px solid #4b8ed8;
+}
+
+input[type="checkbox"] {
+ width: auto;
+}
+
+button {
+ box-sizing: border-box;
+ width: 100%;
+ color: white;
+ background-color: #419bdd;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 30px;
+ text-transform: uppercase;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+.alert-danger {
+ box-sizing: border-box;
+ width: 100%;
+ color: #D8000C;
+ background-color: #FFD2D2;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+.alert-info {
+ box-sizing: border-box;
+ width: 100%;
+ color: #00529B;
+ background-color: #BDE5F8;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+}
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
new file mode 100644
index 000000000..effdfbeb3
--- /dev/null
+++ b/test/web/static_fe/static_fe_controller_test.exs
@@ -0,0 +1,181 @@
+defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
+ use Pleroma.Web.ConnCase
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ clear_config_all([:static_fe, :enabled]) do
+ Pleroma.Config.put([:static_fe, :enabled], true)
+ end
+
+ describe "user profile page" do
+ test "just the profile as HTML", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ assert html_response(conn, 200) =~ user.nickname
+ end
+
+ test "renders json unless there's an html accept header", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/users/#{user.nickname}")
+
+ assert json_response(conn, 200)
+ end
+
+ test "404 when user not found", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/limpopo")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "profile does not include private messages", %{conn: conn} do
+ user = insert(:user)
+ CommonAPI.post(user, %{"status" => "public"})
+ CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">public<"
+ refute html =~ ">private<"
+ end
+
+ test "pagination", %{conn: conn} do
+ user = insert(:user)
+ Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">test30<"
+ assert html =~ ">test11<"
+ refute html =~ ">test10<"
+ refute html =~ ">test1<"
+ end
+
+ test "pagination, page 2", %{conn: conn} do
+ user = insert(:user)
+ activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+ {:ok, a11} = Enum.at(activities, 11)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}?max_id=#{a11.id}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">test1<"
+ assert html =~ ">test10<"
+ refute html =~ ">test20<"
+ refute html =~ ">test29<"
+ end
+ end
+
+ describe "notice rendering" do
+ test "single notice page", %{conn: conn} do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ html = html_response(conn, 200)
+ assert html =~ "<header>"
+ assert html =~ user.nickname
+ assert html =~ "testing a thing!"
+ end
+
+ test "shows the whole thread", %{conn: conn} do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"})
+
+ CommonAPI.post(user, %{
+ "status" => "these are the voyages or something",
+ "in_reply_to_status_id" => activity.id
+ })
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ html = html_response(conn, 200)
+ assert html =~ "the final frontier"
+ assert html =~ "voyages"
+ end
+
+ test "404 when notice not found", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/88c9c317")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "404 for private status", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "404 for remote cached status", %{conn: conn} do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => user.follower_address,
+ "cc" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+ end
+end