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] 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue