Added timestamps to tweets and they organize by newest on top

This commit is contained in:
2026-04-01 11:58:12 -04:00
parent ae35600822
commit 0ac0b68029
5 changed files with 193 additions and 7 deletions

View File

@@ -4,6 +4,7 @@
export type UUID = string; export type UUID = string;
export type UtcDateTimeUsec = string;
// media Schema // media Schema
export type mediaResourceSchema = { export type mediaResourceSchema = {
@@ -31,13 +32,15 @@ export type mediaAttributesOnlySchema = {
// tweets Schema // tweets Schema
export type tweetsResourceSchema = { export type tweetsResourceSchema = {
__type: "Resource"; __type: "Resource";
__primitiveFields: "id" | "content" | "likes" | "userId" | "state" | "likedByMe"; __primitiveFields: "id" | "content" | "likes" | "userId" | "insertedAt" | "state" | "likedByMe" | "userEmail";
id: UUID; id: UUID;
content: string; content: string;
likes: number; likes: number;
userId: UUID; userId: UUID;
insertedAt: UtcDateTimeUsec;
state: "posted" | "drafted"; state: "posted" | "drafted";
likedByMe: boolean; likedByMe: boolean;
userEmail: string | null;
media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; }; media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; };
}; };
@@ -45,11 +48,12 @@ export type tweetsResourceSchema = {
export type tweetsAttributesOnlySchema = { export type tweetsAttributesOnlySchema = {
__type: "Resource"; __type: "Resource";
__primitiveFields: "id" | "content" | "likes" | "userId" | "state"; __primitiveFields: "id" | "content" | "likes" | "userId" | "insertedAt" | "state";
id: UUID; id: UUID;
content: string; content: string;
likes: number; likes: number;
userId: UUID; userId: UUID;
insertedAt: UtcDateTimeUsec;
state: "posted" | "drafted"; state: "posted" | "drafted";
}; };
@@ -121,12 +125,29 @@ export type tweetsFilterInput = {
in?: Array<UUID>; in?: Array<UUID>;
}; };
insertedAt?: {
eq?: UtcDateTimeUsec;
notEq?: UtcDateTimeUsec;
greaterThan?: UtcDateTimeUsec;
greaterThanOrEqual?: UtcDateTimeUsec;
lessThan?: UtcDateTimeUsec;
lessThanOrEqual?: UtcDateTimeUsec;
in?: Array<UtcDateTimeUsec>;
};
state?: { state?: {
eq?: "posted" | "drafted"; eq?: "posted" | "drafted";
notEq?: "posted" | "drafted"; notEq?: "posted" | "drafted";
in?: Array<"posted" | "drafted">; in?: Array<"posted" | "drafted">;
}; };
userEmail?: {
eq?: string;
notEq?: string;
in?: Array<string>;
isNil?: boolean;
};
likedByMe?: { likedByMe?: {
eq?: boolean; eq?: boolean;
notEq?: boolean; notEq?: boolean;
@@ -141,14 +162,14 @@ export type tweetsFilterInput = {
export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const; export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const;
export type mediaFilterField = (typeof mediaFilterFields)[number]; export type mediaFilterField = (typeof mediaFilterFields)[number];
export const tweetsFilterFields = ["id", "content", "likes", "userId", "state", "likedByMe", "user", "media"] as const; export const tweetsFilterFields = ["id", "content", "likes", "userId", "insertedAt", "state", "userEmail", "likedByMe", "user", "media"] as const;
export type tweetsFilterField = (typeof tweetsFilterFields)[number]; export type tweetsFilterField = (typeof tweetsFilterFields)[number];
export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const; export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const;
export type mediaSortField = (typeof mediaSortFields)[number]; export type mediaSortField = (typeof mediaSortFields)[number];
export const tweetsSortFields = ["id", "content", "likes", "userId", "state", "likedByMe"] as const; export const tweetsSortFields = ["id", "content", "likes", "userId", "insertedAt", "state", "userEmail", "likedByMe"] as const;
export type tweetsSortField = (typeof tweetsSortFields)[number]; export type tweetsSortField = (typeof tweetsSortFields)[number];

View File

@@ -33,6 +33,8 @@ type Tweet = {
userId: string; userId: string;
state: string; state: string;
media?: MediaItem[]; media?: MediaItem[];
userEmail?: string | null;
insertedAt?: string | null;
}; };
// ── Auth context ─────────────────────────────────────────────────────────────── // ── Auth context ───────────────────────────────────────────────────────────────
@@ -376,7 +378,7 @@ function TweetCard({ tweet }: { tweet: Tweet }) {
</div> </div>
<div className="mx-tweet-body"> <div className="mx-tweet-body">
<div className="mx-tweet-header"> <div className="mx-tweet-header">
<span className="mx-tweet-handle">@mixer</span> <span className="mx-tweet-handle">{tweet.userEmail ?? "@mixer"}</span>
<span className="mx-tweet-dot">·</span> <span className="mx-tweet-dot">·</span>
<span className="mx-tweet-time">{timeAgo()}</span> <span className="mx-tweet-time">{timeAgo()}</span>
{canModify && ( {canModify && (
@@ -490,8 +492,8 @@ function Feed() {
queryKey: ["tweets"], queryKey: ["tweets"],
queryFn: async () => { queryFn: async () => {
const res = await readTweet({ const res = await readTweet({
fields: ["id", "content", "likes", "likedByMe", "userId", "state", { media: ["id", "s3Key"] }], fields: ["id", "content", "likes", "likedByMe", "userId", "state", "userEmail", "insertedAt", { media: ["id", "s3Key"] }],
sort: "-id", sort: "-insertedAt",
headers: buildCSRFHeaders(), headers: buildCSRFHeaders(),
}); });
if (!res.success) throw new Error("Failed to load tweets"); if (!res.success) throw new Error("Failed to load tweets");

View File

@@ -129,6 +129,12 @@ defmodule Mixer.Posts.Tweet do
allow_nil? false allow_nil? false
public? true public? true
end end
create_timestamp :inserted_at do
public? true
end
update_timestamp :updated_at
end end
relationships do relationships do
@@ -146,6 +152,12 @@ defmodule Mixer.Posts.Tweet do
has_many :tweet_likes, Mixer.Posts.TweetLike has_many :tweet_likes, Mixer.Posts.TweetLike
end end
calculations do
calculate :user_email, :string, expr(user.email) do
public? true
end
end
aggregates do aggregates do
exists :liked_by_me, :tweet_likes do exists :liked_by_me, :tweet_likes do
public? true public? true

View File

@@ -0,0 +1,28 @@
defmodule Mixer.Repo.Migrations.AddTimestampsToTweets do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:tweets) do
add :inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
end
end
def down do
alter table(:tweets) do
remove :updated_at
remove :inserted_at
end
end
end

View File

@@ -0,0 +1,123 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "content",
"type": "text"
},
{
"allow_nil?": false,
"default": "0",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "likes",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "tweets_user_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "users"
},
"scale": null,
"size": null,
"source": "user_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "\"drafted\"",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "state",
"type": "text"
}
],
"base_filter": null,
"check_constraints": [],
"create_table_options": null,
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "5CA1873A0545807862B314C4E49F4E4538905E9BD3B40C33EE1AFE6ABD60538C",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mixer.Repo",
"schema": null,
"table": "tweets"
}