diff --git a/app/components/building-blocks/open-discussion-preview.tsx b/app/components/building-blocks/open-discussion-preview.tsx new file mode 100644 index 00000000..08185498 --- /dev/null +++ b/app/components/building-blocks/open-discussion-preview.tsx @@ -0,0 +1,41 @@ +import { Link } from '@remix-run/react' +import moment from 'moment' +import { type FrontPagePost } from '#app/modules/posts/post-types.ts' +import { PostContent } from './post-content.tsx' + +export function OpenDiscussionPreview({ + post, + className, +}: { + post: FrontPagePost + className?: string +}) { + const ageString = moment(post.createdAt).fromNow() + const commentString = post.nTransitiveComments == 1 ? 'comment' : 'comments' + + return ( +
+
+
+
{ageString}
+ +
+ + {post.nTransitiveComments} {commentString} + +
+
+
+
+ ) +} diff --git a/app/components/building-blocks/poll-post-preview.tsx b/app/components/building-blocks/poll-post-preview.tsx index 28e3c4ed..2c9da830 100644 --- a/app/components/building-blocks/poll-post-preview.tsx +++ b/app/components/building-blocks/poll-post-preview.tsx @@ -2,7 +2,7 @@ import { Link } from '@remix-run/react' import moment from 'moment' import { useState } from 'react' import { type Artefact, type Quote } from '#app/modules/claims/claim-types.ts' -import { type Poll } from '#app/modules/posts/post-types.ts' +import { type FrontPagePoll } from '#app/modules/posts/post-types.ts' import { isValidTweetUrl } from '#app/utils/twitter-utils.ts' import { Icon } from '../ui/icon.tsx' import { EmbeddedTweet } from './embedded-integration.tsx' @@ -13,7 +13,7 @@ export function PollPostPreview({ post, className, }: { - post: Poll + post: FrontPagePoll className?: string }) { const ageString = moment(post.createdAt).fromNow() @@ -38,7 +38,7 @@ export function PollPostPreview({ deactivateLinks={false} linkTo={`/post/${post.id}`} /> - {showPollPostContext && post.context && ( + {showPollPostContext && post.context && post.context.artefact && ( postDetailsRef: React.RefObject treeContext: TreeContext postState: PostState }) { + const post = replyTree.post const user = useOptionalUser() + const isPoll = postIsPoll(post) const loggedIn: boolean = user !== null const isAdminUser: boolean = user ? Boolean(user.isAdmin) : false const [showAdminUI, setShowAdminUI] = useState(false) @@ -87,11 +92,7 @@ export function PostActionBar({ }) } - const isTargetPost = post.id == treeContext.targetPostId - - const isTopLevelPost = post.parentId === null - - const submitVote = async function (direction: Direction) { + const submitVote = async function (direction: VoteDirection) { const payLoad = { postId: post.id, focussedPostId: treeContext.targetPostId, @@ -162,35 +163,17 @@ export function PostActionBar({ ))} - {loggedIn && (!isTargetPost || !isTopLevelPost || !post.pollType) && ( + {loggedIn && !isDeleted && ( <> - - Vote - + ) : ( + + )} )} {false && ( @@ -262,6 +245,90 @@ export function PostActionBar({ ) } +function VoteButtons({ + postState, + submitVote, +}: { + postState: PostState + submitVote: (direction: VoteDirection) => Promise +}) { + return ( + <> + + Vote + + + ) +} + +function PollVoteButtons({ + poll, + postState, + submitVote, +}: { + poll: Poll + postState: PostState + submitVote: (direction: VoteDirection) => Promise +}) { + // TODO: handle else case properly + const upvoteLabel = poll.pollType == PollType.FactCheck ? 'True' : 'Agree' + + // TODO: handle else case properly + const downvoteLabel = + poll.pollType == PollType.FactCheck ? 'False' : 'Disagree' + + return ( +
+ + +
+ ) +} + function AdminFeatureBar({ isDeleted, handleDeletePost, @@ -336,7 +403,7 @@ function ReplyForm({ }) const responseDecoded = (await response.json()) as { commentTreeState: CommentTreeState - newReplyTree: ReplyTree + newReplyTree: MutableReplyTree } // after successful submission, remove from localstorage localStorage.removeItem(storageKey) diff --git a/app/components/building-blocks/post-details.tsx b/app/components/building-blocks/post-details.tsx index 99e090b7..fd3ac20f 100644 --- a/app/components/building-blocks/post-details.tsx +++ b/app/components/building-blocks/post-details.tsx @@ -1,37 +1,29 @@ -import { Link, useNavigate } from '@remix-run/react' -import type * as Immutable from 'immutable' +import { useNavigate } from '@remix-run/react' +import type Immutable from 'immutable' import { useRef } from 'react' import PollResult from '#app/components/building-blocks/poll-result.tsx' import { PostActionBar } from '#app/components/building-blocks/post-action-bar.tsx' import { PostContent } from '#app/components/building-blocks/post-content.tsx' import { PostInfoBar } from '#app/components/building-blocks/post-info-bar.tsx' -import { type FallacyList } from '#app/modules/fallacies/fallacy-types.ts' -import { PollType, type Post } from '#app/modules/posts/post-types.ts' +import { postIsPoll } from '#app/modules/posts/post-service.ts' +import { VoteDirection } from '#app/modules/posts/post-types.ts' +import { type ReplyTree } from '#app/modules/posts/ranking/ranking-types.ts' import { type TreeContext } from '#app/routes/post.$postId.tsx' -import { - type CommentTreeState, - Direction, - type ImmutableReplyTree, - type PostState, -} from '#app/types/api-types.ts' import { invariant } from '#app/utils/misc.tsx' -import { useOptionalUser } from '#app/utils/user.ts' export function PostDetails({ - post, - fallacyList, - className, replyTree, pathFromTargetPost, treeContext, + className, }: { - post: Post - fallacyList: FallacyList - className?: string - replyTree: ImmutableReplyTree + replyTree: ReplyTree pathFromTargetPost: Immutable.List treeContext: TreeContext + className?: string }) { + const post = replyTree.post + const fallacyList = replyTree.fallacyList const postState = treeContext.commentTreeState.posts[post.id] invariant( postState !== undefined, @@ -40,15 +32,13 @@ export function PostDetails({ const navigate = useNavigate() - const isDeleted = postState.isDeleted - const postDetailsRef = useRef(null) // used for scrolling to this post + const isPoll = postIsPoll(post) + const isDeleted = postState.isDeleted const isTargetPost = treeContext.targetPostId === post.id - const isTopLevelPost = post.parentId === null - - const userHasVoted = postState.voteState.vote != Direction.Neutral + const userHasVoted = postState.voteState.vote != VoteDirection.Neutral return (
)} - {isTargetPost && isTopLevelPost && post.pollType && ( - + - )} - +
- {isTargetPost && isTopLevelPost && post.pollType && ( + {isTargetPost && isTopLevelPost && isPoll && ( ) } - -function PollVoteButtons({ - post, - postState, - treeContext, -}: { - post: Post - postState: PostState - treeContext: TreeContext -}) { - const user = useOptionalUser() - const loggedIn: boolean = user !== null - - const submitVote = async function (direction: Direction) { - const payLoad = { - postId: post.id, - focussedPostId: treeContext.targetPostId, - direction: direction, - currentVoteState: postState.voteState.vote, - } - const response = await fetch('/vote', { - method: 'POST', - body: JSON.stringify(payLoad), - headers: { - 'Content-Type': 'application/json', - }, - }) - const newCommentTreeState = (await response.json()) as CommentTreeState - treeContext.setCommentTreeState(newCommentTreeState) - } - - // TODO: handle else case properly - const upvoteLabel = post.pollType == PollType.FactCheck ? 'True' : 'Agree' - - // todo: handle else case properly - const downvoteLabel = - post.pollType == PollType.FactCheck ? 'False' : 'Disagree' - - return loggedIn ? ( -
- - -
- ) : ( -
- Log in to comment and vote. -
- ) -} diff --git a/app/components/building-blocks/post-info-bar.tsx b/app/components/building-blocks/post-info-bar.tsx index 22caf20d..25c524fd 100644 --- a/app/components/building-blocks/post-info-bar.tsx +++ b/app/components/building-blocks/post-info-bar.tsx @@ -1,15 +1,15 @@ import moment from 'moment' import { useState } from 'react' import { type FallacyList } from '#app/modules/fallacies/fallacy-types.ts' -import { type Post } from '#app/modules/posts/post-types.ts' -import { type PostState } from '#app/types/api-types.ts' +import { type Poll, type Post } from '#app/modules/posts/post-types.ts' +import { type PostState } from '#app/modules/posts/ranking/ranking-types.ts' export function PostInfoBar({ post, fallacyList, postState, }: { - post: Post + post: Post | Poll fallacyList: FallacyList postState: PostState }) { diff --git a/app/components/building-blocks/post-with-replies.tsx b/app/components/building-blocks/post-with-replies.tsx index 7034fef2..b1ffaec3 100644 --- a/app/components/building-blocks/post-with-replies.tsx +++ b/app/components/building-blocks/post-with-replies.tsx @@ -1,6 +1,6 @@ import { PostDetails } from '#app/components/building-blocks/post-details.tsx' +import { type ReplyTree } from '#app/modules/posts/ranking/ranking-types.ts' import { type TreeContext } from '#app/routes/post.$postId.tsx' -import { type ImmutableReplyTree } from '#app/types/api-types.ts' export function PostWithReplies({ replyTree, @@ -8,7 +8,7 @@ export function PostWithReplies({ treeContext, className, }: { - replyTree: ImmutableReplyTree + replyTree: ReplyTree pathFromTargetPost: Immutable.List treeContext: TreeContext className?: string @@ -24,16 +24,14 @@ export function PostWithReplies({ <> {!hideChildren && (
- {replyTree.replies.map((tree: ImmutableReplyTree) => { + {replyTree.replies.map((tree: ReplyTree) => { return ( = +import { type ColumnType, type Selectable, type Insertable } from 'kysely' + +type Generated = T extends ColumnType ? ColumnType : ColumnType -export type Timestamp = ColumnType -export type Vote = { +type Vote = { userId: string postId: number vote: number latestVoteEventId: number voteEventTime: number } -export type Password = { + +type Password = { hash: string userId: string } -export type Post = { + +type Post = { id: Generated parentId: number | null content: string @@ -25,29 +27,34 @@ export type Post = { deletedAt: number | null isPrivate: number } -export type Fallacy = { + +type Fallacy = { postId: number detection: string } -export type PostStats = { + +type PostStats = { postId: number replies: number } -export type Session = { + +type Session = { id: string expirationDate: number createdAt: Generated updatedAt: number userId: string } -export type User = { + +type User = { id: string email: string username: string createdAt: Generated isAdmin: number } -export type Verification = { + +type Verification = { id: string createdAt: Generated /** @@ -84,7 +91,7 @@ export type Verification = { expiresAt: number | null } -export type VoteEvent = { +type VoteEvent = { voteEventId: Generated voteEventTime: Generated userId: string @@ -93,7 +100,7 @@ export type VoteEvent = { vote: number } -export type Score = { +type Score = { voteEventId: number voteEventTime: number postId: number @@ -104,7 +111,7 @@ export type Score = { score: number } -export type Effect = { +type Effect = { postId: number commentId: number | null p: number @@ -117,49 +124,49 @@ export type Effect = { weight: number } -export type EffectWithDefault = Effect +type EffectWithDefault = Effect -export type EffectEvent = Effect & { +type EffectEvent = Effect & { voteEventId: number voteEventTime: number } -export type FullScore = Score & +type FullScore = Score & Effect & { criticalThreadId: number | null } -export type Lineage = { +type Lineage = { ancestorId: number descendantId: number separation: number } -export type HNItem = { +type HNItem = { hnId: number postId: number } -export type Poll = { +type Poll = { claimId: number postId: number pollType: string } -export type Artefact = { +type Artefact = { id: Generated url: string createdAt: Generated } -export type Quote = { +type Quote = { id: Generated artefactId: number quote: string createdAt: Generated } -export type Claim = { +type Claim = { id: Generated quoteId: number claim: string @@ -167,7 +174,7 @@ export type Claim = { createdAt: Generated } -export type QuoteFallacy = { +type QuoteFallacy = { id: Generated quoteId: number name: string @@ -176,12 +183,12 @@ export type QuoteFallacy = { createdAt: Generated } -export type Tag = { +type Tag = { id: Generated tag: string } -export type PostTag = { +type PostTag = { postId: number tagId: number } @@ -213,3 +220,18 @@ export type DB = { Tag: Tag PostTag: PostTag } + +export type DBUser = Selectable +export type DBPost = Selectable +export type DBPassword = Selectable +export type DBPostStats = Selectable +export type DBVerification = Selectable +export type DBScore = Selectable +export type DBFullScore = Selectable +export type DBLineage = Selectable +export type DBEffect = Selectable +export type DBInsertableScore = Insertable +export type DBVoteEvent = Selectable +export type DBVote = Selectable +export type DBInsertableVoteEvent = Insertable +export type DBHNItem = Selectable diff --git a/app/modules/auth/auth-types.ts b/app/modules/auth/auth-types.ts new file mode 100644 index 00000000..adfd2280 --- /dev/null +++ b/app/modules/auth/auth-types.ts @@ -0,0 +1,7 @@ +export type User = { + id: string + username: string + isAdmin: number +} + +// TODO: migrate to keratin authn -> create authn integration in this module diff --git a/app/modules/claims/artefact-repository.ts b/app/modules/claims/artefact-repository.ts index 53a0f805..5441df8d 100644 --- a/app/modules/claims/artefact-repository.ts +++ b/app/modules/claims/artefact-repository.ts @@ -1,5 +1,5 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { type Artefact } from './claim-types.ts' export async function getOrCreateArtefact( diff --git a/app/modules/claim-extraction/claim-extraction-client.ts b/app/modules/claims/claim-extraction-client.ts similarity index 100% rename from app/modules/claim-extraction/claim-extraction-client.ts rename to app/modules/claims/claim-extraction-client.ts diff --git a/app/modules/claims/claim-repository.ts b/app/modules/claims/claim-repository.ts index bcc85f17..1c9f23c3 100644 --- a/app/modules/claims/claim-repository.ts +++ b/app/modules/claims/claim-repository.ts @@ -1,23 +1,7 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { type Claim } from './claim-types.ts' -// export async function createClaim( -// trx: Transaction, -// claim: string, -// ): Promise { -// const createdClaim = await trx -// .insertInto('Claim') -// .values({ claim }) -// .returningAll() -// .executeTakeFirstOrThrow() - -// return { -// id: createdClaim.id, -// claim: createdClaim.claim, -// } -// } - export async function insertClaim( trx: Transaction, quoteId: number, diff --git a/app/modules/claims/claim-service.ts b/app/modules/claims/claim-service.ts index 11d1b75b..a238b2ae 100644 --- a/app/modules/claims/claim-service.ts +++ b/app/modules/claims/claim-service.ts @@ -1,12 +1,12 @@ import { type Transaction } from 'kysely' import { MAX_CHARS_PER_QUOTE } from '#app/constants.ts' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { invariant } from '#app/utils/misc.tsx' import { extractTweetTextGraphQL } from '#app/utils/tweet_extraction.server.ts' import { isValidTweetUrl, parseTweetURL } from '#app/utils/twitter-utils.ts' -import { extractClaims } from '../claim-extraction/claim-extraction-client.ts' import { fallacyDetection } from '../fallacies/fallacy-detection-client.ts' import { getArtefact, getOrCreateArtefact } from './artefact-repository.ts' +import { extractClaims } from './claim-extraction-client.ts' import { getClaims, insertClaim } from './claim-repository.ts' import { type Claim, diff --git a/app/modules/claims/claim-types.ts b/app/modules/claims/claim-types.ts index 3084ecd5..77e4caf9 100644 --- a/app/modules/claims/claim-types.ts +++ b/app/modules/claims/claim-types.ts @@ -29,6 +29,6 @@ export type QuoteFallacy = { } export type ClaimContext = { - artefact: Artefact - quote: Quote + artefact: Artefact | null + quote: Quote | null } diff --git a/app/modules/claims/quote-fallacy-repository.ts b/app/modules/claims/quote-fallacy-repository.ts index 918a3636..c14040d4 100644 --- a/app/modules/claims/quote-fallacy-repository.ts +++ b/app/modules/claims/quote-fallacy-repository.ts @@ -1,7 +1,7 @@ import { type Transaction } from 'kysely' +import { type DB } from '#app/database/types.ts' import { fallacyDetection } from '#app/modules/fallacies/fallacy-detection-client.ts' import { type FallacyList } from '#app/modules/fallacies/fallacy-types.ts' -import { type DB } from '#app/types/kysely-types.ts' import { type QuoteFallacy } from './claim-types.ts' import { getQuote } from './quote-repository.ts' diff --git a/app/modules/claims/quote-repository.ts b/app/modules/claims/quote-repository.ts index 9cbc10ba..00e1ca8b 100644 --- a/app/modules/claims/quote-repository.ts +++ b/app/modules/claims/quote-repository.ts @@ -1,5 +1,5 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { type Quote } from './claim-types.ts' export async function insertQuote( diff --git a/app/modules/fallacies/fallacy-repository.ts b/app/modules/fallacies/fallacy-repository.ts index d5ded832..cc8f11e7 100644 --- a/app/modules/fallacies/fallacy-repository.ts +++ b/app/modules/fallacies/fallacy-repository.ts @@ -1,10 +1,10 @@ import { type Transaction, sql } from 'kysely' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' +import { type DB } from '#app/database/types.ts' import { type FallacyList, FallacyListSchema, } from '#app/modules/fallacies/fallacy-types.ts' -import { type DB } from '#app/types/kysely-types.ts' export async function storeFallacies( postId: number, diff --git a/app/modules/hacker-news/hacker-news-repository.ts b/app/modules/hacker-news/hacker-news-repository.ts index 85b898a7..97afe6e2 100644 --- a/app/modules/hacker-news/hacker-news-repository.ts +++ b/app/modules/hacker-news/hacker-news-repository.ts @@ -1,5 +1,5 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' export async function getPostIdForHNItem( trx: Transaction, diff --git a/app/modules/hacker-news/hacker-news-service.ts b/app/modules/hacker-news/hacker-news-service.ts index 678585ba..8d223b00 100644 --- a/app/modules/hacker-news/hacker-news-service.ts +++ b/app/modules/hacker-news/hacker-news-service.ts @@ -1,7 +1,7 @@ import { decode } from 'html-entities' import { type Transaction } from 'kysely' import TurndownService from 'turndown' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { invariant } from '#app/utils/misc.tsx' import { getRootPostId } from '../posts/post-repository.ts' import { createPost } from '../posts/post-service.ts' diff --git a/app/modules/posts/polls/poll-repository.ts b/app/modules/posts/polls/poll-repository.ts index 683dae29..d30fc6fd 100644 --- a/app/modules/posts/polls/poll-repository.ts +++ b/app/modules/posts/polls/poll-repository.ts @@ -1,24 +1,25 @@ import { sql, type Transaction } from 'kysely' +import { type DB } from '#app/database/types.ts' import { getArtefact } from '#app/modules/claims/artefact-repository.ts' import { getClaim, updatePostIdOnClaim, } from '#app/modules/claims/claim-repository.ts' +import { getClaimContextByPollPostId } from '#app/modules/claims/claim-service.ts' import { getQuote } from '#app/modules/claims/quote-repository.ts' import { getDescendantCount, getPost, } from '#app/modules/posts/post-repository.ts' import { createPost } from '#app/modules/posts/post-service.ts' -import { type DB } from '#app/types/kysely-types.ts' -import { type Poll, type PollType, type Post } from '../post-types.ts' +import { type Poll, type FrontPagePoll, type PollType } from '../post-types.ts' export async function getOrCreatePoll( trx: Transaction, userId: string, claimId: number, pollType: PollType, -): Promise { +): Promise { const existingPoll = await trx .selectFrom('Post') .innerJoin('Poll', 'Poll.postId', 'Post.id') @@ -38,6 +39,7 @@ export async function getOrCreatePoll( deletedAt: existingPoll.deletedAt, isPrivate: existingPoll.isPrivate, pollType: existingPoll.pollType as PollType, + context: await getClaimContextByPollPostId(trx, existingPoll.id), } } @@ -60,13 +62,42 @@ export async function getOrCreatePoll( }) .execute() - return await getPost(trx, postId) + const persistedPost = await getPost(trx, postId) + return { + id: persistedPost.id, + parentId: persistedPost.parentId, + content: persistedPost.content, + createdAt: persistedPost.createdAt, + deletedAt: persistedPost.deletedAt, + isPrivate: persistedPost.isPrivate, + pollType: pollType, + context: await getClaimContextByPollPostId(trx, persistedPost.id), + } } -export async function getPollPost( +export async function getPollByPostId( trx: Transaction, - postId: number, + pollPostId: number, ): Promise { + const result = await trx + .selectFrom('Post') + .innerJoin('Poll', 'Poll.postId', 'Post.id') + .where('Post.id', '=', pollPostId) + .selectAll('Post') + .select('Poll.pollType as pollType') + .executeTakeFirstOrThrow() + + return { + ...result, + pollType: result.pollType as PollType, + context: await getClaimContextByPollPostId(trx, pollPostId), + } +} + +export async function getFrontPagePoll( + trx: Transaction, + postId: number, +): Promise { // TODO: check whether the post is actually a poll let query = trx @@ -88,24 +119,24 @@ export async function getPollPost( .select(sql`replies`.as('nReplies')) .orderBy('Post.createdAt', 'desc') - const post = await query.executeTakeFirstOrThrow() + const result = await query.executeTakeFirstOrThrow() return { - id: post.id, - parentId: post.parentId, - content: post.content, - createdAt: post.createdAt, - deletedAt: post.deletedAt, - isPrivate: post.isPrivate, - pollType: post.pollType ? (post.pollType as PollType) : null, - context: post.artefactId - ? { - artefact: await getArtefact(trx, post.artefactId), - quote: post.quoteId ? await getQuote(trx, post.quoteId) : null, - } - : null, - oSize: post.oSize, - nTransitiveComments: await getDescendantCount(trx, post.id), - p: post.p, + id: result.id, + parentId: result.parentId, + content: result.content, + createdAt: result.createdAt, + deletedAt: result.deletedAt, + isPrivate: result.isPrivate, + pollType: result.pollType as PollType, + context: { + artefact: result.artefactId + ? await getArtefact(trx, result.artefactId) + : null, + quote: result.quoteId ? await getQuote(trx, result.quoteId) : null, + }, + oSize: result.oSize, + nTransitiveComments: await getDescendantCount(trx, result.id), + p: result.p, } } diff --git a/app/modules/posts/post-repository.ts b/app/modules/posts/post-repository.ts index 49beef4b..f1e70c4c 100644 --- a/app/modules/posts/post-repository.ts +++ b/app/modules/posts/post-repository.ts @@ -1,14 +1,9 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { checkIsAdminOrThrow } from '#app/utils/auth.server.ts' import { invariant } from '#app/utils/misc.tsx' import { initPostStats } from './post-service.ts' -import { - type PollType, - type Post, - type PostWithScore, - type StatsPost, -} from './post-types.ts' +import { type Post, type StatsPost } from './post-types.ts' export async function insertPost( trx: Transaction, @@ -39,37 +34,11 @@ export async function getPost( trx: Transaction, postId: number, ): Promise { - const result = await trx + return await trx .selectFrom('Post') - .leftJoin('Poll', 'Poll.postId', 'Post.id') .where('Post.id', '=', postId) .selectAll('Post') - .select(['pollType']) .executeTakeFirstOrThrow() - - return { - ...result, - pollType: result.pollType ? (result.pollType as PollType) : null, - } -} - -export async function getPostWithScore( - trx: Transaction, - postId: number, -): Promise { - const scoredPost = await trx - .selectFrom('Post') - .innerJoin('FullScore', 'FullScore.postId', 'Post.id') - .leftJoin('Poll', 'Poll.postId', 'Post.id') - .where('Post.id', '=', postId) - .selectAll('Post') - .select(['pollType', 'oSize', 'score']) - .executeTakeFirstOrThrow() - - return { - ...scoredPost, - pollType: scoredPost.pollType ? (scoredPost.pollType as PollType) : null, - } } export async function incrementReplyCount( @@ -105,10 +74,7 @@ export async function getStatsPost( throw new Error(`Failed to read scored post postId=${postId}`) } - return { - ...scoredPost, - pollType: scoredPost.pollType ? (scoredPost.pollType as PollType) : null, - } + return scoredPost } export async function getReplyIds( @@ -191,9 +157,7 @@ export async function getTransitiveParents( ), ) .selectFrom('transitive_parents') - .leftJoin('Poll', 'Poll.postId', 'transitive_parents.id') - .selectAll('transitive_parents') - .selectAll('Poll') + .selectAll() .execute() // the topmost parent is the first element in the array @@ -208,7 +172,6 @@ export async function getTransitiveParents( createdAt: post.createdAt, deletedAt: post.deletedAt, isPrivate: post.isPrivate, - pollType: post.pollType ? (post.pollType as PollType) : null, } }) diff --git a/app/modules/posts/post-service.ts b/app/modules/posts/post-service.ts index 4d2492a2..843ad706 100644 --- a/app/modules/posts/post-service.ts +++ b/app/modules/posts/post-service.ts @@ -1,19 +1,23 @@ import { type Transaction } from 'kysely' import { MAX_CHARS_PER_POST } from '#app/constants.ts' -import { Direction } from '#app/types/api-types.ts' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { invariant } from '#app/utils/misc.tsx' -import { getFallacies } from '../fallacies/fallacy-repository.ts' import { insertPostTag, insertTag } from '../tags/tag-repository.ts' import { extractTags } from '../tags/tagger-client.ts' -import { getPollPost } from './polls/poll-repository.ts' +import { getFrontPagePoll } from './polls/poll-repository.ts' import { getDescendantCount, getPost, incrementReplyCount, insertPost, } from './post-repository.ts' -import { type FrontPagePost, type Poll, type PollType } from './post-types.ts' +import { + VoteDirection, + type FrontPagePost, + type FrontPagePoll, + type Post, + type Poll, +} from './post-types.ts' import { vote } from './scoring/vote-service.ts' export async function createPost( @@ -42,7 +46,7 @@ export async function createPost( } if (options?.withUpvote !== undefined ? options.withUpvote : true) { - await vote(trx, authorId, persistedPost.id, Direction.Up) + await vote(trx, authorId, persistedPost.id, VoteDirection.Up) } if (parentId !== null) { @@ -68,7 +72,7 @@ export async function getPostsAndPollsByTagId( tagId: number, ): Promise<{ posts: FrontPagePost[] - polls: Poll[] + polls: FrontPagePoll[] }> { const results = await trx .selectFrom('Post') @@ -98,8 +102,6 @@ export async function getPostsAndPollsByTagId( createdAt: row.createdAt, deletedAt: row.deletedAt, isPrivate: row.isPrivate, - pollType: row.pollType as PollType, - fallacyList: await getFallacies(trx, row.id), oSize: stats.oSize, nTransitiveComments: await getDescendantCount(trx, row.id), p: stats.p, @@ -111,7 +113,7 @@ export async function getPostsAndPollsByTagId( results .filter(row => row.pollType !== null) .map(async row => { - return await getPollPost(trx, row.id) + return await getFrontPagePoll(trx, row.id) }), ) @@ -131,3 +133,7 @@ export async function tagPost(trx: Transaction, postId: number) { persistedTags.map(async tag => await insertPostTag(trx, post.id, tag.id)), ) } + +export function postIsPoll(post: Post | Poll): post is Poll { + return (post as Poll).pollType !== undefined +} diff --git a/app/modules/posts/post-types.ts b/app/modules/posts/post-types.ts index fe6c17b6..752c6061 100644 --- a/app/modules/posts/post-types.ts +++ b/app/modules/posts/post-types.ts @@ -1,5 +1,4 @@ -import { type Artefact, type Quote } from '../claims/claim-types.ts' -import { type FallacyList } from '../fallacies/fallacy-types.ts' +import { type ClaimContext } from '../claims/claim-types.ts' export type Post = { id: number @@ -8,26 +7,22 @@ export type Post = { createdAt: number deletedAt: number | null isPrivate: number - pollType: PollType | null } -export type PostWithScore = Post & { score: number } +export type Poll = Post & { + pollType: PollType + context: ClaimContext +} export type FrontPagePost = Post & { - fallacyList: FallacyList oSize: number nTransitiveComments: number p: number } -export type Poll = Post & { - context: { - artefact: Artefact - quote: Quote | null - } | null - oSize: number - nTransitiveComments: number - p: number +export type FrontPagePoll = FrontPagePost & { + pollType: PollType + context: ClaimContext } export type StatsPost = Post & { @@ -56,3 +51,28 @@ export enum PollType { FactCheck = 'factCheck', Opinion = 'opinion', } + +export type Effect = { + postId: number + commentId: number | null + p: number + pCount: number + pSize: number + q: number + qCount: number + qSize: number + r: number + weight: number +} + +export enum VoteDirection { + Up = 1, + Down = -1, + Neutral = 0, +} + +export type VoteState = { + postId: number + vote: VoteDirection + isInformed: boolean +} diff --git a/app/modules/posts/scoring/ranking-service.ts b/app/modules/posts/ranking/ranking-service.ts similarity index 75% rename from app/modules/posts/scoring/ranking-service.ts rename to app/modules/posts/ranking/ranking-service.ts index a308d83e..b4335056 100644 --- a/app/modules/posts/scoring/ranking-service.ts +++ b/app/modules/posts/ranking/ranking-service.ts @@ -1,6 +1,7 @@ -import * as Immutable from 'immutable' +import Immutable from 'immutable' import { type Transaction, sql } from 'kysely' import { MAX_POSTS_PER_PAGE } from '#app/constants.ts' +import { type DB } from '#app/database/types.ts' import { getArtefact } from '#app/modules/claims/artefact-repository.ts' import { getQuote } from '#app/modules/claims/quote-repository.ts' import { getFallacies } from '#app/modules/fallacies/fallacy-repository.ts' @@ -8,45 +9,25 @@ import { getDescendantCount, getDescendantIds, getPost, - getPostWithScore, getReplyIds, } from '#app/modules/posts/post-repository.ts' +import { invariant } from '#app/utils/misc.tsx' +import { getPollByPostId } from '../polls/poll-repository.ts' import { type VoteState, - type ReplyTree, - type ImmutableReplyTree, + type FrontPagePost, + type FrontPagePoll, + type PollType, +} from '../post-types.ts' +import { getEffect } from '../scoring/effect-repository.ts' +import { effectSizeOnTarget } from '../scoring/scoring-utils.ts' +import { getUserVotes } from '../scoring/vote-repository.ts' +import { defaultVoteState } from '../scoring/vote-service.ts' +import { type CommentTreeState, -} from '#app/types/api-types.ts' -import { type DB } from '#app/types/kysely-types.ts' -import { invariant } from '#app/utils/misc.tsx' -import { type FrontPagePost, type Poll, type PollType } from '../post-types.ts' -import { getEffect } from './effect-repository.ts' -import { effectSizeOnTarget } from './scoring-utils.ts' -import { getUserVotes } from './vote-repository.ts' -import { defaultVoteState } from './vote-service.ts' - -export function toImmutableReplyTree(replyTree: ReplyTree): ImmutableReplyTree { - return { - ...replyTree, - replies: Immutable.List(replyTree.replies.map(toImmutableReplyTree)), - } -} - -export function addReplyToReplyTree( - tree: ImmutableReplyTree, - reply: ImmutableReplyTree, -): ImmutableReplyTree { - if (reply.post.parentId == tree.post.id) { - return { - ...tree, - replies: tree.replies.insert(0, reply), - } - } - return { - ...tree, - replies: tree.replies.map(child => addReplyToReplyTree(child, reply)), - } -} + type ReplyTree, + type MutableReplyTree, +} from './ranking-types.ts' export async function getCommentTreeState( trx: Transaction, @@ -65,7 +46,6 @@ export async function getCommentTreeState( 'FullScore.p as p', 'FullScore.oSize as voteCount', 'Post.deletedAt as deletedAt', - 'FullScore.criticalThreadId', ]) .where('Post.id', 'in', descendantIds.concat([targetPostId])) .where('p', 'is not', null) @@ -75,29 +55,14 @@ export async function getCommentTreeState( ? await getUserVotes(trx, userId, descendantIds.concat([targetPostId])) : undefined - const criticalCommentIdToTargetId: { [key: number]: number[] } = {} - results.forEach(res => { - const criticalThreadId = res.criticalThreadId - if (criticalThreadId !== null) { - const entry = criticalCommentIdToTargetId[criticalThreadId] - if (entry !== undefined) { - entry.push(res.postId) - } else { - criticalCommentIdToTargetId[criticalThreadId] = [res.postId] - } - } - }) - let commentTreeState: CommentTreeState = { targetPostId, - criticalCommentIdToTargetId, posts: {}, } await Promise.all( results.map(async result => { commentTreeState.posts[result.postId] = { - criticalCommentId: result.criticalThreadId, // We have to use the non-null assertion here because kysely doesn't // return values as non-null type even if we filter nulls with a where // condition. We can, however, be sure that the values are never null. @@ -119,9 +84,7 @@ export async function getCommentTreeState( return commentTreeState } -export function getAllPostIdsInTree( - tree: ImmutableReplyTree, -): Immutable.List { +export function getAllPostIdsInTree(tree: ReplyTree): Immutable.List { if (tree.replies.size === 0) { return Immutable.List([tree.post.id]) } @@ -131,22 +94,36 @@ export function getAllPostIdsInTree( ) } -export async function getReplyTree( +export async function getMutableReplyTree( trx: Transaction, postId: number, userId: string | null, commentTreeState: CommentTreeState, -): Promise { +): Promise { const directReplyIds = await getReplyIds(trx, postId) - const post = await getPostWithScore(trx, postId) + const isPoll = await trx + .selectFrom('Poll') + .where('Poll.postId', '=', postId) + .selectAll() + .execute() + .then(res => res.length !== 0) + const post = isPoll + ? await getPollByPostId(trx, postId) + : await getPost(trx, postId) + const score: number = await trx + .selectFrom('FullScore') + .where('FullScore.postId', '=', postId) + .select('score') + .executeTakeFirstOrThrow() + .then(row => row.score) - const replies: ReplyTree[] = await Promise.all( + const replies: MutableReplyTree[] = await Promise.all( // Recursively get all subtrees. // Stopping criterion: Once we reach a leaf node, its replies will be an // empty array, so the Promise.all will resolve immediately. directReplyIds.map( async replyId => - await getReplyTree(trx, replyId, userId, commentTreeState), + await getMutableReplyTree(trx, replyId, userId, commentTreeState), ), ) @@ -186,12 +163,13 @@ export async function getReplyTree( const effectSizeA = effectSizeOnTarget(effectA) const effectSizeB = effectSizeOnTarget(effectB) - const tieBreaker = b.post.score - a.post.score + const tieBreaker = b.score - a.score return effectSizeB - effectSizeA || tieBreaker }) return { post: post, + score: score, fallacyList: await getFallacies(trx, postId), replies: replies, } @@ -230,7 +208,6 @@ export async function getChronologicalToplevelPosts( isPrivate: post.isPrivate, pollType: post.pollType ? (post.pollType as PollType) : null, parent: post.parentId ? await getPost(trx, post.parentId) : null, - fallacyList: await getFallacies(trx, post.id), oSize: post.oSize, nTransitiveComments: await getDescendantCount(trx, post.id), p: post.p, @@ -243,7 +220,7 @@ export async function getChronologicalToplevelPosts( export async function getChronologicalPolls( trx: Transaction, -): Promise { +): Promise { let query = trx .selectFrom('Post') .where('Post.parentId', 'is', null) @@ -274,13 +251,13 @@ export async function getChronologicalPolls( createdAt: post.createdAt, deletedAt: post.deletedAt, isPrivate: post.isPrivate, - pollType: post.pollType ? (post.pollType as PollType) : null, - context: post.artefactId - ? { - artefact: await getArtefact(trx, post.artefactId), - quote: post.quoteId ? await getQuote(trx, post.quoteId) : null, - } - : null, + pollType: post.pollType as PollType, + context: { + artefact: post.artefactId + ? await getArtefact(trx, post.artefactId) + : null, + quote: post.quoteId ? await getQuote(trx, post.quoteId) : null, + }, oSize: post.oSize, nTransitiveComments: await getDescendantCount(trx, post.id), p: post.p, diff --git a/app/modules/posts/ranking/ranking-types.ts b/app/modules/posts/ranking/ranking-types.ts new file mode 100644 index 00000000..fe4f90a5 --- /dev/null +++ b/app/modules/posts/ranking/ranking-types.ts @@ -0,0 +1,37 @@ +import type Immutable from 'immutable' +import { type FallacyList } from '#app/modules/fallacies/fallacy-types.ts' +import { + type Effect, + type VoteState, + type Post, + type Poll, +} from '#app/modules/posts/post-types.ts' + +export type MutableReplyTree = { + post: Post | Poll + score: number + fallacyList: FallacyList + replies: MutableReplyTree[] +} + +export type ReplyTree = { + post: Post | Poll + score: number + fallacyList: FallacyList + replies: Immutable.List +} + +export type PostState = { + voteState: VoteState + voteCount: number + p: number | null + effectOnTargetPost: Effect | null + isDeleted: boolean +} + +export type CommentTreeState = { + targetPostId: number + posts: { + [key: number]: PostState + } +} diff --git a/app/modules/posts/ranking/ranking-utils.ts b/app/modules/posts/ranking/ranking-utils.ts new file mode 100644 index 00000000..16801dfd --- /dev/null +++ b/app/modules/posts/ranking/ranking-utils.ts @@ -0,0 +1,25 @@ +import Immutable from 'immutable' +import { type ReplyTree, type MutableReplyTree } from './ranking-types.ts' + +export function toImmutableReplyTree(replyTree: MutableReplyTree): ReplyTree { + return { + ...replyTree, + replies: Immutable.List(replyTree.replies.map(toImmutableReplyTree)), + } +} + +export function addReplyToReplyTree( + tree: ReplyTree, + reply: ReplyTree, +): ReplyTree { + if (reply.post.parentId == tree.post.id) { + return { + ...tree, + replies: tree.replies.insert(0, reply), + } + } + return { + ...tree, + replies: tree.replies.map(child => addReplyToReplyTree(child, reply)), + } +} diff --git a/app/modules/posts/scoring/effect-repository.ts b/app/modules/posts/scoring/effect-repository.ts index 05d7376b..4a2fbf6f 100644 --- a/app/modules/posts/scoring/effect-repository.ts +++ b/app/modules/posts/scoring/effect-repository.ts @@ -1,6 +1,5 @@ import { type Transaction } from 'kysely' -import { type DBEffect } from '#app/types/db-types.ts' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB, type DBEffect } from '#app/database/types.ts' import { snakeToCamelCaseObject } from './scoring-utils.ts' export async function insertEffectEvent(trx: Transaction, data: any) { diff --git a/app/modules/posts/scoring/global-brain-service.ts b/app/modules/posts/scoring/global-brain-service.ts index 9f7608fc..e2aa0e54 100644 --- a/app/modules/posts/scoring/global-brain-service.ts +++ b/app/modules/posts/scoring/global-brain-service.ts @@ -1,7 +1,6 @@ import global_brain from '@socialprotocols/globalbrain-node' import { type Transaction } from 'kysely' -import { type DBVoteEvent } from '#app/types/db-types.ts' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB, type DBVoteEvent } from '#app/database/types.ts' import { insertEffectEvent } from './effect-repository.ts' import { insertScoreEvent } from './score-repository.ts' import { camelToSnakeCase } from './scoring-utils.ts' diff --git a/app/modules/posts/scoring/score-repository.ts b/app/modules/posts/scoring/score-repository.ts index 0c8a497f..079a14df 100644 --- a/app/modules/posts/scoring/score-repository.ts +++ b/app/modules/posts/scoring/score-repository.ts @@ -1,5 +1,5 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { snakeToCamelCaseObject } from './scoring-utils.ts' export async function insertScoreEvent(trx: Transaction, data: any) { diff --git a/app/modules/posts/scoring/scoring-utils.ts b/app/modules/posts/scoring/scoring-utils.ts index 35f0b197..557c0b88 100644 --- a/app/modules/posts/scoring/scoring-utils.ts +++ b/app/modules/posts/scoring/scoring-utils.ts @@ -1,4 +1,4 @@ -import { type Effect } from '#app/types/api-types.ts' +import { type Effect } from '../post-types.ts' export function snakeToCamelCase(str: string): string { return str.replace(/([-_][a-z])/g, group => diff --git a/app/modules/posts/scoring/vote-repository.ts b/app/modules/posts/scoring/vote-repository.ts index ed54234a..7dce2d22 100644 --- a/app/modules/posts/scoring/vote-repository.ts +++ b/app/modules/posts/scoring/vote-repository.ts @@ -1,17 +1,17 @@ import { sql, type Transaction } from 'kysely' -import { type Direction, type VoteState } from '#app/types/api-types.ts' import { + type DB, type DBInsertableVoteEvent, type DBVoteEvent, -} from '#app/types/db-types.ts' -import { type DB } from '#app/types/kysely-types.ts' +} from '#app/database/types.ts' import { invariant } from '#app/utils/misc.tsx' +import { type VoteDirection, type VoteState } from '../post-types.ts' export async function insertVoteEvent( trx: Transaction, userId: string, postId: number, - vote: Direction, + vote: VoteDirection, ): Promise { const voteInt = vote as number diff --git a/app/modules/posts/scoring/vote-service.ts b/app/modules/posts/scoring/vote-service.ts index ef1a9138..653d5e5e 100644 --- a/app/modules/posts/scoring/vote-service.ts +++ b/app/modules/posts/scoring/vote-service.ts @@ -1,13 +1,12 @@ import { type Transaction } from 'kysely' -import { Direction, type VoteState } from '#app/types/api-types.ts' -import { type DBVoteEvent } from '#app/types/db-types.ts' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB, type DBVoteEvent } from '#app/database/types.ts' +import { VoteDirection, type VoteState } from '../post-types.ts' import { sendVoteEvent } from './global-brain-service.ts' import { insertVoteEvent } from './vote-repository.ts' export function defaultVoteState(postId: number): VoteState { return { - vote: Direction.Neutral, + vote: VoteDirection.Neutral, postId: postId, isInformed: false, } @@ -18,7 +17,7 @@ export async function vote( trx: Transaction, userId: string, postId: number, - direction: Direction, + direction: VoteDirection, ): Promise { let voteEvent: DBVoteEvent = await insertVoteEvent( trx, diff --git a/app/modules/tags/tag-repository.ts b/app/modules/tags/tag-repository.ts index eafa13e4..cba5a297 100644 --- a/app/modules/tags/tag-repository.ts +++ b/app/modules/tags/tag-repository.ts @@ -1,5 +1,5 @@ import { type Transaction } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' +import { type DB } from '#app/database/types.ts' import { type Tag } from './tag-types.ts' export async function insertTag( diff --git a/app/root.tsx b/app/root.tsx index e03302ac..45c0b5bd 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -28,7 +28,7 @@ import { AuthenticityTokenProvider } from 'remix-utils/csrf/react' import { ExternalScripts } from 'remix-utils/external-scripts' import { HoneypotProvider } from 'remix-utils/honeypot/react' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { GeneralErrorBoundary } from './components/error-boundary.tsx' import { ErrorList } from './components/forms.tsx' import { EpicProgress } from './components/progress-bar.tsx' @@ -41,9 +41,9 @@ import { DropdownMenuTrigger, } from './components/ui/dropdown-menu.tsx' import { href as iconsHref, Icon } from './components/ui/icon.tsx' +import { type User } from './modules/auth/auth-types.ts' import { SITE_NAME } from './site.ts' import tailwindStyleSheetUrl from './styles/tailwind.css' -import { type User } from './types/api-types.ts' import { getUserId, logout } from './utils/auth.server.ts' import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx' import { csrf } from './utils/csrf.server.ts' diff --git a/app/routes/_actions+/create-poll.tsx b/app/routes/_actions+/create-poll.tsx index e13cc896..0315baf6 100644 --- a/app/routes/_actions+/create-poll.tsx +++ b/app/routes/_actions+/create-poll.tsx @@ -1,6 +1,6 @@ import { type ActionFunctionArgs } from '@remix-run/node' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getOrCreatePoll } from '#app/modules/posts/polls/poll-repository.ts' import { PollType } from '#app/modules/posts/post-types.ts' import { requireUserId } from '#app/utils/auth.server.ts' diff --git a/app/routes/_actions+/create-post.tsx b/app/routes/_actions+/create-post.tsx index bcceeaa1..8ba8da31 100644 --- a/app/routes/_actions+/create-post.tsx +++ b/app/routes/_actions+/create-post.tsx @@ -2,7 +2,7 @@ import { type ActionFunctionArgs } from '@remix-run/node' import { redirect } from '@remix-run/server-runtime' import { z } from 'zod' import { zfd } from 'zod-form-data' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { fallacyDetection } from '#app/modules/fallacies/fallacy-detection-client.ts' import { storeFallacies } from '#app/modules/fallacies/fallacy-repository.ts' import { createPost } from '#app/modules/posts/post-service.ts' diff --git a/app/routes/_actions+/delete-post.tsx b/app/routes/_actions+/delete-post.tsx index 991c4dae..36aefd10 100644 --- a/app/routes/_actions+/delete-post.tsx +++ b/app/routes/_actions+/delete-post.tsx @@ -1,8 +1,8 @@ import { type ActionFunctionArgs } from '@remix-run/node' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { setDeletedAt } from '#app/modules/posts/post-repository.ts' -import { getCommentTreeState } from '#app/modules/posts/scoring/ranking-service.ts' +import { getCommentTreeState } from '#app/modules/posts/ranking/ranking-service.ts' import { getUserId } from '#app/utils/auth.server.ts' import { invariant } from '#app/utils/misc.tsx' diff --git a/app/routes/_actions+/refresh.$postId.tsx b/app/routes/_actions+/refresh.$postId.tsx index dba65c20..818afdf5 100644 --- a/app/routes/_actions+/refresh.$postId.tsx +++ b/app/routes/_actions+/refresh.$postId.tsx @@ -1,7 +1,7 @@ import { redirect, type ActionFunctionArgs } from '@remix-run/node' import invariant from 'tiny-invariant' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { fallacyDetection } from '#app/modules/fallacies/fallacy-detection-client.ts' import { storeFallacies } from '#app/modules/fallacies/fallacy-repository.ts' import { getPost } from '#app/modules/posts/post-repository.ts' diff --git a/app/routes/_actions+/reply.tsx b/app/routes/_actions+/reply.tsx index 409dd6ea..5c2dcc76 100644 --- a/app/routes/_actions+/reply.tsx +++ b/app/routes/_actions+/reply.tsx @@ -1,14 +1,14 @@ import { type ActionFunctionArgs } from '@remix-run/node' import invariant from 'tiny-invariant' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { fallacyDetection } from '#app/modules/fallacies/fallacy-detection-client.ts' import { storeFallacies } from '#app/modules/fallacies/fallacy-repository.ts' import { createPost } from '#app/modules/posts/post-service.ts' import { getCommentTreeState, - getReplyTree, -} from '#app/modules/posts/scoring/ranking-service.ts' + getMutableReplyTree, +} from '#app/modules/posts/ranking/ranking-service.ts' import { requireUserId } from '#app/utils/auth.server.ts' type ReplyData = { @@ -62,7 +62,12 @@ export const action = async (args: ActionFunctionArgs) => { ) return { commentTreeState, - newReplyTree: await getReplyTree(trx, postId, userId, commentTreeState), + newReplyTree: await getMutableReplyTree( + trx, + postId, + userId, + commentTreeState, + ), } } else return {} }) diff --git a/app/routes/_actions+/submit-artefact.tsx b/app/routes/_actions+/submit-artefact.tsx index 435b729f..30bc58f9 100644 --- a/app/routes/_actions+/submit-artefact.tsx +++ b/app/routes/_actions+/submit-artefact.tsx @@ -1,6 +1,6 @@ import { type ActionFunctionArgs } from '@remix-run/node' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { submitArtefact } from '#app/modules/claims/claim-service.ts' const artefactDtoSchema = z.object({ diff --git a/app/routes/_actions+/submit-quote.tsx b/app/routes/_actions+/submit-quote.tsx index cdac88a3..8d7a1cb4 100644 --- a/app/routes/_actions+/submit-quote.tsx +++ b/app/routes/_actions+/submit-quote.tsx @@ -1,6 +1,6 @@ import { type ActionFunctionArgs } from '@remix-run/node' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { submitQuote } from '#app/modules/claims/claim-service.ts' const artefactDtoSchema = z.object({ diff --git a/app/routes/_actions+/vote.tsx b/app/routes/_actions+/vote.tsx index 5cc9dff7..028aa432 100644 --- a/app/routes/_actions+/vote.tsx +++ b/app/routes/_actions+/vote.tsx @@ -1,15 +1,16 @@ import { type ActionFunctionArgs } from '@remix-run/node' -import { db } from '#app/db.ts' -import { getCommentTreeState } from '#app/modules/posts/scoring/ranking-service.ts' +import { db } from '#app/database/db.ts' +import { VoteDirection } from '#app/modules/posts/post-types.ts' +import { getCommentTreeState } from '#app/modules/posts/ranking/ranking-service.ts' +import { type CommentTreeState } from '#app/modules/posts/ranking/ranking-types.ts' import { vote } from '#app/modules/posts/scoring/vote-service.ts' -import { type CommentTreeState, Direction } from '#app/types/api-types.ts' import { requireUserId } from '#app/utils/auth.server.ts' type VoteData = { postId: number focussedPostId: number - direction: Direction - currentVoteState: Direction + direction: VoteDirection + currentVoteState: VoteDirection } export const action = async (args: ActionFunctionArgs) => { @@ -20,7 +21,7 @@ export const action = async (args: ActionFunctionArgs) => { // Example: state is Up and we receive another Up means that we clear the vote. const newState = dataParsed.direction == dataParsed.currentVoteState - ? Direction.Neutral + ? VoteDirection.Neutral : dataParsed.direction const userId: string = await requireUserId(request) diff --git a/app/routes/_auth+/forgot-password.tsx b/app/routes/_auth+/forgot-password.tsx index 53ac4dae..e66eacd2 100644 --- a/app/routes/_auth+/forgot-password.tsx +++ b/app/routes/_auth+/forgot-password.tsx @@ -14,7 +14,7 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { SITE_NAME } from '#app/site.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' import { sendEmail } from '#app/utils/email.server.ts' diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index 50d4a751..f7df4673 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -15,7 +15,7 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' import { SITE_NAME } from '#app/site.ts' import { diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index 153c40ce..570da4f0 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -13,7 +13,7 @@ import { safeRedirect } from 'remix-utils/safe-redirect' import { z } from 'zod' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { SITE_NAME } from '#app/site.ts' import { requireAnonymous, sessionKey, signup } from '#app/utils/auth.server.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index c35ac3a0..690df681 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -11,7 +11,7 @@ import { Form, useActionData, useLoaderData } from '@remix-run/react' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { SITE_NAME } from '#app/site.ts' import { requireAnonymous, resetUserPassword } from '#app/utils/auth.server.ts' import { invariant, useIsPending } from '#app/utils/misc.tsx' diff --git a/app/routes/_auth+/signup.tsx b/app/routes/_auth+/signup.tsx index 821b55bd..4b96f4c4 100644 --- a/app/routes/_auth+/signup.tsx +++ b/app/routes/_auth+/signup.tsx @@ -14,7 +14,7 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { SITE_NAME } from '#app/site.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' import { sendEmail } from '#app/utils/email.server.ts' diff --git a/app/routes/_auth+/verify.tsx b/app/routes/_auth+/verify.tsx index df38c646..6ab8af7e 100644 --- a/app/routes/_auth+/verify.tsx +++ b/app/routes/_auth+/verify.tsx @@ -9,7 +9,7 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { handleVerification as handleChangeEmailVerification } from '#app/routes/settings+/profile.change-email.tsx' import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' import { type twoFAVerifyVerificationType } from '#app/routes/settings+/profile.two-factor.verify.tsx' diff --git a/app/routes/privacy.tsx b/app/routes/_legal+/privacy.tsx similarity index 100% rename from app/routes/privacy.tsx rename to app/routes/_legal+/privacy.tsx diff --git a/app/routes/tos.tsx b/app/routes/_legal+/tos.tsx similarity index 100% rename from app/routes/tos.tsx rename to app/routes/_legal+/tos.tsx diff --git a/app/routes/artefact+/$artefactId.tsx b/app/routes/artefact+/$artefactId.tsx index d528e381..05b08336 100644 --- a/app/routes/artefact+/$artefactId.tsx +++ b/app/routes/artefact+/$artefactId.tsx @@ -8,7 +8,7 @@ import { PostContent } from '#app/components/building-blocks/post-content.tsx' import { Markdown } from '#app/components/markdown.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { MAX_CHARS_PER_POST } from '#app/constants.ts' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getArtefact } from '#app/modules/claims/artefact-repository.ts' import { type Artefact, type Quote } from '#app/modules/claims/claim-types.ts' import { getQuotes } from '#app/modules/claims/quote-repository.ts' diff --git a/app/routes/discussions.tsx b/app/routes/discussions.tsx index a6acfe0b..265f0cce 100644 --- a/app/routes/discussions.tsx +++ b/app/routes/discussions.tsx @@ -1,31 +1,20 @@ -import { type LoaderFunctionArgs } from '@remix-run/node' import { Link, useLoaderData } from '@remix-run/react' -import moment from 'moment' -import { PostContent } from '#app/components/building-blocks/post-content.tsx' +import { OpenDiscussionPreview } from '#app/components/building-blocks/open-discussion-preview.tsx' import { Markdown } from '#app/components/markdown.tsx' -import { db } from '#app/db.ts' -import { type FrontPagePost } from '#app/modules/posts/post-types.ts' -import { getChronologicalToplevelPosts } from '#app/modules/posts/scoring/ranking-service.ts' -import { getUserId } from '#app/utils/auth.server.ts' +import { db } from '#app/database/db.ts' +import { getChronologicalToplevelPosts } from '#app/modules/posts/ranking/ranking-service.ts' -export async function loader({ request }: LoaderFunctionArgs) { - const userId: string | null = await getUserId(request) - const loggedIn = userId !== null +export async function loader() { const feed = await db.transaction().execute(async trx => { return await getChronologicalToplevelPosts(trx) }) - return { loggedIn, feed } + return { feed } } -export default function Explore() { - // due to the loader, this component will never be rendered, but we'll return - // the error boundary just in case. - let data = useLoaderData() - - return -} +export default function OpenDiscussions() { + let { feed } = useLoaderData() + const filteredFeed = feed.filter(post => !post.isPrivate) -export function FrontpageFeed({ feed }: { feed: FrontPagePost[] }) { return (
@@ -39,51 +28,11 @@ export function FrontpageFeed({ feed }: { feed: FrontPagePost[] }) { start a discussion
- -
- ) -} - -function PostList({ feed }: { feed: FrontPagePost[] }) { - const filteredFeed = feed.filter(post => !post.isPrivate) - return filteredFeed.map(post => { - return - }) -} - -export function TopLevelPost({ - post, - className, -}: { - post: FrontPagePost - className?: string -}) { - const ageString = moment(post.createdAt).fromNow() - const commentString = post.nTransitiveComments == 1 ? 'comment' : 'comments' - - return ( -
-
-
-
{ageString}
- -
- - {post.nTransitiveComments} {commentString} - -
-
-
+ {filteredFeed.map(post => { + return ( + + ) + })}
) } diff --git a/app/routes/hn.$hnId.tsx b/app/routes/hn.$hnId.tsx index 8af916b4..40a39ce4 100644 --- a/app/routes/hn.$hnId.tsx +++ b/app/routes/hn.$hnId.tsx @@ -1,7 +1,7 @@ import { type LoaderFunctionArgs } from '@remix-run/node' import { redirect } from '@remix-run/react' import { z } from 'zod' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { syncWithHN } from '#app/modules/hacker-news/hacker-news-service.ts' const hnIdSchema = z.coerce.number() diff --git a/app/routes/me.tsx b/app/routes/me.tsx index e1b9af49..e291bd7c 100644 --- a/app/routes/me.tsx +++ b/app/routes/me.tsx @@ -1,5 +1,5 @@ import { redirect, type LoaderFunctionArgs } from '@remix-run/node' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { logout, requireUserId } from '#app/utils/auth.server.ts' export async function loader({ request }: LoaderFunctionArgs) { diff --git a/app/routes/polls.tsx b/app/routes/polls.tsx index b035010f..91c1af06 100644 --- a/app/routes/polls.tsx +++ b/app/routes/polls.tsx @@ -1,8 +1,8 @@ import { Link, useLoaderData } from '@remix-run/react' import { PollPostPreview } from '#app/components/building-blocks/poll-post-preview.tsx' import { Markdown } from '#app/components/markdown.tsx' -import { db } from '#app/db.ts' -import { getChronologicalPolls } from '#app/modules/posts/scoring/ranking-service.ts' +import { db } from '#app/database/db.ts' +import { getChronologicalPolls } from '#app/modules/posts/ranking/ranking-service.ts' export async function loader() { const feed = await db.transaction().execute(async trx => { diff --git a/app/routes/post.$postId.tsx b/app/routes/post.$postId.tsx index 4c1e8496..10bc1d35 100644 --- a/app/routes/post.$postId.tsx +++ b/app/routes/post.$postId.tsx @@ -1,6 +1,6 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node' import { type MetaFunction, useLoaderData, useParams } from '@remix-run/react' -import * as Immutable from 'immutable' +import Immutable from 'immutable' import { type Dispatch, type SetStateAction, useState } from 'react' import { z } from 'zod' import { EmbeddedTweet } from '#app/components/building-blocks/embedded-integration.tsx' @@ -8,25 +8,30 @@ import { ParentThread } from '#app/components/building-blocks/parent-thread.tsx' import { PostWithReplies } from '#app/components/building-blocks/post-with-replies.tsx' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { Icon } from '#app/components/ui/icon.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getClaimContextByPollPostId } from '#app/modules/claims/claim-service.ts' -import { type ClaimContext } from '#app/modules/claims/claim-types.ts' +import { + type Artefact, + type Quote, + type ClaimContext, +} from '#app/modules/claims/claim-types.ts' import { updateHN } from '#app/modules/hacker-news/hacker-news-service.ts' import { getTransitiveParents } from '#app/modules/posts/post-repository.ts' -import { type Post } from '#app/modules/posts/post-types.ts' +import { postIsPoll } from '#app/modules/posts/post-service.ts' +import { VoteDirection, type Post } from '#app/modules/posts/post-types.ts' import { - addReplyToReplyTree, getCommentTreeState, - getReplyTree, - toImmutableReplyTree, -} from '#app/modules/posts/scoring/ranking-service.ts' + getMutableReplyTree, +} from '#app/modules/posts/ranking/ranking-service.ts' import { - Direction, - type ReplyTree, + type MutableReplyTree, type CommentTreeState, - type ImmutableReplyTree, - type CollapsedState, -} from '#app/types/api-types.ts' + type ReplyTree, +} from '#app/modules/posts/ranking/ranking-types.ts' +import { + addReplyToReplyTree, + toImmutableReplyTree, +} from '#app/modules/posts/ranking/ranking-utils.ts' import { getUserId } from '#app/utils/auth.server.ts' import { invariant } from '#app/utils/misc.tsx' import { isValidTweetUrl } from '#app/utils/twitter-utils.ts' @@ -43,25 +48,25 @@ export async function loader({ params, request }: LoaderFunctionArgs) { commentTreeState, pollContext, }: { - mutableReplyTree: ReplyTree + mutableReplyTree: MutableReplyTree transitiveParents: Post[] commentTreeState: CommentTreeState pollContext: ClaimContext | null } = await db.transaction().execute(async trx => { await updateHN(trx, postId) const commentTreeState = await getCommentTreeState(trx, postId, userId) - const mutableReplyTree = await getReplyTree( + const replyTree = await getMutableReplyTree( trx, postId, userId, commentTreeState, ) - const isPoll = mutableReplyTree.post.pollType !== null + const isPoll = postIsPoll(replyTree.post) const pollContext = isPoll ? await getClaimContextByPollPostId(trx, postId) : null return { - mutableReplyTree: mutableReplyTree, + mutableReplyTree: replyTree, transitiveParents: await getTransitiveParents(trx, postId), commentTreeState: commentTreeState, pollContext: pollContext, @@ -119,6 +124,11 @@ export default function PostPage() { const { mutableReplyTree, transitiveParents, commentTreeState, pollContext } = useLoaderData() + // We need to use MutableReplyTree because Immutable types are not preserved + // in serialization. However, we convert the mutable reply tree to an + // immutable one as early as possible in the frontend. + const replyTree = toImmutableReplyTree(mutableReplyTree) + const params = useParams() // subcomponent and key needed for react to not preserve state on page changes @@ -126,7 +136,7 @@ export default function PostPage() { <> void + onReplySubmit: (reply: ReplyTree) => void targetHasVote: boolean targetPostId: number commentTreeState: CommentTreeState @@ -148,19 +158,23 @@ export type TreeContext = { ) => void } +export type CollapsedState = { + currentlyFocussedPostId: number | null + hidePost: Immutable.Map + hideChildren: Immutable.Map +} + export function DiscussionView({ - mutableReplyTree, + initialReplyTree, transitiveParents, initialCommentTreeState, pollContext, }: { - mutableReplyTree: ReplyTree + initialReplyTree: ReplyTree transitiveParents: Post[] initialCommentTreeState: CommentTreeState pollContext: ClaimContext | null }) { - const initialReplyTree = toImmutableReplyTree(mutableReplyTree) - const [replyTreeState, setReplyTreeState] = useState(initialReplyTree) const [commentTreeState, setCommentTreeState] = useState( initialCommentTreeState, @@ -178,14 +192,12 @@ export function DiscussionView({ `post ${postId} not found in commentTreeState`, ) - function onReplySubmit(reply: ImmutableReplyTree) { - const newReplyTreeState = addReplyToReplyTree(replyTreeState, reply) - setReplyTreeState(newReplyTreeState) - } - const treeContext: TreeContext = { - onReplySubmit, - targetHasVote: postState.voteState.vote !== Direction.Neutral, + onReplySubmit: (reply: ReplyTree) => { + const newReplyTreeState = addReplyToReplyTree(replyTreeState, reply) + setReplyTreeState(newReplyTreeState) + }, + targetHasVote: postState.voteState.vote !== VoteDirection.Neutral, targetPostId: postId, commentTreeState: commentTreeState, setCommentTreeState: setCommentTreeState, @@ -212,47 +224,19 @@ export function DiscussionView({ }, } - const isTweet = pollContext - ? isValidTweetUrl(pollContext?.artefact.url) - : false - - const [showPollContext, setShowPollContext] = useState(false) - return ( <> - {pollContext && ( -
- - {showPollContext && ( -
- {isTweet ? ( - - ) : ( - <> - - {pollContext.quote.quote} - - )} -
- )} -
+ {pollContext && pollContext.artefact && pollContext.quote && ( + )} (false) + return ( +
+ + {showPollContext && ( +
+ {isTweet ? ( + + ) : ( + <> + + {quote.quote} + + )} +
+ )} +
+ ) +} + function collapseParentSiblingsAndIndirectChildren( pathFromFocussedPost: Immutable.List, collapsedState: CollapsedState, - replyTree: ImmutableReplyTree, + replyTree: ReplyTree, ): CollapsedState { // go down the tree along the path // and collapse all siblings on the way. diff --git a/app/routes/quote+/$quoteId.tsx b/app/routes/quote+/$quoteId.tsx index b9beb8e4..ecc39edd 100644 --- a/app/routes/quote+/$quoteId.tsx +++ b/app/routes/quote+/$quoteId.tsx @@ -13,7 +13,7 @@ import { TabsList, TabsTrigger, } from '#app/components/ui/tabs.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getArtefact } from '#app/modules/claims/artefact-repository.ts' import { getClaims } from '#app/modules/claims/claim-repository.ts' import { @@ -24,8 +24,8 @@ import { } from '#app/modules/claims/claim-types.ts' import { getQuoteFallacies } from '#app/modules/claims/quote-fallacy-repository.ts' import { getQuote } from '#app/modules/claims/quote-repository.ts' -import { getPollPost } from '#app/modules/posts/polls/poll-repository.ts' -import { type Poll, PollType } from '#app/modules/posts/post-types.ts' +import { getFrontPagePoll } from '#app/modules/posts/polls/poll-repository.ts' +import { type FrontPagePoll, PollType } from '#app/modules/posts/post-types.ts' import { isValidTweetUrl } from '#app/utils/twitter-utils.ts' import { useOptionalUser } from '#app/utils/user.ts' @@ -44,7 +44,7 @@ export async function loader({ params }: LoaderFunctionArgs) { artefact: Artefact quote: Quote claims: Claim[] - posts: Poll[] + posts: FrontPagePoll[] quoteFallacies: QuoteFallacy[] } = await db.transaction().execute(async trx => { const claims = await getClaims(trx, quoteId) @@ -53,7 +53,7 @@ export async function loader({ params }: LoaderFunctionArgs) { .map(claim => claim.postId!) // eslint-disable-line @typescript-eslint/no-non-null-assertion const posts = await Promise.all( submittedClaimsPostIds.map( - async postId => await getPollPost(trx, postId), + async postId => await getFrontPagePoll(trx, postId), ), ) const quoteFallacies = await getQuoteFallacies(trx, quoteId) @@ -301,7 +301,7 @@ function QuotePollPost({ post, className, }: { - post: Poll + post: FrontPagePoll className?: string }) { const ageString = moment(post.createdAt).fromNow() diff --git a/app/routes/resources+/healthcheck.tsx b/app/routes/resources+/healthcheck.tsx index 8cd3064a..36b2da8c 100644 --- a/app/routes/resources+/healthcheck.tsx +++ b/app/routes/resources+/healthcheck.tsx @@ -1,6 +1,6 @@ // learn more: https://fly.io/docs/reference/configuration/#services-http_checks import { type LoaderFunctionArgs } from '@remix-run/node' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' export async function loader({ request }: LoaderFunctionArgs) { const host = diff --git a/app/routes/settings+/profile.change-email.tsx b/app/routes/settings+/profile.change-email.tsx index e79a7f34..bdd20809 100644 --- a/app/routes/settings+/profile.change-email.tsx +++ b/app/routes/settings+/profile.change-email.tsx @@ -14,7 +14,7 @@ import { z } from 'zod' import { ErrorList, Field } from '#app/components/forms.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { prepareVerification, requireRecentVerification, diff --git a/app/routes/settings+/profile.index.tsx b/app/routes/settings+/profile.index.tsx index 0515d616..6881dc60 100644 --- a/app/routes/settings+/profile.index.tsx +++ b/app/routes/settings+/profile.index.tsx @@ -12,7 +12,7 @@ import { z } from 'zod' import { ErrorList, Field } from '#app/components/forms.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { requireUserId, sessionKey } from '#app/utils/auth.server.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' import { invariantResponse, useDoubleCheck } from '#app/utils/misc.tsx' diff --git a/app/routes/settings+/profile.password.tsx b/app/routes/settings+/profile.password.tsx index 20dcf17e..b888b805 100644 --- a/app/routes/settings+/profile.password.tsx +++ b/app/routes/settings+/profile.password.tsx @@ -14,7 +14,7 @@ import { ErrorList, Field } from '#app/components/forms.tsx' import { Button } from '#app/components/ui/button.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getPasswordHash, requireUserId, diff --git a/app/routes/settings+/profile.password_.create.tsx b/app/routes/settings+/profile.password_.create.tsx index ef80253d..09d56473 100644 --- a/app/routes/settings+/profile.password_.create.tsx +++ b/app/routes/settings+/profile.password_.create.tsx @@ -12,7 +12,7 @@ import { ErrorList, Field } from '#app/components/forms.tsx' import { Button } from '#app/components/ui/button.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getPasswordHash, requireUserId } from '#app/utils/auth.server.ts' import { useIsPending } from '#app/utils/misc.tsx' import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts' diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index 2485ed38..9351fbd0 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -3,7 +3,7 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node' import { Outlet } from '@remix-run/react' import { z } from 'zod' import { Icon } from '#app/components/ui/icon.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { requireUserId } from '#app/utils/auth.server.ts' import { invariantResponse } from '#app/utils/misc.tsx' diff --git a/app/routes/settings+/profile.two-factor.disable.tsx b/app/routes/settings+/profile.two-factor.disable.tsx index e6e33b05..07c8c49d 100644 --- a/app/routes/settings+/profile.two-factor.disable.tsx +++ b/app/routes/settings+/profile.two-factor.disable.tsx @@ -8,7 +8,7 @@ import { useFetcher } from '@remix-run/react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { requireRecentVerification } from '#app/routes/_auth+/verify.tsx' import { requireUserId } from '#app/utils/auth.server.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' diff --git a/app/routes/settings+/profile.two-factor.index.tsx b/app/routes/settings+/profile.two-factor.index.tsx index d1cbee9d..944f0889 100644 --- a/app/routes/settings+/profile.two-factor.index.tsx +++ b/app/routes/settings+/profile.two-factor.index.tsx @@ -10,7 +10,7 @@ import { Link, useFetcher, useLoaderData } from '@remix-run/react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { requireUserId } from '#app/utils/auth.server.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' import { generateTOTP } from '#app/utils/totp.server.ts' diff --git a/app/routes/settings+/profile.two-factor.verify.tsx b/app/routes/settings+/profile.two-factor.verify.tsx index 65b85aeb..0769542f 100644 --- a/app/routes/settings+/profile.two-factor.verify.tsx +++ b/app/routes/settings+/profile.two-factor.verify.tsx @@ -19,7 +19,7 @@ import { z } from 'zod' import { ErrorList, Field } from '#app/components/forms.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { isCodeValid } from '#app/routes/_auth+/verify.tsx' import { requireUserId } from '#app/utils/auth.server.ts' import { validateCSRF } from '#app/utils/csrf.server.ts' diff --git a/app/routes/stats.$postId.tsx b/app/routes/stats.$postId.tsx index 699dbb4e..44d696f7 100644 --- a/app/routes/stats.$postId.tsx +++ b/app/routes/stats.$postId.tsx @@ -4,12 +4,12 @@ import invariant from 'tiny-invariant' import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { Markdown } from '#app/components/markdown.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' +import { type DBEffect } from '#app/database/types.ts' import { getStatsPost } from '#app/modules/posts/post-repository.ts' import { type StatsPost } from '#app/modules/posts/post-types.ts' import { getEffects } from '#app/modules/posts/scoring/effect-repository.ts' import { relativeEntropy } from '#app/modules/posts/scoring/scoring-utils.ts' -import { type DBEffect } from '#app/types/db-types.ts' const postIdSchema = z.coerce.number() diff --git a/app/routes/tag.$tagId.tsx b/app/routes/tag.$tagId.tsx index 7e803c6d..d5788d73 100644 --- a/app/routes/tag.$tagId.tsx +++ b/app/routes/tag.$tagId.tsx @@ -1,6 +1,7 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' import { z } from 'zod' +import { OpenDiscussionPreview } from '#app/components/building-blocks/open-discussion-preview.tsx' import { PollPostPreview } from '#app/components/building-blocks/poll-post-preview.tsx' import { Markdown } from '#app/components/markdown.tsx' import { @@ -9,12 +10,14 @@ import { TabsList, TabsTrigger, } from '#app/components/ui/tabs.tsx' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { getPostsAndPollsByTagId } from '#app/modules/posts/post-service.ts' -import { type FrontPagePost, type Poll } from '#app/modules/posts/post-types.ts' +import { + type FrontPagePost, + type FrontPagePoll, +} from '#app/modules/posts/post-types.ts' import { getTagById } from '#app/modules/tags/tag-repository.ts' import { type Tag } from '#app/modules/tags/tag-types.ts' -import { TopLevelPost } from './discussions.tsx' const tagIdSchema = z.coerce.number() @@ -27,7 +30,7 @@ export async function loader({ params }: LoaderFunctionArgs) { }: { tag: Tag posts: FrontPagePost[] - polls: Poll[] + polls: FrontPagePoll[] } = await db.transaction().execute(async trx => { const tag = await getTagById(trx, tagId) const { posts, polls } = await getPostsAndPollsByTagId(trx, tag.id) @@ -76,7 +79,7 @@ export default function TagPage() {
{posts.map(post => { return ( - -} - -export type PostState = { - criticalCommentId: number | null - voteState: VoteState - voteCount: number - p: number | null - effectOnTargetPost: Effect | null - isDeleted: boolean -} - -export type CommentTreeState = { - targetPostId: number - criticalCommentIdToTargetId: { - [key: number]: number[] - } - posts: { - [key: number]: PostState - } -} - -export type CollapsedState = { - currentlyFocussedPostId: number | null - hidePost: Immutable.Map - hideChildren: Immutable.Map -} - -export enum Direction { - Up = 1, - Down = -1, - Neutral = 0, -} - -export type VoteState = { - postId: number - vote: Direction - isInformed: boolean -} diff --git a/app/types/db-types.ts b/app/types/db-types.ts deleted file mode 100644 index 0e4d49f3..00000000 --- a/app/types/db-types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type Selectable, type Insertable } from 'kysely' -import type * as schema from '#app/types/kysely-types.ts' - -// The types exported from kysely-types (which are generated from prisma.schema) are not used directly in app code. -// Instead, Kysely wants values of type Selectable, Insertable, etc. This is because what fields are -// required/optional depend on how the type is being used -- for example IDs are optional when inserting but required -// when selecting. Now, since it is the Selectable type, which has all fields that should exist in a DB record, that we -// will generally want to use in App code, for convenience we export all of these in this file. - -export type DBUser = Selectable -export type DBPost = Selectable -export type DBPassword = Selectable -export type DBPostStats = Selectable -export type DBVerification = Selectable -export type DBScore = Selectable -export type DBFullScore = Selectable -export type DBLineage = Selectable -export type DBEffect = Selectable -export type DBInsertableScore = Insertable -export type DBVoteEvent = Selectable -export type DBVote = Selectable -export type DBInsertableVoteEvent = Insertable -export type DBHNItem = Selectable diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index d093c8cf..7a04d055 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -2,8 +2,8 @@ import { createId } from '@paralleldrive/cuid2' import { redirect } from '@remix-run/node' import bcrypt from 'bcryptjs' import { safeRedirect } from 'remix-utils/safe-redirect' -import { db } from '#app/db.ts' -import { type DBPassword, type DBUser } from '#app/types/db-types.ts' +import { db } from '#app/database/db.ts' +import { type DBPassword, type DBUser } from '#app/database/types.ts' import { combineHeaders, invariant } from './misc.tsx' import { authSessionStorage } from './session.server.ts' @@ -20,11 +20,6 @@ export async function getUserId(request: Request) { const sessionId = authSession.get(sessionKey) if (!sessionId) return null - // const session = await prisma.session.findUnique({ - // select: { user: { select: { id: true } } }, - // where: { id: sessionId, expirationDate: { gt: new Date() } }, - // }) - const session = await db .selectFrom('Session') .innerJoin('User', 'User.id', 'Session.userId') diff --git a/app/utils/user.ts b/app/utils/user.ts index 0aa96a37..187d963e 100644 --- a/app/utils/user.ts +++ b/app/utils/user.ts @@ -1,7 +1,7 @@ import { type SerializeFrom } from '@remix-run/node' import { useRouteLoaderData } from '@remix-run/react' +import { type User } from '#app/modules/auth/auth-types.ts' import { type loader as rootLoader } from '#app/root.tsx' -import { type User } from '#app/types/api-types.ts' function isUser(user: any): user is SerializeFrom['user'] { return user && typeof user === 'object' && typeof user.id === 'string' diff --git a/import-hn.ts b/import-hn.ts index ba72a88c..0e656e18 100644 --- a/import-hn.ts +++ b/import-hn.ts @@ -4,7 +4,7 @@ import zlib from 'zlib' import * as cliProgress from 'cli-progress' import { glob } from 'glob' import TurndownService from 'turndown' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { createPost } from '#app/modules/posts/post-service.ts' const turndownService = new TurndownService({ emDelimiter: '*' }) diff --git a/migrate.ts b/migrate.ts index 24b3d1ae..32b605df 100644 --- a/migrate.ts +++ b/migrate.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs' import * as path from 'path' import { fileURLToPath } from 'url' import { FileMigrationProvider, Migrator } from 'kysely' -import { db } from './app/db.ts' +import { db } from './app/database/db.ts' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) diff --git a/other/import-society-library-debatemap.ts b/other/import-society-library-debatemap.ts index b3e1fc24..04011c15 100644 --- a/other/import-society-library-debatemap.ts +++ b/other/import-society-library-debatemap.ts @@ -1,5 +1,5 @@ import fs from 'fs' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { createPost } from '#app/modules/posts/post-service.ts' import { invariant } from '../app/utils/misc.tsx' diff --git a/other/replay-vote-events.ts b/other/replay-vote-events.ts index 7e700f51..f9d91c75 100644 --- a/other/replay-vote-events.ts +++ b/other/replay-vote-events.ts @@ -1,6 +1,6 @@ import { sendVoteEvent } from '#app/modules/posts/scoring/global-brain-service.ts' -import { db } from '../app/db.ts' -import { type DBVoteEvent } from '../app/types/db-types.ts' +import { db } from '../app/database/db.ts' +import { type DBVoteEvent } from '../app/database/types.ts' async function replayVoteEvents() { console.log('Replaying vote events') diff --git a/seed.ts b/seed.ts index 8b1c874a..f51d6475 100644 --- a/seed.ts +++ b/seed.ts @@ -1,8 +1,8 @@ import bcrypt from 'bcryptjs' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { createPost } from '#app/modules/posts/post-service.ts' +import { VoteDirection } from '#app/modules/posts/post-types.ts' import { vote } from '#app/modules/posts/scoring/vote-service.ts' -import { Direction } from '#app/types/api-types.ts' import { getPasswordHash } from '#app/utils/auth.server.ts' export async function seed() { @@ -78,7 +78,7 @@ export async function seed() { // And also downvotes db.transaction().execute( - async trx => await vote(trx, bob, post1, Direction.Down), + async trx => await vote(trx, bob, post1, VoteDirection.Down), ) // And responds to bob's response @@ -137,27 +137,27 @@ export async function seed() { }) await db.transaction().execute(async trx => { - await vote(trx, alice, post6, Direction.Down) + await vote(trx, alice, post6, VoteDirection.Down) // agreed with 2 (shown 3) - await vote(trx, charlie, post2, Direction.Up) + await vote(trx, charlie, post2, VoteDirection.Up) // changed mind after seeing 2 - await vote(trx, charlie, post1, Direction.Down) + await vote(trx, charlie, post1, VoteDirection.Down) // changed mind back (for no particular reason) - await vote(trx, charlie, post1, Direction.Up) + await vote(trx, charlie, post1, VoteDirection.Up) // duplicate vote - await vote(trx, charlie, post1, Direction.Up) + await vote(trx, charlie, post1, VoteDirection.Up) // changed mind back again - await vote(trx, charlie, post1, Direction.Down) + await vote(trx, charlie, post1, VoteDirection.Down) // and s some other votes - await vote(trx, charlie, post1, Direction.Down) - await vote(trx, charlie, post2, Direction.Down) - await vote(trx, charlie, post2, Direction.Up) + await vote(trx, charlie, post1, VoteDirection.Down) + await vote(trx, charlie, post2, VoteDirection.Down) + await vote(trx, charlie, post2, VoteDirection.Up) await vote(trx, charlie, 3, 1) await vote(trx, charlie, 2, -1) await vote(trx, bob, 6, -1) diff --git a/tests/db-utils.ts b/tests/db-utils.ts index 74afb926..81239690 100644 --- a/tests/db-utils.ts +++ b/tests/db-utils.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker' import bcrypt from 'bcryptjs' import { UniqueEnforcer } from 'enforce-unique' import { type Kysely, sql } from 'kysely' -import { type DB } from '#app/types/kysely-types.ts' // this is the Database interface we defined earlier +import { type DB } from '#app/database/types.ts' const uniqueUsernameEnforcer = new UniqueEnforcer() diff --git a/tests/setup/custom-matchers.ts b/tests/setup/custom-matchers.ts index 3ed8b029..12ffbfe1 100644 --- a/tests/setup/custom-matchers.ts +++ b/tests/setup/custom-matchers.ts @@ -1,6 +1,6 @@ import * as setCookieParser from 'set-cookie-parser' import { expect } from 'vitest' -import { db } from '#app/db.ts' +import { db } from '#app/database/db.ts' import { sessionKey } from '#app/utils/auth.server.ts' import { authSessionStorage } from '#app/utils/session.server.ts' import { diff --git a/tests/setup/db-setup.ts b/tests/setup/db-setup.ts index 5b1a686d..04fac465 100644 --- a/tests/setup/db-setup.ts +++ b/tests/setup/db-setup.ts @@ -15,7 +15,7 @@ beforeAll(async () => { // we *must* use dynamic imports here so the process.env.DATABASE_URL is set // before prisma is imported and initialized afterEach(async () => { - const { db } = await import('#app/db.ts') + const { db } = await import('#app/database/db.ts') await cleanupDb(db) })