Skip to content

Commit afcbb13

Browse files
authored
Merge pull request #7028 from Expensify/amal-policy-rooms-details-page
Report Details and Settings for User Created Policy Rooms
2 parents e44706a + 6ddfd43 commit afcbb13

File tree

16 files changed

+449
-109
lines changed

16 files changed

+449
-109
lines changed
File renamed without changes.

src/ROUTES.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export default {
7878
) => `r/${reportID}/participants/details?login=${encodeURIComponent(login)}`,
7979
REPORT_WITH_ID_DETAILS: 'r/:reportID/details',
8080
getReportDetailsRoute: reportID => `r/${reportID}/details`,
81+
REPORT_SETTINGS: 'r/:reportID/settings',
82+
getReportSettingsRoute: reportID => `r/${reportID}/settings`,
8183
LOGIN_WITH_SHORT_LIVED_TOKEN: 'transition',
8284
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
8385
GET_ASSISTANCE: 'get-assistance/:taskID',

src/components/Button.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ const propTypes = {
4444
/** Call the onPress function when Enter key is pressed */
4545
pressOnEnter: PropTypes.bool,
4646

47-
/** Additional styles to add after local styles */
47+
/** Additional styles to add after local styles. Applied to Pressable portion of button */
4848
style: PropTypes.oneOfType([
4949
PropTypes.arrayOf(PropTypes.object),
5050
PropTypes.object,
5151
]),
5252

53+
/** Additional button styles. Specific to the OpacityView of button */
54+
innerStyles: PropTypes.arrayOf(PropTypes.object),
55+
5356
/** Additional text styles */
5457
textStyles: PropTypes.arrayOf(PropTypes.object),
5558

@@ -82,6 +85,7 @@ const defaultProps = {
8285
onPressOut: () => {},
8386
pressOnEnter: false,
8487
style: [],
88+
innerStyles: [],
8589
textStyles: [],
8690
success: false,
8791
danger: false,
@@ -195,6 +199,7 @@ class Button extends Component {
195199
(this.props.danger && hovered) ? styles.buttonDangerHovered : undefined,
196200
this.props.shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
197201
this.props.shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
202+
...this.props.innerStyles,
198203
]}
199204
>
200205
{this.renderContent()}

src/components/Icon/Expensicons.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import DownArrow from '../../../assets/images/down.svg';
2222
import Download from '../../../assets/images/download.svg';
2323
import Emoji from '../../../assets/images/emoji.svg';
2424
import Exclamation from '../../../assets/images/exclamation.svg';
25+
import Exit from '../../../assets/images/exit.svg';
2526
import Eye from '../../../assets/images/eye.svg';
2627
import EyeDisabled from '../../../assets/images/eye-disabled.svg';
2728
import ExpensifyCard from '../../../assets/images/expensifycard.svg';
@@ -58,7 +59,6 @@ import Receipt from '../../../assets/images/receipt.svg';
5859
import ReceiptSearch from '../../../assets/images/receipt-search.svg';
5960
import RotateLeft from '../../../assets/images/rotate-left.svg';
6061
import Send from '../../../assets/images/send.svg';
61-
import SignOut from '../../../assets/images/sign-out.svg';
6262
import Sync from '../../../assets/images/sync.svg';
6363
import Transfer from '../../../assets/images/transfer.svg';
6464
import ThreeDots from '../../../assets/images/three-dots.svg';
@@ -95,6 +95,7 @@ export {
9595
Download,
9696
Emoji,
9797
Exclamation,
98+
Exit,
9899
Eye,
99100
EyeDisabled,
100101
ExpensifyCard,
@@ -131,7 +132,6 @@ export {
131132
ReceiptSearch,
132133
RotateLeft,
133134
Send,
134-
SignOut,
135135
Sync,
136136
Transfer,
137137
ThreeDots,

src/components/RoomNameInput.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React, {Component} from 'react';
2+
import PropTypes from 'prop-types';
3+
import _ from 'underscore';
4+
import {withOnyx} from 'react-native-onyx';
5+
import CONST from '../CONST';
6+
import ONYXKEYS from '../ONYXKEYS';
7+
import styles from '../styles/styles';
8+
import compose from '../libs/compose';
9+
import withLocalize, {withLocalizePropTypes} from './withLocalize';
10+
import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from '../pages/workspace/withFullPolicy';
11+
12+
import TextInputWithPrefix from './TextInputWithPrefix';
13+
14+
const propTypes = {
15+
/** Callback to execute when the text input is modified correctly */
16+
onChangeText: PropTypes.func,
17+
18+
/** Callback to execute when an error gets found/cleared/modified */
19+
onChangeError: PropTypes.func,
20+
21+
/** Initial room name to show in input field. This should include the '#' already prefixed to the name */
22+
initialValue: PropTypes.string,
23+
24+
/** Whether we should show the input as disabled */
25+
disabled: PropTypes.bool,
26+
27+
/** ID of policy whose room names we should be checking for duplicates */
28+
policyID: PropTypes.string,
29+
30+
...withLocalizePropTypes,
31+
...fullPolicyPropTypes,
32+
33+
/* Onyx Props */
34+
35+
/** All reports shared with the user */
36+
reports: PropTypes.shape({
37+
/** The report name */
38+
reportName: PropTypes.string,
39+
40+
/** ID of the report */
41+
reportID: PropTypes.number,
42+
}).isRequired,
43+
44+
/** The policies which the user has access to and which the report could be tied to */
45+
policies: PropTypes.shape({
46+
/** The policy name */
47+
name: PropTypes.string,
48+
49+
/** ID of the policy */
50+
id: PropTypes.string,
51+
}).isRequired,
52+
};
53+
54+
const defaultProps = {
55+
onChangeText: () => {},
56+
onChangeError: () => {},
57+
initialValue: '',
58+
disabled: false,
59+
policyID: '',
60+
...fullPolicyDefaultProps,
61+
};
62+
63+
class RoomNameInput extends Component {
64+
constructor(props) {
65+
super(props);
66+
this.state = {
67+
roomName: props.initialValue,
68+
error: '',
69+
};
70+
71+
this.originalRoomName = props.initialValue;
72+
73+
this.checkAndModifyRoomName = this.checkAndModifyRoomName.bind(this);
74+
}
75+
76+
componentDidUpdate(prevProps, prevState) {
77+
// As we are modifying the text input, we'll bubble up any changes/errors so the parent component can see it
78+
if (prevState.roomName !== this.state.roomName) {
79+
this.props.onChangeText(this.state.roomName);
80+
}
81+
if (prevState.error !== this.state.error) {
82+
this.props.onChangeError(this.state.error);
83+
}
84+
}
85+
86+
/**
87+
* Modifies the room name to follow our conventions:
88+
* - Max length 80 characters
89+
* - Cannot not include space or special characters, and we automatically apply an underscore for spaces
90+
* - Must be lowercase
91+
* Also checks to see if this room name already exists, and displays an error message if so.
92+
* @param {String} roomName
93+
*
94+
*/
95+
checkAndModifyRoomName(roomName) {
96+
const modifiedRoomNameWithoutHash = roomName.substring(1)
97+
.replace(/ /g, '_')
98+
.replace(/[^a-zA-Z\d_]/g, '')
99+
.substring(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH)
100+
.toLowerCase();
101+
const finalRoomName = `#${modifiedRoomNameWithoutHash}`;
102+
103+
const isExistingRoomName = _.some(
104+
_.values(this.props.reports),
105+
report => report && report.policyID === this.props.policyID && report.reportName === finalRoomName,
106+
);
107+
108+
// We error if the room name already exists. We don't care if it matches the original name provided in this
109+
// component because then we are not changing the room's name.
110+
const error = isExistingRoomName && finalRoomName !== this.originalRoomName
111+
? this.props.translate('newRoomPage.roomAlreadyExists')
112+
: '';
113+
114+
this.setState({
115+
roomName: finalRoomName,
116+
error,
117+
});
118+
}
119+
120+
render() {
121+
return (
122+
<TextInputWithPrefix
123+
disabled={this.props.disabled}
124+
label={this.props.translate('newRoomPage.roomName')}
125+
prefixCharacter="#"
126+
placeholder={this.props.translate('newRoomPage.social')}
127+
containerStyles={[styles.mb5]}
128+
onChangeText={roomName => this.checkAndModifyRoomName(roomName)}
129+
value={this.state.roomName.substring(1)}
130+
errorText={this.state.error}
131+
autoCapitalize="none"
132+
/>
133+
);
134+
}
135+
}
136+
137+
RoomNameInput.propTypes = propTypes;
138+
RoomNameInput.defaultProps = defaultProps;
139+
140+
export default compose(
141+
withLocalize,
142+
withFullPolicy,
143+
withOnyx({
144+
reports: {
145+
key: ONYXKEYS.COLLECTION.REPORT,
146+
},
147+
policies: {
148+
key: ONYXKEYS.COLLECTION.POLICY,
149+
},
150+
}),
151+
)(RoomNameInput);

src/languages/en.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default {
9595
transferBalance: 'Transfer Balance',
9696
cantFindAddress: 'Can\'t find your address? ',
9797
enterManually: 'Enter it manually',
98+
leaveRoom: 'Leave room',
9899
},
99100
attachmentPicker: {
100101
cameraPermissionRequired: 'Camera permission required',
@@ -213,8 +214,9 @@ export default {
213214
other: 'Unexpected error, please try again later',
214215
},
215216
},
216-
reportDetailsPage: {
217-
notificationPreferencesDescription: 'Notify me about new messages',
217+
notificationPreferences: {
218+
description: 'How often should we notify you when there are new messages to catch up on in this room?',
219+
label: 'Notify me about new messages',
218220
always: 'Always',
219221
daily: 'Daily',
220222
mute: 'Mute',

src/languages/es.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default {
9595
transferBalance: 'Transferencia de saldo',
9696
cantFindAddress: '¿No encuentras tu dirección? ',
9797
enterManually: 'Ingresar manualmente',
98+
leaveRoom: 'Salir de la sala de chat',
9899
},
99100
attachmentPicker: {
100101
cameraPermissionRequired: 'Se necesita permiso para usar la cámara',
@@ -213,8 +214,9 @@ export default {
213214
other: 'Error inesperado, por favor inténtalo más tarde',
214215
},
215216
},
216-
reportDetailsPage: {
217-
notificationPreferencesDescription: 'Avisar sobre nuevos mensajes',
217+
notificationPreferences: {
218+
description: '¿Con qué frecuencia podemos notificarle si haya nuevos mensajes en esta sala de chat?',
219+
label: 'Avisar sobre nuevos mensajes',
218220
always: 'Siempre',
219221
daily: 'Cada día',
220222
mute: 'Nunca',

src/libs/Navigation/AppNavigator/AuthScreens.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ class AuthScreens extends React.Component {
301301
component={ModalStackNavigators.ReportDetailsModalStackNavigator}
302302
listeners={modalScreenListeners}
303303
/>
304+
<RootStack.Screen
305+
name="Report_Settings"
306+
options={modalScreenOptions}
307+
component={ModalStackNavigators.ReportSettingsModalStackNavigator}
308+
listeners={modalScreenListeners}
309+
/>
304310
<RootStack.Screen
305311
name="Participants"
306312
options={modalScreenOptions}

src/libs/Navigation/AppNavigator/ModalStackNavigators.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import CONST from '../../../CONST';
4444
import AddDebitCardPage from '../../../pages/settings/Payments/AddDebitCardPage';
4545
import TransferBalancePage from '../../../pages/settings/Payments/TransferBalancePage';
4646
import ChooseTransferAccountPage from '../../../pages/settings/Payments/ChooseTransferAccountPage';
47+
import ReportSettingsPage from '../../../pages/ReportSettingsPage';
4748

4849
const defaultSubRouteOptions = {
4950
cardStyle: styles.navigationScreenCardStyle,
@@ -141,6 +142,11 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator([{
141142
name: 'Report_Details_Root',
142143
}]);
143144

145+
const ReportSettingsModalStackNavigator = createModalStackNavigator([{
146+
Component: ReportSettingsPage,
147+
name: 'Report_Settings_Root',
148+
}]);
149+
144150
const ReportParticipantsModalStackNavigator = createModalStackNavigator([
145151
{
146152
Component: ReportParticipantsPage,
@@ -310,6 +316,7 @@ export {
310316
IOUDetailsModalStackNavigator,
311317
DetailsModalStackNavigator,
312318
ReportDetailsModalStackNavigator,
319+
ReportSettingsModalStackNavigator,
313320
ReportParticipantsModalStackNavigator,
314321
SearchModalStackNavigator,
315322
NewGroupModalStackNavigator,

src/libs/Navigation/linkingConfig.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ export default {
139139
Report_Details_Root: ROUTES.REPORT_WITH_ID_DETAILS,
140140
},
141141
},
142+
Report_Settings: {
143+
screens: {
144+
Report_Settings_Root: ROUTES.REPORT_SETTINGS,
145+
},
146+
},
142147
NewGroup: {
143148
screens: {
144149
NewGroup_Root: ROUTES.NEW_GROUP,

src/libs/actions/Report.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ function getSimplifiedReportObject(report) {
194194
// Used for archived rooms, will store the policy name that the room used to belong to.
195195
const oldPolicyName = lodashGet(report, ['reportNameValuePairs', 'oldPolicyName'], '');
196196

197+
// Used for User Created Policy Rooms, will denote how access to a chat room is given among workspace members
198+
const visibility = lodashGet(report, ['reportNameValuePairs', 'visibility']);
199+
197200
return {
198201
reportID: report.reportID,
199202
reportName,
@@ -217,6 +220,7 @@ function getSimplifiedReportObject(report) {
217220
stateNum: report.state,
218221
statusNum: report.status,
219222
oldPolicyName,
223+
visibility,
220224
};
221225
}
222226

0 commit comments

Comments
 (0)