feat: Attach images to posts
This commit is contained in:
parent
42027c046c
commit
cf364a11c6
6 changed files with 162 additions and 46 deletions
|
|
@ -4,10 +4,11 @@
|
||||||
- [x] Broadcasting them to followers
|
- [x] Broadcasting them to followers
|
||||||
- [x] Post formatting
|
- [x] Post formatting
|
||||||
- [x] Deleting posts
|
- [x] Deleting posts
|
||||||
- [ ] Sending posts w/ images / videos
|
- [x] Sending posts w/ images
|
||||||
- [ ] Making posts with CWs
|
- [ ] Making posts with CWs
|
||||||
- [ ] Making polls
|
- [ ] 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?)
|
- [ ] 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
|
## Profile
|
||||||
|
|
||||||
|
|
@ -38,7 +39,8 @@
|
||||||
- [x] Your posts show up in timeline
|
- [x] Your posts show up in timeline
|
||||||
- [x] Posts from accounts you follow show up in timeline
|
- [x] Posts from accounts you follow show up in timeline
|
||||||
- [x] Show the actor avatar and display name
|
- [x] Show the actor avatar and display name
|
||||||
- [ ] Receiving posts w/ images / videos
|
- [ ] Receiving posts w/ images
|
||||||
|
- [ ] Receiving posts w/ videos
|
||||||
- [ ] Liking posts
|
- [ ] Liking posts
|
||||||
- [ ] Unliking posts
|
- [ ] Unliking posts
|
||||||
- [ ] Displaying CW posts behind CW
|
- [ ] Displaying CW posts behind CW
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ defmodule Postland.Activities do
|
||||||
alias Postland.Repo
|
alias Postland.Repo
|
||||||
alias Postland.Follows
|
alias Postland.Follows
|
||||||
|
|
||||||
def record_markdown_post(markdown) do
|
def record_markdown_post(markdown, attachments) do
|
||||||
id = Ecto.UUID.autogenerate()
|
id = Ecto.UUID.autogenerate()
|
||||||
html = Earmark.as_html!(markdown)
|
html = Earmark.as_html!(markdown)
|
||||||
|
|
||||||
|
|
@ -48,7 +48,8 @@ defmodule Postland.Activities do
|
||||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"cc" => [
|
"cc" => [
|
||||||
"#{Postland.my_actor_id()}/followers"
|
"#{Postland.my_actor_id()}/followers"
|
||||||
]
|
],
|
||||||
|
"attachment" => attachments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@ defmodule Postland.Timeline do
|
||||||
get_from_html_item(activity, "content")
|
get_from_html_item(activity, "content")
|
||||||
end
|
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
|
def attribution(activity) do
|
||||||
Postland.Actors.actor(get_from_html_item(activity, "attributedTo"))
|
Postland.Actors.actor(get_from_html_item(activity, "attributedTo"))
|
||||||
end
|
end
|
||||||
|
|
@ -27,9 +36,6 @@ defmodule Postland.Timeline do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribution(map) do
|
|
||||||
end
|
|
||||||
|
|
||||||
def timeline do
|
def timeline do
|
||||||
from(a in Object, where: a.type == "Note", order_by: [desc: a.inserted_at])
|
from(a in Object, where: a.type == "Note", order_by: [desc: a.inserted_at])
|
||||||
# TODO: Only accounts I'm following + myself
|
# TODO: Only accounts I'm following + myself
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule Postland.Uploads do
|
||||||
Application.get_env(:postland, :upload_path)
|
Application.get_env(:postland, :upload_path)
|
||||||
end
|
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)
|
extension = extension_from_mime(mime_type)
|
||||||
dest = Path.join(upload_path(), "#{uuid}#{extension}")
|
dest = Path.join(upload_path(), "#{uuid}#{extension}")
|
||||||
File.cp!(path, dest)
|
File.cp!(path, dest)
|
||||||
|
|
|
||||||
|
|
@ -70,23 +70,23 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
user = Postland.Accounts.solo_user()
|
user = Postland.Accounts.solo_user()
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="relative">
|
<div class="relative px-5">
|
||||||
<.form class="py-5" phx-submit="create_post" phx-change="change_post">
|
<div class="flex items-start space-x-4">
|
||||||
<div class="flex items-start space-x-4">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
<img
|
||||||
<img
|
class="inline-block h-16 w-16 rounded-full drop-shadow-lg"
|
||||||
class="inline-block h-16 w-16 rounded-full drop-shadow-lg"
|
alt=""
|
||||||
alt=""
|
src={
|
||||||
src={
|
if(user.icon,
|
||||||
if(user.icon,
|
do: unverified_url(PostlandWeb.Endpoint, user.icon),
|
||||||
do: unverified_url(PostlandWeb.Endpoint, user.icon),
|
else: url(~p"/images/avatar.png")
|
||||||
else: url(~p"/images/avatar.png")
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div class="min-w-0 flex-1 relative">
|
||||||
<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="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>
|
<label for="post-body" class="sr-only">post body</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows="3"
|
rows="3"
|
||||||
|
|
@ -96,28 +96,71 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
placeholder="post body"
|
placeholder="post body"
|
||||||
value={@post_content}
|
value={@post_content}
|
||||||
></textarea>
|
></textarea>
|
||||||
<!-- Spacer element to match the height of the toolbar -->
|
</.form>
|
||||||
<div class="py-2" aria-hidden="true">
|
<div class="flex">
|
||||||
<!-- Matches height of button in toolbar (1px border + 36px content height) -->
|
<div :for={attachment <- @attachments} class="m-2 w-1/2">
|
||||||
<div class="py-px">
|
<.attachment_edit attachment={attachment} id={attachment["url"]} />
|
||||||
<div class="h-9"></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2">
|
<!-- Spacer element to match the height of the toolbar -->
|
||||||
<div class="flex items-center space-x-5"></div>
|
<div class="py-2" aria-hidden="true">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex justify-between py-2 px-3">
|
||||||
<button
|
<div class="flex items-end space-x-5">
|
||||||
type="submit"
|
<div class="h-9 flex items-center text-gray-500">
|
||||||
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"
|
<label class="relative">
|
||||||
>
|
<.live_file_input
|
||||||
Post
|
id="file-upload"
|
||||||
</button>
|
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>
|
</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>
|
||||||
</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>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -134,10 +177,14 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
%{host: "<<nil>>"}
|
%{host: "<<nil>>"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attachments =
|
||||||
|
Postland.Timeline.attachments(assigns.post)
|
||||||
|
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
|> Map.put(:author, author)
|
|> Map.put(:author, author)
|
||||||
|> Map.put(:author_host, host)
|
|> Map.put(:author_host, host)
|
||||||
|
|> Map.put(:attachments, attachments)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex items-start space-x-4 w-full">
|
<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"}
|
src={get_in(@author, ["icon", "url"]) || "/images/avatar.png"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
<%= @author["name"] %>
|
<%= @author["name"] %>
|
||||||
|
|
@ -160,6 +207,21 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
<div class="px-4 py-5 sm:p-6 prose">
|
<div class="px-4 py-5 sm:p-6 prose">
|
||||||
<%= {:safe, Postland.Timeline.html_content(@post)} %>
|
<%= {:safe, Postland.Timeline.html_content(@post)} %>
|
||||||
</div>
|
</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">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
<a
|
<a
|
||||||
:if={@author["id"] == Postland.my_actor_id()}
|
:if={@author["id"] == Postland.my_actor_id()}
|
||||||
|
|
@ -220,7 +282,7 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div class="flex min-h-full items-center justify-center">
|
<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
|
<.focus_wrap
|
||||||
id={"#{@id}-container"}
|
id={"#{@id}-container"}
|
||||||
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ defmodule PostlandWeb.TimelineLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.post_form post_content={@post} />
|
<.post_form post_content={@post} attachments={@attachments} upload={@uploads.files} />
|
||||||
<div id="timeline-posts" phx-update="stream">
|
<div id="timeline-posts" phx-update="stream">
|
||||||
<div :for={{id, post} <- @streams.posts} id={id} class="mt-10">
|
<div :for={{id, post} <- @streams.posts} id={id} class="mt-10">
|
||||||
<.post_card post={post} post_dom_id={id} />
|
<.post_card post={post} post_dom_id={id} />
|
||||||
|
|
@ -16,11 +16,22 @@ defmodule PostlandWeb.TimelineLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
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
|
end
|
||||||
|
|
||||||
def handle_event("create_post", %{"post" => post}, socket) do
|
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 =
|
new_posts =
|
||||||
results
|
results
|
||||||
|
|
@ -31,6 +42,7 @@ defmodule PostlandWeb.TimelineLive do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:post, "")
|
|> assign(:post, "")
|
||||||
|
|> assign(:attachments, [])
|
||||||
|> then(fn socket ->
|
|> then(fn socket ->
|
||||||
Enum.reduce(new_posts, socket, fn {_id, post}, socket ->
|
Enum.reduce(new_posts, socket, fn {_id, post}, socket ->
|
||||||
stream_insert(socket, :posts, post, at: 0)
|
stream_insert(socket, :posts, post, at: 0)
|
||||||
|
|
@ -55,4 +67,37 @@ defmodule PostlandWeb.TimelineLive do
|
||||||
def handle_event("change_post", %{"post" => post}, socket) do
|
def handle_event("change_post", %{"post" => post}, socket) do
|
||||||
{:noreply, socket |> assign(:post, post)}
|
{:noreply, socket |> assign(:post, post)}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue