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
|
||||||
*.db-*
|
*.db-*
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
/uploads/
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -4,12 +4,12 @@
|
||||||
- [x] Making posts locally
|
- [x] Making posts locally
|
||||||
- [x] Figuring out follower list
|
- [x] Figuring out follower list
|
||||||
- [x] Sending to followers
|
- [x] Sending to followers
|
||||||
- [ ] Post formatting
|
- [x] Post formatting
|
||||||
- [ ] Sending posts w/ images / videos
|
- [ ] Sending posts w/ images / videos
|
||||||
- [ ] Receiving posts w/ images / videos
|
- [ ] Receiving posts w/ images / videos
|
||||||
- [ ] Timeline
|
- [ ] Timeline
|
||||||
- [x] My posts
|
- [x] My posts
|
||||||
- [ ] Posts from accounts you follow
|
- [x] Posts from accounts you follow
|
||||||
- [ ] Show the actor avatar and display name
|
- [ ] Show the actor avatar and display name
|
||||||
- [ ] Deleting posts
|
- [ ] Deleting posts
|
||||||
- [ ] Profile
|
- [ ] Profile
|
||||||
|
|
@ -30,17 +30,22 @@
|
||||||
## UX
|
## UX
|
||||||
|
|
||||||
- [x] Posting
|
- [x] Posting
|
||||||
- [ ] Timeline
|
- [x] Timeline
|
||||||
- [~] My posts
|
- [x] My posts
|
||||||
- [ ] Posts from accounts you follow
|
- [x] Posts from accounts you follow
|
||||||
- [ ] Deleting posts
|
- [ ] Deleting posts
|
||||||
- [~] Following
|
- [~] Following
|
||||||
- [ ] Unfollowing
|
- [ ] Unfollowing
|
||||||
- [ ] Being followed
|
- [ ] Being followed
|
||||||
- [ ] Accepting follows
|
- [ ] Accepting follows
|
||||||
- Approving / declining follows and authorized instance list
|
- [ ] Approving / declining follows and authorized instance list
|
||||||
- [ ] Liking
|
- [ ] Liking
|
||||||
- [ ] Unliking
|
- [ ] Unliking
|
||||||
- [ ] CW posts
|
- [ ] CW posts
|
||||||
- [ ] Polls
|
- [ ] Polls
|
||||||
- [ ] DMs
|
- [ ] DMs
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [ ] Measure test coverage
|
||||||
|
- [ ] Add tests
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,16 @@ config :postland,
|
||||||
ecto_repos: [Postland.Repo],
|
ecto_repos: [Postland.Repo],
|
||||||
generators: [timestamp_type: :utc_datetime]
|
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
|
# Configures the endpoint
|
||||||
config :postland, PostlandWeb.Endpoint,
|
config :postland, PostlandWeb.Endpoint,
|
||||||
url: [host: "localhost"],
|
url: [host: "localhost"],
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ defmodule Postland do
|
||||||
|
|
||||||
use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router
|
use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias Postland.Accounts
|
alias Postland.Accounts
|
||||||
|
|
||||||
def temporary_password() do
|
def temporary_password() do
|
||||||
|
|
@ -31,6 +33,9 @@ defmodule Postland do
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_boot_setup() do
|
def on_boot_setup() do
|
||||||
|
File.mkdir_p!(Postland.Uploads.upload_path())
|
||||||
|
Logger.info("File upload path: #{Postland.Uploads.upload_path()}")
|
||||||
|
|
||||||
if !setup?() do
|
if !setup?() do
|
||||||
# TODO: Require the DB have restrictive permissions
|
# TODO: Require the DB have restrictive permissions
|
||||||
%{public: public_key, private: private_key} = generate_keys()
|
%{public: public_key, private: private_key} = generate_keys()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule Postland.Accounts.User do
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :summary, :string
|
field :summary, :string
|
||||||
|
field :icon, :string
|
||||||
field :username, :string
|
field :username, :string
|
||||||
field :password, :string, virtual: true, redact: true
|
field :password, :string, virtual: true, redact: true
|
||||||
field :hashed_password, :string, redact: true
|
field :hashed_password, :string, redact: true
|
||||||
|
|
@ -119,6 +120,11 @@ defmodule Postland.Accounts.User do
|
||||||
|> cast(attrs, [:name, :summary])
|
|> cast(attrs, [:name, :summary])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def icon_changeset(user, path) do
|
||||||
|
user
|
||||||
|
|> cast(%{icon: path}, [:icon])
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Verifies the password.
|
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.
|
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||||
"""
|
"""
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router
|
||||||
|
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
use Gettext, backend: PostlandWeb.Gettext
|
use Gettext, backend: PostlandWeb.Gettext
|
||||||
|
|
||||||
def post_form(assigns) do
|
def post_form(assigns) do
|
||||||
|
user = Postland.Accounts.solo_user()
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<.form class="py-5" phx-submit="create_post" phx-change="change_post">
|
<.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 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>
|
||||||
<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">
|
||||||
|
|
@ -65,14 +77,14 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_card(assigns) do
|
def post_card(assigns) do
|
||||||
author = Postland.Timeline.attribution(assigns.post)
|
author = Postland.Timeline.attribution(assigns.post) || %{}
|
||||||
|
|
||||||
%{host: host} =
|
%{host: host} =
|
||||||
case author do
|
case author do
|
||||||
%{"id" => id} ->
|
%{"id" => id} ->
|
||||||
URI.parse(id)
|
URI.parse(id)
|
||||||
|
|
||||||
nil ->
|
_ ->
|
||||||
%{host: "<<nil>>"}
|
%{host: "<<nil>>"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -84,7 +96,11 @@ defmodule PostlandWeb.CoreComponents do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex items-start space-x-4 w-full">
|
<div class="flex items-start space-x-4 w-full">
|
||||||
<div class="flex-shrink-0">
|
<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>
|
||||||
<div class="divide-y flex-grow divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
|
<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">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ defmodule PostlandWeb.ActorJSON do
|
||||||
"icon" => %{
|
"icon" => %{
|
||||||
"type" => "Image",
|
"type" => "Image",
|
||||||
"mediaType" => "image/png",
|
"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
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@ defmodule PostlandWeb.WebfingerJSON do
|
||||||
%{
|
%{
|
||||||
"rel" => "http://webfinger.net/rel/avatar",
|
"rel" => "http://webfinger.net/rel/avatar",
|
||||||
"type" => "image/png",
|
"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
|
defmodule PostlandWeb.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :postland
|
use Phoenix.Endpoint, otp_app: :postland
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
# The session will be stored in the cookie and signed,
|
# The session will be stored in the cookie and signed,
|
||||||
# this means its contents can be read but not tampered with.
|
# this means its contents can be read but not tampered with.
|
||||||
# Set :encryption_salt if you would also like to encrypt it.
|
# Set :encryption_salt if you would also like to encrypt it.
|
||||||
|
|
@ -25,6 +27,13 @@ defmodule PostlandWeb.Endpoint do
|
||||||
gzip: false,
|
gzip: false,
|
||||||
only: PostlandWeb.static_paths()
|
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 reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
if code_reloading? do
|
if code_reloading? do
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@ defmodule PostlandWeb.ProfileLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex items-start space-x-4">
|
<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">
|
<div class="flex flex-col bg-white rounded-lg shadow container">
|
||||||
<.form for={@form} id="profile-form" phx-change="validate" phx-submit="submit">
|
<.form for={@form} id="profile-form" phx-change="validate" phx-submit="submit">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
|
@ -39,7 +48,17 @@ defmodule PostlandWeb.ProfileLive do
|
||||||
%{host: host} = URI.parse(Postland.my_actor_id())
|
%{host: host} = URI.parse(Postland.my_actor_id())
|
||||||
account = Postland.Accounts.solo_user()
|
account = Postland.Accounts.solo_user()
|
||||||
form = account |> User.profile_changeset(%{}) |> to_form()
|
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
|
end
|
||||||
|
|
||||||
def handle_event("edit", _, socket) do
|
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)}
|
{:noreply, socket |> put_flash(:error, "An error occurred.") |> assign(form: form)}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
3
mix.exs
3
mix.exs
|
|
@ -61,7 +61,8 @@ defmodule Postland.MixProject do
|
||||||
{:req, "~> 0.5.6"},
|
{:req, "~> 0.5.6"},
|
||||||
{:stream_data, "~> 1.1.1", only: [:test]},
|
{:stream_data, "~> 1.1.1", only: [:test]},
|
||||||
{:earmark, "~> 1.4.47"},
|
{:earmark, "~> 1.4.47"},
|
||||||
{:cachex, "~> 4.0"}
|
{:cachex, "~> 4.0"},
|
||||||
|
{:mime, "~> 2.0.6"}
|
||||||
]
|
]
|
||||||
end
|
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