1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-08 05:47:07 +09:00

GO-5297 Merge branch 'main' of github.com:anyproto/anytype-heart into go-5297-push-service-1st-iteration

This commit is contained in:
AnastasiaShemyakinskaya 2025-04-07 14:34:32 +02:00
commit 49680f375e
No known key found for this signature in database
GPG key ID: CCD60ED83B103281
44 changed files with 5150 additions and 4701 deletions

View file

@ -116,6 +116,10 @@ func validateDetails(s *pb.SnapshotWithType, info *useCaseInfo) (err error) {
continue
}
if k == bundle.RelationKeyAutoWidgetTargets.String() && val == "bin" {
continue
}
_, found := info.objects[val]
if !found {
if isBrokenTemplate(k, val) {

View file

@ -32,15 +32,15 @@ const CName = "core.block.chats"
var log = logging.Logger(CName).Desugar()
type Service interface {
AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *model.ChatMessage) (string, error)
EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *model.ChatMessage) error
AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatobject.Message) (string, error)
EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatobject.Message) error
ToggleMessageReaction(ctx context.Context, chatObjectId string, messageId string, emoji string) error
DeleteMessage(ctx context.Context, chatObjectId string, messageId string) error
GetMessages(ctx context.Context, chatObjectId string, req chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*model.ChatMessage, error)
GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatobject.Message, error)
SubscribeLastMessages(ctx context.Context, chatObjectId string, limit int, subId string) (*chatobject.SubscribeLastMessagesResponse, error)
ReadMessages(ctx context.Context, chatObjectId string, afterOrderId string, beforeOrderId string, lastDbState int64) error
UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string) error
ReadMessages(ctx context.Context, req ReadMessagesRequest) error
UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatobject.CounterType) error
Unsubscribe(chatObjectId string, subId string) error
SubscribeToMessagePreviews(ctx context.Context) (string, error)
@ -231,13 +231,14 @@ func (s *service) Close(ctx context.Context) error {
s.componentCtxCancel()
err = errors.Join(err,
s.crossSpaceSubService.Unsubscribe(allChatsSubscriptionId),
)
unsubErr := s.crossSpaceSubService.Unsubscribe(allChatsSubscriptionId)
if !errors.Is(err, crossspacesub.ErrSubscriptionNotFound) {
err = errors.Join(err, unsubErr)
}
return err
}
func (s *service) AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *model.ChatMessage) (string, error) {
func (s *service) AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatobject.Message) (string, error) {
var messageId, spaceId string
err := cache.Do(s.objectGetter, chatObjectId, func(sb chatobject.StoreObject) error {
var err error
@ -261,7 +262,7 @@ func (s *service) sendPushNotification(spaceId, chatObjectId string, message *mo
}
}
func (s *service) EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *model.ChatMessage) error {
func (s *service) EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatobject.Message) error {
return cache.Do(s.objectGetter, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.EditMessage(ctx, messageId, newMessage)
})
@ -292,8 +293,8 @@ func (s *service) GetMessages(ctx context.Context, chatObjectId string, req chat
return resp, err
}
func (s *service) GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*model.ChatMessage, error) {
var res []*model.ChatMessage
func (s *service) GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatobject.Message, error) {
var res []*chatobject.Message
err := cache.Do(s.objectGetter, chatObjectId, func(sb chatobject.StoreObject) error {
msg, err := sb.GetMessagesByIds(ctx, messageIds)
if err != nil {
@ -324,14 +325,22 @@ func (s *service) Unsubscribe(chatObjectId string, subId string) error {
})
}
func (s *service) ReadMessages(ctx context.Context, chatObjectId string, afterOrderId string, beforeOrderId string, lastAddedMessageTimestamp int64) error {
return cache.Do(s.objectGetter, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.MarkReadMessages(ctx, afterOrderId, beforeOrderId, lastAddedMessageTimestamp)
type ReadMessagesRequest struct {
ChatObjectId string
AfterOrderId string
BeforeOrderId string
LastStateId string
CounterType chatobject.CounterType
}
func (s *service) ReadMessages(ctx context.Context, req ReadMessagesRequest) error {
return cache.Do(s.objectGetter, req.ChatObjectId, func(sb chatobject.StoreObject) error {
return sb.MarkReadMessages(ctx, req.AfterOrderId, req.BeforeOrderId, req.LastStateId, req.CounterType)
})
}
func (s *service) UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string) error {
func (s *service) UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatobject.CounterType) error {
return cache.Do(s.objectGetter, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.MarkMessagesAsUnread(ctx, afterOrderId)
return sb.MarkMessagesAsUnread(ctx, afterOrderId, counterType)
})
}

View file

@ -66,7 +66,7 @@ func (s *Service) CreateWorkspace(ctx context.Context, req *pb.RpcWorkspaceCreat
if err != nil {
return "", fmt.Errorf("set details for space %s: %w", newSpace.Id(), err)
}
_, err = s.builtinObjectService.CreateObjectsForUseCase(nil, newSpace.Id(), req.UseCase)
_, _, err = s.builtinObjectService.CreateObjectsForUseCase(nil, newSpace.Id(), req.UseCase)
if err != nil {
return "", fmt.Errorf("import use-case: %w", err)
}

View file

@ -9,26 +9,27 @@ import (
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/globalsign/mgo/bson"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/util/timeid"
)
type ChatHandler struct {
collection anystore.Collection
repository *repository
subscription *subscription
currentIdentity string
myParticipantId string
// forceNotRead forces handler to mark all messages as not read. It's useful for unit testing
forceNotRead bool
}
func (d *ChatHandler) CollectionName() string {
return collectionName
return CollectionName
}
func (d *ChatHandler) Init(ctx context.Context, s *storestate.StoreState) (err error) {
coll, err := s.Collection(ctx, collectionName)
coll, err := s.Collection(ctx, CollectionName)
if err != nil {
return err
}
@ -38,35 +39,48 @@ func (d *ChatHandler) Init(ctx context.Context, s *storestate.StoreState) (err e
if iErr != nil && !errors.Is(iErr, anystore.ErrIndexExists) {
return iErr
}
d.collection = coll
return
}
func (d *ChatHandler) BeforeCreate(ctx context.Context, ch storestate.ChangeOp) (err error) {
msg := newMessageWrapper(ch.Arena, ch.Value)
msg.setCreatedAt(ch.Change.Timestamp)
msg.setCreator(ch.Change.Creator)
func (d *ChatHandler) BeforeCreate(ctx context.Context, ch storestate.ChangeOp) error {
msg, err := unmarshalMessage(ch.Value)
if err != nil {
return fmt.Errorf("unmarshal message: %w", err)
}
msg.CreatedAt = ch.Change.Timestamp
msg.Creator = ch.Change.Creator
if d.forceNotRead {
msg.setRead(false)
msg.Read = false
msg.MentionRead = false
} else {
if ch.Change.Creator == d.currentIdentity {
msg.setRead(true)
msg.Read = true
msg.MentionRead = true
} else {
msg.setRead(false)
msg.Read = false
msg.MentionRead = false
}
}
msg.setAddedAt(timeid.NewNano())
model := msg.toModel()
model.OrderId = ch.Change.Order
msg.StateId = bson.NewObjectId().Hex()
prevOrderId, err := getPrevOrderId(ctx, d.collection, ch.Change.Order)
isMentioned, err := msg.IsCurrentUserMentioned(ctx, d.myParticipantId, d.currentIdentity, d.repository)
if err != nil {
return fmt.Errorf("check if current user is mentioned: %w", err)
}
msg.CurrentUserMentioned = isMentioned
msg.OrderId = ch.Change.Order
prevOrderId, err := d.repository.getPrevOrderId(ctx, ch.Change.Order)
if err != nil {
return fmt.Errorf("get prev order id: %w", err)
}
d.subscription.add(prevOrderId, model)
return
d.subscription.add(ctx, prevOrderId, msg)
msg.MarshalAnyenc(ch.Value, ch.Arena)
return nil
}
func (d *ChatHandler) BeforeModify(ctx context.Context, ch storestate.ChangeOp) (mode storestate.ModifyMode, err error) {
@ -74,7 +88,7 @@ func (d *ChatHandler) BeforeModify(ctx context.Context, ch storestate.ChangeOp)
}
func (d *ChatHandler) BeforeDelete(ctx context.Context, ch storestate.ChangeOp) (mode storestate.DeleteMode, err error) {
coll, err := ch.State.Collection(ctx, collectionName)
coll, err := ch.State.Collection(ctx, CollectionName)
if err != nil {
return storestate.DeleteModeDelete, fmt.Errorf("get collection: %w", err)
}
@ -86,12 +100,16 @@ func (d *ChatHandler) BeforeDelete(ctx context.Context, ch storestate.ChangeOp)
return storestate.DeleteModeDelete, fmt.Errorf("get message: %w", err)
}
message := newMessageWrapper(ch.Arena, doc.Value())
if message.getCreator() != ch.Change.Creator {
message, err := unmarshalMessage(doc.Value())
if err != nil {
return storestate.DeleteModeDelete, fmt.Errorf("unmarshal message: %w", err)
}
if message.Creator != ch.Change.Creator {
return storestate.DeleteModeDelete, errors.New("can't delete not own message")
}
d.subscription.delete(messageId)
return storestate.DeleteModeDelete, nil
}
@ -109,8 +127,10 @@ func (d *ChatHandler) UpgradeKeyModifier(ch storestate.ChangeOp, key *pb.KeyModi
}
if modified {
msg := newMessageWrapper(a, result)
model := msg.toModel()
msg, err := unmarshalMessage(result)
if err != nil {
return nil, false, fmt.Errorf("unmarshal message: %w", err)
}
switch path {
case reactionsKey:
@ -121,15 +141,15 @@ func (d *ChatHandler) UpgradeKeyModifier(ch storestate.ChangeOp, key *pb.KeyModi
}
// TODO Count validation
d.subscription.updateReactions(model)
d.subscription.updateReactions(msg)
case contentKey:
creator := model.Creator
creator := msg.Creator
if creator != ch.Change.Creator {
return v, false, errors.Join(storestate.ErrValidation, fmt.Errorf("can't modify someone else's message"))
}
result.Set(modifiedAtKey, a.NewNumberInt(int(ch.Change.Timestamp)))
model.ModifiedAt = ch.Change.Timestamp
d.subscription.updateFull(model)
msg.ModifiedAt = ch.Change.Timestamp
msg.MarshalAnyenc(result, a)
d.subscription.updateFull(msg)
default:
return nil, false, fmt.Errorf("invalid key path %s", key.KeyPath)
}

View file

@ -2,14 +2,11 @@ package chatobject
import (
"context"
"errors"
"fmt"
"sort"
"time"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
"github.com/anyproto/any-sync/util/slice"
"go.uber.org/zap"
@ -19,6 +16,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
@ -28,10 +26,12 @@ import (
)
const (
collectionName = "chats"
descOrder = "-_o.id"
ascOrder = "_o.id"
descAdded = "-a"
CollectionName = "chats"
descOrder = "-_o.id"
ascOrder = "_o.id"
descStateId = "-stateId"
diffManagerMessages = "messages"
diffManagerMentions = "mentions"
)
var log = logging.Logger("core.block.editor.chatobject").Desugar()
@ -40,15 +40,15 @@ type StoreObject interface {
smartblock.SmartBlock
anystoredebug.AnystoreDebug
AddMessage(ctx context.Context, sessionCtx session.Context, message *model.ChatMessage) (string, error)
AddMessage(ctx context.Context, sessionCtx session.Context, message *Message) (string, error)
GetMessages(ctx context.Context, req GetMessagesRequest) (*GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*model.ChatMessage, error)
EditMessage(ctx context.Context, messageId string, newMessage *model.ChatMessage) error
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error)
EditMessage(ctx context.Context, messageId string, newMessage *Message) error
ToggleMessageReaction(ctx context.Context, messageId string, emoji string) error
DeleteMessage(ctx context.Context, messageId string) error
SubscribeLastMessages(ctx context.Context, subId string, limit int, asyncInit bool) (*SubscribeLastMessagesResponse, error)
MarkReadMessages(ctx context.Context, afterOrderId string, beforeOrderId string, lastAddedMessageTimestamp int64) error
MarkMessagesAsUnread(ctx context.Context, afterOrderId string) error
MarkReadMessages(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType CounterType) error
MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType CounterType) error
Unsubscribe(subId string) error
}
@ -73,7 +73,6 @@ type storeObject struct {
locker smartblock.Locker
seenHeadsCollector seenHeadsCollector
collection anystore.Collection
accountService AccountService
storeSource source.Store
store *storestate.StoreState
@ -82,6 +81,7 @@ type storeObject struct {
crdtDb anystore.DB
spaceIndex spaceindex.Store
chatHandler *ChatHandler
repository *repository
arenaPool *anyenc.ArenaPool
componentCtx context.Context
@ -108,18 +108,52 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
if !ok {
return fmt.Errorf("source is not a store")
}
storeSource.SetDiffManagerOnRemoveHook(s.markReadMessages)
err := s.SmartBlock.Init(ctx)
collection, err := s.crdtDb.Collection(ctx.Ctx, storeSource.Id()+CollectionName)
if err != nil {
return fmt.Errorf("get collection: %w", err)
}
s.repository = &repository{
collection: collection,
arenaPool: s.arenaPool,
}
// Use Object and Space IDs from source, because object is not initialized yet
myParticipantId := domain.NewParticipantId(ctx.Source.SpaceID(), s.accountService.AccountID())
s.subscription = s.newSubscription(
domain.FullID{ObjectID: ctx.Source.Id(), SpaceID: ctx.Source.SpaceID()},
s.accountService.AccountID(),
myParticipantId,
)
messagesOpts := newReadHandler(CounterTypeMessage, s.subscription)
mentionsOpts := newReadHandler(CounterTypeMention, s.subscription)
// Diff managers should be added before SmartBlock.Init, because they have to be initialized in source.ReadStoreDoc
storeSource.RegisterDiffManager(diffManagerMessages, func(removed []string) {
markErr := s.markReadMessages(removed, messagesOpts)
if markErr != nil {
log.Error("mark read messages", zap.Error(markErr))
}
})
storeSource.RegisterDiffManager(diffManagerMentions, func(removed []string) {
markErr := s.markReadMessages(removed, mentionsOpts)
if markErr != nil {
log.Error("mark read mentions", zap.Error(markErr))
}
})
err = s.SmartBlock.Init(ctx)
if err != nil {
return err
}
s.storeSource = storeSource
s.subscription = newSubscription(s.SpaceID(), s.Id(), s.eventSender, s.spaceIndex)
s.chatHandler = &ChatHandler{
repository: s.repository,
subscription: s.subscription,
currentIdentity: s.accountService.AccountID(),
myParticipantId: myParticipantId,
}
stateStore, err := storestate.New(ctx.Ctx, s.Id(), s.crdtDb, s.chatHandler)
@ -127,12 +161,8 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
return fmt.Errorf("create state store: %w", err)
}
s.store = stateStore
s.collection, err = s.store.Collection(s.componentCtx, collectionName)
if err != nil {
return fmt.Errorf("get s.collection.ction: %w", err)
}
s.subscription.chatState, err = s.initialChatState()
err = s.subscription.loadChatState(s.componentCtx)
if err != nil {
return fmt.Errorf("init chat state: %w", err)
}
@ -153,353 +183,50 @@ func (s *storeObject) onUpdate() {
s.subscription.flush()
}
// initialChatState returns the initial chat state for the chat object from the DB
func (s *storeObject) initialChatState() (*model.ChatState, error) {
txn, err := s.collection.ReadTx(s.componentCtx)
func (s *storeObject) GetMessageById(ctx context.Context, id string) (*Message, error) {
messages, err := s.GetMessagesByIds(ctx, []string{id})
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
return nil, err
}
defer txn.Commit()
oldestOrderId, err := s.getOldestOrderId(txn)
if err != nil {
return nil, fmt.Errorf("get oldest order id: %w", err)
if len(messages) == 0 {
return nil, fmt.Errorf("message not found")
}
count, err := s.countUnreadMessages(txn)
if err != nil {
return nil, fmt.Errorf("update messages: %w", err)
}
lastAdded, err := s.getLastAddedDate(txn)
if err != nil {
return nil, fmt.Errorf("get last added date: %w", err)
}
return &model.ChatState{
Messages: &model.ChatStateUnreadState{
OldestOrderId: oldestOrderId,
Counter: int32(count),
},
// todo: add replies counter
DbTimestamp: int64(lastAdded),
}, nil
return messages[0], nil
}
func (s *storeObject) getOldestOrderId(txn anystore.ReadTx) (string, error) {
unreadQuery := s.collection.Find(unreadFilter()).Sort(ascOrder)
iter, err := unreadQuery.Limit(1).Iter(txn.Context())
if err != nil {
return "", fmt.Errorf("init iter: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
return doc.Value().GetObject(orderKey).Get("id").GetString(), nil
}
return "", nil
}
func (s *storeObject) countUnreadMessages(txn anystore.ReadTx) (int, error) {
unreadQuery := s.collection.Find(unreadFilter())
return unreadQuery.Limit(1).Count(txn.Context())
}
func unreadFilter() query.Filter {
// Use Not because old messages don't have read key
return query.Not{
Filter: query.Key{Path: []string{readKey}, Filter: query.NewComp(query.CompOpEq, true)},
}
}
func (s *storeObject) getLastAddedDate(txn anystore.ReadTx) (int, error) {
lastAddedDate := s.collection.Find(nil).Sort(descAdded).Limit(1)
iter, err := lastAddedDate.Iter(txn.Context())
if err != nil {
return 0, fmt.Errorf("find last added date: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return 0, fmt.Errorf("get doc: %w", err)
}
return doc.Value().GetInt(addedKey), nil
}
return 0, nil
}
func (s *storeObject) markReadMessages(changeIds []string) {
if len(changeIds) == 0 {
return
}
txn, err := s.collection.WriteTx(s.componentCtx)
if err != nil {
log.With(zap.Error(err)).Error("markReadMessages: start write tx")
return
}
defer txn.Commit()
var idsModified []string
for _, id := range changeIds {
if id == s.Id() {
// skip tree root
continue
}
res, err := s.collection.UpdateId(txn.Context(), id, query.MustParseModifier(`{"$set":{"`+readKey+`":true}}`))
// Not all changes are messages, skip them
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
log.Error("markReadMessages: update message", zap.Error(err), zap.String("changeId", id), zap.String("chatObjectId", s.Id()))
continue
}
if res.Modified > 0 {
idsModified = append(idsModified, id)
}
}
if len(idsModified) > 0 {
newOldestOrderId, err := s.getOldestOrderId(txn)
if err != nil {
log.Error("markReadMessages: get oldest order id", zap.Error(err))
err = txn.Rollback()
if err != nil {
log.Error("markReadMessages: rollback transaction", zap.Error(err))
}
}
s.subscription.updateChatState(func(state *model.ChatState) {
state.Messages.OldestOrderId = newOldestOrderId
})
s.subscription.updateReadStatus(idsModified, true)
s.subscription.flush()
}
}
func (s *storeObject) MarkReadMessages(ctx context.Context, afterOrderId, beforeOrderId string, lastAddedMessageTimestamp int64) error {
// 1. select all messages with orderId < beforeOrderId and addedTime < lastDbState
// 2. use the last(by orderId) message id as lastHead
// 3. update the MarkSeenHeads
// 2. mark messages as read in the DB
msgs, err := s.getUnreadMessageIdsInRange(ctx, afterOrderId, beforeOrderId, lastAddedMessageTimestamp)
if err != nil {
return fmt.Errorf("get message: %w", err)
}
// mark the whole tree as seen from the current message
return s.storeSource.MarkSeenHeads(ctx, msgs)
}
func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string) error {
txn, err := s.collection.WriteTx(ctx)
if err != nil {
return fmt.Errorf("create tx: %w", err)
}
defer txn.Rollback()
msgs, err := s.getReadMessagesAfter(txn, afterOrderId)
if err != nil {
return fmt.Errorf("get read messages: %w", err)
}
if len(msgs) == 0 {
return nil
}
for _, msgId := range msgs {
_, err := s.collection.UpdateId(txn.Context(), msgId, query.MustParseModifier(`{"$set":{"`+readKey+`":false}}`))
if err != nil {
return fmt.Errorf("update message: %w", err)
}
}
newOldestOrderId, err := s.getOldestOrderId(txn)
if err != nil {
return fmt.Errorf("get oldest order id: %w", err)
}
lastAdded, err := s.getLastAddedDate(txn)
if err != nil {
return fmt.Errorf("get last added date: %w", err)
}
s.subscription.updateChatState(func(state *model.ChatState) {
state.Messages.OldestOrderId = newOldestOrderId
state.DbTimestamp = int64(lastAdded)
})
s.subscription.updateReadStatus(msgs, false)
s.subscription.flush()
seenHeads, err := s.seenHeadsCollector.collectSeenHeads(ctx, afterOrderId)
if err != nil {
return fmt.Errorf("get seen heads: %w", err)
}
err = s.storeSource.InitDiffManager(ctx, seenHeads)
if err != nil {
return fmt.Errorf("init diff manager: %w", err)
}
err = s.storeSource.StoreSeenHeads(txn.Context())
if err != nil {
return fmt.Errorf("store seen heads: %w", err)
}
return txn.Commit()
}
func (s *storeObject) getReadMessagesAfter(txn anystore.ReadTx, afterOrderId string) ([]string, error) {
iter, err := s.collection.Find(query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{readKey}, Filter: query.NewComp(query.CompOpEq, true)},
}).Iter(txn.Context())
if err != nil {
return nil, fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *storeObject) getUnreadMessageIdsInRange(ctx context.Context, afterOrderId, beforeOrderId string, lastAddedMessageTimestamp int64) ([]string, error) {
iter, err := s.collection.Find(
query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLte, beforeOrderId)},
query.Or{
query.Not{query.Key{Path: []string{addedKey}, Filter: query.Exists{}}},
query.Key{Path: []string{addedKey}, Filter: query.NewComp(query.CompOpLte, lastAddedMessageTimestamp)},
},
unreadFilter(),
},
).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find id: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*model.ChatMessage, error) {
txn, err := s.collection.ReadTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
messages := make([]*model.ChatMessage, 0, len(messageIds))
for _, messageId := range messageIds {
obj, err := s.collection.FindId(txn.Context(), messageId)
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("find id: %w", err))
}
msg := newMessageWrapper(nil, obj.Value())
messages = append(messages, msg.toModel())
}
return messages, txn.Commit()
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error) {
return s.repository.getMessagesByIds(ctx, messageIds)
}
type GetMessagesResponse struct {
Messages []*model.ChatMessage
Messages []*Message
ChatState *model.ChatState
}
func (s *storeObject) GetMessages(ctx context.Context, req GetMessagesRequest) (*GetMessagesResponse, error) {
var qry anystore.Query
if req.AfterOrderId != "" {
operator := query.CompOpGt
if req.IncludeBoundary {
operator = query.CompOpGte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.AfterOrderId)}).Sort(ascOrder).Limit(uint(req.Limit))
} else if req.BeforeOrderId != "" {
operator := query.CompOpLt
if req.IncludeBoundary {
operator = query.CompOpLte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.BeforeOrderId)}).Sort(descOrder).Limit(uint(req.Limit))
} else {
qry = s.collection.Find(nil).Sort(descOrder).Limit(uint(req.Limit))
}
msgs, err := s.queryMessages(ctx, qry)
msgs, err := s.repository.getMessages(ctx, req)
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
return nil, err
}
sort.Slice(msgs, func(i, j int) bool {
return msgs[i].OrderId < msgs[j].OrderId
})
return &GetMessagesResponse{
Messages: msgs,
ChatState: s.subscription.getChatState(),
}, nil
}
func (s *storeObject) queryMessages(ctx context.Context, query anystore.Query) ([]*model.ChatMessage, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
iter, err := query.Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find iter: %w", err)
}
defer iter.Close()
var res []*model.ChatMessage
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
message := newMessageWrapper(arena, doc.Value()).toModel()
res = append(res, message)
}
return res, nil
}
func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *model.ChatMessage) (string, error) {
func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *Message) (string, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
message.Read = true
obj := marshalModel(arena, message)
obj := arena.NewObject()
message.MarshalAnyenc(obj, arena)
builder := storestate.Builder{}
err := builder.Create(collectionName, storestate.IdFromChange, obj)
err := builder.Create(CollectionName, storestate.IdFromChange, obj)
if err != nil {
return "", fmt.Errorf("create chat: %w", err)
}
@ -518,7 +245,7 @@ func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context
func (s *storeObject) DeleteMessage(ctx context.Context, messageId string) error {
builder := storestate.Builder{}
builder.Delete(collectionName, messageId)
builder.Delete(CollectionName, messageId)
_, err := s.storeSource.PushStoreChange(ctx, source.PushStoreChangeParams{
Changes: builder.ChangeSet,
State: s.store,
@ -530,16 +257,18 @@ func (s *storeObject) DeleteMessage(ctx context.Context, messageId string) error
return nil
}
func (s *storeObject) EditMessage(ctx context.Context, messageId string, newMessage *model.ChatMessage) error {
func (s *storeObject) EditMessage(ctx context.Context, messageId string, newMessage *Message) error {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
obj := marshalModel(arena, newMessage)
obj := arena.NewObject()
newMessage.MarshalAnyenc(obj, arena)
builder := storestate.Builder{}
err := builder.Modify(collectionName, messageId, []string{contentKey}, pb.ModifyOp_Set, obj.Get(contentKey))
err := builder.Modify(CollectionName, messageId, []string{contentKey}, pb.ModifyOp_Set, obj.Get(contentKey))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
@ -561,7 +290,7 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
s.arenaPool.Put(arena)
}()
hasReaction, err := s.hasMyReaction(ctx, arena, messageId, emoji)
hasReaction, err := s.repository.hasMyReaction(ctx, s.accountService.AccountID(), messageId, emoji)
if err != nil {
return fmt.Errorf("check reaction: %w", err)
}
@ -569,12 +298,12 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
builder := storestate.Builder{}
if hasReaction {
err = builder.Modify(collectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_Pull, arena.NewString(s.accountService.AccountID()))
err = builder.Modify(CollectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_Pull, arena.NewString(s.accountService.AccountID()))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
} else {
err = builder.Modify(collectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_AddToSet, arena.NewString(s.accountService.AccountID()))
err = builder.Modify(CollectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_AddToSet, arena.NewString(s.accountService.AccountID()))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
@ -591,57 +320,35 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
return nil
}
func (s *storeObject) hasMyReaction(ctx context.Context, arena *anyenc.Arena, messageId string, emoji string) (bool, error) {
doc, err := s.collection.FindId(ctx, messageId)
if err != nil {
return false, fmt.Errorf("find message: %w", err)
}
myIdentity := s.accountService.AccountID()
msg := newMessageWrapper(arena, doc.Value())
reactions := msg.reactionsToModel()
if v, ok := reactions.GetReactions()[emoji]; ok {
if slices.Contains(v.GetIds(), myIdentity) {
return true, nil
}
}
return false, nil
}
type SubscribeLastMessagesResponse struct {
Messages []*model.ChatMessage
Messages []*Message
ChatState *model.ChatState
}
func (s *storeObject) SubscribeLastMessages(ctx context.Context, subId string, limit int, asyncInit bool) (*SubscribeLastMessagesResponse, error) {
txn, err := s.store.NewTx(ctx)
txn, err := s.repository.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("init read transaction: %w", err)
}
defer txn.Commit()
query := s.collection.Find(nil).Sort(descOrder).Limit(uint(limit))
messages, err := s.queryMessages(txn.Context(), query)
messages, err := s.repository.getLastMessages(txn.Context(), uint(limit))
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
// reverse
sort.Slice(messages, func(i, j int) bool {
return messages[i].OrderId < messages[j].OrderId
})
s.subscription.subscribe(subId)
if asyncInit {
var previousOrderId string
if len(messages) > 0 {
previousOrderId, err = getPrevOrderId(txn.Context(), s.collection, messages[0].OrderId)
previousOrderId, err = s.repository.getPrevOrderId(txn.Context(), messages[0].OrderId)
if err != nil {
return nil, fmt.Errorf("get previous order id: %w", err)
}
}
for _, message := range messages {
s.subscription.add(previousOrderId, message)
s.subscription.add(ctx, previousOrderId, message)
previousOrderId = message.OrderId
}
@ -657,28 +364,6 @@ func (s *storeObject) SubscribeLastMessages(ctx context.Context, subId string, l
}
}
func getPrevOrderId(ctx context.Context, coll anystore.Collection, orderId string) (string, error) {
iter, err := coll.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLt, orderId)}).
Sort(descOrder).
Limit(1).
Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
if iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("read doc: %w", err)
}
prevOrderId := doc.Value().GetString(orderKey, "id")
return prevOrderId, nil
}
return "", nil
}
func (s *storeObject) Unsubscribe(subId string) error {
s.subscription.unsubscribe(subId)
return nil

View file

@ -18,6 +18,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/core/block/source/mock_source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event/mock_event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
@ -25,6 +26,10 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const (
testSpaceId = "spaceId1"
)
type accountServiceStub struct {
accountId string
}
@ -48,6 +53,8 @@ type fixture struct {
sourceCreator string
eventSender *mock_event.MockSender
events []*pb.EventMessage
generateOrderIdFunc func(tx *storestate.StoreStateTx) string
}
const testCreator = "accountId1"
@ -85,16 +92,21 @@ func newFixture(t *testing.T) *fixture {
}).Return().Maybe()
source := mock_source.NewMockStore(t)
source.EXPECT().Id().Return("chatId1")
source.EXPECT().SpaceID().Return(testSpaceId)
source.EXPECT().ReadStoreDoc(ctx, mock.Anything, mock.Anything).Return(nil)
source.EXPECT().PushStoreChange(mock.Anything, mock.Anything).RunAndReturn(fx.applyToStore).Maybe()
var onSeenHook func([]string)
source.EXPECT().SetDiffManagerOnRemoveHook(mock.Anything).Run(func(hook func([]string)) {
onSeenHook = hook
onSeenHooks := map[string]func([]string){}
source.EXPECT().RegisterDiffManager(mock.Anything, mock.Anything).Run(func(name string, hook func([]string)) {
onSeenHooks[name] = hook
}).Return()
source.EXPECT().InitDiffManager(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
source.EXPECT().StoreSeenHeads(mock.Anything, mock.Anything).Return(nil).Maybe()
// Imitate diff manager
source.EXPECT().MarkSeenHeads(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, seenHeads []string) error {
source.EXPECT().MarkSeenHeads(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, name string, seenHeads []string) error {
allMessagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{
AfterOrderId: "",
IncludeBoundary: true,
@ -114,7 +126,7 @@ func newFixture(t *testing.T) *fixture {
}
}
onSeenHook(collectedHeads)
onSeenHooks[name](collectedHeads)
return nil
}).Maybe()
@ -138,7 +150,7 @@ func TestAddMessage(t *testing.T) {
sessionCtx := session.NewContext()
fx := newFixture(t)
fx.eventSender.EXPECT().BroadcastToOtherSessions(mock.Anything, mock.Anything).Return()
fx.eventSender.EXPECT().BroadcastToOtherSessions(mock.Anything, mock.Anything).Return().Maybe()
inputMessage := givenComplexMessage()
messageId, err := fx.AddMessage(ctx, sessionCtx, inputMessage)
@ -153,7 +165,6 @@ func TestAddMessage(t *testing.T) {
want := givenComplexMessage()
want.Id = messageId
want.Creator = testCreator
want.Read = true
got := messagesResp.Messages[0]
assertMessagesEqual(t, want, got)
@ -178,12 +189,13 @@ func TestAddMessage(t *testing.T) {
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
assert.Equal(t, messagesResp.ChatState.DbTimestamp, messagesResp.Messages[0].AddedAt)
assert.Equal(t, messagesResp.ChatState.LastStateId, messagesResp.Messages[0].StateId)
want := givenComplexMessage()
want.Id = messageId
want.Creator = testCreator
want.Read = false
want.MentionRead = false
got := messagesResp.Messages[0]
assertMessagesEqual(t, want, got)
@ -206,7 +218,7 @@ func TestGetMessages(t *testing.T) {
require.NoError(t, err)
lastMessage := messagesResp.Messages[4]
assert.Equal(t, messagesResp.ChatState.DbTimestamp, lastMessage.AddedAt)
assert.Equal(t, messagesResp.ChatState.LastStateId, lastMessage.StateId)
wantTexts := []string{"text 6", "text 7", "text 8", "text 9", "text 10"}
for i, msg := range messagesResp.Messages {
@ -252,7 +264,6 @@ func TestGetMessagesByIds(t *testing.T) {
want := givenComplexMessage()
want.Id = messageId
want.Creator = testCreator
want.Read = true
got := messages[0]
assertMessagesEqual(t, want, got)
}
@ -283,7 +294,6 @@ func TestEditMessage(t *testing.T) {
want := editedMessage
want.Id = messageId
want.Creator = testCreator
want.Read = true
got := messagesResp.Messages[0]
assert.True(t, got.ModifiedAt > 0)
@ -387,46 +397,7 @@ func TestToggleReaction(t *testing.T) {
assert.Equal(t, want, got)
}
func TestReadMessages(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false)
err := fx.MarkReadMessages(ctx, "", messagesResp.Messages[2].OrderId, messagesResp.ChatState.DbTimestamp)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", messagesResp.Messages[2].OrderId, true)
fx.assertReadStatus(t, ctx, messagesResp.Messages[3].OrderId, "", false)
}
func TestMarkMessagesAsNotRead(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages added by myself are read
fx.assertReadStatus(t, ctx, "", "", true)
fx.source.EXPECT().InitDiffManager(mock.Anything, mock.Anything).Return(nil)
fx.source.EXPECT().StoreSeenHeads(mock.Anything).Return(nil)
err := fx.MarkMessagesAsUnread(ctx, "")
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", "", false)
}
func (fx *fixture) assertReadStatus(t *testing.T, ctx context.Context, afterOrderId string, beforeOrderId string, isRead bool) *GetMessagesResponse {
func (fx *fixture) assertReadStatus(t *testing.T, ctx context.Context, afterOrderId string, beforeOrderId string, isRead bool, isMentionRead bool) *GetMessagesResponse {
messageResp, err := fx.GetMessages(ctx, GetMessagesRequest{
AfterOrderId: afterOrderId,
BeforeOrderId: beforeOrderId,
@ -437,17 +408,25 @@ func (fx *fixture) assertReadStatus(t *testing.T, ctx context.Context, afterOrde
for _, m := range messageResp.Messages {
assert.Equal(t, isRead, m.Read)
assert.Equal(t, isMentionRead, m.MentionRead)
}
return messageResp
}
func (fx *fixture) generateOrderId(tx *storestate.StoreStateTx) string {
if fx.generateOrderIdFunc != nil {
return fx.generateOrderIdFunc(tx)
}
return tx.NextOrder(tx.GetMaxOrder())
}
func (fx *fixture) applyToStore(ctx context.Context, params source.PushStoreChangeParams) (string, error) {
changeId := bson.NewObjectId().Hex()
tx, err := params.State.NewTx(ctx)
if err != nil {
return "", fmt.Errorf("new tx: %w", err)
}
order := tx.NextOrder(tx.GetMaxOrder())
order := fx.generateOrderId(tx)
err = tx.ApplyChangeSet(storestate.ChangeSet{
Id: changeId,
Order: order,
@ -466,71 +445,100 @@ func (fx *fixture) applyToStore(ctx context.Context, params source.PushStoreChan
return changeId, nil
}
func givenSimpleMessage(text string) *model.ChatMessage {
return &model.ChatMessage{
Id: "",
OrderId: "",
Creator: "",
Read: false,
Message: &model.ChatMessageMessageContent{
Text: text,
Style: model.BlockContentText_Paragraph,
func givenSimpleMessage(text string) *Message {
return &Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
Creator: "",
Read: true,
MentionRead: true,
Message: &model.ChatMessageMessageContent{
Text: text,
Style: model.BlockContentText_Paragraph,
},
},
}
}
func givenComplexMessage() *model.ChatMessage {
return &model.ChatMessage{
Id: "",
OrderId: "",
Creator: "",
Read: false,
ReplyToMessageId: "replyToMessageId1",
Message: &model.ChatMessageMessageContent{
Text: "text!",
Style: model.BlockContentText_Quote,
Marks: []*model.BlockContentTextMark{
{
Range: &model.Range{
From: 0,
To: 1,
func givenMessageWithMention(text string) *Message {
return &Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
Creator: "",
Read: true,
MentionRead: true,
Message: &model.ChatMessageMessageContent{
Text: text,
Style: model.BlockContentText_Paragraph,
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Mention,
Param: domain.NewParticipantId(testSpaceId, testCreator),
Range: &model.Range{From: 0, To: 1},
},
Type: model.BlockContentTextMark_Link,
Param: "https://example.com",
},
{
Range: &model.Range{
From: 2,
To: 3,
},
Type: model.BlockContentTextMark_Italic,
},
},
},
Attachments: []*model.ChatMessageAttachment{
{
Target: "attachmentId1",
Type: model.ChatMessageAttachment_IMAGE,
},
{
Target: "attachmentId2",
Type: model.ChatMessageAttachment_LINK,
},
},
Reactions: &model.ChatMessageReactions{
Reactions: map[string]*model.ChatMessageReactionsIdentityList{
"🥰": {
Ids: []string{"identity1", "identity2"},
},
"🤔": {
Ids: []string{"identity3"},
},
},
},
}
}
func assertMessagesEqual(t *testing.T, want, got *model.ChatMessage) {
func givenComplexMessage() *Message {
return &Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
Creator: "",
Read: true,
MentionRead: true,
ReplyToMessageId: "replyToMessageId1",
Message: &model.ChatMessageMessageContent{
Text: "text!",
Style: model.BlockContentText_Quote,
Marks: []*model.BlockContentTextMark{
{
Range: &model.Range{
From: 0,
To: 1,
},
Type: model.BlockContentTextMark_Link,
Param: "https://example.com",
},
{
Range: &model.Range{
From: 2,
To: 3,
},
Type: model.BlockContentTextMark_Italic,
},
},
},
Attachments: []*model.ChatMessageAttachment{
{
Target: "attachmentId1",
Type: model.ChatMessageAttachment_IMAGE,
},
{
Target: "attachmentId2",
Type: model.ChatMessageAttachment_LINK,
},
},
Reactions: &model.ChatMessageReactions{
Reactions: map[string]*model.ChatMessageReactionsIdentityList{
"🥰": {
Ids: []string{"identity1", "identity2"},
},
"🤔": {
Ids: []string{"identity3"},
},
},
},
},
}
}
func assertMessagesEqual(t *testing.T, want, got *Message) {
// Cleanup order id
assert.NotEmpty(t, got.OrderId)
got.OrderId = ""
@ -538,8 +546,8 @@ func assertMessagesEqual(t *testing.T, want, got *model.ChatMessage) {
assert.NotZero(t, got.CreatedAt)
got.CreatedAt = 0
assert.NotZero(t, got.AddedAt)
got.AddedAt = 0
assert.NotEmpty(t, got.StateId)
got.StateId = ""
assert.Equal(t, want, got)
}

View file

@ -1,53 +1,67 @@
package chatobject
import (
"context"
"fmt"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const (
creatorKey = "creator"
createdAtKey = "createdAt"
modifiedAtKey = "modifiedAt"
reactionsKey = "reactions"
contentKey = "content"
readKey = "read"
addedKey = "a"
orderKey = "_o"
creatorKey = "creator"
createdAtKey = "createdAt"
modifiedAtKey = "modifiedAt"
reactionsKey = "reactions"
contentKey = "content"
readKey = "read"
mentionReadKey = "mentionRead"
hasMentionKey = "hasMention"
stateIdKey = "stateId"
orderKey = "_o"
)
type messageWrapper struct {
val *anyenc.Value
arena *anyenc.Arena
type Message struct {
*model.ChatMessage
// CurrentUserMentioned is memoized result of IsCurrentUserMentioned
CurrentUserMentioned bool
}
func newMessageWrapper(arena *anyenc.Arena, val *anyenc.Value) *messageWrapper {
return &messageWrapper{arena: arena, val: val}
}
func (m *messageWrapper) getCreator() string {
return string(m.val.GetStringBytes(creatorKey))
}
func (m *messageWrapper) setCreator(v string) {
m.val.Set(creatorKey, m.arena.NewString(v))
}
func (m *messageWrapper) setRead(v bool) {
if v {
m.val.Set(readKey, m.arena.NewTrue())
} else {
m.val.Set(readKey, m.arena.NewFalse())
func (m *Message) IsCurrentUserMentioned(ctx context.Context, myParticipantId string, myIdentity string, repo *repository) (bool, error) {
for _, mark := range m.Message.Marks {
if mark.Type == model.BlockContentTextMark_Mention && mark.Param == myParticipantId {
return true, nil
}
}
if m.ReplyToMessageId != "" {
msgs, err := repo.getMessagesByIds(ctx, []string{m.ReplyToMessageId})
if err != nil {
return false, fmt.Errorf("get messages by id: %w", err)
}
if len(msgs) == 1 {
msg := msgs[0]
if msg.Creator == myIdentity {
return true, nil
}
}
}
return false, nil
}
func (m *messageWrapper) setCreatedAt(v int64) {
m.val.Set(createdAtKey, m.arena.NewNumberInt(int(v)))
func unmarshalMessage(val *anyenc.Value) (*Message, error) {
return newMessageWrapper(val).toModel()
}
func (m *messageWrapper) setAddedAt(v int64) {
m.val.Set(addedKey, m.arena.NewNumberInt(int(v)))
type messageUnmarshaller struct {
val *anyenc.Value
}
func newMessageWrapper(val *anyenc.Value) *messageUnmarshaller {
return &messageUnmarshaller{val: val}
}
/*
@ -84,12 +98,12 @@ func (m *messageWrapper) setAddedAt(v int64) {
*/
func marshalModel(arena *anyenc.Arena, msg *model.ChatMessage) *anyenc.Value {
func (m *Message) MarshalAnyenc(marshalTo *anyenc.Value, arena *anyenc.Arena) {
message := arena.NewObject()
message.Set("text", arena.NewString(msg.Message.Text))
message.Set("style", arena.NewNumberInt(int(msg.Message.Style)))
message.Set("text", arena.NewString(m.Message.Text))
message.Set("style", arena.NewNumberInt(int(m.Message.Style)))
marks := arena.NewArray()
for i, inMark := range msg.Message.Marks {
for i, inMark := range m.Message.Marks {
mark := arena.NewObject()
mark.Set("from", arena.NewNumberInt(int(inMark.Range.From)))
mark.Set("to", arena.NewNumberInt(int(inMark.Range.To)))
@ -102,7 +116,7 @@ func marshalModel(arena *anyenc.Arena, msg *model.ChatMessage) *anyenc.Value {
message.Set("marks", marks)
attachments := arena.NewObject()
for i, inAttachment := range msg.Attachments {
for i, inAttachment := range m.Attachments {
attachment := arena.NewObject()
attachment.Set("type", arena.NewNumberInt(int(inAttachment.Type)))
attachments.Set(inAttachment.Target, attachment)
@ -114,7 +128,7 @@ func marshalModel(arena *anyenc.Arena, msg *model.ChatMessage) *anyenc.Value {
content.Set("attachments", attachments)
reactions := arena.NewObject()
for emoji, inReaction := range msg.GetReactions().GetReactions() {
for emoji, inReaction := range m.GetReactions().GetReactions() {
identities := arena.NewArray()
for j, identity := range inReaction.Ids {
identities.SetArrayItem(j, arena.NewString(identity))
@ -122,41 +136,48 @@ func marshalModel(arena *anyenc.Arena, msg *model.ChatMessage) *anyenc.Value {
reactions.Set(emoji, identities)
}
root := arena.NewObject()
root.Set(creatorKey, arena.NewString(msg.Creator))
root.Set(createdAtKey, arena.NewNumberInt(int(msg.CreatedAt)))
root.Set(modifiedAtKey, arena.NewNumberInt(int(msg.ModifiedAt)))
root.Set("replyToMessageId", arena.NewString(msg.ReplyToMessageId))
root.Set(contentKey, content)
var read *anyenc.Value
if msg.Read {
read = arena.NewTrue()
marshalTo.Set("id", arena.NewString(m.Id))
marshalTo.Set(creatorKey, arena.NewString(m.Creator))
marshalTo.Set(createdAtKey, arena.NewNumberInt(int(m.CreatedAt)))
marshalTo.Set(modifiedAtKey, arena.NewNumberInt(int(m.ModifiedAt)))
marshalTo.Set("replyToMessageId", arena.NewString(m.ReplyToMessageId))
marshalTo.Set(contentKey, content)
marshalTo.Set(readKey, arenaNewBool(arena, m.Read))
marshalTo.Set(mentionReadKey, arenaNewBool(arena, m.MentionRead))
marshalTo.Set(hasMentionKey, arenaNewBool(arena, m.CurrentUserMentioned))
marshalTo.Set(stateIdKey, arena.NewString(m.StateId))
marshalTo.Set(reactionsKey, reactions)
}
func arenaNewBool(a *anyenc.Arena, value bool) *anyenc.Value {
if value {
return a.NewTrue()
} else {
read = arena.NewFalse()
}
root.Set(readKey, read)
root.Set(reactionsKey, reactions)
return root
}
func (m *messageWrapper) toModel() *model.ChatMessage {
return &model.ChatMessage{
Id: string(m.val.GetStringBytes("id")),
Creator: string(m.val.GetStringBytes(creatorKey)),
CreatedAt: int64(m.val.GetInt(createdAtKey)),
ModifiedAt: int64(m.val.GetInt(modifiedAtKey)),
AddedAt: int64(m.val.GetInt(addedKey)),
OrderId: string(m.val.GetStringBytes("_o", "id")),
ReplyToMessageId: string(m.val.GetStringBytes("replyToMessageId")),
Message: m.contentToModel(),
Read: m.val.GetBool(readKey),
Attachments: m.attachmentsToModel(),
Reactions: m.reactionsToModel(),
return a.NewFalse()
}
}
func (m *messageWrapper) contentToModel() *model.ChatMessageMessageContent {
func (m *messageUnmarshaller) toModel() (*Message, error) {
return &Message{
ChatMessage: &model.ChatMessage{
Id: string(m.val.GetStringBytes("id")),
Creator: string(m.val.GetStringBytes(creatorKey)),
CreatedAt: int64(m.val.GetInt(createdAtKey)),
ModifiedAt: int64(m.val.GetInt(modifiedAtKey)),
StateId: m.val.GetString(stateIdKey),
OrderId: string(m.val.GetStringBytes("_o", "id")),
ReplyToMessageId: string(m.val.GetStringBytes("replyToMessageId")),
Message: m.contentToModel(),
Read: m.val.GetBool(readKey),
MentionRead: m.val.GetBool(mentionReadKey),
Attachments: m.attachmentsToModel(),
Reactions: m.reactionsToModel(),
},
CurrentUserMentioned: m.val.GetBool(hasMentionKey),
}, nil
}
func (m *messageUnmarshaller) contentToModel() *model.ChatMessageMessageContent {
inMarks := m.val.GetArray(contentKey, "message", "marks")
marks := make([]*model.BlockContentTextMark, 0, len(inMarks))
for _, inMark := range inMarks {
@ -177,7 +198,7 @@ func (m *messageWrapper) contentToModel() *model.ChatMessageMessageContent {
}
}
func (m *messageWrapper) attachmentsToModel() []*model.ChatMessageAttachment {
func (m *messageUnmarshaller) attachmentsToModel() []*model.ChatMessageAttachment {
inAttachments := m.val.GetObject(contentKey, "attachments")
var attachments []*model.ChatMessageAttachment
if inAttachments != nil {
@ -192,7 +213,7 @@ func (m *messageWrapper) attachmentsToModel() []*model.ChatMessageAttachment {
return attachments
}
func (m *messageWrapper) reactionsToModel() *model.ChatMessageReactions {
func (m *messageUnmarshaller) reactionsToModel() *model.ChatMessageReactions {
inReactions := m.val.GetObject(reactionsKey)
reactions := &model.ChatMessageReactions{
Reactions: map[string]*model.ChatMessageReactionsIdentityList{},

View file

@ -0,0 +1,240 @@
package chatobject
import (
"context"
"fmt"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type CounterType int
const (
CounterTypeMessage = CounterType(iota)
CounterTypeMention
)
type readHandler interface {
getUnreadFilter() query.Filter
getMessagesFilter() query.Filter
getDiffManagerName() string
getReadKey() string
readModifier(value bool) query.Modifier
readMessages(newOldestOrderId string, idsModified []string)
unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string)
}
type readMessagesHandler struct {
subscription *subscription
}
func (h *readMessagesHandler) getUnreadFilter() query.Filter {
return query.Not{
Filter: query.Key{Path: []string{readKey}, Filter: query.NewComp(query.CompOpEq, true)},
}
}
func (h *readMessagesHandler) getMessagesFilter() query.Filter {
return nil
}
func (h *readMessagesHandler) getDiffManagerName() string {
return diffManagerMessages
}
func (h *readMessagesHandler) getReadKey() string {
return readKey
}
func (h *readMessagesHandler) readMessages(newOldestOrderId string, idsModified []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
return state
})
h.subscription.updateMessageRead(idsModified, true)
}
func (h *readMessagesHandler) unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
h.subscription.updateMessageRead(msgIds, false)
}
func (h *readMessagesHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
return v, false, nil
})
}
type readMentionsHandler struct {
subscription *subscription
}
func (h *readMentionsHandler) getUnreadFilter() query.Filter {
return query.And{
query.Key{Path: []string{hasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)},
query.Key{Path: []string{mentionReadKey}, Filter: query.NewComp(query.CompOpEq, false)},
}
}
func (h *readMentionsHandler) getMessagesFilter() query.Filter {
return query.Key{Path: []string{hasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)}
}
func (h *readMentionsHandler) getDiffManagerName() string {
return diffManagerMentions
}
func (h *readMentionsHandler) getReadKey() string {
return mentionReadKey
}
func (h *readMentionsHandler) readMessages(newOldestOrderId string, idsModified []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
return state
})
h.subscription.updateMentionRead(idsModified, true)
}
func (h *readMentionsHandler) unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
h.subscription.updateMentionRead(msgIds, false)
}
func (h *readMentionsHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
if v.GetBool(hasMentionKey) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
}
return v, false, nil
})
}
func newReadHandler(counterType CounterType, subscription *subscription) readHandler {
switch counterType {
case CounterTypeMessage:
return &readMessagesHandler{subscription: subscription}
case CounterTypeMention:
return &readMentionsHandler{subscription: subscription}
default:
panic("unknown counter type")
}
}
func (s *storeObject) MarkReadMessages(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, counterType CounterType) error {
handler := newReadHandler(counterType, s.subscription)
// 1. select all messages with orderId < beforeOrderId and addedTime < lastDbState
// 2. use the last(by orderId) message id as lastHead
// 3. update the MarkSeenHeads
// 2. mark messages as read in the DB
msgs, err := s.repository.getUnreadMessageIdsInRange(ctx, afterOrderId, beforeOrderId, lastStateId, handler)
if err != nil {
return fmt.Errorf("get message: %w", err)
}
// mark the whole tree as seen from the current message
return s.storeSource.MarkSeenHeads(ctx, handler.getDiffManagerName(), msgs)
}
func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType CounterType) error {
txn, err := s.repository.writeTx(ctx)
if err != nil {
return fmt.Errorf("create tx: %w", err)
}
defer txn.Rollback()
handler := newReadHandler(counterType, s.subscription)
messageIds, err := s.repository.getReadMessagesAfter(txn.Context(), afterOrderId, handler)
if err != nil {
return fmt.Errorf("get read messages: %w", err)
}
if len(messageIds) == 0 {
return nil
}
idsModified := s.repository.setReadFlag(txn.Context(), s.Id(), messageIds, handler, false)
if len(idsModified) == 0 {
return nil
}
newOldestOrderId, err := s.repository.getOldestOrderId(txn.Context(), handler)
if err != nil {
return fmt.Errorf("get oldest order id: %w", err)
}
lastAdded, err := s.repository.getLastStateId(txn.Context())
if err != nil {
return fmt.Errorf("get last added date: %w", err)
}
handler.unreadMessages(newOldestOrderId, lastAdded, idsModified)
s.subscription.flush()
seenHeads, err := s.seenHeadsCollector.collectSeenHeads(ctx, afterOrderId)
if err != nil {
return fmt.Errorf("get seen heads: %w", err)
}
err = s.storeSource.InitDiffManager(ctx, diffManagerMessages, seenHeads)
if err != nil {
return fmt.Errorf("init diff manager: %w", err)
}
err = s.storeSource.StoreSeenHeads(txn.Context(), diffManagerMessages)
if err != nil {
return fmt.Errorf("store seen heads: %w", err)
}
return txn.Commit()
}
func (s *storeObject) markReadMessages(changeIds []string, handler readHandler) error {
if len(changeIds) == 0 {
return nil
}
txn, err := s.repository.writeTx(s.componentCtx)
if err != nil {
return fmt.Errorf("start write tx: %w", err)
}
defer txn.Rollback()
idsModified := s.repository.setReadFlag(txn.Context(), s.Id(), changeIds, handler, true)
if len(idsModified) > 0 {
newOldestOrderId, err := s.repository.getOldestOrderId(txn.Context(), handler)
if err != nil {
return fmt.Errorf("get oldest order id: %w", err)
}
err = txn.Commit()
if err != nil {
return fmt.Errorf("commit: %w", err)
}
handler.readMessages(newOldestOrderId, idsModified)
s.subscription.flush()
}
return nil
}

View file

@ -0,0 +1,166 @@
package chatobject
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func TestReadMessages(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err := fx.MarkReadMessages(ctx, "", messagesResp.Messages[2].OrderId, messagesResp.ChatState.LastStateId, CounterTypeMessage)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", messagesResp.Messages[2].OrderId, true, false)
fx.assertReadStatus(t, ctx, messagesResp.Messages[3].OrderId, "", false, false)
}
func TestReadMessagesLoadedInBackground(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
firstMessageId, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("first message")))
require.NoError(t, err)
firstMessage, err := fx.GetMessageById(ctx, firstMessageId)
require.NoError(t, err)
fx.generateOrderIdFunc = func(tx *storestate.StoreStateTx) string {
prev, err := storestate.LexId.NextBefore("", firstMessage.OrderId)
require.NoError(t, err)
return prev
}
// The second messages is before the first one
secondMessageId, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("second message")))
require.NoError(t, err)
secondMessage, err := fx.GetMessageById(ctx, secondMessageId)
require.NoError(t, err)
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, firstMessage.StateId, CounterTypeMessage)
require.NoError(t, err)
gotResponse, err := fx.GetMessages(ctx, GetMessagesRequest{})
require.NoError(t, err)
firstMessage.Read = true
wantMessages := []*Message{
secondMessage,
firstMessage,
}
wantResponse := &GetMessagesResponse{
Messages: wantMessages,
ChatState: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: secondMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{},
LastStateId: secondMessage.StateId,
},
}
assert.Equal(t, wantResponse, gotResponse)
}
func TestReadMentions(t *testing.T) {
t.Run("mentioned directly in marks", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenMessageWithMention(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err := fx.MarkReadMessages(ctx, "", messagesResp.Messages[2].OrderId, messagesResp.ChatState.LastStateId, CounterTypeMention)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", messagesResp.Messages[2].OrderId, false, true)
fx.assertReadStatus(t, ctx, messagesResp.Messages[3].OrderId, "", false, false)
})
t.Run("author of replied message", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
firstMessageId, err := fx.AddMessage(ctx, nil, givenSimpleMessage("message to reply to"))
require.NoError(t, err)
secondMessageInput := givenSimpleMessage("a reply")
secondMessageInput.ReplyToMessageId = firstMessageId
secondMessageId, err := fx.AddMessage(ctx, nil, secondMessageInput)
require.NoError(t, err)
secondMessage, err := fx.GetMessageById(ctx, secondMessageId)
require.NoError(t, err)
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err = fx.MarkReadMessages(ctx, "", secondMessage.OrderId, messagesResp.ChatState.LastStateId, CounterTypeMention)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, secondMessage.OrderId, secondMessage.OrderId, false, true)
})
}
func TestMarkMessagesAsNotRead(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenSimpleMessage(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages added by myself are read
fx.assertReadStatus(t, ctx, "", "", true, true)
err := fx.MarkMessagesAsUnread(ctx, "", CounterTypeMessage)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", "", false, true)
}
func TestMarkMentionsAsNotRead(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
const n = 10
for i := 0; i < n; i++ {
_, err := fx.AddMessage(ctx, nil, givenMessageWithMention(fmt.Sprintf("message %d", i+1)))
require.NoError(t, err)
}
// All messages added by myself are read
fx.assertReadStatus(t, ctx, "", "", true, true)
err := fx.MarkMessagesAsUnread(ctx, "", CounterTypeMention)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", "", true, false)
}

View file

@ -0,0 +1,331 @@
package chatobject
import (
"context"
"errors"
"fmt"
"slices"
"sort"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type repository struct {
collection anystore.Collection
arenaPool *anyenc.ArenaPool
}
func (s *repository) writeTx(ctx context.Context) (anystore.WriteTx, error) {
return s.collection.WriteTx(ctx)
}
func (s *repository) readTx(ctx context.Context) (anystore.ReadTx, error) {
return s.collection.ReadTx(ctx)
}
func (s *repository) getLastStateId(ctx context.Context) (string, error) {
lastAddedDate := s.collection.Find(nil).Sort(descStateId).Limit(1)
iter, err := lastAddedDate.Iter(ctx)
if err != nil {
return "", fmt.Errorf("find last added date: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return "", fmt.Errorf("unmarshal message: %w", err)
}
return msg.StateId, nil
}
return "", nil
}
func (s *repository) getPrevOrderId(ctx context.Context, orderId string) (string, error) {
iter, err := s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLt, orderId)}).
Sort(descOrder).
Limit(1).
Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
if iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("read doc: %w", err)
}
prevOrderId := doc.Value().GetString(orderKey, "id")
return prevOrderId, nil
}
return "", nil
}
// initialChatState returns the initial chat state for the chat object from the DB
func (s *repository) loadChatState(ctx context.Context) (*model.ChatState, error) {
txn, err := s.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messagesState, err := s.loadChatStateByType(txn.Context(), CounterTypeMessage)
if err != nil {
return nil, fmt.Errorf("get messages state: %w", err)
}
mentionsState, err := s.loadChatStateByType(txn.Context(), CounterTypeMention)
if err != nil {
return nil, fmt.Errorf("get mentions state: %w", err)
}
lastStateId, err := s.getLastStateId(txn.Context())
if err != nil {
return nil, fmt.Errorf("get last added date: %w", err)
}
return &model.ChatState{
Messages: messagesState,
Mentions: mentionsState,
LastStateId: lastStateId,
}, nil
}
func (s *repository) loadChatStateByType(ctx context.Context, counterType CounterType) (*model.ChatStateUnreadState, error) {
opts := newReadHandler(counterType, nil)
oldestOrderId, err := s.getOldestOrderId(ctx, opts)
if err != nil {
return nil, fmt.Errorf("get oldest order id: %w", err)
}
count, err := s.countUnreadMessages(ctx, opts)
if err != nil {
return nil, fmt.Errorf("update messages: %w", err)
}
return &model.ChatStateUnreadState{
OldestOrderId: oldestOrderId,
Counter: int32(count),
}, nil
}
func (s *repository) getOldestOrderId(ctx context.Context, handler readHandler) (string, error) {
unreadQuery := s.collection.Find(handler.getUnreadFilter()).Sort(ascOrder)
iter, err := unreadQuery.Limit(1).Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iter: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
orders := doc.Value().GetObject(orderKey)
if orders != nil {
return orders.Get("id").GetString(), nil
}
}
return "", nil
}
func (s *repository) countUnreadMessages(ctx context.Context, handler readHandler) (int, error) {
unreadQuery := s.collection.Find(handler.getUnreadFilter())
return unreadQuery.Count(ctx)
}
func (s *repository) getReadMessagesAfter(ctx context.Context, afterOrderId string, handler readHandler) ([]string, error) {
filter := query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{handler.getReadKey()}, Filter: query.NewComp(query.CompOpEq, true)},
}
if handler.getMessagesFilter() != nil {
filter = append(filter, handler.getMessagesFilter())
}
iter, err := s.collection.Find(filter).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *repository) getUnreadMessageIdsInRange(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, handler readHandler) ([]string, error) {
qry := query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLte, beforeOrderId)},
query.Or{
query.Not{query.Key{Path: []string{stateIdKey}, Filter: query.Exists{}}},
query.Key{Path: []string{stateIdKey}, Filter: query.NewComp(query.CompOpLte, lastStateId)},
},
handler.getUnreadFilter(),
}
iter, err := s.collection.Find(qry).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find id: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (r *repository) setReadFlag(ctx context.Context, chatObjectId string, msgIds []string, handler readHandler, value bool) []string {
var idsModified []string
for _, id := range msgIds {
if id == chatObjectId {
// skip tree root
continue
}
res, err := r.collection.UpdateId(ctx, id, handler.readModifier(value))
// Not all changes are messages, skip them
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
log.Error("markReadMessages: update message", zap.Error(err), zap.String("changeId", id), zap.String("chatObjectId", chatObjectId))
continue
}
if res.Modified > 0 {
idsModified = append(idsModified, id)
}
}
return idsModified
}
func (s *repository) getMessages(ctx context.Context, req GetMessagesRequest) ([]*Message, error) {
var qry anystore.Query
if req.AfterOrderId != "" {
operator := query.CompOpGt
if req.IncludeBoundary {
operator = query.CompOpGte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.AfterOrderId)}).Sort(ascOrder).Limit(uint(req.Limit))
} else if req.BeforeOrderId != "" {
operator := query.CompOpLt
if req.IncludeBoundary {
operator = query.CompOpLte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.BeforeOrderId)}).Sort(descOrder).Limit(uint(req.Limit))
} else {
qry = s.collection.Find(nil).Sort(descOrder).Limit(uint(req.Limit))
}
msgs, err := s.queryMessages(ctx, qry)
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
return msgs, nil
}
func (s *repository) queryMessages(ctx context.Context, query anystore.Query) ([]*Message, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
iter, err := query.Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find iter: %w", err)
}
defer iter.Close()
var res []*Message
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return nil, fmt.Errorf("unmarshal message: %w", err)
}
res = append(res, msg)
}
// reverse
sort.Slice(res, func(i, j int) bool {
return res[i].OrderId < res[j].OrderId
})
return res, nil
}
func (s *repository) hasMyReaction(ctx context.Context, myIdentity string, messageId string, emoji string) (bool, error) {
doc, err := s.collection.FindId(ctx, messageId)
if err != nil {
return false, fmt.Errorf("find message: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return false, fmt.Errorf("unmarshal message: %w", err)
}
if v, ok := msg.GetReactions().GetReactions()[emoji]; ok {
if slices.Contains(v.GetIds(), myIdentity) {
return true, nil
}
}
return false, nil
}
func (s *repository) getMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error) {
txn, err := s.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messages := make([]*Message, 0, len(messageIds))
for _, messageId := range messageIds {
obj, err := s.collection.FindId(txn.Context(), messageId)
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("find id: %w", err))
}
msg, err := unmarshalMessage(obj.Value())
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("unmarshal message: %w", err))
}
messages = append(messages, msg)
}
return messages, txn.Commit()
}
func (s *repository) getLastMessages(ctx context.Context, limit uint) ([]*Message, error) {
qry := s.collection.Find(nil).Sort(descOrder).Limit(limit)
return s.queryMessages(ctx, qry)
}

View file

@ -1,6 +1,7 @@
package chatobject
import (
"context"
"slices"
"time"
@ -19,28 +20,40 @@ import (
const LastMessageSubscriptionId = "lastMessage"
type subscription struct {
spaceId string
chatId string
eventSender event.Sender
spaceIndex spaceindex.Store
componentCtx context.Context
spaceId string
chatId string
myIdentity string
myParticipantId string
sessionContext session.Context
eventsBuffer []*pb.EventMessage
eventsBuffer []*pb.EventMessage
identityCache *expirable.LRU[string, *domain.Details]
ids []string
chatState *model.ChatState
needReloadState bool
chatStateUpdated bool
// Deps
spaceIndex spaceindex.Store
eventSender event.Sender
repository *repository
}
func newSubscription(spaceId string, chatId string, eventSender event.Sender, spaceIndex spaceindex.Store) *subscription {
func (s *storeObject) newSubscription(fullId domain.FullID, myIdentity string, myParticipantId string) *subscription {
return &subscription{
spaceId: spaceId,
chatId: chatId,
eventSender: eventSender,
spaceIndex: spaceIndex,
identityCache: expirable.NewLRU[string, *domain.Details](50, nil, time.Minute),
componentCtx: s.componentCtx,
spaceId: fullId.SpaceID,
chatId: fullId.ObjectID,
eventSender: s.eventSender,
spaceIndex: s.spaceIndex,
myIdentity: myIdentity,
myParticipantId: myParticipantId,
identityCache: expirable.NewLRU[string, *domain.Details](50, nil, time.Minute),
repository: s.repository,
}
}
@ -71,20 +84,43 @@ func (s *subscription) setSessionContext(ctx session.Context) {
s.sessionContext = ctx
}
func (s *subscription) loadChatState(ctx context.Context) error {
state, err := s.repository.loadChatState(ctx)
if err != nil {
return err
}
s.chatState = state
return nil
}
func (s *subscription) getChatState() *model.ChatState {
return copyChatState(s.chatState)
}
func (s *subscription) updateChatState(updater func(*model.ChatState)) {
updater(s.chatState)
func (s *subscription) updateChatState(updater func(*model.ChatState) *model.ChatState) {
s.chatState = updater(s.chatState)
s.chatStateUpdated = true
}
// flush is called after commiting changes
func (s *subscription) flush() {
if !s.canSend() {
return
}
// Reload ChatState after commit
if s.needReloadState {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
newState, err := s.repository.loadChatState(s.componentCtx)
if err != nil {
log.Error("failed to reload chat state", zap.Error(err))
return state
}
return newState
})
s.needReloadState = false
}
events := slices.Clone(s.eventsBuffer)
s.eventsBuffer = s.eventsBuffer[:0]
@ -123,18 +159,30 @@ func (s *subscription) getIdentityDetails(identity string) (*domain.Details, err
return details, nil
}
func (s *subscription) add(prevOrderId string, message *model.ChatMessage) {
s.updateChatState(func(state *model.ChatState) {
func (s *subscription) add(ctx context.Context, prevOrderId string, message *Message) {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
if !message.Read {
if message.OrderId < state.Messages.OldestOrderId {
if message.OrderId < state.Messages.OldestOrderId || state.Messages.OldestOrderId == "" {
state.Messages.OldestOrderId = message.OrderId
}
state.Messages.Counter++
isMentioned, err := message.IsCurrentUserMentioned(ctx, s.myParticipantId, s.myIdentity, s.repository)
if err != nil {
log.Error("subscription add: check if the current user is mentioned", zap.Error(err))
}
if isMentioned {
state.Mentions.Counter++
if message.OrderId < state.Mentions.OldestOrderId || state.Mentions.OldestOrderId == "" {
state.Mentions.OldestOrderId = message.OrderId
}
}
}
if message.AddedAt > state.DbTimestamp {
state.DbTimestamp = message.AddedAt
if message.StateId > state.LastStateId {
state.LastStateId = message.StateId
}
return state
})
if !s.canSend() {
@ -143,7 +191,7 @@ func (s *subscription) add(prevOrderId string, message *model.ChatMessage) {
ev := &pb.EventChatAdd{
Id: message.Id,
Message: message,
Message: message.ChatMessage,
OrderId: message.OrderId,
AfterOrderId: prevOrderId,
SubIds: slices.Clone(s.ids),
@ -153,7 +201,7 @@ func (s *subscription) add(prevOrderId string, message *model.ChatMessage) {
identityDetails, err := s.getIdentityDetails(message.Creator)
if err != nil {
log.Error("get identity details", zap.Error(err))
} else {
} else if identityDetails.Len() > 0 {
ev.Dependencies = append(ev.Dependencies, identityDetails.ToProto())
}
@ -161,7 +209,7 @@ func (s *subscription) add(prevOrderId string, message *model.ChatMessage) {
attachmentDetails, err := s.spaceIndex.GetDetails(attachment.Target)
if err != nil {
log.Error("get attachment details", zap.Error(err))
} else {
} else if attachmentDetails.Len() > 0 {
ev.Dependencies = append(ev.Dependencies, attachmentDetails.ToProto())
}
}
@ -179,15 +227,18 @@ func (s *subscription) delete(messageId string) {
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatDelete{
ChatDelete: ev,
}))
// We can't reload chat state here because Delete operation hasn't been commited yet
s.needReloadState = true
}
func (s *subscription) updateFull(message *model.ChatMessage) {
func (s *subscription) updateFull(message *Message) {
if !s.canSend() {
return
}
ev := &pb.EventChatUpdate{
Id: message.Id,
Message: message,
Message: message.ChatMessage,
SubIds: slices.Clone(s.ids),
}
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatUpdate{
@ -195,7 +246,7 @@ func (s *subscription) updateFull(message *model.ChatMessage) {
}))
}
func (s *subscription) updateReactions(message *model.ChatMessage) {
func (s *subscription) updateReactions(message *Message) {
if !s.canSend() {
return
}
@ -209,22 +260,45 @@ func (s *subscription) updateReactions(message *model.ChatMessage) {
}))
}
// updateReadStatus updates the read status of the messages with the given ids
// updateMessageRead updates the read status of the messages with the given ids
// read ids should ONLY contain ids if they were actually modified in the DB
func (s *subscription) updateReadStatus(ids []string, read bool) {
s.updateChatState(func(state *model.ChatState) {
func (s *subscription) updateMessageRead(ids []string, read bool) {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
if read {
state.Messages.Counter -= int32(len(ids))
} else {
state.Messages.Counter += int32(len(ids))
}
return state
})
if !s.canSend() {
return
}
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatUpdateReadStatus{
ChatUpdateReadStatus: &pb.EventChatUpdateReadStatus{
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatUpdateMessageReadStatus{
ChatUpdateMessageReadStatus: &pb.EventChatUpdateMessageReadStatus{
Ids: ids,
IsRead: read,
SubIds: slices.Clone(s.ids),
},
}))
}
func (s *subscription) updateMentionRead(ids []string, read bool) {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
if read {
state.Mentions.Counter -= int32(len(ids))
} else {
state.Mentions.Counter += int32(len(ids))
}
return state
})
if !s.canSend() {
return
}
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatUpdateMentionReadStatus{
ChatUpdateMentionReadStatus: &pb.EventChatUpdateMentionReadStatus{
Ids: ids,
IsRead: read,
SubIds: slices.Clone(s.ids),
@ -249,7 +323,7 @@ func copyChatState(state *model.ChatState) *model.ChatState {
return &model.ChatState{
Messages: copyReadState(state.Messages),
Mentions: copyReadState(state.Mentions),
DbTimestamp: state.DbTimestamp,
LastStateId: state.LastStateId,
}
}

View file

@ -5,8 +5,14 @@ import (
"fmt"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func TestSubscription(t *testing.T) {
@ -28,6 +34,8 @@ func TestSubscription(t *testing.T) {
assert.Equal(t, wantTexts[i], msg.Message.Text)
}
lastOrderId := resp.Messages[len(resp.Messages)-1].OrderId
var lastStateId string
t.Run("add message", func(t *testing.T) {
fx.events = nil
@ -35,13 +43,40 @@ func TestSubscription(t *testing.T) {
require.NoError(t, err)
require.Len(t, fx.events, 2)
ev := fx.events[0].GetChatAdd()
require.NotNil(t, ev)
assert.Equal(t, messageId, ev.Id)
message, err := fx.GetMessageById(ctx, messageId)
require.NoError(t, err)
evState := fx.events[1].GetChatStateUpdate()
require.NotNil(t, evState)
assert.True(t, evState.State.DbTimestamp > 0)
lastStateId = message.StateId
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: message.Id,
OrderId: message.OrderId,
AfterOrderId: lastOrderId,
Message: message.ChatMessage,
SubIds: []string{"subId"},
Dependencies: nil,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{},
Mentions: &model.ChatStateUnreadState{},
LastStateId: message.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
})
t.Run("edit message", func(t *testing.T) {
@ -54,10 +89,22 @@ func TestSubscription(t *testing.T) {
require.NoError(t, err)
require.Len(t, fx.events, 1)
ev := fx.events[0].GetChatUpdate()
require.NotNil(t, ev)
assert.Equal(t, resp.Messages[0].Id, ev.Id)
assert.Equal(t, edited.Message.Text, ev.Message.Message.Text)
message, err := fx.GetMessageById(ctx, resp.Messages[0].Id)
require.NoError(t, err)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatUpdate{
ChatUpdate: &pb.EventChatUpdate{
Id: resp.Messages[0].Id,
Message: message.ChatMessage,
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
})
t.Run("toggle message reaction", func(t *testing.T) {
@ -67,11 +114,31 @@ func TestSubscription(t *testing.T) {
require.NoError(t, err)
require.Len(t, fx.events, 1)
ev := fx.events[0].GetChatUpdateReactions()
require.NotNil(t, ev)
assert.Equal(t, resp.Messages[0].Id, ev.Id)
_, ok := ev.Reactions.Reactions["👍"]
assert.True(t, ok)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatUpdateReactions{
ChatUpdateReactions: &pb.EventChatUpdateReactions{
Id: resp.Messages[0].Id,
Reactions: &model.ChatMessageReactions{
Reactions: map[string]*model.ChatMessageReactionsIdentityList{
"👍": {
Ids: []string{testCreator},
},
"🥰": {
Ids: []string{"identity1", "identity2"},
},
"🤔": {
Ids: []string{"identity3"},
},
},
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
})
t.Run("delete message", func(t *testing.T) {
@ -79,10 +146,384 @@ func TestSubscription(t *testing.T) {
err = fx.DeleteMessage(ctx, resp.Messages[0].Id)
require.NoError(t, err)
require.Len(t, fx.events, 1)
require.Len(t, fx.events, 2)
ev := fx.events[0].GetChatDelete()
require.NotNil(t, ev)
assert.Equal(t, resp.Messages[0].Id, ev.Id)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatDelete{
ChatDelete: &pb.EventChatDelete{
Id: resp.Messages[0].Id,
SubIds: []string{"subId"},
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{},
Mentions: &model.ChatStateUnreadState{},
LastStateId: lastStateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
})
}
func TestSubscriptionMessageCounters(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
subscribeResp, err := fx.SubscribeLastMessages(ctx, "subId", 10, false)
require.NoError(t, err)
assert.Empty(t, subscribeResp.Messages)
assert.Equal(t, &model.ChatState{
Messages: &model.ChatStateUnreadState{},
Mentions: &model.ChatStateUnreadState{},
LastStateId: "",
}, subscribeResp.ChatState)
// Add first message
firstMessageId, err := fx.AddMessage(ctx, nil, givenSimpleMessage("first"))
require.NoError(t, err)
firstMessage, err := fx.GetMessageById(ctx, firstMessageId)
require.NoError(t, err)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: firstMessage.Id,
OrderId: firstMessage.OrderId,
AfterOrderId: "",
Message: firstMessage.ChatMessage,
SubIds: []string{"subId"},
Dependencies: nil,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: firstMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{},
LastStateId: firstMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
fx.events = nil
secondMessageId, err := fx.AddMessage(ctx, nil, givenSimpleMessage("second"))
require.NoError(t, err)
secondMessage, err := fx.GetMessageById(ctx, secondMessageId)
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: secondMessage.Id,
OrderId: secondMessage.OrderId,
AfterOrderId: firstMessage.OrderId,
Message: secondMessage.ChatMessage,
SubIds: []string{"subId"},
Dependencies: nil,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 2,
OldestOrderId: firstMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{},
LastStateId: secondMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
// Read first message
fx.events = nil
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, secondMessage.StateId, CounterTypeMessage)
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatUpdateMessageReadStatus{
ChatUpdateMessageReadStatus: &pb.EventChatUpdateMessageReadStatus{
SubIds: []string{"subId"},
Ids: []string{firstMessageId},
IsRead: true,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: secondMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{},
LastStateId: secondMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
}
func TestSubscriptionMentionCounters(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
subscribeResp, err := fx.SubscribeLastMessages(ctx, "subId", 10, false)
require.NoError(t, err)
assert.Empty(t, subscribeResp.Messages)
assert.Equal(t, &model.ChatState{
Messages: &model.ChatStateUnreadState{},
Mentions: &model.ChatStateUnreadState{},
LastStateId: "",
}, subscribeResp.ChatState)
// Add first message
firstMessageId, err := fx.AddMessage(ctx, nil, givenMessageWithMention("first"))
require.NoError(t, err)
firstMessage, err := fx.GetMessageById(ctx, firstMessageId)
require.NoError(t, err)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: firstMessage.Id,
OrderId: firstMessage.OrderId,
AfterOrderId: "",
Message: firstMessage.ChatMessage,
SubIds: []string{"subId"},
Dependencies: nil,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: firstMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: firstMessage.OrderId,
},
LastStateId: firstMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
fx.events = nil
secondMessageId, err := fx.AddMessage(ctx, nil, givenMessageWithMention("second"))
require.NoError(t, err)
secondMessage, err := fx.GetMessageById(ctx, secondMessageId)
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: secondMessage.Id,
OrderId: secondMessage.OrderId,
AfterOrderId: firstMessage.OrderId,
Message: secondMessage.ChatMessage,
SubIds: []string{"subId"},
Dependencies: nil,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 2,
OldestOrderId: firstMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{
Counter: 2,
OldestOrderId: firstMessage.OrderId,
},
LastStateId: secondMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
// Read first message
fx.events = nil
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, secondMessage.StateId, CounterTypeMention)
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatUpdateMentionReadStatus{
ChatUpdateMentionReadStatus: &pb.EventChatUpdateMentionReadStatus{
SubIds: []string{"subId"},
Ids: []string{firstMessageId},
IsRead: true,
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{
Counter: 2,
OldestOrderId: firstMessage.OrderId,
},
Mentions: &model.ChatStateUnreadState{
Counter: 1,
OldestOrderId: secondMessage.OrderId,
},
LastStateId: secondMessage.StateId,
},
SubIds: []string{"subId"},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
}
func TestSubscriptionWithDeps(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
_, err := fx.SubscribeLastMessages(ctx, LastMessageSubscriptionId, 10, false)
require.NoError(t, err)
myParticipantId := domain.NewParticipantId(testSpaceId, testCreator)
identityDetails := domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(myParticipantId),
bundle.RelationKeyName: domain.String("John Doe"),
})
err = fx.spaceIndex.UpdateObjectDetails(ctx, myParticipantId, identityDetails)
require.NoError(t, err)
attachmentDetails := domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String("fileObjectId1"),
bundle.RelationKeyName: domain.String("file 1"),
})
err = fx.spaceIndex.UpdateObjectDetails(ctx, "fileObjectId1", attachmentDetails)
require.NoError(t, err)
inputMessage := givenSimpleMessage("hello!")
inputMessage.Attachments = []*model.ChatMessageAttachment{
{
Target: attachmentDetails.GetString(bundle.RelationKeyId),
Type: model.ChatMessageAttachment_FILE,
},
{
Target: "unknown object id",
Type: model.ChatMessageAttachment_FILE,
},
}
messageId, err := fx.AddMessage(ctx, nil, inputMessage)
require.NoError(t, err)
message, err := fx.GetMessageById(ctx, messageId)
require.NoError(t, err)
wantEvents := []*pb.EventMessage{
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatAdd{
ChatAdd: &pb.EventChatAdd{
Id: message.Id,
OrderId: message.OrderId,
AfterOrderId: "",
Message: message.ChatMessage,
SubIds: []string{LastMessageSubscriptionId},
Dependencies: []*types.Struct{
identityDetails.ToProto(),
attachmentDetails.ToProto(),
},
},
},
},
{
SpaceId: testSpaceId,
Value: &pb.EventMessageValueOfChatStateUpdate{
ChatStateUpdate: &pb.EventChatUpdateState{
State: &model.ChatState{
Messages: &model.ChatStateUnreadState{},
Mentions: &model.ChatStateUnreadState{},
LastStateId: message.StateId,
},
SubIds: []string{LastMessageSubscriptionId},
},
},
},
}
assert.Equal(t, wantEvents, fx.events)
}

View file

@ -158,9 +158,6 @@ func (c *layoutConverter) fromAnyToBookmark(st *state.State) error {
}
func (c *layoutConverter) fromAnyToTodo(st *state.State) error {
if err := st.SetAlign(model.Block_AlignLeft); err != nil {
return err
}
template.InitTemplate(st,
template.WithTitle,
template.WithRelations([]domain.RelationKey{bundle.RelationKeyDone}),
@ -181,8 +178,6 @@ func (c *layoutConverter) fromNoteToSet(st *state.State) error {
func (c *layoutConverter) fromAnyToSet(st *state.State) error {
source := st.Details().GetStringList(bundle.RelationKeySetOf)
addFeaturedRelationSetOf(st)
dvBlock, err := dataview.BlockBySource(c.objectStore.SpaceIndex(st.SpaceID()), source, "")
if err != nil {
return err
@ -194,14 +189,6 @@ func (c *layoutConverter) fromAnyToSet(st *state.State) error {
return nil
}
func addFeaturedRelationSetOf(st *state.State) {
fr := st.Details().GetStringList(bundle.RelationKeyFeaturedRelations)
if !slices.Contains(fr, bundle.RelationKeySetOf.String()) {
fr = append(fr, bundle.RelationKeySetOf.String())
}
st.SetDetail(bundle.RelationKeyFeaturedRelations, domain.StringList(fr))
}
func (c *layoutConverter) fromSetToCollection(st *state.State) error {
dvBlock := st.Get(template.DataviewBlockId)
if dvBlock == nil {

View file

@ -95,7 +95,7 @@ func (ot *ObjectType) Init(ctx *smartblock.InitContext) (err error) {
func (ot *ObjectType) CreationStateMigration(ctx *smartblock.InitContext) migration.Migration {
return migration.Migration{
Version: 2,
Version: 4,
Proc: func(s *state.State) {
if len(ctx.ObjectTypeKeys) > 0 && len(ctx.State.ObjectTypeKeys()) == 0 {
ctx.State.SetObjectTypeKeys(ctx.ObjectTypeKeys)
@ -417,7 +417,7 @@ func (ot *ObjectType) dataviewTemplates() []template.StateTransformer {
dvContent.Dataview.TargetObjectId = ot.Id()
return []template.StateTransformer{
template.WithDataviewID(state.DataviewBlockID, dvContent, false),
template.WithDataviewIDIfNotExists(state.DataviewBlockID, dvContent, false),
template.WithForcedDetail(bundle.RelationKeySetOf, domain.StringList([]string{ot.Id()})),
}
}

View file

@ -26,10 +26,10 @@ const (
IdFromChange = "$changeId"
)
var lexId = lexid.Must(lexid.CharsAllNoEscape, 4, 100)
var LexId = lexid.Must(lexid.CharsAllNoEscape, 4, 100)
const (
collChangeOrders = "_change_orders"
CollChangeOrders = "_change_orders"
)
func New(ctx context.Context, id string, db anystore.DB, handlers ...Handler) (state *StoreState, err error) {
@ -104,7 +104,7 @@ type StoreState struct {
}
func (ss *StoreState) init(ctx context.Context) (err error) {
if ss.collChangeOrders, err = ss.Collection(ctx, collChangeOrders); err != nil {
if ss.collChangeOrders, err = ss.Collection(ctx, CollChangeOrders); err != nil {
return
}
for _, h := range ss.handlers {

View file

@ -48,7 +48,7 @@ func (stx *StoreStateTx) GetMaxOrder() string {
}
func (stx *StoreStateTx) NextOrder(prev string) string {
return lexId.Next(prev)
return LexId.Next(prev)
}
func (stx *StoreStateTx) SetOrder(changeId, order string) (err error) {

View file

@ -405,6 +405,22 @@ var WithAllBlocksEditsRestricted = StateTransformer(func(s *state.State) {
})
})
var WithDataviewIDIfNotExists = func(id string, dataview *model.BlockContentOfDataview, forceViews bool) StateTransformer {
return func(s *state.State) {
WithEmpty(s)
if !s.Exists(id) {
s.Set(simple.New(&model.Block{Content: dataview, Id: id}))
if !s.IsParentOf(s.RootId(), id) {
err := s.InsertTo(s.RootId(), model.Block_Inner, id)
if err != nil {
log.Errorf("template WithDataview failed to insert: %v", err)
}
}
}
}
}
var WithDataviewID = func(id string, dataview *model.BlockContentOfDataview, forceViews bool) StateTransformer {
return func(s *state.State) {
WithEmpty(s)
@ -414,10 +430,8 @@ var WithDataviewID = func(id string, dataview *model.BlockContentOfDataview, for
if dvBlock, ok := b.(simpleDataview.Block); !ok {
return true
} else {
if len(dvBlock.Model().GetDataview().Relations) == 0 ||
!slice.UnsortedEqual(dvBlock.Model().GetDataview().Source, dataview.Dataview.Source) ||
if !slice.UnsortedEqual(dvBlock.Model().GetDataview().Source, dataview.Dataview.Source) ||
len(dvBlock.Model().GetDataview().Views) == 0 ||
forceViews && len(dvBlock.Model().GetDataview().Relations) != len(dataview.Dataview.Relations) ||
forceViews && !pbtypes.DataviewViewsEqualSorted(dvBlock.Model().GetDataview().Views, dataview.Dataview.Views) {
/* log.With("object" s.RootId()).With("name", pbtypes.GetString(s.Details(), "name")).Warnf("dataview needs to be migrated: %v, %v, %v, %v",

View file

@ -166,7 +166,6 @@ func (oc *ObjectCreator) Create(dataObject *DataObject, sn *common.Snapshot) (*d
func canUpdateObject(sbType coresb.SmartBlockType) bool {
return sbType != coresb.SmartBlockTypeRelation &&
sbType != coresb.SmartBlockTypeObjectType &&
sbType != coresb.SmartBlockTypeRelationOption &&
sbType != coresb.SmartBlockTypeFileObject &&
sbType != coresb.SmartBlockTypeParticipant
@ -367,6 +366,13 @@ func (oc *ObjectCreator) setSpaceDashboardID(spaceID string, st *state.State) {
func (oc *ObjectCreator) resetState(newID string, st *state.State) *domain.Details {
var respDetails *domain.Details
err := cache.Do(oc.objectGetterDeleter, newID, func(b smartblock.SmartBlock) error {
currentRevision := b.Details().GetInt64(bundle.RelationKeyRevision)
newRevision := st.Details().GetInt64(bundle.RelationKeyRevision)
if currentRevision > newRevision {
// never update objects with older revision
// we use revision for bundled objects like relations and object types
return nil
}
err := history.ResetToVersion(b, st)
if err != nil {
log.With(zap.String("object id", newID)).Errorf("failed to set state %s: %s", newID, err)

View file

@ -42,6 +42,7 @@ import (
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core"
"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/filestore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
@ -494,7 +495,15 @@ func (i *Import) getObjectID(
return err
}
oldIDToNew[snapshot.Id] = id
if payload.RootRawChange != nil {
var isBundled bool
switch snapshot.Snapshot.SbType {
case smartblock.SmartBlockTypeObjectType:
isBundled = bundle.HasObjectTypeByKey(domain.TypeKey(snapshot.Snapshot.Data.Key))
case smartblock.SmartBlockTypeRelation:
isBundled = bundle.HasRelation(domain.RelationKey(snapshot.Snapshot.Data.Key))
}
// bundled types will be created and then updated, cause they can be installed asynchronously
if payload.RootRawChange != nil && !isBundled {
createPayloads[id] = payload
}
return i.extractInternalKey(snapshot, oldIDToNew)

View file

@ -54,6 +54,7 @@ var (
model.Restrictions_Publish,
}
sysTypesRestrictions = ObjectRestrictions{
model.Restrictions_Blocks,
model.Restrictions_LayoutChange,
model.Restrictions_TypeChange,
model.Restrictions_Template,

View file

@ -109,7 +109,7 @@ type Service struct {
}
type builtinObjects interface {
CreateObjectsForUseCase(ctx session.Context, spaceID string, req pb.RpcObjectImportUseCaseRequestUseCase) (code pb.RpcObjectImportUseCaseResponseErrorCode, err error)
CreateObjectsForUseCase(ctx session.Context, spaceID string, req pb.RpcObjectImportUseCaseRequestUseCase) (dashboardId string, code pb.RpcObjectImportUseCaseResponseErrorCode, err error)
}
type openedObjects struct {

View file

@ -276,17 +276,17 @@ func (_c *MockStore_Id_Call) RunAndReturn(run func() string) *MockStore_Id_Call
return _c
}
// InitDiffManager provides a mock function with given fields: ctx, seenHeads
func (_m *MockStore) InitDiffManager(ctx context.Context, seenHeads []string) error {
ret := _m.Called(ctx, seenHeads)
// InitDiffManager provides a mock function with given fields: ctx, name, seenHeads
func (_m *MockStore) InitDiffManager(ctx context.Context, name string, seenHeads []string) error {
ret := _m.Called(ctx, name, seenHeads)
if len(ret) == 0 {
panic("no return value specified for InitDiffManager")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok {
r0 = rf(ctx, seenHeads)
if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok {
r0 = rf(ctx, name, seenHeads)
} else {
r0 = ret.Error(0)
}
@ -301,14 +301,15 @@ type MockStore_InitDiffManager_Call struct {
// InitDiffManager is a helper method to define mock.On call
// - ctx context.Context
// - name string
// - seenHeads []string
func (_e *MockStore_Expecter) InitDiffManager(ctx interface{}, seenHeads interface{}) *MockStore_InitDiffManager_Call {
return &MockStore_InitDiffManager_Call{Call: _e.mock.On("InitDiffManager", ctx, seenHeads)}
func (_e *MockStore_Expecter) InitDiffManager(ctx interface{}, name interface{}, seenHeads interface{}) *MockStore_InitDiffManager_Call {
return &MockStore_InitDiffManager_Call{Call: _e.mock.On("InitDiffManager", ctx, name, seenHeads)}
}
func (_c *MockStore_InitDiffManager_Call) Run(run func(ctx context.Context, seenHeads []string)) *MockStore_InitDiffManager_Call {
func (_c *MockStore_InitDiffManager_Call) Run(run func(ctx context.Context, name string, seenHeads []string)) *MockStore_InitDiffManager_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].([]string))
run(args[0].(context.Context), args[1].(string), args[2].([]string))
})
return _c
}
@ -318,22 +319,22 @@ func (_c *MockStore_InitDiffManager_Call) Return(_a0 error) *MockStore_InitDiffM
return _c
}
func (_c *MockStore_InitDiffManager_Call) RunAndReturn(run func(context.Context, []string) error) *MockStore_InitDiffManager_Call {
func (_c *MockStore_InitDiffManager_Call) RunAndReturn(run func(context.Context, string, []string) error) *MockStore_InitDiffManager_Call {
_c.Call.Return(run)
return _c
}
// MarkSeenHeads provides a mock function with given fields: ctx, heads
func (_m *MockStore) MarkSeenHeads(ctx context.Context, heads []string) error {
ret := _m.Called(ctx, heads)
// MarkSeenHeads provides a mock function with given fields: ctx, name, heads
func (_m *MockStore) MarkSeenHeads(ctx context.Context, name string, heads []string) error {
ret := _m.Called(ctx, name, heads)
if len(ret) == 0 {
panic("no return value specified for MarkSeenHeads")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok {
r0 = rf(ctx, heads)
if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok {
r0 = rf(ctx, name, heads)
} else {
r0 = ret.Error(0)
}
@ -348,14 +349,15 @@ type MockStore_MarkSeenHeads_Call struct {
// MarkSeenHeads is a helper method to define mock.On call
// - ctx context.Context
// - name string
// - heads []string
func (_e *MockStore_Expecter) MarkSeenHeads(ctx interface{}, heads interface{}) *MockStore_MarkSeenHeads_Call {
return &MockStore_MarkSeenHeads_Call{Call: _e.mock.On("MarkSeenHeads", ctx, heads)}
func (_e *MockStore_Expecter) MarkSeenHeads(ctx interface{}, name interface{}, heads interface{}) *MockStore_MarkSeenHeads_Call {
return &MockStore_MarkSeenHeads_Call{Call: _e.mock.On("MarkSeenHeads", ctx, name, heads)}
}
func (_c *MockStore_MarkSeenHeads_Call) Run(run func(ctx context.Context, heads []string)) *MockStore_MarkSeenHeads_Call {
func (_c *MockStore_MarkSeenHeads_Call) Run(run func(ctx context.Context, name string, heads []string)) *MockStore_MarkSeenHeads_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].([]string))
run(args[0].(context.Context), args[1].(string), args[2].([]string))
})
return _c
}
@ -365,7 +367,7 @@ func (_c *MockStore_MarkSeenHeads_Call) Return(_a0 error) *MockStore_MarkSeenHea
return _c
}
func (_c *MockStore_MarkSeenHeads_Call) RunAndReturn(run func(context.Context, []string) error) *MockStore_MarkSeenHeads_Call {
func (_c *MockStore_MarkSeenHeads_Call) RunAndReturn(run func(context.Context, string, []string) error) *MockStore_MarkSeenHeads_Call {
_c.Call.Return(run)
return _c
}
@ -636,35 +638,36 @@ func (_c *MockStore_ReadStoreDoc_Call) RunAndReturn(run func(context.Context, *s
return _c
}
// SetDiffManagerOnRemoveHook provides a mock function with given fields: f
func (_m *MockStore) SetDiffManagerOnRemoveHook(f func([]string)) {
_m.Called(f)
// RegisterDiffManager provides a mock function with given fields: name, onRemoveHook
func (_m *MockStore) RegisterDiffManager(name string, onRemoveHook func([]string)) {
_m.Called(name, onRemoveHook)
}
// MockStore_SetDiffManagerOnRemoveHook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDiffManagerOnRemoveHook'
type MockStore_SetDiffManagerOnRemoveHook_Call struct {
// MockStore_RegisterDiffManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterDiffManager'
type MockStore_RegisterDiffManager_Call struct {
*mock.Call
}
// SetDiffManagerOnRemoveHook is a helper method to define mock.On call
// - f func([]string)
func (_e *MockStore_Expecter) SetDiffManagerOnRemoveHook(f interface{}) *MockStore_SetDiffManagerOnRemoveHook_Call {
return &MockStore_SetDiffManagerOnRemoveHook_Call{Call: _e.mock.On("SetDiffManagerOnRemoveHook", f)}
// RegisterDiffManager is a helper method to define mock.On call
// - name string
// - onRemoveHook func([]string)
func (_e *MockStore_Expecter) RegisterDiffManager(name interface{}, onRemoveHook interface{}) *MockStore_RegisterDiffManager_Call {
return &MockStore_RegisterDiffManager_Call{Call: _e.mock.On("RegisterDiffManager", name, onRemoveHook)}
}
func (_c *MockStore_SetDiffManagerOnRemoveHook_Call) Run(run func(f func([]string))) *MockStore_SetDiffManagerOnRemoveHook_Call {
func (_c *MockStore_RegisterDiffManager_Call) Run(run func(name string, onRemoveHook func([]string))) *MockStore_RegisterDiffManager_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(func([]string)))
run(args[0].(string), args[1].(func([]string)))
})
return _c
}
func (_c *MockStore_SetDiffManagerOnRemoveHook_Call) Return() *MockStore_SetDiffManagerOnRemoveHook_Call {
func (_c *MockStore_RegisterDiffManager_Call) Return() *MockStore_RegisterDiffManager_Call {
_c.Call.Return()
return _c
}
func (_c *MockStore_SetDiffManagerOnRemoveHook_Call) RunAndReturn(run func(func([]string))) *MockStore_SetDiffManagerOnRemoveHook_Call {
func (_c *MockStore_RegisterDiffManager_Call) RunAndReturn(run func(string, func([]string))) *MockStore_RegisterDiffManager_Call {
_c.Call.Return(run)
return _c
}
@ -747,17 +750,17 @@ func (_c *MockStore_SpaceID_Call) RunAndReturn(run func() string) *MockStore_Spa
return _c
}
// StoreSeenHeads provides a mock function with given fields: ctx
func (_m *MockStore) StoreSeenHeads(ctx context.Context) error {
ret := _m.Called(ctx)
// StoreSeenHeads provides a mock function with given fields: ctx, name
func (_m *MockStore) StoreSeenHeads(ctx context.Context, name string) error {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for StoreSeenHeads")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, name)
} else {
r0 = ret.Error(0)
}
@ -772,13 +775,14 @@ type MockStore_StoreSeenHeads_Call struct {
// StoreSeenHeads is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockStore_Expecter) StoreSeenHeads(ctx interface{}) *MockStore_StoreSeenHeads_Call {
return &MockStore_StoreSeenHeads_Call{Call: _e.mock.On("StoreSeenHeads", ctx)}
// - name string
func (_e *MockStore_Expecter) StoreSeenHeads(ctx interface{}, name interface{}) *MockStore_StoreSeenHeads_Call {
return &MockStore_StoreSeenHeads_Call{Call: _e.mock.On("StoreSeenHeads", ctx, name)}
}
func (_c *MockStore_StoreSeenHeads_Call) Run(run func(ctx context.Context)) *MockStore_StoreSeenHeads_Call {
func (_c *MockStore_StoreSeenHeads_Call) Run(run func(ctx context.Context, name string)) *MockStore_StoreSeenHeads_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
run(args[0].(context.Context), args[1].(string))
})
return _c
}
@ -788,7 +792,7 @@ func (_c *MockStore_StoreSeenHeads_Call) Return(_a0 error) *MockStore_StoreSeenH
return _c
}
func (_c *MockStore_StoreSeenHeads_Call) RunAndReturn(run func(context.Context) error) *MockStore_StoreSeenHeads_Call {
func (_c *MockStore_StoreSeenHeads_Call) RunAndReturn(run func(context.Context, string) error) *MockStore_StoreSeenHeads_Call {
_c.Call.Return(run)
return _c
}

View file

@ -193,7 +193,7 @@ func (s *service) newTreeSource(ctx context.Context, space Space, id string, bui
fileObjectMigrator: s.fileObjectMigrator,
}
if sbt == smartblock.SmartBlockTypeChatDerivedObject || sbt == smartblock.SmartBlockTypeAccountObject {
return &store{source: src, sbType: sbt}, nil
return &store{source: src, sbType: sbt, diffManagers: map[string]*diffManager{}}, nil
}
return src, nil

View file

@ -36,12 +36,18 @@ type Store interface {
ReadStoreDoc(ctx context.Context, stateStore *storestate.StoreState, onUpdateHook func()) (err error)
PushStoreChange(ctx context.Context, params PushStoreChangeParams) (changeId string, err error)
SetPushChangeHook(onPushChange PushChangeHook)
// RegisterDiffManager sets a hook that will be called when a change is removed (marked as read) from the diff manager
// must be called before ReadStoreDoc.
//
// If a head is marked as read in the diff manager, all earlier heads for that branch marked as read as well
RegisterDiffManager(name string, onRemoveHook func(removed []string))
// MarkSeenHeads marks heads as seen in a diff manager. Then the diff manager will call a hook from SetDiffManagerOnRemoveHook
MarkSeenHeads(ctx context.Context, heads []string) error
SetDiffManagerOnRemoveHook(f func(removed []string))
MarkSeenHeads(ctx context.Context, name string, heads []string) error
// StoreSeenHeads persists current seen heads in any-store
StoreSeenHeads(ctx context.Context) error
InitDiffManager(ctx context.Context, seenHeads []string) error
StoreSeenHeads(ctx context.Context, name string) error
// InitDiffManager initializes a diff manager with specified seen heads
InitDiffManager(ctx context.Context, name string, seenHeads []string) error
}
type PushStoreChangeParams struct {
@ -57,12 +63,17 @@ var (
type store struct {
*source
store *storestate.StoreState
onUpdateHook func()
onPushChange PushChangeHook
onDiffManagerRemove func(removed []string)
diffManager *objecttree.DiffManager
sbType smartblock.SmartBlockType
store *storestate.StoreState
onUpdateHook func()
onPushChange PushChangeHook
sbType smartblock.SmartBlockType
diffManagers map[string]*diffManager
}
type diffManager struct {
diffManager *objecttree.DiffManager
onRemove func(removed []string)
}
func (s *store) GetFileKeysSnapshot() []*pb.ChangeFileKeys {
@ -73,13 +84,34 @@ func (s *store) SetPushChangeHook(onPushChange PushChangeHook) {
s.onPushChange = onPushChange
}
// SetDiffManagerOnRemoveHook sets a hook that will be called when a change is removed from the diff manager
// must be called only before ReadStoreDoc
func (s *store) SetDiffManagerOnRemoveHook(f func(removed []string)) {
s.onDiffManagerRemove = f
func (s *store) RegisterDiffManager(name string, onRemoveHook func(removed []string)) {
if _, ok := s.diffManagers[name]; !ok {
s.diffManagers[name] = &diffManager{
onRemove: onRemoveHook,
}
}
}
func (s *store) InitDiffManager(ctx context.Context, seenHeads []string) (err error) {
func (s *store) initDiffManagers(ctx context.Context) error {
for name := range s.diffManagers {
seenHeads, err := s.loadSeenHeads(ctx, name)
if err != nil {
return fmt.Errorf("load seen heads: %w", err)
}
err = s.InitDiffManager(ctx, name, seenHeads)
if err != nil {
return fmt.Errorf("init diff manager: %w", err)
}
}
return nil
}
func (s *store) InitDiffManager(ctx context.Context, name string, seenHeads []string) (err error) {
manager, ok := s.diffManagers[name]
if !ok {
return nil
}
curTreeHeads := s.source.Tree().Heads()
buildTree := func(heads []string) (objecttree.ReadableObjectTree, error) {
@ -89,11 +121,16 @@ func (s *store) InitDiffManager(ctx context.Context, seenHeads []string) (err er
})
}
onRemove := func(removed []string) {
if s.onDiffManagerRemove != nil {
s.onDiffManagerRemove(removed)
if manager.onRemove != nil {
manager.onRemove(removed)
}
}
s.diffManager, err = objecttree.NewDiffManager(seenHeads, curTreeHeads, buildTree, onRemove)
manager.diffManager, err = objecttree.NewDiffManager(seenHeads, curTreeHeads, buildTree, onRemove)
if err != nil {
return fmt.Errorf("init diff manager: %w", err)
}
return
}
@ -136,11 +173,7 @@ func (s *store) ReadStoreDoc(ctx context.Context, storeState *storestate.StoreSt
s.onUpdateHook = onUpdateHook
s.store = storeState
seenHeads, err := s.loadSeenHeads(ctx)
if err != nil {
return fmt.Errorf("load seen heads: %w", err)
}
err = s.InitDiffManager(ctx, seenHeads)
err = s.initDiffManagers(ctx)
if err != nil {
return err
}
@ -216,10 +249,16 @@ func (s *store) PushStoreChange(ctx context.Context, params PushStoreChangeParam
if err != nil {
return "", err
}
s.diffManager.Add(&objecttree.Change{
Id: changeId,
PreviousIds: ch.PreviousIds,
})
for _, m := range s.diffManagers {
if m.diffManager != nil {
m.diffManager.Add(&objecttree.Change{
Id: changeId,
PreviousIds: ch.PreviousIds,
})
}
}
return changeId, err
}
@ -237,25 +276,42 @@ func (s *store) update(ctx context.Context, tree objecttree.ObjectTree) error {
return errors.Join(tx.Rollback(), err)
}
err = tx.Commit()
s.diffManager.Update(tree)
for _, m := range s.diffManagers {
if m.diffManager != nil {
m.diffManager.Update(tree)
}
}
if err == nil {
s.onUpdateHook()
}
return err
}
func (s *store) MarkSeenHeads(ctx context.Context, heads []string) error {
s.diffManager.Remove(heads)
return s.StoreSeenHeads(ctx)
func (s *store) MarkSeenHeads(ctx context.Context, name string, heads []string) error {
manager, ok := s.diffManagers[name]
if ok {
manager.diffManager.Remove(heads)
return s.StoreSeenHeads(ctx, name)
}
return nil
}
func (s *store) StoreSeenHeads(ctx context.Context) error {
coll, err := s.store.Collection(ctx, "seenHeads")
func seenHeadsCollectionName(name string) string {
return "seenHeads/" + name
}
func (s *store) StoreSeenHeads(ctx context.Context, name string) error {
manager, ok := s.diffManagers[name]
if !ok {
return nil
}
coll, err := s.store.Collection(ctx, seenHeadsCollectionName(name))
if err != nil {
return fmt.Errorf("get collection: %w", err)
}
seenHeads := s.diffManager.SeenHeads()
seenHeads := manager.diffManager.SeenHeads()
raw, err := json.Marshal(seenHeads)
if err != nil {
return fmt.Errorf("marshal seen heads: %w", err)
@ -268,8 +324,8 @@ func (s *store) StoreSeenHeads(ctx context.Context) error {
return coll.UpsertOne(ctx, doc)
}
func (s *store) loadSeenHeads(ctx context.Context) ([]string, error) {
coll, err := s.store.Collection(ctx, "seenHeads")
func (s *store) loadSeenHeads(ctx context.Context, name string) ([]string, error) {
coll, err := s.store.Collection(ctx, seenHeadsCollectionName(name))
if err != nil {
return nil, fmt.Errorf("get collection: %w", err)
}

View file

@ -8,61 +8,75 @@ import (
"github.com/anyproto/anytype-heart/core/block/chats"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func (mw *Middleware) ChatAddMessage(cctx context.Context, req *pb.RpcChatAddMessageRequest) *pb.RpcChatAddMessageResponse {
ctx := mw.newContext(cctx)
chatService := mustService[chats.Service](mw)
messageId, err := chatService.AddMessage(cctx, ctx, req.ChatObjectId, req.Message)
code := mapErrorCode[pb.RpcChatAddMessageResponseErrorCode](err)
messageId, err := chatService.AddMessage(cctx, ctx, req.ChatObjectId, &chatobject.Message{ChatMessage: req.Message})
if err != nil {
code := mapErrorCode[pb.RpcChatAddMessageResponseErrorCode](err)
return &pb.RpcChatAddMessageResponse{
Error: &pb.RpcChatAddMessageResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatAddMessageResponse{
MessageId: messageId,
Event: ctx.GetResponseEvent(),
Error: &pb.RpcChatAddMessageResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) ChatEditMessageContent(cctx context.Context, req *pb.RpcChatEditMessageContentRequest) *pb.RpcChatEditMessageContentResponse {
chatService := mustService[chats.Service](mw)
err := chatService.EditMessage(cctx, req.ChatObjectId, req.MessageId, req.EditedMessage)
code := mapErrorCode[pb.RpcChatEditMessageContentResponseErrorCode](err)
return &pb.RpcChatEditMessageContentResponse{
Error: &pb.RpcChatEditMessageContentResponseError{
Code: code,
Description: getErrorDescription(err),
},
err := chatService.EditMessage(cctx, req.ChatObjectId, req.MessageId, &chatobject.Message{ChatMessage: req.EditedMessage})
if err != nil {
code := mapErrorCode[pb.RpcChatEditMessageContentResponseErrorCode](err)
return &pb.RpcChatEditMessageContentResponse{
Error: &pb.RpcChatEditMessageContentResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatEditMessageContentResponse{}
}
func (mw *Middleware) ChatToggleMessageReaction(cctx context.Context, req *pb.RpcChatToggleMessageReactionRequest) *pb.RpcChatToggleMessageReactionResponse {
chatService := mustService[chats.Service](mw)
err := chatService.ToggleMessageReaction(cctx, req.ChatObjectId, req.MessageId, req.Emoji)
code := mapErrorCode[pb.RpcChatToggleMessageReactionResponseErrorCode](err)
return &pb.RpcChatToggleMessageReactionResponse{
Error: &pb.RpcChatToggleMessageReactionResponseError{
Code: code,
Description: getErrorDescription(err),
},
if err != nil {
code := mapErrorCode[pb.RpcChatToggleMessageReactionResponseErrorCode](err)
return &pb.RpcChatToggleMessageReactionResponse{
Error: &pb.RpcChatToggleMessageReactionResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatToggleMessageReactionResponse{}
}
func (mw *Middleware) ChatDeleteMessage(cctx context.Context, req *pb.RpcChatDeleteMessageRequest) *pb.RpcChatDeleteMessageResponse {
chatService := mustService[chats.Service](mw)
err := chatService.DeleteMessage(cctx, req.ChatObjectId, req.MessageId)
code := mapErrorCode[pb.RpcChatDeleteMessageResponseErrorCode](err)
return &pb.RpcChatDeleteMessageResponse{
Error: &pb.RpcChatDeleteMessageResponseError{
Code: code,
Description: getErrorDescription(err),
},
if err != nil {
code := mapErrorCode[pb.RpcChatDeleteMessageResponseErrorCode](err)
return &pb.RpcChatDeleteMessageResponse{
Error: &pb.RpcChatDeleteMessageResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatDeleteMessageResponse{}
}
func (mw *Middleware) ChatGetMessages(cctx context.Context, req *pb.RpcChatGetMessagesRequest) *pb.RpcChatGetMessagesResponse {
@ -74,14 +88,19 @@ func (mw *Middleware) ChatGetMessages(cctx context.Context, req *pb.RpcChatGetMe
Limit: int(req.Limit),
IncludeBoundary: req.IncludeBoundary,
})
code := mapErrorCode[pb.RpcChatGetMessagesResponseErrorCode](err)
if err != nil {
code := mapErrorCode[pb.RpcChatGetMessagesResponseErrorCode](err)
return &pb.RpcChatGetMessagesResponse{
Error: &pb.RpcChatGetMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatGetMessagesResponse{
Messages: resp.Messages,
Messages: messagesToProto(resp.Messages),
ChatState: resp.ChatState,
Error: &pb.RpcChatGetMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
@ -89,13 +108,17 @@ func (mw *Middleware) ChatGetMessagesByIds(cctx context.Context, req *pb.RpcChat
chatService := mustService[chats.Service](mw)
messages, err := chatService.GetMessagesByIds(cctx, req.ChatObjectId, req.MessageIds)
code := mapErrorCode[pb.RpcChatGetMessagesByIdsResponseErrorCode](err)
if err != nil {
code := mapErrorCode[pb.RpcChatGetMessagesByIdsResponseErrorCode](err)
return &pb.RpcChatGetMessagesByIdsResponse{
Error: &pb.RpcChatGetMessagesByIdsResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatGetMessagesByIdsResponse{
Messages: messages,
Error: &pb.RpcChatGetMessagesByIdsResponseError{
Code: code,
Description: getErrorDescription(err),
},
Messages: messagesToProto(messages),
}
}
@ -103,15 +126,19 @@ func (mw *Middleware) ChatSubscribeLastMessages(cctx context.Context, req *pb.Rp
chatService := mustService[chats.Service](mw)
resp, err := chatService.SubscribeLastMessages(cctx, req.ChatObjectId, int(req.Limit), req.SubId)
code := mapErrorCode[pb.RpcChatSubscribeLastMessagesResponseErrorCode](err)
if err != nil {
code := mapErrorCode[pb.RpcChatSubscribeLastMessagesResponseErrorCode](err)
return &pb.RpcChatSubscribeLastMessagesResponse{
Error: &pb.RpcChatSubscribeLastMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatSubscribeLastMessagesResponse{
Messages: resp.Messages,
Messages: messagesToProto(resp.Messages),
NumMessagesBefore: 0,
ChatState: resp.ChatState,
Error: &pb.RpcChatSubscribeLastMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
@ -119,26 +146,33 @@ func (mw *Middleware) ChatUnsubscribe(cctx context.Context, req *pb.RpcChatUnsub
chatService := mustService[chats.Service](mw)
err := chatService.Unsubscribe(req.ChatObjectId, req.SubId)
code := mapErrorCode[pb.RpcChatUnsubscribeResponseErrorCode](err)
return &pb.RpcChatUnsubscribeResponse{
Error: &pb.RpcChatUnsubscribeResponseError{
Code: code,
Description: getErrorDescription(err),
},
if err != nil {
code := mapErrorCode[pb.RpcChatUnsubscribeResponseErrorCode](err)
return &pb.RpcChatUnsubscribeResponse{
Error: &pb.RpcChatUnsubscribeResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatUnsubscribeResponse{}
}
func (mw *Middleware) ChatSubscribeToMessagePreviews(cctx context.Context, req *pb.RpcChatSubscribeToMessagePreviewsRequest) *pb.RpcChatSubscribeToMessagePreviewsResponse {
chatService := mustService[chats.Service](mw)
subId, err := chatService.SubscribeToMessagePreviews(cctx)
code := mapErrorCode[pb.RpcChatSubscribeToMessagePreviewsResponseErrorCode](err)
if err != nil {
code := mapErrorCode[pb.RpcChatSubscribeToMessagePreviewsResponseErrorCode](err)
return &pb.RpcChatSubscribeToMessagePreviewsResponse{
Error: &pb.RpcChatSubscribeToMessagePreviewsResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatSubscribeToMessagePreviewsResponse{
SubId: subId,
Error: &pb.RpcChatSubscribeToMessagePreviewsResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
@ -146,37 +180,60 @@ func (mw *Middleware) ChatUnsubscribeFromMessagePreviews(cctx context.Context, r
chatService := mustService[chats.Service](mw)
err := chatService.UnsubscribeFromMessagePreviews()
code := mapErrorCode[pb.RpcChatUnsubscribeFromMessagePreviewsResponseErrorCode](err)
return &pb.RpcChatUnsubscribeFromMessagePreviewsResponse{
Error: &pb.RpcChatUnsubscribeFromMessagePreviewsResponseError{
Code: code,
Description: getErrorDescription(err),
},
if err != nil {
code := mapErrorCode[pb.RpcChatUnsubscribeFromMessagePreviewsResponseErrorCode](err)
return &pb.RpcChatUnsubscribeFromMessagePreviewsResponse{
Error: &pb.RpcChatUnsubscribeFromMessagePreviewsResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatUnsubscribeFromMessagePreviewsResponse{}
}
func (mw *Middleware) ChatReadMessages(cctx context.Context, request *pb.RpcChatReadMessagesRequest) *pb.RpcChatReadMessagesResponse {
chatService := mustService[chats.Service](mw)
err := chatService.ReadMessages(cctx, request.ChatObjectId, request.AfterOrderId, request.BeforeOrderId, request.LastDbTimestamp)
code := mapErrorCode(err,
errToCode(anystore.ErrDocNotFound, pb.RpcChatReadMessagesResponseError_MESSAGES_NOT_FOUND),
)
return &pb.RpcChatReadMessagesResponse{
Error: &pb.RpcChatReadMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
err := chatService.ReadMessages(cctx, chats.ReadMessagesRequest{
ChatObjectId: request.ChatObjectId,
AfterOrderId: request.AfterOrderId,
BeforeOrderId: request.BeforeOrderId,
LastStateId: request.LastStateId,
CounterType: chatobject.CounterType(request.Type),
})
if err != nil {
code := mapErrorCode(err,
errToCode(anystore.ErrDocNotFound, pb.RpcChatReadMessagesResponseError_MESSAGES_NOT_FOUND),
)
return &pb.RpcChatReadMessagesResponse{
Error: &pb.RpcChatReadMessagesResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatReadMessagesResponse{}
}
func (mw *Middleware) ChatUnreadMessages(cctx context.Context, request *pb.RpcChatUnreadRequest) *pb.RpcChatUnreadResponse {
chatService := mustService[chats.Service](mw)
err := chatService.UnreadMessages(cctx, request.ChatObjectId, request.AfterOrderId)
code := mapErrorCode[pb.RpcChatUnreadResponseErrorCode](err)
return &pb.RpcChatUnreadResponse{
Error: &pb.RpcChatUnreadResponseError{
Code: code,
Description: getErrorDescription(err),
},
err := chatService.UnreadMessages(cctx, request.ChatObjectId, request.AfterOrderId, chatobject.CounterType(request.Type))
if err != nil {
code := mapErrorCode[pb.RpcChatUnreadResponseErrorCode](err)
return &pb.RpcChatUnreadResponse{
Error: &pb.RpcChatUnreadResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
return &pb.RpcChatUnreadResponse{}
}
func messagesToProto(msgs []*chatobject.Message) []*model.ChatMessage {
res := make([]*model.ChatMessage, 0, len(msgs))
for _, msg := range msgs {
res = append(res, msg.ChatMessage)
}
return res
}

View file

@ -22,7 +22,7 @@ func NewParticipantId(spaceId, identity string) string {
return fmt.Sprintf("%s%s_%s", ParticipantPrefix, spaceId, identity)
}
func ParseParticipantId(participantId string) (string, string, error) {
func ParseParticipantId(participantId string) (spaceId string, identity string, err error) {
if !strings.HasPrefix(participantId, ParticipantPrefix) {
return "", "", fmt.Errorf("participant id must start with _participant_")
}

View file

@ -15,6 +15,7 @@ type reindexFlags struct {
deletedObjects bool
eraseLinks bool
removeParticipants bool
chats bool
}
func (f *reindexFlags) any() bool {
@ -29,7 +30,8 @@ func (f *reindexFlags) any() bool {
f.removeOldFiles ||
f.deletedObjects ||
f.removeParticipants ||
f.eraseLinks
f.eraseLinks ||
f.chats
}
func (f *reindexFlags) enableAll() {
@ -45,6 +47,7 @@ func (f *reindexFlags) enableAll() {
f.deletedObjects = true
f.removeParticipants = true
f.eraseLinks = true
f.chats = true
}
func (f *reindexFlags) String() string {

View file

@ -10,7 +10,9 @@ import (
"github.com/anyproto/any-sync/commonspace/headsync/headstorage"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/core/block/object/objectcache"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/syncstatus/detailsupdater/helper"
@ -49,6 +51,7 @@ const (
ForceReindexDeletedObjectsCounter int32 = 1
ForceReindexParticipantsCounter int32 = 1
ForceReindexChatsCounter int32 = 1
)
type allDeletedIdsProvider interface {
@ -76,6 +79,7 @@ func (i *indexer) buildFlags(spaceID string) (reindexFlags, error) {
AreOldFilesRemoved: true,
ReindexDeletedObjects: 0, // Set to zero to force reindexing of deleted objects when objectstore was deleted
ReindexParticipants: ForceReindexParticipantsCounter,
ReindexChats: ForceReindexChatsCounter,
}
}
@ -116,6 +120,9 @@ func (i *indexer) buildFlags(spaceID string) (reindexFlags, error) {
if checksums.LinksErase != ForceLinksReindexCounter {
flags.eraseLinks = true
}
if checksums.ReindexChats != ForceReindexChatsCounter {
flags.chats = true
}
if spaceID == addr.AnytypeMarketplaceWorkspace && checksums.MarketplaceForceReindexCounter != ForceMarketplaceReindex {
flags.enableAll()
}
@ -201,6 +208,13 @@ func (i *indexer) ReindexSpace(space clientspace.Space) (err error) {
}()
}
if flags.chats {
err = i.reindexChats(ctx, space)
if err != nil {
log.Error("reindex chats", zap.Error(err))
}
}
if flags.deletedObjects {
err = i.reindexDeletedObjects(space)
if err != nil {
@ -220,6 +234,56 @@ func (i *indexer) ReindexSpace(space clientspace.Space) (err error) {
return i.saveLatestChecksums(space.Id())
}
func (i *indexer) reindexChats(ctx context.Context, space clientspace.Space) error {
ids, err := i.getIdsForTypes(space, coresb.SmartBlockTypeChatDerivedObject)
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
db := i.store.GetCrdtDb(space.Id())
txn, err := db.WriteTx(ctx)
if err != nil {
return fmt.Errorf("write tx: %w", err)
}
defer txn.Rollback()
for _, id := range ids {
col, err := db.OpenCollection(txn.Context(), id+chatobject.CollectionName)
if errors.Is(err, anystore.ErrCollectionNotFound) {
continue
}
if err != nil {
return fmt.Errorf("open collection: %w", err)
}
err = col.Drop(txn.Context())
if err != nil {
return fmt.Errorf("drop chat collection: %w", err)
}
col, err = db.OpenCollection(txn.Context(), id+storestate.CollChangeOrders)
if errors.Is(err, anystore.ErrCollectionNotFound) {
continue
}
if err != nil {
return fmt.Errorf("open orders collection: %w", err)
}
err = col.Drop(txn.Context())
if err != nil {
return fmt.Errorf("drop chat orders collection: %w", err)
}
}
err = txn.Commit()
if err != nil {
return fmt.Errorf("commit: %w", err)
}
return nil
}
func (i *indexer) addSyncDetails(space clientspace.Space) {
typesForSyncRelations := helper.SyncRelationsSmartblockTypes()
syncStatus := domain.ObjectSyncStatusSynced
@ -511,6 +575,7 @@ func (i *indexer) getLatestChecksums(isMarketplace bool) (checksums model.Object
LinksErase: ForceLinksReindexCounter,
ReindexDeletedObjects: ForceReindexDeletedObjectsCounter,
ReindexParticipants: ForceReindexParticipantsCounter,
ReindexChats: ForceReindexChatsCounter,
}
if isMarketplace {
checksums.MarketplaceForceReindexCounter = ForceMarketplaceReindex

View file

@ -693,7 +693,7 @@ func (mw *Middleware) ObjectImportNotionValidateToken(ctx context.Context,
func (mw *Middleware) ObjectImportUseCase(cctx context.Context, req *pb.RpcObjectImportUseCaseRequest) *pb.RpcObjectImportUseCaseResponse {
ctx := mw.newContext(cctx)
response := func(code pb.RpcObjectImportUseCaseResponseErrorCode, err error) *pb.RpcObjectImportUseCaseResponse {
response := func(_ string, code pb.RpcObjectImportUseCaseResponseErrorCode, err error) *pb.RpcObjectImportUseCaseResponse {
resp := &pb.RpcObjectImportUseCaseResponse{
Error: &pb.RpcObjectImportUseCaseResponseError{
Code: code,

View file

@ -18,6 +18,10 @@ var log = logging.Logger(CName).Desugar()
const CName = "core.subscription.crossspacesub"
var (
ErrSubscriptionNotFound = fmt.Errorf("subscription not found")
)
type Service interface {
app.ComponentRunnable
Subscribe(req subscriptionservice.SubscribeRequest) (resp *subscriptionservice.SubscribeResponse, err error)
@ -110,7 +114,7 @@ func (s *service) Unsubscribe(subId string) error {
sub, ok := s.subscriptions[subId]
if !ok {
return fmt.Errorf("subscription not found")
return ErrSubscriptionNotFound
}
err := sub.close()

View file

@ -1485,6 +1485,7 @@
- [Rpc.Chat.SubscribeLastMessages.Response.Error.Code](#anytype-Rpc-Chat-SubscribeLastMessages-Response-Error-Code)
- [Rpc.Chat.SubscribeToMessagePreviews.Response.Error.Code](#anytype-Rpc-Chat-SubscribeToMessagePreviews-Response-Error-Code)
- [Rpc.Chat.ToggleMessageReaction.Response.Error.Code](#anytype-Rpc-Chat-ToggleMessageReaction-Response-Error-Code)
- [Rpc.Chat.Unread.ReadType](#anytype-Rpc-Chat-Unread-ReadType)
- [Rpc.Chat.Unread.Response.Error.Code](#anytype-Rpc-Chat-Unread-Response-Error-Code)
- [Rpc.Chat.Unsubscribe.Response.Error.Code](#anytype-Rpc-Chat-Unsubscribe-Response-Error-Code)
- [Rpc.Chat.UnsubscribeFromMessagePreviews.Response.Error.Code](#anytype-Rpc-Chat-UnsubscribeFromMessagePreviews-Response-Error-Code)
@ -1817,8 +1818,9 @@
- [Event.Chat.Add](#anytype-Event-Chat-Add)
- [Event.Chat.Delete](#anytype-Event-Chat-Delete)
- [Event.Chat.Update](#anytype-Event-Chat-Update)
- [Event.Chat.UpdateMentionReadStatus](#anytype-Event-Chat-UpdateMentionReadStatus)
- [Event.Chat.UpdateMessageReadStatus](#anytype-Event-Chat-UpdateMessageReadStatus)
- [Event.Chat.UpdateReactions](#anytype-Event-Chat-UpdateReactions)
- [Event.Chat.UpdateReadStatus](#anytype-Event-Chat-UpdateReadStatus)
- [Event.Chat.UpdateState](#anytype-Event-Chat-UpdateState)
- [Event.File](#anytype-Event-File)
- [Event.File.LimitReached](#anytype-Event-File-LimitReached)
@ -10868,7 +10870,7 @@ Get marks list in the selected range in text block.
| chatObjectId | [string](#string) | | id of the chat object |
| afterOrderId | [string](#string) | | read from this orderId; if empty - read from the beginning of the chat |
| beforeOrderId | [string](#string) | | read til this orderId |
| lastDbTimestamp | [int64](#int64) | | dbTimestamp from the last processed ChatState event(or GetMessages). Used to prevent race conditions |
| lastStateId | [string](#string) | | stateId from the last processed ChatState event(or GetMessages). Used to prevent race conditions |
@ -11096,7 +11098,8 @@ Get marks list in the selected range in text block.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| chatObjectId | [string](#string) | | id of the chat object |
| type | [Rpc.Chat.Unread.ReadType](#anytype-Rpc-Chat-Unread-ReadType) | | |
| chatObjectId | [string](#string) | | |
| afterOrderId | [string](#string) | | |
@ -23741,8 +23744,8 @@ Middleware-to-front-end response, that can contain a NULL error or a non-NULL er
| Name | Number | Description |
| ---- | ------ | ----------- |
| messages | 0 | |
| replies | 1 | |
| Messages | 0 | |
| Mentions | 1 | |
@ -23799,6 +23802,18 @@ Middleware-to-front-end response, that can contain a NULL error or a non-NULL er
<a name="anytype-Rpc-Chat-Unread-ReadType"></a>
### Rpc.Chat.Unread.ReadType
| Name | Number | Description |
| ---- | ------ | ----------- |
| Messages | 0 | |
| Mentions | 1 | |
<a name="anytype-Rpc-Chat-Unread-Response-Error-Code"></a>
### Rpc.Chat.Unread.Response.Error.Code
@ -28666,7 +28681,6 @@ Precondition: user A opened a block
| ----- | ---- | ----- | ----------- |
| id | [string](#string) | | |
| subIds | [string](#string) | repeated | |
| state | [model.ChatState](#anytype-model-ChatState) | | Chat state. dbState should be persisted after rendered |
@ -28690,6 +28704,40 @@ Precondition: user A opened a block
<a name="anytype-Event-Chat-UpdateMentionReadStatus"></a>
### Event.Chat.UpdateMentionReadStatus
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| ids | [string](#string) | repeated | |
| isRead | [bool](#bool) | | |
| subIds | [string](#string) | repeated | |
<a name="anytype-Event-Chat-UpdateMessageReadStatus"></a>
### Event.Chat.UpdateMessageReadStatus
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| ids | [string](#string) | repeated | |
| isRead | [bool](#bool) | | |
| subIds | [string](#string) | repeated | |
<a name="anytype-Event-Chat-UpdateReactions"></a>
### Event.Chat.UpdateReactions
@ -28707,23 +28755,6 @@ Precondition: user A opened a block
<a name="anytype-Event-Chat-UpdateReadStatus"></a>
### Event.Chat.UpdateReadStatus
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| ids | [string](#string) | repeated | |
| isRead | [bool](#bool) | | |
| subIds | [string](#string) | repeated | |
<a name="anytype-Event-Chat-UpdateState"></a>
### Event.Chat.UpdateState
@ -28975,7 +29006,8 @@ Precondition: user A opened a block
| chatAdd | [Event.Chat.Add](#anytype-Event-Chat-Add) | | |
| chatUpdate | [Event.Chat.Update](#anytype-Event-Chat-Update) | | |
| chatUpdateReactions | [Event.Chat.UpdateReactions](#anytype-Event-Chat-UpdateReactions) | | |
| chatUpdateReadStatus | [Event.Chat.UpdateReadStatus](#anytype-Event-Chat-UpdateReadStatus) | | received to update per-message read status (if needed to highlight the unread messages in the UI) |
| chatUpdateMessageReadStatus | [Event.Chat.UpdateMessageReadStatus](#anytype-Event-Chat-UpdateMessageReadStatus) | | received to update per-message read status (if needed to highlight the unread messages in the UI) |
| chatUpdateMentionReadStatus | [Event.Chat.UpdateMentionReadStatus](#anytype-Event-Chat-UpdateMentionReadStatus) | | received to update per-message mention read status (if needed to highlight the unread mentions in the UI) |
| chatDelete | [Event.Chat.Delete](#anytype-Event-Chat-Delete) | | |
| chatStateUpdate | [Event.Chat.UpdateState](#anytype-Event-Chat-UpdateState) | | in case new unread messages received or chat state changed (e.g. message read on another device) |
| keyUpdate | [Event.Key.Update](#anytype-Event-Key-Update) | | |
@ -30127,6 +30159,7 @@ Precondition: user A and user B opened the same block
| marketplaceForceReindexCounter | [int32](#int32) | | |
| reindexDeletedObjects | [int32](#int32) | | |
| reindexParticipants | [int32](#int32) | | |
| reindexChats | [int32](#int32) | | |
@ -30869,16 +30902,17 @@ Used to decode block meta only, without the content itself
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [string](#string) | | Unique message identifier |
| orderId | [string](#string) | | Used for subscriptions |
| orderId | [string](#string) | | Lexicographical id for message in order of tree traversal |
| creator | [string](#string) | | Identifier for the message creator |
| createdAt | [int64](#int64) | | |
| modifiedAt | [int64](#int64) | | |
| addedAt | [int64](#int64) | | Message received and added to db at |
| stateId | [string](#string) | | stateId is ever-increasing id (BSON ObjectId) for this message. Unlike orderId, this ID is ordered by the time messages are added. For example, it&#39;s useful to prevent accidental reading of messages from the past when a ChatReadMessages request is sent: a message from the past may appear, but the client is still unaware of it |
| replyToMessageId | [string](#string) | | Identifier for the message being replied to |
| message | [ChatMessage.MessageContent](#anytype-model-ChatMessage-MessageContent) | | Message content |
| attachments | [ChatMessage.Attachment](#anytype-model-ChatMessage-Attachment) | repeated | Attachments slice |
| reactions | [ChatMessage.Reactions](#anytype-model-ChatMessage-Reactions) | | Reactions to the message |
| read | [bool](#bool) | | Message read status |
| mentionRead | [bool](#bool) | | |
@ -30974,7 +31008,7 @@ Used to decode block meta only, without the content itself
| ----- | ---- | ----- | ----------- |
| messages | [ChatState.UnreadState](#anytype-model-ChatState-UnreadState) | | unread messages |
| mentions | [ChatState.UnreadState](#anytype-model-ChatState-UnreadState) | | unread mentions |
| dbTimestamp | [int64](#int64) | | reflects the state of the chat db at the moment of sending response/event that includes this state |
| lastStateId | [string](#string) | | reflects the state of the chat db at the moment of sending response/event that includes this state |

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -8312,15 +8312,16 @@ message Rpc {
message ReadMessages {
enum ReadType {
messages = 0;
replies = 1;
Messages = 0;
Mentions = 1;
}
message Request {
ReadType type = 1;
string chatObjectId = 2; // id of the chat object
string afterOrderId = 3; // read from this orderId; if empty - read from the beginning of the chat
string beforeOrderId = 4; // read til this orderId
int64 lastDbTimestamp = 5; // dbTimestamp from the last processed ChatState event(or GetMessages). Used to prevent race conditions
string lastStateId = 5; // stateId from the last processed ChatState event(or GetMessages). Used to prevent race conditions
}
message Response {
@ -8343,8 +8344,14 @@ message Rpc {
}
message Unread {
enum ReadType {
Messages = 0;
Mentions = 1;
}
message Request {
string chatObjectId = 2; // id of the chat object
ReadType type = 1;
string chatObjectId = 2;
string afterOrderId = 3;
}

View file

@ -117,7 +117,8 @@ message Event {
Chat.Add chatAdd = 128;
Chat.Update chatUpdate = 129;
Chat.UpdateReactions chatUpdateReactions = 130;
Chat.UpdateReadStatus chatUpdateReadStatus = 134; // received to update per-message read status (if needed to highlight the unread messages in the UI)
Chat.UpdateMessageReadStatus chatUpdateMessageReadStatus = 134; // received to update per-message read status (if needed to highlight the unread messages in the UI)
Chat.UpdateMentionReadStatus chatUpdateMentionReadStatus = 135; // received to update per-message mention read status (if needed to highlight the unread mentions in the UI)
Chat.Delete chatDelete = 131;
Chat.UpdateState chatStateUpdate = 133; // in case new unread messages received or chat state changed (e.g. message read on another device)
@ -137,7 +138,6 @@ message Event {
message Delete {
string id = 1;
repeated string subIds = 2;
model.ChatState state = 3; // Chat state. dbState should be persisted after rendered
}
message Update {
string id = 1;
@ -150,7 +150,12 @@ message Event {
repeated string subIds = 3;
}
message UpdateReadStatus {
message UpdateMessageReadStatus {
repeated string ids = 1;
bool isRead = 2;
repeated string subIds = 3;
}
message UpdateMentionReadStatus {
repeated string ids = 1;
bool isRead = 2;
repeated string subIds = 3;

View file

@ -90,7 +90,7 @@ func makeFilterByCondition(spaceID string, rawFilter FilterRequest, store Object
relationKey := domain.RelationKey(parts[0])
nestedRelationKey := domain.RelationKey(parts[1])
if rawFilter.Condition == model.BlockContentDataviewFilter_NotEqual {
if rawFilter.Condition == model.BlockContentDataviewFilter_NotEqual || rawFilter.Condition == model.BlockContentDataviewFilter_NotIn {
return makeFilterNestedNotIn(spaceID, rawFilter, store, relationKey, nestedRelationKey)
} else {
return makeFilterNestedIn(spaceID, rawFilter, store, relationKey, nestedRelationKey)
@ -858,13 +858,29 @@ type FilterNestedNotIn struct {
IDs []string
}
func negativeConditionToPositive(cond model.BlockContentDataviewFilterCondition) (model.BlockContentDataviewFilterCondition, error) {
switch cond {
case model.BlockContentDataviewFilter_NotEqual:
return model.BlockContentDataviewFilter_Equal, nil
case model.BlockContentDataviewFilter_NotIn:
return model.BlockContentDataviewFilter_In, nil
default:
return 0, fmt.Errorf("condition %d is not supported", cond)
}
}
func makeFilterNestedNotIn(spaceID string, rawFilter FilterRequest, store ObjectStore, relationKey domain.RelationKey, nestedRelationKey domain.RelationKey) (Filter, error) {
rawNestedFilter := rawFilter
rawNestedFilter.RelationKey = nestedRelationKey
cond, err := negativeConditionToPositive(rawFilter.Condition)
if err != nil {
return nil, fmt.Errorf("convert condition: %w", err)
}
subQueryRawFilter := rawFilter
subQueryRawFilter.RelationKey = nestedRelationKey
subQueryRawFilter.Condition = model.BlockContentDataviewFilter_Equal
subQueryRawFilter.Condition = cond
subQueryFilter, err := MakeFilter(spaceID, subQueryRawFilter, store)
if err != nil {

View file

@ -1263,3 +1263,67 @@ func TestDsObjectStore_QueryAndProcess(t *testing.T) {
assert.Equal(t, []string{"id3"}, ids)
})
}
func TestNestedFilters(t *testing.T) {
t.Run("not in", func(t *testing.T) {
store := NewStoreFixture(t)
store.AddObjects(t, []TestObject{
{
bundle.RelationKeyId: domain.String("id1"),
bundle.RelationKeyType: domain.String("templateType"),
},
{
bundle.RelationKeyId: domain.String("id2"),
bundle.RelationKeyType: domain.String("pageType"),
},
{
bundle.RelationKeyId: domain.String("id3"),
bundle.RelationKeyType: domain.String("pageType"),
},
{
bundle.RelationKeyId: domain.String("id4"),
bundle.RelationKeyType: domain.String("hiddenType"),
},
{
bundle.RelationKeyId: domain.String("templateType"),
bundle.RelationKeyUniqueKey: domain.String("ot-template"),
},
{
bundle.RelationKeyId: domain.String("pageType"),
bundle.RelationKeyUniqueKey: domain.String("ot-page"),
},
{
bundle.RelationKeyId: domain.String("hiddenType"),
bundle.RelationKeyUniqueKey: domain.String("ot-hidden"),
},
})
got, err := store.Query(database.Query{
Filters: []database.FilterRequest{
{
RelationKey: "type.uniqueKey",
Condition: model.BlockContentDataviewFilter_NotIn,
Value: domain.StringList([]string{"ot-hidden", "ot-template"}),
},
},
})
require.NoError(t, err)
assertRecordsHaveIds(t, got, []string{"id2", "id3", "templateType", "pageType", "hiddenType"})
})
}
func assertRecordsHaveIds(t *testing.T, records []database.Record, wantIds []string) {
require.Equal(t, len(wantIds), len(records))
gotIds := map[string]struct{}{}
for _, r := range records {
gotIds[r.Details.GetString(bundle.RelationKeyId)] = struct{}{}
}
for _, id := range wantIds {
_, ok := gotIds[id]
require.True(t, ok)
}
}

View file

@ -453,6 +453,7 @@ type ObjectStoreChecksums struct {
MarketplaceForceReindexCounter int32 `protobuf:"varint,15,opt,name=marketplaceForceReindexCounter,proto3" json:"marketplaceForceReindexCounter,omitempty"`
ReindexDeletedObjects int32 `protobuf:"varint,16,opt,name=reindexDeletedObjects,proto3" json:"reindexDeletedObjects,omitempty"`
ReindexParticipants int32 `protobuf:"varint,17,opt,name=reindexParticipants,proto3" json:"reindexParticipants,omitempty"`
ReindexChats int32 `protobuf:"varint,18,opt,name=reindexChats,proto3" json:"reindexChats,omitempty"`
}
func (m *ObjectStoreChecksums) Reset() { *m = ObjectStoreChecksums{} }
@ -607,6 +608,13 @@ func (m *ObjectStoreChecksums) GetReindexParticipants() int32 {
return 0
}
func (m *ObjectStoreChecksums) GetReindexChats() int32 {
if m != nil {
return m.ReindexChats
}
return 0
}
func init() {
proto.RegisterType((*ObjectInfo)(nil), "anytype.model.ObjectInfo")
proto.RegisterType((*ObjectDetails)(nil), "anytype.model.ObjectDetails")
@ -623,55 +631,56 @@ func init() {
}
var fileDescriptor_9c35df71910469a5 = []byte{
// 766 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xcb, 0x6e, 0xd3, 0x40,
0x14, 0xad, 0x93, 0x26, 0x69, 0x6e, 0x48, 0x1f, 0x53, 0x10, 0x43, 0x41, 0x96, 0x15, 0x55, 0x28,
0xaa, 0x20, 0x81, 0x3e, 0x36, 0x2c, 0x40, 0x6a, 0x4b, 0xa5, 0x42, 0xa5, 0x20, 0xb7, 0x08, 0x89,
0x9d, 0x1d, 0x4f, 0xda, 0x21, 0x13, 0x8f, 0xe5, 0x19, 0xa3, 0x66, 0xc1, 0x92, 0x0d, 0x2b, 0x7e,
0x80, 0xff, 0x61, 0xd9, 0x25, 0x4b, 0xd4, 0xfe, 0x01, 0xe2, 0x03, 0x90, 0x67, 0x9c, 0xd4, 0x71,
0xdd, 0x14, 0x09, 0x96, 0x3e, 0xf7, 0x9c, 0xe3, 0x33, 0xf7, 0x7a, 0xae, 0xa1, 0x19, 0xf4, 0x8f,
0xdb, 0x8c, 0xba, 0xed, 0xc0, 0x6d, 0x0f, 0xb8, 0x47, 0x58, 0x3b, 0x08, 0xb9, 0xe4, 0xa2, 0xcd,
0x78, 0xd7, 0x61, 0x42, 0xf2, 0x90, 0xb4, 0x14, 0x82, 0xea, 0x8e, 0x3f, 0x94, 0xc3, 0x80, 0xb4,
0x14, 0x6d, 0xe5, 0xc1, 0x31, 0xe7, 0xc7, 0x8c, 0x68, 0xba, 0x1b, 0xf5, 0xda, 0x42, 0x86, 0x51,
0x57, 0x6a, 0xf2, 0xca, 0xea, 0x75, 0xb6, 0xea, 0x41, 0x68, 0x56, 0xe3, 0x97, 0x01, 0xd0, 0x71,
0x3f, 0x90, 0xae, 0xdc, 0xf7, 0x7b, 0x1c, 0xcd, 0x43, 0x81, 0x7a, 0xd8, 0xb0, 0x8c, 0x66, 0xd5,
0x2e, 0x50, 0x0f, 0x3d, 0x84, 0x79, 0xae, 0xaa, 0x47, 0xc3, 0x80, 0xbc, 0x0d, 0x99, 0xc0, 0x05,
0xab, 0xd8, 0xac, 0xda, 0x19, 0x14, 0x3d, 0x85, 0x8a, 0x47, 0xa4, 0x43, 0x99, 0xc0, 0x45, 0xcb,
0x68, 0xd6, 0xd6, 0xef, 0xb6, 0x74, 0xb8, 0xd6, 0x28, 0x5c, 0xeb, 0x50, 0x85, 0xb3, 0x47, 0x3c,
0xb4, 0x05, 0xd5, 0x90, 0x30, 0x47, 0x52, 0xee, 0x0b, 0x3c, 0x6b, 0x15, 0x95, 0x68, 0xe2, 0x80,
0x2d, 0x3b, 0xa9, 0xdb, 0x97, 0x4c, 0x84, 0xa1, 0x22, 0x7c, 0x1a, 0x04, 0x44, 0xe2, 0x92, 0x8a,
0x39, 0x7a, 0x44, 0x4d, 0x58, 0x38, 0x71, 0xc4, 0xbe, 0xef, 0xf2, 0xc8, 0xf7, 0x0e, 0xa8, 0xdf,
0x17, 0xb8, 0x6c, 0x19, 0xcd, 0x39, 0x3b, 0x0b, 0x37, 0xb6, 0xa1, 0xae, 0xcf, 0xbc, 0x9b, 0x64,
0x49, 0xc5, 0x37, 0xfe, 0x2e, 0x7e, 0xa3, 0x03, 0x35, 0xed, 0xa1, 0x2c, 0x91, 0x09, 0x40, 0xf5,
0x2b, 0xf6, 0x77, 0x63, 0x93, 0xb8, 0x49, 0x29, 0x04, 0x59, 0x50, 0xe3, 0x91, 0x1c, 0x13, 0x74,
0x17, 0xd3, 0x50, 0xe3, 0x13, 0x2c, 0xa4, 0x0c, 0xd5, 0x34, 0x36, 0xa0, 0x92, 0x58, 0x28, 0xc7,
0xda, 0xfa, 0xbd, 0x4c, 0x83, 0x2e, 0x27, 0x67, 0x8f, 0x98, 0x68, 0x0b, 0xe6, 0x46, 0xb6, 0xea,
0x35, 0x53, 0x55, 0x63, 0x6a, 0xe3, 0x8b, 0x01, 0xcb, 0x97, 0x85, 0x77, 0x54, 0x9e, 0xe8, 0x83,
0x65, 0xbf, 0x88, 0xc7, 0x30, 0x4b, 0xfd, 0x1e, 0xc7, 0x05, 0xd5, 0xa7, 0x29, 0xd6, 0x8a, 0x86,
0x36, 0xa1, 0xc4, 0xd4, 0x28, 0xf4, 0x67, 0x61, 0xe6, 0xf2, 0xc7, 0x27, 0xb6, 0x35, 0xb9, 0xf1,
0xcd, 0x80, 0xfb, 0x93, 0x61, 0x3a, 0x49, 0xce, 0xff, 0x12, 0xea, 0x05, 0xd4, 0x79, 0xda, 0x0f,
0x17, 0x6f, 0xea, 0xd3, 0x24, 0xbf, 0xf1, 0xd9, 0x00, 0x73, 0x4a, 0xbe, 0x78, 0xe0, 0xff, 0x18,
0x71, 0x35, 0x2f, 0x62, 0x35, 0x9b, 0xe3, 0x77, 0x19, 0x6e, 0x6b, 0xe9, 0x61, 0xbc, 0x26, 0x76,
0x4e, 0x48, 0xb7, 0x2f, 0xa2, 0x81, 0x40, 0x2d, 0x40, 0x6e, 0xe4, 0x7b, 0x8c, 0x78, 0x9d, 0xf1,
0x45, 0x15, 0x49, 0x9a, 0x9c, 0x0a, 0x5a, 0x83, 0xc5, 0x04, 0xb5, 0xc7, 0x77, 0xb2, 0xa0, 0xd8,
0x57, 0xf0, 0x78, 0x27, 0x24, 0xd8, 0x81, 0x33, 0xe4, 0x91, 0xd4, 0xb3, 0xad, 0xda, 0x19, 0x14,
0x3d, 0x87, 0x15, 0xbd, 0x25, 0xc4, 0x1e, 0x0f, 0xbb, 0xc4, 0x26, 0xd4, 0xf7, 0xc8, 0xe9, 0x0e,
0x8f, 0x7c, 0x49, 0x42, 0x3c, 0x6b, 0x19, 0xcd, 0x92, 0x3d, 0x85, 0x81, 0x9e, 0x01, 0xee, 0x51,
0x46, 0x72, 0xd5, 0x25, 0xa5, 0xbe, 0xb6, 0x8e, 0x1e, 0xc1, 0x12, 0xf5, 0x4e, 0x6d, 0xe2, 0x46,
0x94, 0x79, 0x23, 0x51, 0x59, 0x89, 0xae, 0x16, 0xe2, 0xcd, 0xd1, 0x8b, 0x18, 0x93, 0xe4, 0x54,
0x26, 0x15, 0x5c, 0x51, 0xdc, 0x2c, 0x1c, 0x8f, 0x65, 0x04, 0xbd, 0x0c, 0x1d, 0x41, 0x70, 0x4d,
0xf1, 0x26, 0xc1, 0x54, 0x37, 0x8f, 0xc8, 0x20, 0x60, 0x8e, 0x24, 0x02, 0xcf, 0x4d, 0x74, 0x73,
0x8c, 0xa7, 0xba, 0xa9, 0xe7, 0x21, 0x70, 0x55, 0x59, 0x66, 0x50, 0xf4, 0x0a, 0x2c, 0x75, 0xda,
0x78, 0xce, 0xaf, 0xc9, 0x30, 0xb7, 0x2b, 0xa0, 0x94, 0x37, 0xf2, 0xe2, 0xaf, 0xc3, 0x09, 0x49,
0x87, 0x79, 0x7b, 0x31, 0xd3, 0x26, 0x03, 0xfe, 0x91, 0x78, 0xf8, 0x96, 0x5a, 0x96, 0x39, 0x95,
0x78, 0x92, 0x4e, 0x48, 0x76, 0x09, 0x23, 0x72, 0x1c, 0x28, 0xb1, 0x24, 0x1e, 0xae, 0x2b, 0xdd,
0x14, 0x46, 0xbc, 0x1c, 0xd5, 0xbd, 0xd6, 0x2d, 0x9b, 0x57, 0x29, 0x53, 0x08, 0xda, 0x03, 0x73,
0xe0, 0x84, 0x7d, 0x22, 0x03, 0xe6, 0x74, 0x49, 0xde, 0xc9, 0x16, 0x94, 0xe6, 0x06, 0x16, 0xda,
0x84, 0x3b, 0xa1, 0x46, 0x26, 0x93, 0xe0, 0x45, 0x25, 0xcf, 0x2f, 0xa2, 0x27, 0xb0, 0x9c, 0x14,
0xde, 0x38, 0xa1, 0xa4, 0x5d, 0x1a, 0x38, 0xbe, 0x14, 0x78, 0x49, 0x69, 0xf2, 0x4a, 0xdb, 0x6b,
0xdf, 0xcf, 0x4d, 0xe3, 0xec, 0xdc, 0x34, 0x7e, 0x9e, 0x9b, 0xc6, 0xd7, 0x0b, 0x73, 0xe6, 0xec,
0xc2, 0x9c, 0xf9, 0x71, 0x61, 0xce, 0xbc, 0x5f, 0xcc, 0xfe, 0x74, 0xdd, 0xb2, 0xfa, 0x83, 0x6c,
0xfc, 0x09, 0x00, 0x00, 0xff, 0xff, 0x63, 0x84, 0x4f, 0x64, 0xe6, 0x07, 0x00, 0x00,
// 783 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xcf, 0x4f, 0xdb, 0x48,
0x14, 0xc6, 0x09, 0x21, 0xe4, 0x85, 0xf0, 0x63, 0xd8, 0xd5, 0xce, 0xb2, 0x2b, 0xcb, 0xb2, 0xd0,
0x2a, 0x42, 0xbb, 0xc9, 0x96, 0x1f, 0x97, 0x1e, 0x5a, 0x09, 0x28, 0x12, 0x2d, 0x52, 0x2a, 0x43,
0x55, 0xa9, 0x37, 0x3b, 0x9e, 0x90, 0x69, 0x26, 0x1e, 0xcb, 0x33, 0xae, 0xc8, 0xa1, 0xc7, 0x5e,
0x7a, 0xea, 0xb1, 0x97, 0xfe, 0x3f, 0x3d, 0x72, 0xec, 0xb1, 0x82, 0xff, 0xa0, 0x7f, 0x41, 0xe5,
0x19, 0x27, 0x38, 0xc6, 0x84, 0x4a, 0xed, 0xd1, 0xdf, 0xfb, 0xbe, 0xcf, 0xdf, 0xbc, 0xe7, 0x79,
0x86, 0x66, 0x38, 0x38, 0x6f, 0x33, 0xea, 0xb5, 0x43, 0xaf, 0x3d, 0xe4, 0x3e, 0x61, 0xed, 0x30,
0xe2, 0x92, 0x8b, 0x36, 0xe3, 0x5d, 0x97, 0x09, 0xc9, 0x23, 0xd2, 0x52, 0x08, 0x6a, 0xb8, 0xc1,
0x48, 0x8e, 0x42, 0xd2, 0x52, 0xb4, 0x8d, 0xbf, 0xcf, 0x39, 0x3f, 0x67, 0x44, 0xd3, 0xbd, 0xb8,
0xd7, 0x16, 0x32, 0x8a, 0xbb, 0x52, 0x93, 0x37, 0x36, 0xef, 0xb2, 0x55, 0x0f, 0x42, 0xb3, 0xec,
0x6f, 0x06, 0x40, 0xc7, 0x7b, 0x4d, 0xba, 0xf2, 0x38, 0xe8, 0x71, 0xb4, 0x0c, 0x25, 0xea, 0x63,
0xc3, 0x32, 0x9a, 0x35, 0xa7, 0x44, 0x7d, 0xf4, 0x0f, 0x2c, 0x73, 0x55, 0x3d, 0x1b, 0x85, 0xe4,
0x45, 0xc4, 0x04, 0x2e, 0x59, 0xe5, 0x66, 0xcd, 0xc9, 0xa1, 0xe8, 0x01, 0x54, 0x7d, 0x22, 0x5d,
0xca, 0x04, 0x2e, 0x5b, 0x46, 0xb3, 0xbe, 0xfd, 0x47, 0x4b, 0x87, 0x6b, 0x8d, 0xc3, 0xb5, 0x4e,
0x55, 0x38, 0x67, 0xcc, 0x43, 0x7b, 0x50, 0x8b, 0x08, 0x73, 0x25, 0xe5, 0x81, 0xc0, 0xf3, 0x56,
0x59, 0x89, 0xa6, 0x0e, 0xd8, 0x72, 0xd2, 0xba, 0x73, 0xc3, 0x44, 0x18, 0xaa, 0x22, 0xa0, 0x61,
0x48, 0x24, 0xae, 0xa8, 0x98, 0xe3, 0x47, 0xd4, 0x84, 0x95, 0xbe, 0x2b, 0x8e, 0x03, 0x8f, 0xc7,
0x81, 0x7f, 0x42, 0x83, 0x81, 0xc0, 0x0b, 0x96, 0xd1, 0x5c, 0x74, 0xf2, 0xb0, 0xbd, 0x0f, 0x0d,
0x7d, 0xe6, 0xc3, 0x34, 0x4b, 0x26, 0xbe, 0xf1, 0x63, 0xf1, 0xed, 0x0e, 0xd4, 0xb5, 0x87, 0xb2,
0x44, 0x26, 0x00, 0xd5, 0xaf, 0x38, 0x3e, 0x4c, 0x4c, 0x92, 0x26, 0x65, 0x10, 0x64, 0x41, 0x9d,
0xc7, 0x72, 0x42, 0xd0, 0x5d, 0xcc, 0x42, 0xf6, 0x5b, 0x58, 0xc9, 0x18, 0xaa, 0x69, 0xec, 0x40,
0x35, 0xb5, 0x50, 0x8e, 0xf5, 0xed, 0x3f, 0x73, 0x0d, 0xba, 0x99, 0x9c, 0x33, 0x66, 0xa2, 0x3d,
0x58, 0x1c, 0xdb, 0xaa, 0xd7, 0xcc, 0x54, 0x4d, 0xa8, 0xf6, 0x7b, 0x03, 0xd6, 0x6f, 0x0a, 0x2f,
0xa9, 0xec, 0xeb, 0x83, 0xe5, 0xbf, 0x88, 0xff, 0x60, 0x9e, 0x06, 0x3d, 0x8e, 0x4b, 0xaa, 0x4f,
0x33, 0xac, 0x15, 0x0d, 0xed, 0x42, 0x85, 0xa9, 0x51, 0xe8, 0xcf, 0xc2, 0x2c, 0xe4, 0x4f, 0x4e,
0xec, 0x68, 0xb2, 0xfd, 0xc9, 0x80, 0xbf, 0xa6, 0xc3, 0x74, 0xd2, 0x9c, 0xbf, 0x24, 0xd4, 0x63,
0x68, 0xf0, 0xac, 0x1f, 0x2e, 0xdf, 0xd7, 0xa7, 0x69, 0xbe, 0xfd, 0xce, 0x00, 0x73, 0x46, 0xbe,
0x64, 0xe0, 0x3f, 0x19, 0x71, 0xb3, 0x28, 0x62, 0x2d, 0x9f, 0xe3, 0x63, 0x15, 0x7e, 0xd3, 0xd2,
0xd3, 0x64, 0x4d, 0x1c, 0xf4, 0x49, 0x77, 0x20, 0xe2, 0xa1, 0x40, 0x2d, 0x40, 0x5e, 0x1c, 0xf8,
0x8c, 0xf8, 0x9d, 0xc9, 0x45, 0x15, 0x69, 0x9a, 0x82, 0x0a, 0xda, 0x82, 0xd5, 0x14, 0x75, 0x26,
0x77, 0xb2, 0xa4, 0xd8, 0xb7, 0xf0, 0x64, 0x27, 0xa4, 0xd8, 0x89, 0x3b, 0xe2, 0xb1, 0xd4, 0xb3,
0xad, 0x39, 0x39, 0x14, 0x3d, 0x82, 0x0d, 0xbd, 0x25, 0xc4, 0x11, 0x8f, 0xba, 0xc4, 0x21, 0x34,
0xf0, 0xc9, 0xc5, 0x01, 0x8f, 0x03, 0x49, 0x22, 0x3c, 0x6f, 0x19, 0xcd, 0x8a, 0x33, 0x83, 0x81,
0x1e, 0x02, 0xee, 0x51, 0x46, 0x0a, 0xd5, 0x15, 0xa5, 0xbe, 0xb3, 0x8e, 0xfe, 0x85, 0x35, 0xea,
0x5f, 0x38, 0xc4, 0x8b, 0x29, 0xf3, 0xc7, 0xa2, 0x05, 0x25, 0xba, 0x5d, 0x48, 0x36, 0x47, 0x2f,
0x66, 0x4c, 0x92, 0x0b, 0x99, 0x56, 0x70, 0x55, 0x71, 0xf3, 0x70, 0x32, 0x96, 0x31, 0xf4, 0x24,
0x72, 0x05, 0xc1, 0x75, 0xc5, 0x9b, 0x06, 0x33, 0xdd, 0x3c, 0x23, 0xc3, 0x90, 0xb9, 0x92, 0x08,
0xbc, 0x38, 0xd5, 0xcd, 0x09, 0x9e, 0xe9, 0xa6, 0x9e, 0x87, 0xc0, 0x35, 0x65, 0x99, 0x43, 0xd1,
0x53, 0xb0, 0xd4, 0x69, 0x93, 0x39, 0x3f, 0x23, 0xa3, 0xc2, 0xae, 0x80, 0x52, 0xde, 0xcb, 0x4b,
0xbe, 0x0e, 0x37, 0x22, 0x1d, 0xe6, 0x1f, 0x25, 0x4c, 0x87, 0x0c, 0xf9, 0x1b, 0xe2, 0xe3, 0x25,
0xb5, 0x2c, 0x0b, 0x2a, 0xc9, 0x24, 0xdd, 0x88, 0x1c, 0x12, 0x46, 0xe4, 0x24, 0x50, 0x6a, 0x49,
0x7c, 0xdc, 0x50, 0xba, 0x19, 0x8c, 0x64, 0x39, 0xaa, 0x7b, 0xad, 0x5b, 0xb6, 0xac, 0x52, 0x66,
0x10, 0x74, 0x04, 0xe6, 0xd0, 0x8d, 0x06, 0x44, 0x86, 0xcc, 0xed, 0x92, 0xa2, 0x93, 0xad, 0x28,
0xcd, 0x3d, 0x2c, 0xb4, 0x0b, 0xbf, 0x47, 0x1a, 0x99, 0x4e, 0x82, 0x57, 0x95, 0xbc, 0xb8, 0x88,
0xfe, 0x87, 0xf5, 0xb4, 0xf0, 0xdc, 0x8d, 0x24, 0xed, 0xd2, 0xd0, 0x0d, 0xa4, 0xc0, 0x6b, 0x4a,
0x53, 0x54, 0x42, 0x36, 0x2c, 0xa5, 0xf0, 0x41, 0xdf, 0x95, 0x02, 0x23, 0x45, 0x9d, 0xc2, 0xf6,
0xb7, 0x3e, 0x5f, 0x99, 0xc6, 0xe5, 0x95, 0x69, 0x7c, 0xbd, 0x32, 0x8d, 0x0f, 0xd7, 0xe6, 0xdc,
0xe5, 0xb5, 0x39, 0xf7, 0xe5, 0xda, 0x9c, 0x7b, 0xb5, 0x9a, 0xff, 0x31, 0x7b, 0x0b, 0xea, 0x2f,
0xb3, 0xf3, 0x3d, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xb3, 0x53, 0xec, 0x0a, 0x08, 0x00, 0x00,
}
func (m *ObjectInfo) Marshal() (dAtA []byte, err error) {
@ -1064,6 +1073,13 @@ func (m *ObjectStoreChecksums) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.ReindexChats != 0 {
i = encodeVarintLocalstore(dAtA, i, uint64(m.ReindexChats))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0x90
}
if m.ReindexParticipants != 0 {
i = encodeVarintLocalstore(dAtA, i, uint64(m.ReindexParticipants))
i--
@ -1404,6 +1420,9 @@ func (m *ObjectStoreChecksums) Size() (n int) {
if m.ReindexParticipants != 0 {
n += 2 + sovLocalstore(uint64(m.ReindexParticipants))
}
if m.ReindexChats != 0 {
n += 2 + sovLocalstore(uint64(m.ReindexChats))
}
return n
}
@ -2829,6 +2848,25 @@ func (m *ObjectStoreChecksums) Unmarshal(dAtA []byte) error {
break
}
}
case 18:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field ReindexChats", wireType)
}
m.ReindexChats = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowLocalstore
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.ReindexChats |= int32(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipLocalstore(dAtA[iNdEx:])

File diff suppressed because it is too large Load diff

View file

@ -65,4 +65,5 @@ message ObjectStoreChecksums {
int32 marketplaceForceReindexCounter = 15;
int32 reindexDeletedObjects = 16;
int32 reindexParticipants = 17;
int32 reindexChats = 18;
}

View file

@ -1394,21 +1394,25 @@ message ChatState {
}
UnreadState messages = 1; // unread messages
UnreadState mentions = 2; // unread mentions
int64 dbTimestamp = 3; // reflects the state of the chat db at the moment of sending response/event that includes this state
string lastStateId = 3; // reflects the state of the chat db at the moment of sending response/event that includes this state
}
message ChatMessage {
string id = 1; // Unique message identifier
string orderId = 2; // Used for subscriptions
string orderId = 2; // Lexicographical id for message in order of tree traversal
string creator = 3; // Identifier for the message creator
int64 createdAt = 4;
int64 modifiedAt = 9;
int64 addedAt = 11; // Message received and added to db at
// stateId is ever-increasing id (BSON ObjectId) for this message. Unlike orderId, this ID is ordered by the time messages are added. For example, it's useful to prevent accidental reading of messages from the past when a ChatReadMessages request is sent: a message from the past may appear, but the client is still unaware of it
string stateId = 11;
string replyToMessageId = 5; // Identifier for the message being replied to
MessageContent message = 6; // Message content
repeated Attachment attachments = 7; // Attachments slice
Reactions reactions = 8; // Reactions to the message
bool read = 10; // Message read status
bool mentionRead = 12;
message MessageContent {
string text = 1; // The text content of the message part
Block.Content.Text.Style style = 2; // The style/type of the message part

View file

@ -87,7 +87,7 @@ func createAccountAndStartApp(t *testing.T, defaultUsecase pb.RpcObjectImportUse
eventQueue: eventQueue,
}
objCreator := getService[builtinobjects.BuiltinObjects](testApp)
_, err = objCreator.CreateObjectsForUseCase(session.NewContext(), acc.Info.AccountSpaceId, defaultUsecase)
_, _, err = objCreator.CreateObjectsForUseCase(session.NewContext(), acc.Info.AccountSpaceId, defaultUsecase)
require.NoError(t, err)
t.Cleanup(func() {

View file

@ -87,7 +87,7 @@ var (
type BuiltinObjects interface {
app.Component
CreateObjectsForUseCase(ctx session.Context, spaceID string, req pb.RpcObjectImportUseCaseRequestUseCase) (code pb.RpcObjectImportUseCaseResponseErrorCode, err error)
CreateObjectsForUseCase(ctx session.Context, spaceID string, req pb.RpcObjectImportUseCaseRequestUseCase) (dashboardId string, code pb.RpcObjectImportUseCaseResponseErrorCode, err error)
CreateObjectsForExperience(ctx context.Context, spaceID, url, title string, newSpace bool) (err error)
InjectMigrationDashboard(spaceID string) error
}
@ -127,21 +127,21 @@ func (b *builtinObjects) CreateObjectsForUseCase(
ctx session.Context,
spaceID string,
useCase pb.RpcObjectImportUseCaseRequestUseCase,
) (code pb.RpcObjectImportUseCaseResponseErrorCode, err error) {
) (dashboardId string, code pb.RpcObjectImportUseCaseResponseErrorCode, err error) {
if useCase == pb.RpcObjectImportUseCaseRequest_NONE {
return pb.RpcObjectImportUseCaseResponseError_NULL, nil
return "", pb.RpcObjectImportUseCaseResponseError_NULL, nil
}
start := time.Now()
archive, found := archives[useCase]
if !found {
return pb.RpcObjectImportUseCaseResponseError_BAD_INPUT,
return "", pb.RpcObjectImportUseCaseResponseError_BAD_INPUT,
fmt.Errorf("failed to import builtinObjects: invalid Use Case value: %v", useCase)
}
if err = b.inject(ctx, spaceID, useCase, archive); err != nil {
return pb.RpcObjectImportUseCaseResponseError_UNKNOWN_ERROR,
if dashboardId, err = b.inject(ctx, spaceID, useCase, archive); err != nil {
return "", pb.RpcObjectImportUseCaseResponseError_UNKNOWN_ERROR,
fmt.Errorf("failed to import builtinObjects for Use Case %s: %w",
pb.RpcObjectImportUseCaseRequestUseCase_name[int32(useCase)], err)
}
@ -151,7 +151,7 @@ func (b *builtinObjects) CreateObjectsForUseCase(
log.Debugf("built-in objects injection time exceeded timeout of %s and is %s", injectionTimeout.String(), spent.String())
}
return pb.RpcObjectImportUseCaseResponseError_NULL, nil
return dashboardId, pb.RpcObjectImportUseCaseResponseError_NULL, nil
}
func (b *builtinObjects) CreateObjectsForExperience(ctx context.Context, spaceID, url, title string, isNewSpace bool) (err error) {
@ -223,21 +223,22 @@ func (b *builtinObjects) provideNotification(spaceID string, progress process.Pr
}
func (b *builtinObjects) InjectMigrationDashboard(spaceID string) error {
return b.inject(nil, spaceID, migrationUseCase, migrationDashboardZip)
_, err := b.inject(nil, spaceID, migrationUseCase, migrationDashboardZip)
return err
}
func (b *builtinObjects) inject(ctx session.Context, spaceID string, useCase pb.RpcObjectImportUseCaseRequestUseCase, archive []byte) (err error) {
func (b *builtinObjects) inject(ctx session.Context, spaceID string, useCase pb.RpcObjectImportUseCaseRequestUseCase, archive []byte) (dashboardId string, err error) {
path := filepath.Join(b.tempDirService.TempDir(), time.Now().Format("tmp.20060102.150405.99")+".zip")
if err = os.WriteFile(path, archive, 0644); err != nil {
return fmt.Errorf("failed to save use case archive to temporary file: %w", err)
return "", fmt.Errorf("failed to save use case archive to temporary file: %w", err)
}
if err = b.importArchive(context.Background(), spaceID, path, "", pb.RpcObjectImportRequestPbParams_SPACE, nil, false); err != nil {
return err
return "", err
}
// TODO: GO-2627 Home page handling should be moved to importer
b.handleHomePage(path, spaceID, func() {
dashboardId = b.handleHomePage(path, spaceID, func() {
if rmErr := os.Remove(path); rmErr != nil {
log.Errorf("failed to remove temporary file: %v", anyerror.CleanupError(rmErr))
}
@ -259,7 +260,7 @@ func (b *builtinObjects) importArchive(
importRequest := &importer.ImportRequest{
RpcObjectImportRequest: &pb.RpcObjectImportRequest{
SpaceId: spaceID,
UpdateExistingObjects: false,
UpdateExistingObjects: true,
Type: model.Import_Pb,
Mode: pb.RpcObjectImportRequest_ALL_OR_NOTHING,
NoProgress: progress == nil,
@ -282,7 +283,7 @@ func (b *builtinObjects) importArchive(
return res.Err
}
func (b *builtinObjects) handleHomePage(path, spaceId string, removeFunc func(), isMigration bool) {
func (b *builtinObjects) handleHomePage(path, spaceId string, removeFunc func(), isMigration bool) (dashboardId string) {
defer removeFunc()
oldID := migrationDashboardName
if !isMigration {
@ -311,7 +312,9 @@ func (b *builtinObjects) handleHomePage(path, spaceId string, removeFunc func(),
log.Errorf("failed to get space: %w", err)
return
}
dashboardId = newID
b.setHomePageIdToWorkspace(spc, newID)
return
}
func (b *builtinObjects) getOldHomePageId(zipReader *zip.Reader) (id string, err error) {