package protocol

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"math/rand"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/golang/protobuf/proto"
	"github.com/google/uuid"
	"github.com/pkg/errors"
	"github.com/waku-org/go-waku/waku/v2/protocol/relay"
	"go.uber.org/zap"
	"golang.org/x/time/rate"

	gethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/event"
	"github.com/ethereum/go-ethereum/p2p"
	"github.com/status-im/status-go/account"
	"github.com/status-im/status-go/appmetrics"
	"github.com/status-im/status-go/connection"
	"github.com/status-im/status-go/contracts"
	"github.com/status-im/status-go/deprecation"
	"github.com/status-im/status-go/eth-node/crypto"
	"github.com/status-im/status-go/eth-node/types"
	"github.com/status-im/status-go/images"
	"github.com/status-im/status-go/multiaccounts"
	"github.com/status-im/status-go/multiaccounts/accounts"
	"github.com/status-im/status-go/multiaccounts/settings"
	sociallinkssettings "github.com/status-im/status-go/multiaccounts/settings_social_links"
	"github.com/status-im/status-go/protocol/anonmetrics"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/communities"
	"github.com/status-im/status-go/protocol/encryption"
	"github.com/status-im/status-go/protocol/encryption/multidevice"
	"github.com/status-im/status-go/protocol/encryption/sharedsecret"
	"github.com/status-im/status-go/protocol/ens"
	"github.com/status-im/status-go/protocol/identity"
	"github.com/status-im/status-go/protocol/identity/alias"
	"github.com/status-im/status-go/protocol/identity/identicon"
	"github.com/status-im/status-go/protocol/protobuf"
	"github.com/status-im/status-go/protocol/pushnotificationclient"
	"github.com/status-im/status-go/protocol/pushnotificationserver"
	"github.com/status-im/status-go/protocol/requests"
	"github.com/status-im/status-go/protocol/sqlite"
	"github.com/status-im/status-go/protocol/transport"
	v1protocol "github.com/status-im/status-go/protocol/v1"
	"github.com/status-im/status-go/protocol/verification"
	"github.com/status-im/status-go/server"
	"github.com/status-im/status-go/services/browsers"
	ensservice "github.com/status-im/status-go/services/ens"
	"github.com/status-im/status-go/services/ext/mailservers"
	mailserversDB "github.com/status-im/status-go/services/mailservers"
	"github.com/status-im/status-go/services/wallet"
	"github.com/status-im/status-go/services/wallet/token"
	"github.com/status-im/status-go/signal"
	"github.com/status-im/status-go/telemetry"
)

// todo: kozieiev: get rid of wakutransp word
type chatContext string

const (
	PubKeyStringLength = 132

	transactionSentTxt = "Transaction sent"

	publicChat  chatContext = "public-chat"
	privateChat chatContext = "private-chat"
)

const messageResendMinDelay = 30
const messageResendMaxCount = 3

var communityAdvertiseIntervalSecond int64 = 60 * 60

// messageCacheIntervalMs is how long we should keep processed messages in the cache, in ms
var messageCacheIntervalMs uint64 = 1000 * 60 * 60 * 48

// Messenger is a entity managing chats and messages.
// It acts as a bridge between the application and encryption
// layers.
// It needs to expose an interface to manage installations
// because installations are managed by the user.
// Similarly, it needs to expose an interface to manage
// mailservers because they can also be managed by the user.
type Messenger struct {
	node                      types.Node
	server                    *p2p.Server
	peerStore                 *mailservers.PeerStore
	config                    *config
	identity                  *ecdsa.PrivateKey
	persistence               *sqlitePersistence
	transport                 *transport.Transport
	encryptor                 *encryption.Protocol
	sender                    *common.MessageSender
	ensVerifier               *ens.Verifier
	anonMetricsClient         *anonmetrics.Client
	anonMetricsServer         *anonmetrics.Server
	pushNotificationClient    *pushnotificationclient.Client
	pushNotificationServer    *pushnotificationserver.Server
	communitiesManager        *communities.Manager
	communitiesKeyDistributor CommunitiesKeyDistributor
	accountsManager           account.Manager
	mentionsManager           *MentionManager
	logger                    *zap.Logger

	outputCSV bool
	csvFile   *os.File

	verifyTransactionClient    EthClient
	featureFlags               common.FeatureFlags
	shutdownTasks              []func() error
	shouldPublishContactCode   bool
	systemMessagesTranslations *systemMessageTranslationsMap
	allChats                   *chatMap
	allContacts                *contactMap
	allInstallations           *installationMap
	modifiedInstallations      *stringBoolMap
	installationID             string
	mailserverCycle            mailserverCycle
	database                   *sql.DB
	multiAccounts              *multiaccounts.Database
	mailservers                *mailserversDB.Database
	settings                   *accounts.Database
	account                    *multiaccounts.Account
	mailserversDatabase        *mailserversDB.Database
	browserDatabase            *browsers.Database
	httpServer                 *server.MediaServer

	quit   chan struct{}
	ctx    context.Context
	cancel context.CancelFunc

	importingCommunities map[string]bool
	importRateLimiter    *rate.Limiter
	importDelayer        struct {
		wait chan struct{}
		once sync.Once
	}

	requestedCommunitiesLock sync.RWMutex
	requestedCommunities     map[string]*transport.Filter

	requestedContactsLock sync.RWMutex
	requestedContacts     map[string]*transport.Filter

	connectionState                      connection.State
	telemetryClient                      *telemetry.Client
	contractMaker                        *contracts.ContractMaker
	downloadHistoryArchiveTasksWaitGroup sync.WaitGroup
	verificationDatabase                 *verification.Persistence
	savedAddressesManager                *wallet.SavedAddressesManager
	walletAPI                            *wallet.API

	// TODO(samyoul) Determine if/how the remaining usage of this mutex can be removed
	mutex                     sync.Mutex
	mailPeersMutex            sync.Mutex
	handleMessagesMutex       sync.Mutex
	handleImportMessagesMutex sync.Mutex

	// flag to disable checking #hasPairedDevices
	localPairing bool
	// flag to enable backedup messages processing, false by default
	processBackedupMessages bool
}

type connStatus int

const (
	disconnected connStatus = iota + 1
	connecting
	connected
)

type peerStatus struct {
	status                connStatus
	canConnectAfter       time.Time
	lastConnectionAttempt time.Time
	mailserver            mailserversDB.Mailserver
}
type mailserverCycle struct {
	sync.RWMutex
	activeMailserver          *mailserversDB.Mailserver
	peers                     map[string]peerStatus
	events                    chan *p2p.PeerEvent
	subscription              event.Subscription
	availabilitySubscriptions []chan struct{}
}

type EnvelopeEventsInterceptor struct {
	EnvelopeEventsHandler transport.EnvelopeEventsHandler
	Messenger             *Messenger
}

// EnvelopeSent triggered when envelope delivered at least to 1 peer.
func (interceptor EnvelopeEventsInterceptor) EnvelopeSent(identifiers [][]byte) {
	if interceptor.Messenger != nil {
		var ids []string
		for _, identifierBytes := range identifiers {
			ids = append(ids, types.EncodeHex(identifierBytes))
		}

		err := interceptor.Messenger.processSentMessages(ids)
		if err != nil {
			interceptor.Messenger.logger.Info("messenger failed to process sent messages", zap.Error(err))
		}

		// We notify the client, regardless whether we were able to mark them as sent
		interceptor.EnvelopeEventsHandler.EnvelopeSent(identifiers)
	} else {
		// NOTE(rasom): In case if interceptor.Messenger is not nil and
		// some error occurred on processing sent message we don't want
		// to send envelop.sent signal to the client, thus `else` cause
		// is necessary.
		interceptor.EnvelopeEventsHandler.EnvelopeSent(identifiers)
	}
}

// EnvelopeExpired triggered when envelope is expired but wasn't delivered to any peer.
func (interceptor EnvelopeEventsInterceptor) EnvelopeExpired(identifiers [][]byte, err error) {
	//we don't track expired events in Messenger, so just redirect to handler
	interceptor.EnvelopeEventsHandler.EnvelopeExpired(identifiers, err)
}

// MailServerRequestCompleted triggered when the mailserver sends a message to notify that the request has been completed
func (interceptor EnvelopeEventsInterceptor) MailServerRequestCompleted(requestID types.Hash, lastEnvelopeHash types.Hash, cursor []byte, err error) {
	//we don't track mailserver requests in Messenger, so just redirect to handler
	interceptor.EnvelopeEventsHandler.MailServerRequestCompleted(requestID, lastEnvelopeHash, cursor, err)
}

// MailServerRequestExpired triggered when the mailserver request expires
func (interceptor EnvelopeEventsInterceptor) MailServerRequestExpired(hash types.Hash) {
	//we don't track mailserver requests in Messenger, so just redirect to handler
	interceptor.EnvelopeEventsHandler.MailServerRequestExpired(hash)
}

func NewMessenger(
	nodeName string,
	identity *ecdsa.PrivateKey,
	node types.Node,
	installationID string,
	peerStore *mailservers.PeerStore,
	accountsManager account.Manager,
	opts ...Option,
) (*Messenger, error) {
	var messenger *Messenger

	c := config{}

	for _, opt := range opts {
		if err := opt(&c); err != nil {
			return nil, err
		}
	}

	logger := c.logger
	if c.logger == nil {
		var err error
		if logger, err = zap.NewDevelopment(); err != nil {
			return nil, errors.Wrap(err, "failed to create a logger")
		}
	}

	if c.systemMessagesTranslations == nil {
		c.systemMessagesTranslations = defaultSystemMessagesTranslations
	}

	// Configure the database.
	if c.appDb == nil {
		return nil, errors.New("database instance or database path needs to be provided")
	}
	database := c.appDb

	// Apply any post database creation changes to the database
	for _, opt := range c.afterDbCreatedHooks {
		if err := opt(&c); err != nil {
			return nil, err
		}
	}

	// Apply migrations for all components.
	err := sqlite.Migrate(database)
	if err != nil {
		return nil, errors.Wrap(err, "failed to apply migrations")
	}

	// Initialize transport layer.
	var transp *transport.Transport

	if waku, err := node.GetWaku(nil); err == nil && waku != nil {
		transp, err = transport.NewTransport(
			waku,
			identity,
			database,
			"waku_keys",
			nil,
			c.envelopesMonitorConfig,
			logger,
		)
		if err != nil {
			return nil, errors.Wrap(err, "failed to create  Transport")
		}
	} else {
		logger.Info("failed to find Waku service; trying WakuV2", zap.Error(err))
		wakuV2, err := node.GetWakuV2(nil)
		if err != nil || wakuV2 == nil {
			return nil, errors.Wrap(err, "failed to find Whisper and Waku V1/V2 services")
		}
		transp, err = transport.NewTransport(
			wakuV2,
			identity,
			database,
			"wakuv2_keys",
			nil,
			c.envelopesMonitorConfig,
			logger,
		)
		if err != nil {
			return nil, errors.Wrap(err, "failed to create  Transport")
		}
	}

	// Initialize encryption layer.
	encryptionProtocol := encryption.New(
		database,
		installationID,
		logger,
	)

	sender, err := common.NewMessageSender(
		identity,
		database,
		encryptionProtocol,
		transp,
		logger,
		c.featureFlags,
	)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create messageSender")
	}

	// Initialise anon metrics client
	var anonMetricsClient *anonmetrics.Client
	if c.anonMetricsClientConfig != nil &&
		c.anonMetricsClientConfig.ShouldSend &&
		c.anonMetricsClientConfig.Active == anonmetrics.ActiveClientPhrase {

		anonMetricsClient = anonmetrics.NewClient(sender)
		anonMetricsClient.Config = c.anonMetricsClientConfig
		anonMetricsClient.Identity = identity
		anonMetricsClient.DB = appmetrics.NewDB(database)
		anonMetricsClient.Logger = logger
	}

	// Initialise anon metrics server
	var anonMetricsServer *anonmetrics.Server
	if c.anonMetricsServerConfig != nil &&
		c.anonMetricsServerConfig.Enabled &&
		c.anonMetricsServerConfig.Active == anonmetrics.ActiveServerPhrase {

		server, err := anonmetrics.NewServer(c.anonMetricsServerConfig.PostgresURI)
		if err != nil {
			return nil, errors.Wrap(err, "failed to create anonmetrics.Server")
		}

		anonMetricsServer = server
		anonMetricsServer.Config = c.anonMetricsServerConfig
		anonMetricsServer.Logger = logger
	}

	var telemetryClient *telemetry.Client
	if c.telemetryServerURL != "" {
		telemetryClient = telemetry.NewClient(logger, c.telemetryServerURL, c.account.KeyUID, nodeName)
	}

	// Initialize push notification server
	var pushNotificationServer *pushnotificationserver.Server
	if c.pushNotificationServerConfig != nil && c.pushNotificationServerConfig.Enabled {
		c.pushNotificationServerConfig.Identity = identity
		pushNotificationServerPersistence := pushnotificationserver.NewSQLitePersistence(database)
		pushNotificationServer = pushnotificationserver.New(c.pushNotificationServerConfig, pushNotificationServerPersistence, sender)
	}

	// Initialize push notification client
	pushNotificationClientPersistence := pushnotificationclient.NewPersistence(database)
	pushNotificationClientConfig := c.pushNotificationClientConfig
	if pushNotificationClientConfig == nil {
		pushNotificationClientConfig = &pushnotificationclient.Config{}
	}

	sqlitePersistence := newSQLitePersistence(database)
	// Overriding until we handle different identities
	pushNotificationClientConfig.Identity = identity
	pushNotificationClientConfig.Logger = logger
	pushNotificationClientConfig.InstallationID = installationID

	pushNotificationClient := pushnotificationclient.New(pushNotificationClientPersistence, pushNotificationClientConfig, sender, sqlitePersistence)

	ensVerifier := ens.New(node, logger, transp, database, c.verifyENSURL, c.verifyENSContractAddress)

	var walletAPI *wallet.API
	if c.walletService != nil {
		walletAPI = wallet.NewAPI(c.walletService)
	}

	managerOptions := []communities.ManagerOption{
		communities.WithAccountManager(accountsManager),
	}

	if walletAPI != nil {
		managerOptions = append(managerOptions, communities.WithCollectiblesManager(walletAPI))
	}

	if c.tokenManager != nil {
		managerOptions = append(managerOptions, communities.WithTokenManager(c.tokenManager))
	} else if c.rpcClient != nil {
		tokenManager := token.NewTokenManager(c.walletDb, c.rpcClient, c.rpcClient.NetworkManager)
		managerOptions = append(managerOptions, communities.WithTokenManager(communities.NewDefaultTokenManager(tokenManager)))
	}

	if c.walletConfig != nil {
		managerOptions = append(managerOptions, communities.WithWalletConfig(c.walletConfig))
	}

	if c.communityTokensService != nil {
		managerOptions = append(managerOptions, communities.WithCommunityTokensService(c.communityTokensService))
	}

	communitiesManager, err := communities.NewManager(identity, database, encryptionProtocol, logger, ensVerifier, transp, c.torrentConfig, managerOptions...)
	if err != nil {
		return nil, err
	}

	settings, err := accounts.NewDB(database)
	if err != nil {
		return nil, err
	}

	mailservers := mailserversDB.NewDB(database)

	savedAddressesManager := wallet.NewSavedAddressesManager(c.walletDb)

	myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey))
	myContact, err := buildContact(myPublicKeyString, &identity.PublicKey)
	if err != nil {
		return nil, errors.New("failed to build contact of ourself: " + err.Error())
	}

	ctx, cancel := context.WithCancel(context.Background())

	messenger = &Messenger{
		config:                 &c,
		node:                   node,
		identity:               identity,
		persistence:            sqlitePersistence,
		transport:              transp,
		encryptor:              encryptionProtocol,
		sender:                 sender,
		anonMetricsClient:      anonMetricsClient,
		anonMetricsServer:      anonMetricsServer,
		telemetryClient:        telemetryClient,
		pushNotificationClient: pushNotificationClient,
		pushNotificationServer: pushNotificationServer,
		communitiesManager:     communitiesManager,
		communitiesKeyDistributor: &CommunitiesKeyDistributorImpl{
			sender:    sender,
			encryptor: encryptionProtocol,
		},
		accountsManager:            accountsManager,
		ensVerifier:                ensVerifier,
		featureFlags:               c.featureFlags,
		systemMessagesTranslations: c.systemMessagesTranslations,
		allChats:                   new(chatMap),
		allContacts: &contactMap{
			logger: logger,
			me:     myContact,
		},
		allInstallations:        new(installationMap),
		installationID:          installationID,
		modifiedInstallations:   new(stringBoolMap),
		verifyTransactionClient: c.verifyTransactionClient,
		database:                database,
		multiAccounts:           c.multiAccount,
		settings:                settings,
		peerStore:               peerStore,
		verificationDatabase:    verification.NewPersistence(database),
		mailservers:             mailservers,
		mailserverCycle: mailserverCycle{
			peers:                     make(map[string]peerStatus),
			availabilitySubscriptions: make([]chan struct{}, 0),
		},
		mailserversDatabase:      c.mailserversDatabase,
		account:                  c.account,
		quit:                     make(chan struct{}),
		ctx:                      ctx,
		cancel:                   cancel,
		requestedCommunitiesLock: sync.RWMutex{},
		requestedCommunities:     make(map[string]*transport.Filter),
		requestedContactsLock:    sync.RWMutex{},
		requestedContacts:        make(map[string]*transport.Filter),
		importingCommunities:     make(map[string]bool),
		importRateLimiter:        rate.NewLimiter(rate.Every(importSlowRate), 1),
		importDelayer: struct {
			wait chan struct{}
			once sync.Once
		}{wait: make(chan struct{})},
		browserDatabase: c.browserDatabase,
		httpServer:      c.httpServer,
		contractMaker: &contracts.ContractMaker{
			RPCClient: c.rpcClient,
		},
		shutdownTasks: []func() error{
			ensVerifier.Stop,
			pushNotificationClient.Stop,
			communitiesManager.Stop,
			encryptionProtocol.Stop,
			func() error {
				ctx, cancel := context.WithTimeout(context.Background(), time.Second)
				defer cancel()
				err := transp.ResetFilters(ctx)
				if err != nil {
					logger.Warn("could not reset filters", zap.Error(err))
				}
				// We don't want to thrown an error in this case, this is a soft
				// fail
				return nil
			},
			transp.Stop,
			func() error { sender.Stop(); return nil },
			// Currently this often fails, seems like it's safe to ignore them
			// https://github.com/uber-go/zap/issues/328
			func() error { _ = logger.Sync; return nil },
			database.Close,
		},
		logger:                logger,
		savedAddressesManager: savedAddressesManager,
	}
	messenger.mentionsManager = NewMentionManager(messenger)

	if c.walletService != nil {
		messenger.walletAPI = walletAPI
	}

	if c.outputMessagesCSV {
		messenger.outputCSV = c.outputMessagesCSV
		csvFile, err := os.Create("messages-" + fmt.Sprint(time.Now().Unix()) + ".csv")
		if err != nil {
			return nil, err
		}

		_, err = csvFile.Write([]byte("timestamp\tmessageID\tfrom\ttopic\tchatID\tmessageType\tmessage\n"))
		if err != nil {
			return nil, err
		}

		messenger.csvFile = csvFile
		messenger.shutdownTasks = append(messenger.shutdownTasks, csvFile.Close)
	}

	if anonMetricsClient != nil {
		messenger.shutdownTasks = append(messenger.shutdownTasks, anonMetricsClient.Stop)
	}
	if anonMetricsServer != nil {
		messenger.shutdownTasks = append(messenger.shutdownTasks, anonMetricsServer.Stop)
	}

	if c.envelopesMonitorConfig != nil {
		interceptor := EnvelopeEventsInterceptor{c.envelopesMonitorConfig.EnvelopeEventsHandler, messenger}
		err := messenger.transport.SetEnvelopeEventsHandler(interceptor)
		if err != nil {
			logger.Info("Unable to set envelopes event handler", zap.Error(err))
		}
	}

	return messenger, nil
}

func (m *Messenger) SetP2PServer(server *p2p.Server) {
	m.server = server
}

func (m *Messenger) EnableBackedupMessagesProcessing() {
	m.processBackedupMessages = true
}

func (m *Messenger) processSentMessages(ids []string) error {
	if m.connectionState.Offline {
		return errors.New("Can't mark message as sent while offline")
	}

	for _, id := range ids {
		rawMessage, err := m.persistence.RawMessageByID(id)
		// If we have no raw message, we create a temporary one, so that
		// the sent status is preserved
		if err == sql.ErrNoRows || rawMessage == nil {
			rawMessage = &common.RawMessage{
				ID:          id,
				MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
			}
		} else if err != nil {
			return errors.Wrapf(err, "Can't get raw message with id %v", id)
		}

		rawMessage.Sent = true

		err = m.persistence.SaveRawMessage(rawMessage)
		if err != nil {
			return errors.Wrapf(err, "Can't save raw message marked as sent")
		}

		err = m.UpdateMessageOutgoingStatus(id, common.OutgoingStatusSent)
		if err != nil {
			return err
		}
	}

	return nil
}

func shouldResendMessage(message *common.RawMessage, t common.TimeSource) (bool, error) {
	if !(message.MessageType == protobuf.ApplicationMetadataMessage_EMOJI_REACTION ||
		message.MessageType == protobuf.ApplicationMetadataMessage_CHAT_MESSAGE) {
		return false, errors.Errorf("Should resend only specific types of messages, can't resend %v", message.MessageType)
	}

	if message.Sent {
		return false, errors.New("Should resend only non-sent messages")
	}

	if message.SendCount > messageResendMaxCount {
		return false, nil
	}

	//exponential backoff depends on how many attempts to send message already made
	backoff := uint64(math.Pow(2, float64(message.SendCount-1))) * messageResendMinDelay * uint64(time.Second.Milliseconds())
	backoffElapsed := t.GetCurrentTime() > (message.LastSent + backoff)
	return backoffElapsed, nil
}

func (m *Messenger) resendExpiredMessages() error {
	if m.connectionState.Offline {
		return errors.New("offline")
	}

	ids, err := m.persistence.ExpiredMessagesIDs(messageResendMaxCount)
	if err != nil {
		return errors.Wrapf(err, "Can't get expired reactions from db")
	}

	for _, id := range ids {
		rawMessage, err := m.persistence.RawMessageByID(id)
		if err != nil {
			return errors.Wrapf(err, "Can't get raw message with id %v", id)
		}

		chat, ok := m.allChats.Load(rawMessage.LocalChatID)
		if !ok {
			return ErrChatNotFound
		}

		if !(chat.Public() || chat.CommunityChat()) {
			return errors.New("Only public chats and community chats messages are resent")
		}

		ok, err = shouldResendMessage(rawMessage, m.getTimesource())
		if err != nil {
			return err
		}

		if ok {
			err = m.persistence.SaveRawMessage(rawMessage)
			if err != nil {
				return errors.Wrapf(err, "Can't save raw message marked as non-expired")
			}

			err = m.reSendRawMessage(context.Background(), rawMessage.ID)
			if err != nil {
				return errors.Wrapf(err, "Can't resend expired message with id %v", rawMessage.ID)
			}
		}
	}
	return nil
}

func (m *Messenger) ToForeground() {
	if m.httpServer != nil {
		m.httpServer.ToForeground()
	}
}

func (m *Messenger) ToBackground() {
	if m.httpServer != nil {
		m.httpServer.ToBackground()
	}
}

func (m *Messenger) Start() (*MessengerResponse, error) {
	now := time.Now().UnixMilli()
	if err := m.settings.CheckAndDeleteExpiredKeypairsAndAccounts(uint64(now)); err != nil {
		return nil, err
	}

	m.logger.Info("starting messenger", zap.String("identity", types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))))
	// Start push notification server
	if m.pushNotificationServer != nil {
		if err := m.pushNotificationServer.Start(); err != nil {
			return nil, err
		}
	}

	// Start push notification client
	if m.pushNotificationClient != nil {
		m.handlePushNotificationClientRegistrations(m.pushNotificationClient.SubscribeToRegistrations())

		if err := m.pushNotificationClient.Start(); err != nil {
			return nil, err
		}
	}

	// Start anonymous metrics client
	if m.anonMetricsClient != nil {
		if err := m.anonMetricsClient.Start(); err != nil {
			return nil, err
		}
	}

	ensSubscription := m.ensVerifier.Subscribe()

	// Subscrbe
	if err := m.ensVerifier.Start(); err != nil {
		return nil, err
	}

	if err := m.communitiesManager.Start(); err != nil {
		return nil, err
	}

	// set shared secret handles
	m.sender.SetHandleSharedSecrets(m.handleSharedSecrets)

	subscriptions, err := m.encryptor.Start(m.identity)
	if err != nil {
		return nil, err
	}

	// handle stored shared secrets
	err = m.handleSharedSecrets(subscriptions.SharedSecrets)
	if err != nil {
		return nil, err
	}

	m.handleEncryptionLayerSubscriptions(subscriptions)
	m.handleCommunitiesSubscription(m.communitiesManager.Subscribe())
	m.handleCommunitiesHistoryArchivesSubscription(m.communitiesManager.Subscribe())
	m.updateCommunitiesActiveMembersPeriodically()
	m.handleConnectionChange(m.online())
	m.handleENSVerificationSubscription(ensSubscription)
	m.watchConnectionChange()
	m.watchChatsAndCommunitiesToUnmute()
	m.watchCommunitiesToUnmute()
	m.watchExpiredMessages()
	m.watchIdentityImageChanges()
	m.watchWalletBalances()
	m.watchPendingCommunityRequestToJoin()
	m.broadcastLatestUserStatus()
	m.timeoutAutomaticStatusUpdates()
	m.startBackupLoop()
	err = m.startAutoMessageLoop()
	if err != nil {
		return nil, err
	}
	m.startSyncSettingsLoop()
	m.startCommunityRekeyLoop()
	m.startCuratedCommunitiesUpdateLoop()

	if err := m.cleanTopics(); err != nil {
		return nil, err
	}
	response := &MessengerResponse{}

	mailservers, err := m.allMailservers()
	if err != nil {
		return nil, err
	}

	response.Mailservers = mailservers
	err = m.StartMailserverCycle()
	if err != nil {
		return nil, err
	}

	if m.torrentClientReady() {
		controlledCommunities, err := m.communitiesManager.ControlledCommunities()
		if err == nil && len(controlledCommunities) > 0 {
			available := m.SubscribeMailserverAvailable()
			go func() {
				<-available
				m.InitHistoryArchiveTasks(controlledCommunities)
			}()

			for _, c := range controlledCommunities {
				if c.Joined() && c.HasTokenPermissions() {
					go m.communitiesManager.ReevaluateMembersPeriodically(c.ID())
				}
			}
		}
	}

	joinedCommunities, err := m.communitiesManager.Joined()
	if err != nil {
		return nil, err
	}

	for _, joinedCommunity := range joinedCommunities {
		// resume importing message history archives in case
		// imports have been interrupted previously
		err := m.resumeHistoryArchivesImport(joinedCommunity.ID())
		if err != nil {
			return nil, err
		}
	}
	m.enableHistoryArchivesImportAfterDelay()

	if m.httpServer != nil {
		err = m.httpServer.Start()
		if err != nil {
			return nil, err
		}
	}

	err = m.GarbageCollectRemovedBookmarks()
	if err != nil {
		return nil, err
	}

	err = m.garbageCollectRemovedSavedAddresses()
	if err != nil {
		return nil, err
	}

	return response, nil
}

func (m *Messenger) IdentityPublicKey() *ecdsa.PublicKey {
	return &m.identity.PublicKey
}

func (m *Messenger) IdentityPublicKeyCompressed() []byte {
	return crypto.CompressPubkey(m.IdentityPublicKey())
}

func (m *Messenger) IdentityPublicKeyString() string {
	return types.EncodeHex(crypto.FromECDSAPub(m.IdentityPublicKey()))
}

// cleanTopics remove any topic that does not have a Listen flag set
func (m *Messenger) cleanTopics() error {
	if m.mailserversDatabase == nil {
		return nil
	}
	var filters []*transport.Filter
	for _, f := range m.transport.Filters() {
		if f.Listen && !f.Ephemeral {
			filters = append(filters, f)
		}
	}

	m.logger.Debug("keeping topics", zap.Any("filters", filters))

	return m.mailserversDatabase.SetTopics(filters)
}

// handle connection change is called each time we go from offline/online or viceversa
func (m *Messenger) handleConnectionChange(online bool) {
	if online {
		if m.pushNotificationClient != nil {
			m.pushNotificationClient.Online()
		}

		if m.shouldPublishContactCode {
			if err := m.publishContactCode(); err != nil {
				m.logger.Error("could not publish on contact code", zap.Error(err))
				return
			}
			m.shouldPublishContactCode = false
		}
		go func() {
			_, err := m.RequestAllHistoricMessagesWithRetries(false)
			if err != nil {
				m.logger.Warn("failed to fetch historic messages", zap.Error(err))
			}
		}()

	} else {
		if m.pushNotificationClient != nil {
			m.pushNotificationClient.Offline()
		}

	}

	m.ensVerifier.SetOnline(online)
}

func (m *Messenger) online() bool {
	switch m.transport.WakuVersion() {
	case 2:
		return m.transport.PeerCount() > 0
	default:
		return m.node.PeersCount() > 0
	}
}

func (m *Messenger) buildContactCodeAdvertisement() (*protobuf.ContactCodeAdvertisement, error) {
	if m.pushNotificationClient == nil || !m.pushNotificationClient.Enabled() {
		return nil, nil
	}
	m.logger.Debug("adding push notification info to contact code bundle")
	info, err := m.pushNotificationClient.MyPushNotificationQueryInfo()
	if err != nil {
		return nil, err
	}
	if len(info) == 0 {
		return nil, nil
	}
	return &protobuf.ContactCodeAdvertisement{
		PushNotificationInfo: info,
	}, nil
}

// publishContactCode sends a public message wrapped in the encryption
// layer, which will propagate our bundle
func (m *Messenger) publishContactCode() error {
	var payload []byte
	m.logger.Debug("sending contact code")
	contactCodeAdvertisement, err := m.buildContactCodeAdvertisement()
	if err != nil {
		m.logger.Error("could not build contact code advertisement", zap.Error(err))
	}

	if contactCodeAdvertisement == nil {
		contactCodeAdvertisement = &protobuf.ContactCodeAdvertisement{}
	}

	err = m.attachChatIdentity(contactCodeAdvertisement)
	if err != nil {
		return err
	}

	if contactCodeAdvertisement.ChatIdentity != nil {
		m.logger.Debug("attached chat identity", zap.Int("images len", len(contactCodeAdvertisement.ChatIdentity.Images)))
	} else {
		m.logger.Debug("no attached chat identity")
	}

	payload, err = proto.Marshal(contactCodeAdvertisement)
	if err != nil {
		return err
	}

	contactCodeTopic := transport.ContactCodeTopic(&m.identity.PublicKey)
	rawMessage := common.RawMessage{
		LocalChatID: contactCodeTopic,
		MessageType: protobuf.ApplicationMetadataMessage_CONTACT_CODE_ADVERTISEMENT,
		Payload:     payload,
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err = m.sender.SendPublic(ctx, contactCodeTopic, rawMessage)
	if err != nil {
		m.logger.Warn("failed to send a contact code", zap.Error(err))
	}

	joinedCommunities, err := m.communitiesManager.Joined()
	if err != nil {
		return err
	}
	for _, community := range joinedCommunities {
		rawMessage.LocalChatID = community.MemberUpdateChannelID()
		rawMessage.PubsubTopic = community.PubsubTopic()
		_, err = m.sender.SendPublic(ctx, rawMessage.LocalChatID, rawMessage)
		if err != nil {
			return err
		}
	}

	m.logger.Debug("contact code sent")
	return err
}

// contactCodeAdvertisement attaches a protobuf.ChatIdentity to the given protobuf.ContactCodeAdvertisement,
// if the `shouldPublish` conditions are met
func (m *Messenger) attachChatIdentity(cca *protobuf.ContactCodeAdvertisement) error {
	contactCodeTopic := transport.ContactCodeTopic(&m.identity.PublicKey)
	shouldPublish, err := m.shouldPublishChatIdentity(contactCodeTopic)
	if err != nil {
		return err
	}

	if !shouldPublish {
		return nil
	}

	cca.ChatIdentity, err = m.createChatIdentity(privateChat)
	if err != nil {
		return err
	}

	img, err := m.multiAccounts.GetIdentityImage(m.account.KeyUID, images.SmallDimName)
	if err != nil {
		return err
	}

	displayName, err := m.settings.DisplayName()
	if err != nil {
		return err
	}

	bio, err := m.settings.Bio()
	if err != nil {
		return err
	}

	socialLinks, err := m.settings.GetSocialLinks()
	if err != nil {
		return err
	}

	identityHash, err := m.getIdentityHash(displayName, bio, img, socialLinks)
	if err != nil {
		return err
	}

	err = m.persistence.SaveWhenChatIdentityLastPublished(contactCodeTopic, identityHash)
	if err != nil {
		return err
	}

	return nil
}

// handleStandaloneChatIdentity sends a standalone ChatIdentity message to a public or private channel if the publish criteria is met
func (m *Messenger) handleStandaloneChatIdentity(chat *Chat) error {
	if chat.ChatType != ChatTypePublic && chat.ChatType != ChatTypeOneToOne {
		return nil
	}
	shouldPublishChatIdentity, err := m.shouldPublishChatIdentity(chat.ID)
	if err != nil {
		return err
	}
	if !shouldPublishChatIdentity {
		return nil
	}

	ci, err := m.createChatIdentity(publicChat)
	if err != nil {
		return err
	}

	payload, err := proto.Marshal(ci)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID: chat.ID,
		MessageType: protobuf.ApplicationMetadataMessage_CHAT_IDENTITY,
		Payload:     payload,
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if chat.ChatType == ChatTypePublic {
		_, err = m.sender.SendPublic(ctx, chat.ID, rawMessage)
		if err != nil {
			return err
		}
	} else {
		pk, err := chat.PublicKey()
		if err != nil {
			return err
		}
		_, err = m.sender.SendPrivate(ctx, pk, &rawMessage)
		if err != nil {
			return err
		}

	}

	img, err := m.multiAccounts.GetIdentityImage(m.account.KeyUID, images.SmallDimName)
	if err != nil {
		return err
	}

	displayName, err := m.settings.DisplayName()
	if err != nil {
		return err
	}

	bio, err := m.settings.Bio()
	if err != nil {
		return err
	}

	socialLinks, err := m.settings.GetSocialLinks()
	if err != nil {
		return err
	}

	identityHash, err := m.getIdentityHash(displayName, bio, img, socialLinks)
	if err != nil {
		return err
	}

	err = m.persistence.SaveWhenChatIdentityLastPublished(chat.ID, identityHash)
	if err != nil {
		return err
	}

	return nil
}

func (m *Messenger) getIdentityHash(displayName, bio string, img *images.IdentityImage, socialLinks identity.SocialLinks) ([]byte, error) {
	socialLinksData, err := socialLinks.Serialize()
	if err != nil {
		return []byte{}, err
	}
	if img == nil {
		return crypto.Keccak256([]byte(displayName), []byte(bio), socialLinksData), nil
	}
	return crypto.Keccak256(img.Payload, []byte(displayName), []byte(bio), socialLinksData), nil
}

// shouldPublishChatIdentity returns true if the last time the ChatIdentity was attached was more than 24 hours ago
func (m *Messenger) shouldPublishChatIdentity(chatID string) (bool, error) {
	if m.account == nil {
		return false, nil
	}

	// Check we have at least one image or a display name
	img, err := m.multiAccounts.GetIdentityImage(m.account.KeyUID, images.SmallDimName)
	if err != nil {
		return false, err
	}

	displayName, err := m.settings.DisplayName()
	if err != nil {
		return false, err
	}

	if img == nil && displayName == "" {
		return false, nil
	}

	lp, hash, err := m.persistence.GetWhenChatIdentityLastPublished(chatID)
	if err != nil {
		return false, err
	}

	bio, err := m.settings.Bio()
	if err != nil {
		return false, err
	}

	socialLinks, err := m.settings.GetSocialLinks()
	if err != nil {
		return false, err
	}

	identityHash, err := m.getIdentityHash(displayName, bio, img, socialLinks)
	if err != nil {
		return false, err
	}

	if !bytes.Equal(hash, identityHash) {
		return true, nil
	}

	// Note: If Alice does not add bob as a contact she will not update her contact code with images
	return lp == 0 || time.Now().Unix()-lp > 24*60*60, nil
}

// createChatIdentity creates a context based protobuf.ChatIdentity.
// context 'public-chat' will attach only the 'thumbnail' IdentityImage
// context 'private-chat' will attach all IdentityImage
func (m *Messenger) createChatIdentity(context chatContext) (*protobuf.ChatIdentity, error) {
	m.logger.Info(fmt.Sprintf("account keyUID '%s'", m.account.KeyUID))
	m.logger.Info(fmt.Sprintf("context '%s'", context))

	displayName, err := m.settings.DisplayName()
	if err != nil {
		return nil, err
	}

	bio, err := m.settings.Bio()
	if err != nil {
		return nil, err
	}

	socialLinks, err := m.settings.GetSocialLinks()
	if err != nil {
		return nil, err
	}

	ci := &protobuf.ChatIdentity{
		Clock:       m.transport.GetCurrentTime(),
		EnsName:     "", // TODO add ENS name handling to dedicate PR
		DisplayName: displayName,
		Description: bio,
		SocialLinks: socialLinks.ToProtobuf(),
	}

	err = m.attachIdentityImagesToChatIdentity(context, ci)
	if err != nil {
		return nil, err
	}

	return ci, nil
}

// adaptIdentityImageToProtobuf Adapts a images.IdentityImage to protobuf.IdentityImage
func (m *Messenger) adaptIdentityImageToProtobuf(img *images.IdentityImage) *protobuf.IdentityImage {
	return &protobuf.IdentityImage{
		Payload:    img.Payload,
		SourceType: protobuf.IdentityImage_RAW_PAYLOAD, // TODO add ENS avatar handling to dedicated PR
		ImageType:  images.GetProtobufImageType(img.Payload),
	}
}

func (m *Messenger) attachIdentityImagesToChatIdentity(context chatContext, ci *protobuf.ChatIdentity) error {
	s, err := m.getSettings()
	if err != nil {
		return err
	}

	if s.ProfilePicturesShowTo == settings.ProfilePicturesShowToNone {
		m.logger.Info(fmt.Sprintf("settings.ProfilePicturesShowTo is set to '%d', skipping attaching IdentityImages", s.ProfilePicturesShowTo))
		return nil
	}

	ciis := make(map[string]*protobuf.IdentityImage)

	switch context {
	case publicChat:
		m.logger.Info(fmt.Sprintf("handling %s ChatIdentity", context))

		img, err := m.multiAccounts.GetIdentityImage(m.account.KeyUID, images.SmallDimName)
		if err != nil {
			return err
		}

		if img == nil {
			return nil
		}

		m.logger.Debug(fmt.Sprintf("%s images.IdentityImage '%s'", context, spew.Sdump(img)))

		ciis[images.SmallDimName] = m.adaptIdentityImageToProtobuf(img)
		m.logger.Debug(fmt.Sprintf("%s protobuf.IdentityImage '%s'", context, spew.Sdump(ciis)))
		ci.Images = ciis

	case privateChat:
		m.logger.Info(fmt.Sprintf("handling %s ChatIdentity", context))

		imgs, err := m.multiAccounts.GetIdentityImages(m.account.KeyUID)
		if err != nil {
			return err
		}

		m.logger.Debug(fmt.Sprintf("%s images.IdentityImage '%s'", context, spew.Sdump(imgs)))

		for _, img := range imgs {
			ciis[img.Name] = m.adaptIdentityImageToProtobuf(img)
		}
		m.logger.Debug(fmt.Sprintf("%s protobuf.IdentityImage '%s'", context, spew.Sdump(ciis)))
		ci.Images = ciis

	default:
		return fmt.Errorf("unknown ChatIdentity context '%s'", context)
	}

	if s.ProfilePicturesShowTo == settings.ProfilePicturesShowToContactsOnly {
		err := EncryptIdentityImagesWithContactPubKeys(ci.Images, m)
		if err != nil {
			return err
		}
	}

	return nil
}

// handleSharedSecrets process the negotiated secrets received from the encryption layer
func (m *Messenger) handleSharedSecrets(secrets []*sharedsecret.Secret) error {
	for _, secret := range secrets {
		fSecret := types.NegotiatedSecret{
			PublicKey: secret.Identity,
			Key:       secret.Key,
		}
		_, err := m.transport.ProcessNegotiatedSecret(fSecret)
		if err != nil {
			return err
		}
	}
	return nil
}

// handleInstallations adds the installations in the installations map
func (m *Messenger) handleInstallations(installations []*multidevice.Installation) {
	for _, installation := range installations {
		if installation.Identity == contactIDFromPublicKey(&m.identity.PublicKey) {
			if _, ok := m.allInstallations.Load(installation.ID); !ok {
				m.allInstallations.Store(installation.ID, installation)
				m.modifiedInstallations.Store(installation.ID, true)
			}
		}
	}
}

// handleEncryptionLayerSubscriptions handles events from the encryption layer
func (m *Messenger) handleEncryptionLayerSubscriptions(subscriptions *encryption.Subscriptions) {
	go func() {
		for {
			select {
			case <-subscriptions.SendContactCode:
				if err := m.publishContactCode(); err != nil {
					m.logger.Error("failed to publish contact code", zap.Error(err))
				}
				// we also piggy-back to clean up cached messages
				if err := m.transport.CleanMessagesProcessed(m.getTimesource().GetCurrentTime() - messageCacheIntervalMs); err != nil {
					m.logger.Error("failed to clean processed messages", zap.Error(err))
				}

			case <-subscriptions.Quit:
				m.logger.Debug("quitting encryption subscription loop")
				return
			}
		}
	}()
}

func (m *Messenger) handleENSVerified(records []*ens.VerificationRecord) {
	var contacts []*Contact
	for _, record := range records {
		m.logger.Info("handling record", zap.Any("record", record))
		contact, ok := m.allContacts.Load(record.PublicKey)
		if !ok {
			m.logger.Info("contact not found")
			continue
		}

		contact.ENSVerified = record.Verified
		contact.EnsName = record.Name
		contacts = append(contacts, contact)
	}

	m.logger.Info("handled records", zap.Any("contacts", contacts))
	if len(contacts) != 0 {
		if err := m.persistence.SaveContacts(contacts); err != nil {
			m.logger.Error("failed to save contacts", zap.Error(err))
			return
		}
	}

	m.logger.Info("calling on contacts")
	if m.config.onContactENSVerified != nil {
		m.logger.Info("called on contacts")
		response := &MessengerResponse{Contacts: contacts}
		m.config.onContactENSVerified(response)
	}

}

func (m *Messenger) handleENSVerificationSubscription(c chan []*ens.VerificationRecord) {
	go func() {
		for {
			select {
			case records, more := <-c:
				if !more {
					m.logger.Info("No more records, quitting")
					return
				}
				if len(records) != 0 {
					m.logger.Info("handling records", zap.Any("records", records))
					m.handleENSVerified(records)
				}
			case <-m.quit:
				return
			}
		}
	}()
}

// watchConnectionChange checks the connection status and call handleConnectionChange when this changes
func (m *Messenger) watchConnectionChange() {
	m.logger.Debug("watching connection changes")
	state := m.online()
	go func() {
		for {
			select {
			case <-time.After(200 * time.Millisecond):
				newState := m.online()
				if state != newState {
					state = newState
					m.logger.Debug("connection changed", zap.Bool("online", state))
					m.handleConnectionChange(state)
				}
			case <-m.quit:
				return
			}
		}
	}()
}

// watchChatsAndCommunitiesToUnmute regularly checks for chats and communities that should be unmuted
func (m *Messenger) watchChatsAndCommunitiesToUnmute() {
	m.logger.Debug("watching unmuted chats")
	go func() {
		for {
			select {
			case <-time.After(3 * time.Second): // Poll every 3 seconds
				response := &MessengerResponse{}
				m.allChats.Range(func(chatID string, c *Chat) bool {
					chatMuteTill, _ := time.Parse(time.RFC3339, c.MuteTill.Format(time.RFC3339))
					currTime, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))

					if currTime.After(chatMuteTill) && !chatMuteTill.Equal(time.Time{}) && c.Muted {
						err := m.persistence.UnmuteChat(c.ID)
						if err != nil {
							m.logger.Info("err", zap.Any("Couldn't unmute chat", err))
							return false
						}
						c.Muted = false
						c.MuteTill = time.Time{}
						response.AddChat(c)
					}
					return true
				})

				if !response.IsEmpty() {
					signal.SendNewMessages(response)
				}
			case <-m.quit:
				return
			}
		}
	}()
}

// watchCommunitiesToUnmute regularly checks for communities that should be unmuted
func (m *Messenger) watchCommunitiesToUnmute() {
	m.logger.Debug("watching unmuted communities")
	go func() {
		for {
			select {
			case <-time.After(3 * time.Second): // Poll every 3 seconds
				response, err := m.CheckCommunitiesToUnmute()
				if err != nil {
					return
				}

				if !response.IsEmpty() {
					signal.SendNewMessages(response)
				}
			case <-m.quit:
				return
			}
		}
	}()
}

// watchExpiredMessages regularly checks for expired emojis and invoke their resending
func (m *Messenger) watchExpiredMessages() {
	m.logger.Debug("watching expired messages")
	go func() {
		for {
			select {
			case <-time.After(time.Second):
				if m.online() {
					err := m.resendExpiredMessages()
					if err != nil {
						m.logger.Debug("Error when resending expired emoji reactions", zap.Error(err))
					}
				}
			case <-m.quit:
				return
			}
		}
	}()
}

// watchIdentityImageChanges checks for identity images changes and publishes to the contact code when it happens
func (m *Messenger) watchIdentityImageChanges() {
	m.logger.Debug("watching identity image changes")
	if m.multiAccounts == nil {
		return
	}

	channel := m.multiAccounts.SubscribeToIdentityImageChanges()

	go func() {
		for {
			select {
			case <-channel:
				err := m.syncProfilePictures(m.dispatchMessage)
				if err != nil {
					m.logger.Error("failed to sync profile pictures to paired devices", zap.Error(err))
				}
				err = m.PublishIdentityImage()
				if err != nil {
					m.logger.Error("failed to publish identity image", zap.Error(err))
				}
			case <-m.quit:
				return
			}
		}
	}()
}

func (m *Messenger) watchPendingCommunityRequestToJoin() {
	m.logger.Debug("watching community request to join")

	go func() {
		for {
			select {
			case <-time.After(time.Minute * 10):
				_, err := m.CheckAndDeletePendingRequestToJoinCommunity(false)
				if err != nil {
					m.logger.Error("failed to check and delete pending request to join community", zap.Error(err))
				}
			case <-m.quit:
				return
			}
		}
	}()
}

func (m *Messenger) PublishIdentityImage() error {
	// Reset last published time for ChatIdentity so new contact can receive data
	err := m.resetLastPublishedTimeForChatIdentity()
	if err != nil {
		m.logger.Error("failed to reset publish time", zap.Error(err))
		return err
	}

	// If not online, we schedule it
	if !m.online() {
		m.shouldPublishContactCode = true
		return nil
	}

	return m.publishContactCode()
}

// handlePushNotificationClientRegistration handles registration events
func (m *Messenger) handlePushNotificationClientRegistrations(c chan struct{}) {
	go func() {
		for {
			_, more := <-c
			if !more {
				return
			}
			if err := m.publishContactCode(); err != nil {
				m.logger.Error("failed to publish contact code", zap.Error(err))
			}

		}
	}()
}

// Init analyzes chats and contacts in order to setup filters
// which are responsible for retrieving messages.
func (m *Messenger) Init() error {

	// Seed the for color generation
	rand.Seed(time.Now().Unix())

	logger := m.logger.With(zap.String("site", "Init"))

	var (
		filtersToInit []transport.FiltersToInitialize
		publicKeys    []*ecdsa.PublicKey
	)

	joinedCommunities, err := m.communitiesManager.Joined()
	if err != nil {
		return err
	}
	for _, org := range joinedCommunities {
		// the org advertise on the public topic derived by the pk
		filtersToInit = append(filtersToInit, org.DefaultFilters()...)

		// This is for status-go versions that didn't have `CommunitySettings`
		// We need to ensure communities that existed before community settings
		// were introduced will have community settings as well
		exists, err := m.communitiesManager.CommunitySettingsExist(org.ID())
		if err != nil {
			logger.Warn("failed to check if community settings exist", zap.Error(err))
			continue
		}

		if !exists {
			communitySettings := communities.CommunitySettings{
				CommunityID:                  org.IDString(),
				HistoryArchiveSupportEnabled: true,
			}

			err = m.communitiesManager.SaveCommunitySettings(communitySettings)
			if err != nil {
				logger.Warn("failed to save community settings", zap.Error(err))
			}
			continue
		}

		// In case we do have settings, but the history archive support is disabled
		// for this community, we enable it, as this should be the default for all
		// non-admin communities
		communitySettings, err := m.communitiesManager.GetCommunitySettingsByID(org.ID())
		if err != nil {
			logger.Warn("failed to fetch community settings", zap.Error(err))
			continue
		}

		if !org.IsControlNode() && !communitySettings.HistoryArchiveSupportEnabled {
			communitySettings.HistoryArchiveSupportEnabled = true
			err = m.communitiesManager.UpdateCommunitySettings(*communitySettings)
			if err != nil {
				logger.Warn("failed to update community settings", zap.Error(err))
			}
		}
	}

	spectatedCommunities, err := m.communitiesManager.Spectated()
	if err != nil {
		return err
	}

	for _, org := range spectatedCommunities {
		filtersToInit = append(filtersToInit, org.DefaultFilters()...)
	}

	// Init filters for the communities we control
	var communityFiltersToInitialize []transport.CommunityFilterToInitialize
	controlledCommunities, err := m.communitiesManager.ControlledCommunities()
	if err != nil {
		return err
	}

	for _, c := range controlledCommunities {
		communityFiltersToInitialize = append(communityFiltersToInitialize, transport.CommunityFilterToInitialize{
			CommunityID: c.ID(),
			PrivKey:     c.PrivateKey(),
		})
	}

	_, err = m.transport.InitCommunityFilters(communityFiltersToInitialize)
	if err != nil {
		return err
	}

	// Get chat IDs and public keys from the existing chats.
	// TODO: Get only active chats by the query.
	chats, err := m.persistence.Chats()
	if err != nil {
		return err
	}
	for _, chat := range chats {
		if err := chat.Validate(); err != nil {
			logger.Warn("failed to validate chat", zap.Error(err))
			continue
		}

		if err = m.initChatFirstMessageTimestamp(chat); err != nil {
			logger.Warn("failed to init first message timestamp", zap.Error(err))
			continue
		}

		m.allChats.Store(chat.ID, chat)

		if !chat.Active || chat.Timeline() {
			continue
		}

		switch chat.ChatType {
		case ChatTypePublic, ChatTypeProfile:
			filtersToInit = append(filtersToInit, transport.FiltersToInitialize{ChatID: chat.ID, PubsubTopic: relay.DefaultWakuTopic})
		case ChatTypeCommunityChat:
			communityID, err := hexutil.Decode(chat.CommunityID)
			if err != nil {
				return err
			}
			filtersToInit = append(filtersToInit, transport.FiltersToInitialize{ChatID: chat.ID, PubsubTopic: transport.GetPubsubTopic(communityID)})
		case ChatTypeOneToOne:
			pk, err := chat.PublicKey()
			if err != nil {
				return err
			}
			publicKeys = append(publicKeys, pk)
		case ChatTypePrivateGroupChat:
			for _, member := range chat.Members {
				publicKey, err := member.PublicKey()
				if err != nil {
					return errors.Wrapf(err, "invalid public key for member %s in chat %s", member.ID, chat.Name)
				}
				publicKeys = append(publicKeys, publicKey)
			}
		default:
			return errors.New("invalid chat type")
		}
	}

	// Timeline and profile chats are deprecated.
	// This code can be removed after some reasonable time.

	// upsert timeline chat
	if !deprecation.ChatProfileDeprecated {
		err = m.ensureTimelineChat()
		if err != nil {
			return err
		}
	}

	// upsert profile chat
	if !deprecation.ChatTimelineDeprecated {
		err = m.ensureMyOwnProfileChat()
		if err != nil {
			return err
		}
	}

	// Get chat IDs and public keys from the contacts.
	contacts, err := m.persistence.Contacts()
	if err != nil {
		return err
	}
	for idx, contact := range contacts {
		if err = m.updateContactImagesURL(contact); err != nil {
			return err
		}
		m.allContacts.Store(contact.ID, contacts[idx])
		// We only need filters for contacts added by us and not blocked.
		if !contact.added() || contact.Blocked {
			continue
		}
		publicKey, err := contact.PublicKey()
		if err != nil {
			logger.Error("failed to get contact's public key", zap.Error(err))
			continue
		}
		publicKeys = append(publicKeys, publicKey)
	}

	installations, err := m.encryptor.GetOurInstallations(&m.identity.PublicKey)
	if err != nil {
		return err
	}

	for _, installation := range installations {
		m.allInstallations.Store(installation.ID, installation)
	}

	err = m.setInstallationHostname()
	if err != nil {
		return err
	}

	_, err = m.transport.InitFilters(filtersToInit, publicKeys)
	return err
}

// Shutdown takes care of ensuring a clean shutdown of Messenger
func (m *Messenger) Shutdown() (err error) {
	close(m.quit)
	m.cancel()
	m.downloadHistoryArchiveTasksWaitGroup.Wait()
	for i, task := range m.shutdownTasks {
		m.logger.Debug("running shutdown task", zap.Int("n", i))
		if tErr := task(); tErr != nil {
			m.logger.Info("shutdown task failed", zap.Error(tErr))
			if err == nil {
				// First error appeared.
				err = tErr
			} else {
				// We return all errors. They will be concatenated in the order of occurrence,
				// however, they will also be returned as a single error.
				err = errors.Wrap(err, tErr.Error())
			}
		}
	}
	return
}

func (m *Messenger) EnableInstallation(id string) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	err := m.encryptor.EnableInstallation(&m.identity.PublicKey, id)
	if err != nil {
		return err
	}
	installation.Enabled = true
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allInstallations.Store(id, installation)
	return nil
}

func (m *Messenger) DisableInstallation(id string) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	err := m.encryptor.DisableInstallation(&m.identity.PublicKey, id)
	if err != nil {
		return err
	}
	installation.Enabled = false
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allInstallations.Store(id, installation)
	return nil
}

func (m *Messenger) Installations() []*multidevice.Installation {
	installations := make([]*multidevice.Installation, m.allInstallations.Len())

	var i = 0
	m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) {
		installations[i] = installation
		i++
		return true
	})
	return installations
}

func (m *Messenger) setInstallationMetadata(id string, data *multidevice.InstallationMetadata) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	installation.InstallationMetadata = data
	return m.encryptor.SetInstallationMetadata(m.IdentityPublicKey(), id, data)
}

func (m *Messenger) SetInstallationMetadata(id string, data *multidevice.InstallationMetadata) error {
	return m.setInstallationMetadata(id, data)
}

func (m *Messenger) SetInstallationName(id string, name string) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	installation.InstallationMetadata.Name = name
	return m.encryptor.SetInstallationName(m.IdentityPublicKey(), id, name)
}

// NOT IMPLEMENTED
func (m *Messenger) SelectMailserver(id string) error {
	return ErrNotImplemented
}

// NOT IMPLEMENTED
func (m *Messenger) AddMailserver(enode string) error {
	return ErrNotImplemented
}

// NOT IMPLEMENTED
func (m *Messenger) RemoveMailserver(id string) error {
	return ErrNotImplemented
}

// NOT IMPLEMENTED
func (m *Messenger) Mailservers() ([]string, error) {
	return nil, ErrNotImplemented
}

func (m *Messenger) initChatFirstMessageTimestamp(chat *Chat) error {
	if !chat.CommunityChat() || chat.FirstMessageTimestamp != FirstMessageTimestampUndefined {
		return nil
	}

	oldestMessageTimestamp, hasAnyMessage, err := m.persistence.OldestMessageWhisperTimestampByChatID(chat.ID)
	if err != nil {
		return err
	}

	if hasAnyMessage {
		if oldestMessageTimestamp == FirstMessageTimestampUndefined {
			return nil
		}
		return m.updateChatFirstMessageTimestamp(chat, whisperToUnixTimestamp(oldestMessageTimestamp), &MessengerResponse{})
	}

	return m.updateChatFirstMessageTimestamp(chat, FirstMessageTimestampNoMessage, &MessengerResponse{})
}

func (m *Messenger) addMessagesAndChat(chat *Chat, messages []*common.Message, response *MessengerResponse) (*MessengerResponse, error) {
	response.AddChat(chat)
	response.AddMessages(messages)
	err := m.persistence.SaveMessages(response.Messages())
	if err != nil {
		return nil, err
	}

	return response, m.saveChat(chat)
}

func (m *Messenger) reregisterForPushNotifications() error {
	m.logger.Info("contact state changed, re-registering for push notification")
	if m.pushNotificationClient == nil {
		return nil
	}

	return m.pushNotificationClient.Reregister(m.pushNotificationOptions())
}

// pull a message from the database and send it again
func (m *Messenger) reSendRawMessage(ctx context.Context, messageID string) error {
	message, err := m.persistence.RawMessageByID(messageID)
	if err != nil {
		return err
	}

	chat, ok := m.allChats.Load(message.LocalChatID)
	if !ok {
		return errors.New("chat not found")
	}

	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             message.Payload,
		PubsubTopic:         message.PubsubTopic,
		MessageType:         message.MessageType,
		Recipients:          message.Recipients,
		ResendAutomatically: message.ResendAutomatically,
		SendCount:           message.SendCount,
	})
	return err
}

// ReSendChatMessage pulls a message from the database and sends it again
func (m *Messenger) ReSendChatMessage(ctx context.Context, messageID string) error {
	return m.reSendRawMessage(ctx, messageID)
}

func (m *Messenger) SetLocalPairing(localPairing bool) {
	m.localPairing = localPairing
}
func (m *Messenger) hasPairedDevices() bool {
	logger := m.logger.Named("hasPairedDevices")

	if m.localPairing {
		return true
	}

	var count int
	m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) {
		if installation.Enabled {
			count++
		}
		return true
	})
	logger.Debug("installations info",
		zap.Int("Number of installations", m.allInstallations.Len()),
		zap.Int("Number of enabled installations", count))
	return count > 1
}

func (m *Messenger) HasPairedDevices() bool {
	return m.hasPairedDevices()
}

// sendToPairedDevices will check if we have any paired devices and send to them if necessary
func (m *Messenger) sendToPairedDevices(ctx context.Context, spec common.RawMessage) error {
	hasPairedDevices := m.hasPairedDevices()
	// We send a message to any paired device
	if hasPairedDevices {
		_, err := m.sender.SendPrivate(ctx, &m.identity.PublicKey, &spec)
		if err != nil {
			return err
		}
	}
	return nil
}

func (m *Messenger) dispatchPairInstallationMessage(ctx context.Context, spec common.RawMessage) (common.RawMessage, error) {
	var err error
	var id []byte

	id, err = m.sender.SendPairInstallation(ctx, &m.identity.PublicKey, spec)

	if err != nil {
		return spec, err
	}
	spec.ID = types.EncodeHex(id)
	spec.SendCount++
	err = m.persistence.SaveRawMessage(&spec)
	if err != nil {
		return spec, err
	}

	return spec, nil
}

func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMessage) (common.RawMessage, error) {
	var err error
	var id []byte
	logger := m.logger.With(zap.String("site", "dispatchMessage"), zap.String("chatID", rawMessage.LocalChatID))
	chat, ok := m.allChats.Load(rawMessage.LocalChatID)
	if !ok {
		return rawMessage, errors.New("no chat found")
	}

	switch chat.ChatType {
	case ChatTypeOneToOne:
		publicKey, err := chat.PublicKey()
		if err != nil {
			return rawMessage, err
		}

		//SendPrivate will alter message identity and possibly datasyncid, so we save an unchanged
		//message for sending to paired devices later
		specCopyForPairedDevices := rawMessage
		if !common.IsPubKeyEqual(publicKey, &m.identity.PublicKey) || rawMessage.SkipProtocolLayer {
			id, err = m.sender.SendPrivate(ctx, publicKey, &rawMessage)

			if err != nil {
				return rawMessage, err
			}
		}

		err = m.sendToPairedDevices(ctx, specCopyForPairedDevices)

		if err != nil {
			return rawMessage, err
		}

	case ChatTypePublic, ChatTypeProfile:
		logger.Debug("sending public message", zap.String("chatName", chat.Name))
		id, err = m.sender.SendPublic(ctx, chat.ID, rawMessage)
		if err != nil {
			return rawMessage, err
		}
	case ChatTypeCommunityChat:
		communityID, err := hexutil.Decode(chat.CommunityID)
		if err != nil {
			return rawMessage, err
		}

		rawMessage.PubsubTopic = transport.GetPubsubTopic(communityID)

		// TODO: add grant
		canPost, err := m.communitiesManager.CanPost(&m.identity.PublicKey, chat.CommunityID, chat.CommunityChatID(), nil)
		if err != nil {
			return rawMessage, err
		}

		// We allow emoji reactions by anyone
		if rawMessage.MessageType != protobuf.ApplicationMetadataMessage_EMOJI_REACTION && !canPost {
			m.logger.Error("can't post on chat", zap.String("chat-id", chat.ID), zap.String("chat-name", chat.Name))

			return rawMessage, errors.New("can't post on chat")
		}

		logger.Debug("sending community chat message", zap.String("chatName", chat.Name))
		isCommunityEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID)
		if err != nil {
			return rawMessage, err
		}
		isChannelEncrypted, err := m.communitiesManager.IsChannelEncrypted(chat.CommunityID, chat.ID)
		if err != nil {
			return rawMessage, err
		}
		isEncrypted := isCommunityEncrypted || isChannelEncrypted
		if !isEncrypted {
			id, err = m.sender.SendPublic(ctx, chat.ID, rawMessage)
			if err != nil {
				return rawMessage, err
			}
		} else {
			rawMessage.CommunityID, err = types.DecodeHex(chat.CommunityID)
			if err != nil {
				return rawMessage, err
			}

			if isChannelEncrypted {
				rawMessage.HashRatchetGroupID = []byte(chat.ID)
			} else {
				rawMessage.HashRatchetGroupID = rawMessage.CommunityID
			}

			id, err = m.sender.SendCommunityMessage(ctx, rawMessage)
			if err != nil {
				return rawMessage, err
			}
		}
	case ChatTypePrivateGroupChat:
		logger.Debug("sending group message", zap.String("chatName", chat.Name))
		if rawMessage.Recipients == nil {
			rawMessage.Recipients, err = chat.MembersAsPublicKeys()
			if err != nil {
				return rawMessage, err
			}
		}

		hasPairedDevices := m.hasPairedDevices()

		if !hasPairedDevices {

			// Filter out my key from the recipients
			n := 0
			for _, recipient := range rawMessage.Recipients {
				if !common.IsPubKeyEqual(recipient, &m.identity.PublicKey) {
					rawMessage.Recipients[n] = recipient
					n++
				}
			}
			rawMessage.Recipients = rawMessage.Recipients[:n]
		}

		// We won't really send the message out if there's no recipients
		if len(rawMessage.Recipients) == 0 {
			rawMessage.Sent = true
		}

		// We skip wrapping in some cases (emoji reactions for example)
		if !rawMessage.SkipGroupMessageWrap {
			rawMessage.MessageType = protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE
		}

		id, err = m.sender.SendGroup(ctx, rawMessage.Recipients, rawMessage)
		if err != nil {
			return rawMessage, err
		}

	default:
		return rawMessage, errors.New("chat type not supported")
	}
	rawMessage.ID = types.EncodeHex(id)
	rawMessage.SendCount++
	rawMessage.LastSent = m.getTimesource().GetCurrentTime()
	err = m.persistence.SaveRawMessage(&rawMessage)
	if err != nil {
		return rawMessage, err
	}

	return rawMessage, nil
}

// SendChatMessage takes a minimal message and sends it based on the corresponding chat
func (m *Messenger) SendChatMessage(ctx context.Context, message *common.Message) (*MessengerResponse, error) {
	return m.sendChatMessage(ctx, message)
}

// SendChatMessages takes a array of messages and sends it based on the corresponding chats
func (m *Messenger) SendChatMessages(ctx context.Context, messages []*common.Message) (*MessengerResponse, error) {
	var response MessengerResponse

	generatedAlbumID, err := uuid.NewRandom()
	if err != nil {
		return nil, err
	}

	imagesCount := uint32(0)
	for _, message := range messages {
		if message.ContentType == protobuf.ChatMessage_IMAGE {
			imagesCount++
		}

	}

	for _, message := range messages {
		if message.ContentType == protobuf.ChatMessage_IMAGE && len(messages) > 1 {
			err = message.SetAlbumIDAndImagesCount(generatedAlbumID.String(), imagesCount)
			if err != nil {
				return nil, err
			}
		}
		messageResponse, err := m.SendChatMessage(ctx, message)
		if err != nil {
			return nil, err
		}
		err = response.Merge(messageResponse)
		if err != nil {
			return nil, err
		}
	}

	return &response, nil
}

// sendChatMessage takes a minimal message and sends it based on the corresponding chat
func (m *Messenger) sendChatMessage(ctx context.Context, message *common.Message) (*MessengerResponse, error) {
	displayName, err := m.settings.DisplayName()
	if err != nil {
		return nil, err
	}

	message.DisplayName = displayName
	if len(message.ImagePath) != 0 {

		err := message.LoadImage()
		if err != nil {
			return nil, err
		}

	} else if len(message.CommunityID) != 0 {
		community, err := m.communitiesManager.GetByIDString(message.CommunityID)
		if err != nil {
			return nil, err
		}

		if community == nil {
			return nil, errors.New("community not found")
		}

		wrappedCommunity, err := community.ToProtocolMessageBytes()
		if err != nil {
			return nil, err
		}
		message.Payload = &protobuf.ChatMessage_Community{Community: wrappedCommunity}

		message.ContentType = protobuf.ChatMessage_COMMUNITY
	} else if len(message.AudioPath) != 0 {
		err := message.LoadAudio()
		if err != nil {
			return nil, err
		}
	}

	unfurledLinks, err := message.ConvertLinkPreviewsToProto()
	// We consider link previews non-critical data, so we do not want to block
	// messages from being sent.
	if err != nil {
		m.logger.Error("failed to convert link previews", zap.Error(err))
	} else {
		message.UnfurledLinks = unfurledLinks
	}

	var response MessengerResponse

	// A valid added chat is required.
	chat, ok := m.allChats.Load(message.ChatId)
	if !ok {
		return nil, errors.New("Chat not found")
	}

	err = m.handleStandaloneChatIdentity(chat)
	if err != nil {
		return nil, err
	}

	err = extendMessageFromChat(message, chat, &m.identity.PublicKey, m.getTimesource())
	if err != nil {
		return nil, err
	}

	err = m.addContactRequestPropagatedState(message)
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.encodeChatEntity(chat, message)
	if err != nil {
		return nil, err
	}

	rawMessage := common.RawMessage{
		LocalChatID:          chat.ID,
		SendPushNotification: m.featureFlags.PushNotifications,
		Payload:              encodedMessage,
		MessageType:          protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
		ResendAutomatically:  true,
	}

	// We want to save the raw message before dispatching it, to avoid race conditions
	// since it might get dispatched and confirmed before it's saved.
	// This is not the best solution, probably it would be better to split
	// the sent status in a different table and join on query for messages,
	// but that's a much larger change and it would require an expensive migration of clients
	rawMessage.BeforeDispatch = func(rawMessage *common.RawMessage) error {
		if rawMessage.Sent {
			message.OutgoingStatus = common.OutgoingStatusSent
		}
		message.ID = rawMessage.ID
		err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
		if err != nil {
			return err
		}

		err = chat.UpdateFromMessage(message, m.getTimesource())
		if err != nil {
			return err
		}

		return m.persistence.SaveMessages([]*common.Message{message})
	}

	rawMessage, err = m.dispatchMessage(ctx, rawMessage)
	if err != nil {
		return nil, err
	}

	msg, err := m.pullMessagesAndResponsesFromDB([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	if err := m.updateChatFirstMessageTimestamp(chat, whisperToUnixTimestamp(message.WhisperTimestamp), &response); err != nil {
		return nil, err
	}

	response.SetMessages(msg)
	response.AddChat(chat)

	m.logger.Debug("sent message", zap.String("id", message.ID))
	m.prepareMessages(response.messages)

	return &response, m.saveChat(chat)
}

func whisperToUnixTimestamp(whisperTimestamp uint64) uint32 {
	return uint32(whisperTimestamp / 1000)
}

func (m *Messenger) updateChatFirstMessageTimestamp(chat *Chat, timestamp uint32, response *MessengerResponse) error {
	// Currently supported only for communities
	if !chat.CommunityChat() {
		return nil
	}

	community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
	if err != nil {
		return err
	}

	if community.IsControlNode() && chat.UpdateFirstMessageTimestamp(timestamp) {
		community, changes, err := m.communitiesManager.EditChatFirstMessageTimestamp(community.ID(), chat.ID, chat.FirstMessageTimestamp)
		if err != nil {
			return err
		}

		response.AddCommunity(community)
		response.CommunityChanges = append(response.CommunityChanges, changes)
	}

	return nil
}

func (m *Messenger) ShareImageMessage(request *requests.ShareImageMessage) (*MessengerResponse, error) {
	if err := request.Validate(); err != nil {
		return nil, err
	}
	response := &MessengerResponse{}

	msg, err := m.persistence.MessageByID(request.MessageID)
	if err != nil {
		return nil, err
	}

	var messages []*common.Message
	for _, pk := range request.Users {
		message := common.NewMessage()
		message.ChatId = pk.String()
		message.Payload = msg.Payload
		message.Text = "This message has been shared with you"
		message.ContentType = protobuf.ChatMessage_IMAGE
		messages = append(messages, message)

		r, err := m.CreateOneToOneChat(&requests.CreateOneToOneChat{ID: pk})
		if err != nil {
			return nil, err
		}

		if err := response.Merge(r); err != nil {
			return nil, err
		}
	}

	sendMessagesResponse, err := m.SendChatMessages(context.Background(), messages)
	if err != nil {
		return nil, err
	}

	if err := response.Merge(sendMessagesResponse); err != nil {
		return nil, err
	}

	return response, nil
}

func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	keyUID := m.account.KeyUID
	images, err := m.multiAccounts.GetIdentityImages(keyUID)
	if err != nil {
		return err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	pictures := make([]*protobuf.SyncProfilePicture, len(images))
	clock, chat := m.getLastClockWithRelatedChat()
	for i, image := range images {
		p := &protobuf.SyncProfilePicture{}
		p.Name = image.Name
		p.Payload = image.Payload
		p.Width = uint32(image.Width)
		p.Height = uint32(image.Height)
		p.FileSize = uint32(image.FileSize)
		p.ResizeTarget = uint32(image.ResizeTarget)
		if image.Clock == 0 {
			p.Clock = clock
		} else {
			p.Clock = image.Clock
		}
		pictures[i] = p
	}

	message := &protobuf.SyncProfilePictures{}
	message.KeyUid = keyUID
	message.Pictures = pictures

	encodedMessage, err := proto.Marshal(message)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURES,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

// SyncDevices sends all public chats and contacts to paired devices
// TODO remove use of photoPath in contacts
func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string, rawMessageHandler RawMessageHandler) (err error) {
	if rawMessageHandler == nil {
		rawMessageHandler = m.dispatchMessage
	}

	myID := contactIDFromPublicKey(&m.identity.PublicKey)

	displayName, err := m.settings.DisplayName()
	if err != nil {
		return err
	}

	if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath, rawMessageHandler); err != nil {
		return err
	}

	m.allChats.Range(func(chatID string, chat *Chat) (shouldContinue bool) {
		isPublicChat := !chat.Timeline() && !chat.ProfileUpdates() && chat.Public()
		if isPublicChat && chat.Active {
			err = m.syncPublicChat(ctx, chat, rawMessageHandler)
			if err != nil {
				return false
			}
		}

		if (isPublicChat || chat.OneToOne() || chat.PrivateGroupChat()) && !chat.Active && chat.DeletedAtClockValue > 0 {
			pending, err := m.persistence.HasPendingNotificationsForChat(chat.ID)
			if err != nil {
				return false
			}

			if !pending {
				err = m.syncChatRemoving(ctx, chatID, rawMessageHandler)
				if err != nil {
					return false
				}
			}
		}

		if (isPublicChat || chat.OneToOne() || chat.PrivateGroupChat() || chat.CommunityChat()) && chat.Active {
			err := m.syncChatMessagesRead(ctx, chatID, chat.ReadMessagesAtClockValue, rawMessageHandler)
			if err != nil {
				return false
			}
		}

		if isPublicChat && chat.Active && chat.DeletedAtClockValue > 0 {
			err = m.syncClearHistory(ctx, chat, rawMessageHandler)
			if err != nil {
				return false
			}
		}

		return true
	})
	if err != nil {
		return err
	}

	m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
		if contact.ID != myID &&
			(contact.LocalNickname != "" || contact.added() || contact.Blocked) {
			if err = m.syncContact(ctx, contact, rawMessageHandler); err != nil {
				return false
			}
		}
		return true
	})

	cs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests()
	if err != nil {
		return err
	}
	for _, c := range cs {
		if err = m.syncCommunity(ctx, c, rawMessageHandler); err != nil {
			return err
		}
	}

	bookmarks, err := m.browserDatabase.GetBookmarks()
	if err != nil {
		return err
	}
	for _, b := range bookmarks {
		if err = m.SyncBookmark(ctx, b, rawMessageHandler); err != nil {
			return err
		}
	}

	trustedUsers, err := m.verificationDatabase.GetAllTrustStatus()
	if err != nil {
		return err
	}
	for id, ts := range trustedUsers {
		if err = m.SyncTrustedUser(ctx, id, ts, rawMessageHandler); err != nil {
			return err
		}
	}

	verificationRequests, err := m.verificationDatabase.GetVerificationRequests()
	if err != nil {
		return err
	}
	for i := range verificationRequests {
		if err = m.SyncVerificationRequest(ctx, &verificationRequests[i], rawMessageHandler); err != nil {
			return err
		}
	}

	err = m.syncSettings(rawMessageHandler)
	if err != nil {
		return err
	}

	err = m.syncProfilePictures(rawMessageHandler)
	if err != nil {
		return err
	}

	ids, err := m.persistence.LatestContactRequestIDs()

	if err != nil {
		return err
	}

	for id, state := range ids {
		if state == common.ContactRequestStateAccepted || state == common.ContactRequestStateDismissed {
			accepted := state == common.ContactRequestStateAccepted
			err := m.syncContactRequestDecision(ctx, id, accepted, rawMessageHandler)
			if err != nil {
				return err
			}
		}
	}

	// we have to sync deleted keypairs as well
	keypairs, err := m.settings.GetAllKeypairs()
	if err != nil {
		return err
	}

	for _, kp := range keypairs {
		err = m.syncKeypair(kp, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	// we have to sync deleted watch only accounts as well
	woAccounts, err := m.settings.GetAllWatchOnlyAccounts()
	if err != nil {
		return err
	}

	for _, woAcc := range woAccounts {
		err = m.syncWalletAccount(woAcc, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	savedAddresses, err := m.savedAddressesManager.GetRawSavedAddresses()
	if err != nil {
		return err
	}

	for i := range savedAddresses {
		sa := savedAddresses[i]

		err = m.syncSavedAddress(ctx, sa, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	if err = m.syncEnsUsernameDetails(ctx, rawMessageHandler); err != nil {
		return err
	}

	if err = m.syncDeleteForMeMessage(ctx, rawMessageHandler); err != nil {
		return err
	}

	err = m.syncAccountsPositions(rawMessageHandler)
	if err != nil {
		return err
	}

	return m.syncSocialLinks(context.Background(), rawMessageHandler)
}

func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID string, accepted bool, rawMessageHandler RawMessageHandler) error {
	m.logger.Info("syncContactRequestDecision", zap.Any("from", requestID))
	if !m.hasPairedDevices() {
		return nil
	}

	clock, chat := m.getLastClockWithRelatedChat()

	var status protobuf.SyncContactRequestDecision_DecisionStatus
	if accepted {
		status = protobuf.SyncContactRequestDecision_ACCEPTED
	} else {
		status = protobuf.SyncContactRequestDecision_DECLINED
	}

	message := &protobuf.SyncContactRequestDecision{
		RequestId:      requestID,
		Clock:          clock,
		DecisionStatus: status,
	}

	encodedMessage, err := proto.Marshal(message)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	return nil
}

func (m *Messenger) getLastClockWithRelatedChat() (uint64, *Chat) {
	chatID := contactIDFromPublicKey(&m.identity.PublicKey)

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource())
		// We don't want to show the chat to the user
		chat.Active = false
	}

	m.allChats.Store(chat.ID, chat)
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	return clock, chat
}

// SendPairInstallation sends a pair installation message
func (m *Messenger) SendPairInstallation(ctx context.Context, rawMessageHandler RawMessageHandler) (*MessengerResponse, error) {
	var err error
	var response MessengerResponse

	installation, ok := m.allInstallations.Load(m.installationID)
	if !ok {
		return nil, errors.New("no installation found")
	}

	if installation.InstallationMetadata == nil {
		return nil, errors.New("no installation metadata")
	}

	clock, chat := m.getLastClockWithRelatedChat()

	pairMessage := &protobuf.SyncPairInstallation{
		Clock:          clock,
		Name:           installation.InstallationMetadata.Name,
		InstallationId: installation.ID,
		DeviceType:     installation.InstallationMetadata.DeviceType,
		Version:        installation.Version}
	encodedMessage, err := proto.Marshal(pairMessage)
	if err != nil {
		return nil, err
	}

	if rawMessageHandler == nil {
		rawMessageHandler = m.dispatchPairInstallationMessage
	}
	_, err = rawMessageHandler(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_PAIR_INSTALLATION,
		ResendAutomatically: true,
	})
	if err != nil {
		return nil, err
	}

	response.AddChat(chat)

	chat.LastClockValue = clock
	err = m.saveChat(chat)
	if err != nil {
		return nil, err
	}
	return &response, nil
}

// syncPublicChat sync a public chat with paired devices
func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat, rawMessageHandler RawMessageHandler) error {
	var err error
	if !m.hasPairedDevices() {
		return nil
	}
	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncInstallationPublicChat{
		Clock: clock,
		Id:    publicChat.ID,
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) syncClearHistory(ctx context.Context, publicChat *Chat, rawMessageHandler RawMessageHandler) error {
	var err error
	if !m.hasPairedDevices() {
		return nil
	}
	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncClearHistory{
		ChatId:    publicChat.ID,
		ClearedAt: publicChat.DeletedAtClockValue,
	}

	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_CLEAR_HISTORY,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) syncChatRemoving(ctx context.Context, id string, rawMessageHandler RawMessageHandler) error {
	var err error
	if !m.hasPairedDevices() {
		return nil
	}
	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncChatRemoved{
		Clock: clock,
		Id:    id,
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_CHAT_REMOVED,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

// syncContact sync as contact with paired devices
func (m *Messenger) syncContact(ctx context.Context, contact *Contact, rawMessageHandler RawMessageHandler) error {
	var err error
	if contact.IsSyncing {
		return nil
	}
	if !m.hasPairedDevices() {
		return nil
	}
	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := m.buildSyncContactMessage(contact)

	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_CONTACT_V2,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) syncCommunity(ctx context.Context, community *communities.Community, rawMessageHandler RawMessageHandler) error {
	logger := m.logger.Named("syncCommunity")
	if !m.hasPairedDevices() {
		logger.Debug("device has no paired devices")
		return nil
	}
	logger.Debug("device has paired device(s)")

	clock, chat := m.getLastClockWithRelatedChat()

	communitySettings, err := m.communitiesManager.GetCommunitySettingsByID(community.ID())
	if err != nil {
		return err
	}

	syncMessage, err := community.ToSyncInstallationCommunityProtobuf(clock, communitySettings)
	if err != nil {
		return err
	}

	encodedKeys, err := m.encryptor.GetAllHREncodedKeys(community.ID())
	if err != nil {
		return err
	}
	syncMessage.EncryptionKeys = encodedKeys

	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_COMMUNITY,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}
	logger.Debug("message dispatched")

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) SyncBookmark(ctx context.Context, bookmark *browsers.Bookmark, rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncBookmark{
		Clock:     clock,
		Url:       bookmark.URL,
		Name:      bookmark.Name,
		ImageUrl:  bookmark.ImageURL,
		Removed:   bookmark.Removed,
		DeletedAt: bookmark.DeletedAt,
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_BOOKMARK,
		ResendAutomatically: true,
	}
	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) SyncEnsNamesWithDispatchMessage(ctx context.Context, usernameDetail *ensservice.UsernameDetail) error {
	return m.syncEnsUsernameDetail(ctx, usernameDetail, m.dispatchMessage)
}

func (m *Messenger) syncEnsUsernameDetails(ctx context.Context, rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	ensNameDetails, err := m.getEnsUsernameDetails()
	if err != nil {
		return err
	}
	for _, d := range ensNameDetails {
		if err = m.syncEnsUsernameDetail(ctx, d, rawMessageHandler); err != nil {
			return err
		}
	}
	return nil
}

func (m *Messenger) saveEnsUsernameDetailProto(syncMessage *protobuf.SyncEnsUsernameDetail) (*ensservice.UsernameDetail, error) {
	ud := &ensservice.UsernameDetail{
		Username: syncMessage.Username,
		Clock:    syncMessage.Clock,
		ChainID:  syncMessage.ChainId,
		Removed:  syncMessage.Removed,
	}
	db := ensservice.NewEnsDatabase(m.database)
	err := db.SaveOrUpdateEnsUsername(ud)
	if err != nil {
		return nil, err
	}
	return ud, nil
}

func (m *Messenger) HandleSyncEnsUsernameDetail(state *ReceivedMessageState, syncMessage *protobuf.SyncEnsUsernameDetail, statusMessage *v1protocol.StatusMessage) error {
	ud, err := m.saveEnsUsernameDetailProto(syncMessage)
	if err != nil {
		return err
	}
	state.Response.AddEnsUsernameDetail(ud)
	return nil
}

func (m *Messenger) syncEnsUsernameDetail(ctx context.Context, usernameDetail *ensservice.UsernameDetail, rawMessageHandler RawMessageHandler) error {
	syncMessage := &protobuf.SyncEnsUsernameDetail{
		Clock:    usernameDetail.Clock,
		Username: usernameDetail.Username,
		ChainId:  usernameDetail.ChainID,
		Removed:  usernameDetail.Removed,
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         contactIDFromPublicKey(&m.identity.PublicKey),
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_ENS_USERNAME_DETAIL,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	return err
}

func (m *Messenger) syncAccountCustomizationColor(ctx context.Context, acc *multiaccounts.Account) error {
	if !m.hasPairedDevices() {
		return nil
	}

	_, chat := m.getLastClockWithRelatedChat()

	message := &protobuf.SyncAccountCustomizationColor{
		KeyUid:             acc.KeyUID,
		CustomizationColor: string(acc.CustomizationColor),
		UpdatedAt:          acc.CustomizationColorClock,
	}

	encodedMessage, err := proto.Marshal(message)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_ACCOUNT_CUSTOMIZATION_COLOR,
		ResendAutomatically: true,
	}

	_, err = m.dispatchMessage(ctx, rawMessage)
	return err
}

func (m *Messenger) SyncTrustedUser(ctx context.Context, publicKey string, ts verification.TrustStatus, rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncTrustedUser{
		Clock:  clock,
		Id:     publicKey,
		Status: protobuf.SyncTrustedUser_TrustStatus(ts),
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_TRUSTED_USER,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) SyncVerificationRequest(ctx context.Context, vr *verification.Request, rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	clock, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncVerificationRequest{
		Id:                 vr.ID,
		Clock:              clock,
		From:               vr.From,
		To:                 vr.To,
		Challenge:          vr.Challenge,
		Response:           vr.Response,
		RequestedAt:        vr.RequestedAt,
		RepliedAt:          vr.RepliedAt,
		VerificationStatus: protobuf.SyncVerificationRequest_VerificationStatus(vr.RequestStatus),
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_VERIFICATION_REQUEST,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

// RetrieveAll retrieves messages from all filters, processes them and returns a
// MessengerResponse to the client
func (m *Messenger) RetrieveAll() (*MessengerResponse, error) {
	chatWithMessages, err := m.transport.RetrieveRawAll()
	if err != nil {
		return nil, err
	}

	return m.handleRetrievedMessages(chatWithMessages, true)
}

func (m *Messenger) GetStats() types.StatsSummary {
	return m.transport.GetStats()
}

type CurrentMessageState struct {
	// Message is the protobuf message received
	Message *protobuf.ChatMessage
	// MessageID is the ID of the message
	MessageID string
	// WhisperTimestamp is the whisper timestamp of the message
	WhisperTimestamp uint64
	// Contact is the contact associated with the author of the message
	Contact *Contact
	// PublicKey is the public key of the author of the message
	PublicKey *ecdsa.PublicKey
}

type ReceivedMessageState struct {
	// State on the message being processed
	CurrentMessageState *CurrentMessageState
	// AllChats in memory
	AllChats *chatMap
	// All contacts in memory
	AllContacts *contactMap
	// List of contacts modified
	ModifiedContacts *stringBoolMap
	// All installations in memory
	AllInstallations *installationMap
	// List of communities modified
	ModifiedInstallations *stringBoolMap
	// Map of existing messages
	ExistingMessagesMap map[string]bool
	// EmojiReactions is a list of emoji reactions for the current batch
	// indexed by from-message-id-emoji-type
	EmojiReactions map[string]*EmojiReaction
	// GroupChatInvitations is a list of invitation requests or rejections
	GroupChatInvitations map[string]*GroupChatInvitation
	// Response to the client
	Response *MessengerResponse
	// Timesource is a time source for clock values/timestamps.
	Timesource              common.TimeSource
	AllBookmarks            map[string]*browsers.Bookmark
	AllVerificationRequests []*verification.Request
	AllTrustStatus          map[string]verification.TrustStatus
}

func (m *Messenger) markDeliveredMessages(acks [][]byte) {
	for _, ack := range acks {
		//get message ID from database by datasync ID, with at-least-one
		// semantic
		messageIDBytes, err := m.persistence.MarkAsConfirmed(ack, true)
		if err != nil {
			m.logger.Info("got datasync acknowledge for message we don't have in db", zap.String("ack", hex.EncodeToString(ack)))
			continue
		}

		messageID := messageIDBytes.String()
		//mark messages as delivered

		err = m.UpdateMessageOutgoingStatus(messageID, common.OutgoingStatusDelivered)
		if err != nil {
			m.logger.Debug("Can't set message status as delivered", zap.Error(err))
		}

		//send signal to client that message status updated
		if m.config.messengerSignalsHandler != nil {
			message, err := m.persistence.MessageByID(messageID)
			if err != nil {
				m.logger.Debug("Can't get message from database", zap.Error(err))
				continue
			}
			m.config.messengerSignalsHandler.MessageDelivered(message.LocalChatID, messageID)
		}
	}
}

// addNewMessageNotification takes a common.Message and generates a new NotificationBody and appends it to the
// []Response.Notifications if the message is m.New
func (r *ReceivedMessageState) addNewMessageNotification(publicKey ecdsa.PublicKey, m *common.Message, responseTo *common.Message, profilePicturesVisibility int) error {
	if !m.New {
		return nil
	}

	pubKey, err := m.GetSenderPubKey()
	if err != nil {
		return err
	}
	contactID := contactIDFromPublicKey(pubKey)

	chat, ok := r.AllChats.Load(m.LocalChatID)
	if !ok {
		return fmt.Errorf("chat ID '%s' not present", m.LocalChatID)
	}

	contact, ok := r.AllContacts.Load(contactID)
	if !ok {
		return fmt.Errorf("contact ID '%s' not present", contactID)
	}

	if !chat.Muted {
		if showMessageNotification(publicKey, m, chat, responseTo) {
			notification, err := NewMessageNotification(m.ID, m, chat, contact, r.AllContacts, profilePicturesVisibility)
			if err != nil {
				return err
			}
			r.Response.AddNotification(notification)
		}
	}

	return nil
}

// updateExistingActivityCenterNotification updates AC notification if it exists and hasn't been read yet
func (r *ReceivedMessageState) updateExistingActivityCenterNotification(publicKey ecdsa.PublicKey, m *Messenger, message *common.Message, responseTo *common.Message) error {
	notification, err := m.persistence.GetActivityCenterNotificationByID(types.FromHex(message.ID))
	if err != nil {
		return err
	}

	if notification == nil || notification.Read {
		return nil
	}

	notification.Message = message
	notification.ReplyMessage = responseTo
	notification.UpdatedAt = m.getCurrentTimeInMillis()

	err = m.addActivityCenterNotification(r.Response, notification)
	if err != nil {
		return err
	}

	return nil
}

// addNewActivityCenterNotification takes a common.Message and generates a new ActivityCenterNotification and appends it to the
// []Response.ActivityCenterNotifications if the message is m.New
func (r *ReceivedMessageState) addNewActivityCenterNotification(publicKey ecdsa.PublicKey, m *Messenger, message *common.Message, responseTo *common.Message) error {
	if !message.New {
		return nil
	}

	chat, ok := r.AllChats.Load(message.LocalChatID)
	if !ok {
		return fmt.Errorf("chat ID '%s' not present", message.LocalChatID)
	}

	// Use albumId as notificationId to prevent multiple notifications
	// for same message with multiple images
	var idToUse string

	image := message.GetImage()
	if image != nil && image.GetAlbumId() != "" {
		idToUse = message.GetImage().GetAlbumId()
	} else {
		idToUse = message.ID
	}

	isNotification, notificationType := showMentionOrReplyActivityCenterNotification(publicKey, message, chat, responseTo)
	if isNotification {
		notification := &ActivityCenterNotification{
			ID:           types.FromHex(idToUse),
			Name:         chat.Name,
			Message:      message,
			ReplyMessage: responseTo,
			Type:         notificationType,
			Timestamp:    message.WhisperTimestamp,
			ChatID:       chat.ID,
			CommunityID:  chat.CommunityID,
			Author:       message.From,
			UpdatedAt:    m.getCurrentTimeInMillis(),
		}

		return m.addActivityCenterNotification(r.Response, notification)
	}

	return nil
}

func (m *Messenger) buildMessageState() *ReceivedMessageState {
	return &ReceivedMessageState{
		AllChats:              m.allChats,
		AllContacts:           m.allContacts,
		ModifiedContacts:      new(stringBoolMap),
		AllInstallations:      m.allInstallations,
		ModifiedInstallations: m.modifiedInstallations,
		ExistingMessagesMap:   make(map[string]bool),
		EmojiReactions:        make(map[string]*EmojiReaction),
		GroupChatInvitations:  make(map[string]*GroupChatInvitation),
		Response:              &MessengerResponse{},
		Timesource:            m.getTimesource(),
		AllBookmarks:          make(map[string]*browsers.Bookmark),
		AllTrustStatus:        make(map[string]verification.TrustStatus),
	}
}

func (m *Messenger) outputToCSV(timestamp uint32, messageID types.HexBytes, from string, topic types.TopicType, chatID string, msgType protobuf.ApplicationMetadataMessage_Type, parsedMessage interface{}) {
	if !m.outputCSV {
		return
	}

	msgJSON, err := json.Marshal(parsedMessage)
	if err != nil {
		m.logger.Error("could not marshall message", zap.Error(err))
		return
	}

	line := fmt.Sprintf("%d\t%s\t%s\t%s\t%s\t%s\t%s\n", timestamp, messageID.String(), from, topic.String(), chatID, msgType, msgJSON)
	_, err = m.csvFile.Write([]byte(line))
	if err != nil {
		m.logger.Error("could not write to csv", zap.Error(err))
		return
	}
}

func (m *Messenger) handleImportedMessages(messagesToHandle map[transport.Filter][]*types.Message) error {

	messageState := m.buildMessageState()

	logger := m.logger.With(zap.String("site", "handleImportedMessages"))

	for filter, messages := range messagesToHandle {
		for _, shhMessage := range messages {

			statusMessages, _, err := m.sender.HandleMessages(shhMessage)
			if err != nil {
				logger.Info("failed to decode messages", zap.Error(err))
				continue
			}

			for _, msg := range statusMessages {
				logger := logger.With(zap.String("message-id", msg.TransportMessage.ThirdPartyID))
				logger.Debug("processing message")

				publicKey := msg.SigPubKey()
				senderID := contactIDFromPublicKey(publicKey)

				// Don't process duplicates
				messageID := msg.TransportMessage.ThirdPartyID
				exists, err := m.messageExists(messageID, messageState.ExistingMessagesMap)
				if err != nil {
					logger.Warn("failed to check message exists", zap.Error(err))
				}
				if exists {
					logger.Debug("messageExists", zap.String("messageID", messageID))
					continue
				}

				var contact *Contact
				if c, ok := messageState.AllContacts.Load(senderID); ok {
					contact = c
				} else {
					c, err := buildContact(senderID, publicKey)
					if err != nil {
						logger.Info("failed to build contact", zap.Error(err))
						continue
					}
					contact = c
					messageState.AllContacts.Store(senderID, contact)
				}
				messageState.CurrentMessageState = &CurrentMessageState{
					MessageID:        messageID,
					WhisperTimestamp: uint64(msg.TransportMessage.Timestamp) * 1000,
					Contact:          contact,
					PublicKey:        publicKey,
				}

				if msg.UnwrappedPayload != nil {

					logger.Debug("Handling parsed message")

					switch msg.Type {

					case protobuf.ApplicationMetadataMessage_CHAT_MESSAGE:
						logger.Debug("Handling ChatMessage")

						protoMessage := &protobuf.ChatMessage{}
						err := proto.Unmarshal(msg.UnwrappedPayload, protoMessage)
						if err != nil {
							logger.Warn("failed to unmarshal ChatMessage", zap.Error(err))
							continue
						}

						messageState.CurrentMessageState.Message = protoMessage
						m.outputToCSV(msg.TransportMessage.Timestamp, msg.ID, senderID, filter.ContentTopic, filter.ChatID, msg.Type, messageState.CurrentMessageState.Message)
						err = m.HandleImportedChatMessage(messageState)
						if err != nil {
							logger.Warn("failed to handle ChatMessage", zap.Error(err))
							continue
						}
					}
				}
			}
		}
	}

	importMessageAuthors := messageState.Response.DiscordMessageAuthors()
	if len(importMessageAuthors) > 0 {
		err := m.persistence.SaveDiscordMessageAuthors(importMessageAuthors)
		if err != nil {
			return err
		}
	}

	importMessagesToSave := messageState.Response.DiscordMessages()
	if len(importMessagesToSave) > 0 {
		m.communitiesManager.LogStdout(fmt.Sprintf("saving %d discord messages", len(importMessagesToSave)))
		m.handleImportMessagesMutex.Lock()
		err := m.persistence.SaveDiscordMessages(importMessagesToSave)
		if err != nil {
			m.communitiesManager.LogStdout("failed to save discord messages", zap.Error(err))
			m.handleImportMessagesMutex.Unlock()
			return err
		}
		m.handleImportMessagesMutex.Unlock()
	}

	messageAttachmentsToSave := messageState.Response.DiscordMessageAttachments()
	if len(messageAttachmentsToSave) > 0 {
		m.communitiesManager.LogStdout(fmt.Sprintf("saving %d discord message attachments", len(messageAttachmentsToSave)))
		m.handleImportMessagesMutex.Lock()
		err := m.persistence.SaveDiscordMessageAttachments(messageAttachmentsToSave)
		if err != nil {
			m.communitiesManager.LogStdout("failed to save discord message attachments", zap.Error(err))
			m.handleImportMessagesMutex.Unlock()
			return err
		}
		m.handleImportMessagesMutex.Unlock()
	}

	messagesToSave := messageState.Response.Messages()
	if len(messagesToSave) > 0 {
		m.communitiesManager.LogStdout(fmt.Sprintf("saving %d app messages", len(messagesToSave)))
		m.handleMessagesMutex.Lock()
		err := m.SaveMessages(messagesToSave)
		if err != nil {
			m.handleMessagesMutex.Unlock()
			return err
		}
		m.handleMessagesMutex.Unlock()
	}

	// Save chats if they were modified
	if len(messageState.Response.chats) > 0 {
		err := m.saveChats(messageState.Response.Chats())
		if err != nil {
			return err
		}
	}
	return nil
}

func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filter][]*types.Message, storeWakuMessages bool) (*MessengerResponse, error) {

	m.handleMessagesMutex.Lock()
	defer m.handleMessagesMutex.Unlock()

	messageState := m.buildMessageState()

	logger := m.logger.With(zap.String("site", "RetrieveAll"))

	controlledCommunitiesChatIDs, err := m.communitiesManager.GetControlledCommunitiesChatIDs()
	if err != nil {
		logger.Info("failed to retrieve admin communities", zap.Error(err))
	}

	for filter, messages := range chatWithMessages {
		var processedMessages []string
		for _, shhMessage := range messages {
			logger := logger.With(zap.String("hash", types.EncodeHex(shhMessage.Hash)))
			// Indicates tha all messages in the batch have been processed correctly
			allMessagesProcessed := true

			if controlledCommunitiesChatIDs[filter.ChatID] && storeWakuMessages {
				logger.Debug("storing waku message")
				err := m.communitiesManager.StoreWakuMessage(shhMessage)
				if err != nil {
					logger.Warn("failed to store waku message", zap.Error(err))
				}
			}

			statusMessages, acks, err := m.sender.HandleMessages(shhMessage)
			if err != nil {
				logger.Info("failed to decode messages", zap.Error(err))
				continue
			}

			if m.telemetryClient != nil {
				go m.telemetryClient.PushReceivedMessages(filter, shhMessage, statusMessages)
			}
			m.markDeliveredMessages(acks)

			logger.Debug("processing messages further", zap.Int("count", len(statusMessages)))

			for _, msg := range statusMessages {
				logger := logger.With(zap.String("message-id", msg.ID.String()))
				logger.Info("processing message")
				publicKey := msg.SigPubKey()

				m.handleInstallations(msg.Installations)
				err := m.handleSharedSecrets(msg.SharedSecrets)
				if err != nil {
					// log and continue, non-critical error
					logger.Warn("failed to handle shared secrets")
				}

				senderID := contactIDFromPublicKey(publicKey)
				ownID := contactIDFromPublicKey(m.IdentityPublicKey())
				m.logger.Info("processing message", zap.Any("type", msg.Type), zap.String("senderID", senderID))

				if senderID == ownID {
					// Skip own messages of certain types
					if msg.Type == protobuf.ApplicationMetadataMessage_CONTACT_CODE_ADVERTISEMENT {
						continue
					}
				}

				contact, contactFound := messageState.AllContacts.Load(senderID)

				if _, ok := m.requestedContacts[senderID]; !ok {
					// Check for messages from blocked users
					if contactFound && contact.Blocked {
						continue
					}
				}

				// Don't process duplicates
				messageID := types.EncodeHex(msg.ID)
				exists, err := m.messageExists(messageID, messageState.ExistingMessagesMap)
				if err != nil {
					logger.Warn("failed to check message exists", zap.Error(err))
				}
				if exists {
					logger.Debug("messageExists", zap.String("messageID", messageID))
					continue
				}

				if !contactFound {
					c, err := buildContact(senderID, publicKey)
					if err != nil {
						logger.Info("failed to build contact", zap.Error(err))
						allMessagesProcessed = false
						continue
					}
					contact = c
					messageState.AllContacts.Store(senderID, contact)
					m.forgetContactInfoRequest(senderID)
				}
				messageState.CurrentMessageState = &CurrentMessageState{
					MessageID:        messageID,
					WhisperTimestamp: uint64(msg.TransportMessage.Timestamp) * 1000,
					Contact:          contact,
					PublicKey:        publicKey,
				}

				if msg.UnwrappedPayload != nil {

					err := m.dispatchToHandler(messageState, msg.UnwrappedPayload, msg, filter)
					if err != nil {
						allMessagesProcessed = false
						logger.Warn("failed to process protobuf", zap.Error(err))
						continue
					}
					logger.Debug("Handled parsed message")

				} else {
					logger.Debug("parsed message is nil")
				}
			}

			// Process any community changes
			for _, changes := range messageState.Response.CommunityChanges {
				if changes.ShouldMemberJoin {
					response, err := m.joinCommunity(context.TODO(), changes.Community.ID(), false)
					if err != nil {
						logger.Error("cannot join community", zap.Error(err))
						continue
					}

					if err := messageState.Response.Merge(response); err != nil {
						logger.Error("cannot merge join community response", zap.Error(err))
						continue
					}

				} else if changes.ShouldMemberLeave {
					// this means we've been kicked by the community owner/admin,
					// in this case we don't want to unsubscribe from community updates
					// so we still get notified accordingly when something changes,
					// hence, we're setting `unsubscribeFromCommunity` to `false` here
					response, err := m.leaveCommunity(changes.Community.ID(), false)
					if err != nil {
						logger.Error("cannot leave community", zap.Error(err))
						continue
					}

					if err := messageState.Response.Merge(response); err != nil {
						logger.Error("cannot merge join community response", zap.Error(err))
						continue
					}

					// Activity Center notification
					now := m.getCurrentTimeInMillis()
					notification := &ActivityCenterNotification{
						ID:          types.FromHex(uuid.New().String()),
						Type:        ActivityCenterNotificationTypeCommunityKicked,
						Timestamp:   now,
						CommunityID: changes.Community.IDString(),
						Read:        false,
						UpdatedAt:   now,
					}

					err = m.addActivityCenterNotification(response, notification)
					if err != nil {
						logger.Error("failed to save notification", zap.Error(err))
						continue
					}

					if err := messageState.Response.Merge(response); err != nil {
						logger.Error("cannot merge notification response", zap.Error(err))
						continue
					}
				}
			}

			// Clean up as not used by clients currently
			messageState.Response.CommunityChanges = nil

			// NOTE: for now we confirm messages as processed regardless whether we
			// actually processed them, this is because we need to differentiate
			// from messages that we want to retry to process and messages that
			// are never going to be processed
			m.transport.MarkP2PMessageAsProcessed(gethcommon.BytesToHash(shhMessage.Hash))

			if allMessagesProcessed {
				processedMessages = append(processedMessages, types.EncodeHex(shhMessage.Hash))
			}
		}

		if len(processedMessages) != 0 {
			if err := m.transport.ConfirmMessagesProcessed(processedMessages, m.getTimesource().GetCurrentTime()); err != nil {
				logger.Warn("failed to confirm processed messages", zap.Error(err))
			}
		}
	}

	return m.saveDataAndPrepareResponse(messageState)
}

func (m *Messenger) saveDataAndPrepareResponse(messageState *ReceivedMessageState) (*MessengerResponse, error) {
	var err error
	var contactsToSave []*Contact
	messageState.ModifiedContacts.Range(func(id string, value bool) (shouldContinue bool) {
		contact, ok := messageState.AllContacts.Load(id)
		if ok {
			contactsToSave = append(contactsToSave, contact)
			messageState.Response.AddContact(contact)
		}
		return true
	})

	// Hydrate chat alias and identicon
	for id := range messageState.Response.chats {
		chat, _ := messageState.AllChats.Load(id)
		if chat == nil {
			continue
		}
		if chat.OneToOne() {
			contact, ok := m.allContacts.Load(chat.ID)
			if ok {
				chat.Alias = contact.Alias
				chat.Identicon = contact.Identicon
			}
		}

		messageState.Response.AddChat(chat)
	}

	messageState.ModifiedInstallations.Range(func(id string, value bool) (shouldContinue bool) {
		installation, _ := messageState.AllInstallations.Load(id)
		messageState.Response.Installations = append(messageState.Response.Installations, installation)
		if installation.InstallationMetadata != nil {
			err = m.setInstallationMetadata(id, installation.InstallationMetadata)
			if err != nil {
				return false
			}
		}

		return true
	})
	if err != nil {
		return nil, err
	}

	if len(messageState.Response.chats) > 0 {
		err = m.saveChats(messageState.Response.Chats())
		if err != nil {
			return nil, err
		}
	}

	messagesToSave := messageState.Response.Messages()
	if len(messagesToSave) > 0 {
		err = m.SaveMessages(messagesToSave)
		if err != nil {
			return nil, err
		}
	}

	for _, emojiReaction := range messageState.EmojiReactions {
		messageState.Response.AddEmojiReaction(emojiReaction)
	}

	for _, groupChatInvitation := range messageState.GroupChatInvitations {
		messageState.Response.Invitations = append(messageState.Response.Invitations, groupChatInvitation)
	}

	if len(contactsToSave) > 0 {
		err = m.persistence.SaveContacts(contactsToSave)
		if err != nil {
			return nil, err
		}
	}

	newMessagesIds := map[string]struct{}{}
	for _, message := range messagesToSave {
		if message.New {
			newMessagesIds[message.ID] = struct{}{}
		}
	}

	messagesWithResponses, err := m.pullMessagesAndResponsesFromDB(messagesToSave)
	if err != nil {
		return nil, err
	}
	messagesByID := map[string]*common.Message{}
	for _, message := range messagesWithResponses {
		messagesByID[message.ID] = message
	}
	messageState.Response.SetMessages(messagesWithResponses)

	notificationsEnabled, err := m.settings.GetNotificationsEnabled()
	if err != nil {
		return nil, err
	}

	profilePicturesVisibility, err := m.settings.GetProfilePicturesVisibility()
	if err != nil {
		return nil, err
	}

	m.prepareMessages(messageState.Response.messages)

	for _, message := range messageState.Response.messages {
		if _, ok := newMessagesIds[message.ID]; ok {
			message.New = true

			if notificationsEnabled {
				// Create notification body to be eventually passed to `localnotifications.SendMessageNotifications()`
				if err = messageState.addNewMessageNotification(m.identity.PublicKey, message, messagesByID[message.ResponseTo], profilePicturesVisibility); err != nil {
					return nil, err
				}
			}

			// Create activity center notification body to be eventually passed to `activitycenter.SendActivityCenterNotifications()`
			if err = messageState.addNewActivityCenterNotification(m.identity.PublicKey, m, message, messagesByID[message.ResponseTo]); err != nil {
				return nil, err
			}
		}
	}

	// Reset installations
	m.modifiedInstallations = new(stringBoolMap)

	if len(messageState.AllBookmarks) > 0 {
		bookmarks, err := m.storeSyncBookmarks(messageState.AllBookmarks)
		if err != nil {
			return nil, err
		}
		messageState.Response.AddBookmarks(bookmarks)
	}

	if len(messageState.AllVerificationRequests) > 0 {
		for _, vr := range messageState.AllVerificationRequests {
			messageState.Response.AddVerificationRequest(vr)
		}
	}

	if len(messageState.AllTrustStatus) > 0 {
		messageState.Response.AddTrustStatuses(messageState.AllTrustStatus)
	}

	// Hydrate pinned messages
	for _, pinnedMessage := range messageState.Response.PinMessages() {
		if pinnedMessage.Pinned {
			pinnedMessage.Message = &common.PinnedMessage{
				Message:  messageState.Response.GetMessage(pinnedMessage.MessageId),
				PinnedBy: pinnedMessage.From,
				PinnedAt: pinnedMessage.Clock,
			}
		}
	}

	return messageState.Response, nil
}

func (m *Messenger) storeSyncBookmarks(bookmarkMap map[string]*browsers.Bookmark) ([]*browsers.Bookmark, error) {
	var bookmarks []*browsers.Bookmark
	for _, bookmark := range bookmarkMap {
		bookmarks = append(bookmarks, bookmark)
	}
	return m.browserDatabase.StoreSyncBookmarks(bookmarks)
}

func (m *Messenger) MessageByID(id string) (*common.Message, error) {
	return m.persistence.MessageByID(id)
}

func (m *Messenger) MessagesExist(ids []string) (map[string]bool, error) {
	return m.persistence.MessagesExist(ids)
}

func (m *Messenger) FirstUnseenMessageID(chatID string) (string, error) {
	return m.persistence.FirstUnseenMessageID(chatID)
}

func (m *Messenger) latestIncomingMessageClock(chatID string) (uint64, error) {
	return m.persistence.latestIncomingMessageClock(chatID)
}

func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common.Message, string, error) {
	chat, err := m.persistence.Chat(chatID)
	if err != nil {
		return nil, "", err
	}

	if chat == nil {
		return nil, "", ErrChatNotFound
	}

	var msgs []*common.Message
	var nextCursor string

	if chat.Timeline() {
		var chatIDs = []string{"@" + contactIDFromPublicKey(&m.identity.PublicKey)}
		m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
			if contact.added() {
				chatIDs = append(chatIDs, "@"+contact.ID)
			}
			return true
		})
		msgs, nextCursor, err = m.persistence.MessageByChatIDs(chatIDs, cursor, limit)
		if err != nil {
			return nil, "", err
		}
	} else {
		msgs, nextCursor, err = m.persistence.MessageByChatID(chatID, cursor, limit)
		if err != nil {
			return nil, "", err
		}

	}
	if m.httpServer != nil {
		for idx := range msgs {
			m.prepareMessage(msgs[idx], m.httpServer)
		}
	}

	return msgs, nextCursor, nil
}

func (m *Messenger) prepareMessages(messages map[string]*common.Message) {
	if m.httpServer != nil {
		for idx := range messages {
			m.prepareMessage(messages[idx], m.httpServer)
		}
	}
}

func (m *Messenger) prepareMessage(msg *common.Message, s *server.MediaServer) {
	if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) {
		msg.QuotedMessage.ImageLocalURL = s.MakeImageURL(msg.QuotedMessage.ID)
	}
	if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_AUDIO) {
		msg.QuotedMessage.AudioLocalURL = s.MakeAudioURL(msg.QuotedMessage.ID)
	}
	if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_STICKER) {
		msg.QuotedMessage.HasSticker = true
	}
	if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_DISCORD_MESSAGE) {
		dm := msg.QuotedMessage.DiscordMessage
		exists, err := m.persistence.HasDiscordMessageAuthorImagePayload(dm.Author.Id)
		if err != nil {
			return
		}

		if exists {
			msg.QuotedMessage.DiscordMessage.Author.LocalUrl = s.MakeDiscordAuthorAvatarURL(dm.Author.Id)
		}
	}

	if msg.ContentType == protobuf.ChatMessage_IMAGE {
		msg.ImageLocalURL = s.MakeImageURL(msg.ID)
	}

	if msg.ContentType == protobuf.ChatMessage_DISCORD_MESSAGE {

		dm := msg.GetDiscordMessage()
		exists, err := m.persistence.HasDiscordMessageAuthorImagePayload(dm.Author.Id)
		if err != nil {
			return
		}

		if exists {
			dm.Author.LocalUrl = s.MakeDiscordAuthorAvatarURL(dm.Author.Id)
		}

		for idx, attachment := range dm.Attachments {
			if strings.Contains(attachment.ContentType, "image") {
				hasPayload, err := m.persistence.HasDiscordMessageAttachmentPayload(attachment.Id, dm.Id)
				if err != nil {
					m.logger.Error("failed to check if message attachment exist", zap.Error(err))
					continue
				}
				if hasPayload {
					localURL := s.MakeDiscordAttachmentURL(dm.Id, attachment.Id)
					dm.Attachments[idx].LocalUrl = localURL
				}
			}
		}
		msg.Payload = &protobuf.ChatMessage_DiscordMessage{
			DiscordMessage: dm,
		}
	}
	if msg.ContentType == protobuf.ChatMessage_AUDIO {
		msg.AudioLocalURL = s.MakeAudioURL(msg.ID)
	}
	if msg.ContentType == protobuf.ChatMessage_STICKER {
		msg.StickerLocalURL = s.MakeStickerURL(msg.GetSticker().Hash)
	}

	msg.LinkPreviews = msg.ConvertFromProtoToLinkPreviews(s.MakeLinkPreviewThumbnailURL)
}

func (m *Messenger) AllMessageByChatIDWhichMatchTerm(chatID string, searchTerm string, caseSensitive bool) ([]*common.Message, error) {
	_, err := m.persistence.Chat(chatID)
	if err != nil {
		return nil, err
	}

	return m.persistence.AllMessageByChatIDWhichMatchTerm(chatID, searchTerm, caseSensitive)
}

func (m *Messenger) AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communityIds []string, chatIds []string, searchTerm string, caseSensitive bool) ([]*common.Message, error) {
	return m.persistence.AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communityIds, chatIds, searchTerm, caseSensitive)
}

func (m *Messenger) SaveMessages(messages []*common.Message) error {
	return m.persistence.SaveMessages(messages)
}

func (m *Messenger) DeleteMessage(id string) error {
	return m.persistence.DeleteMessage(id)
}

func (m *Messenger) DeleteMessagesByChatID(id string) error {
	return m.persistence.DeleteMessagesByChatID(id)
}

// MarkMessagesSeen marks messages with `ids` as seen in the chat `chatID`.
// It returns the number of affected messages or error. If there is an error,
// the number of affected messages is always zero.
func (m *Messenger) MarkMessagesSeen(chatID string, ids []string) (uint64, uint64, error) {
	count, countWithMentions, err := m.persistence.MarkMessagesSeen(chatID, ids)
	if err != nil {
		return 0, 0, err
	}
	chat, err := m.persistence.Chat(chatID)
	if err != nil {
		return 0, 0, err
	}
	m.allChats.Store(chatID, chat)
	return count, countWithMentions, nil
}

func (m *Messenger) syncChatMessagesRead(ctx context.Context, chatID string, clock uint64, rawMessageHandler RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	_, chat := m.getLastClockWithRelatedChat()

	syncMessage := &protobuf.SyncChatMessagesRead{
		Clock: clock,
		Id:    chatID,
	}
	encodedMessage, err := proto.Marshal(syncMessage)
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_CHAT_MESSAGES_READ,
		ResendAutomatically: true,
	}

	_, err = rawMessageHandler(ctx, rawMessage)

	return err
}

func (m *Messenger) markAllRead(chatID string, clock uint64, shouldBeSynced bool) error {
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return errors.New("chat not found")
	}

	_, _, err := m.persistence.MarkAllRead(chatID, clock)
	if err != nil {
		return err
	}

	if shouldBeSynced {
		err := m.syncChatMessagesRead(context.Background(), chatID, clock, m.dispatchMessage)
		if err != nil {
			return err
		}
	}

	chat.ReadMessagesAtClockValue = clock
	chat.Highlight = false

	chat.UnviewedMessagesCount = 0
	chat.UnviewedMentionsCount = 0

	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allChats.Store(chat.ID, chat)
	return m.persistence.SaveChats([]*Chat{chat})
}

func (m *Messenger) MarkAllRead(chatID string) error {
	notifications, err := m.persistence.DismissAllActivityCenterNotificationsFromChatID(chatID, m.getCurrentTimeInMillis())
	if err != nil {
		return err
	}
	err = m.syncActivityCenterNotifications(notifications)
	if err != nil {
		m.logger.Error("MarkAllRead, failed to sync activity center notifications", zap.Error(err))
		return err
	}

	clock, _ := m.latestIncomingMessageClock(chatID)

	if clock == 0 {
		chat, ok := m.allChats.Load(chatID)
		if !ok {
			return errors.New("chat not found")
		}
		clock, _ = chat.NextClockAndTimestamp(m.getTimesource())
	}

	return m.markAllRead(chatID, clock, true)
}

func (m *Messenger) MarkAllReadInCommunity(communityID string) ([]string, error) {
	notifications, err := m.persistence.DismissAllActivityCenterNotificationsFromCommunity(communityID, m.getCurrentTimeInMillis())
	if err != nil {
		return nil, err
	}

	chatIDs, err := m.persistence.AllChatIDsByCommunity(communityID)
	if err != nil {
		return nil, err
	}

	err = m.persistence.MarkAllReadMultiple(chatIDs)
	if err != nil {
		return nil, err
	}

	for _, chatID := range chatIDs {
		chat, ok := m.allChats.Load(chatID)

		if ok {
			chat.UnviewedMessagesCount = 0
			chat.UnviewedMentionsCount = 0
			m.allChats.Store(chat.ID, chat)
		} else {
			err = errors.New(fmt.Sprintf("chat with chatID %s not found", chatID))
		}
	}
	if err != nil {
		return chatIDs, err
	}

	err = m.syncActivityCenterNotifications(notifications)
	if err != nil {
		m.logger.Error("MarkAllReadInCommunity, error syncing activity center notifications", zap.Error(err))
	}

	return chatIDs, err
}

// MuteChat signals to the messenger that we don't want to be notified
// on new messages from this chat
func (m *Messenger) MuteChat(request *requests.MuteChat) (time.Time, error) {
	chat, ok := m.allChats.Load(request.ChatID)
	if !ok {
		// Only one to one chan be muted when it's not in the database
		publicKey, err := common.HexToPubkey(request.ChatID)
		if err != nil {
			return time.Time{}, err
		}

		// Create a one to one chat and set active to false
		chat = CreateOneToOneChat(request.ChatID, publicKey, m.getTimesource())
		chat.Active = false
		err = m.initChatSyncFields(chat)
		if err != nil {
			return time.Time{}, err
		}
		err = m.saveChat(chat)
		if err != nil {
			return time.Time{}, err
		}
	}

	var contact *Contact
	if chat.OneToOne() {
		contact, _ = m.allContacts.Load(request.ChatID)
	}

	var MuteTill time.Time

	switch request.MutedType {
	case MuteTill1Min:
		MuteTill = time.Now().Add(MuteFor1MinDuration)
	case MuteFor15Min:
		MuteTill = time.Now().Add(MuteFor15MinsDuration)
	case MuteFor1Hr:
		MuteTill = time.Now().Add(MuteFor1HrsDuration)
	case MuteFor8Hr:
		MuteTill = time.Now().Add(MuteFor8HrsDuration)
	case MuteFor1Week:
		MuteTill = time.Now().Add(MuteFor1WeekDuration)
	default:
		MuteTill = time.Time{}
	}
	err := m.saveChat(chat)
	if err != nil {
		return time.Time{}, err
	}

	muteTillTimeRemoveMs, err := time.Parse(time.RFC3339, MuteTill.Format(time.RFC3339))

	if err != nil {
		return time.Time{}, err
	}

	return m.muteChat(chat, contact, muteTillTimeRemoveMs)
}

func (m *Messenger) MuteChatV2(muteParams *requests.MuteChat) (time.Time, error) {
	return m.MuteChat(muteParams)
}

func (m *Messenger) muteChat(chat *Chat, contact *Contact, mutedTill time.Time) (time.Time, error) {
	err := m.persistence.MuteChat(chat.ID, mutedTill)
	if err != nil {
		return time.Time{}, err
	}

	chat.Muted = true
	chat.MuteTill = mutedTill
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allChats.Store(chat.ID, chat)

	if contact != nil {
		err := m.syncContact(context.Background(), contact, m.dispatchMessage)
		if err != nil {
			return time.Time{}, err
		}
	}

	if !chat.MuteTill.IsZero() {
		err := m.reregisterForPushNotifications()
		if err != nil {
			return time.Time{}, err
		}
		return mutedTill, nil
	}

	return time.Time{}, m.reregisterForPushNotifications()
}

// UnmuteChat signals to the messenger that we want to be notified
// on new messages from this chat
func (m *Messenger) UnmuteChat(chatID string) error {
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return errors.New("chat not found")
	}

	var contact *Contact
	if chat.OneToOne() {
		contact, _ = m.allContacts.Load(chatID)
	}

	return m.unmuteChat(chat, contact)
}

func (m *Messenger) unmuteChat(chat *Chat, contact *Contact) error {
	err := m.persistence.UnmuteChat(chat.ID)
	if err != nil {
		return err
	}

	chat.Muted = false
	chat.MuteTill = time.Time{}
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allChats.Store(chat.ID, chat)

	if chat.CommunityChat() {
		community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
		if err != nil {
			return err
		}

		err = m.communitiesManager.SetMuted(community.ID(), false)
		if err != nil {
			return err
		}
	}

	if contact != nil {
		err := m.syncContact(context.Background(), contact, m.dispatchMessage)
		if err != nil {
			return err
		}
	}
	return m.reregisterForPushNotifications()
}

func (m *Messenger) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error {
	return m.persistence.UpdateMessageOutgoingStatus(id, newOutgoingStatus)
}

// Identicon returns an identicon based on the input string
func Identicon(id string) (string, error) {
	return identicon.GenerateBase64(id)
}

// GenerateAlias name returns the generated name given a public key hex encoded prefixed with 0x
func GenerateAlias(id string) (string, error) {
	return alias.GenerateFromPublicKeyString(id)
}

func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*MessengerResponse, error) {
	var response MessengerResponse

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	message := common.NewMessage()
	err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport)
	if err != nil {
		return nil, err
	}

	message.MessageType = protobuf.MessageType_ONE_TO_ONE
	message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND
	message.Seen = true
	message.Text = "Request transaction"

	request := &protobuf.RequestTransaction{
		Clock:    message.Clock,
		Address:  address,
		Value:    value,
		Contract: contract,
		ChatId:   chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}
	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_REQUEST_TRANSACTION,
		ResendAutomatically: true,
	})

	message.CommandParameters = &common.CommandParameters{
		ID:           rawMessage.ID,
		Value:        value,
		Address:      address,
		Contract:     contract,
		CommandState: common.CommandStateRequestTransaction,
	}

	if err != nil {
		return nil, err
	}
	messageID := rawMessage.ID

	message.ID = messageID
	message.CommandParameters.ID = messageID
	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*MessengerResponse, error) {
	var response MessengerResponse

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	message := common.NewMessage()
	err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport)
	if err != nil {
		return nil, err
	}

	message.MessageType = protobuf.MessageType_ONE_TO_ONE
	message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND
	message.Seen = true
	message.Text = "Request address for transaction"

	request := &protobuf.RequestAddressForTransaction{
		Clock:    message.Clock,
		Value:    value,
		Contract: contract,
		ChatId:   chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}
	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_REQUEST_ADDRESS_FOR_TRANSACTION,
		ResendAutomatically: true,
	})

	message.CommandParameters = &common.CommandParameters{
		ID:           rawMessage.ID,
		From:         from,
		Value:        value,
		Contract:     contract,
		CommandState: common.CommandStateRequestAddressForTransaction,
	}

	if err != nil {
		return nil, err
	}
	messageID := rawMessage.ID

	message.ID = messageID
	message.CommandParameters.ID = messageID
	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*MessengerResponse, error) {
	var response MessengerResponse

	message, err := m.MessageByID(messageID)
	if err != nil {
		return nil, err
	}

	if message == nil {
		return nil, errors.New("message not found")
	}

	chatID := message.LocalChatID

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	clock, timestamp := chat.NextClockAndTimestamp(m.transport)
	message.Clock = clock
	message.WhisperTimestamp = timestamp
	message.Timestamp = timestamp
	message.Text = "Request address for transaction accepted"
	message.Seen = true
	message.OutgoingStatus = common.OutgoingStatusSending

	// Hide previous message
	previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID)
	if err != nil {
		return nil, err
	}

	if previousMessage == nil {
		return nil, errors.New("No previous message found")
	}

	err = m.persistence.HideMessage(previousMessage.ID)
	if err != nil {
		return nil, err
	}

	message.Replace = previousMessage.ID

	request := &protobuf.AcceptRequestAddressForTransaction{
		Clock:   message.Clock,
		Id:      messageID,
		Address: address,
		ChatId:  chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}

	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION,
		ResendAutomatically: true,
	})

	if err != nil {
		return nil, err
	}

	message.ID = rawMessage.ID
	message.CommandParameters.Address = address
	message.CommandParameters.CommandState = common.CommandStateRequestAddressForTransactionAccepted

	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID string) (*MessengerResponse, error) {
	var response MessengerResponse

	message, err := m.MessageByID(messageID)
	if err != nil {
		return nil, err
	}

	if message == nil {
		return nil, errors.New("message not found")
	}

	chatID := message.LocalChatID

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	clock, timestamp := chat.NextClockAndTimestamp(m.transport)
	message.Clock = clock
	message.WhisperTimestamp = timestamp
	message.Timestamp = timestamp
	message.Text = "Transaction request declined"
	message.Seen = true
	message.OutgoingStatus = common.OutgoingStatusSending
	message.Replace = messageID

	err = m.persistence.HideMessage(messageID)
	if err != nil {
		return nil, err
	}

	request := &protobuf.DeclineRequestTransaction{
		Clock:  message.Clock,
		Id:     messageID,
		ChatId: chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}

	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_TRANSACTION,
		ResendAutomatically: true,
	})

	if err != nil {
		return nil, err
	}

	message.ID = rawMessage.ID
	message.CommandParameters.CommandState = common.CommandStateRequestTransactionDeclined

	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*MessengerResponse, error) {
	var response MessengerResponse

	message, err := m.MessageByID(messageID)
	if err != nil {
		return nil, err
	}

	if message == nil {
		return nil, errors.New("message not found")
	}

	chatID := message.LocalChatID

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	clock, timestamp := chat.NextClockAndTimestamp(m.transport)
	message.Clock = clock
	message.WhisperTimestamp = timestamp
	message.Timestamp = timestamp
	message.Text = "Request address for transaction declined"
	message.Seen = true
	message.OutgoingStatus = common.OutgoingStatusSending
	message.Replace = messageID

	err = m.persistence.HideMessage(messageID)
	if err != nil {
		return nil, err
	}

	request := &protobuf.DeclineRequestAddressForTransaction{
		Clock:  message.Clock,
		Id:     messageID,
		ChatId: chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}

	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION,
		ResendAutomatically: true,
	})

	if err != nil {
		return nil, err
	}

	message.ID = rawMessage.ID
	message.CommandParameters.CommandState = common.CommandStateRequestAddressForTransactionDeclined

	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature []byte) (*MessengerResponse, error) {
	var response MessengerResponse

	message, err := m.MessageByID(messageID)
	if err != nil {
		return nil, err
	}

	if message == nil {
		return nil, errors.New("message not found")
	}

	chatID := message.LocalChatID

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	clock, timestamp := chat.NextClockAndTimestamp(m.transport)
	message.Clock = clock
	message.WhisperTimestamp = timestamp
	message.Timestamp = timestamp
	message.Seen = true
	message.Text = transactionSentTxt
	message.OutgoingStatus = common.OutgoingStatusSending

	// Hide previous message
	previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID)
	if err != nil && err != common.ErrRecordNotFound {
		return nil, err
	}

	if previousMessage != nil {
		err = m.persistence.HideMessage(previousMessage.ID)
		if err != nil {
			return nil, err
		}
		message.Replace = previousMessage.ID
	}

	err = m.persistence.HideMessage(messageID)
	if err != nil {
		return nil, err
	}

	request := &protobuf.SendTransaction{
		Clock:           message.Clock,
		Id:              messageID,
		TransactionHash: transactionHash,
		Signature:       signature,
		ChatId:          chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}

	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SEND_TRANSACTION,
		ResendAutomatically: true,
	})

	if err != nil {
		return nil, err
	}

	message.ID = rawMessage.ID
	message.CommandParameters.TransactionHash = transactionHash
	message.CommandParameters.Signature = signature
	message.CommandParameters.CommandState = common.CommandStateTransactionSent

	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature []byte) (*MessengerResponse, error) {
	var response MessengerResponse

	// A valid added chat is required.
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, errors.New("Chat not found")
	}
	if chat.ChatType != ChatTypeOneToOne {
		return nil, errors.New("Need to be a one-to-one chat")
	}

	message := common.NewMessage()
	err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport)
	if err != nil {
		return nil, err
	}

	message.MessageType = protobuf.MessageType_ONE_TO_ONE
	message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND
	message.LocalChatID = chatID

	clock, timestamp := chat.NextClockAndTimestamp(m.transport)
	message.Clock = clock
	message.WhisperTimestamp = timestamp
	message.Seen = true
	message.Timestamp = timestamp
	message.Text = transactionSentTxt

	request := &protobuf.SendTransaction{
		Clock:           message.Clock,
		TransactionHash: transactionHash,
		Signature:       signature,
		ChatId:          chatID,
	}
	encodedMessage, err := proto.Marshal(request)
	if err != nil {
		return nil, err
	}

	rawMessage, err := m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SEND_TRANSACTION,
		ResendAutomatically: true,
	})

	if err != nil {
		return nil, err
	}

	message.ID = rawMessage.ID
	message.CommandParameters = &common.CommandParameters{
		TransactionHash: transactionHash,
		Value:           value,
		Contract:        contract,
		Signature:       signature,
		CommandState:    common.CommandStateTransactionSent,
	}

	err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
	if err != nil {
		return nil, err
	}

	err = chat.UpdateFromMessage(message, m.transport)
	if err != nil {
		return nil, err
	}

	err = m.persistence.SaveMessages([]*common.Message{message})
	if err != nil {
		return nil, err
	}

	return m.addMessagesAndChat(chat, []*common.Message{message}, &response)
}

func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types.Address) (*MessengerResponse, error) {
	if m.verifyTransactionClient == nil {
		return nil, nil
	}

	logger := m.logger.With(zap.String("site", "ValidateTransactions"))
	logger.Debug("Validating transactions")
	txs, err := m.persistence.TransactionsToValidate()
	if err != nil {
		logger.Error("Error pulling", zap.Error(err))
		return nil, err
	}
	logger.Debug("Txs", zap.Int("count", len(txs)), zap.Any("txs", txs))
	var response MessengerResponse
	validator := NewTransactionValidator(addresses, m.persistence, m.verifyTransactionClient, m.logger)
	responses, err := validator.ValidateTransactions(ctx)
	if err != nil {
		logger.Error("Error validating", zap.Error(err))
		return nil, err
	}
	for _, validationResult := range responses {
		var message *common.Message
		chatID := contactIDFromPublicKey(validationResult.Transaction.From)
		chat, ok := m.allChats.Load(chatID)
		if !ok {
			chat = OneToOneFromPublicKey(validationResult.Transaction.From, m.transport)
		}
		if validationResult.Message != nil {
			message = validationResult.Message
		} else {
			message = common.NewMessage()
			err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport)
			if err != nil {
				return nil, err
			}
		}

		message.MessageType = protobuf.MessageType_ONE_TO_ONE
		message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND
		message.LocalChatID = chatID
		message.OutgoingStatus = ""

		clock, timestamp := chat.NextClockAndTimestamp(m.transport)
		message.Clock = clock
		message.Timestamp = timestamp
		message.WhisperTimestamp = timestamp
		message.Text = "Transaction received"
		message.Seen = false

		message.ID = validationResult.Transaction.MessageID
		if message.CommandParameters == nil {
			message.CommandParameters = &common.CommandParameters{}
		} else {
			message.CommandParameters = validationResult.Message.CommandParameters
		}

		message.CommandParameters.Value = validationResult.Value
		message.CommandParameters.Contract = validationResult.Contract
		message.CommandParameters.Address = validationResult.Address
		message.CommandParameters.CommandState = common.CommandStateTransactionSent
		message.CommandParameters.TransactionHash = validationResult.Transaction.TransactionHash

		err = message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
		if err != nil {
			return nil, err
		}

		err = chat.UpdateFromMessage(message, m.transport)
		if err != nil {
			return nil, err
		}

		if len(message.CommandParameters.ID) != 0 {
			// Hide previous message
			previousMessage, err := m.persistence.MessageByCommandID(chatID, message.CommandParameters.ID)
			if err != nil && err != common.ErrRecordNotFound {
				return nil, err
			}

			if previousMessage != nil {
				err = m.persistence.HideMessage(previousMessage.ID)
				if err != nil {
					return nil, err
				}
				message.Replace = previousMessage.ID
			}
		}

		response.AddMessage(message)
		m.allChats.Store(chat.ID, chat)
		response.AddChat(chat)

		contact, err := m.getOrBuildContactFromMessage(message)
		if err != nil {
			return nil, err
		}

		notificationsEnabled, err := m.settings.GetNotificationsEnabled()
		if err != nil {
			return nil, err
		}

		profilePicturesVisibility, err := m.settings.GetProfilePicturesVisibility()
		if err != nil {
			return nil, err
		}

		if notificationsEnabled {
			notification, err := NewMessageNotification(message.ID, message, chat, contact, m.allContacts, profilePicturesVisibility)
			if err != nil {
				return nil, err
			}
			response.AddNotification(notification)
		}

	}

	if len(response.messages) > 0 {
		err = m.SaveMessages(response.Messages())
		if err != nil {
			return nil, err
		}
	}
	return &response, nil
}

// pullMessagesAndResponsesFromDB pulls all the messages and the one that have
// been replied to from the database
func (m *Messenger) pullMessagesAndResponsesFromDB(messages []*common.Message) ([]*common.Message, error) {
	var messageIDs []string
	for _, message := range messages {
		messageIDs = append(messageIDs, message.ID)
		if len(message.ResponseTo) != 0 {
			messageIDs = append(messageIDs, message.ResponseTo)
		}

	}
	// We pull from the database all the messages & replies involved,
	// so we let the db build the correct messages
	return m.persistence.MessagesByIDs(messageIDs)
}

func (m *Messenger) SignMessage(message string) ([]byte, error) {
	hash := crypto.TextHash([]byte(message))
	return crypto.Sign(hash, m.identity)
}

func (m *Messenger) getTimesource() common.TimeSource {
	return m.transport
}

func (m *Messenger) getCurrentTimeInMillis() uint64 {
	return m.getTimesource().GetCurrentTime()
}

// AddPushNotificationsServer adds a push notification server
func (m *Messenger) AddPushNotificationsServer(ctx context.Context, publicKey *ecdsa.PublicKey, serverType pushnotificationclient.ServerType) error {
	if m.pushNotificationClient == nil {
		return errors.New("push notification client not enabled")
	}
	return m.pushNotificationClient.AddPushNotificationsServer(publicKey, serverType)
}

// RemovePushNotificationServer removes a push notification server
func (m *Messenger) RemovePushNotificationServer(ctx context.Context, publicKey *ecdsa.PublicKey) error {
	if m.pushNotificationClient == nil {
		return errors.New("push notification client not enabled")
	}
	return m.pushNotificationClient.RemovePushNotificationServer(publicKey)
}

// UnregisterFromPushNotifications unregister from any server
func (m *Messenger) UnregisterFromPushNotifications(ctx context.Context) error {
	return m.pushNotificationClient.Unregister()
}

// DisableSendingPushNotifications signals the client not to send any push notification
func (m *Messenger) DisableSendingPushNotifications() error {
	if m.pushNotificationClient == nil {
		return errors.New("push notification client not enabled")
	}
	m.pushNotificationClient.DisableSending()
	return nil
}

// EnableSendingPushNotifications signals the client to send push notifications
func (m *Messenger) EnableSendingPushNotifications() error {
	if m.pushNotificationClient == nil {
		return errors.New("push notification client not enabled")
	}
	m.pushNotificationClient.EnableSending()
	return nil
}

func (m *Messenger) pushNotificationOptions() *pushnotificationclient.RegistrationOptions {
	var contactIDs []*ecdsa.PublicKey
	var mutedChatIDs []string
	var publicChatIDs []string
	var blockedChatIDs []string

	m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
		if contact.added() && !contact.Blocked {
			pk, err := contact.PublicKey()
			if err != nil {
				m.logger.Warn("could not parse contact public key")
				return true
			}
			contactIDs = append(contactIDs, pk)
		} else if contact.Blocked {
			blockedChatIDs = append(blockedChatIDs, contact.ID)
		}
		return true
	})

	m.allChats.Range(func(chatID string, chat *Chat) (shouldContinue bool) {
		if chat.Muted {
			mutedChatIDs = append(mutedChatIDs, chat.ID)
			return true
		}
		if chat.Active && (chat.Public() || chat.CommunityChat()) {
			publicChatIDs = append(publicChatIDs, chat.ID)
		}
		return true
	})

	return &pushnotificationclient.RegistrationOptions{
		ContactIDs:     contactIDs,
		MutedChatIDs:   mutedChatIDs,
		PublicChatIDs:  publicChatIDs,
		BlockedChatIDs: blockedChatIDs,
	}
}

// RegisterForPushNotification register deviceToken with any push notification server enabled
func (m *Messenger) RegisterForPushNotifications(ctx context.Context, deviceToken, apnTopic string, tokenType protobuf.PushNotificationRegistration_TokenType) error {
	if m.pushNotificationClient == nil {
		return errors.New("push notification client not enabled")
	}
	m.mutex.Lock()
	defer m.mutex.Unlock()

	err := m.pushNotificationClient.Register(deviceToken, apnTopic, tokenType, m.pushNotificationOptions())
	if err != nil {
		m.logger.Error("failed to register for push notifications", zap.Error(err))
		return err
	}
	return nil
}

// RegisteredForPushNotifications returns whether we successfully registered with all the servers
func (m *Messenger) RegisteredForPushNotifications() (bool, error) {
	if m.pushNotificationClient == nil {
		return false, errors.New("no push notification client")
	}
	return m.pushNotificationClient.Registered()
}

// EnablePushNotificationsFromContactsOnly is used to indicate that we want to received push notifications only from contacts
func (m *Messenger) EnablePushNotificationsFromContactsOnly() error {
	if m.pushNotificationClient == nil {
		return errors.New("no push notification client")
	}
	m.mutex.Lock()
	defer m.mutex.Unlock()

	return m.pushNotificationClient.EnablePushNotificationsFromContactsOnly(m.pushNotificationOptions())
}

// DisablePushNotificationsFromContactsOnly is used to indicate that we want to received push notifications from anyone
func (m *Messenger) DisablePushNotificationsFromContactsOnly() error {
	if m.pushNotificationClient == nil {
		return errors.New("no push notification client")
	}
	m.mutex.Lock()
	defer m.mutex.Unlock()

	return m.pushNotificationClient.DisablePushNotificationsFromContactsOnly(m.pushNotificationOptions())
}

// EnablePushNotificationsBlockMentions is used to indicate that we dont want to received push notifications for mentions
func (m *Messenger) EnablePushNotificationsBlockMentions() error {
	if m.pushNotificationClient == nil {
		return errors.New("no push notification client")
	}
	m.mutex.Lock()
	defer m.mutex.Unlock()

	return m.pushNotificationClient.EnablePushNotificationsBlockMentions(m.pushNotificationOptions())
}

// DisablePushNotificationsBlockMentions is used to indicate that we want to received push notifications for mentions
func (m *Messenger) DisablePushNotificationsBlockMentions() error {
	if m.pushNotificationClient == nil {
		return errors.New("no push notification client")
	}
	m.mutex.Lock()
	defer m.mutex.Unlock()

	return m.pushNotificationClient.DisablePushNotificationsBlockMentions(m.pushNotificationOptions())
}

// GetPushNotificationsServers returns the servers used for push notifications
func (m *Messenger) GetPushNotificationsServers() ([]*pushnotificationclient.PushNotificationServer, error) {
	if m.pushNotificationClient == nil {
		return nil, errors.New("no push notification client")
	}
	return m.pushNotificationClient.GetServers()
}

// StartPushNotificationsServer initialize and start a push notification server, using the current messenger identity key
func (m *Messenger) StartPushNotificationsServer() error {
	if m.pushNotificationServer == nil {
		pushNotificationServerPersistence := pushnotificationserver.NewSQLitePersistence(m.database)
		config := &pushnotificationserver.Config{
			Enabled:  true,
			Logger:   m.logger,
			Identity: m.identity,
		}
		m.pushNotificationServer = pushnotificationserver.New(config, pushNotificationServerPersistence, m.sender)
	}

	return m.pushNotificationServer.Start()
}

// StopPushNotificationServer stops the push notification server if running
func (m *Messenger) StopPushNotificationsServer() error {
	m.pushNotificationServer = nil
	return nil
}

func generateAliasAndIdenticon(pk string) (string, string, error) {
	identicon, err := identicon.GenerateBase64(pk)
	if err != nil {
		return "", "", err
	}

	name, err := alias.GenerateFromPublicKeyString(pk)
	if err != nil {
		return "", "", err
	}
	return name, identicon, nil

}

func (m *Messenger) SendEmojiReaction(ctx context.Context, chatID, messageID string, emojiID protobuf.EmojiReaction_Type) (*MessengerResponse, error) {
	var response MessengerResponse

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	emojiR := &EmojiReaction{
		EmojiReaction: &protobuf.EmojiReaction{
			Clock:     clock,
			MessageId: messageID,
			ChatId:    chatID,
			Type:      emojiID,
		},
		LocalChatID: chatID,
		From:        types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
	}
	encodedMessage, err := m.encodeChatEntity(chat, emojiR)
	if err != nil {
		return nil, err
	}

	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:          chatID,
		Payload:              encodedMessage,
		SkipGroupMessageWrap: true,
		MessageType:          protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
		// Don't resend using datasync, that would create quite a lot
		// of traffic if clicking too eagelry
		ResendAutomatically: false,
	})
	if err != nil {
		return nil, err
	}

	response.AddEmojiReaction(emojiR)
	response.AddChat(chat)

	err = m.persistence.SaveEmojiReaction(emojiR)
	if err != nil {
		return nil, errors.Wrap(err, "Can't save emoji reaction in db")
	}

	return &response, nil
}

func (m *Messenger) EmojiReactionsByChatID(chatID string, cursor string, limit int) ([]*EmojiReaction, error) {
	chat, err := m.persistence.Chat(chatID)
	if err != nil {
		return nil, err
	}

	if chat.Timeline() {
		var chatIDs = []string{"@" + contactIDFromPublicKey(&m.identity.PublicKey)}
		m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
			if contact.added() {
				chatIDs = append(chatIDs, "@"+contact.ID)
			}
			return true
		})
		return m.persistence.EmojiReactionsByChatIDs(chatIDs, cursor, limit)
	}
	return m.persistence.EmojiReactionsByChatID(chatID, cursor, limit)
}

func (m *Messenger) EmojiReactionsByChatIDMessageID(chatID string, messageID string) ([]*EmojiReaction, error) {
	_, err := m.persistence.Chat(chatID)
	if err != nil {
		return nil, err
	}

	return m.persistence.EmojiReactionsByChatIDMessageID(chatID, messageID)
}

func (m *Messenger) SendEmojiReactionRetraction(ctx context.Context, emojiReactionID string) (*MessengerResponse, error) {
	emojiR, err := m.persistence.EmojiReactionByID(emojiReactionID)
	if err != nil {
		return nil, err
	}

	// Check that the sender is the key owner
	pk := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
	if emojiR.From != pk {
		return nil, errors.Errorf("identity mismatch, "+
			"emoji reactions can only be retracted by the reaction sender, "+
			"emoji reaction sent by '%s', current identity '%s'",
			emojiR.From, pk,
		)
	}

	// Get chat and clock
	chat, ok := m.allChats.Load(emojiR.GetChatId())
	if !ok {
		return nil, ErrChatNotFound
	}
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	// Update the relevant fields
	emojiR.Clock = clock
	emojiR.Retracted = true

	encodedMessage, err := m.encodeChatEntity(chat, emojiR)
	if err != nil {
		return nil, err
	}

	// Send the marshalled EmojiReactionRetraction protobuf
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:          emojiR.GetChatId(),
		Payload:              encodedMessage,
		SkipGroupMessageWrap: true,
		MessageType:          protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
		// Don't resend using datasync, that would create quite a lot
		// of traffic if clicking too eagelry
		ResendAutomatically: false,
	})
	if err != nil {
		return nil, err
	}

	// Update MessengerResponse
	response := MessengerResponse{}
	emojiR.Retracted = true
	response.AddEmojiReaction(emojiR)
	response.AddChat(chat)

	// Persist retraction state for emoji reaction
	err = m.persistence.SaveEmojiReaction(emojiR)
	if err != nil {
		return nil, err
	}

	return &response, nil
}

func (m *Messenger) encodeChatEntity(chat *Chat, message common.ChatEntity) ([]byte, error) {
	var encodedMessage []byte
	var err error
	l := m.logger.With(zap.String("site", "Send"), zap.String("chatID", chat.ID))

	switch chat.ChatType {
	case ChatTypeOneToOne:
		l.Debug("sending private message")
		message.SetMessageType(protobuf.MessageType_ONE_TO_ONE)
		encodedMessage, err = proto.Marshal(message.GetProtobuf())
		if err != nil {
			return nil, err
		}

	case ChatTypePublic, ChatTypeProfile:
		l.Debug("sending public message", zap.String("chatName", chat.Name))
		message.SetMessageType(protobuf.MessageType_PUBLIC_GROUP)
		encodedMessage, err = proto.Marshal(message.GetProtobuf())
		if err != nil {
			return nil, err
		}

	case ChatTypeCommunityChat:
		l.Debug("sending community chat message", zap.String("chatName", chat.Name))
		// TODO: add grant
		message.SetMessageType(protobuf.MessageType_COMMUNITY_CHAT)
		encodedMessage, err = proto.Marshal(message.GetProtobuf())
		if err != nil {
			return nil, err
		}

	case ChatTypePrivateGroupChat:
		message.SetMessageType(protobuf.MessageType_PRIVATE_GROUP)
		l.Debug("sending group message", zap.String("chatName", chat.Name))
		if !message.WrapGroupMessage() {
			encodedMessage, err = proto.Marshal(message.GetProtobuf())
			if err != nil {
				return nil, err
			}
		} else {

			group, err := newProtocolGroupFromChat(chat)
			if err != nil {
				return nil, err
			}

			// NOTE(cammellos): Disabling for now since the optimiziation is not
			// applicable anymore after we changed group rules to allow
			// anyone to change group details
			encodedMessage, err = m.sender.EncodeMembershipUpdate(group, message)
			if err != nil {
				return nil, err
			}
		}

	default:
		return nil, errors.New("chat type not supported")
	}

	return encodedMessage, nil
}

func (m *Messenger) getOrBuildContactFromMessage(msg *common.Message) (*Contact, error) {
	if c, ok := m.allContacts.Load(msg.From); ok {
		return c, nil
	}

	senderPubKey, err := msg.GetSenderPubKey()
	if err != nil {
		return nil, err
	}
	senderID := contactIDFromPublicKey(senderPubKey)
	c, err := buildContact(senderID, senderPubKey)
	if err != nil {
		return nil, err
	}

	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allContacts.Store(msg.From, c)
	return c, nil
}

func (m *Messenger) BloomFilter() []byte {
	return m.transport.BloomFilter()
}

func (m *Messenger) getSettings() (settings.Settings, error) {
	sDB, err := accounts.NewDB(m.database)
	if err != nil {
		return settings.Settings{}, err
	}
	return sDB.GetSettings()
}

func (m *Messenger) getEnsUsernameDetails() (result []*ensservice.UsernameDetail, err error) {
	db := ensservice.NewEnsDatabase(m.database)
	return db.GetEnsUsernames(nil)
}

func ToVerificationRequest(message *protobuf.SyncVerificationRequest) *verification.Request {
	return &verification.Request{
		From:          message.From,
		To:            message.To,
		Challenge:     message.Challenge,
		Response:      message.Response,
		RequestedAt:   message.RequestedAt,
		RepliedAt:     message.RepliedAt,
		RequestStatus: verification.RequestStatus(message.VerificationStatus),
	}
}

func (m *Messenger) HandleSyncVerificationRequest(state *ReceivedMessageState, message *protobuf.SyncVerificationRequest, statusMessage *v1protocol.StatusMessage) error {
	verificationRequest := ToVerificationRequest(message)

	err := m.verificationDatabase.SaveVerificationRequest(verificationRequest)
	if err != nil {
		return err
	}

	myPubKey := hexutil.Encode(crypto.FromECDSAPub(&m.identity.PublicKey))

	state.AllVerificationRequests = append(state.AllVerificationRequests, verificationRequest)

	if message.From == myPubKey { // Verification requests we sent
		contact, ok := m.allContacts.Load(message.To)
		if !ok {
			m.logger.Info("contact not found")
			return nil
		}

		contact.VerificationStatus = VerificationStatus(message.VerificationStatus)
		if err := m.persistence.SaveContact(contact, nil); err != nil {
			return err
		}

		m.allContacts.Store(contact.ID, contact)
		state.ModifiedContacts.Store(contact.ID, true)

		// TODO: create activity center notif

	}
	// else { // Verification requests we received
	// // TODO: activity center notif
	//}

	return nil
}

func (m *Messenger) ImageServerURL() string {
	return m.httpServer.MakeImageServerURL()
}

func (m *Messenger) myHexIdentity() string {
	return common.PubkeyToHex(&m.identity.PublicKey)
}

func (m *Messenger) GetMentionsManager() *MentionManager {
	return m.mentionsManager
}

func (m *Messenger) getConnectedMessages(message *common.Message, chatID string) ([]*common.Message, error) {
	var connectedMessages []*common.Message
	// In case of Image messages, we need to delete all the images in the album
	if message.ContentType == protobuf.ChatMessage_IMAGE {
		image := message.GetImage()
		if image != nil && image.AlbumId != "" {
			messagesInTheAlbum, err := m.persistence.albumMessages(chatID, image.GetAlbumId())
			if err != nil {
				return nil, err
			}
			connectedMessages = append(connectedMessages, messagesInTheAlbum...)
			return connectedMessages, nil
		}
	}
	return append(connectedMessages, message), nil
}

func (m *Messenger) withChatClock(callback func(string, uint64) error) error {
	clock, chat := m.getLastClockWithRelatedChat()
	err := callback(chat.ID, clock)
	if err != nil {
		return err
	}
	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) syncDeleteForMeMessage(ctx context.Context, rawMessageDispatcher RawMessageHandler) error {
	deleteForMes, err := m.persistence.GetDeleteForMeMessages()
	if err != nil {
		return err
	}

	return m.withChatClock(func(chatID string, _ uint64) error {
		for _, deleteForMe := range deleteForMes {
			encodedMessage, err2 := proto.Marshal(deleteForMe)
			if err2 != nil {
				return err2
			}
			rawMessage := common.RawMessage{
				LocalChatID:         chatID,
				Payload:             encodedMessage,
				MessageType:         protobuf.ApplicationMetadataMessage_SYNC_DELETE_FOR_ME_MESSAGE,
				ResendAutomatically: true,
			}
			_, err2 = rawMessageDispatcher(ctx, rawMessage)
			if err2 != nil {
				return err2
			}
		}
		return nil
	})
}

func (m *Messenger) syncSocialLinks(ctx context.Context, rawMessageDispatcher RawMessageHandler) error {
	if !m.hasPairedDevices() {
		return nil
	}

	dbSocialLinks, err := m.settings.GetSocialLinks()
	if err != nil {
		return err
	}

	dbClock, err := m.settings.GetSocialLinksClock()
	if err != nil {
		return err
	}

	_, chat := m.getLastClockWithRelatedChat()
	encodedMessage, err := proto.Marshal(dbSocialLinks.ToSyncProtobuf(dbClock))
	if err != nil {
		return err
	}

	rawMessage := common.RawMessage{
		LocalChatID:         chat.ID,
		Payload:             encodedMessage,
		MessageType:         protobuf.ApplicationMetadataMessage_SYNC_SOCIAL_LINKS,
		ResendAutomatically: true,
	}

	_, err = rawMessageDispatcher(ctx, rawMessage)
	return err
}

func (m *Messenger) HandleSyncSocialLinks(state *ReceivedMessageState, message *protobuf.SyncSocialLinks, statusMessage *v1protocol.StatusMessage) error {
	return m.handleSyncSocialLinks(message, func(links identity.SocialLinks) {
		state.Response.SocialLinksInfo = &identity.SocialLinksInfo{
			Links:   links,
			Removed: len(links) == 0,
		}
	})
}

func (m *Messenger) handleSyncSocialLinks(message *protobuf.SyncSocialLinks, callback func(identity.SocialLinks)) error {
	if message == nil {
		return nil
	}
	var (
		links identity.SocialLinks
		err   error
	)
	for _, sl := range message.SocialLinks {
		link := &identity.SocialLink{
			Text: sl.Text,
			URL:  sl.Url,
		}
		err = ValidateSocialLink(link)
		if err != nil {
			return err
		}

		links = append(links, link)
	}

	err = m.settings.AddOrReplaceSocialLinksIfNewer(links, message.Clock)
	if err != nil {
		if err == sociallinkssettings.ErrOlderSocialLinksProvided {
			return nil
		}
		return err
	}

	callback(links)

	return nil
}

func (m *Messenger) GetDeleteForMeMessages() ([]*protobuf.SyncDeleteForMeMessage, error) {
	return m.persistence.GetDeleteForMeMessages()
}