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 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];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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