interface: convert launch store to zustand

This commit is contained in:
Tyler Brown Cifu Shuster 2021-02-26 08:40:41 -08:00
parent b8cd15a788
commit d17794f93d
12 changed files with 151 additions and 100 deletions

View File

@ -1,61 +1,70 @@
import _ from 'lodash';
import { LaunchUpdate } from '~/types/launch-update';
import { LaunchState, LaunchUpdate, WeatherState } from '~/types/launch-update';
import { Cage } from '~/types/cage';
import { StoreState } from '../../store/type';
import useLaunchState from '../state/launch';
import { compose } from 'lodash/fp';
type LaunchState = Pick<StoreState, 'launch' | 'weather' | 'userLocation'>;
export default class LaunchReducer<S extends LaunchState> {
reduce(json: Cage, state: S) {
export default class LaunchReducer {
reduce(json: Cage) {
const data = _.get(json, 'launch-update', false);
if (data) {
this.initial(data, state);
this.changeFirstTime(data, state);
this.changeOrder(data, state);
this.changeFirstTime(data, state);
this.changeIsShown(data, state);
useLaunchState.setState(
compose([
initial,
changeFirstTime,
changeOrder,
changeFirstTime,
changeIsShown,
].map(reducer => reducer.bind(reducer, data))
)(useLaunchState.getState())
)
}
const weatherData = _.get(json, 'weather', false);
const weatherData: WeatherState = _.get(json, 'weather', false);
if (weatherData) {
state.weather = weatherData;
useLaunchState.setState({ weather: weatherData });
}
const locationData = _.get(json, 'location', false);
if (locationData) {
state.userLocation = locationData;
}
}
initial(json: LaunchUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.launch = data;
}
}
changeFirstTime(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeFirstTime', false);
if (data) {
state.launch.firstTime = data;
}
}
changeOrder(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeOrder', false);
if (data) {
state.launch.tileOrdering = data;
}
}
changeIsShown(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeIsShown', false);
if (data) {
const tile = state.launch.tiles[data.name];
console.log(tile);
if (tile) {
tile.isShown = data.isShown;
}
useLaunchState.setState({ userLocation: locationData });
}
}
}
export const initial = (json: LaunchUpdate, state: LaunchState): LaunchState => {
const data = _.get(json, 'initial', false);
if (data) {
Object.keys(data).forEach(key => {
state[key] = data[key];
});
}
return state;
}
export const changeFirstTime = (json: LaunchUpdate, state: LaunchState): LaunchState => {
const data = _.get(json, 'changeFirstTime', false);
if (data) {
state.firstTime = data;
}
return state;
}
export const changeOrder = (json: LaunchUpdate, state: LaunchState): LaunchState => {
const data = _.get(json, 'changeOrder', false);
if (data) {
state.tileOrdering = data;
}
return state;
}
export const changeIsShown = (json: LaunchUpdate, state: LaunchState): LaunchState => {
const data = _.get(json, 'changeIsShown', false);
if (data) {
const tile = state.tiles[data.name];
if (tile) {
tile.isShown = data.isShown;
}
}
return state;
}

View File

@ -0,0 +1,41 @@
import React from "react";
import create, { State } from "zustand";
import { persist } from "zustand/middleware";
import { Tile, WeatherState } from "~/types/launch-update";
import { stateSetter } from "../lib/util";
export interface LaunchState extends State {
firstTime: boolean;
tileOrdering: string[];
tiles: {
[app: string]: Tile;
},
weather: WeatherState | null,
userLocation: string | null;
set: (fn: (state: LaunchState) => void) => void;
};
const useLaunchState = create<LaunchState>(persist((set, get) => ({
firstTime: true,
tileOrdering: [],
tiles: {},
weather: null,
userLocation: null,
set: fn => stateSetter(fn, set)
}), {
name: 'LandscapeLaunchState'
}));
function withLaunchState<P, S extends keyof LaunchState>(Component: any, stateMemberKeys?: S[]) {
return React.forwardRef((props: Omit<P, S>, ref) => {
const launchState = stateMemberKeys ? useLaunchState(
state => stateMemberKeys.reduce(
(object, key) => ({ ...object, [key]: state[key] }), {}
)
): useLaunchState();
return <Component ref={ref} {...launchState} {...props} />
});
}
export { useLaunchState as default, withLaunchState };

View File

@ -106,7 +106,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.localReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data);
this.launchReducer.reduce(data, this.state);
this.launchReducer.reduce(data);
this.connReducer.reduce(data, this.state);
GraphReducer(data);
HarkReducer(data);

View File

@ -165,8 +165,6 @@ class App extends React.Component {
<ErrorBoundary>
<Omnibox
associations={state.associations}
apps={state.launch}
tiles={state.launch.tiles}
api={this.api}
show={this.props.omniboxShown}
toggle={this.props.toggleOmnibox}

View File

@ -177,11 +177,7 @@ export default function LaunchApp(props) {
</Box>
</Tile>
<Tiles
tiles={props.launch.tiles}
tileOrdering={props.launch.tileOrdering}
api={props.api}
location={props.userLocation}
weather={props.weather}
/>
<ModalButton
icon="Plus"

View File

@ -5,50 +5,50 @@ import CustomTile from './tiles/custom';
import ClockTile from './tiles/clock';
import WeatherTile from './tiles/weather';
export default class Tiles extends React.PureComponent {
render() {
const { props } = this;
import useLaunchState from '~/logic/state/launch';
const tiles = props.tileOrdering.filter((key) => {
const tile = props.tiles[key];
const Tiles = (props) => {
const weather = useLaunchState(state => state.weather);
const tileOrdering = useLaunchState(state => state.tileOrdering);
const tileState = useLaunchState(state => state.tiles);
const tiles = tileOrdering.filter((key) => {
const tile = tileState[key];
return tile.isShown;
}).map((key) => {
const tile = props.tiles[key];
if ('basic' in tile.type) {
const basic = tile.type.basic;
return tile.isShown;
}).map((key) => {
const tile = tileState[key];
if ('basic' in tile.type) {
const basic = tile.type.basic;
return (
<BasicTile
key={key}
title={basic.title}
iconUrl={basic.iconUrl}
linkedUrl={basic.linkedUrl}
/>
);
} else if ('custom' in tile.type) {
if (key === 'weather') {
return (
<BasicTile
<WeatherTile
key={key}
title={basic.title}
iconUrl={basic.iconUrl}
linkedUrl={basic.linkedUrl}
api={props.api}
/>
);
} else if ('custom' in tile.type) {
if (key === 'weather') {
return (
<WeatherTile
key={key}
api={props.api}
weather={props.weather}
location={props.location}
/>
);
} else if (key === 'clock') {
const location = 'nearest-area' in props.weather ? props.weather['nearest-area'][0] : '';
return (
<ClockTile key={key} location={location} />
);
}
} else {
return <CustomTile key={key} />;
} else if (key === 'clock') {
const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : '';
return (
<ClockTile key={key} location={location} />
);
}
});
} else {
return <CustomTile key={key} />;
}
});
return (
<React.Fragment>{tiles}</React.Fragment>
);
}
return (
<>{tiles}</>
);
}
export default Tiles;

View File

@ -2,6 +2,7 @@ import React from 'react';
import moment from 'moment';
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
import ErrorBoundary from '~/views/components/ErrorBoundary';
import { withLaunchState } from '~/logic/state/launch';
import Tile from './tile';
@ -34,7 +35,7 @@ const imperialCountries = [
'Liberia',
];
export default class WeatherTile extends React.Component {
class WeatherTile extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -289,3 +290,4 @@ export default class WeatherTile extends React.Component {
}
}
export default withLaunchState(WeatherTile);

View File

@ -20,6 +20,7 @@ import { ImageInput } from '~/views/components/ImageInput';
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
import { resourceFromPath } from '~/logic/lib/group';
import GroupSearch from '~/views/components/GroupSearch';
import useContactState from '~/logic/state/contacts';
const formSchema = Yup.object({
nickname: Yup.string(),
@ -41,7 +42,8 @@ const emptyContact = {
};
export function EditProfile(props: any): ReactElement {
const { contact, ship, api, isPublic } = props;
const { contact, ship, api } = props;
const isPublic = useContactState(state => state.isContactPublic);
const history = useHistory();
if (contact) {
contact.isPublic = isPublic;

View File

@ -17,17 +17,19 @@ import { EditProfile } from './EditProfile';
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
import { uxToHex } from '~/logic/lib/util';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import useContactState from '~/logic/state/contacts';
export function Profile(props: any): ReactElement {
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
hideAvatars
}));
const history = useHistory();
const nackedContacts = useContactState(state => state.nackedContacts);
if (!props.ship) {
return null;
}
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
const { contact, hasLoaded, isPublic, isEdit, ship } = props;
const nacked = nackedContacts.has(ship);
useEffect(() => {
@ -109,7 +111,6 @@ export function Profile(props: any): ReactElement {
s3={props.s3}
api={props.api}
associations={props.associations}
isPublic={isPublic}
/>
) : (
<ViewProfile
@ -117,7 +118,6 @@ export function Profile(props: any): ReactElement {
nacked={nacked}
ship={ship}
contact={contact}
isPublic={isPublic}
associations={props.associations}
/>
) }

View File

@ -14,12 +14,15 @@ import RichText from '~/views/components/RichText';
import { GroupLink } from '~/views/components/GroupLink';
import { lengthOrder } from '~/logic/lib/util';
import useLocalState from '~/logic/state/local';
import useContactState from '~/logic/state/contacts';
export function ViewProfile(props: any): ReactElement {
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
hideNicknames
}));
const { api, contact, nacked, isPublic, ship, associations, groups } = props;
const { api, contact, nacked, ship, associations, groups } = props;
const isPublic = useContactState(state => state.isContactPublic);
return (
<>

View File

@ -21,7 +21,6 @@ export default function ProfileScreen(props: any) {
render={({ match }) => {
const ship = match.params.ship;
const isEdit = match.url.includes('edit');
const isPublic = props.isContactPublic;
const contact = contacts?.[ship];
return (
@ -45,7 +44,6 @@ export default function ProfileScreen(props: any) {
api={props.api}
s3={props.s3}
isEdit={isEdit}
isPublic={isPublic}
/>
</Box>
</Box>

View File

@ -20,6 +20,7 @@ import useContactState from '~/logic/state/contacts';
import useGroupState from '~/logic/state/groups';
import useHarkState from '~/logic/state/hark';
import useInviteState from '~/logic/state/invite';
import useLaunchState from '~/logic/state/launch';
interface OmniboxProps {
associations: Associations;
@ -44,6 +45,7 @@ export function Omnibox(props: OmniboxProps) {
const contactState = useContactState(state => state.contacts);
const notifications = useHarkState(state => state.notifications);
const invites = useInviteState(state => state.invites);
const tiles = useLaunchState(state => state.tiles);
const contacts = useMemo(() => {
const maybeShip = `~${deSig(query)}`;
@ -61,11 +63,11 @@ export function Omnibox(props: OmniboxProps) {
return makeIndex(
contacts,
props.associations,
props.tiles,
tiles,
selectedGroup,
groups
);
}, [location.pathname, contacts, props.associations, groups, props.tiles]);
}, [location.pathname, contacts, props.associations, groups, tiles]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle();