From 0ac0b6802919dd4015db65172be1d9579f25419d Mon Sep 17 00:00:00 2001 From: qdust41 Date: Wed, 1 Apr 2026 11:58:12 -0400 Subject: [PATCH] Added timestamps to tweets and they organize by newest on top --- assets/js/ash_types.ts | 29 ++++- assets/js/index.tsx | 8 +- lib/mixer/posts/tweet.ex | 12 ++ ...0260401154312_add_timestamps_to_tweets.exs | 28 ++++ .../repo/tweets/20260401154313.json | 123 ++++++++++++++++++ 5 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 priv/repo/migrations/20260401154312_add_timestamps_to_tweets.exs create mode 100644 priv/resource_snapshots/repo/tweets/20260401154313.json diff --git a/assets/js/ash_types.ts b/assets/js/ash_types.ts index b6a0be8..e7600d6 100644 --- a/assets/js/ash_types.ts +++ b/assets/js/ash_types.ts @@ -4,6 +4,7 @@ export type UUID = string; +export type UtcDateTimeUsec = string; // media Schema export type mediaResourceSchema = { @@ -31,13 +32,15 @@ export type mediaAttributesOnlySchema = { // tweets Schema export type tweetsResourceSchema = { __type: "Resource"; - __primitiveFields: "id" | "content" | "likes" | "userId" | "state" | "likedByMe"; + __primitiveFields: "id" | "content" | "likes" | "userId" | "insertedAt" | "state" | "likedByMe" | "userEmail"; id: UUID; content: string; likes: number; userId: UUID; + insertedAt: UtcDateTimeUsec; state: "posted" | "drafted"; likedByMe: boolean; + userEmail: string | null; media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; }; }; @@ -45,11 +48,12 @@ export type tweetsResourceSchema = { export type tweetsAttributesOnlySchema = { __type: "Resource"; - __primitiveFields: "id" | "content" | "likes" | "userId" | "state"; + __primitiveFields: "id" | "content" | "likes" | "userId" | "insertedAt" | "state"; id: UUID; content: string; likes: number; userId: UUID; + insertedAt: UtcDateTimeUsec; state: "posted" | "drafted"; }; @@ -121,12 +125,29 @@ export type tweetsFilterInput = { in?: Array; }; + insertedAt?: { + eq?: UtcDateTimeUsec; + notEq?: UtcDateTimeUsec; + greaterThan?: UtcDateTimeUsec; + greaterThanOrEqual?: UtcDateTimeUsec; + lessThan?: UtcDateTimeUsec; + lessThanOrEqual?: UtcDateTimeUsec; + in?: Array; + }; + state?: { eq?: "posted" | "drafted"; notEq?: "posted" | "drafted"; in?: Array<"posted" | "drafted">; }; + userEmail?: { + eq?: string; + notEq?: string; + in?: Array; + isNil?: boolean; + }; + likedByMe?: { eq?: boolean; notEq?: boolean; @@ -141,14 +162,14 @@ export type tweetsFilterInput = { export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const; 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 const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const; 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]; diff --git a/assets/js/index.tsx b/assets/js/index.tsx index 65af790..421c325 100644 --- a/assets/js/index.tsx +++ b/assets/js/index.tsx @@ -33,6 +33,8 @@ type Tweet = { userId: string; state: string; media?: MediaItem[]; + userEmail?: string | null; + insertedAt?: string | null; }; // ── Auth context ─────────────────────────────────────────────────────────────── @@ -376,7 +378,7 @@ function TweetCard({ tweet }: { tweet: Tweet }) {
- @mixer + {tweet.userEmail ?? "@mixer"} · {timeAgo()} {canModify && ( @@ -490,8 +492,8 @@ function Feed() { queryKey: ["tweets"], queryFn: async () => { const res = await readTweet({ - fields: ["id", "content", "likes", "likedByMe", "userId", "state", { media: ["id", "s3Key"] }], - sort: "-id", + fields: ["id", "content", "likes", "likedByMe", "userId", "state", "userEmail", "insertedAt", { media: ["id", "s3Key"] }], + sort: "-insertedAt", headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load tweets"); diff --git a/lib/mixer/posts/tweet.ex b/lib/mixer/posts/tweet.ex index 911819e..345270b 100644 --- a/lib/mixer/posts/tweet.ex +++ b/lib/mixer/posts/tweet.ex @@ -129,6 +129,12 @@ defmodule Mixer.Posts.Tweet do allow_nil? false public? true end + + create_timestamp :inserted_at do + public? true + end + + update_timestamp :updated_at end relationships do @@ -146,6 +152,12 @@ defmodule Mixer.Posts.Tweet do has_many :tweet_likes, Mixer.Posts.TweetLike end + calculations do + calculate :user_email, :string, expr(user.email) do + public? true + end + end + aggregates do exists :liked_by_me, :tweet_likes do public? true diff --git a/priv/repo/migrations/20260401154312_add_timestamps_to_tweets.exs b/priv/repo/migrations/20260401154312_add_timestamps_to_tweets.exs new file mode 100644 index 0000000..1a24799 --- /dev/null +++ b/priv/repo/migrations/20260401154312_add_timestamps_to_tweets.exs @@ -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 diff --git a/priv/resource_snapshots/repo/tweets/20260401154313.json b/priv/resource_snapshots/repo/tweets/20260401154313.json new file mode 100644 index 0000000..c8eceef --- /dev/null +++ b/priv/resource_snapshots/repo/tweets/20260401154313.json @@ -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" +} \ No newline at end of file