Skip to content

Commit 1a21fcb

Browse files
Arinji2anay-208
andauthored
Add reply feature (#92)
* Finish Getting Reply Data * Add Image + Reply Spline Icons * Add Reply Component * Update Message to use reply * Update Page to pass reply data * Update text color + remove unused scroll margin * Remove old linking system * Add in new wrapper * Use wrapper * Format tailwind styles * Fix Mobile Responsive * Fix Bugs + Seperate group if message has a reply * Add Support for Deleted Replies * Add a bit of gap between replies and messages * Format with Prettier * Format Page With Prettier * Gap only for mobile Co-authored-by: Anay Paraswani <[email protected]> * Use Hash Instead of Search Params * Update with Ternary Fix + Check fixing to satify types * Add CN + utilize it * Use Scroll Padding + Remove JS Implementation of Scrolling --------- Co-authored-by: Anay Parswani <[email protected]> Co-authored-by: Anay Paraswani <[email protected]>
1 parent bda2d2d commit 1a21fcb

File tree

13 files changed

+398
-117
lines changed

13 files changed

+398
-117
lines changed

apps/web/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type RootLayoutProps = { children: ReactNode }
4545

4646
const RootLayout = ({ children }: RootLayoutProps) => {
4747
return (
48-
<html lang="en" className={`${inter.className} dark`}>
48+
<html lang="en" className={`scroll-pt-60 ${inter.className} dark`}>
4949
<body className="bg-neutral-50 dark:bg-neutral-900 text-slate-900 dark:text-white">
5050
<header className="border-b border-neutral-700">
5151
<div className="container max-w-7xl flex mx-auto px-4 py-6 justify-between items-center">

apps/web/app/post/[id]/page.tsx

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const getPostMessage = async (postId: string) => {
6161
.innerJoin('users', 'users.snowflakeId', 'messages.userId')
6262
.select([
6363
'messages.id',
64+
'messages.snowflakeId',
6465
'messages.content',
6566
'messages.createdAt',
6667
'users.id as authorId',
@@ -97,6 +98,7 @@ const getMessages = async (postId: string) => {
9798
'messages.snowflakeId',
9899
'messages.content',
99100
'messages.createdAt',
101+
'messages.replyToMessageId',
100102
'users.id as authorId',
101103
'users.avatarUrl as authorAvatarUrl',
102104
'users.username as authorUsername',
@@ -179,6 +181,10 @@ const Post = async ({ params }: PostProps) => {
179181
const messages = await getMessages(params.id)
180182
const postMessage = await getPostMessage(params.id)
181183
const answerMessage = messages.find((m) => m.snowflakeId === post.answerId)
184+
// For replies. in `messages`, the post message is not included.
185+
// Incase The user has replied to the post message, we are creating this to search through allMessages
186+
const allMessages = [...messages, ...(postMessage ? [postMessage] : [])]
187+
// If a user has sent multiple messages in a row, group them
182188
const groupedMessages = groupMessagesByUser(messages, post.answerId)
183189
const hasAnswer =
184190
post.answerId && messages.some((m) => m.snowflakeId === post.answerId)
@@ -241,23 +247,23 @@ const Post = async ({ params }: PostProps) => {
241247
/>
242248
<LayoutWithSidebar className="mt-4">
243249
<div>
244-
<h1 className="mb-4 font-semibold text-3xl">{post.title}</h1>
250+
<h1 className="mb-4 text-3xl font-semibold">{post.title}</h1>
245251

246-
<div className="flex flex-col sm:flex-row gap-2 sm:items-center justify-between">
252+
<div className="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
247253
<div className="flex flex-wrap items-center gap-2">
248254
{hasAnswer ? (
249-
<div className="px-2.5 py-1 border border-green-400 text-green-400 rounded-full opacity-60">
255+
<div className="rounded-full border border-green-400 px-2.5 py-1 text-green-400 opacity-60">
250256
Answered
251257
</div>
252258
) : (
253-
<div className="px-2.5 py-1 border rounded-full opacity-50">
259+
<div className="rounded-full border px-2.5 py-1 opacity-50">
254260
Unanswered
255261
</div>
256262
)}
257263
<div>
258264
{post.userIsPublic ? (
259265
<Link
260-
className=" text-white opacity-90"
266+
className="text-white opacity-90"
261267
href={`/user/${post.userID}`}
262268
>
263269
{truncatedName}
@@ -267,14 +273,14 @@ const Post = async ({ params }: PostProps) => {
267273
)}{' '}
268274
<span className="opacity-50">
269275
posted this in{' '}
270-
<span className=" font-semibold">#{post.channelName}</span>
276+
<span className="font-semibold">#{post.channelName}</span>
271277
</span>
272278
</div>
273279
</div>
274280

275281
<a
276282
href={`https://discord.com/channels/752553802359505017/${post.snowflakeId}/${post.snowflakeId}`}
277-
className="shrink-0 w-fit px-4 py-1.5 font-semibold text-white border-neutral-700 border rounded hover:bg-neutral-700 hover:no-underline transition-colors"
283+
className="w-fit shrink-0 rounded border border-neutral-700 px-4 py-1.5 font-semibold text-white transition-colors hover:bg-neutral-700 hover:no-underline"
278284
target="_blank"
279285
rel="noopener noreferrer"
280286
>
@@ -309,8 +315,8 @@ const Post = async ({ params }: PostProps) => {
309315
</MessageGroup>
310316

311317
{answerMessage && (
312-
<div className="p-2 sm:p-3 space-y-1.5 border border-green-400 rounded">
313-
<div className="flex space-x-2 items-center text-green-400">
318+
<div className="space-y-1.5 rounded border border-green-400 p-2 sm:p-3">
319+
<div className="flex items-center space-x-2 text-green-400">
314320
<CheckCircleSolidIcon />
315321
<div className="text-sm">
316322
Answered by{' '}
@@ -336,7 +342,7 @@ const Post = async ({ params }: PostProps) => {
336342

337343
<a
338344
href={`#message-${answerMessage.snowflakeId}`}
339-
className="mt-2 opacity-80 font-semibold text-sm space-x-1"
345+
className="mt-2 space-x-1 text-sm font-semibold opacity-80"
340346
>
341347
<span>View full answer</span>
342348
<ArrowDownIcon size={4} />
@@ -357,26 +363,51 @@ const Post = async ({ params }: PostProps) => {
357363
(m) => m.snowflakeId === post.answerId,
358364
)}
359365
>
360-
{group.messages.map((message, i) => (
361-
<Message
362-
key={message.id.toString()}
363-
snowflakeId={message.snowflakeId}
364-
createdAt={message.createdAt}
365-
content={message.content}
366-
isFirstRow={i === 0}
367-
author={{
368-
username: message.authorUsername,
369-
avatarUrl: message.authorAvatarUrl,
370-
isPublic: message.userIsPublic,
371-
isOP: postMessage
372-
? message.authorId === postMessage.authorId
373-
: false,
374-
isModerator: message.userIsModerator,
375-
userID: message.userID,
376-
}}
377-
attachments={message.attachments}
378-
/>
379-
))}
366+
{group.messages.map((message, i) => {
367+
const hasReply = message.replyToMessageId !== null
368+
const replyMessage = hasReply
369+
? allMessages.find(
370+
(m) => m.snowflakeId === message.replyToMessageId,
371+
)
372+
: 'deleted'
373+
return (
374+
<Message
375+
key={message.id.toString()}
376+
snowflakeId={message.snowflakeId}
377+
createdAt={message.createdAt}
378+
content={message.content}
379+
reply={
380+
hasReply &&
381+
replyMessage &&
382+
typeof replyMessage !== 'string'
383+
? {
384+
author: {
385+
username: replyMessage.authorUsername,
386+
avatarUrl: replyMessage.authorAvatarUrl,
387+
},
388+
messageID: replyMessage.snowflakeId,
389+
content: replyMessage.content,
390+
attachments: replyMessage.attachments,
391+
}
392+
: hasReply && !replyMessage
393+
? 'deleted'
394+
: undefined
395+
}
396+
isFirstRow={i === 0 || hasReply}
397+
author={{
398+
username: message.authorUsername,
399+
avatarUrl: message.authorAvatarUrl,
400+
isPublic: message.userIsPublic,
401+
isOP: postMessage
402+
? message.authorId === postMessage.authorId
403+
: false,
404+
isModerator: message.userIsModerator,
405+
userID: message.userID,
406+
}}
407+
attachments={message.attachments}
408+
/>
409+
)
410+
})}
380411
</MessageGroup>
381412
))}
382413
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { IconProps, IconSvg } from './base'
2+
3+
export const ImageIcon = (props: IconProps) => (
4+
<IconSvg
5+
aria-label="Reply Spline"
6+
role="img"
7+
viewBox="0 0 24 24"
8+
width={20}
9+
height={20}
10+
{...props}
11+
>
12+
<path
13+
fill="currentColor"
14+
fill-rule="evenodd"
15+
d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Zm13.35 8.13 3.5 4.67c.37.5.02 1.2-.6 1.2H5.81a.75.75 0 0 1-.59-1.22l1.86-2.32a1.5 1.5 0 0 1 2.34 0l.5.64 2.23-2.97a2 2 0 0 1 3.2 0ZM10.2 5.98c.23-.91-.88-1.55-1.55-.9a.93.93 0 0 1-1.3 0c-.67-.65-1.78-.01-1.55.9a.93.93 0 0 1-.65 1.12c-.9.26-.9 1.54 0 1.8.48.14.77.63.65 1.12-.23.91.88 1.55 1.55.9a.93.93 0 0 1 1.3 0c.67.65 1.78.01 1.55-.9a.93.93 0 0 1 .65-1.12c.9-.26.9-1.54 0-1.8a.93.93 0 0 1-.65-1.12Z"
16+
clip-rule="evenodd"
17+
/>
18+
</IconSvg>
19+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { IconProps, IconSvg } from './base'
2+
3+
export const BadReplyIcon = (props: IconProps) => (
4+
<IconSvg
5+
aria-label="Bad Reply"
6+
role="img"
7+
viewBox="0 0 12 8"
8+
width={12}
9+
height={8}
10+
{...props}
11+
>
12+
<path
13+
d="M0.809739 3.59646L5.12565 0.468433C5.17446 0.431163 5.23323 0.408043 5.2951 0.401763C5.35698 0.395482 5.41943 0.406298 5.4752 0.432954C5.53096 0.45961 5.57776 0.50101 5.61013 0.552343C5.64251 0.603676 5.65914 0.662833 5.6581 0.722939V2.3707C10.3624 2.3707 11.2539 5.52482 11.3991 7.21174C11.4028 7.27916 11.3848 7.34603 11.3474 7.40312C11.3101 7.46021 11.2554 7.50471 11.1908 7.53049C11.1262 7.55626 11.0549 7.56204 10.9868 7.54703C10.9187 7.53201 10.857 7.49695 10.8104 7.44666C8.72224 5.08977 5.6581 5.63359 5.6581 5.63359V7.28135C5.65831 7.34051 5.64141 7.39856 5.60931 7.44894C5.5772 7.49932 5.53117 7.54004 5.4764 7.5665C5.42163 7.59296 5.3603 7.60411 5.29932 7.59869C5.23834 7.59328 5.18014 7.57151 5.13128 7.53585L0.809739 4.40892C0.744492 4.3616 0.691538 4.30026 0.655067 4.22975C0.618596 4.15925 0.599609 4.08151 0.599609 4.00269C0.599609 3.92386 0.618596 3.84612 0.655067 3.77562C0.691538 3.70511 0.744492 3.64377 0.809739 3.59646Z"
14+
fill="currentColor"
15+
></path>
16+
</IconSvg>
17+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { IconProps, IconSvg } from './base'
2+
3+
export const ReplySplineIcon = (props: IconProps) => (
4+
<IconSvg
5+
aria-label="Reply Spline"
6+
role="img"
7+
viewBox="0 0 21 4"
8+
width={21}
9+
height={4}
10+
{...props}
11+
>
12+
<path d="M1 9V6C1 3.23858 3.23858 1 6 1H18" stroke="#72767D" />
13+
</IconSvg>
14+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import { cn } from '@/utils/cn'
4+
import { useHashFocus } from '@/utils/hooks/useHashFocus'
5+
import React, { useEffect, useState } from 'react'
6+
7+
type MessageWrapperProps = {
8+
children: React.ReactNode
9+
snowflakeId: string
10+
}
11+
export const MessageWrapper = ({
12+
children,
13+
snowflakeId,
14+
}: MessageWrapperProps) => {
15+
const { isHashFocused } = useHashFocus(`#message-${snowflakeId}`)
16+
const [isHighlighted, setIsHighlighted] = useState(false)
17+
18+
useEffect(() => {
19+
if (!isHashFocused) return
20+
setIsHighlighted(true)
21+
let timeout: NodeJS.Timeout
22+
timeout = setTimeout(() => {
23+
setIsHighlighted(false)
24+
}, 1000)
25+
return () => {
26+
if (timeout) clearTimeout(timeout)
27+
}
28+
}, [isHashFocused])
29+
return (
30+
<div
31+
id={`message-${snowflakeId}`}
32+
className={cn(
33+
'group rounded transition-colors duration-300 ease-in-out',
34+
{
35+
'bg-white/10': isHighlighted,
36+
},
37+
)}
38+
>
39+
{children}
40+
</div>
41+
)
42+
}

0 commit comments

Comments
 (0)