Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/web/src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,30 @@
gap: 6px;
}


.rk-post-quick-edit {
border: 1px solid #cde2f8;
border-radius: 999px;
background: #edf5ff;
color: #355f8d;
padding: 5px 11px;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.01em;
white-space: nowrap;
cursor: pointer;
}

.rk-post-quick-edit:hover:not(:disabled) {
border-color: #b7d6f5;
background: #e3f0ff;
}

.rk-post-quick-edit:disabled {
opacity: 0.64;
cursor: not-allowed;
}

.rk-admin-quick-delete {
border: 1px solid #efb8c4;
border-radius: 999px;
Expand Down
17 changes: 15 additions & 2 deletions apps/web/src/features/community/components/CommunityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,21 @@ export function CommunityFeed() {

const editMutation = useMutation({
mutationFn: async ({ postId, content }: { postId: string; content: string }) => updateCommunityPost(postId, content),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['community_posts'] })
onSuccess: async (updatedPost) => {
queryClient.setQueryData<CommunityPost[]>(communityPostsQueryKey, (previous) => {
if (!previous) return []
return previous.map((post) => {
if (post.id !== updatedPost.id) return post

return {
...post,
content: updatedPost.content,
updated_at: updatedPost.updated_at,
}
})
})

await queryClient.invalidateQueries({ queryKey: ['community_posts'] })
setStatusTone('success')
setStatusMessage('Post updated.')
},
Expand Down
33 changes: 29 additions & 4 deletions apps/web/src/features/feed/pages/FeedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,19 @@ function getRsvpMemberLabel(userId: string, labelsById: Record<string, string>):
return `User ${userId.slice(0, 8)}`
}



function isSameAuthorByFallback(viewer: { id: string; label: string } | null | undefined, ownerUserId: string | null | undefined, ownerAuthor: string | null | undefined): boolean {
if (!viewer) return false
if (ownerUserId && ownerUserId === viewer.id) return true

const viewerLabel = viewer.label.trim().toLowerCase()
const ownerLabel = (ownerAuthor ?? '').trim().toLowerCase()
if (!viewerLabel || !ownerLabel) return false

return viewerLabel === ownerLabel
Comment on lines +373 to +377

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict fallback ownership check to missing user IDs

This fallback returns true whenever display names match, even after a user-id mismatch, so two accounts with the same nickname will both be treated as owners and see the new Edit controls for each other’s feed posts/comments. Because the edit flows call update mutations once this guard passes, this broadens edit eligibility beyond actual ownership; the fallback should only run when ownerUserId is absent.

Useful? React with 👍 / 👎.

}

function getAvatarFallbackText(name: string): string {
const trimmed = name.trim()
if (!trimmed) return '?'
Expand Down Expand Up @@ -1257,7 +1270,7 @@ export function FeedPage() {


async function editFeedPost(post: Post) {
if (!user || user.id !== post.user_id) return
if (!isSameAuthorByFallback(user, post.user_id, post.author)) return

const nextLocation = window.prompt('Edit destination', post.location)?.trim() ?? ''
if (!nextLocation || nextLocation === post.location) return
Expand All @@ -1278,7 +1291,7 @@ export function FeedPage() {
}

async function editFeedComment(comment: PostComment) {
if (!user || user.id !== comment.user_id) return
if (!isSameAuthorByFallback(user, comment.user_id, comment.author)) return

const nextText = window.prompt('Edit comment', comment.text)?.trim() ?? ''
if (!nextText || nextText === comment.text) return
Expand Down Expand Up @@ -1687,7 +1700,8 @@ export function FeedPage() {
const isClosingSoon = deadlineDiffMs !== null && deadlineDiffMs > 0 && deadlineDiffMs <= 24 * 60 * 60 * 1000
const remainingSeats = Math.max(rsvpSummary.capacity - rsvpSummary.goingCount, 0)
const postedAgoLabel = formatTimeAgo(post.created_at)

const canEditPost = isSameAuthorByFallback(user, post.user_id, post.author)

const commentThreads = buildCommentThreads(post.comments)

return (
Expand Down Expand Up @@ -1729,6 +1743,16 @@ export function FeedPage() {
<span className={`rk-status rk-status-${post.status}`}>{getStatusLabel(post.status)}</span>
{isClosingSoon ? <span className="rk-status rk-status-closing">Closing Soon</span> : null}
{isClosed ? <span className="rk-status rk-status-closed">Closed</span> : null}
{canEditPost ? (
<button
type="button"
className="rk-post-quick-edit"
onClick={() => void editFeedPost(post)}
disabled={Boolean(isPostEditPendingByPostId[post.id])}
>
{isPostEditPendingByPostId[post.id] ? 'Saving...' : 'Edit'}
</button>
) : null}
{user?.isAdmin ? (
<button
type="button"
Expand Down Expand Up @@ -1892,6 +1916,7 @@ export function FeedPage() {
const isReplyOpen = Boolean(replyOpenByCommentId[node.id])
const replyDraft = replyDraftByCommentId[node.id] ?? ''
const initial = (node.author || '?').charAt(0).toUpperCase()
const canEditComment = isSameAuthorByFallback(user, node.user_id, node.author)

return (
<div key={node.id} className={`rk-comment-item ${depth > 0 ? 'rk-comment-item-reply' : ''}`}>
Expand All @@ -1915,7 +1940,7 @@ export function FeedPage() {
>
{isReplyOpen ? 'Cancel' : 'Reply'}
</button>
{user?.id === node.user_id ? (
{canEditComment ? (
<button
type="button"
className="rk-comment-reply-button"
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/services/community/community.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export async function updateCommunityPost(postId: string, content: string): Prom
.select()
.single()

if (error && (error.code === '42501' || error.code === 'PGRST301')) {
throw new Error('Community edit permission is not enabled. Apply the latest Supabase RLS update policy and try again.')
}

throwIfPostgrestError(error)

if (!data) {
Expand Down Expand Up @@ -167,6 +171,10 @@ export async function updateComment(commentId: string, content: string): Promise
.select()
.single()

if (error && (error.code === '42501' || error.code === 'PGRST301')) {
throw new Error('Community edit permission is not enabled. Apply the latest Supabase RLS update policy and try again.')
}

throwIfPostgrestError(error)

if (!data) {
Expand Down