Skip to content
Merged
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
7 changes: 7 additions & 0 deletions cmd/campfire/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -112,6 +113,12 @@ func newPostCmd(f *factory.Factory) *cobra.Command {
return fmt.Errorf("failed to convert message: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

// Post the message
line, err := campfireOps.PostCampfireLine(f.Context(), projectID, campfireID, richContent, "text/html")
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions cmd/card/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/needmore/bc4/internal/attachments"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/utils"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -174,6 +175,14 @@ can be attached by using the flag multiple times.`,
richContent = rc
}

// Replace inline @Name mentions with bc-attachment tags
if richContent != "" {
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, resolvedProjectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
}

// Handle attachments
if len(attach) > 0 {
for _, attachPath := range attach {
Expand Down
14 changes: 14 additions & 0 deletions cmd/card/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/needmore/bc4/internal/attachments"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -101,6 +102,12 @@ func (m editModel) updateCard() tea.Cmd {
return cardUpdatedMsg{err: err}
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(m.factory.Context(), richContent, m.client, m.projectID)
if err != nil {
return cardUpdatedMsg{err: err}
}

req := api.CardUpdateRequest{
Title: m.cardTitle,
Content: richContent,
Expand Down Expand Up @@ -381,6 +388,13 @@ func updateCardNonInteractive(f *factory.Factory, client *api.Client, projectID
if err != nil {
return fmt.Errorf("failed to convert content: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

req.Content = richContent
} else {
req.Content = card.Content
Expand Down
27 changes: 4 additions & 23 deletions cmd/comment/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"

Expand All @@ -14,14 +13,12 @@ import (
"github.com/needmore/bc4/internal/attachments"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/ui"
"github.com/needmore/bc4/internal/utils"
"github.com/spf13/cobra"
)

var mentionRe = regexp.MustCompile(`(?:^|[>\s])(@[\w]+(?:\.[\w]+)*)`)

func newCreateCmd(f *factory.Factory) *cobra.Command {
var content string
var attachmentPath string
Expand Down Expand Up @@ -126,25 +123,9 @@ You can provide comment content in several ways:
}

// Replace inline @Name mentions with bc-attachment tags
// Supports @FirstName and @First.Last for disambiguation
submatches := mentionRe.FindAllStringSubmatch(richContent, -1)
if len(submatches) > 0 {
resolver := utils.NewUserResolver(client.Client, projectID)
// Extract capture group (the @mention) and convert @First.Last to "First Last"
mentions := make([]string, len(submatches))
identifiers := make([]string, len(submatches))
for i, sm := range submatches {
mentions[i] = sm[1]
identifiers[i] = strings.ReplaceAll(strings.TrimPrefix(sm[1], "@"), ".", " ")
}
people, err := resolver.ResolvePeople(f.Context(), identifiers)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
for i, match := range mentions {
tag := attachments.BuildTag(people[i].AttachableSGID)
richContent = strings.Replace(richContent, match, tag, 1)
}
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

// Attach file if provided
Expand Down
7 changes: 7 additions & 0 deletions cmd/comment/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -124,6 +125,12 @@ You can provide updated content in several ways:
return fmt.Errorf("failed to convert markdown: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

// Update the comment
req := api.CommentUpdateRequest{
Content: richContent,
Expand Down
7 changes: 7 additions & 0 deletions cmd/document/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/ui"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -107,6 +108,12 @@ You can provide document content in several ways:
return fmt.Errorf("failed to convert markdown: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

// Create the document
req := api.DocumentCreateRequest{
Title: title,
Expand Down
8 changes: 8 additions & 0 deletions cmd/document/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -132,6 +133,13 @@ You can provide updated content in several ways:
if err != nil {
return fmt.Errorf("failed to convert markdown: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

req.Content = richContent
hasUpdate = true
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/message/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/needmore/bc4/internal/cmdutil"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -137,6 +138,13 @@ cat updated.md | bc4 message edit 12345`,
if err != nil {
return fmt.Errorf("failed to convert markdown: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

req.Content = richContent
hasUpdate = true
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/message/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/ui"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -107,6 +108,12 @@ You can provide message content in several ways:
return fmt.Errorf("failed to convert markdown: %w", err)
}

// Replace inline @Name mentions with bc-attachment tags
richContent, err = mentions.Resolve(f.Context(), richContent, client.Client, projectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}

// Create the message
req := api.MessageCreateRequest{
Subject: title,
Expand Down
13 changes: 13 additions & 0 deletions cmd/todo/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/needmore/bc4/internal/attachments"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/utils"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -232,6 +233,18 @@ func runAdd(f *factory.Factory, opts *addOptions, args []string) error {
}
}

// Replace inline @Name mentions with bc-attachment tags
richTitle, err = mentions.Resolve(f.Context(), richTitle, client.Client, resolvedProjectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
if richDescription != "" {
richDescription, err = mentions.Resolve(f.Context(), richDescription, client.Client, resolvedProjectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
}

// Handle attachments
if len(opts.attach) > 0 {
for _, attachPath := range opts.attach {
Expand Down
9 changes: 9 additions & 0 deletions cmd/todo/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/needmore/bc4/internal/cmdutil"
"github.com/needmore/bc4/internal/factory"
"github.com/needmore/bc4/internal/markdown"
"github.com/needmore/bc4/internal/mentions"
"github.com/needmore/bc4/internal/parser"
"github.com/needmore/bc4/internal/utils"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -187,6 +188,10 @@ func runEdit(f *factory.Factory, opts *editOptions, args []string) error {
if err != nil {
return fmt.Errorf("failed to convert title: %w", err)
}
richTitle, err = mentions.Resolve(f.Context(), richTitle, client.Client, resolvedProjectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
req.Content = richTitle
}

Expand All @@ -196,6 +201,10 @@ func runEdit(f *factory.Factory, opts *editOptions, args []string) error {
if err != nil {
return fmt.Errorf("failed to convert description: %w", err)
}
richDescription, err = mentions.Resolve(f.Context(), richDescription, client.Client, resolvedProjectID)
if err != nil {
return fmt.Errorf("failed to resolve mentions: %w", err)
}
req.Description = richDescription
}

Expand Down
52 changes: 52 additions & 0 deletions internal/mentions/mentions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package mentions

import (
"context"
"regexp"
"strings"

"github.com/needmore/bc4/internal/api"
"github.com/needmore/bc4/internal/attachments"
"github.com/needmore/bc4/internal/utils"
)

// mentionRe matches @Name and @First.Last mentions in rich text content.
// Mentions must appear at the start of the string, after > (blockquotes),
// or after whitespace.
var mentionRe = regexp.MustCompile(`(?:^|[>\s])(@[\w]+(?:\.[\w]+)*)`)

// Resolve finds @mentions in rich text content and replaces them with
// Basecamp bc-attachment tags. It uses the project's people list to
// resolve mention identifiers to their AttachableSGID values.
//
// Supports @FirstName and @First.Last syntax. Returns the content
// unchanged if no mentions are found.
func Resolve(ctx context.Context, richContent string, client api.APIClient, projectID string) (string, error) {
indexMatches := mentionRe.FindAllStringSubmatchIndex(richContent, -1)
if len(indexMatches) == 0 {
return richContent, nil
}

resolver := utils.NewUserResolver(client, projectID)

// Extract capture group (the @mention) and convert @First.Last to "First Last"
identifiers := make([]string, len(indexMatches))
for i, loc := range indexMatches {
// loc[2]:loc[3] is the capture group (the @mention)
mention := richContent[loc[2]:loc[3]]
identifiers[i] = strings.ReplaceAll(strings.TrimPrefix(mention, "@"), ".", " ")
}

people, err := resolver.ResolvePeople(ctx, identifiers)
if err != nil {
return "", err
}

// Replace from end to start so earlier indices remain valid
for i := len(indexMatches) - 1; i >= 0; i-- {
tag := attachments.BuildTag(people[i].AttachableSGID)
richContent = richContent[:indexMatches[i][2]] + tag + richContent[indexMatches[i][3]:]
}

return richContent, nil
}
Loading
Loading