feat: Federate posts
This commit is contained in:
parent
47b5313293
commit
44489ae211
10 changed files with 151 additions and 27 deletions
18
README.md
18
README.md
|
|
@ -2,19 +2,30 @@
|
|||
|
||||
- [ ] Posting
|
||||
- [x] Making posts locally
|
||||
- [ ] Figuring out follower list
|
||||
- [ ] Sending to followers
|
||||
- [x] Figuring out follower list
|
||||
- [x] Sending to followers
|
||||
- [ ] Post formatting
|
||||
- [ ] Sending posts w/ images / videos
|
||||
- [ ] Receiving posts w/ images / videos
|
||||
- [ ] Timeline
|
||||
- [x] My posts
|
||||
- [ ] Posts from accounts you follow
|
||||
- [ ] Show the actor avatar and display name
|
||||
- [ ] Deleting posts
|
||||
- [ ] Profile
|
||||
- [ ] Name field (for display name)
|
||||
- [x] Following
|
||||
- [ ] Scrape public posts from the outbox when you follow
|
||||
- [ ] Unfollowing
|
||||
- [x] Being followed
|
||||
- [x] Accepting follows
|
||||
- [ ] Approving / declining follows and authorized instance list
|
||||
- [ ] Liking
|
||||
- [ ] Unliking
|
||||
- [ ] CW posts
|
||||
- [ ] Polls
|
||||
- [ ] DMs
|
||||
- [ ] Followers-only posts (or maybe this is handled because we only send posts to followers? but we also include public in the TO field?)
|
||||
|
||||
## UX
|
||||
|
||||
|
|
@ -30,3 +41,6 @@
|
|||
- Approving / declining follows and authorized instance list
|
||||
- [ ] Liking
|
||||
- [ ] Unliking
|
||||
- [ ] CW posts
|
||||
- [ ] Polls
|
||||
- [ ] DMs
|
||||
|
|
|
|||
|
|
@ -20,20 +20,21 @@ module.exports = {
|
|||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
require('@tailwindcss/typography'),
|
||||
// Allows prefixing tailwind classes with LiveView classes to add rules
|
||||
// only when LiveView classes are applied, for example:
|
||||
//
|
||||
// <div class="phx-click-loading:animate-ping">
|
||||
//
|
||||
plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
|
||||
|
||||
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
|
||||
// See your `CoreComponents.icon/1` for more information.
|
||||
//
|
||||
plugin(function({matchComponents, theme}) {
|
||||
plugin(function ({ matchComponents, theme }) {
|
||||
let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
|
||||
let values = {}
|
||||
let icons = [
|
||||
|
|
@ -45,11 +46,11 @@ module.exports = {
|
|||
icons.forEach(([suffix, dir]) => {
|
||||
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
|
||||
let name = path.basename(file, ".svg") + suffix
|
||||
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
|
||||
values[name] = { name, fullPath: path.join(iconsDir, dir, file) }
|
||||
})
|
||||
})
|
||||
matchComponents({
|
||||
"hero": ({name, fullPath}) => {
|
||||
"hero": ({ name, fullPath }) => {
|
||||
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
|
||||
let size = theme("spacing.6")
|
||||
if (name.endsWith("-mini")) {
|
||||
|
|
@ -69,7 +70,7 @@ module.exports = {
|
|||
"height": size
|
||||
}
|
||||
}
|
||||
}, {values})
|
||||
}, { values })
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ defmodule Postland.Accounts.User do
|
|||
import Ecto.Changeset
|
||||
|
||||
schema "users" do
|
||||
field :name, :string
|
||||
field :summary, :string
|
||||
field :username, :string
|
||||
field :password, :string, virtual: true, redact: true
|
||||
field :hashed_password, :string, redact: true
|
||||
|
|
@ -112,6 +114,11 @@ defmodule Postland.Accounts.User do
|
|||
change(user, confirmed_at: now)
|
||||
end
|
||||
|
||||
def profile_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:name, :summary])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies the password.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ defmodule Postland.Activities do
|
|||
id = Ecto.UUID.autogenerate()
|
||||
html = Earmark.as_html!(markdown)
|
||||
|
||||
# Standard Mastodon will display the following kinds of post formatting:
|
||||
# bold, italics, strikethrough, ordered and unordered lists, blockquotes,
|
||||
# inline code, fenced code blocks, headers. Headers are displayed as bold
|
||||
# text in a separate paragraph.
|
||||
|
||||
# CW posts have a summary field which is the warning itself, a content
|
||||
# field (like a non-CW post) that is the body, and a "sensitive" => true field
|
||||
|
||||
activity =
|
||||
%{
|
||||
"@context" => [
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Postland.Timeline do
|
|||
end
|
||||
|
||||
def attribution(activity) do
|
||||
get_from_html_item(activity, "attributedTo")
|
||||
Postland.Actors.actor(get_from_html_item(activity, "attributedTo"))
|
||||
end
|
||||
|
||||
defp get_from_html_item(activity, field_name) do
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ defmodule PostlandWeb.CoreComponents do
|
|||
<.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-10 w-10 rounded-full" alt="" src="/images/avatar.png" />
|
||||
<img class="inline-block h-16 w-16 rounded-full" alt="" src="/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-indigo-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">
|
||||
<label for="post-body" class="sr-only">post body</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
|
|
@ -49,7 +49,7 @@ defmodule PostlandWeb.CoreComponents do
|
|||
<div class="flex-shrink-0">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
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>
|
||||
|
|
@ -65,16 +65,37 @@ defmodule PostlandWeb.CoreComponents do
|
|||
end
|
||||
|
||||
def post_card(assigns) do
|
||||
author = Postland.Timeline.attribution(assigns.post)
|
||||
|
||||
%{host: host} =
|
||||
case author do
|
||||
%{"id" => id} ->
|
||||
URI.parse(id)
|
||||
|
||||
nil ->
|
||||
%{host: "<<nil>>"}
|
||||
end
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> Map.put(:author, author)
|
||||
|> Map.put(:author_host, host)
|
||||
|
||||
~H"""
|
||||
<div class="flex items-start space-x-4 w-full">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="inline-block h-10 w-10 rounded-full" alt="" src="/images/avatar.png" />
|
||||
<img class="inline-block h-16 w-16 rounded-full" alt="" src="/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">
|
||||
<%= Postland.Timeline.attribution(@post) %>
|
||||
<span class="font-bold">
|
||||
<%= @author["name"] %>
|
||||
</span>
|
||||
<span class="ml-2 text-gray-500">
|
||||
@<%= @author["preferredUsername"] %>@<%= @author_host %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="px-4 py-5 sm:p-6 prose">
|
||||
<%= {:safe, Postland.Timeline.html_content(@post)} %>
|
||||
</div>
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
|
|
|
|||
|
|
@ -11,20 +11,20 @@
|
|||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-purple-50 antialiased">
|
||||
<header class="flex w-full bg-purple-950 text-purple-100 py-4">
|
||||
<body class="bg-violet-50 antialiased">
|
||||
<header class="flex w-full bg-violet-700 text-violet-100 py-4">
|
||||
<div class="flex-1 px-4 uppercase font-bold ">
|
||||
Postland
|
||||
<a href={~p"/"}>Postland</a>
|
||||
</div>
|
||||
<ul class="relative z-10 flex flex-1 items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
|
||||
<%= if @current_user do %>
|
||||
<li class="text-[0.8125rem] leading-6">
|
||||
<%= @current_user.username %>
|
||||
<li class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300">
|
||||
<a href={~p"/about"}>Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
href={~p"/users/settings"}
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-purple-300"
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
|
||||
>
|
||||
Settings
|
||||
</.link>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<.link
|
||||
href={~p"/users/log_out"}
|
||||
method="delete"
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-purple-300"
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
|
||||
>
|
||||
Log out
|
||||
</.link>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<li>
|
||||
<.link
|
||||
href={~p"/users/log_in"}
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-purple-300"
|
||||
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
|
||||
>
|
||||
Log in
|
||||
</.link>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,69 @@
|
|||
defmodule PostlandWeb.ProfileLive do
|
||||
use PostlandWeb, :live_view
|
||||
|
||||
alias Postland.Accounts.User
|
||||
alias Postland.Repo
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>Nothing here yet.</div>
|
||||
<div class="flex items-start space-x-4">
|
||||
<img class="h-16 w-16 rounded-full" src={url(~p"/images/avatar.png")} alt="" />
|
||||
<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">
|
||||
<h2 :if={!@editing} class="font-bold"><%= @account.name %></h2>
|
||||
<.input :if={@editing} field={@form[:name]} label="Display Name" />
|
||||
<span :if={!@editing} class="text-gray-500">
|
||||
@<%= @account.username %>@<%= @host %>
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="p-4">
|
||||
<p :if={!@editing} class="text-gray-500 text-sm">
|
||||
<%= {:safe, Earmark.as_html!(@account.summary || "")} %>
|
||||
</p>
|
||||
<.input :if={@editing} field={@form[:summary]} type="textarea" label="Bio" />
|
||||
</div>
|
||||
</.form>
|
||||
<hr />
|
||||
<div :if={assigns[:current_user]} class="text-right p-4">
|
||||
<.button :if={!@editing} phx-click="edit">Edit Profile</.button>
|
||||
<.button :if={@editing} phx-click="edit" form="profile-form">Save Profile</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_params, _session, socket) 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)}
|
||||
end
|
||||
|
||||
def handle_event("edit", _, socket) do
|
||||
{:noreply, assign(socket, :editing, true)}
|
||||
end
|
||||
|
||||
def handle_event("validate", params, socket) do
|
||||
changeset = User.profile_changeset(socket.assigns.account, params)
|
||||
form = to_form(changeset)
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"user" => params}, socket) do
|
||||
changeset = User.profile_changeset(socket.assigns.account, params)
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:ok, account} ->
|
||||
form = account |> User.profile_changeset(%{}) |> to_form()
|
||||
{:noreply, assign(socket, account: account, editing: false, form: form)}
|
||||
|
||||
{:error, changeset} ->
|
||||
form = to_form(changeset)
|
||||
|
||||
{:noreply, socket |> put_flash(:error, "An error occurred.") |> assign(form: form)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ defmodule PostlandWeb.Router do
|
|||
scope "/", PostlandWeb do
|
||||
pipe_through [:browser, :redirect_if_not_set_up]
|
||||
|
||||
live "/about", ProfileLive, :show
|
||||
live_session :mount_current_user,
|
||||
on_mount: [{PostlandWeb.UserAuth, :mount_current_user}] do
|
||||
live "/about", ProfileLive, :show
|
||||
end
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
|
|
|||
10
priv/repo/migrations/20241024233705_add_name_to_users.exs
Normal file
10
priv/repo/migrations/20241024233705_add_name_to_users.exs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Postland.Repo.Migrations.AddNameToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table("users") do
|
||||
add :name, :string
|
||||
add :summary, :text
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue