Skip to content

Commit 28910e9

Browse files
authored
Merge pull request #1570 from lucas-neuhaus-dev/add-last-read
Add Unread Action Indicator
2 parents 40b31e0 + be9ba30 commit 28910e9

File tree

4 files changed

+126
-8
lines changed

4 files changed

+126
-8
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import {Animated, View} from 'react-native';
3+
import PropTypes from 'prop-types';
4+
import styles from '../styles/styles';
5+
import Text from './Text';
6+
7+
const propTypes = {
8+
// Animated opacity
9+
// eslint-disable-next-line react/forbid-prop-types
10+
animatedOpacity: PropTypes.object.isRequired,
11+
};
12+
13+
const UnreadActionIndicator = props => (
14+
<Animated.View style={[
15+
styles.unreadIndicatorContainer,
16+
{opacity: props.animatedOpacity},
17+
]}
18+
>
19+
<View style={styles.unreadIndicatorLine} />
20+
<Text style={styles.unreadIndicatorText}>
21+
NEW
22+
</Text>
23+
</Animated.View>
24+
);
25+
26+
UnreadActionIndicator.propTypes = propTypes;
27+
UnreadActionIndicator.displayName = 'UnreadActionIndicator';
28+
29+
export default UnreadActionIndicator;

src/pages/home/report/ReportActionsView.js

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import React from 'react';
2-
import {View, Keyboard, AppState} from 'react-native';
2+
import {
3+
Animated,
4+
View,
5+
Keyboard,
6+
AppState,
7+
} from 'react-native';
38
import PropTypes from 'prop-types';
49
import _ from 'underscore';
510
import lodashGet from 'lodash.get';
611
import {withOnyx} from 'react-native-onyx';
712
import Text from '../../../components/Text';
13+
import UnreadActionIndicator from '../../../components/UnreadActionIndicator';
814
import {fetchActions, updateLastReadActionID} from '../../../libs/actions/Report';
915
import ONYXKEYS from '../../../ONYXKEYS';
1016
import ReportActionItem from './ReportActionItem';
@@ -23,6 +29,12 @@ const propTypes = {
2329

2430
/* Onyx Props */
2531

32+
// The report currently being looked at
33+
report: PropTypes.shape({
34+
// Number of actions unread
35+
unreadActionCount: PropTypes.number,
36+
}),
37+
2638
// Array of report actions for this report
2739
reportActions: PropTypes.objectOf(PropTypes.shape(ReportActionPropTypes)),
2840

@@ -34,6 +46,9 @@ const propTypes = {
3446
};
3547

3648
const defaultProps = {
49+
report: {
50+
unreadActionCount: 0,
51+
},
3752
reportActions: {},
3853
session: {},
3954
};
@@ -46,8 +61,17 @@ class ReportActionsView extends React.Component {
4661
this.scrollToListBottom = this.scrollToListBottom.bind(this);
4762
this.recordMaxAction = this.recordMaxAction.bind(this);
4863
this.onVisibilityChange = this.onVisibilityChange.bind(this);
49-
this.sortedReportActions = this.updateSortedReportActions();
64+
65+
this.sortedReportActions = [];
5066
this.timers = [];
67+
this.unreadIndicatorOpacity = new Animated.Value(1);
68+
69+
// Helper variable that keeps track of the unread action count before it updates to zero
70+
this.unreadActionCount = 0;
71+
72+
// Helper variable that prevents the unread indicator to show up for new messages
73+
// received while the report is still active
74+
this.shouldShowUnreadActionIndicator = true;
5175

5276
this.state = {
5377
refetchNeeded: true,
@@ -145,6 +169,31 @@ class ReportActionsView extends React.Component {
145169
this.setState({refetchNeeded});
146170
}
147171

172+
/**
173+
* Checks if the unreadActionIndicator should be shown.
174+
* If it does, starts a timeout for the fading out animation and creates
175+
* a flag to not show it again if the report is still open
176+
*/
177+
setUpUnreadActionIndicator() {
178+
if (!this.props.isActiveReport || !this.shouldShowUnreadActionIndicator) {
179+
return;
180+
}
181+
182+
this.unreadActionCount = this.props.report.unreadActionCount;
183+
184+
if (this.unreadActionCount > 0) {
185+
this.unreadIndicatorOpacity = new Animated.Value(1);
186+
this.timers.push(setTimeout(() => {
187+
Animated.timing(this.unreadIndicatorOpacity, {
188+
toValue: 0,
189+
useNativeDriver: false,
190+
}).start();
191+
}, 3000));
192+
}
193+
194+
this.shouldShowUnreadActionIndicator = false;
195+
}
196+
148197
/**
149198
* Updates and sorts the report actions by sequence number
150199
*/
@@ -239,12 +288,21 @@ class ReportActionsView extends React.Component {
239288
needsLayoutCalculation,
240289
}) {
241290
return (
242-
<ReportActionItem
243-
action={item.action}
244-
displayAsGroup={this.isConsecutiveActionMadeByPreviousActor(index)}
245-
onLayout={onLayout}
246-
needsLayoutCalculation={needsLayoutCalculation}
247-
/>
291+
292+
// Using <View /> instead of a Fragment because there is a difference between how
293+
// <InvertedFlatList /> are implemented on native and web/desktop which leads to
294+
// the unread indicator on native to render below the message instead of above it.
295+
<View>
296+
{this.unreadActionCount > 0 && index === this.unreadActionCount - 1 && (
297+
<UnreadActionIndicator animatedOpacity={this.unreadIndicatorOpacity} />
298+
)}
299+
<ReportActionItem
300+
action={item.action}
301+
displayAsGroup={this.isConsecutiveActionMadeByPreviousActor(index)}
302+
onLayout={onLayout}
303+
needsLayoutCalculation={needsLayoutCalculation}
304+
/>
305+
</View>
248306
);
249307
}
250308

@@ -263,6 +321,7 @@ class ReportActionsView extends React.Component {
263321
);
264322
}
265323

324+
this.setUpUnreadActionIndicator();
266325
this.updateSortedReportActions();
267326
return (
268327
<InvertedFlatList
@@ -281,6 +340,9 @@ ReportActionsView.propTypes = propTypes;
281340
ReportActionsView.defaultProps = defaultProps;
282341

283342
export default withOnyx({
343+
report: {
344+
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
345+
},
284346
reportActions: {
285347
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
286348
canEvict: props => !props.isActiveReport,

src/styles/styles.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,32 @@ const styles = {
10071007
marginLeft: 8,
10081008
},
10091009

1010+
unreadIndicatorContainer: {
1011+
position: 'absolute',
1012+
top: -10,
1013+
left: 0,
1014+
width: '100%',
1015+
height: 20,
1016+
paddingHorizontal: 20,
1017+
flexDirection: 'row',
1018+
alignItems: 'center',
1019+
},
1020+
1021+
unreadIndicatorLine: {
1022+
height: 1,
1023+
backgroundColor: themeColors.unreadIndicator,
1024+
flexGrow: 1,
1025+
marginRight: 8,
1026+
opacity: 0.5,
1027+
},
1028+
1029+
unreadIndicatorText: {
1030+
color: themeColors.unreadIndicator,
1031+
fontFamily: fontFamily.GTA_BOLD,
1032+
fontSize: variables.fontSizeSmall,
1033+
fontWeight: fontWeightBold,
1034+
},
1035+
10101036
flipUpsideDown: {
10111037
transform: [{rotate: '180deg'}],
10121038
},

src/styles/themes/default.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ export default {
3131
pillBG: colors.gray2,
3232
buttonDisabledBG: colors.gray2,
3333
buttonHoveredBG: colors.gray1,
34+
unreadIndicator: colors.green,
3435
};

0 commit comments

Comments
 (0)