feat: Minimal posting form

This commit is contained in:
Ro 2024-10-16 10:30:23 -05:00
parent 7894049b6c
commit 3a9c23da95
12 changed files with 173 additions and 18 deletions

View file

@ -7,7 +7,7 @@
- [ ] Unfollowing
- [x] Being followed
- [x] Accepting follows
- Approving / declining follows and authorized instance list
- [ ] Approving / declining follows and authorized instance list
- [ ] Liking
- [ ] Unliking
@ -16,7 +16,7 @@
- [ ] Posting
- [ ] Timeline
- [ ] Deleting posts
- [ ] Following
- [~] Following
- [ ] Unfollowing
- [ ] Being followed
- [ ] Accepting follows

View file

@ -7,13 +7,47 @@ defmodule Postland.Activities do
alias Postland.Repo
alias Postland.Follows
def record_markdown_post(markdown) do
id = Ecto.UUID.autogenerate()
html = Earmark.as_html!(markdown)
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => url(~p"/activities/#{id}"),
"type" => "Create",
"actor" => Postland.my_actor_id(),
"object" => [
%{
"id" => url(~p"/activities/#{id}"),
"type" => "Note",
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"attributedTo" => Postland.my_actor_id(),
"mediaType" => "text/html",
"content" => html,
"to" => "https://www.w3.org/ns/activitystreams#Public"
},
%{
"id" => url(~p"/activities/#{id}"),
"type" => "Note",
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"attributedTo" => Postland.my_actor_id(),
"mediaType" => "text/markdown",
"content" => markdown,
"to" => "https://www.w3.org/ns/activitystreams#Public"
}
]
}
|> Postland.Activity.changeset()
|> Repo.insert()
end
def process_activity(params) do
case record_activity(params) do
{:ok, activity} ->
cause_effects(activity)
other ->
other
other ->
other
end
end
@ -23,16 +57,20 @@ defmodule Postland.Activities do
|> Repo.insert()
end
def cause_effects(%Activity{type: "Accept", data: %{"object" => %{"type" => "Follow", "id" => follow_id}}} = activity) do
def cause_effects(
%Activity{type: "Accept", data: %{"object" => %{"type" => "Follow", "id" => follow_id}}} =
activity
) do
pattern = ~r|/follows/([^/]+)|
actor_id = case Regex.run(pattern, follow_id) do
[_, encoded_actor_id] ->
Base.url_decode64!(encoded_actor_id, padding: false)
actor_id =
case Regex.run(pattern, follow_id) do
[_, encoded_actor_id] ->
Base.url_decode64!(encoded_actor_id, padding: false)
_other ->
nil
end
_other ->
nil
end
case Follows.get(Postland.my_actor_id(), actor_id) do
nil ->
@ -44,14 +82,17 @@ defmodule Postland.Activities do
Follows.confirm_request(request)
end
{:ok, activity}
{:ok, activity}
end
def cause_effects(%Activity{actor_id: actor_id, type: "Follow", data: %{"object" => object}} = activity) do
def cause_effects(
%Activity{actor_id: actor_id, type: "Follow", data: %{"object" => object}} = activity
) do
if object == Postland.my_actor_id() do
case Postland.Follows.record_inbound_request(actor_id) do
{:ok, _follow} ->
{:ok, activity}
other ->
other
end

16
lib/postland/post.ex Normal file
View file

@ -0,0 +1,16 @@
defmodule Postland.Post do
def html_content(activity) do
case Map.get(activity.data, "object") do
map when is_map(map) ->
Map.get(map, "content")
list when is_list(list) ->
Enum.find_value(list, fn map ->
Map.get(map, "mediaType") == "text/html" && Map.get(map, "content")
end) || ""
nil ->
""
end
end
end

View file

@ -19,6 +19,64 @@ defmodule PostlandWeb.CoreComponents do
alias Phoenix.LiveView.JS
use Gettext, backend: PostlandWeb.Gettext
def post_form(assigns) do
~H"""
<div class="relative">
<.form class="py-5" phx-submit="create_post" phx-change="change_post">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<img class="inline-block h-10 w-10 rounded-full" alt="" src="/images/avatar.png" />
</div>
<div class="min-w-0 flex-1 relative">
<div class="overflow-hidden bg-white relative rounded-lg shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<label for="post-body" class="sr-only">post body</label>
<textarea
rows="3"
name="post"
id="post-body"
class="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="post body"
></textarea>
<!-- Spacer element to match the height of the toolbar -->
<div class="py-2" aria-hidden="true">
<!-- Matches height of button in toolbar (1px border + 36px content height) -->
<div class="py-px">
<div class="h-9"></div>
</div>
<div class="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2">
<div class="flex items-center space-x-5"></div>
<div class="flex-shrink-0">
<button
type="submit"
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Post
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</.form>
</div>
"""
end
def post_card(assigns) do
~H"""
<div class="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6">
<%= {:safe, Postland.Post.html_content(@post)} %>
</div>
<div class="px-4 py-4 sm:px-6">
<!-- Content goes here -->
<!-- We use less vertical padding on card footers at all sizes than on headers or body sections -->
</div>
</div>
"""
end
@doc """
Renders a modal.

View file

@ -11,7 +11,7 @@
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<body class="bg-purple-50 antialiased">
<header class="flex w-full bg-purple-950 text-purple-100 py-4">
<div class="flex-1 px-4 uppercase font-bold ">
Postland

View file

@ -20,6 +20,11 @@ defmodule PostlandWeb.ActorJSON do
"id" => url(~p"/actor#main-key"),
"owner" => url(~p"/actor"),
"publicKeyPem" => user.public_key
},
"icon" => %{
"type" => "Image",
"mediaType" => "image/png",
"url" => url(~p"/images/avatar.png")
}
}
end

View file

@ -16,8 +16,13 @@ defmodule PostlandWeb.WebfingerJSON do
},
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" =>"text/html",
"href" => url(~p"/about")
"type" => "text/html",
"href" => url(~p"/about")
},
%{
"rel" => "http://webfinger.net/rel/avatar",
"type" => "image/png",
"href" => url(~p"/images/avatar.png")
}
]
}

View file

@ -0,0 +1,28 @@
defmodule PostlandWeb.TimelineLive do
use PostlandWeb, :live_view
def render(assigns) do
~H"""
<.post_form post_content={@post} />
<div id="timeline-posts" phx-update="stream">
<div :for={{id, post} <- @streams.posts} id={id} class="mt-10">
<.post_card post={post} />
</div>
</div>
"""
end
def mount(_params, _session, socket) do
{:ok, socket |> assign(:post, "") |> stream(:posts, [])}
end
def handle_event("create_post", %{"post" => post}, socket) do
{:ok, post} = Postland.Activities.record_markdown_post(post)
{:noreply, socket |> assign(:post, "") |> stream_insert(:posts, post)}
end
def handle_event("change_post", %{"post" => post}, socket) do
{:noreply, socket |> assign(:post, post)}
end
end

View file

@ -34,7 +34,6 @@ defmodule PostlandWeb.Router do
scope "/", PostlandWeb do
pipe_through [:browser, :redirect_if_not_set_up]
get "/", PageController, :home
live "/about", ProfileLive, :show
end
@ -79,6 +78,7 @@ defmodule PostlandWeb.Router do
live_session :require_authenticated_user,
on_mount: [{PostlandWeb.UserAuth, :ensure_authenticated}] do
live "/", TimelineLive, :show
live "/users/settings", UserSettingsLive, :edit
live "/@:acct", OtherProfileLive, :show
end

View file

@ -59,7 +59,8 @@ defmodule Postland.MixProject do
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"},
{:req, "~> 0.5.6"},
{:stream_data, "~> 1.1.1", only: [:test]}
{:stream_data, "~> 1.1.1", only: [:test]},
{:earmark, "~> 1.4.47"}
]
end

View file

@ -7,6 +7,7 @@
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
"ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.2", "200226e057f76c40be55fbac77771eb1a233260ab8ec7283f5da6d9402bde8de", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "a3838919c5a34c268c28cafab87b910bcda354a9a4e778658da46c149bb2c1da"},

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB