Added timestamps to tweets and they organize by newest on top
This commit is contained in:
@@ -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<UUID>;
|
||||
};
|
||||
|
||||
insertedAt?: {
|
||||
eq?: UtcDateTimeUsec;
|
||||
notEq?: UtcDateTimeUsec;
|
||||
greaterThan?: UtcDateTimeUsec;
|
||||
greaterThanOrEqual?: UtcDateTimeUsec;
|
||||
lessThan?: UtcDateTimeUsec;
|
||||
lessThanOrEqual?: UtcDateTimeUsec;
|
||||
in?: Array<UtcDateTimeUsec>;
|
||||
};
|
||||
|
||||
state?: {
|
||||
eq?: "posted" | "drafted";
|
||||
notEq?: "posted" | "drafted";
|
||||
in?: Array<"posted" | "drafted">;
|
||||
};
|
||||
|
||||
userEmail?: {
|
||||
eq?: string;
|
||||
notEq?: string;
|
||||
in?: Array<string>;
|
||||
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];
|
||||
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
<div className="mx-tweet-body">
|
||||
<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-time">{timeAgo()}</span>
|
||||
{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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
123
priv/resource_snapshots/repo/tweets/20260401154313.json
Normal file
123
priv/resource_snapshots/repo/tweets/20260401154313.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user