[NEW] Custom Status (#1811)
* [NEW] Custom Status * [FIX] Subscribe to changes * [FIX] Improve code using Banner component * [IMPROVEMENT] Toggle modal * [NEW] Edit custom status from Sidebar * [FIX] Modal when tablet * [FIX] Styles * [FIX] Switch to react-native-promp-android * [FIX] Custom Status UI * [TESTS] E2E Custom Status * Fix banner * Fix banner * Fix subtitle * status text * Fix topic header * Fix RoomActionsView topic * Fix header alignment on Android * [FIX] RoomInfo crashes when without statusText * [FIX] Use users.setStatus * [FIX] Remove customStatus of ProfileView * [FIX] Room View Thread Header Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
3437b9039f
commit
d8c8817f04
|
@ -14,6 +14,9 @@ export default {
|
|||
Accounts_AllowUserProfileChange: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Accounts_AllowUserStatusMessageChange: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Accounts_AllowUsernameChange: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
|
|
@ -42,7 +42,7 @@ export const CloseModalButton = React.memo(({ navigation, testID, onPress = () =
|
|||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
export const CloseShareExtensionButton = React.memo(({ onPress, testID }) => (
|
||||
export const CancelModalButton = React.memo(({ onPress, testID }) => (
|
||||
<CustomHeaderButtons left>
|
||||
{isIOS
|
||||
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
|
||||
|
@ -79,7 +79,7 @@ CloseModalButton.propTypes = {
|
|||
testID: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
CloseShareExtensionButton.propTypes = {
|
||||
CancelModalButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired,
|
||||
testID: PropTypes.string.isRequired
|
||||
};
|
||||
|
|
|
@ -33,9 +33,10 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const Content = React.memo(({
|
||||
title, subtitle, disabled, testID, right, color, theme
|
||||
title, subtitle, disabled, testID, left, right, color, theme
|
||||
}) => (
|
||||
<View style={[styles.container, disabled && styles.disabled]} testID={testID}>
|
||||
{left ? left() : null}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[styles.title, { color: color || themes[theme].titleText }]}>{title}</Text>
|
||||
{subtitle
|
||||
|
@ -79,6 +80,7 @@ Item.propTypes = {
|
|||
Content.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
left: PropTypes.func,
|
||||
right: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
testID: PropTypes.string,
|
||||
|
|
|
@ -225,7 +225,7 @@ class UploadModal extends Component {
|
|||
hideModalContentWhileAnimating
|
||||
avoidKeyboard
|
||||
>
|
||||
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && sharedStyles.modal]}>
|
||||
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && [sharedStyles.modal, sharedStyles.modalFormSheet]]}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
|
||||
</View>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { View } from 'react-native';
|
|||
import { STATUS_COLORS, themes } from '../../constants/colors';
|
||||
|
||||
const Status = React.memo(({
|
||||
status, size, style, theme
|
||||
status, size, style, theme, ...props
|
||||
}) => (
|
||||
<View
|
||||
style={
|
||||
|
@ -18,6 +18,7 @@ const Status = React.memo(({
|
|||
borderColor: themes[theme].backgroundColor
|
||||
}
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Status.propTypes = {
|
||||
|
|
|
@ -26,7 +26,7 @@ class StatusContainer extends React.PureComponent {
|
|||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
status: state.meteor.connected ? state.activeUsers[ownProps.id] : 'offline'
|
||||
status: state.meteor.connected ? (state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status) : 'offline'
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(StatusContainer));
|
||||
|
|
|
@ -65,6 +65,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
testID: PropTypes.string,
|
||||
iconLeft: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
left: PropTypes.element,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
|
@ -116,7 +117,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
render() {
|
||||
const { showPassword } = this.state;
|
||||
const {
|
||||
label, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
|
||||
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
|
||||
} = this.props;
|
||||
const { dangerColor } = themes[theme];
|
||||
return (
|
||||
|
@ -166,6 +167,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
{iconLeft ? this.iconLeft : null}
|
||||
{secureTextEntry ? this.iconPassword : null}
|
||||
{loading ? this.loading : null}
|
||||
{left}
|
||||
</View>
|
||||
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
|
||||
</View>
|
||||
|
|
|
@ -10,6 +10,7 @@ export default {
|
|||
'error-could-not-change-email': 'Could not change email',
|
||||
'error-could-not-change-name': 'Could not change name',
|
||||
'error-could-not-change-username': 'Could not change username',
|
||||
'error-could-not-change-status': 'Could not change status',
|
||||
'error-delete-protected-role': 'Cannot delete a protected role',
|
||||
'error-department-not-found': 'Department not found',
|
||||
'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages',
|
||||
|
@ -159,6 +160,7 @@ export default {
|
|||
Created_snippet: 'Created a snippet',
|
||||
Create_a_new_workspace: 'Create a new workspace',
|
||||
Create: 'Create',
|
||||
Custom_Status: 'Custom Status',
|
||||
Dark: 'Dark',
|
||||
Dark_level: 'Dark Level',
|
||||
Default: 'Default',
|
||||
|
@ -177,6 +179,7 @@ export default {
|
|||
Discussions: 'Discussions',
|
||||
Discussion_Desc: 'Help keeping an overview about what\'s going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.',
|
||||
Discussion_name: 'Discussion name',
|
||||
Done: 'Done',
|
||||
Dont_Have_An_Account: 'Don\'t you have an account?',
|
||||
Do_you_have_an_account: 'Do you have an account?',
|
||||
Do_you_have_a_certificate: 'Do you have a certificate?',
|
||||
|
@ -184,6 +187,7 @@ export default {
|
|||
edit: 'edit',
|
||||
edited: 'edited',
|
||||
Edit: 'Edit',
|
||||
Edit_Status: 'Edit Status',
|
||||
Edit_Invite: 'Edit Invite',
|
||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||
Email: 'Email',
|
||||
|
@ -416,6 +420,9 @@ export default {
|
|||
Servers: 'Servers',
|
||||
Server_version: 'Server version: {{version}}',
|
||||
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
|
||||
Set_custom_status: 'Set custom status',
|
||||
Set_status: 'Set status',
|
||||
Status_saved_successfully: 'Status saved successfully!',
|
||||
Settings: 'Settings',
|
||||
Settings_succesfully_changed: 'Settings succesfully changed!',
|
||||
Share: 'Share',
|
||||
|
@ -497,6 +504,7 @@ export default {
|
|||
Voice_call: 'Voice call',
|
||||
Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}',
|
||||
Welcome: 'Welcome',
|
||||
What_are_you_doing_right_now: 'What are you doing right now?',
|
||||
Whats_your_2fa: 'What\'s your 2FA code?',
|
||||
Without_Servers: 'Without Servers',
|
||||
Workspaces: 'Workspaces',
|
||||
|
|
|
@ -173,6 +173,7 @@ export default {
|
|||
Discussions: 'Discussões',
|
||||
Discussion_Desc: 'Ajude a manter uma visão geral sobre o que está acontecendo! Ao criar uma discussão, um sub-canal do que você selecionou é criado e os dois são vinculados.',
|
||||
Discussion_name: 'Nome da discussão',
|
||||
Done: 'Pronto',
|
||||
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||
Do_you_have_an_account: 'Você tem uma conta?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||
|
@ -180,6 +181,7 @@ export default {
|
|||
edited: 'editado',
|
||||
Edit: 'Editar',
|
||||
Edit_Invite: 'Editar convite',
|
||||
Edit_Status: 'Editar Status',
|
||||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
|
@ -449,6 +451,7 @@ export default {
|
|||
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
|
||||
Welcome: 'Bem vindo',
|
||||
Whats_your_2fa: 'Qual seu código de autenticação?',
|
||||
What_are_you_doing_right_now: 'O que você está fazendo agora?',
|
||||
Without_Servers: 'Sem Servidores',
|
||||
Workspaces: 'Workspaces',
|
||||
Yes_action_it: 'Sim, {{action}}!',
|
||||
|
|
13
app/index.js
13
app/index.js
|
@ -298,11 +298,21 @@ const CreateDiscussionStack = createStackNavigator({
|
|||
cardStyle
|
||||
});
|
||||
|
||||
const StatusStack = createStackNavigator({
|
||||
StatusView: {
|
||||
getScreen: () => require('./views/StatusView').default
|
||||
}
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader,
|
||||
cardStyle
|
||||
});
|
||||
|
||||
const InsideStackModal = createStackNavigator({
|
||||
Main: ChatsDrawer,
|
||||
NewMessageStack,
|
||||
AttachmentStack,
|
||||
ModalBlockStack,
|
||||
StatusStack,
|
||||
CreateDiscussionStack,
|
||||
JitsiMeetView: {
|
||||
getScreen: () => require('./views/JitsiMeetView').default
|
||||
|
@ -395,6 +405,9 @@ const SidebarStack = createStackNavigator({
|
|||
},
|
||||
AdminPanelView: {
|
||||
getScreen: () => require('./views/AdminPanelView').default
|
||||
},
|
||||
StatusView: {
|
||||
getScreen: () => require('./views/StatusView').default
|
||||
}
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader,
|
||||
|
|
|
@ -23,6 +23,8 @@ import appSchema from './schema/app';
|
|||
|
||||
import migrations from './model/migrations';
|
||||
|
||||
import serversMigrations from './model/serversMigrations';
|
||||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
|
||||
const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : '';
|
||||
|
@ -36,7 +38,8 @@ class DB {
|
|||
serversDB: new Database({
|
||||
adapter: new SQLiteAdapter({
|
||||
dbName: `${ appGroupPath }default.db`,
|
||||
schema: serversSchema
|
||||
schema: serversSchema,
|
||||
migrations: serversMigrations
|
||||
}),
|
||||
modelClasses: [Server, User],
|
||||
actionsEnabled: true
|
||||
|
|
|
@ -16,5 +16,7 @@ export default class User extends Model {
|
|||
|
||||
@field('status') status;
|
||||
|
||||
@field('statusText') statusText;
|
||||
|
||||
@json('roles', sanitizer) roles;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { schemaMigrations, addColumns } from '@nozbe/watermelondb/Schema/migrations';
|
||||
|
||||
export default schemaMigrations({
|
||||
migrations: [
|
||||
{
|
||||
toVersion: 3,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: 'users',
|
||||
columns: [
|
||||
{ name: 'statusText', type: 'string', isOptional: true }
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||
|
||||
export default appSchema({
|
||||
version: 2,
|
||||
version: 3,
|
||||
tables: [
|
||||
tableSchema({
|
||||
name: 'users',
|
||||
|
@ -11,6 +11,7 @@ export default appSchema({
|
|||
{ name: 'name', type: 'string', isOptional: true },
|
||||
{ name: 'language', type: 'string', isOptional: true },
|
||||
{ name: 'status', type: 'string', isOptional: true },
|
||||
{ name: 'statusText', type: 'string', isOptional: true },
|
||||
{ name: 'roles', type: 'string', isOptional: true }
|
||||
]
|
||||
}),
|
||||
|
|
|
@ -45,7 +45,10 @@ export default async function getUsersPresence() {
|
|||
const result = await this.sdk.get('users.presence', params);
|
||||
if (result.success) {
|
||||
const activeUsers = result.users.reduce((ret, item) => {
|
||||
ret[item._id] = item.status;
|
||||
ret[item._id] = {
|
||||
status: item.status,
|
||||
statusText: item.statusText
|
||||
};
|
||||
return ret;
|
||||
}, {});
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
|
|
|
@ -241,12 +241,12 @@ const RocketChat = {
|
|||
}, 10000);
|
||||
}
|
||||
const userStatus = ddpMessage.fields.args[0];
|
||||
const [id,, status] = userStatus;
|
||||
this.activeUsers[id] = STATUSES[status];
|
||||
const [id,, status, statusText] = userStatus;
|
||||
this.activeUsers[id] = { status: STATUSES[status], statusText };
|
||||
|
||||
const { user: loggedUser } = reduxStore.getState().login;
|
||||
if (loggedUser && loggedUser.id === id) {
|
||||
reduxStore.dispatch(setUser({ status: STATUSES[status] }));
|
||||
reduxStore.dispatch(setUser({ status: STATUSES[status], statusText }));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
@ -378,6 +378,7 @@ const RocketChat = {
|
|||
name: result.me.name,
|
||||
language: result.me.language,
|
||||
status: result.me.status,
|
||||
statusText: result.me.statusText,
|
||||
customFields: result.me.customFields,
|
||||
emails: result.me.emails,
|
||||
roles: result.me.roles
|
||||
|
@ -741,6 +742,10 @@ const RocketChat = {
|
|||
setUserPresenceDefaultStatus(status) {
|
||||
return this.sdk.methodCall('UserPresence:setDefaultStatus', status);
|
||||
},
|
||||
setUserStatus(message) {
|
||||
// RC 1.2.0
|
||||
return this.sdk.post('users.setStatus', { message });
|
||||
},
|
||||
setReaction(emoji, messageId) {
|
||||
// RC 0.62.2
|
||||
return this.sdk.post('chat.react', { emoji, messageId });
|
||||
|
@ -1056,7 +1061,7 @@ const RocketChat = {
|
|||
}
|
||||
|
||||
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
|
||||
reduxStore.dispatch(setUser({ status: 'offline' }));
|
||||
reduxStore.dispatch(setUser({ status: { status: 'offline' } }));
|
||||
}
|
||||
|
||||
if (!this._setUserTimer) {
|
||||
|
@ -1071,9 +1076,9 @@ const RocketChat = {
|
|||
}
|
||||
|
||||
if (!ddpMessage.fields) {
|
||||
this.activeUsers[ddpMessage.id] = 'offline';
|
||||
this.activeUsers[ddpMessage.id] = { status: 'offline' };
|
||||
} else if (ddpMessage.fields.status) {
|
||||
this.activeUsers[ddpMessage.id] = ddpMessage.fields.status;
|
||||
this.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
|
||||
}
|
||||
},
|
||||
getUsersPresence,
|
||||
|
|
|
@ -210,7 +210,7 @@ RoomItem.defaultProps = {
|
|||
const mapStateToProps = (state, ownProps) => ({
|
||||
status:
|
||||
state.meteor.connected && ownProps.type === 'd'
|
||||
? state.activeUsers[ownProps.id]
|
||||
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
|
||||
: 'offline'
|
||||
});
|
||||
|
||||
|
|
|
@ -101,6 +101,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
|||
name: user.name,
|
||||
language: user.language,
|
||||
status: user.status,
|
||||
statusText: user.statusText,
|
||||
roles: user.roles
|
||||
};
|
||||
yield serversDB.action(async() => {
|
||||
|
|
|
@ -78,6 +78,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
|||
name: userRecord.name,
|
||||
language: userRecord.language,
|
||||
status: userRecord.status,
|
||||
statusText: userRecord.statusText,
|
||||
roles: userRecord.roles
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
|
@ -112,17 +112,11 @@ export const initTabletNav = (setState) => {
|
|||
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
|
||||
setState({ inside: false, showModal: false });
|
||||
}
|
||||
if (routeName === 'ModalBlockView') {
|
||||
if (routeName === 'ModalBlockView' || routeName === 'StatusView' || routeName === 'CreateDiscussionView') {
|
||||
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
|
||||
setState({ showModal: true });
|
||||
return null;
|
||||
}
|
||||
if (routeName === 'CreateDiscussionView') {
|
||||
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
|
||||
setState({ showModal: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (routeName === 'RoomView') {
|
||||
const resetAction = StackActions.reset({
|
||||
index: 0,
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Touch from '../../utils/touch';
|
||||
import { leaveRoom as leaveRoomAction } from '../../actions/room';
|
||||
|
@ -25,6 +26,7 @@ import { withTheme } from '../../theme';
|
|||
import { themedHeader } from '../../utils/navigation';
|
||||
import { CloseModalButton } from '../../containers/HeaderButton';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Markdown from '../../containers/markdown';
|
||||
|
||||
class RoomActionsView extends React.Component {
|
||||
static navigationOptions = ({ navigation, screenProps }) => {
|
||||
|
@ -54,12 +56,13 @@ class RoomActionsView extends React.Component {
|
|||
super(props);
|
||||
this.mounted = false;
|
||||
const room = props.navigation.getParam('room');
|
||||
const member = props.navigation.getParam('member');
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
this.state = {
|
||||
room: room || { rid: this.rid, t: this.t },
|
||||
membersCount: 0,
|
||||
member: {},
|
||||
member: member || {},
|
||||
joined: !!room,
|
||||
canViewMembers: false,
|
||||
canAutoTranslate: false,
|
||||
|
@ -81,7 +84,7 @@ class RoomActionsView extends React.Component {
|
|||
|
||||
async componentDidMount() {
|
||||
this.mounted = true;
|
||||
const { room } = this.state;
|
||||
const { room, member } = this.state;
|
||||
if (!room.id) {
|
||||
try {
|
||||
const result = await RocketChat.getChannelInfo(room.rid);
|
||||
|
@ -102,7 +105,7 @@ class RoomActionsView extends React.Component {
|
|||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
} else if (room.t === 'd') {
|
||||
} else if (room.t === 'd' && _.isEmpty(member)) {
|
||||
this.updateRoomMember();
|
||||
}
|
||||
|
||||
|
@ -181,7 +184,7 @@ class RoomActionsView extends React.Component {
|
|||
|
||||
get sections() {
|
||||
const {
|
||||
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
|
||||
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
|
||||
} = this.state;
|
||||
const { jitsiEnabled } = this.props;
|
||||
const {
|
||||
|
@ -217,7 +220,9 @@ class RoomActionsView extends React.Component {
|
|||
name: I18n.t('Room_Info'),
|
||||
route: 'RoomInfoView',
|
||||
// forward room only if room isn't joined
|
||||
params: { rid, t, room },
|
||||
params: {
|
||||
rid, t, room, member
|
||||
},
|
||||
testID: 'room-actions-info'
|
||||
}],
|
||||
renderItem: this.renderRoomInfo
|
||||
|
@ -451,7 +456,14 @@ class RoomActionsView extends React.Component {
|
|||
</View>
|
||||
)
|
||||
}
|
||||
<Text style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text>
|
||||
<Markdown
|
||||
preview
|
||||
msg={t === 'd' ? `@${ name }` : topic}
|
||||
style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]}
|
||||
numberOfLines={1}
|
||||
theme={theme}
|
||||
/>
|
||||
{room.t === 'd' && <Markdown msg={member.statusText} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} preview theme={theme} />}
|
||||
</View>,
|
||||
<DisclosureIndicator theme={theme} key='disclosure-indicator' />
|
||||
], item)
|
||||
|
|
|
@ -4,6 +4,7 @@ import { View, Text, ScrollView } from 'react-native';
|
|||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import Status from '../../containers/Status';
|
||||
|
@ -24,11 +25,12 @@ import { getUserSelector } from '../../selectors/login';
|
|||
import Markdown from '../../containers/markdown';
|
||||
|
||||
const PERMISSION_EDIT_ROOM = 'edit-room';
|
||||
const getRoomTitle = (room, type, name, username, theme) => (type === 'd'
|
||||
const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd'
|
||||
? (
|
||||
<>
|
||||
<Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}>{ name }</Text>
|
||||
{username && <Text testID='room-info-view-username' style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]}>{`@${ username }`}</Text>}
|
||||
{!!statusText && <View testID='room-info-view-custom-status'><Markdown msg={statusText} style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]} preview theme={theme} /></View>}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
|
@ -71,16 +73,22 @@ class RoomInfoView extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
const room = props.navigation.getParam('room');
|
||||
const roomUser = props.navigation.getParam('member');
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
this.state = {
|
||||
room: room || {},
|
||||
roomUser: {},
|
||||
roomUser: roomUser || {},
|
||||
parsedRoles: []
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { roomUser } = this.state;
|
||||
if (this.t === 'd' && !_.isEmpty(roomUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.t === 'd') {
|
||||
const { user } = this.props;
|
||||
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
|
||||
|
@ -342,7 +350,7 @@ class RoomInfoView extends React.Component {
|
|||
>
|
||||
<View style={[styles.avatarContainer, isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||
{this.renderAvatar(room, roomUser)}
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, theme) }</View>
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }</View>
|
||||
{isDirect ? this.renderButtons() : null}
|
||||
</View>
|
||||
{isDirect ? this.renderDirect() : this.renderChannel()}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ScrollView, BorderlessButton } from 'react-native-gesture-handler';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
import Markdown from '../../containers/markdown';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
|
||||
const Banner = React.memo(({
|
||||
text, title, theme
|
||||
}) => {
|
||||
const [showModal, openModal] = useState(false);
|
||||
|
||||
const toggleModal = () => openModal(prevState => !prevState);
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<>
|
||||
<BorderlessButton
|
||||
style={[styles.bannerContainer, { backgroundColor: themes[theme].bannerBackground }]}
|
||||
testID='room-view-banner'
|
||||
onPress={toggleModal}
|
||||
>
|
||||
<Markdown
|
||||
msg={text}
|
||||
theme={theme}
|
||||
numberOfLines={1}
|
||||
preview
|
||||
/>
|
||||
</BorderlessButton>
|
||||
<Modal
|
||||
onBackdropPress={toggleModal}
|
||||
onBackButtonPress={toggleModal}
|
||||
useNativeDriver
|
||||
isVisible={showModal}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
>
|
||||
<View style={[styles.modalView, { backgroundColor: themes[theme].bannerBackground }]}>
|
||||
<Text style={[styles.bannerModalTitle, { color: themes[theme].auxiliaryText }]}>{title}</Text>
|
||||
<ScrollView style={styles.modalScrollView}>
|
||||
<Markdown
|
||||
msg={text}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, (prevProps, nextProps) => prevProps.text === nextProps.text && prevProps.theme === nextProps.theme);
|
||||
|
||||
Banner.propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Banner;
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Text, StyleSheet, ScrollView, TouchableOpacity
|
||||
View, Text, StyleSheet, TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import I18n from '../../../i18n';
|
||||
|
@ -11,18 +11,17 @@ import Icon from './Icon';
|
|||
import { themes } from '../../../constants/colors';
|
||||
import Markdown from '../../../containers/markdown';
|
||||
|
||||
const androidMarginLeft = isTablet ? 0 : 10;
|
||||
const androidMarginLeft = isTablet ? 0 : 4;
|
||||
|
||||
const TITLE_SIZE = 16;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
marginRight: isAndroid ? 15 : 5,
|
||||
marginLeft: isAndroid ? androidMarginLeft : -12
|
||||
marginLeft: isAndroid ? androidMarginLeft : -10
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 6,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
threadContainer: {
|
||||
|
@ -35,36 +34,54 @@ const styles = StyleSheet.create({
|
|||
scroll: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
typing: {
|
||||
subtitle: {
|
||||
...sharedStyles.textRegular,
|
||||
fontSize: 12,
|
||||
flex: 4
|
||||
fontSize: 12
|
||||
},
|
||||
typingUsers: {
|
||||
...sharedStyles.textSemibold
|
||||
}
|
||||
});
|
||||
|
||||
const Typing = React.memo(({ usersTyping, theme }) => {
|
||||
let usersText;
|
||||
if (!usersTyping.length) {
|
||||
const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
||||
if (!subtitle && !usersTyping.length) {
|
||||
return null;
|
||||
} else if (usersTyping.length === 2) {
|
||||
usersText = usersTyping.join(` ${ I18n.t('and') } `);
|
||||
} else {
|
||||
usersText = usersTyping.join(', ');
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.typing, { color: themes[theme].headerTitleColor }]} numberOfLines={1}>
|
||||
<Text style={styles.typingUsers}>{usersText} </Text>
|
||||
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
|
||||
</Text>
|
||||
);
|
||||
|
||||
// typing
|
||||
if (usersTyping.length) {
|
||||
let usersText;
|
||||
if (usersTyping.length === 2) {
|
||||
usersText = usersTyping.join(` ${ I18n.t('and') } `);
|
||||
} else {
|
||||
usersText = usersTyping.join(', ');
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||
<Text style={styles.typingUsers}>{usersText} </Text>
|
||||
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
// subtitle
|
||||
if (subtitle) {
|
||||
return (
|
||||
<Markdown
|
||||
preview
|
||||
msg={subtitle}
|
||||
style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}
|
||||
numberOfLines={1}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Typing.propTypes = {
|
||||
SubTitle.propTypes = {
|
||||
usersTyping: PropTypes.array,
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
subtitle: PropTypes.string
|
||||
};
|
||||
|
||||
const HeaderTitle = React.memo(({
|
||||
|
@ -108,54 +125,45 @@ HeaderTitle.propTypes = {
|
|||
};
|
||||
|
||||
const Header = React.memo(({
|
||||
title, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme
|
||||
title, subtitle, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme
|
||||
}) => {
|
||||
const portrait = height > width;
|
||||
let scale = 1;
|
||||
|
||||
if (!portrait && !tmid) {
|
||||
if (usersTyping.length > 0) {
|
||||
if (usersTyping.length > 0 || subtitle) {
|
||||
scale = 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
if (!tmid) {
|
||||
goRoomActionsView();
|
||||
}
|
||||
};
|
||||
const onPress = () => goRoomActionsView();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID='room-view-header-actions'
|
||||
onPress={onPress}
|
||||
style={[styles.container, { width: width - widthOffset }]}
|
||||
disabled={tmid}
|
||||
>
|
||||
<View style={[styles.titleContainer, tmid && styles.threadContainer]}>
|
||||
<ScrollView
|
||||
showsHorizontalScrollIndicator={false}
|
||||
horizontal
|
||||
bounces={false}
|
||||
contentContainerStyle={styles.scroll}
|
||||
>
|
||||
<Icon type={prid ? 'discussion' : type} status={status} theme={theme} />
|
||||
<HeaderTitle
|
||||
title={title}
|
||||
tmid={tmid}
|
||||
prid={prid}
|
||||
scale={scale}
|
||||
connecting={connecting}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
<Icon type={prid ? 'discussion' : type} status={status} theme={theme} />
|
||||
<HeaderTitle
|
||||
title={title}
|
||||
tmid={tmid}
|
||||
prid={prid}
|
||||
scale={scale}
|
||||
connecting={connecting}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
{type === 'thread' ? null : <Typing usersTyping={usersTyping} theme={theme} />}
|
||||
{tmid ? null : <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
|
|
|
@ -13,11 +13,11 @@ const styles = StyleSheet.create({
|
|||
type: {
|
||||
width: ICON_SIZE,
|
||||
height: ICON_SIZE,
|
||||
marginRight: 8
|
||||
marginRight: 4,
|
||||
marginLeft: -4
|
||||
},
|
||||
status: {
|
||||
marginLeft: 4,
|
||||
marginRight: 12
|
||||
marginRight: 8
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -13,12 +13,14 @@ import { getUserSelector } from '../../../selectors/login';
|
|||
class RoomHeaderView extends Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
prid: PropTypes.string,
|
||||
tmid: PropTypes.string,
|
||||
usersTyping: PropTypes.string,
|
||||
window: PropTypes.object,
|
||||
status: PropTypes.string,
|
||||
statusText: PropTypes.string,
|
||||
connecting: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
widthOffset: PropTypes.number,
|
||||
|
@ -27,7 +29,7 @@ class RoomHeaderView extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const {
|
||||
type, title, status, window, connecting, goRoomActionsView, usersTyping, theme
|
||||
type, title, subtitle, status, statusText, window, connecting, goRoomActionsView, usersTyping, theme
|
||||
} = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
|
@ -38,9 +40,15 @@ class RoomHeaderView extends Component {
|
|||
if (nextProps.title !== title) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.subtitle !== subtitle) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.status !== status) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.statusText !== statusText) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.connecting !== connecting) {
|
||||
return true;
|
||||
}
|
||||
|
@ -61,7 +69,7 @@ class RoomHeaderView extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
window, title, type, prid, tmid, widthOffset, status = 'offline', connecting, usersTyping, goRoomActionsView, theme
|
||||
window, title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, theme
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -69,6 +77,7 @@ class RoomHeaderView extends Component {
|
|||
prid={prid}
|
||||
tmid={tmid}
|
||||
title={title}
|
||||
subtitle={type === 'd' ? statusText : subtitle}
|
||||
type={type}
|
||||
status={status}
|
||||
width={window.width}
|
||||
|
@ -85,19 +94,23 @@ class RoomHeaderView extends Component {
|
|||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
let status;
|
||||
let statusText;
|
||||
const { rid, type } = ownProps;
|
||||
if (type === 'd') {
|
||||
const user = getUserSelector(state);
|
||||
if (user.id) {
|
||||
const userId = rid.replace(user.id, '').trim();
|
||||
status = state.activeUsers[userId];
|
||||
if (state.activeUsers[userId]) {
|
||||
({ status, statusText } = state.activeUsers[userId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connecting: state.meteor.connecting,
|
||||
usersTyping: state.usersTyping,
|
||||
status
|
||||
status,
|
||||
statusText
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View, InteractionManager } from 'react-native';
|
||||
import { ScrollView, BorderlessButton } from 'react-native-gesture-handler';
|
||||
import { connect } from 'react-redux';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||
import moment from 'moment';
|
||||
|
@ -53,7 +51,7 @@ import { Review } from '../../utils/review';
|
|||
import RoomClass from '../../lib/methods/subscriptions/room';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import { CONTAINER_TYPES } from '../../lib/methods/actions';
|
||||
import Markdown from '../../containers/markdown';
|
||||
import Banner from './Banner';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
|
||||
const stateAttrsUpdate = [
|
||||
|
@ -67,15 +65,16 @@ const stateAttrsUpdate = [
|
|||
'editing',
|
||||
'replying',
|
||||
'reacting',
|
||||
'showAnnouncementModal'
|
||||
'member'
|
||||
];
|
||||
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes'];
|
||||
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname'];
|
||||
|
||||
class RoomView extends React.Component {
|
||||
static navigationOptions = ({ navigation, screenProps }) => {
|
||||
const rid = navigation.getParam('rid', null);
|
||||
const prid = navigation.getParam('prid');
|
||||
const title = navigation.getParam('name');
|
||||
const subtitle = navigation.getParam('subtitle');
|
||||
const t = navigation.getParam('t');
|
||||
const tmid = navigation.getParam('tmid');
|
||||
const baseUrl = navigation.getParam('baseUrl');
|
||||
|
@ -98,6 +97,7 @@ class RoomView extends React.Component {
|
|||
prid={prid}
|
||||
tmid={tmid}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
type={t}
|
||||
widthOffset={tmid ? 95 : 130}
|
||||
goRoomActionsView={goRoomActionsView}
|
||||
|
@ -168,6 +168,7 @@ class RoomView extends React.Component {
|
|||
rid: this.rid, t: this.t, name, fname
|
||||
},
|
||||
roomUpdate: {},
|
||||
member: {},
|
||||
lastOpen: null,
|
||||
reactionsModalVisible: false,
|
||||
selectedMessage: selectedMessage || {},
|
||||
|
@ -179,7 +180,6 @@ class RoomView extends React.Component {
|
|||
replying: !!selectedMessage,
|
||||
replyWithMention: false,
|
||||
reacting: false,
|
||||
showAnnouncementModal: false,
|
||||
announcement: null
|
||||
};
|
||||
|
||||
|
@ -207,6 +207,7 @@ class RoomView extends React.Component {
|
|||
if ((room.id || room.rid) && !this.tmid) {
|
||||
navigation.setParams({
|
||||
name: this.getRoomTitle(room),
|
||||
subtitle: room.topic,
|
||||
avatar: room.name,
|
||||
t: room.t,
|
||||
token: user.token,
|
||||
|
@ -236,7 +237,7 @@ class RoomView extends React.Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { state } = this;
|
||||
const { roomUpdate } = state;
|
||||
const { roomUpdate, member } = state;
|
||||
const { appState, theme } = this.props;
|
||||
if (theme !== nextProps.theme) {
|
||||
return true;
|
||||
|
@ -244,6 +245,9 @@ class RoomView extends React.Component {
|
|||
if (appState !== nextProps.appState) {
|
||||
return true;
|
||||
}
|
||||
if (member.statusText !== nextState.member.statusText) {
|
||||
return true;
|
||||
}
|
||||
const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
|
||||
if (stateUpdated) {
|
||||
return true;
|
||||
|
@ -251,8 +255,9 @@ class RoomView extends React.Component {
|
|||
return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key]));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { appState } = this.props;
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { roomUpdate, room } = this.state;
|
||||
const { appState, navigation } = this.props;
|
||||
|
||||
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
|
||||
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => {
|
||||
|
@ -262,6 +267,15 @@ class RoomView extends React.Component {
|
|||
}
|
||||
});
|
||||
}
|
||||
// If it's not direct message
|
||||
if (this.t !== 'd') {
|
||||
if (roomUpdate.topic !== prevState.roomUpdate.topic) {
|
||||
navigation.setParams({ subtitle: roomUpdate.topic });
|
||||
}
|
||||
}
|
||||
if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
|
||||
navigation.setParams({ name: this.getRoomTitle(room) });
|
||||
}
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
|
@ -319,9 +333,11 @@ class RoomView extends React.Component {
|
|||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
goRoomActionsView = () => {
|
||||
const { room } = this.state;
|
||||
const { room, member } = this.state;
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room });
|
||||
navigation.navigate('RoomActionsView', {
|
||||
rid: this.rid, t: this.t, room, member
|
||||
});
|
||||
}
|
||||
|
||||
init = async() => {
|
||||
|
@ -349,7 +365,10 @@ class RoomView extends React.Component {
|
|||
// We run `canAutoTranslate` again in order to refetch auto translate permission
|
||||
// in case of a missing connection or poor connection on room open
|
||||
const canAutoTranslate = await RocketChat.canAutoTranslate();
|
||||
this.setState({ canAutoTranslate, loading: false });
|
||||
|
||||
const member = await this.getRoomMember();
|
||||
|
||||
this.setState({ canAutoTranslate, member, loading: false });
|
||||
} catch (e) {
|
||||
this.setState({ loading: false });
|
||||
this.retryInit = this.retryInit + 1 || 1;
|
||||
|
@ -361,6 +380,27 @@ class RoomView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
getRoomMember = async() => {
|
||||
const { room } = this.state;
|
||||
const { rid, t } = room;
|
||||
|
||||
if (t === 'd') {
|
||||
const { user } = this.props;
|
||||
|
||||
try {
|
||||
const roomUserId = RocketChat.getRoomMemberId(rid, user.id);
|
||||
const result = await RocketChat.getUserInfo(roomUserId);
|
||||
if (result.success) {
|
||||
return result.user;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
findAndObserveRoom = async(rid) => {
|
||||
try {
|
||||
const db = database.active;
|
||||
|
@ -371,6 +411,7 @@ class RoomView extends React.Component {
|
|||
if (!this.tmid) {
|
||||
navigation.setParams({
|
||||
name: this.getRoomTitle(room),
|
||||
subtitle: room.topic,
|
||||
avatar: room.name,
|
||||
t: room.t
|
||||
});
|
||||
|
@ -805,54 +846,6 @@ class RoomView extends React.Component {
|
|||
return message;
|
||||
}
|
||||
|
||||
toggleAnnouncementModal = (showModal) => {
|
||||
this.setState({ showAnnouncementModal: showModal });
|
||||
}
|
||||
|
||||
renderAnnouncement = () => {
|
||||
const { theme } = this.props;
|
||||
const { room } = this.state;
|
||||
if (room.announcement) {
|
||||
return (
|
||||
<BorderlessButton style={[styles.announcementTextContainer, { backgroundColor: themes[theme].bannerBackground }]} key='room-user-status' testID='room-user-status' onPress={() => this.toggleAnnouncementModal(true)}>
|
||||
<Markdown
|
||||
msg={room.announcement}
|
||||
theme={theme}
|
||||
numberOfLines={1}
|
||||
preview
|
||||
/>
|
||||
</BorderlessButton>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderAnnouncementModal = () => {
|
||||
const { room, showAnnouncementModal } = this.state;
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
onBackdropPress={() => this.toggleAnnouncementModal(false)}
|
||||
onBackButtonPress={() => this.toggleAnnouncementModal(false)}
|
||||
useNativeDriver
|
||||
isVisible={showAnnouncementModal}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
>
|
||||
<View style={[styles.modalView, { backgroundColor: themes[theme].bannerBackground }]}>
|
||||
<Text style={[styles.announcementTitle, { color: themes[theme].auxiliaryText }]}>{I18n.t('Announcement')}</Text>
|
||||
<ScrollView style={styles.modalScrollView}>
|
||||
<Markdown
|
||||
msg={room.announcement}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderFooter = () => {
|
||||
const {
|
||||
joined, room, selectedMessage, editing, replying, replyWithMention
|
||||
|
@ -973,7 +966,12 @@ class RoomView extends React.Component {
|
|||
forceInset={{ vertical: 'never' }}
|
||||
>
|
||||
<StatusBar theme={theme} />
|
||||
{this.renderAnnouncement()}
|
||||
<Banner
|
||||
rid={rid}
|
||||
title={I18n.t('Announcement')}
|
||||
text={room.announcement}
|
||||
theme={theme}
|
||||
/>
|
||||
<List
|
||||
ref={this.list}
|
||||
listRef={this.setListRef}
|
||||
|
@ -987,7 +985,6 @@ class RoomView extends React.Component {
|
|||
navigation={navigation}
|
||||
hideSystemMessages={sysMes || Hide_System_Messages}
|
||||
/>
|
||||
{this.renderAnnouncementModal()}
|
||||
{this.renderFooter()}
|
||||
{this.renderActions()}
|
||||
<ReactionPicker
|
||||
|
|
|
@ -25,12 +25,12 @@ export default StyleSheet.create({
|
|||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
announcementTextContainer: {
|
||||
bannerContainer: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center'
|
||||
},
|
||||
announcementTitle: {
|
||||
bannerModalTitle: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Keyboard, View, StyleSheet } from 'react-native';
|
|||
import ShareExtension from 'rn-extensions-share';
|
||||
|
||||
import SearchBox from '../../../containers/SearchBox';
|
||||
import { CloseShareExtensionButton } from '../../../containers/HeaderButton';
|
||||
import { CancelModalButton } from '../../../containers/HeaderButton';
|
||||
import { themes } from '../../../constants/colors';
|
||||
|
||||
import sharedStyles from '../../Styles';
|
||||
|
@ -52,7 +52,7 @@ const Header = React.memo(({
|
|||
{
|
||||
!searching
|
||||
? (
|
||||
<CloseShareExtensionButton
|
||||
<CancelModalButton
|
||||
onPress={ShareExtension.close}
|
||||
testID='share-extension-close'
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,7 @@ import log from '../../utils/log';
|
|||
import { canUploadFile } from '../../utils/media';
|
||||
import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem';
|
||||
import ServerItem from '../../presentation/ServerItem';
|
||||
import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||
import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||
import ShareListHeader from './Header';
|
||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||
|
||||
|
@ -66,7 +66,7 @@ class ShareListView extends React.Component {
|
|||
</CustomHeaderButtons>
|
||||
)
|
||||
: (
|
||||
<CloseShareExtensionButton
|
||||
<CancelModalButton
|
||||
onPress={ShareExtension.close}
|
||||
testID='share-extension-close'
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { themes } from '../../constants/colors';
|
|||
import { withTheme } from '../../theme';
|
||||
|
||||
const Item = React.memo(({
|
||||
left, text, onPress, testID, current, theme
|
||||
left, right, text, onPress, testID, current, theme
|
||||
}) => (
|
||||
<Touch
|
||||
key={testID}
|
||||
|
@ -17,7 +17,7 @@ const Item = React.memo(({
|
|||
theme={theme}
|
||||
style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}
|
||||
>
|
||||
<View style={styles.itemLeft}>
|
||||
<View style={styles.itemHorizontal}>
|
||||
{left}
|
||||
</View>
|
||||
<View style={styles.itemCenter}>
|
||||
|
@ -25,11 +25,15 @@ const Item = React.memo(({
|
|||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.itemHorizontal}>
|
||||
{right}
|
||||
</View>
|
||||
</Touch>
|
||||
));
|
||||
|
||||
Item.propTypes = {
|
||||
left: PropTypes.element,
|
||||
right: PropTypes.element,
|
||||
text: PropTypes.string,
|
||||
current: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ScrollView, Text, View, FlatList, SafeAreaView
|
||||
ScrollView, Text, View, SafeAreaView
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import equal from 'deep-equal';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import Touch from '../../utils/touch';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import Status from '../../containers/Status/Status';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import log from '../../utils/log';
|
||||
import I18n from '../../i18n';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
|
@ -19,12 +16,10 @@ import styles from './styles';
|
|||
import SidebarItem from './SidebarItem';
|
||||
import { themes } from '../../constants/colors';
|
||||
import database from '../../lib/database';
|
||||
import { animateNextTransition } from '../../utils/layoutAnimation';
|
||||
import { withTheme } from '../../theme';
|
||||
import { withSplit } from '../../split';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
|
||||
const keyExtractor = item => item.id;
|
||||
import Navigation from '../../lib/Navigation';
|
||||
|
||||
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />);
|
||||
Separator.propTypes = {
|
||||
|
@ -48,6 +43,7 @@ class Sidebar extends Component {
|
|||
theme: PropTypes.string,
|
||||
loadingServer: PropTypes.bool,
|
||||
useRealName: PropTypes.bool,
|
||||
allowStatusMessage: PropTypes.bool,
|
||||
split: PropTypes.bool
|
||||
}
|
||||
|
||||
|
@ -55,28 +51,23 @@ class Sidebar extends Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
showStatus: false,
|
||||
isAdmin: false,
|
||||
status: []
|
||||
isAdmin: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setStatus();
|
||||
this.setIsAdmin();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { user, loadingServer } = this.props;
|
||||
if (nextProps.user && user && user.language !== nextProps.user.language) {
|
||||
this.setStatus();
|
||||
}
|
||||
const { loadingServer } = this.props;
|
||||
if (loadingServer && nextProps.loadingServer !== loadingServer) {
|
||||
this.setIsAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { status, showStatus, isAdmin } = this.state;
|
||||
const { showStatus, isAdmin } = this.state;
|
||||
const {
|
||||
Site_Name, user, baseUrl, activeItemKey, split, useRealName, theme
|
||||
} = this.props;
|
||||
|
@ -108,6 +99,9 @@ class Sidebar extends Component {
|
|||
if (nextProps.user.username !== user.username) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.user.statusText !== user.statusText) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (nextProps.split !== split) {
|
||||
return true;
|
||||
|
@ -115,33 +109,12 @@ class Sidebar extends Component {
|
|||
if (nextProps.useRealName !== useRealName) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.status, status)) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.isAdmin !== isAdmin) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setStatus = () => {
|
||||
this.setState({
|
||||
status: [{
|
||||
id: 'online',
|
||||
name: I18n.t('Online')
|
||||
}, {
|
||||
id: 'busy',
|
||||
name: I18n.t('Busy')
|
||||
}, {
|
||||
id: 'away',
|
||||
name: I18n.t('Away')
|
||||
}, {
|
||||
id: 'offline',
|
||||
name: I18n.t('Invisible')
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async setIsAdmin() {
|
||||
const db = database.active;
|
||||
const { user } = this.props;
|
||||
|
@ -165,32 +138,6 @@ class Sidebar extends Component {
|
|||
navigation.navigate(route);
|
||||
}
|
||||
|
||||
toggleStatus = () => {
|
||||
animateNextTransition();
|
||||
this.setState(prevState => ({ showStatus: !prevState.showStatus }));
|
||||
}
|
||||
|
||||
renderStatusItem = ({ item }) => {
|
||||
const { user } = this.props;
|
||||
return (
|
||||
<SidebarItem
|
||||
text={item.name}
|
||||
left={<Status style={styles.status} size={12} status={item.id} />}
|
||||
current={user.status === item.id}
|
||||
onPress={() => {
|
||||
this.toggleStatus();
|
||||
if (user.status !== item.id) {
|
||||
try {
|
||||
RocketChat.setUserPresenceDefaultStatus(item.id);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderNavigation = () => {
|
||||
const { isAdmin } = this.state;
|
||||
const { activeItemKey, theme } = this.props;
|
||||
|
@ -230,23 +177,22 @@ class Sidebar extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderStatus = () => {
|
||||
const { status } = this.state;
|
||||
const { user } = this.props;
|
||||
renderCustomStatus = () => {
|
||||
const { user, theme } = this.props;
|
||||
return (
|
||||
<FlatList
|
||||
data={status}
|
||||
extraData={user}
|
||||
renderItem={this.renderStatusItem}
|
||||
keyExtractor={keyExtractor}
|
||||
<SidebarItem
|
||||
text={user.statusText || I18n.t('Edit_Status')}
|
||||
left={<Status style={styles.status} size={12} status={user && user.status} />}
|
||||
right={<CustomIcon name='edit' size={20} color={themes[theme].titleText} />}
|
||||
onPress={() => Navigation.navigate('StatusView')}
|
||||
testID='sidebar-custom-status'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showStatus } = this.state;
|
||||
const {
|
||||
user, Site_Name, baseUrl, useRealName, split, theme
|
||||
user, Site_Name, baseUrl, useRealName, allowStatusMessage, split, theme
|
||||
} = this.props;
|
||||
|
||||
if (!user) {
|
||||
|
@ -265,12 +211,7 @@ class Sidebar extends Component {
|
|||
]}
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
<Touch
|
||||
onPress={this.toggleStatus}
|
||||
testID='sidebar-toggle-status'
|
||||
style={styles.header}
|
||||
theme={theme}
|
||||
>
|
||||
<View style={styles.header} theme={theme}>
|
||||
<Avatar
|
||||
text={user.username}
|
||||
size={30}
|
||||
|
@ -281,19 +222,22 @@ class Sidebar extends Component {
|
|||
/>
|
||||
<View style={styles.headerTextContainer}>
|
||||
<View style={styles.headerUsername}>
|
||||
<Status style={styles.status} size={12} status={user && user.status} theme={theme} />
|
||||
<Text numberOfLines={1} style={[styles.username, { color: themes[theme].titleText }]}>{useRealName ? user.name : user.username}</Text>
|
||||
</View>
|
||||
<Text style={[styles.currentServerText, { color: themes[theme].titleText }]} numberOfLines={1}>{Site_Name}</Text>
|
||||
</View>
|
||||
<CustomIcon name='arrow-down' size={20} style={[styles.headerIcon, showStatus && styles.inverted, { color: themes[theme].titleText }]} />
|
||||
</Touch>
|
||||
</View>
|
||||
|
||||
{!split ? <Separator theme={theme} /> : null}
|
||||
<Separator theme={theme} />
|
||||
|
||||
{!showStatus && !split ? this.renderNavigation() : null}
|
||||
{showStatus ? this.renderStatus() : null}
|
||||
{!split ? <Separator theme={theme} /> : null}
|
||||
{allowStatusMessage ? this.renderCustomStatus() : null}
|
||||
{!split ? (
|
||||
<>
|
||||
<Separator theme={theme} />
|
||||
{this.renderNavigation()}
|
||||
<Separator theme={theme} />
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
@ -305,7 +249,8 @@ const mapStateToProps = state => ({
|
|||
user: getUserSelector(state),
|
||||
baseUrl: state.server.server,
|
||||
loadingServer: state.server.loading,
|
||||
useRealName: state.settings.UI_Use_Real_Name
|
||||
useRealName: state.settings.UI_Use_Real_Name,
|
||||
allowStatusMessage: state.settings.Accounts_AllowUserStatusMessageChange
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(withSplit(Sidebar)));
|
||||
|
|
|
@ -13,7 +13,7 @@ export default StyleSheet.create({
|
|||
itemCurrent: {
|
||||
backgroundColor: '#E1E5E8'
|
||||
},
|
||||
itemLeft: {
|
||||
itemHorizontal: {
|
||||
marginHorizontal: 10,
|
||||
width: 30,
|
||||
alignItems: 'center'
|
||||
|
@ -48,9 +48,6 @@ export default StyleSheet.create({
|
|||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
headerIcon: {
|
||||
paddingHorizontal: 10
|
||||
},
|
||||
avatar: {
|
||||
marginHorizontal: 10
|
||||
},
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import I18n from '../i18n';
|
||||
import Separator from '../containers/Separator';
|
||||
import ListItem from '../containers/ListItem';
|
||||
import Status from '../containers/Status/Status';
|
||||
import TextInput from '../containers/TextInput';
|
||||
import EventEmitter from '../utils/events';
|
||||
import Loading from '../containers/Loading';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import log from '../utils/log';
|
||||
|
||||
import { LISTENER } from '../containers/Toast';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
import { withSplit } from '../split';
|
||||
import { themedHeader } from '../utils/navigation';
|
||||
import { getUserSelector } from '../selectors/login';
|
||||
import { CustomHeaderButtons, Item, CancelModalButton } from '../containers/HeaderButton';
|
||||
|
||||
const STATUS = [{
|
||||
id: 'online',
|
||||
name: I18n.t('Online')
|
||||
}, {
|
||||
id: 'busy',
|
||||
name: I18n.t('Busy')
|
||||
}, {
|
||||
id: 'away',
|
||||
name: I18n.t('Away')
|
||||
}, {
|
||||
id: 'offline',
|
||||
name: I18n.t('Invisible')
|
||||
}];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
status: {
|
||||
marginRight: 16
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 32,
|
||||
marginBottom: 32
|
||||
},
|
||||
inputLeft: {
|
||||
position: 'absolute',
|
||||
top: 18,
|
||||
left: 14
|
||||
},
|
||||
inputStyle: {
|
||||
paddingLeft: 40
|
||||
}
|
||||
});
|
||||
|
||||
class StatusView extends React.Component {
|
||||
static navigationOptions = ({ navigation, screenProps }) => ({
|
||||
title: I18n.t('Edit_Status'),
|
||||
headerLeft: <CancelModalButton onPress={navigation.getParam('close', () => {})} />,
|
||||
headerRight: (
|
||||
<CustomHeaderButtons>
|
||||
<Item
|
||||
title={I18n.t('Done')}
|
||||
onPress={navigation.getParam('submit', () => {})}
|
||||
testID='status-view-submit'
|
||||
/>
|
||||
</CustomHeaderButtons>
|
||||
),
|
||||
...themedHeader(screenProps.theme)
|
||||
})
|
||||
|
||||
static propTypes = {
|
||||
user: PropTypes.shape({
|
||||
status: PropTypes.string,
|
||||
statusText: PropTypes.string
|
||||
}),
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool,
|
||||
navigation: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { statusText } = props.user;
|
||||
this.state = { statusText, loading: false };
|
||||
|
||||
props.navigation.setParams({ submit: this.submit, close: this.close });
|
||||
}
|
||||
|
||||
submit = async() => {
|
||||
const { statusText } = this.state;
|
||||
const { user } = this.props;
|
||||
if (statusText !== user.statusText) {
|
||||
await this.setCustomStatus();
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
close = () => {
|
||||
const { navigation, split } = this.props;
|
||||
if (split) {
|
||||
navigation.goBack();
|
||||
} else {
|
||||
navigation.pop();
|
||||
}
|
||||
}
|
||||
|
||||
setCustomStatus = async() => {
|
||||
const { statusText } = this.state;
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
try {
|
||||
const result = await RocketChat.setUserStatus(statusText);
|
||||
if (result.success) {
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('Status_saved_successfully') });
|
||||
} else {
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('error-could-not-change-status') });
|
||||
}
|
||||
} catch {
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('error-could-not-change-status') });
|
||||
}
|
||||
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
renderSeparator = () => {
|
||||
const { theme } = this.props;
|
||||
return <Separator theme={theme} />;
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
const { statusText } = this.state;
|
||||
const { user, theme } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
theme={theme}
|
||||
value={statusText}
|
||||
containerStyle={styles.inputContainer}
|
||||
onChangeText={text => this.setState({ statusText: text })}
|
||||
left={(
|
||||
<Status
|
||||
testID={`status-view-current-${ user.status }`}
|
||||
style={styles.inputLeft}
|
||||
status={user.status}
|
||||
size={12}
|
||||
/>
|
||||
)}
|
||||
inputStyle={styles.inputStyle}
|
||||
placeholder={I18n.t('What_are_you_doing_right_now')}
|
||||
testID='status-view-input'
|
||||
/>
|
||||
<Separator theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderItem = ({ item }) => {
|
||||
const { theme, user } = this.props;
|
||||
const { id, name } = item;
|
||||
return (
|
||||
<ListItem
|
||||
title={name}
|
||||
onPress={async() => {
|
||||
if (user.status !== item.id) {
|
||||
try {
|
||||
await RocketChat.setUserPresenceDefaultStatus(item.id);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
testID={`status-view-${ id }`}
|
||||
left={() => <Status style={styles.status} size={12} status={item.id} />}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading } = this.state;
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: themes[theme].auxiliaryBackground }
|
||||
]}
|
||||
forceInset={{ vertical: 'never' }}
|
||||
testID='status-view'
|
||||
>
|
||||
<FlatList
|
||||
data={STATUS}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ borderColor: themes[theme].separatorColor }}
|
||||
renderItem={this.renderItem}
|
||||
ListHeaderComponent={this.renderHeader}
|
||||
ListFooterComponent={() => <Separator theme={theme} />}
|
||||
ItemSeparatorComponent={this.renderSeparator}
|
||||
/>
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: getUserSelector(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withSplit(withTheme(StatusView)));
|
|
@ -5,7 +5,7 @@ import {
|
|||
import PropTypes from 'prop-types';
|
||||
import ShareExtension from 'rn-extensions-share';
|
||||
|
||||
import { CloseShareExtensionButton } from '../containers/HeaderButton';
|
||||
import { CancelModalButton } from '../containers/HeaderButton';
|
||||
import sharedStyles from './Styles';
|
||||
import I18n from '../i18n';
|
||||
import { themes } from '../constants/colors';
|
||||
|
@ -34,7 +34,7 @@ class WithoutServerView extends React.Component {
|
|||
static navigationOptions = ({ screenProps }) => ({
|
||||
...themedHeader(screenProps.theme),
|
||||
headerLeft: (
|
||||
<CloseShareExtensionButton
|
||||
<CancelModalButton
|
||||
onPress={ShareExtension.close}
|
||||
testID='share-extension-close'
|
||||
/>
|
||||
|
|
|
@ -34,6 +34,10 @@ describe('Profile screen', () => {
|
|||
await expect(element(by.id('profile-view-avatar')).atIndex(0)).toExist();
|
||||
});
|
||||
|
||||
it('should have custom status', async() => {
|
||||
await expect(element(by.id('profile-view-custom-status'))).toExist();
|
||||
});
|
||||
|
||||
it('should have name', async() => {
|
||||
await expect(element(by.id('profile-view-name'))).toExist();
|
||||
});
|
||||
|
@ -76,6 +80,16 @@ describe('Profile screen', () => {
|
|||
});
|
||||
|
||||
describe('Usage', async() => {
|
||||
it('should change custom status', async() => {
|
||||
await element(by.type('UIScrollView')).atIndex(1).swipe('down');
|
||||
await element(by.id('profile-view-custom-status')).replaceText(`${ data.user }new`);
|
||||
await sleep(1000);
|
||||
await element(by.type('UIScrollView')).atIndex(1).swipe('up');
|
||||
await sleep(1000);
|
||||
await element(by.id('profile-view-submit')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
|
||||
it('should change name and username', async() => {
|
||||
await element(by.type('UIScrollView')).atIndex(1).swipe('down');
|
||||
await element(by.id('profile-view-name')).replaceText(`${ data.user }new`);
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
const {
|
||||
expect, element, by, waitFor
|
||||
} = require('detox');
|
||||
const { sleep } = require('./helpers/app');
|
||||
|
||||
async function waitForToast() {
|
||||
await sleep(5000);
|
||||
}
|
||||
|
||||
describe('Status screen', () => {
|
||||
before(async() => {
|
||||
await element(by.id('rooms-list-view-sidebar')).tap();
|
||||
await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
|
||||
await waitFor(element(by.id('sidebar-custom-status'))).toBeVisible().withTimeout(2000);
|
||||
|
||||
await element(by.id('sidebar-custom-status')).tap();
|
||||
await waitFor(element(by.id('status-view'))).toBeVisible().withTimeout(2000);
|
||||
});
|
||||
|
||||
describe('Render', async() => {
|
||||
it('should have status input', async() => {
|
||||
await expect(element(by.id('status-view-input'))).toBeVisible();
|
||||
await expect(element(by.id('status-view-online'))).toExist();
|
||||
await expect(element(by.id('status-view-busy'))).toExist();
|
||||
await expect(element(by.id('status-view-away'))).toExist();
|
||||
await expect(element(by.id('status-view-offline'))).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Usage', async() => {
|
||||
it('should change status', async() => {
|
||||
await element(by.id('status-view-busy')).tap();
|
||||
sleep(1000);
|
||||
await expect(element(by.id('status-view-current-busy'))).toExist();
|
||||
});
|
||||
|
||||
it('should change status text', async() => {
|
||||
await element(by.id('status-view-input')).replaceText('status-text-new');
|
||||
await sleep(1000);
|
||||
await element(by.id('status-view-submit')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,7 +22,7 @@ const reducers = combineReducers({
|
|||
}
|
||||
}),
|
||||
meteor: () => ({ connected: true }),
|
||||
activeUsers: () => ({ abc: 'online' })
|
||||
activeUsers: () => ({ abc: { status: 'online', statusText: 'dog' } })
|
||||
});
|
||||
const store = createStore(reducers);
|
||||
|
||||
|
|
Loading…
Reference in New Issue