diff --git a/lib/utils/conversion-utils.js b/lib/utils/conversion-utils.js index 0349740d8..1973fd86b 100644 --- a/lib/utils/conversion-utils.js +++ b/lib/utils/conversion-utils.js @@ -1,127 +1,145 @@ // @flow import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import type { TInterface, TType } from 'tcomb'; +import { convertIDToNewSchema } from './migration-utils.js'; import { assertWithValidator, tID } from './validation-utils.js'; +import { + getPendingThreadID, + parsePendingThreadID, +} from '../shared/thread-utils.js'; function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = id => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } - return `${serverPrefixID}|${id}`; + return convertIDToNewSchema(id, serverPrefixID); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = id => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } - throw new Error('invalid_client_id_prefix'); + const pendingIDContents = parsePendingThreadID(id); + if (!pendingIDContents) { + throw new Error('invalid_client_id_prefix'); + } + + if (!pendingIDContents.sourceMessageID) { + return id; + } + + return getPendingThreadID( + pendingIDContents.threadType, + pendingIDContents.memberIDs, + pendingIDContents.sourceMessageID.substr(prefix.length), + ); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } export { convertClientIDsToServerIDs, convertServerIDsToClientIDs, convertObject, }; diff --git a/lib/utils/conversion-utils.test.js b/lib/utils/conversion-utils.test.js index ac30d6449..7dfadaa8a 100644 --- a/lib/utils/conversion-utils.test.js +++ b/lib/utils/conversion-utils.test.js @@ -1,69 +1,84 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import { convertServerIDsToClientIDs, convertClientIDsToServerIDs, } from './conversion-utils.js'; import { tShape, tID, idSchemaRegex } from './validation-utils.js'; describe('id conversion', () => { it('should convert string id', () => { const validator = tShape({ id: tID }); const serverData = { id: '1' }; const clientData = { id: '0|1' }; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); it('should convert a complex type', () => { const validator = tShape({ ids: t.dict(tID, t.list(tID)) }); const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } }; const clientData = { ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] }, }; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); it('should convert a refinement', () => { const validator = t.refinement(tID, () => true); const serverData = '1'; const clientData = '0|1'; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); }); describe('idSchemaRegex tests', () => { it('should capture ids', () => { const regex = new RegExp(`^(${idSchemaRegex})$`); const ids = ['123|123', '0|0', '123', '0']; for (const id of ids) { const result = regex.exec(id); expect(result).not.toBeNull(); invariant(result, 'result is not null'); const matches = [...result]; expect(matches).toHaveLength(2); expect(matches[1]).toBe(id); } }); }); + +describe('Pending ids tests', () => { + it('should convert pending ids', () => { + const validator = t.list(tID); + const serverData = ['pending/sidebar/1', 'pending/type4/1+2+3']; + const clientData = ['pending/sidebar/0|1', 'pending/type4/1+2+3']; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); +}); diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js index a59b91d64..832cdd880 100644 --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -1,66 +1,75 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import type { Persistor } from 'redux-persist/es/types'; import { useServerCall } from 'lib/utils/action-utils.js'; +import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { getInitialReduxState, setInitialReduxState } from './action-types.js'; import { useSelector } from './redux-utils.js'; import Loading from '../loading.react.js'; type Props = { +persistor: Persistor, +children: React.Node, }; function InitialReduxStateGate(props: Props): React.Node { const { children, persistor } = props; const callGetInitialReduxState = useServerCall(getInitialReduxState); const dispatch = useDispatch(); const [initError, setInitError] = React.useState(null); React.useEffect(() => { if (initError) { throw initError; } }, [initError]); const isRehydrated = useSelector(state => !!state._persist?.rehydrated); const prevIsRehydrated = React.useRef(false); React.useEffect(() => { if (!prevIsRehydrated.current && isRehydrated) { prevIsRehydrated.current = isRehydrated; (async () => { try { - const urlInfo = infoFromURL(decodeURI(window.location.href)); + let urlInfo = infoFromURL(decodeURI(window.location.href)); + // Handle older links + if (urlInfo.thread) { + urlInfo = { + ...urlInfo, + thread: convertIDToNewSchema(urlInfo.thread, ashoatKeyserverID), + }; + } const payload = await callGetInitialReduxState(urlInfo); dispatch({ type: setInitialReduxState, payload }); } catch (err) { setInitError(err); } })(); } }, [callGetInitialReduxState, dispatch, isRehydrated]); const initialStateLoaded = useSelector(state => state.initialStateLoaded); const childFunction = React.useCallback( // This argument is passed from `PersistGate`. It means that the state is // rehydrated and we can start fetching the initial info. bootstrapped => { if (bootstrapped && initialStateLoaded) { return children; } else { return ; } }, [children, initialStateLoaded], ); return {childFunction}; } export default InitialReduxStateGate;