refactor account context in mobile app

This commit is contained in:
Roland Osborne 2023-02-09 10:42:06 -08:00
parent 726ca2ebc8
commit aeca7d9ead
5 changed files with 158 additions and 87 deletions

View File

@ -8,11 +8,12 @@ import { setAccountLogin } from 'api/setAccountLogin';
export function useAccountContext() {
const [state, setState] = useState({
offsync: false,
status: {},
});
const store = useContext(StoreContext);
const session = useRef(null);
const access = useRef(null);
const curRevision = useRef(null);
const setRevision = useRef(null);
const syncing = useRef(false);
@ -22,13 +23,12 @@ export function useAccountContext() {
}
const sync = async () => {
if (!syncing.current && setRevision.current !== curRevision.current) {
if (access.current && !syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
try {
const revision = curRevision.current;
const { server, appToken, guid } = session.current;
const status = await getAccountStatus(server, appToken);
const { server, token, guid } = access.current;
const status = await getAccountStatus(server, token);
await store.actions.setAccountStatus(guid, status);
await store.actions.setAccountRevision(guid, revision);
updateState({ status });
@ -46,47 +46,53 @@ export function useAccountContext() {
};
const actions = {
setSession: async (access) => {
const { guid, server, appToken } = access;
setSession: async (session) => {
if (access.current || syncing.current) {
throw new Error('invalid account state');
}
access.current = session;
const { guid, server, token } = session;
const status = await store.actions.getAccountStatus(guid);
const sealKey = await store.actions.getAccountSealKey(guid);
const revision = await store.actions.getAccountRevision(guid);
updateState({ status, sealKey });
setRevision.current = revision;
curRevision.current = revision;
session.current = access;
},
clearSession: () => {
session.current = {};
updateState({ account: null });
access.current = null;
updateState({ account: {} });
},
setRevision: (rev) => {
curRevision.current = rev;
sync();
},
setNotifications: async (flag) => {
const { server, appToken } = session.current;
await setAccountNotifications(server, appToken, flag);
const { server, token } = access.current;
await setAccountNotifications(server, token, flag);
},
setSearchable: async (flag) => {
const { server, appToken } = session.current;
await setAccountSearchable(server, appToken, flag);
const { server, token } = access.current;
await setAccountSearchable(server, token, flag);
},
setAccountSeal: async (seal, key) => {
const { guid, server, appToken } = session.current;
await setAccountSeal(server, appToken, seal);
const { guid, server, token } = access.current;
await setAccountSeal(server, token, seal);
await store.actions.setAccountSealKey(guid, key);
updateState({ sealKey: key });
},
unlockAccountSeal: async (key) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setAccountSealKey(guid, key);
updateState({ sealKey: key });
},
setLogin: async (username, password) => {
const { server, appToken } = session.current;
await setAccountLogin(server, appToken, username, password);
const { server, token } = access.current;
await setAccountLogin(server, token, username, password);
},
resync: async () => {
await sync();
}
}
return { state, actions }

View File

@ -41,7 +41,6 @@ export function useCardContext() {
const curRevision = useRef(null);
const cards = useRef(new Map());
const syncing = useRef(false);
const force = useRef(false);
const store = useContext(StoreContext);
const updateState = (value) => {
@ -94,17 +93,7 @@ export function useCardContext() {
}
}
};
const resync = async () => {
try {
force.current = true;
await sync();
}
catch (err) {
console.log(err);
}
};
const resyncCard = async (cardId) => {
if (!syncing.current) {
syncing.current = true;
@ -132,10 +121,8 @@ export function useCardContext() {
}
const sync = async () => {
if (!syncing.current && (setRevision.current !== curRevision.current || force.current)) {
if (access.current && !syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
force.current = false;
try {
const { server, token, guid } = access.current;
const revision = curRevision.current;
@ -297,9 +284,9 @@ export function useCardContext() {
clearSession: () => {
access.current = null;
},
setRevision: async (revision) => {
setRevision: (revision) => {
curRevision.current = revision;
await sync();
sync();
},
addCard: async (message) => {
const { server, token } = access.current;
@ -489,7 +476,7 @@ export function useCardContext() {
await store.actions.setCardChannelTopicItemUnsealedDetail(guid, cardId, channelId, topicId, revision, unsealed);
},
resync: async () => {
await resync();
await sync();
},
resyncCard: async (cardId) => {
await resyncCard(cardId);

View File

@ -30,7 +30,6 @@ export function useChannelContext() {
const curRevision = useRef(null);
const channels = useRef(new Map());
const syncing = useRef(false);
const force = useRef(false);
const store = useContext(StoreContext);
const updateState = (value) => {
@ -55,21 +54,9 @@ export function useChannelContext() {
updateState({ channels: channels.current });
};
const resync = async () => {
try {
force.current = true;
await sync();
}
catch (err) {
console.log(err);
}
};
const sync = async () => {
if (!syncing.current && (setRevision.current !== curRevision.current || force.current)) {
if (access.current && !syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
force.current = false;
try {
const revision = curRevision.current;
const { server, token, guid } = access.current;
@ -111,10 +98,11 @@ export function useChannelContext() {
setRevision.current = revision;
await store.actions.setChannelRevision(guid, revision);
updateState({ channels: channels.current });
updateState({ offsync: false, channels: channels.current });
}
catch(err) {
console.log(err);
updateState({ offsync: true });
syncing.current = false;
return;
}
@ -144,9 +132,9 @@ export function useChannelContext() {
clearSession: () => {
access.current = null;
},
setRevision: async (rev) => {
setRevision: (rev) => {
curRevision.current = rev;
await sync();
sync();
},
addChannel: async (type, subject, cards) => {
const { server, token } = access.current;
@ -169,7 +157,7 @@ export function useChannelContext() {
return await clearChannelCard(server, token, channelId, cardId);
},
addTopic: async (channelId, type, message, files) => {
const { server, token } = session.current;
const { server, token } = access.current;
if (files?.length > 0) {
const topicId = await addChannelTopic(server, token, channelId, null, null, null);
upload.actions.addTopic(server, token, channelId, topicId, files, async (assets) => {
@ -210,81 +198,79 @@ export function useChannelContext() {
return await getChannelTopic(server, token, channelId, topicId);
},
resync: async () => {
await resync();
await sync();
},
getNotifications: async (channelId) => {
const { server, token } = session.current;
const { server, token } = access.current;
return await getChannelNotifications(server, token, channelId);
},
setNotifications: async (channelId, notify) => {
const { server, token } = session.current;
const { server, token } = access.current;
return await setChannelNotifications(server, token, channelId, notify);
},
setReadRevision: async (channelId, revision) => {
const { guid } = access.current;
await store.actions.setChannelItemReadRevision(guid, channelId, revision);
setChannelField(channelId, 'readRevision', revision);
},
setSyncRevision: async (channelId, revision) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelItemSyncRevision(guid, channelId, revision);
setChannelField(channelId, 'syncRevision', revision);
},
setTopicMarker: async (channelId, marker) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelItemTopicMarker(guid, channelId, revision);
setChannelField(channelId, 'topicMarker', marker);
},
setChannelFlag: async (channelId) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelItemBlocked(guid, channelId);
setChannelField(channelId, 'blocked', true);
},
clearChannelFlag: async (channelId) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.clearChannelItemBlocked(guid, channelId);
setChannelField(channelId, 'blocked', false);
},
setTopicFlag: async (channelId, topicId) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelTopicBlocked(guid, channelId, topicId, true);
},
clearTopicFlag: async (channelId, topicId) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelTopicBlocked(guid, channelId, topicId, false);
},
addChannelAlert: async (channelId) => {
const { server, guid } = session.current;
const { server, guid } = access.current;
return await addFlag(server, guid, channelId);
},
addTopicAlert: async (channelId, topicId) => {
const { server, guid } = session.current;
const { server, guid } = access.current;
return await addFlag(server, guid, channelId, topicId);
},
getTopicItems: async (channelId, revision, count, begin, end) => {
const { guid } = session.current;
const { guid } = access.current;
return await store.actions.getChannelTopicItems(guid, channelId);
},
setTopicItem: async (channelId, topic) => {
const { guid } = session.current;
const { guid } = access.current;
return await store.actions.setChannelTopicItem(guid, channelId, topic);
},
clearTopicItem: async (channelId, topicId) => {
const { guid } = session.current;
const { guid } = access.current;
return await store.actions.clearChannelTopicItem(guid, channelId, topicId);
},
setUnsealedChannelSubject: async (channelId, revision, unsealed) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelItemUnsealedDetail(guid, channelId, revision, unsealed);
},
setUnsealedChannelSummary: async (channelId, revision, unsealed) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelItemUnsealedSummary(guid, channelId, revision, unsealed);
},
setUnsealedTopicSubject: async (channelId, topicId, revision, unsealed) => {
const { guid } = session.current;
const { guid } = access.current;
await store.actions.setChannelTopicItemUnsealedDetail(guid, channelId, topicId, revision, unsealed);
},
};

View File

@ -8,12 +8,13 @@ import { StoreContext } from 'context/StoreContext';
export function useProfileContext() {
const [state, setState] = useState({
offsync: false,
identity: {},
imageUrl: null,
});
const store = useContext(StoreContext);
const session = useRef(null);
const access = useRef(null);
const curRevision = useRef(null);
const setRevision = useRef(null);
const syncing = useRef(false);
@ -23,21 +24,22 @@ export function useProfileContext() {
}
const sync = async () => {
if (!syncing.current && setRevision.current !== curRevision.current) {
if (access.current && !syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
try {
const revision = curRevision.current;
const { server, token, guid } = session.current;
const { server, token, guid } = access.current;
const identity = await getProfile(server, token);
const imageUrl = identity?.image ? getProfileImageUrl(server, token, revision) : null;
await store.actions.setProfile(guid, identity);
await store.actions.setProfileRevision(guid, revision);
updateState({ identity, imageUrl });
updateState({ offsync: false, identity, imageUrl });
setRevision.current = revision;
}
catch(err) {
console.log(err);
updateState({ offsync: true });
syncing.current = false;
return;
}
@ -48,34 +50,33 @@ export function useProfileContext() {
};
const actions = {
setSession: async (access) => {
const { guid, server, token } = access;
setSession: async (session) => {
const { guid, server, token } = session;
const identity = await store.actions.getProfile(guid);
const revision = await store.actions.getProfileRevision(guid);
const imageUrl = identity?.image ? getProfileImageUrl(server, token, revision) : null;
updateState({ identity, imageUrl });
updateState({ offsync: false, identity, imageUrl });
setRevision.current = revision;
curRevision.current = revision;
session.current = access;
access.current = session;
},
clearSession: () => {
session.current = {};
updateState({ identity: {}, imageUrl: null });
access.current = null;
},
setRevision: (rev) => {
curRevision.current = rev;
sync();
},
setProfileData: async (name, location, description) => {
const { server, token } = session.current;
const { server, token } = access.current;
await setProfileData(server, token, name, location, description);
},
setProfileImage: async (image) => {
const { server, token } = session.current;
const { server, token } = access.current;
await setProfileImage(server, token, image);
},
getHandleStatus: async (name) => {
const { server, token } = session.current;
const { server, token } = access.current;
return await getHandle(server, token, name);
},
}

View File

@ -0,0 +1,91 @@
import React, { useState, useEffect, useContext } from 'react';
import { View, Text } from 'react-native';
import { useTestStoreContext } from './useTestStoreContext.hook';
import {render, act, screen, waitFor, fireEvent} from '@testing-library/react-native'
import { AccountContextProvider, AccountContext } from 'context/AccountContext';
import * as fetchUtil from 'api/fetchUtil';
function AccountView() {
const [renderCount, setRenderCount] = useState(0);
const account = useContext(AccountContext);
useEffect(() => {
setRenderCount(renderCount + 1);
}, [account.state]);
return (
<View testID="account" account={account} renderCount={renderCount}>
<Text testID="searchable">{ account.state.status?.searchable }</Text>
</View>
);
}
function AccountTestApp() {
return (
<AccountContextProvider>
<AccountView />
</AccountContextProvider>
)
}
let fetchStatus;
const realUseContext = React.useContext;
const realFetchWithTimeout = fetchUtil.fetchWithTimeout;
const realFetchWithCustomTimeout = fetchUtil.fetchWithCustomTimeout;
beforeEach(() => {
const mockUseContext = jest.fn().mockImplementation((ctx) => {
return useTestStoreContext();
});
React.useContext = mockUseContext;
fetchStatus = {};
const mockFetch = jest.fn().mockImplementation((url, options) => {
return Promise.resolve({
json: () => Promise.resolve(fetchStatus)
});
});
fetchUtil.fetchWithTimeout = mockFetch;
fetchUtil.fetchWithCustomTimeout = mockFetch;
});
afterEach(() => {
React.useContext = realUseContext;
fetchUtil.fetchWithTimeout = realFetchWithTimeout;
fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
});
test('testing', async () => {
render(<AccountTestApp />)
await waitFor(async () => {
expect(screen.getByTestId('searchable').props.children).toBe(undefined);
});
fetchStatus = { searchable: true };
await act(async () => {
const account = screen.getByTestId('account').props.account;
await account.actions.setSession({ guid: 'abc', server: 'test.org', token: '123' });
await account.actions.setRevision(1);
});
await waitFor(async () => {
expect(screen.getByTestId('searchable').props.children).toBe(true);
});
fetchStatus = { searchable: false };
await act(async () => {
const account = screen.getByTestId('account').props.account;
await account.actions.setRevision(2);
});
await waitFor(async () => {
expect(screen.getByTestId('searchable').props.children).toBe(false);
});
});