Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start type checking MessageList's props. #4465

Merged
merged 16 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
17748d8
MessageList: Stop accepting `typingUsers` prop from parent.
chrisbobbe Jun 16, 2021
a74cb23
MessageList: Stop accepting `fetching` prop from parent.
chrisbobbe Jun 16, 2021
134dc95
ChatScreen [nfc]: Pass messages and initialScrollMessageId to Message…
chrisbobbe Feb 3, 2021
9f9dc2d
MessageList [nfc]: Require `messages` and `initialScrollMessageId`.
chrisbobbe Feb 3, 2021
6ed1918
messagesSelectors: Add `getHtmlPieceDescriptorsForMessages`.
chrisbobbe Feb 8, 2021
d7d6d35
MessageList [nfc]: Stop using `getHtmlPieceDescriptorsForShownMessages`.
chrisbobbe Feb 8, 2021
66d16ab
SearchMessagesCard: Use memoized function to get HTML piece descriptors.
chrisbobbe Feb 8, 2021
f803542
SearchMessagesCard [nfc]: Inline `htmlPieceDescriptors`.
chrisbobbe Feb 8, 2021
13c91b6
MessageList [nfc]: Express a conditional a different way.
chrisbobbe Feb 8, 2021
f7519f2
SearchMessagesCard [nfc]: Don't pass htmlPieceDescriptorsForShownMess…
chrisbobbe Jun 16, 2021
c655202
MessageList [nfc]: Remove outer prop htmlPieceDescriptorsForShownMess…
chrisbobbe Jun 16, 2021
923f633
MessageList types: Copy `startEditMessage` to `OuterProps`, where it …
chrisbobbe Feb 3, 2021
932173a
action sheet: Add type wrapper for `connectActionSheet`.
chrisbobbe Feb 3, 2021
767fbf9
MessageList: Use our type-wrapper for `connectActionSheet`.
chrisbobbe Feb 3, 2021
ac97c91
MessageList: Fix type-checking of props.
chrisbobbe Feb 3, 2021
21fd83a
MessageList [nfc]: Spread `OuterProps` in `Props`, to remove repeated…
chrisbobbe Jun 16, 2021
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
21 changes: 15 additions & 6 deletions src/chat/ChatScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { canSendToNarrow } from '../utils/narrow';
import { getLoading, getSession } from '../directSelectors';
import { getFetchingForNarrow } from './fetchingSelectors';
import { getShownMessagesForNarrow, isNarrowValid as getIsNarrowValid } from './narrowsSelectors';
import { getFirstUnreadIdInNarrow } from '../message/messageSelectors';

type Props = $ReadOnly<{|
navigation: AppNavigationProp<'chat'>,
Expand All @@ -42,7 +43,7 @@ const componentStyles = createStyleSheet({
* more details, including how Redux is kept up-to-date during the
* whole process.
*/
const useFetchMessages = args => {
const useMessagesWithFetch = args => {
const { narrow } = args;

const dispatch = useDispatch();
Expand All @@ -52,9 +53,9 @@ const useFetchMessages = args => {
const loading = useSelector(getLoading);
const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
const isFetching = fetching.older || fetching.newer || loading;
const haveNoMessages = useSelector(
state => getShownMessagesForNarrow(state, narrow).length === 0,
);
const messages = useSelector(state => getShownMessagesForNarrow(state, narrow));
const haveNoMessages = messages.length === 0;
const firstUnreadIdInNarrow = useSelector(state => getFirstUnreadIdInNarrow(state, narrow));

// This could live in state, but then we'd risk pointless rerenders;
// we only use it in our `useEffect` callbacks. Using `useRef` is
Expand Down Expand Up @@ -93,7 +94,7 @@ const useFetchMessages = args => {
// `eventQueueId` needed here because it affects `shouldFetchWhenNextFocused`.
}, [isFocused, eventQueueId, fetch]);

return { fetchError, isFetching, haveNoMessages };
return { fetchError, isFetching, messages, haveNoMessages, firstUnreadIdInNarrow };
};

export default function ChatScreen(props: Props) {
Expand All @@ -106,7 +107,13 @@ export default function ChatScreen(props: Props) {

const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));

const { fetchError, isFetching, haveNoMessages } = useFetchMessages({ narrow });
const {
fetchError,
isFetching,
messages,
haveNoMessages,
firstUnreadIdInNarrow,
} = useMessagesWithFetch({ narrow });

const showMessagePlaceholders = haveNoMessages && isFetching;
const sayNoMessages = haveNoMessages && !isFetching;
Expand All @@ -128,6 +135,8 @@ export default function ChatScreen(props: Props) {
return (
<MessageList
narrow={narrow}
messages={messages}
initialScrollMessageId={firstUnreadIdInNarrow}
showMessagePlaceholders={showMessagePlaceholders}
startEditMessage={setEditMessage}
/>
Expand Down
14 changes: 5 additions & 9 deletions src/message/messageSelectors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* @flow strict-local */
import { createSelector } from 'reselect';
import { createSelector, defaultMemoize } from 'reselect';

import type { Message, Narrow, HtmlPieceDescriptor, Selector } from '../types';
import type { Message, Outbox, Narrow, Selector } from '../types';
import {
getAllNarrows,
getFlags,
Expand Down Expand Up @@ -61,13 +61,9 @@ export const getPrivateMessages: Selector<Message[]> = createSelector(
},
);

export const getHtmlPieceDescriptorsForShownMessages: Selector<
HtmlPieceDescriptor[],
Narrow,
> = createSelector(
(state, narrow) => narrow,
getShownMessagesForNarrow,
(narrow, messages) => getHtmlPieceDescriptors(messages, narrow),
export const getHtmlPieceDescriptorsForMessages = defaultMemoize(
(messages: $ReadOnlyArray<Message | Outbox>, narrow: Narrow) =>
getHtmlPieceDescriptors(messages, narrow),
);

export const getFirstUnreadIdInNarrow: Selector<number | null, Narrow> = createSelector(
Expand Down
29 changes: 29 additions & 0 deletions src/react-native-action-sheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* @flow strict-local */
import type { ComponentType, ElementConfig } from 'react';
import { connectActionSheet as connectActionSheetInner } from '@expo/react-native-action-sheet';

import type { BoundedDiff } from './generics';

/* eslint-disable flowtype/generic-spacing */

export type ShowActionSheetWithOptions = (
{ options: string[], cancelButtonIndex: number },
(number) => void,
) => void;

/**
* Exactly like the `connectActionSheet` in
* `react-native-action-sheet` upstream, but more typed.
gnprice marked this conversation as resolved.
Show resolved Hide resolved
*/
export function connectActionSheet<P, C: ComponentType<P>>(
WrappedComponent: C,
): ComponentType<
$ReadOnly<
BoundedDiff<
$Exact<ElementConfig<C>>,
{| showActionSheetWithOptions: ShowActionSheetWithOptions |},
>,
>,
> {
return connectActionSheetInner(WrappedComponent);
}
15 changes: 6 additions & 9 deletions src/search/SearchMessagesCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { createStyleSheet } from '../styles';
import { LoadingIndicator, SearchEmptyState } from '../common';
import { HOME_NARROW } from '../utils/narrow';
import MessageList from '../webview/MessageList';
import getHtmlPieceDescriptors from '../message/getHtmlPieceDescriptors';
import { NULL_ARRAY } from '../nullObjects';

const styles = createStyleSheet({
results: {
Expand All @@ -23,8 +21,6 @@ type Props = $ReadOnly<{|
|}>;

export default class SearchMessagesCard extends PureComponent<Props> {
static NOT_FETCHING = { older: false, newer: false };

render() {
const { isFetching, messages } = this.props;

Expand All @@ -44,18 +40,19 @@ export default class SearchMessagesCard extends PureComponent<Props> {
return <SearchEmptyState text="No results" />;
}

const htmlPieceDescriptors = getHtmlPieceDescriptors(messages, HOME_NARROW);
// TODO: This is kind of a hack.
const narrow = HOME_NARROW;

return (
<View style={styles.results}>
<MessageList
initialScrollMessageId={messages[0].id}
messages={messages}
narrow={HOME_NARROW}
htmlPieceDescriptorsForShownMessages={htmlPieceDescriptors}
fetching={SearchMessagesCard.NOT_FETCHING}
narrow={narrow}
showMessagePlaceholders={false}
typingUsers={NULL_ARRAY}
// TODO: handle editing a message from the search results,
// or make this prop optional
startEditMessage={() => undefined}
/>
</View>
);
Expand Down
107 changes: 46 additions & 61 deletions src/webview/MessageList.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* @flow strict-local */
import React, { Component } from 'react';
import React, { Component, type ComponentType } from 'react';
import { Platform, NativeModules } from 'react-native';
import { WebView } from 'react-native-webview';
import type { WebViewNavigation } from 'react-native-webview';
import { connectActionSheet } from '@expo/react-native-action-sheet';

import { connectActionSheet } from '../react-native-action-sheet';
import type {
AlertWordsState,
Auth,
Expand Down Expand Up @@ -34,20 +34,18 @@ import {
getAllImageEmojiById,
getCurrentTypingUsers,
getDebug,
getHtmlPieceDescriptorsForShownMessages,
getFlags,
getFetchingForNarrow,
getFirstUnreadIdInNarrow,
getMute,
getMutedUsers,
getOwnUser,
getSettings,
getSubscriptions,
getShownMessagesForNarrow,
getRealm,
} from '../selectors';
import { withGetText } from '../boot/TranslationProvider';
import type { ShowActionSheetWithOptions } from '../message/messageActionSheet';
import { getHtmlPieceDescriptorsForMessages } from '../message/messageSelectors';
import type { WebViewInboundEvent } from './generateInboundEvents';
import type { WebViewOutboundEvent } from './handleOutboundEvents';
import getHtml from './html/html';
Expand Down Expand Up @@ -86,25 +84,28 @@ export type BackgroundData = $ReadOnly<{|
twentyFourHourTime: boolean,
|}>;

type OuterProps = {|
narrow: Narrow,
messages: $ReadOnlyArray<Message | Outbox>,
initialScrollMessageId: number | null,
showMessagePlaceholders: boolean,
startEditMessage: (editMessage: EditMessage) => void,
|};

type SelectorProps = {|
// Data independent of the particular narrow or messages we're displaying.
backgroundData: BackgroundData,

// The remaining props contain data specific to the particular narrow or
// particular messages we're displaying. Data that's independent of those
// should go in `BackgroundData`, above.
initialScrollMessageId: number | null,
fetching: Fetching,
messages: $ReadOnlyArray<Message | Outbox>,
htmlPieceDescriptorsForShownMessages: HtmlPieceDescriptor[],
typingUsers: $ReadOnlyArray<UserOrBot>,
|};

// TODO get a type for `connectActionSheet` so this gets fully type-checked.
export type Props = $ReadOnly<{|
narrow: Narrow,
showMessagePlaceholders: boolean,
startEditMessage: (editMessage: EditMessage) => void,
...OuterProps,

dispatch: Dispatch,
...SelectorProps,
Expand Down Expand Up @@ -145,7 +146,7 @@ const assetsUrl =
*/
const webviewAssetsUrl = new URL('webview/', assetsUrl);

class MessageList extends Component<Props> {
class MessageListInner extends Component<Props> {
static contextType = ThemeContext;
context: ThemeData;

Expand Down Expand Up @@ -299,52 +300,36 @@ class MessageList extends Component<Props> {
}
}

type OuterProps = {|
narrow: Narrow,
showMessagePlaceholders: boolean,

/* Remaining props are derived from `narrow` by default. */

messages?: Message[],
htmlPieceDescriptorsForShownMessages?: HtmlPieceDescriptor[],
initialScrollMessageId?: number | null,

/* Passing these two from the parent is kind of a hack; search uses it
to hard-code some behavior. */
fetching?: Fetching,
typingUsers?: UserOrBot[],
|};

export default connect<SelectorProps, _, _>((state, props: OuterProps) => {
// TODO Ideally this ought to be a caching selector that doesn't change
// when the inputs don't. Doesn't matter in a practical way here, because
// we have a `shouldComponentUpdate` that doesn't look at this prop... but
// it'd be better to set an example of the right general pattern.
const backgroundData: BackgroundData = {
alertWords: state.alertWords,
allImageEmojiById: getAllImageEmojiById(state),
auth: getAuth(state),
debug: getDebug(state),
flags: getFlags(state),
mute: getMute(state),
mutedUsers: getMutedUsers(state),
ownUser: getOwnUser(state),
subscriptions: getSubscriptions(state),
theme: getSettings(state).theme,
twentyFourHourTime: getRealm(state).twentyFourHourTime,
};

return {
backgroundData,
initialScrollMessageId:
props.initialScrollMessageId !== undefined
? props.initialScrollMessageId
: getFirstUnreadIdInNarrow(state, props.narrow),
fetching: props.fetching || getFetchingForNarrow(state, props.narrow),
messages: props.messages || getShownMessagesForNarrow(state, props.narrow),
htmlPieceDescriptorsForShownMessages:
props.htmlPieceDescriptorsForShownMessages
|| getHtmlPieceDescriptorsForShownMessages(state, props.narrow),
typingUsers: props.typingUsers || getCurrentTypingUsers(state, props.narrow),
};
})(connectActionSheet(withGetText(MessageList)));
const MessageList: ComponentType<OuterProps> = connect<SelectorProps, _, _>(
(state, props: OuterProps) => {
// TODO Ideally this ought to be a caching selector that doesn't change
// when the inputs don't. Doesn't matter in a practical way here, because
// we have a `shouldComponentUpdate` that doesn't look at this prop... but
// it'd be better to set an example of the right general pattern.
const backgroundData: BackgroundData = {
alertWords: state.alertWords,
allImageEmojiById: getAllImageEmojiById(state),
auth: getAuth(state),
debug: getDebug(state),
flags: getFlags(state),
mute: getMute(state),
mutedUsers: getMutedUsers(state),
ownUser: getOwnUser(state),
subscriptions: getSubscriptions(state),
theme: getSettings(state).theme,
twentyFourHourTime: getRealm(state).twentyFourHourTime,
};

return {
backgroundData,
fetching: getFetchingForNarrow(state, props.narrow),
htmlPieceDescriptorsForShownMessages: getHtmlPieceDescriptorsForMessages(
props.messages,
props.narrow,
),
typingUsers: getCurrentTypingUsers(state, props.narrow),
};
},
)(connectActionSheet(withGetText(MessageListInner)));

export default MessageList;