Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions src/view/com/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useProfilesQuery} from '#/state/queries/profile'
import {type Gif} from '#/state/queries/tenor'
import {type SessionAccount, useAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
Expand Down Expand Up @@ -182,24 +182,46 @@ export const ComposePost = ({
}) => {
const {currentAccount, accounts} = useSession()
const defaultAgent = useAgent()
const [selectedAccount, setSelectedAccount] = useState(currentAccount!)
const [selectedAccountDid, setSelectedAccountDid] = useState(
currentAccount!.did,
)

const {data: agent = defaultAgent} = useQuery({
queryKey: [
'composer-agent',
currentAccount?.did,
currentAccount?.service,
currentAccount?.active,
selectedAccount?.did,
selectedAccount?.service,
selectedAccount?.accessJwt,
selectedAccount?.refreshJwt,
selectedAccount?.active,
selectedAccountDid,
// include account data in the query key to invalidate when tokens change
// hmm we don't want a nested array i think so spreading seems like the way to go(?)
...(() => {
const selectedAccount = accounts.find(
acc => acc.did === selectedAccountDid,
)
return selectedAccount
? [
selectedAccount.service,
selectedAccount.accessJwt,
selectedAccount.refreshJwt,
selectedAccount.active,
]
: []
})(),
],
queryFn: async () => {
if (selectedAccount.did === currentAccount!.did) {
if (selectedAccountDid === currentAccount!.did) {
return defaultAgent
}

// get fresh account data from the session store
const selectedAccount = accounts.find(
acc => acc.did === selectedAccountDid,
)
if (!selectedAccount) {
throw new Error(`Account with DID ${selectedAccountDid} not found`)
}

const session = new CredentialSession(new URL(selectedAccount.service))
Copy link
Contributor

@mary-ext mary-ext Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this have a listener for session updates, any session expiry here is going to result in the session getting marked as invalid (due to stale refresh token) when you do another post (with that account), or actually switch to that account later

selectedAccount should probably be a DID, instead of passing the entire Account object in

Copy link
Author

@SapphoSys SapphoSys Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya good point, i just quickly put things together to get the switcher working in the composer but now that i look at it its redundant

Copy link
Author

@SapphoSys SapphoSys Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mary-ext done. you can check out commits 1ccdbcc and 06aec44

if (
selectedAccount.refreshJwt &&
Expand All @@ -219,7 +241,7 @@ export const ComposePost = ({
})

const queryClient = useQueryClient()
const currentDid = selectedAccount.did
const currentDid = selectedAccountDid
const {closeComposer} = useComposerControls()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const {_} = useLingui()
Expand Down Expand Up @@ -254,8 +276,15 @@ export const ComposePost = ({
)

const onSelectAccount = React.useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure to check what happens if the async code below is slowed down to trigger a race condition of resolving in the opposite order

Copy link
Author

@SapphoSys SapphoSys Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quickly switched from account A to B to C, then back to A. Switching accounts has a (close to) 1 second delay (might be quicker with your connection, it varied for me), with subsequent switches being a lot faster
(this is with the useQuery & useQueryClient logic added in order to cache sessions)

I also decided to add a 1 second setTimeout promise delay just to check if the rapid fire switch requests would be out of order. I haven't noticed that being the case

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I could probably add a condition guard or something just in case... hm
@mozzius let me know if it'd be necessary here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to clarify, i mean that race conditions are always possible — so it’s good to find a way to trigger one reliably (eg by simulating alternating delays) and then to write the code in a way that it’s resilient to the race

async (account: SessionAccount) => {
if (account.did === selectedAccount.did) {
async (accountDid: string) => {
if (accountDid === selectedAccountDid) {
return
}

// get fresh account data from session store
const account = accounts.find(acc => acc.did === accountDid)
if (!account) {
setError('Account not found')
Copy link
Author

@SapphoSys SapphoSys Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not 100% sure about the setError statement here tbh since the codebase only uses setError with either translated strings using msg, or to pass errors just as is (e.message, data.error, etc)
i originally wanted to use the throw statement, but wasn't sure it fit here. this feels a little hacky... hm

return
}

Expand All @@ -281,7 +310,7 @@ export const ComposePost = ({
currentAccount?.did,
currentAccount?.service,
currentAccount?.active,
account.did,
accountDid,
account.service,
account.accessJwt,
account.refreshJwt,
Expand All @@ -290,7 +319,7 @@ export const ComposePost = ({
tempAgent,
)
// if it succeeds, update the selected account
setSelectedAccount(account)
setSelectedAccountDid(accountDid)
} catch (e: any) {
if (
String(e.message).toLowerCase().includes('token has expired') ||
Expand All @@ -308,7 +337,8 @@ export const ComposePost = ({
}
},
[
selectedAccount.did,
selectedAccountDid,
accounts,
closeComposer,
requestSwitchToAccount,
_,
Expand Down Expand Up @@ -854,7 +884,7 @@ export const ComposePost = ({
onClearVideo={clearVideo}
onPublish={onComposerPostPublish}
onError={setError}
selectedAccount={selectedAccount}
selectedAccountDid={selectedAccountDid}
onSelectAccount={onSelectAccount}
profiles={profiles?.profiles}
/>
Expand Down Expand Up @@ -895,7 +925,7 @@ let ComposerPost = React.memo(function ComposerPost({
onSelectVideo,
onError,
onPublish,
selectedAccount,
selectedAccountDid,
onSelectAccount,
profiles,
}: {
Expand All @@ -913,8 +943,8 @@ let ComposerPost = React.memo(function ComposerPost({
onSelectVideo: (postId: string, asset: ImagePickerAsset) => void
onError: (error: string) => void
onPublish: (richtext: RichText) => void
selectedAccount: SessionAccount
onSelectAccount: (account: SessionAccount) => void
selectedAccountDid: string
onSelectAccount: (accountDid: string) => void
profiles: AppBskyActorDefs.ProfileViewDetailed[] | undefined
}) {
const {_} = useLingui()
Expand Down Expand Up @@ -996,7 +1026,7 @@ let ComposerPost = React.memo(function ComposerPost({
]}>
<View style={[a.flex_row, a.align_start, isNative && a.flex_1]}>
<AccountSwitcher
selectedAccount={selectedAccount}
selectedAccountDid={selectedAccountDid}
onSelectAccount={onSelectAccount}
profiles={profiles}
/>
Expand Down
14 changes: 7 additions & 7 deletions src/view/com/composer/account-switcher/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import * as Dialog from '#/components/Dialog'
import {SwitchAccountDialog} from '../SwitchAccount'

interface AccountSwitcherProps {
selectedAccount: SessionAccount
onSelectAccount: (account: SessionAccount) => void
selectedAccountDid: string
onSelectAccount: (accountDid: string) => void
profiles: AppBskyActorDefs.ProfileViewDetailed[] | undefined
}

export const AccountSwitcher: React.FC<AccountSwitcherProps> = ({
selectedAccount,
selectedAccountDid,
onSelectAccount,
profiles,
}) => {
const {accounts} = useSession()
const {_} = useLingui()
const currentProfile = profiles?.find(p => p.did === selectedAccount.did)
const currentProfile = profiles?.find(p => p.did === selectedAccountDid)
const otherAccounts = accounts
.filter((acc: SessionAccount) => acc.did !== selectedAccount.did)
.filter((acc: SessionAccount) => acc.did !== selectedAccountDid)
.map((account: SessionAccount) => ({
account,
profile: profiles?.find(p => p.did === account.did),
Expand All @@ -48,8 +48,8 @@ export const AccountSwitcher: React.FC<AccountSwitcherProps> = ({
</Button>
<SwitchAccountDialog
control={switchAccountControl}
onSelectAccount={onSelectAccount}
currentAccountDid={selectedAccount.did}
onSelectAccount={account => onSelectAccount(account.did)}
currentAccountDid={selectedAccountDid}
/>
</>
)
Expand Down
12 changes: 6 additions & 6 deletions src/view/com/composer/account-switcher/AccountSwitcher.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ import {Button} from '#/components/Button'
import * as Menu from '#/components/Menu'

interface AccountSwitcherProps {
selectedAccount: SessionAccount
onSelectAccount: (account: SessionAccount) => void
selectedAccountDid: string
onSelectAccount: (accountDid: string) => void
profiles: AppBskyActorDefs.ProfileViewDetailed[] | undefined
}

export const AccountSwitcher: React.FC<AccountSwitcherProps> = ({
selectedAccount,
selectedAccountDid,
onSelectAccount,
profiles,
}) => {
const {accounts} = useSession()
const {_} = useLingui()
const currentProfile = profiles?.find(p => p.did === selectedAccount.did)
const currentProfile = profiles?.find(p => p.did === selectedAccountDid)
const otherAccounts = accounts
.filter((acc: SessionAccount) => acc.did !== selectedAccount.did)
.filter((acc: SessionAccount) => acc.did !== selectedAccountDid)
.map((account: SessionAccount) => ({
account,
profile: profiles?.find(p => p.did === account.did),
Expand Down Expand Up @@ -66,7 +66,7 @@ export const AccountSwitcher: React.FC<AccountSwitcherProps> = ({
'@',
)}`,
)}
onPress={() => onSelectAccount(account)}>
onPress={() => onSelectAccount(account.did)}>
<View>
<UserAvatar
avatar={profile?.avatar}
Expand Down