feat: Attach images to posts

This commit is contained in:
Ro 2024-11-05 20:14:49 -06:00
parent 42027c046c
commit cf364a11c6
Signed by: ro
GPG key ID: 5B5AD5A568CDABF9
6 changed files with 162 additions and 46 deletions

View file

@ -4,10 +4,11 @@
- [x] Broadcasting them to followers
- [x] Post formatting
- [x] Deleting posts
- [ ] Sending posts w/ images / videos
- [x] Sending posts w/ images
- [ ] Making posts with CWs
- [ ] Making polls
- [ ] Followers-only posts (or maybe this is handled because we only send posts to followers? but we also include public in the TO field?)
- [] Sending posts with videos
## Profile
@ -38,7 +39,8 @@
- [x] Your posts show up in timeline
- [x] Posts from accounts you follow show up in timeline
- [x] Show the actor avatar and display name
- [ ] Receiving posts w/ images / videos
- [ ] Receiving posts w/ images
- [ ] Receiving posts w/ videos
- [ ] Liking posts
- [ ] Unliking posts
- [ ] Displaying CW posts behind CW

View file

@ -9,7 +9,7 @@ defmodule Postland.Activities do
alias Postland.Repo
alias Postland.Follows
def record_markdown_post(markdown) do
def record_markdown_post(markdown, attachments) do
id = Ecto.UUID.autogenerate()
html = Earmark.as_html!(markdown)
@ -48,7 +48,8 @@ defmodule Postland.Activities do
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [
"#{Postland.my_actor_id()}/followers"
]
],
"attachment" => attachments
}
}

View file

@ -8,6 +8,15 @@ defmodule Postland.Timeline do
get_from_html_item(activity, "content")
end
def attachments(activity) do
activity.data
|> Map.get("items")
|> Enum.find_value(fn item -> item["attachment"] end) ||
[]
# TODO: Filter down to just image/* and video/* MIME types
end
def attribution(activity) do
Postland.Actors.actor(get_from_html_item(activity, "attributedTo"))
end
@ -27,9 +36,6 @@ defmodule Postland.Timeline do
end
end
def attribution(map) do
end
def timeline do
from(a in Object, where: a.type == "Note", order_by: [desc: a.inserted_at])
# TODO: Only accounts I'm following + myself

View file

@ -8,7 +8,7 @@ defmodule Postland.Uploads do
Application.get_env(:postland, :upload_path)
end
def handle_upload(%{client_type: mime_type, uuid: uuid}, %{path: path} = meta) do
def handle_upload(%{client_type: mime_type, uuid: uuid}, %{path: path}) do
extension = extension_from_mime(mime_type)
dest = Path.join(upload_path(), "#{uuid}#{extension}")
File.cp!(path, dest)

View file

@ -70,23 +70,23 @@ defmodule PostlandWeb.CoreComponents do
user = Postland.Accounts.solo_user()
~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-16 w-16 rounded-full drop-shadow-lg"
alt=""
src={
if(user.icon,
do: unverified_url(PostlandWeb.Endpoint, user.icon),
else: url(~p"/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-violet-600">
<div class="relative px-5">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<img
class="inline-block h-16 w-16 rounded-full drop-shadow-lg"
alt=""
src={
if(user.icon,
do: unverified_url(PostlandWeb.Endpoint, user.icon),
else: url(~p"/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-violet-600">
<.form phx-submit="create_post" phx-change="change_post" id="create-post-form">
<label for="post-body" class="sr-only">post body</label>
<textarea
rows="3"
@ -96,28 +96,71 @@ defmodule PostlandWeb.CoreComponents do
placeholder="post body"
value={@post_content}
></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-violet-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-violet-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600"
>
Post
</button>
</.form>
<div class="flex">
<div :for={attachment <- @attachments} class="m-2 w-1/2">
<.attachment_edit attachment={attachment} id={attachment["url"]} />
</div>
</div>
<!-- Spacer element to match the height of the toolbar -->
<div class="py-2" aria-hidden="true">
<div class="flex justify-between py-2 px-3">
<div class="flex items-end space-x-5">
<div class="h-9 flex items-center text-gray-500">
<label class="relative">
<.live_file_input
id="file-upload"
class="opacity-0 absolute top-0 left-0 bottom-0 right-0"
upload={@upload}
form="create-post-form"
/>
<.icon name="hero-photo" />
</label>
</div>
</div>
<div class="flex-shrink-0">
<button
form="create-post-form"
type="submit"
class="inline-flex items-center rounded-md bg-violet-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-violet-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600"
>
Post
</button>
</div>
</div>
</div>
</div>
</div>
</.form>
</div>
</div>
"""
end
def attachment_edit(assigns) do
~H"""
<div class="relative m-2 rounded-lg bg-black shadow border-zinc-300">
<div
class="aspect-square w-full bg-center bg-contain bg-no-repeat mt-1"
style={"background-image: url('#{@attachment["url"]}')"}
>
</div>
<div class="py-2 px-3">
<.input
type="textarea"
placeholder="alt text"
name={@attachment["url"]}
phx-change="change_alt_text"
phx-value-url={@attachment["url"]}
value={@attachment["name"]}
/>
</div>
<div
phx-click="remove_attachment"
phx-value-url={@attachment["url"]}
class="absolute bg-white rounded-full shadow right-2 top-2 z-10 p-2"
>
<.icon name="hero-trash" />
</div>
</div>
"""
end
@ -134,10 +177,14 @@ defmodule PostlandWeb.CoreComponents do
%{host: "<<nil>>"}
end
attachments =
Postland.Timeline.attachments(assigns.post)
assigns =
assigns
|> Map.put(:author, author)
|> Map.put(:author_host, host)
|> Map.put(:attachments, attachments)
~H"""
<div class="flex items-start space-x-4 w-full">
@ -148,7 +195,7 @@ defmodule PostlandWeb.CoreComponents do
src={get_in(@author, ["icon", "url"]) || "/images/avatar.png"}
/>
</div>
<div class="divide-y flex-grow divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
<div class="flex-grow overflow-hidden rounded-lg bg-white shadow">
<div class="px-4 py-4 sm:px-6">
<span class="font-bold">
<%= @author["name"] %>
@ -160,6 +207,21 @@ defmodule PostlandWeb.CoreComponents do
<div class="px-4 py-5 sm:p-6 prose">
<%= {:safe, Postland.Timeline.html_content(@post)} %>
</div>
<div :if={@attachments != []} class="px-4 py-5 sm:p-6">
<div :for={attachment <- @attachments} class="flex">
<div
class="w-1/2 aspect-square bg-black bg-center bg-contain bg-no-repeat rounded-lg shadow"
style={"background-image: url('#{attachment["url"]}')"}
phx-click={show_modal("modal-#{Base.url_encode64(attachment["url"], padding: false)}")}
phx-value-attachment={attachment["url"]}
/>
<.modal id={"modal-#{Base.url_encode64(attachment["url"], padding: false)}"}>
<div class="w-full flex justify-center">
<img src={attachment["url"]} />
</div>
</.modal>
</div>
</div>
<div class="px-4 py-4 sm:px-6">
<a
:if={@author["id"] == Postland.my_actor_id()}
@ -220,7 +282,7 @@ defmodule PostlandWeb.CoreComponents do
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<div class="w-full max-w-4xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}

View file

@ -6,7 +6,7 @@ defmodule PostlandWeb.TimelineLive do
def render(assigns) do
~H"""
<.post_form post_content={@post} />
<.post_form post_content={@post} attachments={@attachments} upload={@uploads.files} />
<div id="timeline-posts" phx-update="stream">
<div :for={{id, post} <- @streams.posts} id={id} class="mt-10">
<.post_card post={post} post_dom_id={id} />
@ -16,11 +16,22 @@ defmodule PostlandWeb.TimelineLive do
end
def mount(_params, _session, socket) do
{:ok, socket |> assign(:post, "") |> stream(:posts, Postland.Timeline.timeline())}
socket =
socket
|> assign(:post, "")
|> assign(:attachments, [])
|> stream(:posts, Postland.Timeline.timeline())
|> allow_upload(:files,
accept: ["image/*"],
auto_upload: true,
progress: &handle_progress/3
)
{:ok, socket}
end
def handle_event("create_post", %{"post" => post}, socket) do
{:ok, results} = Postland.Activities.record_markdown_post(post)
{:ok, results} = Postland.Activities.record_markdown_post(post, socket.assigns.attachments)
new_posts =
results
@ -31,6 +42,7 @@ defmodule PostlandWeb.TimelineLive do
socket =
socket
|> assign(:post, "")
|> assign(:attachments, [])
|> then(fn socket ->
Enum.reduce(new_posts, socket, fn {_id, post}, socket ->
stream_insert(socket, :posts, post, at: 0)
@ -55,4 +67,37 @@ defmodule PostlandWeb.TimelineLive do
def handle_event("change_post", %{"post" => post}, socket) do
{:noreply, socket |> assign(:post, post)}
end
def handle_event("remove_attachment", %{"url" => url}, socket) do
attachments =
socket.assigns.attachments
|> Enum.reject(fn %{"url" => attachment_url} -> attachment_url == url end)
{:noreply, assign(socket, attachments: attachments)}
end
def handle_event("change_alt_text", %{"alt" => value, "url" => url}, socket) do
{:noreply, socket}
end
def handle_progress(:files, entry, socket) do
if entry.done? do
uploaded_file =
consume_uploaded_entry(socket, entry, fn meta ->
Postland.Uploads.handle_upload(entry, meta)
end)
attachment = %{
"type" => "Document",
"mediaType" => entry.client_type,
"url" => unverified_url(PostlandWeb.Endpoint, uploaded_file),
"name" => nil
}
attachments = socket.assigns.attachments ++ [attachment]
{:noreply, assign(socket, attachments: attachments)}
else
{:noreply, socket}
end
end
end