diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/messages/__tests__/convo.test.ts | 20 | ||||
-rw-r--r-- | src/state/messages/convo.ts | 811 | ||||
-rw-r--r-- | src/state/messages/index.tsx | 36 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 1 | ||||
-rw-r--r-- | src/state/queries/actor-autocomplete.ts | 6 | ||||
-rw-r--r-- | src/state/queries/messages/list-converations.ts | 3 | ||||
-rw-r--r-- | src/state/session/index.tsx | 438 | ||||
-rw-r--r-- | src/state/session/types.ts | 7 |
8 files changed, 822 insertions, 500 deletions
diff --git a/src/state/messages/__tests__/convo.test.ts b/src/state/messages/__tests__/convo.test.ts index 03f9218ff..44fe16fef 100644 --- a/src/state/messages/__tests__/convo.test.ts +++ b/src/state/messages/__tests__/convo.test.ts @@ -1,15 +1,23 @@ import {describe, it} from '@jest/globals' describe(`#/state/messages/convo`, () => { - describe(`status states`, () => { + describe(`init`, () => { + it.todo(`fails if sender and recipients aren't found`) it.todo(`cannot re-initialize from a non-unintialized state`) it.todo(`can re-initialize from a failed state`) + }) + + describe(`resume`, () => { + it.todo(`restores previous state if resume fails`) + }) + + describe(`suspend`, () => { + it.todo(`cannot be interacted with when suspended`) + it.todo(`polling is stopped when suspended`) + }) - describe(`destroy`, () => { - it.todo(`cannot be interacted with when destroyed`) - it.todo(`polling is stopped when destroyed`) - it.todo(`events are cleaned up when destroyed`) - }) + describe(`read states`, () => { + it.todo(`should mark messages as read as they come in`) }) describe(`history fetching`, () => { diff --git a/src/state/messages/convo.ts b/src/state/messages/convo.ts index 4bc9913f8..81ab94f43 100644 --- a/src/state/messages/convo.ts +++ b/src/state/messages/convo.ts @@ -4,9 +4,9 @@ import { ChatBskyConvoDefs, ChatBskyConvoSendMessage, } from '@atproto-labs/api' -import {EventEmitter} from 'eventemitter3' import {nanoid} from 'nanoid/non-secure' +import {logger} from '#/logger' import {isNative} from '#/platform/detection' export type ConvoParams = { @@ -18,9 +18,21 @@ export type ConvoParams = { export enum ConvoStatus { Uninitialized = 'uninitialized', Initializing = 'initializing', + Resuming = 'resuming', Ready = 'ready', Error = 'error', - Destroyed = 'destroyed', + Backgrounded = 'backgrounded', + Suspended = 'suspended', +} + +export enum ConvoItemError { + HistoryFailed = 'historyFailed', + ResumeFailed = 'resumeFailed', + PollFailed = 'pollFailed', +} + +export enum ConvoError { + InitFailed = 'initFailed', } export type ConvoItem = @@ -47,28 +59,110 @@ export type ConvoItem = key: string retry: () => void } + | { + type: 'error-recoverable' + key: string + code: ConvoItemError + retry: () => void + } export type ConvoState = | { status: ConvoStatus.Uninitialized + items: [] + convo: undefined + error: undefined + sender: undefined + recipients: undefined + isFetchingHistory: false + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined } | { status: ConvoStatus.Initializing + items: [] + convo: undefined + error: undefined + sender: undefined + recipients: undefined + isFetchingHistory: boolean + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined } | { status: ConvoStatus.Ready items: ConvoItem[] convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => void + fetchMessageHistory: () => void } | { - status: ConvoStatus.Error - error: any + status: ConvoStatus.Suspended + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => Promise<void> + fetchMessageHistory: () => Promise<void> + } + | { + status: ConvoStatus.Backgrounded + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => Promise<void> + fetchMessageHistory: () => Promise<void> + } + | { + status: ConvoStatus.Resuming + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => Promise<void> + fetchMessageHistory: () => Promise<void> } | { - status: ConvoStatus.Destroyed + status: ConvoStatus.Error + items: [] + convo: undefined + error: any + sender: undefined + recipients: undefined + isFetchingHistory: false + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined } +const ACTIVE_POLL_INTERVAL = 2e3 +const BACKGROUND_POLL_INTERVAL = 10e3 + export function isConvoItemMessage( item: ConvoItem, ): item is ConvoItem & {type: 'message'} { @@ -84,15 +178,19 @@ export class Convo { private agent: BskyAgent private __tempFromUserDid: string + private pollInterval = ACTIVE_POLL_INTERVAL private status: ConvoStatus = ConvoStatus.Uninitialized - private error: any + private error: + | { + code: ConvoError + exception?: Error + retry: () => void + } + | undefined private historyCursor: string | undefined | null = undefined private isFetchingHistory = false private eventsCursor: string | undefined = undefined - - convoId: string - convo: ChatBskyConvoDefs.ConvoView | undefined - sender: AppBskyActorDefs.ProfileViewBasic | undefined + private pollingFailure = false private pastMessages: Map< string, @@ -106,87 +204,214 @@ export class Convo { string, {id: string; message: ChatBskyConvoSendMessage.InputSchema['message']} > = new Map() + private deletedMessages: Set<string> = new Set() private footerItems: Map<string, ConvoItem> = new Map() + private headerItems: Map<string, ConvoItem> = new Map() private pendingEventIngestion: Promise<void> | undefined private isProcessingPendingMessages = false + convoId: string + convo: ChatBskyConvoDefs.ConvoView | undefined + sender: AppBskyActorDefs.ProfileViewBasic | undefined + recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined + snapshot: ConvoState | undefined + constructor(params: ConvoParams) { this.convoId = params.convoId this.agent = params.agent this.__tempFromUserDid = params.__tempFromUserDid + + this.subscribe = this.subscribe.bind(this) + this.getSnapshot = this.getSnapshot.bind(this) + this.sendMessage = this.sendMessage.bind(this) + this.deleteMessage = this.deleteMessage.bind(this) + this.fetchMessageHistory = this.fetchMessageHistory.bind(this) } - async initialize() { - if (this.status !== 'uninitialized') return - this.status = ConvoStatus.Initializing + private commit() { + this.snapshot = undefined + this.subscribers.forEach(subscriber => subscriber()) + } - try { - const response = await this.agent.api.chat.bsky.convo.getConvo( - { - convoId: this.convoId, - }, - { - headers: { - Authorization: this.__tempFromUserDid, - }, - }, - ) - const {convo} = response.data + private subscribers: (() => void)[] = [] - this.convo = convo - this.sender = this.convo.members.find( - m => m.did === this.__tempFromUserDid, - ) - this.status = ConvoStatus.Ready + subscribe(subscriber: () => void) { + if (this.subscribers.length === 0) this.init() - this.commit() + this.subscribers.push(subscriber) - await this.fetchMessageHistory() + return () => { + this.subscribers = this.subscribers.filter(s => s !== subscriber) + if (this.subscribers.length === 0) this.suspend() + } + } - this.pollEvents() - } catch (e) { - this.status = ConvoStatus.Error - this.error = e + getSnapshot(): ConvoState { + if (!this.snapshot) this.snapshot = this.generateSnapshot() + // logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo) + return this.snapshot + } + + private generateSnapshot(): ConvoState { + switch (this.status) { + case ConvoStatus.Initializing: { + return { + status: ConvoStatus.Initializing, + items: [], + convo: undefined, + error: undefined, + sender: undefined, + recipients: undefined, + isFetchingHistory: this.isFetchingHistory, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + } + } + case ConvoStatus.Suspended: + case ConvoStatus.Backgrounded: + case ConvoStatus.Resuming: + case ConvoStatus.Ready: { + return { + status: this.status, + items: this.getItems(), + convo: this.convo!, + error: undefined, + sender: this.sender!, + recipients: this.recipients!, + isFetchingHistory: this.isFetchingHistory, + deleteMessage: this.deleteMessage, + sendMessage: this.sendMessage, + fetchMessageHistory: this.fetchMessageHistory, + } + } + case ConvoStatus.Error: { + return { + status: ConvoStatus.Error, + items: [], + convo: undefined, + error: this.error, + sender: undefined, + recipients: undefined, + isFetchingHistory: false, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + } + } + default: { + return { + status: ConvoStatus.Uninitialized, + items: [], + convo: undefined, + error: undefined, + sender: undefined, + recipients: undefined, + isFetchingHistory: false, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + } + } } } - private async pollEvents() { - if (this.status === ConvoStatus.Destroyed) return - if (this.pendingEventIngestion) return - setTimeout(async () => { - this.pendingEventIngestion = this.ingestLatestEvents() - await this.pendingEventIngestion - this.pendingEventIngestion = undefined - this.pollEvents() - }, 5e3) + async init() { + logger.debug('Convo: init', {}, logger.DebugContext.convo) + + if ( + this.status === ConvoStatus.Uninitialized || + this.status === ConvoStatus.Error + ) { + try { + this.status = ConvoStatus.Initializing + this.commit() + + await this.refreshConvo() + this.status = ConvoStatus.Ready + this.commit() + + await this.fetchMessageHistory() + + this.pollEvents() + } catch (e: any) { + logger.error('Convo: failed to init') + this.error = { + exception: e, + code: ConvoError.InitFailed, + retry: () => { + this.error = undefined + this.init() + }, + } + this.status = ConvoStatus.Error + this.commit() + } + } else { + logger.warn(`Convo: cannot init from ${this.status}`) + } } - async fetchMessageHistory() { - if (this.status === ConvoStatus.Destroyed) return - // reached end - if (this.historyCursor === null) return - if (this.isFetchingHistory) return + async resume() { + logger.debug('Convo: resume', {}, logger.DebugContext.convo) + + if ( + this.status === ConvoStatus.Suspended || + this.status === ConvoStatus.Backgrounded + ) { + const fromStatus = this.status + + try { + this.status = ConvoStatus.Resuming + this.commit() + + await this.refreshConvo() + this.status = ConvoStatus.Ready + this.commit() + + // throw new Error('UNCOMMENT TO TEST RESUME FAILURE') + + this.pollInterval = ACTIVE_POLL_INTERVAL + this.pollEvents() + } catch (e) { + logger.error('Convo: failed to resume') + + this.footerItems.set(ConvoItemError.ResumeFailed, { + type: 'error-recoverable', + key: ConvoItemError.ResumeFailed, + code: ConvoItemError.ResumeFailed, + retry: () => { + this.footerItems.delete(ConvoItemError.ResumeFailed) + this.resume() + }, + }) - this.isFetchingHistory = true + this.status = fromStatus + this.commit() + } + } else { + logger.warn(`Convo: cannot resume from ${this.status}`) + } + } + + async background() { + logger.debug('Convo: backgrounded', {}, logger.DebugContext.convo) + this.status = ConvoStatus.Backgrounded + this.pollInterval = BACKGROUND_POLL_INTERVAL this.commit() + } - /* - * Delay if paginating while scrolled. - * - * TODO why does the FlatList jump without this delay? - * - * Tbh it feels a little more natural with a slight delay. - */ - if (this.pastMessages.size > 0) { - await new Promise(y => setTimeout(y, 500)) - } + async suspend() { + logger.debug('Convo: suspended', {}, logger.DebugContext.convo) + this.status = ConvoStatus.Suspended + this.commit() + } - const response = await this.agent.api.chat.bsky.convo.getMessages( + async refreshConvo() { + const response = await this.agent.api.chat.bsky.convo.getConvo( { - cursor: this.historyCursor, convoId: this.convoId, - limit: isNative ? 25 : 50, }, { headers: { @@ -194,100 +419,244 @@ export class Convo { }, }, ) - const {cursor, messages} = response.data + this.convo = response.data.convo + this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid) + this.recipients = this.convo.members.filter( + m => m.did !== this.__tempFromUserDid, + ) + + /* + * Prevent invalid states + */ + if (!this.sender) { + throw new Error('Convo: could not find sender in convo') + } + if (!this.recipients) { + throw new Error('Convo: could not find recipients in convo') + } + } + + async fetchMessageHistory() { + logger.debug('Convo: fetch message history', {}, logger.DebugContext.convo) + + /* + * If historyCursor is null, we've fetched all history. + */ + if (this.historyCursor === null) return - this.historyCursor = cursor || null + /* + * Don't fetch again if a fetch is already in progress + */ + if (this.isFetchingHistory) return - for (const message of messages) { - if ( - ChatBskyConvoDefs.isMessageView(message) || - ChatBskyConvoDefs.isDeletedMessageView(message) - ) { - this.pastMessages.set(message.id, message) + /* + * If we've rendered a retry state for history fetching, exit. Upon retry, + * this will be removed and we'll try again. + */ + if (this.headerItems.has(ConvoItemError.HistoryFailed)) return - // set to latest rev + try { + this.isFetchingHistory = true + this.commit() + + /* + * Delay if paginating while scrolled to prevent momentum scrolling from + * jerking the list around, plus makes it feel a little more human. + */ + if (this.pastMessages.size > 0) { + await new Promise(y => setTimeout(y, 500)) + // throw new Error('UNCOMMENT TO TEST RETRY') + } + + const response = await this.agent.api.chat.bsky.convo.getMessages( + { + cursor: this.historyCursor, + convoId: this.convoId, + limit: isNative ? 25 : 50, + }, + { + headers: { + Authorization: this.__tempFromUserDid, + }, + }, + ) + const {cursor, messages} = response.data + + this.historyCursor = cursor ?? null + + for (const message of messages) { if ( - message.rev > (this.eventsCursor = this.eventsCursor || message.rev) + ChatBskyConvoDefs.isMessageView(message) || + ChatBskyConvoDefs.isDeletedMessageView(message) ) { - this.eventsCursor = message.rev + this.pastMessages.set(message.id, message) + + // set to latest rev + if ( + message.rev > (this.eventsCursor = this.eventsCursor || message.rev) + ) { + this.eventsCursor = message.rev + } } } + } catch (e: any) { + logger.error('Convo: failed to fetch message history') + + this.headerItems.set(ConvoItemError.HistoryFailed, { + type: 'error-recoverable', + key: ConvoItemError.HistoryFailed, + code: ConvoItemError.HistoryFailed, + retry: () => { + this.headerItems.delete(ConvoItemError.HistoryFailed) + this.fetchMessageHistory() + }, + }) + } finally { + this.isFetchingHistory = false + this.commit() } + } - this.isFetchingHistory = false - this.commit() + private async pollEvents() { + if ( + this.status === ConvoStatus.Ready || + this.status === ConvoStatus.Backgrounded + ) { + if (this.pendingEventIngestion) return + + /* + * Represents a failed state, which is retryable. + */ + if (this.pollingFailure) return + + setTimeout(async () => { + this.pendingEventIngestion = this.ingestLatestEvents() + await this.pendingEventIngestion + this.pendingEventIngestion = undefined + this.pollEvents() + }, this.pollInterval) + } } async ingestLatestEvents() { - if (this.status === ConvoStatus.Destroyed) return - - const response = await this.agent.api.chat.bsky.convo.getLog( - { - cursor: this.eventsCursor, - }, - { - headers: { - Authorization: this.__tempFromUserDid, + try { + // throw new Error('UNCOMMENT TO TEST POLL FAILURE') + const response = await this.agent.api.chat.bsky.convo.getLog( + { + cursor: this.eventsCursor, }, - }, - ) - const {logs} = response.data + { + headers: { + Authorization: this.__tempFromUserDid, + }, + }, + ) + const {logs} = response.data - for (const log of logs) { - /* - * If there's a rev, we should handle it. If there's not a rev, we don't - * know what it is. - */ - if (typeof log.rev === 'string') { + let needsCommit = false + + for (const log of logs) { /* - * We only care about new events + * If there's a rev, we should handle it. If there's not a rev, we don't + * know what it is. */ - if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) { + if (typeof log.rev === 'string') { /* - * Update rev regardless of if it's a log type we care about or not + * We only care about new events */ - this.eventsCursor = log.rev - - /* - * This is VERY important. We don't want to insert any messages from - * your other chats. - */ - if (log.convoId !== this.convoId) continue + if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) { + /* + * Update rev regardless of if it's a log type we care about or not + */ + this.eventsCursor = log.rev - if ( - ChatBskyConvoDefs.isLogCreateMessage(log) && - ChatBskyConvoDefs.isMessageView(log.message) - ) { - if (this.newMessages.has(log.message.id)) { - // Trust the log as the source of truth on ordering - this.newMessages.delete(log.message.id) - } - this.newMessages.set(log.message.id, log.message) - } else if ( - ChatBskyConvoDefs.isLogDeleteMessage(log) && - ChatBskyConvoDefs.isDeletedMessageView(log.message) - ) { /* - * Update if we have this in state. If we don't, don't worry about it. + * This is VERY important. We don't want to insert any messages from + * your other chats. */ - if (this.pastMessages.has(log.message.id)) { + if (log.convoId !== this.convoId) continue + + if ( + ChatBskyConvoDefs.isLogCreateMessage(log) && + ChatBskyConvoDefs.isMessageView(log.message) + ) { + if (this.newMessages.has(log.message.id)) { + // Trust the log as the source of truth on ordering + this.newMessages.delete(log.message.id) + } + this.newMessages.set(log.message.id, log.message) + needsCommit = true + } else if ( + ChatBskyConvoDefs.isLogDeleteMessage(log) && + ChatBskyConvoDefs.isDeletedMessageView(log.message) + ) { /* - * For now, we remove deleted messages from the thread, if we receive one. - * - * To support them, it'd look something like this: - * this.pastMessages.set(log.message.id, log.message) + * Update if we have this in state. If we don't, don't worry about it. */ - this.pastMessages.delete(log.message.id) + if (this.pastMessages.has(log.message.id)) { + /* + * For now, we remove deleted messages from the thread, if we receive one. + * + * To support them, it'd look something like this: + * this.pastMessages.set(log.message.id, log.message) + */ + this.pastMessages.delete(log.message.id) + this.newMessages.delete(log.message.id) + this.deletedMessages.delete(log.message.id) + needsCommit = true + } } } } } + + if (needsCommit) { + this.commit() + } + } catch (e: any) { + logger.error('Convo: failed to poll events') + this.pollingFailure = true + this.footerItems.set(ConvoItemError.PollFailed, { + type: 'error-recoverable', + key: ConvoItemError.PollFailed, + code: ConvoItemError.PollFailed, + retry: () => { + this.footerItems.delete(ConvoItemError.PollFailed) + this.pollingFailure = false + this.commit() + this.pollEvents() + }, + }) + this.commit() } + } + + async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { + // Ignore empty messages for now since they have no other purpose atm + if (!message.text.trim()) return + + logger.debug('Convo: send message', {}, logger.DebugContext.convo) + const tempId = nanoid() + + this.pendingMessages.set(tempId, { + id: tempId, + message, + }) this.commit() + + if (!this.isProcessingPendingMessages) { + this.processPendingMessages() + } } async processPendingMessages() { + logger.debug( + `Convo: processing messages (${this.pendingMessages.size} remaining)`, + {}, + logger.DebugContext.convo, + ) + const pendingMessage = Array.from(this.pendingMessages.values()).shift() /* @@ -343,6 +712,12 @@ export class Convo { } async batchRetryPendingMessages() { + logger.debug( + `Convo: retrying ${this.pendingMessages.size} pending messages`, + {}, + logger.DebugContext.convo, + ) + this.footerItems.delete('pending-retry') this.commit() @@ -393,32 +768,43 @@ export class Convo { } } - async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { - if (this.status === ConvoStatus.Destroyed) return - // Ignore empty messages for now since they have no other purpose atm - if (!message.text.trim()) return - - const tempId = nanoid() + async deleteMessage(messageId: string) { + logger.debug('Convo: delete message', {}, logger.DebugContext.convo) - this.pendingMessages.set(tempId, { - id: tempId, - message, - }) + this.deletedMessages.add(messageId) this.commit() - if (!this.isProcessingPendingMessages) { - this.processPendingMessages() + try { + await this.agent.api.chat.bsky.convo.deleteMessageForSelf( + { + convoId: this.convoId, + messageId, + }, + { + encoding: 'application/json', + headers: { + Authorization: this.__tempFromUserDid, + }, + }, + ) + } catch (e) { + this.deletedMessages.delete(messageId) + this.commit() + throw e } } /* * Items in reverse order, since FlatList inverts */ - get items(): ConvoItem[] { + getItems(): ConvoItem[] { const items: ConvoItem[] = [] - // `newMessages` is in insertion order, unshift to reverse - this.newMessages.forEach(m => { + this.headerItems.forEach(item => { + items.push(item) + }) + + this.pastMessages.forEach(m => { if (ChatBskyConvoDefs.isMessageView(m)) { items.unshift({ type: 'message', @@ -436,27 +822,7 @@ export class Convo { } }) - // `newMessages` is in insertion order, unshift to reverse - this.pendingMessages.forEach(m => { - items.unshift({ - type: 'pending-message', - key: m.id, - message: { - ...m.message, - id: nanoid(), - rev: '__fake__', - sentAt: new Date().toISOString(), - sender: this.sender, - }, - nextMessage: null, - }) - }) - - this.footerItems.forEach(item => { - items.unshift(item) - }) - - this.pastMessages.forEach(m => { + this.newMessages.forEach(m => { if (ChatBskyConvoDefs.isMessageView(m)) { items.push({ type: 'message', @@ -474,88 +840,61 @@ export class Convo { } }) - return items.map((item, i) => { - let nextMessage = null - const isMessage = isConvoItemMessage(item) + this.pendingMessages.forEach(m => { + items.push({ + type: 'pending-message', + key: m.id, + message: { + ...m.message, + id: nanoid(), + rev: '__fake__', + sentAt: new Date().toISOString(), + sender: this.sender, + }, + nextMessage: null, + }) + }) + + this.footerItems.forEach(item => { + items.push(item) + }) - if (isMessage) { - if ( - isMessage && - (ChatBskyConvoDefs.isMessageView(item.message) || - ChatBskyConvoDefs.isDeletedMessageView(item.message)) - ) { - const next = items[i - 1] + return items + .filter(item => { + if (isConvoItemMessage(item)) { + return !this.deletedMessages.has(item.message.id) + } + return true + }) + .map((item, i, arr) => { + let nextMessage = null + const isMessage = isConvoItemMessage(item) + if (isMessage) { if ( - isConvoItemMessage(next) && - next && - (ChatBskyConvoDefs.isMessageView(next.message) || - ChatBskyConvoDefs.isDeletedMessageView(next.message)) + isMessage && + (ChatBskyConvoDefs.isMessageView(item.message) || + ChatBskyConvoDefs.isDeletedMessageView(item.message)) ) { - nextMessage = next.message + const next = arr[i + 1] + + if ( + isConvoItemMessage(next) && + next && + (ChatBskyConvoDefs.isMessageView(next.message) || + ChatBskyConvoDefs.isDeletedMessageView(next.message)) + ) { + nextMessage = next.message + } } - } - - return { - ...item, - nextMessage, - } - } - return item - }) - } - - destroy() { - this.status = ConvoStatus.Destroyed - this.commit() - } - - get state(): ConvoState { - switch (this.status) { - case ConvoStatus.Initializing: { - return { - status: ConvoStatus.Initializing, - } - } - case ConvoStatus.Ready: { - return { - status: ConvoStatus.Ready, - items: this.items, - convo: this.convo!, - isFetchingHistory: this.isFetchingHistory, - } - } - case ConvoStatus.Error: { - return { - status: ConvoStatus.Error, - error: this.error, - } - } - case ConvoStatus.Destroyed: { - return { - status: ConvoStatus.Destroyed, - } - } - default: { - return { - status: ConvoStatus.Uninitialized, + return { + ...item, + nextMessage, + } } - } - } - } - - private _emitter = new EventEmitter() - private commit() { - this._emitter.emit('update') - } - - on(event: 'update', cb: () => void) { - this._emitter.on(event, cb) - } - - off(event: 'update', cb: () => void) { - this._emitter.off(event, cb) + return item + }) } } diff --git a/src/state/messages/index.tsx b/src/state/messages/index.tsx index cdc5a4db2..22c4242e2 100644 --- a/src/state/messages/index.tsx +++ b/src/state/messages/index.tsx @@ -1,14 +1,12 @@ -import React, {useContext, useEffect, useMemo, useState} from 'react' +import React, {useContext, useState, useSyncExternalStore} from 'react' import {BskyAgent} from '@atproto-labs/api' +import {useFocusEffect} from '@react-navigation/native' -import {Convo, ConvoParams} from '#/state/messages/convo' +import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo' import {useAgent} from '#/state/session' import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' -const ChatContext = React.createContext<{ - service: Convo - state: Convo['state'] -} | null>(null) +const ChatContext = React.createContext<ConvoState | null>(null) export function useChat() { const ctx = useContext(ChatContext) @@ -24,7 +22,7 @@ export function ChatProvider({ }: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) { const {serviceUrl} = useDmServiceUrlStorage() const {getAgent} = useAgent() - const [service] = useState( + const [convo] = useState( () => new Convo({ convoId, @@ -34,21 +32,17 @@ export function ChatProvider({ __tempFromUserDid: getAgent().session?.did!, }), ) - const [state, setState] = useState(service.state) + const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) - useEffect(() => { - service.initialize() - }, [service]) + useFocusEffect( + React.useCallback(() => { + convo.resume() - useEffect(() => { - const update = () => setState(service.state) - service.on('update', update) - return () => { - service.destroy() - } - }, [service]) - - const value = useMemo(() => ({service, state}), [service, state]) + return () => { + convo.background() + } + }, [convo]), + ) - return <ChatContext.Provider value={value}>{children}</ChatContext.Provider> + return <ChatContext.Provider value={service}>{children}</ChatContext.Provider> } diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 0f61a9711..cf82bcd07 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -47,6 +47,7 @@ export interface EditImageModal { export interface CropImageModal { name: 'crop-image' uri: string + dimensions?: {width: number; height: number} onSelect: (img?: RNImage) => void } diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index 430f6978a..8708a244b 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -24,7 +24,11 @@ export function useActorAutocompleteQuery( const moderationOpts = useModerationOpts() const {getAgent} = useAgent() - prefix = prefix.toLowerCase() + prefix = prefix.toLowerCase().trim() + if (prefix.endsWith('.')) { + // Going from "foo" to "foo." should not clear matches. + prefix = prefix.slice(0, -1) + } return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({ staleTime: STALE.MINUTES.ONE, diff --git a/src/state/queries/messages/list-converations.ts b/src/state/queries/messages/list-converations.ts index 19f2674bd..1e4ecb6d7 100644 --- a/src/state/queries/messages/list-converations.ts +++ b/src/state/queries/messages/list-converations.ts @@ -7,7 +7,7 @@ import {useHeaders} from './temp-headers' export const RQKEY = ['convo-list'] type RQPageParam = string | undefined -export function useListConvos() { +export function useListConvos({refetchInterval}: {refetchInterval: number}) { const headers = useHeaders() const {serviceUrl} = useDmServiceUrlStorage() @@ -24,5 +24,6 @@ export function useListConvos() { }, initialPageParam: undefined as RQPageParam, getNextPageParam: lastPage => lastPage.cursor, + refetchInterval, }) } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 582680e97..276e3b97b 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api' +import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {networkRetry} from '#/lib/async/retry' @@ -35,8 +35,6 @@ const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) configureModerationForGuest() const StateContext = React.createContext<SessionStateContext>({ - isInitialLoad: true, - isSwitchingAccounts: false, accounts: [], currentAccount: undefined, hasSession: false, @@ -47,9 +45,7 @@ const ApiContext = React.createContext<SessionApiContext>({ login: async () => {}, logout: async () => {}, initSession: async () => {}, - resumeSession: async () => {}, removeAccount: () => {}, - selectAccount: async () => {}, updateCurrentAccount: () => {}, clearCurrentAccount: () => {}, }) @@ -60,33 +56,26 @@ function __getAgent() { return __globalAgent } +type AgentState = { + readonly agent: BskyAgent + readonly did: string | undefined +} + type State = { accounts: SessionStateContext['accounts'] - currentAccountDid: string | undefined + currentAgentState: AgentState needsPersist: boolean } export function Provider({children}: React.PropsWithChildren<{}>) { - const [isInitialLoad, setIsInitialLoad] = React.useState(true) - const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false) - const [state, setState] = React.useState<State>({ + const [state, setState] = React.useState<State>(() => ({ accounts: persisted.get('session').accounts, - currentAccountDid: undefined, // assume logged out to start - needsPersist: false, - }) - - const upsertAccount = React.useCallback( - (account: SessionAccount, expired = false) => { - setState(s => { - return { - accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], - currentAccountDid: expired ? undefined : account.did, - needsPersist: true, - } - }) + currentAgentState: { + agent: PUBLIC_BSKY_AGENT, + did: undefined, // assume logged out to start }, - [setState], - ) + needsPersist: false, + })) const clearCurrentAccount = React.useCallback(() => { logger.warn(`session: clear current account`) @@ -94,80 +83,112 @@ export function Provider({children}: React.PropsWithChildren<{}>) { configureModerationForGuest() setState(s => ({ accounts: s.accounts, - currentAccountDid: undefined, + currentAgentState: { + agent: PUBLIC_BSKY_AGENT, + did: undefined, + }, needsPersist: true, })) }, [setState]) - const createPersistSessionHandler = React.useCallback( + const onAgentSessionChange = React.useCallback( ( agent: BskyAgent, account: SessionAccount, - persistSessionCallback: (props: { - expired: boolean - refreshedAccount: SessionAccount - }) => void, - { - networkErrorCallback, - }: { - networkErrorCallback?: () => void - } = {}, - ): AtpPersistSessionHandler => { - return function persistSession(event, session) { - const expired = event === 'expired' || event === 'create-failed' - - if (event === 'network-error') { - logger.warn( - `session: persistSessionHandler received network-error event`, - ) - networkErrorCallback?.() - return - } - - // TODO: use agentToSessionAccount for this too. - const refreshedAccount: SessionAccount = { - service: account.service, - did: session?.did || account.did, - handle: session?.handle || account.handle, - email: session?.email || account.email, - emailConfirmed: session?.emailConfirmed || account.emailConfirmed, - emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, - deactivated: isSessionDeactivated(session?.accessJwt), - pdsUrl: agent.pdsUrl?.toString(), - - /* - * Tokens are undefined if the session expires, or if creation fails for - * any reason e.g. tokens are invalid, network error, etc. - */ - refreshJwt: session?.refreshJwt, - accessJwt: session?.accessJwt, - } - - logger.debug(`session: persistSession`, { - event, - deactivated: refreshedAccount.deactivated, - }) + event: AtpSessionEvent, + session: AtpSessionData | undefined, + ) => { + const expired = event === 'expired' || event === 'create-failed' + + if (event === 'network-error') { + logger.warn( + `session: persistSessionHandler received network-error event`, + ) + logger.warn(`session: clear current account`) + __globalAgent = PUBLIC_BSKY_AGENT + configureModerationForGuest() + setState(s => ({ + accounts: s.accounts, + currentAgentState: { + agent: PUBLIC_BSKY_AGENT, + did: undefined, + }, + needsPersist: true, + })) + return + } - if (expired) { - logger.warn(`session: expired`) - emitSessionDropped() - } + // TODO: use agentToSessionAccount for this too. + const refreshedAccount: SessionAccount = { + service: account.service, + did: session?.did ?? account.did, + handle: session?.handle ?? account.handle, + email: session?.email ?? account.email, + emailConfirmed: session?.emailConfirmed ?? account.emailConfirmed, + emailAuthFactor: session?.emailAuthFactor ?? account.emailAuthFactor, + deactivated: isSessionDeactivated(session?.accessJwt), + pdsUrl: agent.pdsUrl?.toString(), /* - * If the session expired, or it was successfully created/updated, we want - * to update/persist the data. - * - * If the session creation failed, it could be a network error, or it could - * be more serious like an invalid token(s). We can't differentiate, so in - * order to allow the user to get a fresh token (if they need it), we need - * to persist this data and wipe their tokens, effectively logging them - * out. + * Tokens are undefined if the session expires, or if creation fails for + * any reason e.g. tokens are invalid, network error, etc. */ - persistSessionCallback({ - expired, - refreshedAccount, - }) + refreshJwt: session?.refreshJwt, + accessJwt: session?.accessJwt, } + + logger.debug(`session: persistSession`, { + event, + deactivated: refreshedAccount.deactivated, + }) + + if (expired) { + logger.warn(`session: expired`) + emitSessionDropped() + __globalAgent = PUBLIC_BSKY_AGENT + configureModerationForGuest() + setState(s => ({ + accounts: s.accounts, + currentAgentState: { + agent: PUBLIC_BSKY_AGENT, + did: undefined, + }, + needsPersist: true, + })) + } + + /* + * If the session expired, or it was successfully created/updated, we want + * to update/persist the data. + * + * If the session creation failed, it could be a network error, or it could + * be more serious like an invalid token(s). We can't differentiate, so in + * order to allow the user to get a fresh token (if they need it), we need + * to persist this data and wipe their tokens, effectively logging them + * out. + */ + setState(s => { + const existingAccount = s.accounts.find( + a => a.did === refreshedAccount.did, + ) + if ( + !expired && + existingAccount && + refreshedAccount && + JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) + ) { + // Fast path without a state update. + return s + } + return { + accounts: [ + refreshedAccount, + ...s.accounts.filter(a => a.did !== refreshedAccount.did), + ], + currentAgentState: s.currentAgentState, + needsPersist: true, + } + }) }, [], ) @@ -197,26 +218,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, ) - agent.setPersistSessionHandler( - createPersistSessionHandler( - agent, - account, - ({expired, refreshedAccount}) => { - upsertAccount(refreshedAccount, expired) - }, - {networkErrorCallback: clearCurrentAccount}, - ), - ) + agent.setPersistSessionHandler((event, session) => { + onAgentSessionChange(agent, account, event, session) + }) __globalAgent = agent await fetchingGates - upsertAccount(account) + setState(s => { + return { + accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], + currentAgentState: { + did: account.did, + agent: agent, + }, + needsPersist: true, + } + }) logger.debug(`session: created account`, {}, logger.DebugContext.session) track('Create Account') logEvent('account:create:success', {}) }, - [upsertAccount, clearCurrentAccount, createPersistSessionHandler], + [onAgentSessionChange], ) const login = React.useCallback<SessionApiContext['login']>( @@ -229,35 +252,39 @@ export function Provider({children}: React.PropsWithChildren<{}>) { authFactorToken, }) - agent.setPersistSessionHandler( - createPersistSessionHandler( - agent, - account, - ({expired, refreshedAccount}) => { - upsertAccount(refreshedAccount, expired) - }, - {networkErrorCallback: clearCurrentAccount}, - ), - ) + agent.setPersistSessionHandler((event, session) => { + onAgentSessionChange(agent, account, event, session) + }) __globalAgent = agent // @ts-ignore if (IS_DEV && isWeb) window.agent = agent await fetchingGates - upsertAccount(account) + setState(s => { + return { + accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], + currentAgentState: { + did: account.did, + agent: agent, + }, + needsPersist: true, + } + }) logger.debug(`session: logged in`, {}, logger.DebugContext.session) track('Sign In', {resumedSession: false}) logEvent('account:loggedIn', {logContext, withPassword: true}) }, - [upsertAccount, clearCurrentAccount, createPersistSessionHandler], + [onAgentSessionChange], ) const logout = React.useCallback<SessionApiContext['logout']>( async logContext => { logger.debug(`session: logout`) - clearCurrentAccount() + logger.warn(`session: clear current account`) + __globalAgent = PUBLIC_BSKY_AGENT + configureModerationForGuest() setState(s => { return { accounts: s.accounts.map(a => ({ @@ -265,13 +292,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) { refreshJwt: undefined, accessJwt: undefined, })), - currentAccountDid: s.currentAccountDid, + currentAgentState: { + did: undefined, + agent: PUBLIC_BSKY_AGENT, + }, needsPersist: true, } }) logEvent('account:loggedOut', {logContext}) }, - [clearCurrentAccount, setState], + [setState], ) const initSession = React.useCallback<SessionApiContext['initSession']>( @@ -286,16 +316,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl) } - agent.setPersistSessionHandler( - createPersistSessionHandler( - agent, - account, - ({expired, refreshedAccount}) => { - upsertAccount(refreshedAccount, expired) - }, - {networkErrorCallback: clearCurrentAccount}, - ), - ) + agent.setPersistSessionHandler((event, session) => { + onAgentSessionChange(agent, account, event, session) + }) // @ts-ignore if (IS_DEV && isWeb) window.agent = agent @@ -305,8 +328,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isSessionDeactivated(account.accessJwt) || account.deactivated const prevSession = { - accessJwt: account.accessJwt || '', - refreshJwt: account.refreshJwt || '', + accessJwt: account.accessJwt ?? '', + refreshJwt: account.refreshJwt ?? '', did: account.did, handle: account.handle, } @@ -314,23 +337,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) { if (isSessionExpired(account)) { logger.debug(`session: attempting to resume using previous session`) - try { - const freshAccount = await resumeSessionWithFreshAccount() - __globalAgent = agent - await fetchingGates - upsertAccount(freshAccount) - } catch (e) { - /* - * Note: `agent.persistSession` is also called when this fails, and - * we handle that failure via `createPersistSessionHandler` - */ - logger.info(`session: resumeSessionWithFreshAccount failed`, { - message: e, - }) - - __globalAgent = PUBLIC_BSKY_AGENT - // TODO: Should this update currentAccountDid? - } + const freshAccount = await resumeSessionWithFreshAccount() + __globalAgent = agent + await fetchingGates + setState(s => { + return { + accounts: [ + freshAccount, + ...s.accounts.filter(a => a.did !== freshAccount.did), + ], + currentAgentState: { + did: freshAccount.did, + agent: agent, + }, + needsPersist: true, + } + }) } else { logger.debug(`session: attempting to reuse previous session`) @@ -338,7 +360,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) { __globalAgent = agent await fetchingGates - upsertAccount(account) + setState(s => { + return { + accounts: [ + account, + ...s.accounts.filter(a => a.did !== account.did), + ], + currentAgentState: { + did: account.did, + agent: agent, + }, + needsPersist: true, + } + }) if (accountOrSessionDeactivated) { // don't attempt to resume @@ -349,26 +383,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // Intentionally not awaited to unblock the UI: resumeSessionWithFreshAccount() - .then(freshAccount => { - if (JSON.stringify(account) !== JSON.stringify(freshAccount)) { - logger.info( - `session: reuse of previous session returned a fresh account, upserting`, - ) - upsertAccount(freshAccount) - } - }) - .catch(e => { - /* - * Note: `agent.persistSession` is also called when this fails, and - * we handle that failure via `createPersistSessionHandler` - */ - logger.info(`session: resumeSessionWithFreshAccount failed`, { - message: e, - }) - - __globalAgent = PUBLIC_BSKY_AGENT - // TODO: Should this update currentAccountDid? - }) } async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { @@ -386,22 +400,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return sessionAccount } }, - [upsertAccount, clearCurrentAccount, createPersistSessionHandler], - ) - - const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( - async account => { - try { - if (account) { - await initSession(account) - } - } catch (e) { - logger.error(`session: resumeSession failed`, {message: e}) - } finally { - setIsInitialLoad(false) - } - }, - [initSession], + [onAgentSessionChange], ) const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( @@ -409,7 +408,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { setState(s => { return { accounts: s.accounts.filter(a => a.did !== account.did), - currentAccountDid: s.currentAccountDid, + currentAgentState: s.currentAgentState, needsPersist: true, } }) @@ -423,23 +422,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) { account => { setState(s => { const currentAccount = s.accounts.find( - a => a.did === s.currentAccountDid, + a => a.did === s.currentAgentState.did, ) // ignore, should never happen if (!currentAccount) return s const updatedAccount = { ...currentAccount, - handle: account.handle || currentAccount.handle, - email: account.email || currentAccount.email, + handle: account.handle ?? currentAccount.handle, + email: account.email ?? currentAccount.email, emailConfirmed: - account.emailConfirmed !== undefined - ? account.emailConfirmed - : currentAccount.emailConfirmed, + account.emailConfirmed ?? currentAccount.emailConfirmed, emailAuthFactor: - account.emailAuthFactor !== undefined - ? account.emailAuthFactor - : currentAccount.emailAuthFactor, + account.emailAuthFactor ?? currentAccount.emailAuthFactor, } return { @@ -447,7 +442,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { updatedAccount, ...s.accounts.filter(a => a.did !== currentAccount.did), ], - currentAccountDid: s.currentAccountDid, + currentAgentState: s.currentAgentState, needsPersist: true, } }) @@ -455,30 +450,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [setState], ) - const selectAccount = React.useCallback<SessionApiContext['selectAccount']>( - async (account, logContext) => { - setIsSwitchingAccounts(true) - try { - await initSession(account) - setIsSwitchingAccounts(false) - logEvent('account:loggedIn', {logContext, withPassword: false}) - } catch (e) { - // reset this in case of error - setIsSwitchingAccounts(false) - // but other listeners need a throw - throw e - } - }, - [initSession], - ) - React.useEffect(() => { if (state.needsPersist) { state.needsPersist = false persisted.write('session', { accounts: state.accounts, currentAccount: state.accounts.find( - a => a.did === state.currentAccountDid, + a => a.did === state.currentAgentState.did, ), }) } @@ -489,16 +467,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const persistedSession = persisted.get('session') logger.debug(`session: persisted onUpdate`, {}) + setState(s => ({ + accounts: persistedSession.accounts, + currentAgentState: s.currentAgentState, + needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. + })) const selectedAccount = persistedSession.accounts.find( a => a.did === persistedSession.currentAccount?.did, ) if (selectedAccount && selectedAccount.refreshJwt) { - if (selectedAccount.did !== state.currentAccountDid) { + if (selectedAccount.did !== state.currentAgentState.did) { logger.debug(`session: persisted onUpdate, switching accounts`, { from: { - did: state.currentAccountDid, + did: state.currentAgentState.did, }, to: { did: selectedAccount.did, @@ -516,8 +499,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { */ // @ts-ignore we checked for `refreshJwt` above __globalAgent.session = selectedAccount + // TODO: This needs a setState. } - } else if (!selectedAccount && state.currentAccountDid) { + } else if (!selectedAccount && state.currentAgentState.did) { logger.debug( `session: persisted onUpdate, logging out`, {}, @@ -530,28 +514,30 @@ export function Provider({children}: React.PropsWithChildren<{}>) { * handled by `persistSession` (which nukes this accounts tokens only), * or by a `logout` call which nukes all accounts tokens) */ - clearCurrentAccount() + logger.warn(`session: clear current account`) + __globalAgent = PUBLIC_BSKY_AGENT + configureModerationForGuest() + setState(s => ({ + accounts: s.accounts, + currentAgentState: { + did: undefined, + agent: PUBLIC_BSKY_AGENT, + }, + needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. + })) } - - setState(() => ({ - accounts: persistedSession.accounts, - currentAccountDid: selectedAccount?.did, - needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. - })) }) - }, [state, setState, clearCurrentAccount, initSession]) + }, [state, setState, initSession]) const stateContext = React.useMemo( () => ({ accounts: state.accounts, currentAccount: state.accounts.find( - a => a.did === state.currentAccountDid, + a => a.did === state.currentAgentState.did, ), - isInitialLoad, - isSwitchingAccounts, - hasSession: !!state.currentAccountDid, + hasSession: !!state.currentAgentState.did, }), - [state, isInitialLoad, isSwitchingAccounts], + [state], ) const api = React.useMemo( @@ -560,9 +546,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { login, logout, initSession, - resumeSession, removeAccount, - selectAccount, updateCurrentAccount, clearCurrentAccount, }), @@ -571,9 +555,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { login, logout, initSession, - resumeSession, removeAccount, - selectAccount, updateCurrentAccount, clearCurrentAccount, ], diff --git a/src/state/session/types.ts b/src/state/session/types.ts index fbfac82e9..b3252f777 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -6,8 +6,6 @@ export type SessionAccount = PersistedAccount export type SessionStateContext = { accounts: SessionAccount[] currentAccount: SessionAccount | undefined - isInitialLoad: boolean - isSwitchingAccounts: boolean hasSession: boolean } export type SessionApiContext = { @@ -46,12 +44,7 @@ export type SessionApiContext = { */ clearCurrentAccount: () => void initSession: (account: SessionAccount) => Promise<void> - resumeSession: (account?: SessionAccount) => Promise<void> removeAccount: (account: SessionAccount) => void - selectAccount: ( - account: SessionAccount, - logContext: LogEvents['account:loggedIn']['logContext'], - ) => Promise<void> updateCurrentAccount: ( account: Partial< Pick< |