From 3a9c23da9569f0d383e34228115bab33f5c0d6fc Mon Sep 17 00:00:00 2001 From: Ro Date: Wed, 16 Oct 2024 10:30:23 -0500 Subject: [PATCH] feat: Minimal posting form --- README.md | 4 +- lib/postland/activities.ex | 63 +++++++++++++++--- lib/postland/post.ex | 16 +++++ .../components/core_components.ex | 58 ++++++++++++++++ .../components/layouts/root.html.heex | 2 +- lib/postland_web/controllers/actor_json.ex | 5 ++ .../controllers/webfinger_json.ex | 9 ++- lib/postland_web/live/timeline_live.ex | 28 ++++++++ lib/postland_web/router.ex | 2 +- mix.exs | 3 +- mix.lock | 1 + priv/static/images/avatar.png | Bin 0 -> 14741 bytes 12 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 lib/postland/post.ex create mode 100644 lib/postland_web/live/timeline_live.ex create mode 100644 priv/static/images/avatar.png diff --git a/README.md b/README.md index fbedbf7..f4b0eda 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index e97b4b3..8589617 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -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 diff --git a/lib/postland/post.ex b/lib/postland/post.ex new file mode 100644 index 0000000..a00ea48 --- /dev/null +++ b/lib/postland/post.ex @@ -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 diff --git a/lib/postland_web/components/core_components.ex b/lib/postland_web/components/core_components.ex index 1b58fae..4915bca 100644 --- a/lib/postland_web/components/core_components.ex +++ b/lib/postland_web/components/core_components.ex @@ -19,6 +19,64 @@ defmodule PostlandWeb.CoreComponents do alias Phoenix.LiveView.JS use Gettext, backend: PostlandWeb.Gettext + def post_form(assigns) do + ~H""" +
+ <.form class="py-5" phx-submit="create_post" phx-change="change_post"> +
+
+ +
+
+
+ + + + +
+
+
+ +
+ """ + end + + def post_card(assigns) do + ~H""" +
+
+ <%= {:safe, Postland.Post.html_content(@post)} %> +
+
+ + +
+
+ """ + end + @doc """ Renders a modal. diff --git a/lib/postland_web/components/layouts/root.html.heex b/lib/postland_web/components/layouts/root.html.heex index e08cef8..36e26cc 100644 --- a/lib/postland_web/components/layouts/root.html.heex +++ b/lib/postland_web/components/layouts/root.html.heex @@ -11,7 +11,7 @@ - +
Postland diff --git a/lib/postland_web/controllers/actor_json.ex b/lib/postland_web/controllers/actor_json.ex index ad0e7e9..6db93ce 100644 --- a/lib/postland_web/controllers/actor_json.ex +++ b/lib/postland_web/controllers/actor_json.ex @@ -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 diff --git a/lib/postland_web/controllers/webfinger_json.ex b/lib/postland_web/controllers/webfinger_json.ex index e4dbb47..6e55d8d 100644 --- a/lib/postland_web/controllers/webfinger_json.ex +++ b/lib/postland_web/controllers/webfinger_json.ex @@ -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") } ] } diff --git a/lib/postland_web/live/timeline_live.ex b/lib/postland_web/live/timeline_live.ex new file mode 100644 index 0000000..679e20e --- /dev/null +++ b/lib/postland_web/live/timeline_live.ex @@ -0,0 +1,28 @@ +defmodule PostlandWeb.TimelineLive do + use PostlandWeb, :live_view + + def render(assigns) do + ~H""" + <.post_form post_content={@post} /> +
+
+ <.post_card post={post} /> +
+
+ """ + 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 diff --git a/lib/postland_web/router.ex b/lib/postland_web/router.ex index 8acd2f9..1d03462 100644 --- a/lib/postland_web/router.ex +++ b/lib/postland_web/router.ex @@ -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 diff --git a/mix.exs b/mix.exs index 0d08d14..5d274f3 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index 6753c6c..2aa4117 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/priv/static/images/avatar.png b/priv/static/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a19a965d5ce6dc75e6df535b1615045e41285a GIT binary patch literal 14741 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

!XjJ&s~~);(jvGh3Ciwe0DLg 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