refactor post model

This commit is contained in:
john 2025-08-10 18:08:17 +02:00
parent 62f9de9546
commit 30025b4044
8 changed files with 373 additions and 151 deletions

View file

@ -2,10 +2,10 @@ import { Temporal } from '@js-temporal/polyfill'
import { components } from '../../api/schema.ts'
import { immerable } from 'immer'
export interface EmojiReaction {
export interface PostReaction {
emoji: string
count: number
didReact: boolean
authorName: string
reactedOn: Temporal.Instant
}
export class Post {
@ -16,7 +16,7 @@ export class Post {
public readonly media: PostMedia[]
public readonly createdAt: Temporal.Instant
public readonly authorName: string
public readonly reactions: EmojiReaction[]
public readonly reactions: PostReaction[]
public readonly possibleReactions: string[]
constructor(
@ -25,7 +25,7 @@ export class Post {
media: PostMedia[],
createdAt: string | Temporal.Instant,
authorName: string,
reactions: EmojiReaction[] = [],
reactions: PostReaction[] = [],
possibleReactions: string[] = [],
) {
this.postId = postId
@ -44,11 +44,7 @@ export class Post {
dto.media.map((m) => new PostMediaImpl(new URL(m.url), m.width, m.height)),
Temporal.Instant.from(dto.createdAt),
dto.author.username,
dto.reactions.map((r) => ({
emoji: r.emoji,
count: r.count,
didReact: r.didReact,
})),
dto.reactions.map((r) => ({ ...r, reactedOn: Temporal.Instant.from(r.reactedOn) })),
dto.possibleReactions,
)
}

View file

@ -29,22 +29,34 @@ export class PostsService {
return Post.fromDto(response.data.post)
}
async loadPublicFeed(
cursor: string | null,
amount: number | null,
): Promise<{ posts: Post[]; next: string | null }> {
async load(postId: string): Promise<Post | null> {
const response = await this.client.GET('/posts/{postId}', {
params: {
path: { postId },
},
credentials: 'include',
})
if (!response.data?.post) {
return null
}
return Post.fromDto(response.data.post)
}
async loadPublicFeed(cursor: string | null, amount: number | null): Promise<{ posts: Post[] }> {
const response = await this.client.GET('/posts', {
params: {
query: { From: cursor ?? undefined, Amount: amount ?? undefined },
query: { After: cursor ?? undefined, Amount: amount ?? undefined },
},
credentials: 'include',
})
if (!response.data) {
return { posts: [], next: null }
return { posts: [] }
}
return { posts: response.data.posts.map(Post.fromDto), next: response.data.next }
return { posts: response.data.posts.map(Post.fromDto) }
}
async addReaction(postId: string, emoji: string): Promise<void> {

View file

@ -0,0 +1,82 @@
import { useCallback, useState } from 'react'
import { Post, PostMedia, PostReaction } from './posts.ts'
import { Temporal } from '@js-temporal/polyfill'
import { produce } from 'immer'
export interface PostInfo {
postId: string
authorName: string
content: string
createdAt: Temporal.Instant
media: PostMedia[]
possibleReactions: string[]
}
type ReactionMap = Record<string, PostReaction[]>
export function usePostViewModel() {
const [posts, _setPosts] = useState<PostInfo[]>([])
const [reactions, setReactions] = useState<ReactionMap>({})
const setPosts = useCallback((posts: Post[]) => {
_setPosts([...posts])
setReactions(
posts.reduce((acc, post) => {
acc[post.postId] = [...post.reactions]
return acc
}, {} as ReactionMap),
)
}, [])
const addPosts = useCallback((posts: Post[]) => {
_setPosts((current) => {
return [...current, ...posts]
})
setReactions((current) =>
produce(current, (draft) => {
for (const post of posts) {
draft[post.postId] = [...post.reactions]
}
}),
)
}, [])
function addReaction(
postId: string,
emoji: string,
authorName: string,
reactedOn: Temporal.Instant,
) {
setReactions((current) =>
produce(current, (draft) => {
if (draft[postId]?.some((r) => r.emoji === emoji && r.authorName == authorName)) {
return
}
const reaction: PostReaction = { emoji, authorName, reactedOn }
if (!draft[postId]) {
draft[postId] = [{ ...reaction }]
} else {
draft[postId].push({ ...reaction })
}
}),
)
}
function removeReaction(postId: string, emoji: string, authorName: string) {
setReactions((current) =>
produce(current, (draft) => {
if (!draft[postId]) return
draft[postId] = draft[postId].filter(
(r) => r.emoji !== emoji || r.authorName !== authorName,
)
}),
)
}
return { posts, reactions, addPosts, setPosts, addReaction, removeReaction }
}