[NEW] Invite links (#1534)

This commit is contained in:
Diego Mello 2020-01-28 10:22:35 -03:00 committed by GitHub
parent ba27c580f4
commit 0673081465
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 671 additions and 26 deletions

View File

@ -54,3 +54,11 @@ export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']);
export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
'SET_TOKEN',
'SET_PARAMS',
'SET_INVITE',
'CREATE',
'CLEAR',
...defaultTypes
]);

View File

@ -0,0 +1,55 @@
import * as types from './actionsTypes';
export function inviteLinksSetToken(token) {
return {
type: types.INVITE_LINKS.SET_TOKEN,
token
};
}
export function inviteLinksRequest(token) {
return {
type: types.INVITE_LINKS.REQUEST,
token
};
}
export function inviteLinksSuccess() {
return {
type: types.INVITE_LINKS.SUCCESS
};
}
export function inviteLinksFailure() {
return {
type: types.INVITE_LINKS.FAILURE
};
}
export function inviteLinksClear() {
return {
type: types.INVITE_LINKS.CLEAR
};
}
export function inviteLinksCreate(rid) {
return {
type: types.INVITE_LINKS.CREATE,
rid
};
}
export function inviteLinksSetParams(params) {
return {
type: types.INVITE_LINKS.SET_PARAMS,
params
};
}
export function inviteLinksSetInvite(invite) {
return {
type: types.INVITE_LINKS.SET_INVITE,
invite
};
}

View File

@ -59,6 +59,9 @@ export default {
Message_TimeFormat: {
type: 'valueAsString'
},
Message_TimeAndDateFormat: {
type: 'valueAsString'
},
Site_Name: {
type: 'valueAsString'
},

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Aktivität',
Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen',
Add_user: 'Nutzer hinzufügen',
Add_users: 'Nutzer hinzufügen',
Admin_Panel: 'Admin Panel',
Alert: 'Warnen',
alert: 'warnen',

View File

@ -81,7 +81,7 @@ export default {
Activity: 'Activity',
Add_Reaction: 'Add Reaction',
Add_Server: 'Add Server',
Add_user: 'Add user',
Add_users: 'Add users',
Admin_Panel: 'Admin Panel',
Alert: 'Alert',
alert: 'alert',
@ -121,6 +121,7 @@ export default {
Cancel: 'Cancel',
changing_avatar: 'changing avatar',
creating_channel: 'creating channel',
creating_invite: 'creating invite',
Channel_Name: 'Channel Name',
Channels: 'Channels',
Chats: 'Chats',
@ -172,6 +173,7 @@ export default {
edit: 'edit',
edited: 'edited',
Edit: 'Edit',
Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
EMAIL: 'EMAIL',
@ -182,6 +184,7 @@ export default {
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
Expiration_Days: 'Expiration (Days)',
Favorite: 'Favorite',
Favorites: 'Favorites',
Files: 'Files',
@ -195,6 +198,7 @@ export default {
Forgot_password: 'Forgot password',
Forgot_Password: 'Forgot Password',
Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type',
Hide: 'Hide',
@ -208,7 +212,10 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing',
Invalid_or_expired_invite_token: 'Invalid or expired invite token',
Invalid_server_version: 'The server you\'re trying to connect is using a version that\'s not supported by the app anymore: {{currentVersion}}.\n\nWe require version {{minVersion}}',
Invite_Link: 'Invite Link',
Invite_users: 'Invite users',
Join_the_community: 'Join the community',
Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
@ -225,6 +232,7 @@ export default {
Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with',
Logout: 'Logout',
Max_number_of_uses: 'Max number of uses',
members: 'members',
Members: 'Members',
Mentioned_Messages: 'Mentioned Messages',
@ -247,11 +255,13 @@ export default {
N_users: '{{n}} users',
name: 'name',
Name: 'Name',
Never: 'Never',
New_Message: 'New Message',
New_Password: 'New Password',
New_Server: 'New Server',
Next: 'Next',
No_files: 'No files',
No_limit: 'No limit',
No_mentioned_messages: 'No mentioned messages',
No_pinned_messages: 'No pinned messages',
No_results_found: 'No results found',
@ -362,6 +372,7 @@ export default {
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share',
Share_Link: 'Share Link',
Share_this_app: 'Share this app',
Show_Unread_Counter: 'Show Unread Counter',
Show_Unread_Counter_Info: 'Unread counter is displayed as a badge on the right of the channel, in the list',
@ -449,6 +460,10 @@ export default {
You: 'You',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.',
Your_certificate: 'Your Certificate',
Your_invite_link_will_expire_after__usesLeft__uses: 'Your invite link will expire after {{usesLeft}} uses.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Your invite link will expire on {{date}} or after {{usesLeft}} uses.',
Your_invite_link_will_expire_on__date__: 'Your invite link will expire on {{date}}.',
Your_invite_link_will_never_expire: 'Your invite link will never expire.',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
Change_Language: 'Change Language',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Activité',
Add_Reaction: 'Ajouter une réaction',
Add_Server: 'Ajouter un serveur',
Add_user: 'Ajouter un utilisateur',
Add_users: 'Ajouter des utilisateurs',
Alert: 'Alerte',
alert: 'alerte',
alerts: 'alertes',

View File

@ -88,7 +88,7 @@ export default {
Activity: 'Atividade',
Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor',
Add_user: 'Adicionar usuário',
Add_users: 'Adicionar usuário',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
@ -123,6 +123,7 @@ export default {
Cancel: 'Cancelar',
changing_avatar: 'trocando avatar',
creating_channel: 'criando canal',
creating_invite: 'criando convite',
Channel_Name: 'Nome do Canal',
Channels: 'Canais',
Chats: 'Conversas',
@ -169,6 +170,7 @@ export default {
edited: 'editado',
erasing_room: 'apagando sala',
Edit: 'Editar',
Edit_Invite: 'Editar convite',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
@ -176,6 +178,7 @@ export default {
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
Expiration_Days: 'Expira em (dias)',
Favorites: 'Favoritos',
Files: 'Arquivos',
File_description: 'Descrição do arquivo',
@ -188,6 +191,7 @@ export default {
Forgot_password: 'Esqueci minha senha',
Forgot_Password: 'Esqueci minha senha',
Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal',
@ -196,7 +200,10 @@ export default {
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
Invalid_or_expired_invite_token: 'Token de convite inválido ou vencido',
Invalid_server_version: 'O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.',
Invite_Link: 'Link de Convite',
Invite_users: 'Convidar usuários',
Join_the_community: 'Junte-se à comunidade',
Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',
@ -212,6 +219,7 @@ export default {
Login_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!',
Login_with: 'Login with',
Logout: 'Sair',
Max_number_of_uses: 'Número máximo de usos',
Members: 'Membros',
Mentioned_Messages: 'Mensagens mencionadas',
mentioned: 'mencionado',
@ -231,11 +239,13 @@ export default {
N_users: '{{n}} usuários',
name: 'nome',
Name: 'Nome',
Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem',
New_Password: 'Nova Senha',
Next: 'Próximo',
No_files: 'Não há arquivos',
No_limit: 'Sem limite',
No_mentioned_messages: 'Não há menções',
No_pinned_messages: 'Não há mensagens fixadas',
No_results_found: 'Nenhum resultado encontrado',
@ -328,6 +338,7 @@ export default {
Settings: 'Configurações',
Settings_succesfully_changed: 'Configurações salvas com sucesso!',
Share: 'Compartilhar',
Share_Link: 'Share Link',
Sign_in_your_server: 'Entrar no seu servidor',
Sign_Up: 'Registrar',
Some_field_is_invalid_or_empty: 'Algum campo está inválido ou vazio',
@ -401,7 +412,10 @@ export default {
you_were_mentioned: 'você foi mencionado',
you: 'você',
You: 'Você',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Você precisa acessar ao menos um servidor Rocket.Chat para compartilhar.',
Your_invite_link_will_expire_after__usesLeft__uses: 'Seu link de convite irá vencer depois de {{usesLeft}} usos.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Seu link de convite irá vencer em {{date}} ou depois de {{usesLeft}} usos.',
Your_invite_link_will_expire_on__date__: 'Seu link de convite irá vencer em {{date}}.',
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Actividade',
Add_Reaction: 'Adicionar Reacção',
Add_Server: 'Adicionar Servidor',
Add_user: 'Adicionar utilizador',
Add_users: 'Adicionar utilizadores',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',

View File

@ -80,7 +80,7 @@ export default {
Activity: 'Активность',
Add_Reaction: 'Добавить реакцию',
Add_Server: 'Добавить сервер',
Add_user: 'Добавить пользователя',
Add_users: 'Добавить пользователей',
Admin_Panel: 'Панель админа',
Alert: 'Оповещение',
alert: 'оповещение',

View File

@ -80,7 +80,7 @@ export default {
Activity: '按活动排序',
Add_Reaction: '增加回复',
Add_Server: '添加服务器',
Add_user: '添加用户',
Add_users: '添加用户',
Alert: '警告',
alert: '警告',
alerts: '警告',

View File

@ -49,7 +49,7 @@ if (isIOS) {
const parseDeepLinking = (url) => {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
const regex = /^(room|auth|invite)\?/;
if (url.match(regex)) {
url = url.replace(regex, '').trim();
if (url) {
@ -146,6 +146,12 @@ const ChatsStack = createStackNavigator({
SelectedUsersView: {
getScreen: () => require('./views/SelectedUsersView').default
},
InviteUsersView: {
getScreen: () => require('./views/InviteUsersView').default
},
InviteUsersEditView: {
getScreen: () => require('./views/InviteUsersEditView').default
},
MessagesView: {
getScreen: () => require('./views/MessagesView').default
},

View File

@ -1098,6 +1098,23 @@ const RocketChat = {
},
translateMessage(message, targetLanguage) {
return this.sdk.methodCall('autoTranslate.translateMessage', message, targetLanguage);
},
getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
return ((room.prid || useRealName) && room.fname) || room.name;
},
findOrCreateInvite({ rid, days, maxUses }) {
// RC 2.4.0
return this.sdk.post('findOrCreateInvite', { rid, days, maxUses });
},
validateInviteToken(token) {
// RC 2.4.0
return this.sdk.post('validateInviteToken', { token });
},
useInviteToken(token) {
// RC 2.4.0
return this.sdk.post('useInviteToken', { token });
}
};

View File

@ -15,6 +15,7 @@ import crashReport from './crashReport';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import usersTyping from './usersTyping';
import inviteLinks from './inviteLinks';
export default combineReducers({
settings,
@ -32,5 +33,6 @@ export default combineReducers({
crashReport,
customEmojis,
activeUsers,
usersTyping
usersTyping,
inviteLinks
});

View File

@ -0,0 +1,37 @@
import { INVITE_LINKS } from '../actions/actionsTypes';
const initialState = {
token: '',
days: 1,
maxUses: 0,
invite: {}
};
export default (state = initialState, action) => {
switch (action.type) {
case INVITE_LINKS.SET_TOKEN:
return {
token: action.token
};
case INVITE_LINKS.SET_PARAMS:
return {
...state,
...action.params
};
case INVITE_LINKS.SET_INVITE:
return {
...state,
invite: action.invite
};
case INVITE_LINKS.REQUEST:
return state;
case INVITE_LINKS.SUCCESS:
return initialState;
case INVITE_LINKS.FAILURE:
return initialState;
case INVITE_LINKS.CLEAR:
return initialState;
default:
return state;
}
};

View File

@ -6,6 +6,7 @@ import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes';
import { selectServerRequest } from '../actions/server';
import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks';
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
@ -15,6 +16,17 @@ const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
};
const handleInviteLink = function* handleInviteLink({ params, requireLogin = false }) {
if (params.path && params.path.startsWith('invite/')) {
const token = params.path.replace('invite/', '');
if (requireLogin) {
yield put(inviteLinksSetToken(token));
} else {
yield put(inviteLinksRequest(token));
}
}
};
const navigate = function* navigate({ params }) {
yield put(appStart('inside'));
if (params.rid) {
@ -24,6 +36,8 @@ const navigate = function* navigate({ params }) {
yield Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { rid: params.rid, name, t: roomTypes[type] });
}
} else {
yield handleInviteLink({ params });
}
};
@ -63,7 +77,7 @@ const handleOpen = function* handleOpen({ params }) {
const servers = yield serversCollection.find(host);
if (servers && user) {
yield put(selectServerRequest(host));
yield take(types.SERVER.SELECT_SUCCESS);
yield take(types.LOGIN.SUCCESS);
yield navigate({ params });
return;
}
@ -82,6 +96,8 @@ const handleOpen = function* handleOpen({ params }) {
if (params.token) {
yield take(types.SERVER.SELECT_SUCCESS);
yield RocketChat.connect({ server: host, user: { token: params.token } });
} else {
yield handleInviteLink({ params, requireLogin: true });
}
}
};

View File

@ -8,6 +8,7 @@ import createChannel from './createChannel';
import init from './init';
import state from './state';
import deepLinking from './deepLinking';
import inviteLinks from './inviteLinks';
const root = function* root() {
yield all([
@ -19,7 +20,8 @@ const root = function* root() {
messages(),
selectServer(),
state(),
deepLinking()
deepLinking(),
inviteLinks()
]);
};

72
app/sagas/inviteLinks.js Normal file
View File

@ -0,0 +1,72 @@
import {
put, takeLatest, delay, select
} from 'redux-saga/effects';
import { Alert } from 'react-native';
import { INVITE_LINKS } from '../actions/actionsTypes';
import { inviteLinksSuccess, inviteLinksFailure, inviteLinksSetInvite } from '../actions/inviteLinks';
import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import I18n from '../i18n';
const handleRequest = function* handleRequest({ token }) {
try {
const validateResult = yield RocketChat.validateInviteToken(token);
if (!validateResult.valid) {
yield put(inviteLinksFailure());
return;
}
const result = yield RocketChat.useInviteToken(token);
if (!result.success) {
yield put(inviteLinksFailure());
return;
}
if (result.room && result.room.rid) {
yield delay(1000);
yield Navigation.navigate('RoomsListView');
const { room } = result;
Navigation.navigate('RoomView', {
rid: room.rid,
name: RocketChat.getRoomTitle(room),
t: room.t
});
}
yield put(inviteLinksSuccess());
} catch (e) {
yield put(inviteLinksFailure());
log(e);
}
};
const handleFailure = function handleFailure() {
Alert.alert(I18n.t('Oops'), I18n.t('Invalid_or_expired_invite_token'));
};
const handleCreateInviteLink = function* handleCreateInviteLink({ rid }) {
try {
const inviteLinks = yield select(state => state.inviteLinks);
const result = yield RocketChat.findOrCreateInvite({
rid, days: inviteLinks.days, maxUses: inviteLinks.maxUses
});
if (!result.success) {
Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_invite') }));
return;
}
yield put(inviteLinksSetInvite(result));
} catch (e) {
log(e);
}
};
const root = function* root() {
yield takeLatest(INVITE_LINKS.REQUEST, handleRequest);
yield takeLatest(INVITE_LINKS.FAILURE, handleFailure);
yield takeLatest(INVITE_LINKS.CREATE, handleCreateInviteLink);
};
export default root;

View File

@ -20,6 +20,7 @@ import I18n from '../i18n';
import database from '../lib/database';
import EventEmitter from '../utils/events';
import Navigation from '../lib/Navigation';
import { inviteLinksRequest } from '../actions/inviteLinks';
const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -115,17 +116,27 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield put(setUser(user));
EventEmitter.emit('connected');
let currentRoot;
if (!user.username) {
yield put(appStart('setUsername'));
} else if (adding) {
yield put(serverFinishAdd());
yield put(appStart('inside'));
} else {
const currentRoot = yield select(state => state.app.root);
currentRoot = yield select(state => state.app.root);
if (currentRoot !== 'inside') {
yield put(appStart('inside'));
}
}
// after a successful login, check if it's been invited via invite link
currentRoot = yield select(state => state.app.root);
if (currentRoot === 'inside') {
const inviteLinkToken = yield select(state => state.inviteLinks.token);
if (inviteLinkToken) {
yield put(inviteLinksRequest(inviteLinkToken));
}
}
} catch (e) {
log(e);
}

View File

@ -0,0 +1,160 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select';
import {
inviteLinksSetParams as inviteLinksSetParamsAction,
inviteLinksCreate as inviteLinksCreateAction
} from '../../actions/inviteLinks';
import ListItem from '../../containers/ListItem';
import styles from './styles';
import Button from '../../containers/Button';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import Separator from '../../containers/Separator';
const OPTIONS = {
days: [{
label: I18n.t('Never'), value: 0
},
{
label: '1', value: 1
},
{
label: '7', value: 7
},
{
label: '15', value: 15
},
{
label: '30', value: 30
}],
maxUses: [{
label: I18n.t('No_limit'), value: 0
},
{
label: '1', value: 1
},
{
label: '5', value: 5
},
{
label: '10', value: 10
},
{
label: '25', value: 25
},
{
label: '50', value: 50
},
{
label: '100', value: 100
}]
};
class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Invite_users'),
...themedHeader(screenProps.theme)
})
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
timeDateFormat: PropTypes.string,
createInviteLink: PropTypes.func,
inviteLinksSetParams: PropTypes.func
}
constructor(props) {
super(props);
this.rid = props.navigation.getParam('rid');
}
onValueChangePicker = (key, value) => {
const { inviteLinksSetParams } = this.props;
const params = {
[key]: value
};
inviteLinksSetParams(params);
}
createInviteLink = () => {
const { createInviteLink, navigation } = this.props;
createInviteLink(this.rid);
navigation.pop();
}
renderPicker = (key) => {
const { props } = this;
const { theme } = props;
return (
<RNPickerSelect
style={{ viewContainer: styles.viewContainer }}
value={props[key]}
textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }}
useNativeAndroidPickerStyle={false}
placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)}
items={OPTIONS[key]}
/>
);
}
render() {
const { theme } = this.props;
return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}>
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
<StatusBar theme={theme} />
<Separator theme={theme} />
<ListItem
title={I18n.t('Expiration_Days')}
right={() => this.renderPicker('days')}
theme={theme}
/>
<Separator theme={theme} />
<ListItem
title={I18n.t('Max_number_of_uses')}
right={() => this.renderPicker('maxUses')}
theme={theme}
/>
<Separator theme={theme} />
<View style={styles.innerContainer}>
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
<Button
title={I18n.t('Generate_New_Link')}
type='primary'
onPress={this.createInviteLink}
theme={theme}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses
});
const mapDispatchToProps = dispatch => ({
inviteLinksSetParams: params => dispatch(inviteLinksSetParamsAction(params)),
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView));

View File

@ -0,0 +1,45 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles';
export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: {
paddingHorizontal: 20
},
divider: {
width: '100%',
height: StyleSheet.hairlineWidth,
marginVertical: 20
},
sectionSeparatorBorder: {
height: 10
},
marginBottom: {
height: 30
},
contentContainer: {
marginVertical: 10
},
infoText: {
...sharedStyles.textRegular,
fontSize: 13,
paddingHorizontal: 15,
paddingVertical: 10
},
sectionTitle: {
...sharedStyles.separatorBottom,
paddingHorizontal: 15,
paddingVertical: 10,
fontSize: 14
},
viewContainer: {
justifyContent: 'center'
},
pickerText: {
...sharedStyles.textRegular,
fontSize: 16
}
});

View File

@ -0,0 +1,151 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Share, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import moment from 'moment';
import { connect } from 'react-redux';
import {
inviteLinksCreate as inviteLinksCreateAction,
inviteLinksClear as inviteLinksClearAction
} from '../../actions/inviteLinks';
import RCTextInput from '../../containers/TextInput';
import styles from './styles';
import Markdown from '../../containers/markdown';
import Button from '../../containers/Button';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
class InviteUsersView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Invite_users'),
...themedHeader(screenProps.theme)
})
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
timeDateFormat: PropTypes.string,
invite: PropTypes.object,
createInviteLink: PropTypes.func,
clearInviteLink: PropTypes.func
}
constructor(props) {
super(props);
this.rid = props.navigation.getParam('rid');
}
componentDidMount() {
const { createInviteLink } = this.props;
createInviteLink(this.rid);
}
componentWillUnmount() {
const { clearInviteLink } = this.props;
clearInviteLink();
}
share = () => {
const { invite } = this.props;
if (!invite) {
return;
}
Share.share({ message: invite.url });
}
edit = () => {
const { navigation } = this.props;
navigation.navigate('InviteUsersEditView', { rid: this.rid });
}
linkExpirationText = () => {
const { timeDateFormat, invite } = this.props;
if (!invite || !invite.url) {
return null;
}
if (invite.expires) {
const expiration = new Date(invite.expires);
if (invite.maxUses) {
const usesLeft = invite.maxUses - invite.uses;
return I18n.t('Your_invite_link_will_expire_on__date__or_after__usesLeft__uses', { date: moment(expiration).format(timeDateFormat), usesLeft });
}
return I18n.t('Your_invite_link_will_expire_on__date__', { date: moment(expiration).format(timeDateFormat) });
}
if (invite.maxUses) {
const usesLeft = invite.maxUses - invite.uses;
return I18n.t('Your_invite_link_will_expire_after__usesLeft__uses', { usesLeft });
}
return I18n.t('Your_invite_link_will_never_expire');
}
renderExpiration = () => {
const { theme } = this.props;
const expirationMessage = this.linkExpirationText();
return <Markdown msg={expirationMessage} username='' baseUrl='' theme={theme} />;
}
render() {
const {
theme, invite
} = this.props;
return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} forceInset={{ vertical: 'never' }}>
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
<StatusBar theme={theme} />
<View style={styles.innerContainer}>
<RCTextInput
label={I18n.t('Invite_Link')}
theme={theme}
value={invite && invite.url}
editable={false}
/>
{this.renderExpiration()}
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
<Button
title={I18n.t('Share_Link')}
type='primary'
onPress={this.share}
theme={theme}
/>
<Button
title={I18n.t('Edit_Invite')}
type='secondary'
onPress={this.edit}
theme={theme}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
timeDateFormat: state.settings.Message_TimeAndDateFormat,
days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses,
invite: state.inviteLinks.invite
});
const mapDispatchToProps = dispatch => ({
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid)),
clearInviteLink: () => dispatch(inviteLinksClearAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(InviteUsersView));

View File

@ -0,0 +1,16 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1
},
innerContainer: {
padding: 20,
paddingBottom: 0
},
divider: {
width: '100%',
height: StyleSheet.hairlineWidth,
marginVertical: 20
}
});

View File

@ -62,7 +62,8 @@ class RoomActionsView extends React.Component {
joined: !!room,
canViewMembers: false,
canAutoTranslate: false,
canAddUser: false
canAddUser: false,
canInviteUser: false
};
if (room && room.observe && room.rid) {
this.roomObservable = room.observe();
@ -108,6 +109,7 @@ class RoomActionsView extends React.Component {
this.setState({ canAutoTranslate });
this.canAddUser();
this.canInviteUser();
}
componentWillUnmount() {
@ -126,7 +128,6 @@ class RoomActionsView extends React.Component {
}
}
// TODO: move to componentDidMount
// eslint-disable-next-line react/sort-comp
canAddUser = async() => {
const { room, joined } = this.state;
@ -150,8 +151,15 @@ class RoomActionsView extends React.Component {
this.setState({ canAddUser: canAdd });
}
// TODO: move to componentDidMount
// eslint-disable-next-line react/sort-comp
canInviteUser = async() => {
const { room } = this.state;
const { rid } = room;
const permissions = await RocketChat.hasPermission(['create-invite-links'], rid);
const canInviteUser = permissions && permissions['create-invite-links'];
this.setState({ canInviteUser });
}
canViewMembers = async() => {
const { room } = this.state;
const { rid, t, broadcast } = room;
@ -172,7 +180,7 @@ class RoomActionsView extends React.Component {
get sections() {
const {
room, membersCount, canViewMembers, canAddUser, joined, canAutoTranslate
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
} = this.state;
const { jitsiEnabled } = this.props;
const {
@ -302,17 +310,28 @@ class RoomActionsView extends React.Component {
if (canAddUser) {
actions.push({
icon: 'user-plus',
name: I18n.t('Add_user'),
icon: 'plus',
name: I18n.t('Add_users'),
route: 'SelectedUsersView',
params: {
nextActionID: 'ADD_USER',
rid,
title: I18n.t('Add_user')
title: I18n.t('Add_users')
},
testID: 'room-actions-add-user'
});
}
if (canInviteUser) {
actions.push({
icon: 'user-plus',
name: I18n.t('Invite_users'),
route: 'InviteUsersView',
params: {
rid
},
testID: 'room-actions-invite-user'
});
}
sections[2].data = [...actions, ...sections[2].data];
if (joined) {

View File

@ -161,7 +161,6 @@ class RoomsListView extends React.Component {
groupByType: PropTypes.bool,
showFavorites: PropTypes.bool,
showUnread: PropTypes.bool,
useRealName: PropTypes.bool,
StoreLastMessage: PropTypes.bool,
appState: PropTypes.string,
theme: PropTypes.string,
@ -486,10 +485,7 @@ class RoomsListView extends React.Component {
});
}, 300);
getRoomTitle = (item) => {
const { useRealName } = this.props;
return ((item.prid || useRealName) && item.fname) || item.name;
};
getRoomTitle = item => RocketChat.getRoomTitle(item)
goRoom = (item) => {
this.cancelSearchingAndroid();