feat: Upload an avatar
This commit is contained in:
parent
f52402767b
commit
3959deb503
13 changed files with 158 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -42,3 +42,5 @@ npm-debug.log
|
|||
*.db
|
||||
*.db-*
|
||||
|
||||
# Uploads
|
||||
/uploads/
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -4,12 +4,12 @@
|
|||
- [x] Making posts locally
|
||||
- [x] Figuring out follower list
|
||||
- [x] Sending to followers
|
||||
- [ ] Post formatting
|
||||
- [x] Post formatting
|
||||
- [ ] Sending posts w/ images / videos
|
||||
- [ ] Receiving posts w/ images / videos
|
||||
- [ ] Timeline
|
||||
- [x] My posts
|
||||
- [ ] Posts from accounts you follow
|
||||
- [x] Posts from accounts you follow
|
||||
- [ ] Show the actor avatar and display name
|
||||
- [ ] Deleting posts
|
||||
- [ ] Profile
|
||||
|
|
@ -30,17 +30,22 @@
|
|||
## UX
|
||||
|
||||
- [x] Posting
|
||||
- [ ] Timeline
|
||||
- [~] My posts
|
||||
- [ ] Posts from accounts you follow
|
||||
- [x] Timeline
|
||||
- [x] My posts
|
||||
- [x] Posts from accounts you follow
|
||||
- [ ] Deleting posts
|
||||
- [~] Following
|
||||
- [ ] Unfollowing
|
||||
- [ ] Being followed
|
||||
- [ ] Accepting follows
|
||||
- Approving / declining follows and authorized instance list
|
||||
- [ ] Approving / declining follows and authorized instance list
|
||||
- [ ] Liking
|
||||
- [ ] Unliking
|
||||
- [ ] CW posts
|
||||
- [ ] Polls
|
||||
- [ ] DMs
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Measure test coverage
|
||||
- [ ] Add tests
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ config :postland,
|
|||
ecto_repos: [Postland.Repo],
|
||||
generators: [timestamp_type: :utc_datetime]
|
||||
|
||||
upload_path =
|
||||
if config_env() == :prod do
|
||||
System.get_env("DATABASE_PATH") |> Path.dirname() |> Path.join("uploads/")
|
||||
else
|
||||
File.cwd!() |> Path.join("uploads/")
|
||||
end
|
||||
|
||||
config :postland,
|
||||
upload_path: upload_path
|
||||
|
||||
# Configures the endpoint
|
||||
config :postland, PostlandWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ defmodule Postland do
|
|||
|
||||
use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router
|
||||
|
||||
require Logger
|
||||
|
||||
alias Postland.Accounts
|
||||
|
||||
def temporary_password() do
|
||||
|
|
@ -31,6 +33,9 @@ defmodule Postland do
|
|||
end
|
||||
|
||||
def on_boot_setup() do
|
||||
File.mkdir_p!(Postland.Uploads.upload_path())
|
||||
Logger.info("File upload path: #{Postland.Uploads.upload_path()}")
|
||||
|
||||
if !setup?() do
|
||||
# TODO: Require the DB have restrictive permissions
|
||||
%{public: public_key, private: private_key} = generate_keys()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ defmodule Postland.Accounts.User do
|
|||
schema "users" do
|
||||
field :name, :string
|
||||
field :summary, :string
|
||||
field :icon, :string
|
||||
field :username, :string
|
||||
field :password, :string, virtual: true, redact: true
|
||||
field :hashed_password, :string, redact: true
|
||||
|
|
@ -119,6 +120,11 @@ defmodule Postland.Accounts.User do
|
|||
|> cast(attrs, [:name, :summary])
|
||||
end
|
||||
|
||||
def icon_changeset(user, path) do
|
||||
user
|
||||
|> cast(%{icon: path}, [:icon])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies the password.
|
||||
|
||||
|
|
|
|||
27
lib/postland/uploads.ex
Normal file
27
lib/postland/uploads.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
defmodule Postland.Uploads do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: PostlandWeb.Endpoint,
|
||||
router: PostlandWeb.Router,
|
||||
statics: ["uploads"]
|
||||
|
||||
def upload_path() do
|
||||
Application.get_env(:postland, :upload_path)
|
||||
end
|
||||
|
||||
def handle_upload(%{client_type: mime_type, uuid: uuid}, %{path: path} = meta) do
|
||||
extension = extension_from_mime(mime_type)
|
||||
dest = Path.join(upload_path(), "#{uuid}#{extension}")
|
||||
File.cp!(path, dest)
|
||||
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
|
||||
end
|
||||
|
||||
def extension_from_mime(mime_type) do
|
||||
case MIME.extensions(mime_type) do
|
||||
[] ->
|
||||
""
|
||||
|
||||
[ext | _rest] ->
|
||||
".#{ext}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -15,17 +15,29 @@ defmodule PostlandWeb.CoreComponents do
|
|||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
use Gettext, backend: PostlandWeb.Gettext
|
||||
|
||||
def post_form(assigns) 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" alt="" src="/images/avatar.png" />
|
||||
<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">
|
||||
|
|
@ -65,14 +77,14 @@ defmodule PostlandWeb.CoreComponents do
|
|||
end
|
||||
|
||||
def post_card(assigns) do
|
||||
author = Postland.Timeline.attribution(assigns.post)
|
||||
author = Postland.Timeline.attribution(assigns.post) || %{}
|
||||
|
||||
%{host: host} =
|
||||
case author do
|
||||
%{"id" => id} ->
|
||||
URI.parse(id)
|
||||
|
||||
nil ->
|
||||
_ ->
|
||||
%{host: "<<nil>>"}
|
||||
end
|
||||
|
||||
|
|
@ -84,7 +96,11 @@ defmodule PostlandWeb.CoreComponents do
|
|||
~H"""
|
||||
<div class="flex items-start space-x-4 w-full">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="inline-block h-16 w-16 rounded-full" alt="" src="/images/avatar.png" />
|
||||
<img
|
||||
class="inline-block h-16 w-16 rounded-full drop-shadow-lg"
|
||||
alt=""
|
||||
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="px-4 py-4 sm:px-6">
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ defmodule PostlandWeb.ActorJSON do
|
|||
"icon" => %{
|
||||
"type" => "Image",
|
||||
"mediaType" => "image/png",
|
||||
"url" => url(~p"/images/avatar.png")
|
||||
"url" =>
|
||||
if(user.icon,
|
||||
do: unverified_url(PostlandWeb.Endpoint, user.icon),
|
||||
else: url(~p"/images/avatar.png")
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ defmodule PostlandWeb.WebfingerJSON do
|
|||
%{
|
||||
"rel" => "http://webfinger.net/rel/avatar",
|
||||
"type" => "image/png",
|
||||
"href" => url(~p"/images/avatar.png")
|
||||
"href" =>
|
||||
if(user.icon,
|
||||
do: unverified_url(PostlandWeb.Endpoint, user.icon),
|
||||
else: url(~p"/images/avatar.png")
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule PostlandWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :postland
|
||||
|
||||
require Logger
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
# Set :encryption_salt if you would also like to encrypt it.
|
||||
|
|
@ -25,6 +27,13 @@ defmodule PostlandWeb.Endpoint do
|
|||
gzip: false,
|
||||
only: PostlandWeb.static_paths()
|
||||
|
||||
Logger.info("File upload path: #{Postland.Uploads.upload_path()}")
|
||||
|
||||
plug Plug.Static,
|
||||
at: "/uploads",
|
||||
from: Postland.Uploads.upload_path(),
|
||||
gzip: false
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
|
|
|
|||
|
|
@ -7,7 +7,16 @@ defmodule PostlandWeb.ProfileLive do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-start space-x-4">
|
||||
<img class="h-16 w-16 rounded-full" src={url(~p"/images/avatar.png")} alt="" />
|
||||
<div>
|
||||
<img
|
||||
class="h-16 w-16 rounded-full drop-shadow-lg"
|
||||
src={@account.icon || url(~p"/images/avatar.png")}
|
||||
alt=""
|
||||
/>
|
||||
<.form for={@form} phx-submit="avatar" phx-change="avatar">
|
||||
<.live_file_input :if={@editing} upload={@uploads.avatar} />
|
||||
</.form>
|
||||
</div>
|
||||
<div class="flex flex-col bg-white rounded-lg shadow container">
|
||||
<.form for={@form} id="profile-form" phx-change="validate" phx-submit="submit">
|
||||
<div class="p-4">
|
||||
|
|
@ -39,7 +48,17 @@ defmodule PostlandWeb.ProfileLive do
|
|||
%{host: host} = URI.parse(Postland.my_actor_id())
|
||||
account = Postland.Accounts.solo_user()
|
||||
form = account |> User.profile_changeset(%{}) |> to_form()
|
||||
{:ok, assign(socket, account: account, host: host, editing: false, form: form)}
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(account: account, host: host, editing: false, form: form)
|
||||
|> allow_upload(:avatar,
|
||||
accept: ~w(.jpg .jpeg .png),
|
||||
progress: &handle_progress/3,
|
||||
auto_upload: true
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def handle_event("edit", _, socket) do
|
||||
|
|
@ -66,4 +85,30 @@ defmodule PostlandWeb.ProfileLive do
|
|||
{:noreply, socket |> put_flash(:error, "An error occurred.") |> assign(form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("avatar", _, socket) do
|
||||
# The actual upload is handled by auto-upload and the handle_progress callback
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_progress(:avatar, entry, socket) do
|
||||
if entry.done? do
|
||||
uploaded_file =
|
||||
consume_uploaded_entry(socket, entry, fn meta ->
|
||||
Postland.Uploads.handle_upload(entry, meta)
|
||||
end)
|
||||
|
||||
changeset = User.icon_changeset(socket.assigns.account, uploaded_file)
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:ok, account} ->
|
||||
{:noreply, assign(socket, account: account)}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "An error occurred while finishing upload.")}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
3
mix.exs
3
mix.exs
|
|
@ -61,7 +61,8 @@ defmodule Postland.MixProject do
|
|||
{:req, "~> 0.5.6"},
|
||||
{:stream_data, "~> 1.1.1", only: [:test]},
|
||||
{:earmark, "~> 1.4.47"},
|
||||
{:cachex, "~> 4.0"}
|
||||
{:cachex, "~> 4.0"},
|
||||
{:mime, "~> 2.0.6"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Postland.Repo.Migrations.AddIconToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table("users") do
|
||||
add :icon, :string
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue