Compare commits

...

14 Commits

18 changed files with 537 additions and 73 deletions

View File

@ -22,6 +22,7 @@ button.btn-link {
border: none;
color: $link-color;
text-decoration: none;
padding: 0;
&:hover {
cursor: pointer;
@ -29,9 +30,14 @@ button.btn-link {
}
}
button[type=submit]:not(.btn-link) {
input, button:not(.btn-link) {
box-sizing: border-box;
height: 1.75rem;
}
button:not(.btn-link) {
padding: 0 2rem;
font-family: $sans-serif;
padding: 0.25rem 2rem;
background-color: $tint-color;
border: 1px solid darken($tint-color, 20%);
color: white;
@ -56,6 +62,19 @@ h1, h2, h3 {
margin: 0.5rem 0;
}
.flash-info {
padding: 0.5rem;
background-color: lighten(blue, 35%);
border: 1px solid darken(blue, 20%);
color: darken(blue, 35%);
}
.flash-error {
padding: 0.5rem;
background-color: lighten(red, 30%);
border: 1px solid darken(red, 20%);
color: darken(red, 35%);
}
header {
nav {
display: flex;
@ -69,6 +88,10 @@ header {
li {
display: inline;
list-style: none;
&:not(:last-child) {
margin-right: 1rem;
}
}
}
}
@ -141,3 +164,29 @@ ul.status-list {
box-sizing: border-box;
}
}
.search-form {
width: 100%;
display: flex;
input[type=text] {
flex-grow: 1;
margin-right: 1rem;
}
}
.login-form {
.input-group {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
label {
width: 100px;
}
}
}
.actor-actions {
margin-bottom: 1rem;
}

View File

@ -110,6 +110,41 @@ defmodule Clacks.ActivityPub do
}
end
@spec follow(
actor :: String.t(),
followee :: String.t(),
published :: NaiveDateTime.t(),
state :: String.t()
) ::
map()
def follow(actor, followee, published \\ DateTime.utc_now(), state \\ "pending") do
%{
"@context" => @context,
"type" => "Follow",
"id" => activity_id(Ecto.UUID.generate()),
"actor" => actor,
"object" => followee,
"context" => context_id(Ecto.UUID.generate()),
"to" => [followee],
"cc" => [@public],
"published" => published |> DateTime.to_iso8601(),
"state" => state
}
end
@spec undo_follow(actor :: String.t(), follow_activity :: map()) :: map()
def undo_follow(actor, %{"object" => followee} = follow_activity) do
%{
"@context" => @context,
"type" => "Undo",
"id" => activity_id(Ecto.UUID.generate()),
"actor" => actor,
"object" => follow_activity,
"to" => [followee],
"cc" => []
}
end
@spec object_id(id :: String.t()) :: String.t()
def object_id(id) do
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]

View File

@ -67,7 +67,7 @@ defmodule Clacks.ActivityPub.Federator do
@public in activity["to"] or @public in activity["cc"] ->
shared_inbox_for(actor)
actor.data["followers"] in activity["to"] or actor.data["followers"] in activity["cc"] ->
actor["followers"] in activity["to"] or actor["followers"] in activity["cc"] ->
shared_inbox_for(actor)
true ->

View File

@ -7,7 +7,7 @@ defmodule Clacks.ActivityPub.Fetcher do
with %{"type" => type, "id" => remote_id} = actor <- fetch(id),
"person" <- String.downcase(type),
%{host: actor_host} when actor_host == id_host <- URI.parse(remote_id) do
%{host: ^id_host} <- URI.parse(remote_id) do
actor
else
_ ->
@ -20,7 +20,7 @@ defmodule Clacks.ActivityPub.Fetcher do
%{host: id_host} = URI.parse(id)
with %{"actor" => remote_actor} = object <- fetch(id),
%{host: actor_host} when actor_host == id_host <- URI.parse(remote_actor) do
%{host: ^id_host} <- URI.parse(remote_actor) do
object
else
_ ->
@ -40,15 +40,10 @@ defmodule Clacks.ActivityPub.Fetcher do
headers = [Accept: "application/activity+json, application/ld+json"]
opts = [hackney: Application.get_env(:clacks, :hackney_opts, [])]
with {:ok, %HTTPoison.Response{status_code: status_code, body: body}}
when status_code in 200..299 <-
HTTPoison.get(uri, headers, opts),
with {:ok, %HTTPoison.Response{body: body}} <- Clacks.HTTP.get(uri, headers, opts),
{:ok, data} <- Jason.decode(body) do
data
else
{:ok, %HTTPoison.Response{}} ->
nil
{:error, reason} ->
Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}")
nil

View File

@ -0,0 +1,30 @@
defmodule Clacks.ActivityPub.Helper do
alias Clacks.{Activity, Actor, Repo}
require Logger
@spec save_and_federate(activity_data :: map(), actor :: Actor.t()) ::
{:ok, Activity.t()} | :error
def save_and_federate(activity_data, actor) do
changeset = Activity.changeset_for_creating(activity_data, true)
case Repo.insert(changeset) do
{:error, changeset} ->
Logger.error("Couldn't save activity: #{inspect(changeset)}")
:error
{:ok, activity} ->
worker =
%{id: activity.id, actor_id: actor.id}
|> Clacks.Worker.Federate.new()
case Oban.insert(worker) do
{:ok, _} ->
{:ok, activity}
{:error, changeset} ->
Logger.error("Couldn't save federate job: #{inspect(changeset)}")
:error
end
end
end
end

46
lib/clacks/http.ex Normal file
View File

@ -0,0 +1,46 @@
defmodule Clacks.HTTP do
require Logger
def get(url, headers \\ [], options \\ []) do
case HTTPoison.get(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: status_code} = response}
when status_code in 200..299 ->
{:ok, response}
{:ok, %HTTPoison.Response{status_code: status_code, headers: resp_headers}}
when status_code in [301, 302] ->
resp_headers
|> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end)
|> case do
{_, new_url} ->
new_url =
case URI.parse(new_url) do
%URI{host: nil, path: path} ->
# relative path
%URI{URI.parse(url) | path: path} |> URI.to_string()
uri ->
uri
end
Logger.debug("Got 301 redirect from #{url} to #{new_url}")
get(new_url, headers, options)
_ ->
{:error, "Missing Location header for redirect"}
end
{:ok, %HTTPoison.Response{status_code: 403}} ->
{:error, "403 Forbidden"}
{:ok, %HTTPoison.Response{status_code: 404}} ->
{:error, "404 Not Found"}
{:ok, %HTTPoison.Response{status_code: status_code}} ->
{:error, "HTTP #{status_code}"}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
end
end
end

View File

@ -2,15 +2,16 @@ defmodule Clacks.Inbox do
require Logger
alias Clacks.{Repo, Activity, Object, Actor, ActivityPub}
defp store_activity(%{"actor" => actor} = activity, local \\ false) when is_binary(actor) do
defp store_activity(%{"actor" => actor, "id" => ap_id} = activity, local \\ false)
when is_binary(actor) do
changeset =
Activity.changeset(%Activity{}, %{
Activity.changeset(Activity.get_cached_by_ap_id(ap_id) || %Activity{}, %{
data: activity,
local: local,
actor: actor
})
Repo.insert(changeset)
Repo.insert_or_update(changeset)
end
@spec handle(activity :: map()) :: :ok | {:error, reason :: any()}
@ -42,12 +43,13 @@ defmodule Clacks.Inbox do
store_activity(activity)
changeset = Actor.changeset(followed, %{followers: [follower_id | followed.followers]})
new_followers = [follower_id | followed.followers] |> Enum.uniq()
changeset = Actor.changeset(followed, %{followers: new_followers})
case Repo.update(changeset) do
{:error, changeset} ->
Logger.error("Couldn't store Follow activity: #{inspect(changeset)}")
{:error, "Couldn't store Follow activity"}
Logger.error("Couldn't store updated followers: #{inspect(changeset)}")
{:error, "Couldn't store updated followers"}
{:ok, _followed} ->
accept = ActivityPub.accept_follow(activity)
@ -63,8 +65,49 @@ defmodule Clacks.Inbox do
end
end
def handle(
%{
"type" => "Accept",
"actor" => followee_id,
"object" => %{"type" => "Follow", "id" => follow_activity_id, "actor" => follower_id}
} = activity
) do
followee = Actor.get_by_ap_id(followee_id)
follower = Actor.get_by_ap_id(follower_id)
store_activity(activity)
follow_activity = Activity.get_cached_by_ap_id(follow_activity_id)
changeset =
Activity.changeset(follow_activity, %{
data: %{follow_activity.data | "state" => "accepted"}
})
case Repo.update(changeset) do
{:error, changeset} ->
Logger.error("Couldn't store updated Follow activity: #{inspect(changeset)}")
{:error, "Couldn't store updated Follow activity"}
{:ok, _follow_activity} ->
new_followers = [follower_id | followee.followers] |> Enum.uniq()
changeset = Actor.changeset(followee, %{followers: new_followers})
case Repo.update(changeset) do
{:error, changeset} ->
Logger.error("Couldn't store updated followers: #{inspect(changeset)}")
{:error, "Couldn't store updated followers"}
{:ok, _followee} ->
:ok
end
end
end
# as a fallback, just store the activity
def handle(activity) do
Logger.debug("Unhandled activity: #{inspect(activity)}")
case store_activity(activity) do
{:error, changeset} ->
Logger.error("Could not store activity: #{inspect(changeset)}")

View File

@ -24,6 +24,17 @@ defmodule Clacks.Object do
changeset(%__MODULE__{}, %{data: data})
end
@spec get_actor(object :: t()) :: Clacks.Actor.t() | nil
def get_actor(object) do
case object.data["actor"] || object.data["attributedTo"] do
nil ->
nil
id when is_binary(id) ->
Clacks.Actor.get_by_ap_id(id)
end
end
@spec get_by_ap_id(
ap_id :: String.t(),
force_refetch :: boolean(),
@ -42,8 +53,9 @@ defmodule Clacks.Object do
Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id)
end
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean()) :: t() | nil
def fetch(ap_id, synthesize_create \\ true) do
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) ::
t() | Activity.t() | nil
def fetch(ap_id, synthesize_create \\ true, return \\ :object) do
case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do
nil ->
nil
@ -61,21 +73,34 @@ defmodule Clacks.Object do
actor = data["actor"] || data["attributedTo"]
_ = Clacks.Actor.get_by_ap_id(actor)
if synthesize_create do
create = Clacks.ActivityPub.synthesized_create(data)
activity =
case Clacks.Activity.get_by_object_ap_id(ap_id) do
nil ->
if synthesize_create do
create = Clacks.ActivityPub.synthesized_create(data)
changeset =
Clacks.Activity.changeset(%Clacks.Activity{}, %{
data: create,
local: false,
actor: actor
})
changeset =
Clacks.Activity.changeset(%Clacks.Activity{}, %{
data: create,
local: false,
actor: actor
})
{:ok, _create} = Repo.insert_or_update(changeset)
{:ok, create} = Repo.insert_or_update(changeset)
create
else
nil
end
%Clacks.Activity{} = activity ->
activity
end
case return do
:object -> object
:activity -> activity
end
object
{:error, changeset} ->
Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}")
nil

View File

@ -9,26 +9,24 @@ defmodule Clacks.Timeline do
@spec actor_timeline(
actor :: Actor.t(),
params :: map(),
only_public :: boolean(),
actors :: boolean()
only_public :: boolean()
) :: [
Activity.t()
]
def actor_timeline(actor, params, only_public \\ true, actors \\ false) do
def actor_timeline(actor, params, only_public \\ true) do
Activity
|> restrict_to_actor(actor.ap_id)
|> restrict_to_types(@timeline_types)
|> restrict_to_public(only_public)
|> paginate(params)
|> limit(^Map.get(params, "limit", 20))
|> join_with_actors(actors)
|> Repo.all()
end
@spec home_timeline(user :: User.t(), params :: map(), actors :: boolean()) :: [
Activity.t()
@spec home_timeline(user :: User.t(), params :: map()) :: [
{Activity.t(), Actor.t()}
]
def home_timeline(user, params, actors \\ false) do
def home_timeline(user, params) do
user =
case user.actor do
%Ecto.Association.NotLoaded{} ->
@ -39,15 +37,27 @@ defmodule Clacks.Timeline do
end
Activity
|> join_with_actors()
|> where(
[a],
fragment("?->>'actor'", a.data) == ^user.actor.ap_id or
fragment("?->>'actor'", a.data) in ^user.actor.followers
[activity, actor],
fragment("?->>'actor'", activity.data) == ^user.actor.ap_id or
^user.actor.ap_id in actor.followers
)
|> restrict_to_types(@timeline_types)
|> paginate(params)
|> limit(^Map.get(params, "limit", 20))
|> join_with_actors(actors)
|> Repo.all()
end
@spec local_timeline(params :: map()) :: [{Activity.t(), Actor.t()}]
def local_timeline(params) do
Activity
|> where([a], a.local)
|> restrict_to_public(true)
|> restrict_to_types(@timeline_types)
|> paginate(params)
|> limit(^Map.get(params, "limit", 20))
|> join_with_actors()
|> Repo.all()
end
@ -69,11 +79,9 @@ defmodule Clacks.Timeline do
defp restrict_to_public(query, false), do: query
defp join_with_actors(query, true) do
defp join_with_actors(query) do
query
|> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data))
|> select([o, a], {o, a})
end
defp join_with_actors(query, false), do: query
end

View File

@ -3,6 +3,7 @@ defmodule ClacksWeb.FrontendController do
alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity, Object}
alias ClacksWeb.Router.Helpers, as: Routes
alias ClacksWeb.Endpoint
import Ecto.Query
@public "https://www.w3.org/ns/activitystreams#Public"
@ -12,18 +13,20 @@ defmodule ClacksWeb.FrontendController do
render(conn, "home.html", %{
user: user,
actor: user.actor,
statuses_with_authors: Timeline.home_timeline(user, params, true)
statuses_with_authors: Timeline.home_timeline(user, params)
})
end
def index(conn, params) do
Application.get_env(:clacks, :frontend, %{})
|> Keyword.get(:unauthenticated_homepage, :public_timeline)
|> Keyword.get(:unauthenticated_homepage, :local_timeline)
|> index(conn, params)
end
defp index(:public_timeline, conn, params) do
# tood: show public timeline
defp index(:local_timeline, conn, params) do
render(conn, "local_timeline.html", %{
statuses_with_authors: Timeline.local_timeline(params)
})
end
defp index({:profile, nickname}, conn, params) do
@ -36,8 +39,8 @@ defmodule ClacksWeb.FrontendController do
})
_ ->
# otherwise show public timeline
index(:public_timeline, conn)
# otherwise show local timeline
index(:local_timeline, conn)
end
end
@ -53,7 +56,7 @@ defmodule ClacksWeb.FrontendController do
data:
%{
"type" => "Create",
"object" => %{"type" => "Note", "attributedTo" => author_id} = note
"object" => %{"type" => "Note", "attributedTo" => author_id}
} = data
} = activity <- Activity.get(id),
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
@ -119,7 +122,7 @@ defmodule ClacksWeb.FrontendController do
case User.get_by_username(username) do
nil ->
put_status(conn, 404)
resp(conn, 404, "Not Found")
user ->
user = Repo.preload(user, :actor)
@ -127,11 +130,127 @@ defmodule ClacksWeb.FrontendController do
render(conn, "profile.html", %{
current_user: current_user,
actor: user.actor,
statuses: actor_statuses(user.actor, params, only_public: true)
statuses: actor_statuses(user.actor, params, only_public: true),
following_state: following_state(current_user.actor, user.actor)
})
end
end
def actor(conn, %{"id" => id} = params) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
case Repo.get(Actor, id) do
nil ->
resp(conn, 404, "Not Found")
actor ->
render(conn, "profile.html", %{
current_user: current_user,
actor: actor,
statuses: actor_statuses(actor, params, only_public: true),
following_state: following_state(current_user.actor, actor)
})
end
end
def follow(conn, %{"id" => id}) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
case Repo.get(Actor, id) do
nil ->
resp(conn, 404, "Not Found")
followee ->
if current_user.actor.ap_id in followee.followers do
redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee))
else
follow = ActivityPub.follow(current_user.actor.ap_id, followee.ap_id)
ActivityPub.Helper.save_and_federate(follow, current_user.actor)
conn
|> put_flash(:info, "Follow request sent")
|> redirect(to: ClacksWeb.FrontendView.local_actor_link(followee))
end
end
end
def unfollow(conn, %{"id" => id}) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
case Repo.get(Actor, id) do
nil ->
resp(conn, 404, "Not Found")
followee ->
unless current_user.actor.ap_id in followee.followers do
redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee))
else
new_followers = List.delete(followee.followers, current_user.actor.ap_id)
changeset = Actor.changeset(followee, %{followers: new_followers})
{:ok, followee} = Repo.update(changeset)
follow_activity = follow_activity(current_user.actor, followee)
changeset =
Activity.changeset(follow_activity, %{
data: %{follow_activity.data | "state" => "unfollowed"}
})
{:ok, follow_activity} = Repo.update(changeset)
undo_follow = ActivityPub.undo_follow(current_user.actor.ap_id, follow_activity.data)
ActivityPub.Helper.save_and_federate(undo_follow, current_user.actor)
conn
|> put_flash(:info, "Unfollowed")
|> redirect(to: ClacksWeb.FrontendView.local_actor_link(followee))
end
end
end
def search(conn, %{"q" => q}) when is_binary(q) do
current_user = conn.assigns[:user]
actor_results =
case Actor.fetch(q) do
%Actor{} = actor ->
[actor]
_ ->
[]
end
status_results =
with %Activity{
actor: actor_id,
data: %{"type" => "Create", "object" => %{"type" => "Note"}}
} = activity <- Object.fetch(q, true, :activity),
actor <- Actor.get_by_ap_id(actor_id) do
[{activity, actor}]
else
_ ->
[]
end
render(conn, "search.html", %{
current_user: current_user,
q: q,
status_results: status_results,
actor_results: actor_results
})
end
def search(conn, _params) do
current_user = conn.assigns[:user]
render(conn, "search.html", %{
current_user: current_user,
q: "",
status_results: [],
actor_results: []
})
end
def post_status(conn, %{"content" => content} = params) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
@ -140,12 +259,7 @@ defmodule ClacksWeb.FrontendController do
{:ok, object} = Repo.insert(note_changeset)
create = ActivityPub.create(note)
create_changeset = Activity.changeset_for_creating(create, true)
{:ok, activity} = Repo.insert(create_changeset)
%{id: activity.id, actor_id: current_user.actor.id}
|> Clacks.Worker.Federate.new()
|> Oban.insert()
{:ok, activity} = ActivityPub.Helper.save_and_federate(create, current_user.actor)
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
redirect(conn, to: path)
@ -177,4 +291,32 @@ defmodule ClacksWeb.FrontendController do
defp note_for_posting(current_user, %{"content" => content}) do
ActivityPub.note(current_user.actor.ap_id, content)
end
@spec follow_activity(follower :: Actor.t(), followee :: Actor.t()) :: map()
def follow_activity(follower, followee) do
# todo: get latest
query =
Activity
|> where([a], fragment("?->>'type'", a.data) == "Follow")
|> where([a], fragment("?->>'actor'", a.data) == ^follower.ap_id)
|> where([a], fragment("?->>'object'", a.data) == ^followee.ap_id)
|> order_by(desc: :inserted_at)
|> limit(1)
Repo.one(query)
end
@spec following_state(follower :: Actor.t(), followee :: Actor.t()) :: boolean()
defp following_state(follower, followee) do
case follow_activity(follower, followee) do
%Activity{data: %{"state" => "pending"}} ->
:pending
%Activity{data: %{"state" => "accepted"}} ->
:following
_ ->
:not_following
end
end
end

View File

@ -58,8 +58,11 @@ defmodule ClacksWeb.Router do
pipe_through :browser_authenticated
post "/post", FrontendController, :post_status
get "/status/:id/reply", FrontendController, :reply
get "/search", FrontendController, :search
get "/actors/:id", FrontendController, :actor
post "/actors/:id/follow", FrontendController, :follow
post "/actors/:id/unfollow", FrontendController, :unfollow
end
scope "/", ClacksWeb do

View File

@ -1,7 +1,7 @@
<div class="status">
<div class="status-meta">
<h2 class="status-author-nickname">
<a href="<%= @author.ap_id %>">
<a href="<%= local_actor_link(@author) %>">
<%= @author.data["preferredUsername"] %>
</a>
</h2>
@ -12,7 +12,7 @@
</h3>
<p class="status-meta-right">
<span><%= display_timestamp(@note["published"]) %></span>
<a href="<%= @note["url"] %>" class="status-permalink">Permalink</a>
<a href="<%= @note["url"] || @note["id"] %>" class="status-permalink">Permalink</a>
</p>
</div>
<div class="status-content">

View File

@ -0,0 +1,3 @@
<h1>Local Timeline</h1>
<%= render "_timeline.html", conn: @conn, statuses_with_authors: @statuses_with_authors %>

View File

@ -1,5 +1,35 @@
<h1><%= @actor.data["preferredUsername"] %></h1>
<h2>@<%= @actor.data["name"] %></h2>
<h2>
<%= if @actor.local do %>
<%= display_username(@actor) %>
<% else %>
<a href="<%= @actor.ap_id %>">
<%= display_username(@actor) %>
</a>
<% end %>
</h2>
<p><%= @actor.data["summary"] %></p>
<div class="actor-actions">
<%= unless @current_user.actor.ap_id == @actor.ap_id do %>
<%= case @following_state do %>
<% :not_following -> %>
<%= form_tag Routes.frontend_path(@conn, :follow, @actor.id), method: :post, class: "follow-form" do %>
<%= submit "Follow" %>
<% end %>
<% :following -> %>
<%= form_tag Routes.frontend_path(@conn, :unfollow, @actor.id), method: :post, class: "follow-form" do %>
<%= submit "Unfollow" %>
<% end %>
<% :pending -> %>
<!-- todo: cancel follow request -->
<button type="button">Pending</button>
<% end %>
<% end %>
</div>
<%= render "_timeline.html", conn: @conn, statuses_with_authors: Enum.map(@statuses, &({&1, @actor})) %>

View File

@ -0,0 +1,31 @@
<%= form_tag Routes.frontend_path(@conn, :search), method: :get, class: "search-form" do %>
<input type="text" name="q" id="q" placeholder="Search Query" value="<%= @q %>">
<%= submit "Search" %>
<% end %>
<%= if length(@actor_results) > 0 do %>
<hr>
<%= for actor <- @actor_results do %>
<div class="actor">
<h2 class="actor-nickname">
<a href="<%= local_actor_link(actor) %>">
<%= actor.data["preferredUsername"] %>
</a>
</h2>
<h3 class="actor-username">
<a href="<%= actor.ap_id %>">
<%= display_username(actor) %>
</a>
</h3>
</div>
<% end %>
<% end %>
<%= if length(@status_results) do %>
<hr>
<%= for {status, author} <- @status_results do %>
<%= render "_status.html", conn: @conn, author: author, status: status, note: status.data["object"] %>
<% end %>
<% end %>

View File

@ -13,11 +13,14 @@
<nav role="navigation">
<ul>
<li><a href="/"><%= instance_name() %></a></li>
<%= if @conn.assigns[:user] do %>
<li><a href="<%= Routes.frontend_path(@conn, :search) %>">Search</a></li>
<% end %>
</ul>
<ul>
<%= if @conn.assigns[:user] do %>
<li>
Logged in as <a href="<%= Routes.actor_path(@conn, :get, @conn.assigns[:user].username) %>"><%= @conn.assigns[:user].username %></a>.
Logged in as <a href="<%= Routes.actor_path(@conn, :get, @user.username) %>"><%= @user.username %></a>.
</li>
<li>
<%= form_for @conn, Routes.login_path(@conn, :logout), [method: :post], fn f -> %>
@ -34,8 +37,12 @@
</section>
</header>
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= if get_flash(@conn, :info) do %>
<p class="flash-info" role="alert"><%= get_flash(@conn, :info) %></p>
<% end %>
<%= if get_flash(@conn, :error) do %>
<p class="flash-error" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<%= render @view_module, @view_template, assigns %>
</main>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>

View File

@ -1,13 +1,15 @@
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
<%= form_tag Routes.login_path(@conn, :login_post), method: :post, class: "login-form" do %>
<%= if @continue do %>
<input type="hidden" name="continue" value="<%= @continue %>">
<% end %>
<label for="username">Username</label>
<input id="username" type="text" name="username">
<br>
<label for="password">Password</label>
<input id="password" type="password" name="password">
<br>
<div class="input-group">
<label for="username">Username:</label>
<input id="username" type="text" name="username">
</div>
<div class="input-group">
<label for="password">Password:</label>
<input id="password" type="password" name="password">
</div>
<%= submit "Log In" %>
<% end %>

View File

@ -1,6 +1,8 @@
defmodule ClacksWeb.FrontendView do
use ClacksWeb, :view
alias Clacks.{Actor, Activity}
alias ClacksWeb.Router.Helpers, as: Routes
alias ClacksWeb.Endpoint
@spec display_username(actor :: Actor.t()) :: String.t()
@ -13,6 +15,13 @@ defmodule ClacksWeb.FrontendView do
"@" <> name <> "@" <> host
end
def local_actor_link(%Actor{local: true, ap_id: ap_id}), do: ap_id
def local_actor_link(%Actor{local: false, id: id}),
do: Routes.frontend_path(Endpoint, :actor, id)
@spec display_timestamp(str :: String.t()) :: String.t()
def display_timestamp(str) when is_binary(str) do
display_timestamp(Timex.parse!(str, "{ISO:Extended}"))
end
@ -42,6 +51,9 @@ defmodule ClacksWeb.FrontendView do
end
end
@spec prev_page_path(conn :: Plug.Conn.t(), [Activity.t() | {Activity.t(), Actor.t()}]) ::
String.t()
def prev_page_path(conn, activities) do
if Map.has_key?(conn.query_params, "max_id") do
Phoenix.Controller.current_path(conn, %{
@ -52,6 +64,9 @@ defmodule ClacksWeb.FrontendView do
end
end
@spec next_page_path(conn :: Plug.Conn.t(), [Activity.t() | {Activity.t(), Actor.t()}]) ::
String.t()
def next_page_path(conn, activities) do
if length(activities) < 20 do
nil