package components import ( "fmt" "net/url" "tangled.org/oolong.social/oolong/internal/firehose" "tangled.org/oolong.social/oolong/internal/web/bff" ) // CommentButtonProps defines properties for the comment button type CommentButtonProps struct { SubjectURI string // AT-URI of the record to comment on SubjectCID string // CID of the record CommentCount int // Number of comments on this record IsAuthenticated bool // Whether the user is authenticated } // CommentButton renders a comment button with count that links to comments section templ CommentButton(props CommentButtonProps) { } // CommentModerationContext holds moderation state for rendering comment actions type CommentModerationContext struct { IsModerator bool // User has moderator role CanHideRecord bool // User has hide_record permission CanBlockUser bool // User has blacklist_user permission } // CommentSectionProps defines properties for the comment section type CommentSectionProps struct { SubjectURI string // AT-URI of the record SubjectCID string // CID of the record Comments []firehose.IndexedComment // List of comments (threaded order with depth) IsAuthenticated bool // Whether the user is authenticated CurrentUserDID string // DID of the current user (for delete buttons) ModCtx CommentModerationContext // Moderation context for comment actions ViewURL string // URL of the parent brew (for sharing comments) } // CommentSection renders the full comment section with list and form templ CommentSection(props CommentSectionProps) {

Discussion

if len(props.Comments) > 0 { { fmt.Sprintf("%d", len(props.Comments)) } }
if props.SubjectURI == "" || props.SubjectCID == "" {

Comments will be available once this record is indexed.

} else if props.IsAuthenticated { @CommentForm(CommentFormProps{ SubjectURI: props.SubjectURI, SubjectCID: props.SubjectCID, }) } else {

Log in to join the conversation.

} @CommentList(CommentListProps{ Comments: props.Comments, CurrentUserDID: props.CurrentUserDID, SubjectURI: props.SubjectURI, SubjectCID: props.SubjectCID, IsAuthenticated: props.IsAuthenticated, ModCtx: props.ModCtx, ViewURL: props.ViewURL, })
} // CommentFormProps defines properties for the comment form type CommentFormProps struct { SubjectURI string // AT-URI of the record SubjectCID string // CID of the record } // CommentForm renders a form for creating new comments templ CommentForm(props CommentFormProps) {
1000 char limit
} // CommentListProps defines properties for the comment list type CommentListProps struct { Comments []firehose.IndexedComment // List of comments (threaded order with depth) CurrentUserDID string // DID of the current user (for delete buttons) SubjectURI string // AT-URI of the root subject (for reply forms) SubjectCID string // CID of the root subject (for reply forms) IsAuthenticated bool // Whether the user is authenticated (for reply buttons) ModCtx CommentModerationContext // Moderation context for comment actions ViewURL string // URL of the parent brew (for sharing comments) } // CommentList renders a list of comments with threading support templ CommentList(props CommentListProps) {
if len(props.Comments) == 0 {

No comments yet

Be the first to share your thoughts

} else { for _, comment := range props.Comments { @CommentItem(CommentItemProps{ Comment: comment, CanDelete: props.CurrentUserDID == comment.ActorDID, CanReply: props.IsAuthenticated && comment.Depth < 2 && comment.CID != "", IsOwner: props.CurrentUserDID == comment.ActorDID, SubjectURI: props.SubjectURI, SubjectCID: props.SubjectCID, IsAuthenticated: props.IsAuthenticated, ModCtx: props.ModCtx, ViewURL: props.ViewURL, }) } }
} // CommentItemProps defines properties for a single comment type CommentItemProps struct { Comment firehose.IndexedComment CanDelete bool // Whether the current user can delete this comment CanReply bool // Whether the user can reply (authenticated and depth < 2) IsOwner bool // Whether the current user owns this comment SubjectURI string // AT-URI of the root subject (for reply form) SubjectCID string // CID of the root subject (for reply form) IsAuthenticated bool // Whether the user is authenticated ModCtx CommentModerationContext // Moderation context for comment actions ViewURL string // URL of the parent brew (for sharing) } // CommentItem renders a single comment with optional threading indentation templ CommentItem(props CommentItemProps) {
if props.Comment.Depth > 0 {
}
@UserBadge(UserBadgeProps{ ProfileURL: "/profile/" + getCommentHandle(props.Comment), AvatarURL: getCommentAvatarURL(props.Comment), DisplayName: getCommentDisplayName(props.Comment), Handle: getCommentHandle(props.Comment), TimeAgo: bff.FormatTimeAgo(props.Comment.CreatedAt), Size: "sm", }) if props.CanReply { }

{ props.Comment.Text }

@ActionBar(ActionBarProps{ SubjectURI: buildCommentURI(props.Comment), SubjectCID: props.Comment.CID, IsLiked: props.Comment.IsLiked, LikeCount: props.Comment.LikeCount, ShareURL: getCommentShareURL(props.ViewURL, props.Comment), ShareTitle: "Comment on Oolong", ShareText: props.Comment.Text, IsOwner: props.IsOwner, DeleteURL: "/api/comments/" + props.Comment.RKey, DeleteTarget: "#comment-" + props.Comment.RKey, IsAuthenticated: props.IsAuthenticated, IsModerator: props.ModCtx.IsModerator, CanHideRecord: props.ModCtx.CanHideRecord, CanBlockUser: props.ModCtx.CanBlockUser, AuthorDID: props.Comment.ActorDID, })
if props.CanReply {
@ReplyForm(ReplyFormProps{ SubjectURI: props.SubjectURI, SubjectCID: props.SubjectCID, ParentURI: buildCommentURI(props.Comment), ParentCID: props.Comment.CID, })
}
} // ReplyFormProps defines properties for the reply form type ReplyFormProps struct { SubjectURI string // AT-URI of the root subject SubjectCID string // CID of the root subject ParentURI string // AT-URI of the parent comment ParentCID string // CID of the parent comment } // ReplyForm renders a compact inline reply form templ ReplyForm(props ReplyFormProps) {
} // Helper functions for comment display func getCommentHandle(comment firehose.IndexedComment) string { if comment.Handle != "" { return comment.Handle } return comment.ActorDID } func getCommentDisplayName(comment firehose.IndexedComment) string { if comment.DisplayName != nil { return *comment.DisplayName } return "" } func getCommentAvatarURL(comment firehose.IndexedComment) string { if comment.Avatar != nil { return *comment.Avatar } return "" } // getDepthClass returns the CSS class for comment depth func getDepthClass(depth int) string { switch depth { case 1: return "comment-depth-1" case 2: return "comment-depth-2" default: return "" } } // getCommentShareURL returns the share URL for a comment, linking to the comment anchor on the brew page func getCommentShareURL(viewURL string, comment firehose.IndexedComment) string { if viewURL == "" { return "" } return viewURL + "#comment-" + comment.RKey } // buildCommentURI constructs the AT-URI for a comment from its indexed data func buildCommentURI(comment firehose.IndexedComment) string { // The subject URI contains the DID, format: at://did:plc:xxx/social.oolong.alpha.brew/rkey // We need to extract the DID and build a comment URI: at://did:plc:xxx/social.oolong.alpha.comment/rkey return fmt.Sprintf("at://%s/social.oolong.alpha.comment/%s", comment.ActorDID, comment.RKey) } func commentActionURL(subjectURI, subjectCID string) string { values := url.Values{} values.Set("subject_uri", subjectURI) values.Set("subject_cid", subjectCID) return "/api/comments?" + values.Encode() }