feat: Federate posts

This commit is contained in:
Ro 2024-10-26 10:43:59 -05:00
parent 47b5313293
commit 44489ae211
Signed by: ro
GPG key ID: 5B5AD5A568CDABF9
10 changed files with 151 additions and 27 deletions

View file

@ -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

View file

@ -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 })
})
]
}

View file

@ -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.

View file

@ -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" => [

View file

@ -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

View file

@ -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">

View file

@ -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>

View file

@ -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

View file

@ -34,8 +34,11 @@ defmodule PostlandWeb.Router do
scope "/", PostlandWeb do
pipe_through [:browser, :redirect_if_not_set_up]
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.
# scope "/api", PostlandWeb do

View 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