Skip to content
This repository was archived by the owner on Mar 12, 2026. It is now read-only.

Commit 7f9dc04

Browse files
committed
feat: add inline @mention support to comment create
1 parent cda5308 commit 7f9dc04

3 files changed

Lines changed: 70 additions & 9 deletions

File tree

cmd/comment/create.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8+
"regexp"
89
"strconv"
910
"strings"
1011

@@ -15,9 +16,12 @@ import (
1516
"github.com/needmore/bc4/internal/markdown"
1617
"github.com/needmore/bc4/internal/parser"
1718
"github.com/needmore/bc4/internal/ui"
19+
"github.com/needmore/bc4/internal/utils"
1820
"github.com/spf13/cobra"
1921
)
2022

23+
var mentionRe = regexp.MustCompile(`@[\w]+(?:\.[\w]+)*`)
24+
2125
func newCreateCmd(f *factory.Factory) *cobra.Command {
2226
var content string
2327
var attachmentPath string
@@ -121,6 +125,26 @@ You can provide comment content in several ways:
121125
richContent = rc
122126
}
123127

128+
// Replace inline @Name mentions with bc-attachment tags
129+
// Supports @FirstName and @First.Last for disambiguation
130+
inlineMatches := mentionRe.FindAllString(richContent, -1)
131+
if len(inlineMatches) > 0 {
132+
resolver := utils.NewUserResolver(client.Client, projectID)
133+
// Convert @First.Last to "First Last" for resolution
134+
identifiers := make([]string, len(inlineMatches))
135+
for i, m := range inlineMatches {
136+
identifiers[i] = strings.ReplaceAll(strings.TrimPrefix(m, "@"), ".", " ")
137+
}
138+
people, err := resolver.ResolvePeople(f.Context(), identifiers)
139+
if err != nil {
140+
return fmt.Errorf("failed to resolve mentions: %w", err)
141+
}
142+
for i, match := range inlineMatches {
143+
tag := attachments.BuildTag(people[i].AttachableSGID)
144+
richContent = strings.Replace(richContent, match, tag, 1)
145+
}
146+
}
147+
124148
// Attach file if provided
125149
if attachmentPath != "" {
126150
fileData, err := os.ReadFile(attachmentPath)

internal/api/client.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,16 @@ type Company struct {
347347

348348
// Person represents a Basecamp user
349349
type Person struct {
350-
ID int64 `json:"id"`
351-
Name string `json:"name"`
352-
EmailAddress string `json:"email_address"`
353-
Title string `json:"title"`
354-
AvatarURL string `json:"avatar_url"`
355-
Company *Company `json:"company,omitempty"`
356-
CreatedAt string `json:"created_at,omitempty"`
357-
Admin bool `json:"admin,omitempty"`
358-
Owner bool `json:"owner,omitempty"`
350+
ID int64 `json:"id"`
351+
AttachableSGID string `json:"attachable_sgid"`
352+
Name string `json:"name"`
353+
EmailAddress string `json:"email_address"`
354+
Title string `json:"title"`
355+
AvatarURL string `json:"avatar_url"`
356+
Company *Company `json:"company,omitempty"`
357+
CreatedAt string `json:"created_at,omitempty"`
358+
Admin bool `json:"admin,omitempty"`
359+
Owner bool `json:"owner,omitempty"`
359360
}
360361

361362
// Todo represents a Basecamp todo item

internal/utils/users.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,42 @@ func (ur *UserResolver) GetPeople(ctx context.Context) ([]api.Person, error) {
7777
return ur.people, nil
7878
}
7979

80+
// ResolvePeople resolves a list of user identifiers to full Person objects
81+
func (ur *UserResolver) ResolvePeople(ctx context.Context, identifiers []string) ([]api.Person, error) {
82+
if err := ur.ensurePeopleCached(ctx); err != nil {
83+
return nil, err
84+
}
85+
86+
var people []api.Person
87+
var notFound []string
88+
89+
for _, identifier := range identifiers {
90+
identifier = strings.TrimSpace(identifier)
91+
if identifier == "" {
92+
continue
93+
}
94+
95+
personID, found := ur.resolveIdentifier(identifier)
96+
if !found {
97+
notFound = append(notFound, identifier)
98+
continue
99+
}
100+
101+
for _, p := range ur.people {
102+
if p.ID == personID {
103+
people = append(people, p)
104+
break
105+
}
106+
}
107+
}
108+
109+
if len(notFound) > 0 {
110+
return nil, fmt.Errorf("could not find users: %s", strings.Join(notFound, ", "))
111+
}
112+
113+
return people, nil
114+
}
115+
80116
// ensurePeopleCached loads the project people if not already cached
81117
func (ur *UserResolver) ensurePeopleCached(ctx context.Context) error {
82118
if ur.cached {

0 commit comments

Comments
 (0)