1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-10 01:51:07 +09:00

Merge pull request #1536 from anyproto/go-3813-set-lastused-for-relations

GO-3813 Set lastUsedDate for relations
This commit is contained in:
Kirill Stonozhenko 2024-09-11 17:31:42 +03:00 committed by GitHub
commit dcb9b6dc99
Signed by: github
GPG key ID: B5690EEEBB952194
24 changed files with 518 additions and 193 deletions

View file

@ -40,6 +40,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/collection"
"github.com/anyproto/anytype-heart/core/block/editor"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/export"
importer "github.com/anyproto/anytype-heart/core/block/import"
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
@ -301,7 +302,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(nameserviceclient.New()).
Register(payments.New()).
Register(paymentscache.New()).
Register(peerstatus.New())
Register(peerstatus.New()).
Register(lastused.New())
}
func MiddlewareVersion() string {

View file

@ -22,13 +22,23 @@ func (s *Service) SetDetails(ctx session.Context, objectId string, details []*mo
})
}
func (s *Service) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []*model.Detail) (err error) {
return cache.Do(s, objectId, func(b basic.DetailsSettable) error {
return b.SetDetailsAndUpdateLastUsed(ctx, details, true)
})
}
func (s *Service) SetDetailsList(ctx session.Context, objectIds []string, details []*model.Detail) (err error) {
var (
resultError error
anySucceed bool
)
for _, objectId := range objectIds {
err := s.SetDetails(ctx, objectId, details)
for i, objectId := range objectIds {
setDetailsFunc := s.SetDetails
if i == 0 {
setDetailsFunc = s.SetDetailsAndUpdateLastUsed
}
err := setDetailsFunc(ctx, objectId, details)
if err != nil {
resultError = errors.Join(resultError, err)
} else {
@ -51,10 +61,20 @@ func (s *Service) ModifyDetails(objectId string, modifier func(current *types.St
})
}
func (s *Service) ModifyDetailsAndUpdateLastUsed(objectId string, modifier func(current *types.Struct) (*types.Struct, error)) (err error) {
return cache.Do(s, objectId, func(du basic.DetailsUpdatable) error {
return du.UpdateDetailsAndLastUsed(modifier)
})
}
func (s *Service) ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) (resultError error) {
var anySucceed bool
for _, objectId := range req.ObjectIds {
err := s.ModifyDetails(objectId, func(current *types.Struct) (*types.Struct, error) {
for i, objectId := range req.ObjectIds {
modifyDetailsFunc := s.ModifyDetails
if i == 0 {
modifyDetailsFunc = s.ModifyDetailsAndUpdateLastUsed
}
err := modifyDetailsFunc(objectId, func(current *types.Struct) (*types.Struct, error) {
for _, op := range req.Operations {
if !pbtypes.IsEmptyValue(op.Set) {
// Set operation has higher priority than Add and Remove, because it modifies full value

View file

@ -7,6 +7,7 @@ import (
"github.com/samber/lo"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/table"
@ -65,10 +66,12 @@ type CommonOperations interface {
type DetailsSettable interface {
SetDetails(ctx session.Context, details []*model.Detail, showEvent bool) (err error)
SetDetailsAndUpdateLastUsed(ctx session.Context, details []*model.Detail, showEvent bool) (err error)
}
type DetailsUpdatable interface {
UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error)
UpdateDetailsAndLastUsed(update func(current *types.Struct) (*types.Struct, error)) (err error)
}
type Restrictionable interface {
@ -104,12 +107,14 @@ func NewBasic(
objectStore objectstore.ObjectStore,
layoutConverter converter.LayoutConverter,
fileObjectService fileobject.Service,
lastUsedUpdater lastused.ObjectUsageUpdater,
) AllOperations {
return &basic{
SmartBlock: sb,
objectStore: objectStore,
layoutConverter: layoutConverter,
fileObjectService: fileObjectService,
lastUsedUpdater: lastUsedUpdater,
}
}
@ -119,6 +124,7 @@ type basic struct {
objectStore objectstore.ObjectStore
layoutConverter converter.LayoutConverter
fileObjectService fileobject.Service
lastUsedUpdater lastused.ObjectUsageUpdater
}
func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id string, err error) {

View file

@ -45,7 +45,7 @@ func TestBasic_Create(t *testing.T) {
t.Run("generic", func(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
id, err := b.CreateBlock(st, pb.RpcBlockCreateRequest{
Block: &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "ll"}}},
@ -59,7 +59,7 @@ func TestBasic_Create(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
s := sb.NewState()
id, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.TitleBlockId,
@ -80,7 +80,7 @@ func TestBasic_Create(t *testing.T) {
}
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
_, err := b.CreateBlock(sb.NewState(), pb.RpcBlockCreateRequest{})
assert.ErrorIs(t, err, restriction.ErrRestricted)
})
@ -94,7 +94,7 @@ func TestBasic_Duplicate(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "3"}))
st := sb.NewState()
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil).Duplicate(st, st, "", 0, []string{"2"})
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil).Duplicate(st, st, "", 0, []string{"2"})
require.NoError(t, err)
err = sb.Apply(st)
@ -172,7 +172,7 @@ func TestBasic_Duplicate(t *testing.T) {
ts.SetDetail(bundle.RelationKeySpaceId.String(), pbtypes.String(tc.spaceIds[1]))
// when
newIds, err := NewBasic(source, nil, nil, tc.fos()).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
newIds, err := NewBasic(source, nil, nil, tc.fos(), nil).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
require.NoError(t, err)
require.NoError(t, target.Apply(ts))
@ -206,7 +206,7 @@ func TestBasic_Unlink(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
err := b.Unlink(nil, "2")
require.NoError(t, err)
@ -220,7 +220,7 @@ func TestBasic_Unlink(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
err := b.Unlink(nil, "2", "3")
require.NoError(t, err)
@ -237,7 +237,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "3"})).
AddBlock(simple.New(&model.Block{Id: "4"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
err := b.Move(st, st, "4", model.Block_Inner, []string{"3"})
@ -251,7 +251,7 @@ func TestBasic_Move(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
s := sb.NewState()
id1, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.HeaderLayoutId,
@ -300,7 +300,7 @@ func TestBasic_Move(t *testing.T) {
},
),
)
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil)
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil, nil)
state := testDoc.NewState()
// when
@ -316,7 +316,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
require.NoError(t, err)
@ -336,7 +336,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(firstBlock).
AddBlock(secondBlock)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
require.NoError(t, err)
@ -350,7 +350,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
err := b.Move(st, nil, "1", model.Block_Top, []string{"2"})
require.NoError(t, err)
@ -379,7 +379,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving non-root table block '"+block+"' leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -394,7 +394,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("no error on moving root table block", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -408,7 +408,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("no error on moving one row between another", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -422,7 +422,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving rows with incorrect position leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -435,7 +435,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving rows and some other blocks between another leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -448,7 +448,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving the row between itself leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -461,7 +461,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving table block from invalid table leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
st.Unlink("columns")
@ -477,7 +477,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving a block to '"+block+"' block leads to moving it under the table", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
// when
@ -492,7 +492,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving a block to the invalid table leads to moving it under the table", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
st := sb.NewState()
st.Unlink("columns")
@ -516,7 +516,7 @@ func TestBasic_MoveToAnotherObject(t *testing.T) {
sb2 := smarttest.New("test2")
sb2.AddBlock(simple.New(&model.Block{Id: "test2", ChildrenIds: []string{}}))
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil, nil)
srcState := sb1.NewState()
destState := sb2.NewState()
@ -551,7 +551,7 @@ func TestBasic_Replace(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
newId, err := b.Replace(nil, "2", &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "l"}}})
require.NoError(t, err)
require.NotEmpty(t, newId)
@ -561,7 +561,7 @@ func TestBasic_SetFields(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
fields := &types.Struct{
Fields: map[string]*types.Value{
@ -580,7 +580,7 @@ func TestBasic_Update(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
err := b.Update(nil, func(b simple.Block) error {
b.Model().BackgroundColor = "test"
@ -594,7 +594,7 @@ func TestBasic_SetDivStyle(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2", Content: &model.BlockContentOfDiv{Div: &model.BlockContentDiv{}}}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
err := b.SetDivStyle(nil, model.BlockContentDiv_Dots, "2")
require.NoError(t, err)
@ -614,7 +614,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("correct", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
err := b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "testRelKey",
@ -636,7 +636,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("not relation block", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "1",
Key: "key",
@ -645,7 +645,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("relation not found", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "not exists",
@ -661,7 +661,7 @@ func TestBasic_FeaturedRelationAdd(t *testing.T) {
s.AddBundledRelationLinks(bundle.RelationKeyDescription)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
newRel := []string{bundle.RelationKeyDescription.String(), bundle.RelationKeyName.String()}
require.NoError(t, b.FeaturedRelationAdd(nil, newRel...))
@ -677,7 +677,7 @@ func TestBasic_FeaturedRelationRemove(t *testing.T) {
template.WithDescription(s)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
require.NoError(t, b.FeaturedRelationRemove(nil, bundle.RelationKeyDescription.String()))
res := sb.NewState()
@ -714,7 +714,7 @@ func TestBasic_ReplaceLink(t *testing.T) {
}
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
require.NoError(t, b.ReplaceLink(oldId, newId))
res := sb.NewState()

View file

@ -4,11 +4,11 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/gogo/protobuf/types"
"golang.org/x/exp/maps"
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/restriction"
@ -33,11 +33,29 @@ type detailUpdate struct {
}
func (bs *basic) SetDetails(ctx session.Context, details []*model.Detail, showEvent bool) (err error) {
_, err = bs.setDetails(ctx, details, showEvent)
return err
}
func (bs *basic) SetDetailsAndUpdateLastUsed(ctx session.Context, details []*model.Detail, showEvent bool) (err error) {
var keys []domain.RelationKey
keys, err = bs.setDetails(ctx, details, showEvent)
if err != nil {
return err
}
ts := time.Now().Unix()
for _, key := range keys {
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
}
return nil
}
func (bs *basic) setDetails(ctx session.Context, details []*model.Detail, showEvent bool) (updatedKeys []domain.RelationKey, err error) {
s := bs.NewStateCtx(ctx)
// Collect updates handling special cases. These cases could update details themselves, so we
// have to apply changes later
updates := bs.collectDetailUpdates(details, s)
updates, updatedKeys := bs.collectDetailUpdates(details, s)
newDetails := applyDetailUpdates(s.CombinedDetails(), updates)
s.SetDetails(newDetails)
@ -46,43 +64,71 @@ func (bs *basic) SetDetails(ctx session.Context, details []*model.Detail, showEv
flags.AddToState(s)
if err = bs.Apply(s, smartblock.NoRestrictions, smartblock.KeepInternalFlags); err != nil {
return
return nil, err
}
bs.discardOwnSetDetailsEvent(ctx, showEvent)
return nil
return updatedKeys, nil
}
func (bs *basic) UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error) {
_, _, err = bs.updateDetails(update)
return err
}
func (bs *basic) UpdateDetailsAndLastUsed(update func(current *types.Struct) (*types.Struct, error)) (err error) {
var oldDetails, newDetails *types.Struct
oldDetails, newDetails, err = bs.updateDetails(update)
if err != nil {
return err
}
diff := pbtypes.StructDiff(oldDetails, newDetails)
if diff == nil || diff.Fields == nil {
return nil
}
ts := time.Now().Unix()
for key := range diff.Fields {
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), domain.RelationKey(key), ts)
}
return nil
}
func (bs *basic) updateDetails(update func(current *types.Struct) (*types.Struct, error)) (oldDetails, newDetails *types.Struct, err error) {
if update == nil {
return fmt.Errorf("update function is nil")
return nil, nil, fmt.Errorf("update function is nil")
}
s := bs.NewState()
newDetails, err := update(s.CombinedDetails())
oldDetails = s.CombinedDetails()
oldDetailsCopy := pbtypes.CopyStruct(oldDetails, true)
newDetails, err = update(oldDetailsCopy)
if err != nil {
return
}
s.SetDetails(newDetails)
if err = bs.addRelationLinks(s, maps.Keys(newDetails.Fields)...); err != nil {
return
return nil, nil, err
}
return bs.Apply(s)
return oldDetails, newDetails, bs.Apply(s)
}
func (bs *basic) collectDetailUpdates(details []*model.Detail, s *state.State) []*detailUpdate {
func (bs *basic) collectDetailUpdates(details []*model.Detail, s *state.State) ([]*detailUpdate, []domain.RelationKey) {
updates := make([]*detailUpdate, 0, len(details))
keys := make([]domain.RelationKey, 0, len(details))
for _, detail := range details {
update, err := bs.createDetailUpdate(s, detail)
if err == nil {
updates = append(updates, update)
keys = append(keys, domain.RelationKey(update.key))
} else {
log.Errorf("can't set detail %s: %s", detail.Key, err)
}
}
return updates
return updates, keys
}
func applyDetailUpdates(oldDetails *types.Struct, updates []*detailUpdate) *types.Struct {
@ -342,7 +388,7 @@ func (bs *basic) SetObjectTypesInState(s *state.State, objectTypeKeys []domain.T
removeInternalFlags(s)
if pbtypes.GetInt64(bs.CombinedDetails(), bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
objecttype.UpdateLastUsedDate(bs.Space(), bs.objectStore, objectTypeKeys[0])
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), objectTypeKeys[0], time.Now().Unix())
}
toLayout, err := bs.getLayoutForType(objectTypeKeys[0])

View file

@ -32,7 +32,7 @@ func newDUFixture(t *testing.T) *duFixture {
store := objectstore.NewStoreFixture(t)
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil)
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil, nil)
return &duFixture{
sb: sb,

View file

@ -323,7 +323,7 @@ func TestExtractObjects(t *testing.T) {
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, tc.typeKey).Marshal(),
}
ctx := session.NewContext()
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
gotBlockIds := []string{}
@ -378,7 +378,7 @@ func TestExtractObjects(t *testing.T) {
}},
}
ctx := session.NewContext()
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
var block *model.Block
for _, block = range sb.Blocks() {
@ -411,7 +411,7 @@ func TestExtractObjects(t *testing.T) {
}},
}
ctx := session.NewContext()
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
var addedBlocks []*model.Block
for _, message := range sb.Results.Events {

View file

@ -33,7 +33,7 @@ type Dashboard struct {
func NewDashboard(sb smartblock.SmartBlock, objectStore objectstore.ObjectStore, layoutConverter converter.LayoutConverter) *Dashboard {
return &Dashboard{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil),
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil, nil),
Collection: collection.NewCollection(sb, objectStore),
objectStore: objectStore,
}

View file

@ -11,6 +11,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/bookmark"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/file"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/migration"
"github.com/anyproto/anytype-heart/core/block/process"
@ -66,6 +67,7 @@ type ObjectFactory struct {
fileReconciler reconciler.Reconciler
objectDeleter ObjectDeleter
deviceService deviceService
lastUsedUpdater lastused.ObjectUsageUpdater
}
func NewObjectFactory() *ObjectFactory {
@ -94,6 +96,7 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
f.objectDeleter = app.MustComponent[ObjectDeleter](a)
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
f.deviceService = app.MustComponent[deviceService](a)
f.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
return nil
}

View file

@ -25,7 +25,7 @@ var fileRequiredRelations = append(pageRequiredRelations, []domain.RelationKey{
}...)
func (f *ObjectFactory) newFile(sb smartblock.SmartBlock) *File {
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService)
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater)
return &File{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),

View file

@ -0,0 +1,216 @@
package lastused
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/cheggaaa/mb/v3"
"github.com/gogo/protobuf/types"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
CName = "object-usage-updater"
maxInstallationTime = 5 * time.Minute
updateInterval = 5 * time.Second
)
type Key interface {
URL() string
String() string
}
type message struct {
spaceId string
key Key
time int64
}
var log = logger.NewNamed("update-last-used-date")
type ObjectUsageUpdater interface {
app.ComponentRunnable
UpdateLastUsedDate(spaceId string, key Key, timeStamp int64)
}
func New() ObjectUsageUpdater {
return &updater{}
}
type updater struct {
store objectstore.ObjectStore
spaceService space.Service
ctx context.Context
cancel context.CancelFunc
msgBatch *mb.MB[message]
}
func (u *updater) Name() string {
return CName
}
func (u *updater) Init(a *app.App) error {
u.store = app.MustComponent[objectstore.ObjectStore](a)
u.spaceService = app.MustComponent[space.Service](a)
u.msgBatch = mb.New[message](0)
return nil
}
func (u *updater) Run(context.Context) error {
u.ctx, u.cancel = context.WithCancel(context.Background())
go u.lastUsedUpdateHandler()
return nil
}
func (u *updater) Close(context.Context) error {
if u.cancel != nil {
u.cancel()
}
if err := u.msgBatch.Close(); err != nil {
log.Error("failed to close message batch", zap.Error(err))
}
return nil
}
func (u *updater) UpdateLastUsedDate(spaceId string, key Key, ts int64) {
if err := u.msgBatch.Add(u.ctx, message{spaceId: spaceId, key: key, time: ts}); err != nil {
log.Error("failed to add last used date info to message batch", zap.Error(err), zap.String("key", key.String()))
}
}
func (u *updater) lastUsedUpdateHandler() {
var (
accumulator = make(map[string]map[Key]int64)
lock sync.Mutex
)
go func() {
for {
select {
case <-u.ctx.Done():
return
case <-time.After(updateInterval):
lock.Lock()
if len(accumulator) == 0 {
lock.Unlock()
continue
}
for spaceId, keys := range accumulator {
log.Debug("updating lastUsedDate for objects in space", zap.Int("objects num", len(keys)), zap.String("spaceId", spaceId))
u.updateLastUsedDateForKeysInSpace(spaceId, keys)
}
accumulator = make(map[string]map[Key]int64)
lock.Unlock()
}
}
}()
for {
msgs, err := u.msgBatch.Wait(u.ctx)
if err != nil {
return
}
lock.Lock()
for _, msg := range msgs {
if keys := accumulator[msg.spaceId]; keys != nil {
keys[msg.key] = msg.time
} else {
keys = map[Key]int64{
msg.key: msg.time,
}
accumulator[msg.spaceId] = keys
}
}
lock.Unlock()
}
}
func (u *updater) updateLastUsedDateForKeysInSpace(spaceId string, keys map[Key]int64) {
spc, err := u.spaceService.Get(u.ctx, spaceId)
if err != nil {
log.Error("failed to get space", zap.String("spaceId", spaceId), zap.Error(err))
return
}
for key, timeStamp := range keys {
if err = u.updateLastUsedDate(spc, key, timeStamp); err != nil {
log.Error("failed to update last used date", zap.String("spaceId", spaceId), zap.String("key", key.String()), zap.Error(err))
}
}
}
func (u *updater) updateLastUsedDate(spc clientspace.Space, key Key, ts int64) error {
uk, err := domain.UnmarshalUniqueKey(key.URL())
if err != nil {
return fmt.Errorf("failed to unmarshall key: %w", err)
}
if uk.SmartblockType() != coresb.SmartBlockTypeObjectType && uk.SmartblockType() != coresb.SmartBlockTypeRelation {
return fmt.Errorf("cannot update lastUsedDate for object with invalid smartBlock type. Only object types and relations are expected")
}
details, err := u.store.GetObjectByUniqueKey(spc.Id(), uk)
if err != nil {
return fmt.Errorf("failed to get details: %w", err)
}
id := pbtypes.GetString(details.Details, bundle.RelationKeyId.String())
if id == "" {
return fmt.Errorf("failed to get id from details: %w", err)
}
if err = spc.DoCtx(u.ctx, id, func(sb smartblock.SmartBlock) error {
st := sb.NewState()
st.SetLocalDetail(bundle.RelationKeyLastUsedDate.String(), pbtypes.Int64(ts))
return sb.Apply(st)
}); err != nil {
return fmt.Errorf("failed to set lastUsedDate to object: %w", err)
}
return nil
}
func SetLastUsedDateForInitialObjectType(id string, details *types.Struct) {
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil || details.Fields == nil {
return
}
var priority int64
switch id {
case bundle.TypeKeyNote.BundledURL():
priority = 1
case bundle.TypeKeyPage.BundledURL():
priority = 2
case bundle.TypeKeyTask.BundledURL():
priority = 3
case bundle.TypeKeySet.BundledURL():
priority = 4
case bundle.TypeKeyCollection.BundledURL():
priority = 5
default:
priority = 7
}
// we do this trick to order crucial Anytype object types by last date
lastUsed := time.Now().Add(time.Duration(-1 * priority * int64(maxInstallationTime))).Unix()
details.Fields[bundle.RelationKeyLastUsedDate.String()] = pbtypes.Int64(lastUsed)
}

View file

@ -0,0 +1,139 @@
package lastused
import (
"context"
"math/rand"
"testing"
"time"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestSetLastUsedDateForInitialType(t *testing.T) {
isLastUsedDateGreater := func(details1, details2 *types.Struct) bool {
return pbtypes.GetInt64(details1, bundle.RelationKeyLastUsedDate.String()) > pbtypes.GetInt64(details2, bundle.RelationKeyLastUsedDate.String())
}
t.Run("object types are sorted by lastUsedDate in correct order", func(t *testing.T) {
// given
ots := []string{
bundle.TypeKeySet.BundledURL(),
bundle.TypeKeyNote.BundledURL(),
bundle.TypeKeyCollection.BundledURL(),
bundle.TypeKeyTask.BundledURL(),
bundle.TypeKeyPage.BundledURL(),
bundle.TypeKeyDiaryEntry.BundledURL(),
bundle.TypeKeyAudio.BundledURL(),
}
rand.Shuffle(len(ots), func(i, j int) {
ots[i], ots[j] = ots[j], ots[i]
})
detailMap := map[string]*types.Struct{}
// when
for _, id := range ots {
details := &types.Struct{Fields: make(map[string]*types.Value)}
SetLastUsedDateForInitialObjectType(id, details)
detailMap[id] = details
}
// then
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyNote.BundledURL()], detailMap[bundle.TypeKeyPage.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyPage.BundledURL()], detailMap[bundle.TypeKeyTask.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyTask.BundledURL()], detailMap[bundle.TypeKeySet.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeySet.BundledURL()], detailMap[bundle.TypeKeyCollection.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyAudio.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyDiaryEntry.BundledURL()]))
})
}
func TestUpdateLastUsedDate(t *testing.T) {
const spaceId = "space"
ts := time.Now().Unix()
isLastUsedDateRecent := func(details *types.Struct, deltaSeconds int64) bool {
return pbtypes.GetInt64(details, bundle.RelationKeyLastUsedDate.String())+deltaSeconds > time.Now().Unix()
}
store := objectstore.NewStoreFixture(t)
store.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String(bundle.RelationKeyCamera.URL()),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.RelationKeyCamera.URL()),
},
{
bundle.RelationKeyId: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
},
{
bundle.RelationKeyId: pbtypes.String("rel-custom"),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String("rel-custom"),
},
{
bundle.RelationKeyId: pbtypes.String("opt-done"),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String("opt-done"),
},
})
u := updater{store: store}
getSpace := func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, id string, apply func(smartblock.SmartBlock) error) error {
sb := smarttest.New(id)
err := apply(sb)
require.NoError(t, err)
assert.True(t, isLastUsedDateRecent(sb.LocalDetails(), 5))
return nil
})
return spc
}
for _, tc := range []struct {
name string
key Key
getSpace func() clientspace.Space
isErrorExpected bool
}{
{"built-in relation", bundle.RelationKeyCamera, getSpace, false},
{"built-in type", bundle.TypeKeyDiaryEntry, getSpace, false},
{"custom relation", domain.RelationKey("custom"), getSpace, false},
{"option", domain.TypeKey("opt-done"), func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
return spc
}, true},
{"type that is not in store", bundle.TypeKeyAudio, func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
return spc
}, true},
} {
t.Run("update lastUsedDate of "+tc.name, func(t *testing.T) {
err := u.updateLastUsedDate(tc.getSpace(), tc.key, ts)
if tc.isErrorExpected {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View file

@ -1,71 +0,0 @@
package objecttype
import (
"strings"
"time"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const maxInstallationTime = 5 * time.Minute
var log = logging.Logger("update-last-used-date")
func UpdateLastUsedDate(spc smartblock.Space, store objectstore.ObjectStore, key domain.TypeKey) {
uk, err := domain.UnmarshalUniqueKey(key.URL())
if err != nil {
log.Errorf("failed to unmarshall type key '%s': %w", key.String(), err)
return
}
details, err := store.GetObjectByUniqueKey(spc.Id(), uk)
if err != nil {
log.Errorf("failed to get details of type object '%s': %w", key.String(), err)
return
}
id := pbtypes.GetString(details.Details, bundle.RelationKeyId.String())
if id == "" {
log.Errorf("failed to get id from details of type object '%s': %w", key.String(), err)
return
}
if err = spc.Do(id, func(sb smartblock.SmartBlock) error {
st := sb.NewState()
st.SetLocalDetail(bundle.RelationKeyLastUsedDate.String(), pbtypes.Int64(time.Now().Unix()))
return sb.Apply(st)
}); err != nil {
log.Errorf("failed to set lastUsedDate to type object '%s': %w", key.String(), err)
}
}
func SetLastUsedDateForInitialObjectType(id string, details *types.Struct) {
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil || details.Fields == nil {
return
}
var priority int64
switch id {
case bundle.TypeKeyNote.BundledURL():
priority = 1
case bundle.TypeKeyPage.BundledURL():
priority = 2
case bundle.TypeKeyTask.BundledURL():
priority = 3
case bundle.TypeKeySet.BundledURL():
priority = 4
case bundle.TypeKeyCollection.BundledURL():
priority = 5
default:
priority = 7
}
// we do this trick to order crucial Anytype object types by last date
lastUsed := time.Now().Add(time.Duration(-1 * priority * int64(maxInstallationTime))).Unix()
details.Fields[bundle.RelationKeyLastUsedDate.String()] = pbtypes.Int64(lastUsed)
}

View file

@ -1,50 +0,0 @@
package objecttype
import (
"math/rand"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestSetLastUsedDateForInitialType(t *testing.T) {
isLastUsedDateGreater := func(details1, details2 *types.Struct) bool {
return pbtypes.GetInt64(details1, bundle.RelationKeyLastUsedDate.String()) > pbtypes.GetInt64(details2, bundle.RelationKeyLastUsedDate.String())
}
t.Run("object types are sorted by lastUsedDate in correct order", func(t *testing.T) {
// given
ots := []string{
bundle.TypeKeySet.BundledURL(),
bundle.TypeKeyNote.BundledURL(),
bundle.TypeKeyCollection.BundledURL(),
bundle.TypeKeyTask.BundledURL(),
bundle.TypeKeyPage.BundledURL(),
bundle.TypeKeyGoal.BundledURL(),
bundle.TypeKeyAudio.BundledURL(),
}
rand.Shuffle(len(ots), func(i, j int) {
ots[i], ots[j] = ots[j], ots[i]
})
detailMap := map[string]*types.Struct{}
// when
for _, id := range ots {
details := &types.Struct{Fields: make(map[string]*types.Value)}
SetLastUsedDateForInitialObjectType(id, details)
detailMap[id] = details
}
// then
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyNote.BundledURL()], detailMap[bundle.TypeKeyPage.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyPage.BundledURL()], detailMap[bundle.TypeKeyTask.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyTask.BundledURL()], detailMap[bundle.TypeKeySet.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeySet.BundledURL()], detailMap[bundle.TypeKeyCollection.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyAudio.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyGoal.BundledURL()]))
})
}

View file

@ -58,7 +58,7 @@ func (f *ObjectFactory) newPage(sb smartblock.SmartBlock) *Page {
return &Page{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -31,7 +31,7 @@ type participant struct {
}
func (f *ObjectFactory) newParticipant(sb smartblock.SmartBlock) *participant {
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, nil)
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, nil, f.lastUsedUpdater)
return &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,

View file

@ -135,7 +135,7 @@ func newStoreFixture(t *testing.T) *objectstore.StoreFixture {
func newParticipantTest(t *testing.T) (*participant, error) {
sb := smarttest.New("root")
store := newStoreFixture(t)
basicComponent := basic.NewBasic(sb, store, nil, nil)
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
p := &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,

View file

@ -40,7 +40,7 @@ func (f *ObjectFactory) newProfile(sb smartblock.SmartBlock) *Profile {
fileComponent := file.NewFile(sb, f.fileBlockService, f.picker, f.processService, f.fileUploaderService)
return &Profile{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -30,7 +30,7 @@ func NewWidgetObject(
objectStore objectstore.ObjectStore,
layoutConverter converter.LayoutConverter,
) *WidgetObject {
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil)
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil, nil)
return &WidgetObject{
SmartBlock: sb,
Movable: bs,

View file

@ -35,7 +35,7 @@ type Workspaces struct {
func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock) *Workspaces {
w := &Workspaces{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -8,6 +8,7 @@ import (
"github.com/gogo/protobuf/types"
"github.com/pkg/errors"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/domain"
@ -59,6 +60,7 @@ type service struct {
bookmarkService bookmarkService
spaceService space.Service
templateService templateService
lastUsedUpdater lastused.ObjectUsageUpdater
}
func NewCreator() Service {
@ -71,6 +73,7 @@ func (s *service) Init(a *app.App) (err error) {
s.collectionService = app.MustComponent[collectionService](a)
s.spaceService = app.MustComponent[space.Service](a)
s.templateService = app.MustComponent[templateService](a)
s.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
return nil
}

View file

@ -10,7 +10,7 @@ import (
"github.com/gogo/protobuf/types"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -226,7 +226,7 @@ func (s *service) prepareDetailsForInstallingObject(
details.Fields[bundle.RelationKeyIsReadonly.String()] = pbtypes.Bool(false)
if isNewSpace {
objecttype.SetLastUsedDateForInitialObjectType(sourceId, details)
lastused.SetLastUsedDateForInitialObjectType(sourceId, details)
}
bundledRelationIds := pbtypes.GetStringList(details, bundle.RelationKeyRecommendedRelations.String())

View file

@ -9,7 +9,6 @@ import (
"github.com/gogo/protobuf/types"
"golang.org/x/exp/slices"
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/object/objectcache"
@ -103,13 +102,7 @@ func (s *service) CreateSmartBlockFromStateInSpaceWithOptions(
sb.Unlock()
id = sb.Id()
if sbType == coresb.SmartBlockTypeObjectType && pbtypes.GetInt64(newDetails, bundle.RelationKeyLastUsedDate.String()) == 0 {
objecttype.UpdateLastUsedDate(spc, s.objectStore, domain.TypeKey(
strings.TrimPrefix(pbtypes.GetString(newDetails, bundle.RelationKeyUniqueKey.String()), addr.ObjectTypeKeyToIdPrefix)),
)
} else if pbtypes.GetInt64(newDetails, bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
objecttype.UpdateLastUsedDate(spc, s.objectStore, objectTypeKeys[0])
}
s.updateLastUsedDate(spc.Id(), sbType, newDetails, objectTypeKeys[0])
ev.SmartblockCreateMs = time.Since(startTime).Milliseconds() - ev.SetDetailsMs - ev.WorkspaceCreateMs - ev.GetWorkspaceBlockWaitMs
ev.SmartblockType = int(sbType)
@ -118,6 +111,24 @@ func (s *service) CreateSmartBlockFromStateInSpaceWithOptions(
return id, newDetails, nil
}
func (s *service) updateLastUsedDate(spaceId string, sbType coresb.SmartBlockType, details *types.Struct, typeKey domain.TypeKey) {
if pbtypes.GetInt64(details, bundle.RelationKeyLastUsedDate.String()) != 0 {
return
}
uk := pbtypes.GetString(details, bundle.RelationKeyUniqueKey.String())
ts := time.Now().Unix()
switch sbType {
case coresb.SmartBlockTypeObjectType:
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, domain.TypeKey(strings.TrimPrefix(uk, addr.ObjectTypeKeyToIdPrefix)), ts)
case coresb.SmartBlockTypeRelation:
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, domain.RelationKey(strings.TrimPrefix(uk, addr.RelationKeyToIdPrefix)), ts)
default:
if pbtypes.GetInt64(details, bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, typeKey, ts)
}
}
}
func objectTypeKeysToSmartBlockType(typeKeys []domain.TypeKey) coresb.SmartBlockType {
// TODO Add validation for types that user can't create

View file

@ -44,7 +44,7 @@ func (mw *Middleware) ObjectSetDetails(cctx context.Context, req *pb.RpcObjectSe
return m
}
err := mw.doBlockService(func(bs *block.Service) (err error) {
return bs.SetDetails(ctx, req.ContextId, req.GetDetails())
return bs.SetDetailsAndUpdateLastUsed(ctx, req.ContextId, req.GetDetails())
})
if err != nil {
return response(pb.RpcObjectSetDetailsResponseError_UNKNOWN_ERROR, err)