New "saved" page will list all the users/orgs who bookmarked a specific
link.
Implements: https://todo.code.netlandish.com/~netlandish/links/106
Changelog-added: Save count is displayed on all relevant listing pages.
Changelog-added: New page to view all users/orgs who bookmarked a
specific link.
---
api/graph/generated.go | 811 +++++++++++++++++++++++++++++++++-
api/graph/model/models_gen.go | 9 +
api/graph/schema.graphqls | 14 +
api/graph/schema.resolvers.go | 113 ++++-
cmd/links/main.go | 3 +
cmd/test/helpers.go | 3 +
core/routes.go | 120 +++++
models/models.go | 12 +-
models/org_link.go | 7 +-
models/utils.go | 16 +-
templates/feed.html | 1 +
templates/link_list.html | 20 +-
values.go | 1 +
13 files changed, 1098 insertions(+), 32 deletions(-)
diff --git a/api/graph/generated.go b/api/graph/generated.go
index 61befad..c964512 100644
--- a/api/graph/generated.go
+++ b/api/graph/generated.go
@@ -130,6 +130,15 @@ type ComplexityRoot struct {
Status func(childComplexity int) int
}
+ BookmarkCursor struct {
+ Count func(childComplexity int) int
+ Description func(childComplexity int) int
+ Result func(childComplexity int) int
+ TagCloud func(childComplexity int) int
+ Title func(childComplexity int) int
+ URL func(childComplexity int) int
+ }
+
DeletePayload struct {
Message func(childComplexity int) int
ObjectID func(childComplexity int) int
@@ -277,25 +286,27 @@ type ComplexityRoot struct {
}
OrgLink struct {
- ArchiveURL func(childComplexity int) int
- Author func(childComplexity int) int
- BaseURLData func(childComplexity int) int
- BaseURLID func(childComplexity int) int
- CreatedOn func(childComplexity int) int
- Description func(childComplexity int) int
- Hash func(childComplexity int) int
- ID func(childComplexity int) int
- OrgID func(childComplexity int) int
- OrgSlug func(childComplexity int) int
- Starred func(childComplexity int) int
- Tags func(childComplexity int) int
- Title func(childComplexity int) int
- Type func(childComplexity int) int
- URL func(childComplexity int) int
- Unread func(childComplexity int) int
- UpdatedOn func(childComplexity int) int
- UserID func(childComplexity int) int
- Visibility func(childComplexity int) int
+ ArchiveURL func(childComplexity int) int
+ Author func(childComplexity int) int
+ BaseURLCounter func(childComplexity int) int
+ BaseURLData func(childComplexity int) int
+ BaseURLHash func(childComplexity int) int
+ BaseURLID func(childComplexity int) int
+ CreatedOn func(childComplexity int) int
+ Description func(childComplexity int) int
+ Hash func(childComplexity int) int
+ ID func(childComplexity int) int
+ OrgID func(childComplexity int) int
+ OrgSlug func(childComplexity int) int
+ Starred func(childComplexity int) int
+ Tags func(childComplexity int) int
+ Title func(childComplexity int) int
+ Type func(childComplexity int) int
+ URL func(childComplexity int) int
+ Unread func(childComplexity int) int
+ UpdatedOn func(childComplexity int) int
+ UserID func(childComplexity int) int
+ Visibility func(childComplexity int) int
}
OrgLinkCursor struct {
@@ -392,6 +403,7 @@ type ComplexityRoot struct {
GetAdminOrgStats func(childComplexity int, id int) int
GetAdminOrganizations func(childComplexity int, input *model.GetAdminOrganizationsInput) int
GetAuditLogs func(childComplexity int, input *model.AuditLogInput) int
+ GetBookmarks func(childComplexity int, hash string, tags *string) int
GetDomain func(childComplexity int, id int) int
GetDomains func(childComplexity int, orgSlug *string, service *model.DomainService) int
GetFeed func(childComplexity int, input *model.GetFeedInput) int
@@ -532,6 +544,7 @@ type QueryResolver interface {
GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error)
GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) (*model.PopularLinkCursor, error)
GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error)
+ GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error)
GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error)
GetOrgMembers(ctx context.Context, orgSlug string) ([]*models.User, error)
GetDomains(ctx context.Context, orgSlug *string, service *model.DomainService) ([]*models.Domain, error)
@@ -857,6 +870,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.BillingSettings.Status(childComplexity), true
+ case "BookmarkCursor.count":
+ if e.complexity.BookmarkCursor.Count == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.Count(childComplexity), true
+
+ case "BookmarkCursor.description":
+ if e.complexity.BookmarkCursor.Description == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.Description(childComplexity), true
+
+ case "BookmarkCursor.result":
+ if e.complexity.BookmarkCursor.Result == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.Result(childComplexity), true
+
+ case "BookmarkCursor.tagCloud":
+ if e.complexity.BookmarkCursor.TagCloud == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.TagCloud(childComplexity), true
+
+ case "BookmarkCursor.title":
+ if e.complexity.BookmarkCursor.Title == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.Title(childComplexity), true
+
+ case "BookmarkCursor.url":
+ if e.complexity.BookmarkCursor.URL == nil {
+ break
+ }
+
+ return e.complexity.BookmarkCursor.URL(childComplexity), true
+
case "DeletePayload.message":
if e.complexity.DeletePayload.Message == nil {
break
@@ -1738,6 +1793,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.OrgLink.Author(childComplexity), true
+ case "OrgLink.baseUrlCounter":
+ if e.complexity.OrgLink.BaseURLCounter == nil {
+ break
+ }
+
+ return e.complexity.OrgLink.BaseURLCounter(childComplexity), true
+
case "OrgLink.baseUrlData":
if e.complexity.OrgLink.BaseURLData == nil {
break
@@ -1745,6 +1807,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.OrgLink.BaseURLData(childComplexity), true
+ case "OrgLink.baseUrlHash":
+ if e.complexity.OrgLink.BaseURLHash == nil {
+ break
+ }
+
+ return e.complexity.OrgLink.BaseURLHash(childComplexity), true
+
case "OrgLink.baseUrlId":
if e.complexity.OrgLink.BaseURLID == nil {
break
@@ -2307,6 +2376,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.GetAuditLogs(childComplexity, args["input"].(*model.AuditLogInput)), true
+ case "Query.getBookmarks":
+ if e.complexity.Query.GetBookmarks == nil {
+ break
+ }
+
+ args, err := ec.field_Query_getBookmarks_args(context.TODO(), rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Query.GetBookmarks(childComplexity, args["hash"].(string), args["tags"].(*string)), true
+
case "Query.getDomain":
if e.complexity.Query.GetDomain == nil {
break
@@ -4289,6 +4370,65 @@ func (ec *executionContext) field_Query_getAuditLogs_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Query_getBookmarks_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+ var err error
+ args := map[string]interface{}{}
+ arg0, err := ec.field_Query_getBookmarks_argsHash(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["hash"] = arg0
+ arg1, err := ec.field_Query_getBookmarks_argsTags(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["tags"] = arg1
+ return args, nil
+}
+func (ec *executionContext) field_Query_getBookmarks_argsHash(
+ ctx context.Context,
+ rawArgs map[string]interface{},
+) (string, error) {
+ // We won't call the directive if the argument is null.
+ // Set call_argument_directives_with_null to true to call directives
+ // even if the argument is null.
+ _, ok := rawArgs["hash"]
+ if !ok {
+ var zeroVal string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("hash"))
+ if tmp, ok := rawArgs["hash"]; ok {
+ return ec.unmarshalNString2string(ctx, tmp)
+ }
+
+ var zeroVal string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Query_getBookmarks_argsTags(
+ ctx context.Context,
+ rawArgs map[string]interface{},
+) (*string, error) {
+ // We won't call the directive if the argument is null.
+ // Set call_argument_directives_with_null to true to call directives
+ // even if the argument is null.
+ _, ok := rawArgs["tags"]
+ if !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("tags"))
+ if tmp, ok := rawArgs["tags"]; ok {
+ return ec.unmarshalOString2ᚖstring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Query_getDomain_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -7039,6 +7179,323 @@ func (ec *executionContext) fieldContext_BillingSettings_status(_ context.Contex
return fc, nil
}
+func (ec *executionContext) _BookmarkCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_result(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Result, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.([]*models.OrgLink)
+ fc.Result = res
+ return ec.marshalNOrgLink2ᚕᚖlinksᚋmodelsᚐOrgLink(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_result(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_OrgLink_id(ctx, field)
+ case "title":
+ return ec.fieldContext_OrgLink_title(ctx, field)
+ case "description":
+ return ec.fieldContext_OrgLink_description(ctx, field)
+ case "url":
+ return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
+ case "baseUrlId":
+ return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
+ case "orgId":
+ return ec.fieldContext_OrgLink_orgId(ctx, field)
+ case "userId":
+ return ec.fieldContext_OrgLink_userId(ctx, field)
+ case "visibility":
+ return ec.fieldContext_OrgLink_visibility(ctx, field)
+ case "unread":
+ return ec.fieldContext_OrgLink_unread(ctx, field)
+ case "starred":
+ return ec.fieldContext_OrgLink_starred(ctx, field)
+ case "archiveUrl":
+ return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
+ case "type":
+ return ec.fieldContext_OrgLink_type(ctx, field)
+ case "tags":
+ return ec.fieldContext_OrgLink_tags(ctx, field)
+ case "author":
+ return ec.fieldContext_OrgLink_author(ctx, field)
+ case "orgSlug":
+ return ec.fieldContext_OrgLink_orgSlug(ctx, field)
+ case "createdOn":
+ return ec.fieldContext_OrgLink_createdOn(ctx, field)
+ case "updatedOn":
+ return ec.fieldContext_OrgLink_updatedOn(ctx, field)
+ case "baseUrlData":
+ return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BookmarkCursor_title(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_title(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Title, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BookmarkCursor_description(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_description(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Description, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BookmarkCursor_url(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_url(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.URL, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BookmarkCursor_count(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_count(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Count, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BookmarkCursor_tagCloud(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BookmarkCursor_tagCloud(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.TagCloud, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.([]*models.Tag)
+ fc.Result = res
+ return ec.marshalOTag2ᚕᚖlinksᚋmodelsᚐTag(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BookmarkCursor_tagCloud(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BookmarkCursor",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Tag_id(ctx, field)
+ case "name":
+ return ec.fieldContext_Tag_name(ctx, field)
+ case "slug":
+ return ec.fieldContext_Tag_slug(ctx, field)
+ case "createdOn":
+ return ec.fieldContext_Tag_createdOn(ctx, field)
+ case "count":
+ return ec.fieldContext_Tag_count(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _DeletePayload_success(ctx context.Context, field graphql.CollectedField, obj *model.DeletePayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_DeletePayload_success(ctx, field)
if err != nil {
@@ -11132,6 +11589,10 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f
return ec.fieldContext_OrgLink_updatedOn(ctx, field)
case "baseUrlData":
return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
},
@@ -11259,6 +11720,10 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context
return ec.fieldContext_OrgLink_updatedOn(ctx, field)
case "baseUrlData":
return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
},
@@ -11481,6 +11946,10 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f
return ec.fieldContext_OrgLink_updatedOn(ctx, field)
case "baseUrlData":
return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
},
@@ -15358,6 +15827,94 @@ func (ec *executionContext) fieldContext_OrgLink_baseUrlData(_ context.Context,
return fc, nil
}
+func (ec *executionContext) _OrgLink_baseUrlCounter(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.BaseURLCounter, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(int)
+ fc.Result = res
+ return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_OrgLink_baseUrlCounter(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "OrgLink",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Int does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _OrgLink_baseUrlHash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.BaseURLHash, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_OrgLink_baseUrlHash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "OrgLink",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _OrgLinkCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.OrgLinkCursor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_OrgLinkCursor_result(ctx, field)
if err != nil {
@@ -15435,6 +15992,10 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(_ context.Context,
return ec.fieldContext_OrgLink_updatedOn(ctx, field)
case "baseUrlData":
return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
},
@@ -18913,6 +19474,10 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
return ec.fieldContext_OrgLink_updatedOn(ctx, field)
case "baseUrlData":
return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
+ case "baseUrlCounter":
+ return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
+ case "baseUrlHash":
+ return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
},
@@ -18931,6 +19496,107 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
return fc, nil
}
+func (ec *executionContext) _Query_getBookmarks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Query_getBookmarks(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ directive0 := func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Query().GetBookmarks(rctx, fc.Args["hash"].(string), fc.Args["tags"].(*string))
+ }
+
+ directive1 := func(ctx context.Context) (interface{}, error) {
+ scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS")
+ if err != nil {
+ var zeroVal *model.BookmarkCursor
+ return zeroVal, err
+ }
+ kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO")
+ if err != nil {
+ var zeroVal *model.BookmarkCursor
+ return zeroVal, err
+ }
+ if ec.directives.Access == nil {
+ var zeroVal *model.BookmarkCursor
+ return zeroVal, errors.New("directive access is not implemented")
+ }
+ return ec.directives.Access(ctx, nil, directive0, scope, kind)
+ }
+
+ tmp, err := directive1(rctx)
+ if err != nil {
+ return nil, graphql.ErrorOnPath(ctx, err)
+ }
+ if tmp == nil {
+ return nil, nil
+ }
+ if data, ok := tmp.(*model.BookmarkCursor); ok {
+ return data, nil
+ }
+ return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.BookmarkCursor`, tmp)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*model.BookmarkCursor)
+ fc.Result = res
+ return ec.marshalNBookmarkCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query_getBookmarks(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Query",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "result":
+ return ec.fieldContext_BookmarkCursor_result(ctx, field)
+ case "title":
+ return ec.fieldContext_BookmarkCursor_title(ctx, field)
+ case "description":
+ return ec.fieldContext_BookmarkCursor_description(ctx, field)
+ case "url":
+ return ec.fieldContext_BookmarkCursor_url(ctx, field)
+ case "count":
+ return ec.fieldContext_BookmarkCursor_count(ctx, field)
+ case "tagCloud":
+ return ec.fieldContext_BookmarkCursor_tagCloud(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type BookmarkCursor", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Query_getBookmarks_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Query_getOrgLinks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_getOrgLinks(ctx, field)
if err != nil {
@@ -26956,6 +27622,67 @@ func (ec *executionContext) _BillingSettings(ctx context.Context, sel ast.Select
return out
}
+var bookmarkCursorImplementors = []string{"BookmarkCursor"}
+
+func (ec *executionContext) _BookmarkCursor(ctx context.Context, sel ast.SelectionSet, obj *model.BookmarkCursor) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, bookmarkCursorImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("BookmarkCursor")
+ case "result":
+ out.Values[i] = ec._BookmarkCursor_result(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "title":
+ out.Values[i] = ec._BookmarkCursor_title(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "description":
+ out.Values[i] = ec._BookmarkCursor_description(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "url":
+ out.Values[i] = ec._BookmarkCursor_url(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "count":
+ out.Values[i] = ec._BookmarkCursor_count(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "tagCloud":
+ out.Values[i] = ec._BookmarkCursor_tagCloud(ctx, field, obj)
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var deletePayloadImplementors = []string{"DeletePayload"}
func (ec *executionContext) _DeletePayload(ctx context.Context, sel ast.SelectionSet, obj *model.DeletePayload) graphql.Marshaler {
@@ -28339,6 +29066,16 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
+ case "baseUrlCounter":
+ out.Values[i] = ec._OrgLink_baseUrlCounter(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "baseUrlHash":
+ out.Values[i] = ec._OrgLink_baseUrlHash(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -29272,6 +30009,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
+ case "getBookmarks":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Query_getBookmarks(ctx, field)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ rrm := func(ctx context.Context) graphql.Marshaler {
+ return ec.OperationContext.RootResolverMiddleware(ctx,
+ func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ }
+
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "getOrgLinks":
field := field
@@ -30636,6 +31395,20 @@ func (ec *executionContext) marshalNBaseURLData2linksᚋmodelsᚐBaseURLData(ctx
return ec._BaseURLData(ctx, sel, &v)
}
+func (ec *executionContext) marshalNBookmarkCursor2linksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx context.Context, sel ast.SelectionSet, v model.BookmarkCursor) graphql.Marshaler {
+ return ec._BookmarkCursor(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNBookmarkCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx context.Context, sel ast.SelectionSet, v *model.BookmarkCursor) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._BookmarkCursor(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
res, err := graphql.UnmarshalBoolean(v)
return res, graphql.ErrorOnPath(ctx, err)
diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go
index 2404e55..5d3bc9f 100644
--- a/api/graph/model/models_gen.go
+++ b/api/graph/model/models_gen.go
@@ -120,6 +120,15 @@ type AuditLogInput struct {
Limit *int `json:"limit,omitempty"`
}
+type BookmarkCursor struct {
+ Result []*models.OrgLink `json:"result"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ Count int `json:"count"`
+ TagCloud []*models.Tag `json:"tagCloud,omitempty"`
+}
+
type CompleteRegisterInput struct {
Name string `json:"name"`
Username string `json:"username"`
diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls
index 5987562..e170c97 100644
--- a/api/graph/schema.graphqls
+++ b/api/graph/schema.graphqls
@@ -244,6 +244,8 @@ type OrgLink {
createdOn: Time!
updatedOn: Time!
baseUrlData: BaseURLData!
+ baseUrlCounter: Int!
+ baseUrlHash: String!
}
type LinkShort {
@@ -373,6 +375,15 @@ type UserCursor {
pageInfo: PageInfo
}
+type BookmarkCursor {
+ result: [OrgLink]!
+ title: String!
+ description: String!
+ url: String!
+ count: Int!
+ tagCloud: [Tag]
+}
+
type OrgLinkCursor {
result: [OrgLink]!
pageInfo: PageInfo
@@ -805,6 +816,9 @@ type Query {
"Returns a specific organization link"
getOrgLink(hash: String!): OrgLink @access(scope: LINKS, kind: RO)
+ "Returns saved links (bookmarks) for a given URL. Accepts BaseURL hash or full url"
+ getBookmarks(hash: String!, tags: String): BookmarkCursor! @access(scope: LINKS, kind: RO)
+
"Returns an array of organization links"
getOrgLinks(input: GetLinkInput): OrgLinkCursor! @access(scope: LINKS, kind: RO)
diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index bfab68a..ec38373 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -4778,7 +4778,7 @@ func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
return &model.Version{
Major: 0,
Minor: 3,
- Patch: 0,
+ Patch: 1,
DeprecationDate: nil,
}, nil
}
@@ -5103,6 +5103,117 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.Or
return orgLinks[0], nil
}
+// GetBookmarks is the resolver for the getBookmarks field.
+func (r *queryResolver) GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error) {
+ tokenUser := oauth2.ForContext(ctx)
+ if tokenUser == nil {
+ return nil, valid.ErrAuthorization
+ }
+ user := tokenUser.User.(*models.User)
+ lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
+ lt := localizer.GetLocalizer(lang)
+
+ ctx = timezone.Context(ctx, links.GetUserTZ(user))
+ validator := valid.New(ctx)
+
+ opts := &database.FilterOptions{
+ Filter: sq.Eq{"b.public_ready": true},
+ Limit: 1,
+ }
+ _, err := url.Parse(hash)
+ if err != nil {
+ opts.Filter = sq.And{
+ opts.Filter,
+ sq.Eq{"b.url": links.StripURLFragment(hash)},
+ }
+ } else {
+ opts.Filter = sq.And{
+ opts.Filter,
+ sq.Eq{"b.hash": hash},
+ }
+ }
+
+ burls, err := models.GetBaseURLs(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(burls) == 0 {
+ validator.Error("%s", lt.Translate("BaseURL Not Found")).
+ WithCode(valid.ErrNotFoundCode)
+ return nil, nil
+ }
+ burl := burls[0]
+
+ cloudOrder := model.CloudOrderTypeNameAsc
+
+ opts = &database.FilterOptions{
+ Filter: sq.Eq{"ol.base_url_id": burl.ID},
+ OrderBy: "ol.id DESC",
+ Limit: 100,
+ }
+
+ if tags != nil && *tags != "" {
+ f := links.NewTagQuery("tag_links", "org_link_id")
+ subQText, subQVal, err := f.GetSubQuery(tags, nil)
+ if err != nil {
+ return nil, err
+ }
+ opts.Filter = sq.And{
+ opts.Filter,
+ sq.Expr("ol.id IN ("+subQText+")", subQVal...),
+ }
+ }
+
+ if !user.IsAuthenticated() {
+ opts.Filter = sq.And{
+ opts.Filter,
+ sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
+ }
+ } else {
+ // User can see public or all links belonging to an org they have at least
+ // read access to.
+ opts.Filter = sq.And{
+ opts.Filter,
+ sq.Or{
+ // link is public
+ sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
+ // current user is owner of link
+ sq.Eq{"ol.user_id": user.ID},
+ // current user is owner of link organization
+ sq.And{
+ sq.Eq{"o.is_active": true},
+ sq.Eq{"o.owner_id": user.ID},
+ },
+ // current user has read permission of link organization
+ sq.And{
+ sq.Eq{"ou.user_id": user.ID},
+ sq.Eq{"o.is_active": true},
+ sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusBusiness},
+ },
+ },
+ }
+ }
+
+ orgLinks, err := models.GetOrgLinks(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ ltags, err := models.LinkTagCloud(ctx, orgLinks, models.TagCloudOrdering(cloudOrder))
+ if err != nil {
+ return nil, err
+ }
+
+ return &model.BookmarkCursor{
+ Result: orgLinks,
+ TagCloud: ltags,
+ Title: burl.Title,
+ Description: burl.Data.Meta.Description,
+ Count: burl.Counter,
+ URL: burl.URL,
+ }, nil
+}
+
// GetOrgLinks is the resolver for the getOrgLinks field.
func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error) {
if input == nil {
diff --git a/cmd/links/main.go b/cmd/links/main.go
index 9698153..2e26c81 100644
--- a/cmd/links/main.go
+++ b/cmd/links/main.go
@@ -307,6 +307,9 @@ func run() error {
return "tag-normal"
}
},
+ "showCounter": func(obj any) bool {
+ return models.ShowLinkCounter(obj)
+ },
})
srv.TemplateAllowOverride(
"seoData",
diff --git a/cmd/test/helpers.go b/cmd/test/helpers.go
index 967f17e..1396162 100644
--- a/cmd/test/helpers.go
+++ b/cmd/test/helpers.go
@@ -144,6 +144,9 @@ func NewWebTestServer(t *testing.T) (*server.Server, *echo.Echo) {
return "tag-normal"
}
},
+ "showCounter": func(obj any) bool {
+ return models.ShowLinkCounter(obj)
+ },
})
err = srv.LoadTemplatesFS(links.TemplateFS, "templates/*.html", "templates/*.txt")
if err != nil {
diff --git a/core/routes.go b/core/routes.go
index ce9f659..54fa248 100644
--- a/core/routes.go
+++ b/core/routes.go
@@ -57,6 +57,7 @@ func (s *Service) RegisterRoutes() {
s.Group.GET("/link/:hash", s.OrgLinkDetail).Name = s.RouteName("link_detail")
s.Group.GET("/click/:hash", s.OrgLinkRedirect).Name = s.RouteName("link_redirect")
s.Group.GET("/tag-autocomplete", s.TagAutocomplete).Name = s.RouteName("tag_autocomplete")
+ s.Group.GET("/bookmarks/:hash", s.GetBookmarks).Name = s.RouteName("bookmarks")
s.Group.Use(auth.AuthRequired())
s.Group.GET("/:slug/follow-toggle/:action", s.FollowToggle).Name = s.RouteName("follow-toggle")
@@ -1790,6 +1791,8 @@ func (s *Service) UserFeed(c echo.Context) error {
siteName
}
}
+ baseUrlCounter
+ baseUrlHash
}
pageInfo {
cursor
@@ -1918,6 +1921,121 @@ func (s *Service) UserFeed(c echo.Context) error {
return s.Render(c, http.StatusOK, "feed.html", gmap)
}
+// GetBookmarks will show all bookmarks for a specific BaseURL
+func (s *Service) GetBookmarks(c echo.Context) error {
+ lhash := c.Param("hash")
+ if lhash == "" {
+ return echo.NotFoundHandler(c)
+ }
+
+ err := links.TagAbuseRedirect(c)
+ if err != nil {
+ return err
+ }
+
+ type GraphQLResponse struct {
+ OrgLinks struct {
+ Result []models.OrgLink `json:"result"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ Count int `json:"count"`
+ TagCloud []models.Tag `json:"tagCloud"`
+ } `json:"getBookmarks"`
+ }
+
+ var result GraphQLResponse
+ op := gqlclient.NewOperation(
+ `query GetBookmarks($hash: String!, $tags: String) {
+ getBookmarks(hash: $hash, tags: $tags) {
+ result {
+ id
+ title
+ url
+ visibility
+ description
+ userId
+ createdOn
+ author
+ orgSlug
+ unread
+ starred
+ archiveUrl
+ type
+ hash
+ tags {
+ id
+ name
+ slug
+ }
+ }
+ title
+ description
+ url
+ count
+ tagCloud {
+ name
+ slug
+ count
+ }
+ }
+ }`)
+ op.Var("hash", lhash)
+
+ var tag string
+ queries := make(url.Values)
+ if c.QueryParam("tag") != "" {
+ tag = c.QueryParam("tag")
+ op.Var("tags", tag)
+ queries.Add("tag", tag)
+ }
+
+ err = links.Execute(links.LangContext(c), op, &result)
+ if err != nil {
+ if graphError, ok := err.(*gqlclient.Error); ok {
+ err = links.ParseInputErrors(c, graphError, gobwebs.Map{})
+ }
+ return err
+ }
+
+ lt := localizer.GetSessionLocalizer(c)
+ pd := localizer.NewPageData(lt.Translate("Saved Bookmarks"))
+ pd.Data["tags"] = lt.Translate("Tags")
+ pd.Data["no_links"] = lt.Translate("This feed has no links. Booo!")
+ orgLinks := result.OrgLinks.Result
+
+ seoData := links.GetSEOData(c)
+ url := links.GetLinksDomainURL(c)
+ url.Path = c.Echo().Reverse(s.RouteName("bookmarks"))
+ seoData.Description = lt.Translate(
+ "Recent bookmarks for URL %s added on LinkTaco.com",
+ result.OrgLinks.URL,
+ )
+ seoData.Title = pd.Title
+ seoData.Keywords = lt.Translate("recent, public, links, linktaco")
+ seoData.URL = url.String()
+ seoData.TwitterURL = url.String()
+
+ navFlag := "bookmarks"
+ gmap := gobwebs.Map{
+ "pd": pd,
+ "seoData": seoData,
+ "links": orgLinks,
+ "navFlag": navFlag,
+ "currURL": seoData.URL,
+ "tagCloud": result.OrgLinks.TagCloud,
+ "pTitle": result.OrgLinks.Title,
+ "pDescription": result.OrgLinks.Description,
+ "pURL": result.OrgLinks.URL,
+ "pCount": result.OrgLinks.Count,
+ "queries": template.URL(queries.Encode()),
+ "tagFilter": strings.Replace(tag, ",", ", ", -1),
+ "hideSaveCount": true,
+ }
+
+ return s.Render(c, http.StatusOK, "link_list.html", gmap)
+}
+
// OrgLinksList ...
func (s *Service) OrgLinksList(c echo.Context) error {
gctx := c.(*server.Context)
@@ -1984,6 +2102,8 @@ func (s *Service) OrgLinksList(c echo.Context) error {
siteName
}
}
+ baseUrlCounter
+ baseUrlHash
}
pageInfo {
cursor
diff --git a/models/models.go b/models/models.go
index d343917..f38a5f2 100644
--- a/models/models.go
+++ b/models/models.go
@@ -87,11 +87,13 @@ type OrgLink struct {
CreatedOn time.Time `db:"created_on" json:"createdOn"`
UpdatedOn time.Time `db:"updated_on" json:"updatedOn"`
- OrgSlug string `db:"-" json:"orgSlug"`
- BaseURLData BaseURLData `db:"-" json:"baseUrlData"`
- Tags []Tag `db:"-" json:"tags"`
- Clicks sql.NullInt64 `db:"-" json:"-"`
- Author string `db:"-"`
+ OrgSlug string `db:"-" json:"orgSlug"`
+ BaseURLData BaseURLData `db:"-" json:"baseUrlData"`
+ BaseURLCounter int `db:"-" json:"baseUrlCounter"`
+ BaseURLHash string `db:"-" json:"baseUrlHash"`
+ Tags []Tag `db:"-" json:"tags"`
+ Clicks sql.NullInt64 `db:"-" json:"-"`
+ Author string `db:"-"`
}
// OrgUser ...
diff --git a/models/org_link.go b/models/org_link.go
index 39238b1..5fa11e1 100644
--- a/models/org_link.go
+++ b/models/org_link.go
@@ -38,7 +38,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
rows, err := q.
Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.base_url_id", "ol.org_id", "ol.user_id",
"ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.hash",
- "ol.created_on", "ol.updated_on", "o.slug", "u.full_name", "json_agg(t)::jsonb", "b.data").
+ "ol.created_on", "ol.updated_on", "o.slug", "u.full_name", "json_agg(t)::jsonb", "b.data", "b.counter", "b.hash").
From("org_links ol").
Join("organizations o ON o.id = ol.org_id").
Join("users u ON ol.user_id = u.id").
@@ -47,7 +47,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
LeftJoin("tag_links tl ON tl.org_link_id = ol.id").
LeftJoin("tags t ON t.id = tl.tag_id").
LeftJoin("followers f ON f.org_id = ol.org_id").
- GroupBy("ol.id", "o.slug", "u.full_name", "b.data").
+ GroupBy("ol.id", "o.slug", "u.full_name", "b.data", "b.counter", "b.hash").
Distinct().
PlaceholderFormat(sq.Dollar).
RunWith(tx).
@@ -65,7 +65,8 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
var tags string
if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID, &o.OrgID,
&o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.Hash,
- &o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags, &o.BaseURLData); err != nil {
+ &o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags,
+ &o.BaseURLData, &o.BaseURLCounter, &o.BaseURLHash); err != nil {
return err
}
re := regexp.MustCompile(`(,\s)?null,?`)
diff --git a/models/utils.go b/models/utils.go
index be59bc5..7a45864 100644
--- a/models/utils.go
+++ b/models/utils.go
@@ -19,7 +19,7 @@ func getShortCode(ctx context.Context, tx *sql.Tx, table, field string, filter s
counter := 0
for {
- for i := 0; i < codelen; i++ {
+ for i := range codelen {
code[i] = chars[r.Intn(len(chars))]
}
@@ -81,3 +81,17 @@ func TagsToString(t []Tag) string {
}
return tags
}
+
+// ShowLinkCounter says whether or not to show a link counter
+func ShowLinkCounter(obj any) bool {
+ switch v := any(obj).(type) {
+ case *BaseURL, BaseURL:
+ return true
+ case *OrgLink:
+ return v.BaseURLCounter > 0
+ case OrgLink:
+ return v.BaseURLCounter > 0
+ default:
+ return false
+ }
+}
diff --git a/templates/feed.html b/templates/feed.html
index 279245d..9c6aa7a 100644
--- a/templates/feed.html
+++ b/templates/feed.html
@@ -53,6 +53,7 @@
<div class="d-flex items-center">
<h3 class="is-marginless mr-1">
<a class="text-dark" href="{{.URL}}" target="_blank">{{truncate .Title 60}}</a>
+ {{ if showCounter . }}<a href="{{ reverse "core:bookmarks" .BaseURLHash }}" class="button outline primary tag tag-popular is-small">{{if $.isPopular}}{{ .Counter }}{{ else }}{{ .BaseURLCounter }}{{ end }}</a>{{ end }}
</h3>
</div>
{{if .Description}}
diff --git a/templates/link_list.html b/templates/link_list.html
index fae218b..cf9ff5c 100644
--- a/templates/link_list.html
+++ b/templates/link_list.html
@@ -1,5 +1,6 @@
{{template "base" .}}
{{ define "title" }}{{ .title }}{{ end }}
+{{ if ne .navFlag "bookmarks" }}
<form method="GET"
action="{{ if .isUserDomain }}/{{ else }}{{if .isPopular}}{{reverse "core:popular_link_list"}}{{else if .isOrgLink}}{{.currURL}}{{else}}{{reverse "core:recent_link_list"}}{{end}}{{ end }}{{if .hasUnreadFilter}}?filter=unread{{else if .hasStarredFilter}}?filter=starred{{end}}"
id="advanced-search-form"
@@ -41,6 +42,7 @@
</section>
{{end}}
</form>
+{{ end }}
<section class="card shadow-card container">
<div class="row">
<div class="col-9">
@@ -73,6 +75,20 @@
</p>
{{ end }}
{{end}}
+ {{ if eq .navFlag "bookmarks" }}
+ <div class="row">
+ <h3>{{ .pTitle }}
+ <div class="button outline primary tag tag-popular is-small">{{ .pCount }}</div>
+ </h3>
+ </div>
+ <div class="row">
+ <p class="is-marginless">
+ {{ if .pDescription }}<p>{{ htmlSafe (truncate .pDescription 200) }}</p>{{ end }}
+ <small>{{ .pURL }}</small>
+ </p>
+ </div>
+ <hr>
+ {{ end }}
<ul class="list-unstyled pl-0">
{{range .links}}
<li class="mb-1">
@@ -99,9 +115,7 @@
{{end}}
<h3 class="is-marginless mr-1">
<a class="text-dark" href="{{.URL}}" target="_blank">{{truncate .Title 60}}</a>
- {{if $.isPopular}}
- <span class="button outline primary tag tag-popular is-small">{{ .Counter }}</span>
- {{end}}
+ {{ if not $.hideSaveCount }}{{ if showCounter . }}<a href="{{ if $.isPopular }}{{ reverse "core:bookmarks" .Hash }}{{ else }}{{ reverse "core:bookmarks" .BaseURLHash }}{{ end }}" class="button outline primary tag tag-popular is-small">{{if $.isPopular}}{{ .Counter }}{{ else }}{{ .BaseURLCounter }}{{ end }}</a>{{ end }}{{ end }}
</h3>
{{if $.isOrgLink}}
{{if eq .UserID $.currentUserID}}
diff --git a/values.go b/values.go
index 73fca25..4d6ce69 100644
--- a/values.go
+++ b/values.go
@@ -95,6 +95,7 @@ var InvalidSlugs = []string{
"services",
"integrations",
"xmpp",
+ "bookmarks",
}
// Interval used to display data in analytics engagement chart
--
2.47.2