Add models and migrations
This commit is contained in:
parent
182523d36d
commit
9e542bc790
33 changed files with 597 additions and 125 deletions
|
@ -20,12 +20,12 @@ defmodule Nulla.ActivityPub do
|
|||
indexable: "toot:indexable",
|
||||
attributionDomains: %{"@id" => "toot:attributionDomains", "@type" => "@id"},
|
||||
Hashtag: "as:Hashtag",
|
||||
focalPoint: %{"@container" => "@list", "@id" => "toot:focalPoint"}
|
||||
vcard: "http://www.w3.org/2006/vcard/ns#"
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
@spec ap_user(String.t(), Nulla.Users.User.t()) :: Jason.OrderedObject.t()
|
||||
@spec ap_user(String.t(), Nulla.Models.User.t()) :: Jason.OrderedObject.t()
|
||||
def ap_user(domain, user) do
|
||||
Jason.OrderedObject.new([
|
||||
"@context": context(),
|
||||
|
@ -81,4 +81,41 @@ defmodule Nulla.ActivityPub do
|
|||
"vcard:Address": user.location
|
||||
])
|
||||
end
|
||||
|
||||
@spec note(String.t(), Nulla.Models.User.t(), Nulla.Models.Note.t()) :: Jason.OrderedObject.t()
|
||||
def note(domain, user, note) do
|
||||
Jason.OrderedObject.new([
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
Jason.OrderedObject.new(
|
||||
sensitive: "as:sensitive"
|
||||
)
|
||||
],
|
||||
id: "https://#{domain}/@#{user.username}/#{note.id}",
|
||||
type: "Note",
|
||||
summary: nil,
|
||||
inReplyTo: nil,
|
||||
published: note.inserted_at,
|
||||
url: "https://#{domain}/@#{user.username}/#{note.id}",
|
||||
attributedTo: "https://#{domain}/@#{user.username}",
|
||||
to: [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
cc: [
|
||||
"https://#{domain}/@#{user.username}/followers"
|
||||
],
|
||||
sensetive: false,
|
||||
content: note.content,
|
||||
contentMap: Jason.OrderedObject.new(
|
||||
"#{note.language}": "<p>@rf@mastodonsocial.ru Вниманию новичкам!</p><p>Вам небольшое руководство о том, как импротировать пост, которого нет в вашей ленте.</p>"
|
||||
),
|
||||
attachment: [
|
||||
Jason.OrderedObject.new(
|
||||
type: "Document",
|
||||
mediaType: "video/mp4",
|
||||
url: "https://mastodon.ml/system/media_attachments/files/000/040/494/original/8c06de179c11daea.mp4"
|
||||
)
|
||||
]
|
||||
])
|
||||
end
|
||||
end
|
||||
|
|
23
lib/nulla/models/bookmarks.ex
Normal file
23
lib/nulla/models/bookmarks.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule Nulla.Models.Bookmark do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
alias Nulla.Repo
|
||||
alias Nulla.Models.Bookmark
|
||||
|
||||
schema "bookmarks" do
|
||||
field :url, :string
|
||||
field :user_id, :id
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(bookmark, attrs) do
|
||||
bookmark
|
||||
|> cast(attrs, [:url, :user_id])
|
||||
|> validate_required([:url, :user_id])
|
||||
end
|
||||
|
||||
def get_all_bookmarks!(user_id), do: Repo.all(from n in Bookmark, where: n.user_id == ^user_id)
|
||||
end
|
19
lib/nulla/models/follow.ex
Normal file
19
lib/nulla/models/follow.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Nulla.Models.Follow do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "follows" do
|
||||
belongs_to :user, Nulla.Models.User
|
||||
belongs_to :target, Nulla.Models.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(follow, attrs) do
|
||||
follow
|
||||
|> cast(attrs, [:user_id, :target_id])
|
||||
|> validate_required([:user_id, :target_id])
|
||||
|> unique_constraint([:user_id, :target_id])
|
||||
end
|
||||
end
|
19
lib/nulla/models/hashtag.ex
Normal file
19
lib/nulla/models/hashtag.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Nulla.Models.Hashtag do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "hashtags" do
|
||||
field :tag, :string
|
||||
field :usage_count, :integer, default: 0
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(hashtag, attrs) do
|
||||
hashtag
|
||||
|> cast(attrs, [:tag, :usage_count])
|
||||
|> validate_required([:tag])
|
||||
|> unique_constraint(:tag)
|
||||
end
|
||||
end
|
|
@ -1,7 +1,8 @@
|
|||
defmodule Nulla.InstanceSettings do
|
||||
defmodule Nulla.Models.InstanceSettings do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Nulla.Repo
|
||||
alias Nulla.Models.InstanceSettings
|
||||
|
||||
schema "instance_settings" do
|
||||
field :name, :string, default: "Nulla"
|
||||
|
@ -10,14 +11,16 @@ defmodule Nulla.InstanceSettings do
|
|||
field :registration, :boolean, default: false
|
||||
field :max_characters, :integer, default: 5000
|
||||
field :max_upload_size, :integer, default: 50
|
||||
field :public_key, :string
|
||||
field :private_key, :string
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(instance_settings, attrs) do
|
||||
instance_settings
|
||||
|> cast(attrs, [:name, :description, :domain, :registration, :max_characters, :max_upload_size])
|
||||
|> validate_required([:name, :description, :domain, :registration, :max_characters, :max_upload_size])
|
||||
|> cast(attrs, [:name, :description, :domain, :registration, :max_characters, :max_upload_size, :public_key, :private_key])
|
||||
|> validate_required([:name, :description, :domain, :registration, :max_characters, :max_upload_size, :public_key, :private_key])
|
||||
end
|
||||
|
||||
def get_instance_settings!, do: Repo.one!(Nulla.InstanceSettings)
|
||||
def get_instance_settings!, do: Repo.one!(InstanceSettings)
|
||||
end
|
20
lib/nulla/models/media_attachment.ex
Normal file
20
lib/nulla/models/media_attachment.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Nulla.Models.MediaAttachment do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "media_attachments" do
|
||||
field :file, :string
|
||||
field :mime_type, :string
|
||||
field :description, :string
|
||||
|
||||
belongs_to :note, Nulla.Models.Note
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(media, attrs) do
|
||||
media
|
||||
|> cast(attrs, [:note_id, :file, :mime_type, :description])
|
||||
|> validate_required([:note_id, :file])
|
||||
end
|
||||
end
|
23
lib/nulla/models/moderation_log.ex
Normal file
23
lib/nulla/models/moderation_log.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule Nulla.Models.ModerationLog do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "moderation_logs" do
|
||||
field :target_type, :string
|
||||
field :target_id, :string
|
||||
field :action, :string
|
||||
field :reason, :string
|
||||
field :metadata, :map
|
||||
|
||||
belongs_to :moderator, Nulla.Models.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(moderation_log, attrs) do
|
||||
moderation_log
|
||||
|> cast(attrs, [:moderator_id, :target_type, :target_id, :action, :reason, :metadata])
|
||||
|> validate_required([:moderator_id, :target_type, :target_id, :action])
|
||||
end
|
||||
end
|
29
lib/nulla/models/note.ex
Normal file
29
lib/nulla/models/note.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Nulla.Models.Note do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
alias Nulla.Repo
|
||||
alias Nulla.Models.Note
|
||||
|
||||
schema "notes" do
|
||||
field :content, :string
|
||||
field :visibility, Ecto.Enum, values: [:public, :unlisted, :followers, :private], default: :public
|
||||
field :sensitive, :boolean, default: false
|
||||
field :language, :string
|
||||
field :in_reply_to, :string
|
||||
|
||||
belongs_to :user, Nulla.Models.User
|
||||
has_many :media_attachments, Nulla.Models.MediaAttachment
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(note, attrs) do
|
||||
note
|
||||
|> cast(attrs, [:content, :visibility, :sensitive, :language, :in_reply_to, :user_id])
|
||||
|> validate_required([:content, :visibility, :sensitive, :language, :in_reply_to, :user_id])
|
||||
end
|
||||
|
||||
def get_all_notes!(user_id), do: Repo.all(from n in Note, where: n.user_id == ^user_id)
|
||||
end
|
22
lib/nulla/models/notification.ex
Normal file
22
lib/nulla/models/notification.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule Nulla.Models.Notification do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "notifications" do
|
||||
field :type, :string
|
||||
field :data, :map
|
||||
field :read, :boolean, default: false
|
||||
|
||||
belongs_to :user, Nulla.Models.User
|
||||
belongs_to :actor, Nulla.Models.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(notification, attrs) do
|
||||
notification
|
||||
|> cast(attrs, [:user_id, :actor_id, :type, :data, :read])
|
||||
|> validate_required([:user_id, :type])
|
||||
end
|
||||
end
|
21
lib/nulla/models/session.ex
Normal file
21
lib/nulla/models/session.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Nulla.Models.Session do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "sessions" do
|
||||
field :token, :string
|
||||
field :user_agent, :string
|
||||
field :ip, :string
|
||||
|
||||
belongs_to :user, Nulla.Models.User
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(session, attrs) do
|
||||
session
|
||||
|> cast(attrs, [:user_id, :token, :user_agent, :ip])
|
||||
|> validate_required([:user_id, :token])
|
||||
|> unique_constraint(:token)
|
||||
end
|
||||
end
|
|
@ -1,13 +1,14 @@
|
|||
defmodule Nulla.Users.User do
|
||||
defmodule Nulla.Models.User do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Nulla.Repo
|
||||
alias Nulla.Models.User
|
||||
|
||||
schema "users" do
|
||||
field :username, :string
|
||||
field :email, :string
|
||||
field :password, :string
|
||||
field :is_moderator, :boolean, default: false
|
||||
|
||||
field :realname, :string
|
||||
field :bio, :string
|
||||
field :location, :string
|
||||
|
@ -24,6 +25,10 @@ defmodule Nulla.Users.User do
|
|||
field :avatar, :string
|
||||
field :banner, :string
|
||||
|
||||
has_many :user_sessions, Nulla.Models.Session
|
||||
has_many :notes, Nulla.Models.Note
|
||||
has_many :media_attachments, through: [:notes, :media_attachments]
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
|
@ -33,4 +38,6 @@ defmodule Nulla.Users.User do
|
|||
|> cast(attrs, [:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avatar, :banner])
|
||||
|> validate_required([:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avatar, :banner])
|
||||
end
|
||||
|
||||
def get_user_by_username!(username), do: Repo.get_by!(User, username: username)
|
||||
end
|
|
@ -1,106 +0,0 @@
|
|||
defmodule Nulla.Users do
|
||||
@moduledoc """
|
||||
The Users context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Nulla.Repo
|
||||
|
||||
alias Nulla.Users.User
|
||||
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_users()
|
||||
[%User{}, ...]
|
||||
|
||||
"""
|
||||
def list_users do
|
||||
Repo.all(User)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user!(123)
|
||||
%User{}
|
||||
|
||||
iex> get_user!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
def get_user_by_username!(username), do: Repo.get_by!(User, username: username)
|
||||
|
||||
@doc """
|
||||
Creates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_user(%{field: value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> create_user(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_user(attrs \\ %{}) do
|
||||
%User{}
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_user(user, %{field: new_value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> update_user(user, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_user(%User{} = user, attrs) do
|
||||
user
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_user(user)
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> delete_user(user)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_user(user)
|
||||
%Ecto.Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
def change_user(%User{} = user, attrs \\ %{}) do
|
||||
User.changeset(user, attrs)
|
||||
end
|
||||
end
|
|
@ -17,7 +17,7 @@ defmodule NullaWeb do
|
|||
those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
||||
def static_paths, do: ~w(assets files fonts images favicon.ico robots.txt)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
|
|
62
lib/nulla_web/controllers/note_controller.ex
Normal file
62
lib/nulla_web/controllers/note_controller.ex
Normal file
|
@ -0,0 +1,62 @@
|
|||
defmodule NullaWeb.NoteController do
|
||||
use NullaWeb, :controller
|
||||
|
||||
alias Nulla.Notes
|
||||
alias Nulla.Models.Note
|
||||
|
||||
def index(conn, _params) do
|
||||
notes = Notes.list_notes()
|
||||
render(conn, :index, notes: notes)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = Notes.change_note(%Note{})
|
||||
render(conn, :new, changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"note" => note_params}) do
|
||||
case Notes.create_note(note_params) do
|
||||
{:ok, note} ->
|
||||
conn
|
||||
|> put_flash(:info, "Note created successfully.")
|
||||
|> redirect(to: ~p"/notes/#{note}")
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, :new, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
note = Notes.get_note!(id)
|
||||
render(conn, :show, note: note)
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
note = Notes.get_note!(id)
|
||||
changeset = Notes.change_note(note)
|
||||
render(conn, :edit, note: note, changeset: changeset)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "note" => note_params}) do
|
||||
note = Notes.get_note!(id)
|
||||
|
||||
case Notes.update_note(note, note_params) do
|
||||
{:ok, note} ->
|
||||
conn
|
||||
|> put_flash(:info, "Note updated successfully.")
|
||||
|> redirect(to: ~p"/notes/#{note}")
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, :edit, note: note, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
note = Notes.get_note!(id)
|
||||
{:ok, _note} = Notes.delete_note(note)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Note deleted successfully.")
|
||||
|> redirect(to: ~p"/notes")
|
||||
end
|
||||
end
|
13
lib/nulla_web/controllers/note_html.ex
Normal file
13
lib/nulla_web/controllers/note_html.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule NullaWeb.NoteHTML do
|
||||
use NullaWeb, :html
|
||||
|
||||
embed_templates "note_html/*"
|
||||
|
||||
@doc """
|
||||
Renders a note form.
|
||||
"""
|
||||
attr :changeset, Ecto.Changeset, required: true
|
||||
attr :action, :string, required: true
|
||||
|
||||
def note_form(assigns)
|
||||
end
|
8
lib/nulla_web/controllers/note_html/edit.html.heex
Normal file
8
lib/nulla_web/controllers/note_html/edit.html.heex
Normal file
|
@ -0,0 +1,8 @@
|
|||
<.header>
|
||||
Edit Note {@note.id}
|
||||
<:subtitle>Use this form to manage note records in your database.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.note_form changeset={@changeset} action={~p"/notes/#{@note}"} />
|
||||
|
||||
<.back navigate={~p"/notes"}>Back to notes</.back>
|
23
lib/nulla_web/controllers/note_html/index.html.heex
Normal file
23
lib/nulla_web/controllers/note_html/index.html.heex
Normal file
|
@ -0,0 +1,23 @@
|
|||
<.header>
|
||||
Listing Notes
|
||||
<:actions>
|
||||
<.link href={~p"/notes/new"}>
|
||||
<.button>New Note</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table id="notes" rows={@notes} row_click={&JS.navigate(~p"/notes/#{&1}")}>
|
||||
<:col :let={note} label="Content">{note.content}</:col>
|
||||
<:action :let={note}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/notes/#{note}"}>Show</.link>
|
||||
</div>
|
||||
<.link navigate={~p"/notes/#{note}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
<:action :let={note}>
|
||||
<.link href={~p"/notes/#{note}"} method="delete" data-confirm="Are you sure?">
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
8
lib/nulla_web/controllers/note_html/new.html.heex
Normal file
8
lib/nulla_web/controllers/note_html/new.html.heex
Normal file
|
@ -0,0 +1,8 @@
|
|||
<.header>
|
||||
New Note
|
||||
<:subtitle>Use this form to manage note records in your database.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.note_form changeset={@changeset} action={~p"/notes"} />
|
||||
|
||||
<.back navigate={~p"/notes"}>Back to notes</.back>
|
9
lib/nulla_web/controllers/note_html/note_form.html.heex
Normal file
9
lib/nulla_web/controllers/note_html/note_form.html.heex
Normal file
|
@ -0,0 +1,9 @@
|
|||
<.simple_form :let={f} for={@changeset} action={@action}>
|
||||
<.error :if={@changeset.action}>
|
||||
Oops, something went wrong! Please check the errors below.
|
||||
</.error>
|
||||
<.input field={f[:content]} type="text" label="Content" />
|
||||
<:actions>
|
||||
<.button>Save Note</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
15
lib/nulla_web/controllers/note_html/show.html.heex
Normal file
15
lib/nulla_web/controllers/note_html/show.html.heex
Normal file
|
@ -0,0 +1,15 @@
|
|||
<.header>
|
||||
Note {@note.id}
|
||||
<:subtitle>This is a note record from your database.</:subtitle>
|
||||
<:actions>
|
||||
<.link href={~p"/notes/#{@note}/edit"}>
|
||||
<.button>Edit note</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Content">{@note.content}</:item>
|
||||
</.list>
|
||||
|
||||
<.back navigate={~p"/notes"}>Back to notes</.back>
|
|
@ -1,21 +1,23 @@
|
|||
defmodule NullaWeb.UserController do
|
||||
use NullaWeb, :controller
|
||||
alias Nulla.Users
|
||||
alias Nulla.InstanceSettings
|
||||
alias Nulla.Models.User
|
||||
alias Nulla.Models.Note
|
||||
alias Nulla.Models.InstanceSettings
|
||||
alias Nulla.ActivityPub
|
||||
|
||||
def show(conn, %{"username" => username}) do
|
||||
accept = List.first(get_req_header(conn, "accept"))
|
||||
instance_settings = InstanceSettings.get_instance_settings!()
|
||||
domain = instance_settings.domain
|
||||
user = Users.get_user_by_username!(username)
|
||||
user = User.get_user_by_username!(username)
|
||||
notes = Note.get_all_notes!(user.id)
|
||||
|
||||
if accept in ["application/activity+json", "application/ld+json"] do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> json(ActivityPub.ap_user(domain, user))
|
||||
else
|
||||
render(conn, :show, user: user, domain: domain, layout: false)
|
||||
render(conn, :show, domain: domain, user: user, notes: notes, layout: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,4 +38,42 @@ defmodule NullaWeb.UserHTML do
|
|||
|
||||
"#{formatted} (#{relative})"
|
||||
end
|
||||
|
||||
def format_note_datetime(datetime) do
|
||||
Timex.format!(datetime, "{0D} {Mfull} {YYYY}, {h24}:{m}", :strftime)
|
||||
end
|
||||
|
||||
def format_note_datetime_diff(datetime) do
|
||||
now = Timex.now()
|
||||
diff = Timex.diff(now, datetime, :seconds)
|
||||
|
||||
cond do
|
||||
diff < 60 ->
|
||||
"now"
|
||||
|
||||
diff < 3600 ->
|
||||
minutes = div(diff, 60)
|
||||
"#{minutes}m ago"
|
||||
|
||||
diff < 86400 ->
|
||||
hours = div(diff, 3600)
|
||||
"#{hours}h ago"
|
||||
|
||||
diff < 518400 ->
|
||||
days = div(diff, 86400)
|
||||
"#{days}d ago"
|
||||
|
||||
diff < 2419200 ->
|
||||
weeks = div(diff, 604800)
|
||||
"#{weeks}w ago"
|
||||
|
||||
diff < 28512000 ->
|
||||
months = Timex.diff(now, datetime, :months)
|
||||
"#{months}mo ago"
|
||||
|
||||
true ->
|
||||
years = Timex.diff(now, datetime, :years)
|
||||
"#{years}y ago"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,8 +10,11 @@
|
|||
</div>
|
||||
<div class="relative border border-gray-300 shadow-md mt-5 rounded-t-xl overflow-hidden">
|
||||
<div class="relative w-full aspect-[3/1]">
|
||||
<img src={~p"/images/banner.jpg"} class="w-full h-full object-cover" />
|
||||
<img src={~p"/images/avatar.jpg"} class="absolute left-4 bottom-0 translate-y-1/2 rounded-full border-4 border-white w-[8.33vw] h-[8.33vw] min-w-[80px] min-h-[80px] max-w-[160px] max-h-[160px]"/>
|
||||
<img src={"/files/#{@user.banner}"} class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 flex items-end justify-between px-4 pb-2 pointer-events-none">
|
||||
<img src={"/files/#{@user.avatar}"} class="translate-y-1/2 rounded-full border-4 border-white w-[8.33vw] h-[8.33vw] min-w-[80px] min-h-[80px] max-w-[160px] max-h-[160px] pointer-events-auto"/>
|
||||
<button class="px-8 py-2 rounded-full text-sm font-semibold border transition bg-black text-white border-black hover:bg-gray-900 pointer-events-auto">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[4.5vw] px-4 flex flex-col">
|
||||
<span class="text-xl font-bold">{@user.realname}</span>
|
||||
|
@ -26,14 +29,12 @@
|
|||
</dt>
|
||||
<dd><%= @user.location %></dd>
|
||||
<% end %>
|
||||
|
||||
<%= if @user.birthday do %>
|
||||
<dt class="flex items-center gap-2">
|
||||
<.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" />
|
||||
</dt>
|
||||
<dd><%= format_birthdate(@user.birthday) %></dd>
|
||||
<% end %>
|
||||
|
||||
<dt class="flex items-center gap-2">
|
||||
<.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" />
|
||||
</dt>
|
||||
|
@ -59,12 +60,54 @@
|
|||
<a href={~p"/@#{@user.username}/followers"}>31 Followers</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between px-20 py-2 mt-5 border border-gray-300">
|
||||
<div class="flex justify-between px-20 py-2 mt-5 border-y border-gray-300">
|
||||
<a href={~p"/@#{@user.username}/featured"}>Featured</a>
|
||||
<a href={~p"/@#{@user.username}"}>Posts</a>
|
||||
<a href={~p"/@#{@user.username}/with_replies"}>Posts and replies</a>
|
||||
<a href={~p"/@#{@user.username}/media"}>Media</a>
|
||||
</div>
|
||||
<div>
|
||||
<%= for note <- @notes do %>
|
||||
<div class="p-4 border-b border-gray-300">
|
||||
<div class="flex items-start space-x-4">
|
||||
<img src={"/files/#{@user.avatar}"} class="rounded-full w-[58px] h-[58px]"/>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-semibold text-gray-900 text-sm">
|
||||
<%= @user.realname %>
|
||||
</span>
|
||||
<span class="text-gray-500 text-sm">
|
||||
@<%= @user.username %>@<%= @domain %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<%= case note.visibility do %>
|
||||
<% :public -> %>
|
||||
<.icon name="hero-globe-americas" class="h-5 w-5" />
|
||||
<% :unlisted -> %>
|
||||
<.icon name="hero-moon" class="h-5 w-5" />
|
||||
<% :followers -> %>
|
||||
<.icon name="hero-lock-closed" class="h-5 w-5" />
|
||||
<% :private -> %>
|
||||
<.icon name="hero-at-symbol" class="h-5 w-5" />
|
||||
<% end %>
|
||||
<span><%= format_note_datetime_diff(note.inserted_at) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-800">
|
||||
<p><%= note.content %></p>
|
||||
</div>
|
||||
<div class="flex gap-10 mt-4">
|
||||
<button><.icon name="hero-chat-bubble-left" class="h-5 w-5" /></button>
|
||||
<button><.icon name="hero-arrow-path-rounded-square" class="h-5 w-5" /></button>
|
||||
<button><.icon name="hero-plus" class="h-5 w-5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center mt-5 gap-5">
|
||||
<div class="text-sm rounded-xl border border-gray-300 p-4 w-[90%] h-[300px]">
|
||||
|
|
|
@ -9,6 +9,10 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do
|
|||
add :registration, :boolean, default: false, null: false
|
||||
add :max_characters, :integer, default: 5000, null: false
|
||||
add :max_upload_size, :integer, default: 50, null: false
|
||||
add :public_key, :string
|
||||
add :private_key, :string
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,6 @@ defmodule Nulla.Repo.Migrations.CreateUsers do
|
|||
add :email, :string
|
||||
add :password, :string
|
||||
add :is_moderator, :boolean, default: false, null: false
|
||||
|
||||
add :realname, :string
|
||||
add :bio, :string
|
||||
add :location, :string
|
||||
|
|
18
priv/repo/migrations/20250604083506_create_notes.exs
Normal file
18
priv/repo/migrations/20250604083506_create_notes.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateNotes do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:notes) do
|
||||
add :content, :string
|
||||
add :visibility, :string, default: "public"
|
||||
add :sensitive, :boolean, default: false
|
||||
add :language, :string
|
||||
add :in_reply_to, :string
|
||||
add :user_id, references(:users, on_delete: :delete_all)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create index(:notes, [:user_id])
|
||||
end
|
||||
end
|
14
priv/repo/migrations/20250606100445_create_bookmarks.exs
Normal file
14
priv/repo/migrations/20250606100445_create_bookmarks.exs
Normal file
|
@ -0,0 +1,14 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateBookmarks do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:bookmarks) do
|
||||
add :url, :string
|
||||
add :user_id, references(:users, on_delete: :delete_all)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create index(:bookmarks, [:user_id])
|
||||
end
|
||||
end
|
18
priv/repo/migrations/20250606103230_create_notifications.exs
Normal file
18
priv/repo/migrations/20250606103230_create_notifications.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateNotifications do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:notifications) do
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
add :actor_id, references(:users, on_delete: :nilify_all)
|
||||
add :type, :string, null: false
|
||||
add :data, :map
|
||||
add :read, :boolean, default: false, null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:notifications, [:user_id])
|
||||
create index(:notifications, [:actor_id])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateModerationLogs do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:moderation_logs) do
|
||||
add :moderator_id, references(:users, on_delete: :nilify_all), null: false
|
||||
add :target_type, :string, null: false
|
||||
add :target_id, :string, null: false
|
||||
add :action, :string, null: false
|
||||
add :reason, :text
|
||||
add :metadata, :map
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:moderation_logs, [:moderator_id])
|
||||
create index(:moderation_logs, [:target_type, :target_id])
|
||||
end
|
||||
end
|
14
priv/repo/migrations/20250606103649_create_hashtags.exs
Normal file
14
priv/repo/migrations/20250606103649_create_hashtags.exs
Normal file
|
@ -0,0 +1,14 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateHashtags do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:hashtags) do
|
||||
add :tag, :string, null: false
|
||||
add :usage_count, :integer, default: 0, null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:hashtags, [:tag])
|
||||
end
|
||||
end
|
15
priv/repo/migrations/20250606103707_create_follows.exs
Normal file
15
priv/repo/migrations/20250606103707_create_follows.exs
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateFollows do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:follows) do
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
add :target_id, references(:users, on_delete: :delete_all), null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:follows, [:user_id, :target_id])
|
||||
create index(:follows, [:target_id])
|
||||
end
|
||||
end
|
17
priv/repo/migrations/20250606131715_create_sessions.exs
Normal file
17
priv/repo/migrations/20250606131715_create_sessions.exs
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateSessions do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:sessions) do
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
add :token, :string, null: false
|
||||
add :user_agent, :string
|
||||
add :ip, :string
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create index(:sessions, [:user_id])
|
||||
create unique_index(:sessions, [:token])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Nulla.Repo.Migrations.CreateMediaAttachments do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:media_attachments) do
|
||||
add :note_id, references(:notes, on_delete: :delete_all), null: false
|
||||
add :file, :string, null: false
|
||||
add :mime_type, :string
|
||||
add :description, :string
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create index(:media_attachments, [:note_id])
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue