Skip to content

Commit 9885c49

Browse files
Better error handling in ChannelList
1 parent 66694d1 commit 9885c49

File tree

9 files changed

+526
-144
lines changed

9 files changed

+526
-144
lines changed

src/components/ChannelList.js

Lines changed: 282 additions & 115 deletions
Large diffs are not rendered by default.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
3+
import styled from '@stream-io/styled-components';
4+
import { Spinner } from './Spinner';
5+
6+
const Container = styled.View`
7+
width: 100%;
8+
justify-content: center;
9+
align-items: center;
10+
${({ theme }) => theme.channelListFooterLoadingIndicator.container.css}
11+
`;
12+
13+
export const ChannelListFooterLoadingIndicator = () => (
14+
<Container>
15+
<Spinner />
16+
</Container>
17+
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import styled from '@stream-io/styled-components';
3+
import { withTranslationContext } from '../context';
4+
import PropTypes from 'prop-types';
5+
6+
const Container = styled.TouchableOpacity`
7+
width: 100%;
8+
align-items: center;
9+
justify-content: center;
10+
background-color: #fae6e8;
11+
${({ theme }) => theme.channelListHeaderErrorIndicator.container.css}
12+
`;
13+
14+
const ErrorText = styled.Text`
15+
color: red;
16+
font-size: 12;
17+
font-weight: bold;
18+
padding: 3px;
19+
${({ theme }) => theme.channelListHeaderErrorIndicator.errorText.css}
20+
`;
21+
22+
const ChannelListHeaderErrorIndicator = withTranslationContext(
23+
({ onPress, t }) => (
24+
<Container
25+
onPress={() => {
26+
onPress && onPress();
27+
}}
28+
>
29+
<ErrorText>{t('Error while loading, please reload/refresh')}</ErrorText>
30+
</Container>
31+
),
32+
);
33+
34+
ChannelListHeaderErrorIndicator.propTypes = {
35+
onPress: PropTypes.func,
36+
};
37+
38+
export { ChannelListHeaderErrorIndicator };
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import styled from '@stream-io/styled-components';
3+
import { withTranslationContext } from '../context';
4+
5+
const Container = styled.View`
6+
width: 100%;
7+
align-items: center;
8+
justify-content: center;
9+
background-color: #fae6e8;
10+
${({ theme }) => theme.channelListHeaderErrorIndicator.container.css}
11+
`;
12+
13+
const ErrorText = styled.Text`
14+
color: red;
15+
font-size: 12;
16+
font-weight: bold;
17+
padding: 3px;
18+
${({ theme }) => theme.channelListHeaderErrorIndicator.errorText.css}
19+
`;
20+
21+
export const ChannelListHeaderNetworkDownIndicator = withTranslationContext(
22+
({ t }) => (
23+
<Container>
24+
<ErrorText>{t('Connection failure, reconnecting now ...')}</ErrorText>
25+
</Container>
26+
),
27+
);

src/components/ChannelListMessenger.js

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import PropTypes from 'prop-types';
44
import { ChannelPreview } from './ChannelPreview';
55
import { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
66
import { withChatContext } from '../context';
7+
import { ChannelListHeaderNetworkDownIndicator } from './ChannelListHeaderNetworkDownIndicator';
8+
import { ChannelListHeaderErrorIndicator } from './ChannelListHeaderErrorIndicator';
79

810
import { LoadingIndicator } from './LoadingIndicator';
911
import { LoadingErrorIndicator } from './LoadingErrorIndicator';
1012
import { EmptyStateIndicator } from './EmptyStateIndicator';
13+
import { ChannelListFooterLoadingIndicator } from './ChannelListFooterLoadingIndicator';
1114

1215
/**
1316
* ChannelListMessenger - UI component for list of channels, allowing you to select the channel you want to open
@@ -44,8 +47,39 @@ const ChannelListMessenger = withChatContext(
4447
PropTypes.node,
4548
PropTypes.elementType,
4649
]),
50+
/**
51+
* The indicator to display network-down error at top of list, if there is connectivity issue
52+
* Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderNetworkDownIndicator)
53+
*/
54+
HeaderNetworkDownIndicator: PropTypes.oneOfType([
55+
PropTypes.node,
56+
PropTypes.elementType,
57+
]),
58+
/**
59+
* The indicator to display error at top of list, if there was an error loading some page/channels after the first page.
60+
* Default: [ChannelListHeaderErrorIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderErrorIndicator)
61+
*/
62+
HeaderErrorIndicator: PropTypes.oneOfType([
63+
PropTypes.node,
64+
PropTypes.elementType,
65+
]),
66+
/**
67+
* Loading indicator to display at bottom of the list, while loading further pages.
68+
* Default: [ChannelListFooterLoadingIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListFooterLoadingIndicator)
69+
*/
70+
FooterLoadingIndicator: PropTypes.oneOfType([
71+
PropTypes.node,
72+
PropTypes.elementType,
73+
]),
74+
/** Remove all the existing channels from UI and load fresh channels. */
75+
reloadList: PropTypes.func,
4776
/** Loads next page of channels in channels object, which is present here as prop */
4877
loadNextPage: PropTypes.func,
78+
/**
79+
* Refresh the channel list. Its similar to `reloadList`, but it doesn't wipe out existing channels
80+
* from UI before loading new set of channels.
81+
*/
82+
refreshList: PropTypes.func,
4983
/**
5084
* For flatlist
5185
* @see See loeadMoreThreshold [doc](https://facebook.github.io/react-native/docs/flatlist#onendreachedthreshold)
@@ -55,6 +89,10 @@ const ChannelListMessenger = withChatContext(
5589
error: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
5690
/** If channels are being queries. LoadingIndicator will be displayed if true */
5791
loadingChannels: PropTypes.bool,
92+
/** If channel list is being refreshed. Loader at top of the list will be displayed if true. */
93+
refreshing: PropTypes.bool,
94+
/** If further channels are being loadded. Loader will be shown at bottom of the list */
95+
loadingNextPage: PropTypes.bool,
5896
/**
5997
* Besides existing (default) UX behaviour of underlying flatlist of ChannelListMessenger component, if you want
6098
* to attach some additional props to un derlying flatlist, you can add it to following prop.
@@ -89,6 +127,9 @@ const ChannelListMessenger = withChatContext(
89127
Preview: ChannelPreviewMessenger,
90128
LoadingIndicator,
91129
LoadingErrorIndicator,
130+
HeaderNetworkDownIndicator: ChannelListHeaderNetworkDownIndicator,
131+
HeaderErrorIndicator: ChannelListHeaderErrorIndicator,
132+
FooterLoadingIndicator: ChannelListFooterLoadingIndicator,
92133
EmptyStateIndicator,
93134
// https://github.com/facebook/react-native/blob/a7a7970e543959e9db5281914d5f132beb01db8d/Libraries/Lists/VirtualizedList.js#L466
94135
loadMoreThreshold: 2,
@@ -102,38 +143,74 @@ const ChannelListMessenger = withChatContext(
102143

103144
renderLoadingError = () => {
104145
const Indicator = this.props.LoadingErrorIndicator;
105-
return <Indicator error={this.props.error} listType="channel" />;
146+
return (
147+
<Indicator
148+
error={this.props.error}
149+
retry={this.props.reloadList}
150+
listType="channel"
151+
loadNextPage={this.props.loadNextPage}
152+
/>
153+
);
106154
};
107155

108156
renderEmptyState = () => {
109157
const Indicator = this.props.EmptyStateIndicator;
110158
return <Indicator listType="channel" />;
111159
};
112160

161+
renderHeaderIndicator = () => {
162+
const { isOnline, error, refreshList } = this.props;
163+
164+
if (!isOnline) {
165+
const HeaderNetworkDownIndicator = this.props
166+
.HeaderNetworkDownIndicator;
167+
return <HeaderNetworkDownIndicator />;
168+
}
169+
170+
if (error) {
171+
const HeaderErrorIndicator = this.props.HeaderErrorIndicator;
172+
return <HeaderErrorIndicator onPress={refreshList} />;
173+
}
174+
};
175+
113176
renderChannels = () => (
114-
<FlatList
115-
ref={(flRef) => {
116-
this.props.setFlatListRef && this.props.setFlatListRef(flRef);
117-
}}
118-
data={this.props.channels}
119-
onEndReached={this.props.loadNextPage}
120-
onEndReachedThreshold={this.props.loadMoreThreshold}
121-
ListEmptyComponent={this.renderEmptyState}
122-
renderItem={({ item: channel }) => (
123-
<ChannelPreview
124-
{...this.props}
125-
key={channel.cid}
126-
channel={channel}
127-
Preview={this.props.Preview}
128-
/>
129-
)}
130-
keyExtractor={(item) => item.cid}
131-
{...this.props.additionalFlatListProps}
132-
/>
177+
<>
178+
{this.renderHeaderIndicator()}
179+
<FlatList
180+
ref={(flRef) => {
181+
this.props.setFlatListRef && this.props.setFlatListRef(flRef);
182+
}}
183+
data={this.props.channels}
184+
onEndReached={() => this.props.loadNextPage(false)}
185+
onRefresh={() => this.props.refreshList()}
186+
refreshing={this.props.refreshing}
187+
onEndReachedThreshold={this.props.loadMoreThreshold}
188+
ListEmptyComponent={this.renderEmptyState}
189+
ListFooterComponent={() => {
190+
if (this.props.loadingNextPage) {
191+
const FooterLoadingIndicator = this.props.FooterLoadingIndicator;
192+
193+
return <FooterLoadingIndicator />;
194+
}
195+
196+
return null;
197+
}}
198+
renderItem={({ item: channel }) => (
199+
<ChannelPreview
200+
{...this.props}
201+
key={channel.cid}
202+
channel={channel}
203+
Preview={this.props.Preview}
204+
/>
205+
)}
206+
keyExtractor={(item) => item.cid}
207+
{...this.props.additionalFlatListProps}
208+
/>
209+
</>
133210
);
134211

135212
render() {
136-
if (this.props.error) {
213+
if (this.props.error && this.props.channels.length === 0) {
137214
return this.renderLoadingError();
138215
} else if (this.props.loadingChannels) {
139216
return this.renderLoading();

src/components/LoadingErrorIndicator.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,58 @@
11
import React from 'react';
2-
import { Text } from 'react-native';
2+
import styled from '@stream-io/styled-components';
33
import PropTypes from 'prop-types';
44
import { withTranslationContext } from '../context';
55

6-
const LoadingErrorIndicator = ({ listType, t }) => {
6+
const Container = styled.TouchableOpacity`
7+
height: 100%;
8+
justify-content: center;
9+
align-items: center;
10+
${({ theme }) => theme.loadingErrorIndicator.container.css}
11+
`;
12+
13+
const ErrorText = styled.Text`
14+
margin-top: 20px;
15+
font-size: 14px;
16+
font-weight: 600;
17+
${({ theme }) => theme.loadingErrorIndicator.errorText.css}
18+
`;
19+
20+
const RetryText = styled.Text`
21+
font-size: 30px;
22+
font-weight: 600;
23+
${({ theme }) => theme.loadingErrorIndicator.retryText.css}
24+
`;
25+
26+
const LoadingErrorIndicator = ({ listType, retry, t }) => {
727
let Loader;
828
switch (listType) {
929
case 'channel':
10-
Loader = <Text>{t('Error loading channel list ...')}</Text>;
30+
Loader = (
31+
<Container
32+
onPress={() => {
33+
retry && retry();
34+
}}
35+
>
36+
<ErrorText>{t('Error loading channel list ...')}</ErrorText>
37+
<RetryText></RetryText>
38+
</Container>
39+
);
1140
break;
1241
case 'message':
13-
Loader = <Text>{t('Error loading messages for this channel ...')}</Text>;
42+
Loader = (
43+
<Container>
44+
<ErrorText>
45+
{t('Error loading messages for this channel ...')}
46+
</ErrorText>
47+
</Container>
48+
);
1449
break;
1550
default:
16-
Loader = <Text>{t('Error loading')}</Text>;
51+
Loader = (
52+
<Container>
53+
<ErrorText>{t('Error loading')}</ErrorText>
54+
</Container>
55+
);
1756
break;
1857
}
1958

@@ -22,6 +61,8 @@ const LoadingErrorIndicator = ({ listType, t }) => {
2261

2362
LoadingErrorIndicator.propTypes = {
2463
listType: PropTypes.oneOf(['channel', 'message', 'default']),
64+
// Calls the retry handler.
65+
retry: PropTypes.func,
2566
};
2667

2768
const LoadingErrorIndicatorWithContext = withTranslationContext(

src/components/Spinner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { themed } from '../styles/theme';
55

66
const AnimatedView = Animated.createAnimatedComponent(View);
77

8-
const Circle = styled(AnimatedView)`
8+
export const Circle = styled(AnimatedView)`
99
height: 30px;
1010
width: 30px;
1111
margin: 5px;

src/components/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export { Message } from './Message';
1717
export { MessageNotification } from './MessageNotification';
1818
export { MessageSystem } from './MessageSystem';
1919
export { ReactionList } from './ReactionList';
20-
export { Spinner } from './Spinner';
20+
export { Circle, Spinner } from './Spinner';
2121
export { SuggestionsProvider } from './SuggestionsProvider';
2222
export { UploadProgressIndicator } from './UploadProgressIndicator';
2323
export { Attachment } from './Attachment';

src/styles/theme.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,22 @@ export const defaultTheme = {
2929
text: {},
3030
fallback: {},
3131
},
32-
32+
channelListHeaderErrorIndicator: {
33+
container: {},
34+
errorText: {},
35+
},
36+
channelListHeaderNetworkDownIndicator: {
37+
container: {},
38+
errorText: {},
39+
},
40+
channelListFooterLoadingIndicator: {
41+
container: {},
42+
},
43+
loadingErrorIndicator: {
44+
container: {},
45+
errorText: {},
46+
retryText: {},
47+
},
3348
channelPreview: {
3449
container: {},
3550
details: {},

0 commit comments

Comments
 (0)