[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_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS'; export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']); 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: { Message_TimeFormat: {
type: 'valueAsString' type: 'valueAsString'
}, },
Message_TimeAndDateFormat: {
type: 'valueAsString'
},
Site_Name: { Site_Name: {
type: 'valueAsString' type: 'valueAsString'
}, },

View File

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

View File

@ -81,7 +81,7 @@ export default {
Activity: 'Activity', Activity: 'Activity',
Add_Reaction: 'Add Reaction', Add_Reaction: 'Add Reaction',
Add_Server: 'Add Server', Add_Server: 'Add Server',
Add_user: 'Add user', Add_users: 'Add users',
Admin_Panel: 'Admin Panel', Admin_Panel: 'Admin Panel',
Alert: 'Alert', Alert: 'Alert',
alert: 'alert', alert: 'alert',
@ -121,6 +121,7 @@ export default {
Cancel: 'Cancel', Cancel: 'Cancel',
changing_avatar: 'changing avatar', changing_avatar: 'changing avatar',
creating_channel: 'creating channel', creating_channel: 'creating channel',
creating_invite: 'creating invite',
Channel_Name: 'Channel Name', Channel_Name: 'Channel Name',
Channels: 'Channels', Channels: 'Channels',
Chats: 'Chats', Chats: 'Chats',
@ -172,6 +173,7 @@ export default {
edit: 'edit', edit: 'edit',
edited: 'edited', edited: 'edited',
Edit: 'Edit', Edit: 'Edit',
Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty', Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email', Email: 'Email',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
@ -182,6 +184,7 @@ export default {
Everyone_can_access_this_channel: 'Everyone can access this channel', Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room', erasing_room: 'erasing room',
Error_uploading: 'Error uploading', Error_uploading: 'Error uploading',
Expiration_Days: 'Expiration (Days)',
Favorite: 'Favorite', Favorite: 'Favorite',
Favorites: 'Favorites', Favorites: 'Favorites',
Files: 'Files', Files: 'Files',
@ -195,6 +198,7 @@ export default {
Forgot_password: 'Forgot password', Forgot_password: 'Forgot password',
Forgot_Password: 'Forgot Password', Forgot_Password: 'Forgot Password',
Full_table: 'Click to see full table', Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type', Group_by_type: 'Group by type',
Hide: 'Hide', Hide: 'Hide',
@ -208,7 +212,10 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance', 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_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing', 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}}', 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_the_community: 'Join the community',
Join: 'Join', Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel', 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_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with', Login_with: 'Login with',
Logout: 'Logout', Logout: 'Logout',
Max_number_of_uses: 'Max number of uses',
members: 'members', members: 'members',
Members: 'Members', Members: 'Members',
Mentioned_Messages: 'Mentioned Messages', Mentioned_Messages: 'Mentioned Messages',
@ -247,11 +255,13 @@ export default {
N_users: '{{n}} users', N_users: '{{n}} users',
name: 'name', name: 'name',
Name: 'Name', Name: 'Name',
Never: 'Never',
New_Message: 'New Message', New_Message: 'New Message',
New_Password: 'New Password', New_Password: 'New Password',
New_Server: 'New Server', New_Server: 'New Server',
Next: 'Next', Next: 'Next',
No_files: 'No files', No_files: 'No files',
No_limit: 'No limit',
No_mentioned_messages: 'No mentioned messages', No_mentioned_messages: 'No mentioned messages',
No_pinned_messages: 'No pinned messages', No_pinned_messages: 'No pinned messages',
No_results_found: 'No results found', No_results_found: 'No results found',
@ -362,6 +372,7 @@ export default {
Settings: 'Settings', Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!', Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share', Share: 'Share',
Share_Link: 'Share Link',
Share_this_app: 'Share this app', Share_this_app: 'Share this app',
Show_Unread_Counter: 'Show Unread Counter', 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', 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: '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.', 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_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}}', Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!', You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
Change_Language: 'Change Language', Change_Language: 'Change Language',

View File

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

View File

@ -88,7 +88,7 @@ export default {
Activity: 'Atividade', Activity: 'Atividade',
Add_Reaction: 'Reagir', Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor', Add_Server: 'Adicionar servidor',
Add_user: 'Adicionar usuário', Add_users: 'Adicionar usuário',
Alert: 'Alerta', Alert: 'Alerta',
alert: 'alerta', alert: 'alerta',
alerts: 'alertas', alerts: 'alertas',
@ -123,6 +123,7 @@ export default {
Cancel: 'Cancelar', Cancel: 'Cancelar',
changing_avatar: 'trocando avatar', changing_avatar: 'trocando avatar',
creating_channel: 'criando canal', creating_channel: 'criando canal',
creating_invite: 'criando convite',
Channel_Name: 'Nome do Canal', Channel_Name: 'Nome do Canal',
Channels: 'Canais', Channels: 'Canais',
Chats: 'Conversas', Chats: 'Conversas',
@ -169,6 +170,7 @@ export default {
edited: 'editado', edited: 'editado',
erasing_room: 'apagando sala', erasing_room: 'apagando sala',
Edit: 'Editar', Edit: 'Editar',
Edit_Invite: 'Editar convite',
Email_or_password_field_is_empty: 'Email ou senha estão vazios', Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email', Email: 'Email',
email: 'e-mail', email: 'e-mail',
@ -176,6 +178,7 @@ export default {
Enable_notifications: 'Habilitar notificações', Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal', Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo', Error_uploading: 'Erro subindo',
Expiration_Days: 'Expira em (dias)',
Favorites: 'Favoritos', Favorites: 'Favoritos',
Files: 'Arquivos', Files: 'Arquivos',
File_description: 'Descrição do arquivo', File_description: 'Descrição do arquivo',
@ -188,6 +191,7 @@ export default {
Forgot_password: 'Esqueci minha senha', Forgot_password: 'Esqueci minha senha',
Forgot_Password: 'Esqueci minha senha', Forgot_Password: 'Esqueci minha senha',
Full_table: 'Clique para ver a tabela completa', Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos', Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo', Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal', Has_joined_the_channel: 'Entrou no canal',
@ -196,7 +200,10 @@ export default {
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',
is_typing: 'está digitando', 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.', 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_the_community: 'Junte-se à comunidade',
Join: 'Entrar', Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal', 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_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!',
Login_with: 'Login with', Login_with: 'Login with',
Logout: 'Sair', Logout: 'Sair',
Max_number_of_uses: 'Número máximo de usos',
Members: 'Membros', Members: 'Membros',
Mentioned_Messages: 'Mensagens mencionadas', Mentioned_Messages: 'Mensagens mencionadas',
mentioned: 'mencionado', mentioned: 'mencionado',
@ -231,11 +239,13 @@ export default {
N_users: '{{n}} usuários', N_users: '{{n}} usuários',
name: 'nome', name: 'nome',
Name: 'Nome', Name: 'Nome',
Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?', New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem', New_Message: 'Nova Mensagem',
New_Password: 'Nova Senha', New_Password: 'Nova Senha',
Next: 'Próximo', Next: 'Próximo',
No_files: 'Não há arquivos', No_files: 'Não há arquivos',
No_limit: 'Sem limite',
No_mentioned_messages: 'Não há menções', No_mentioned_messages: 'Não há menções',
No_pinned_messages: 'Não há mensagens fixadas', No_pinned_messages: 'Não há mensagens fixadas',
No_results_found: 'Nenhum resultado encontrado', No_results_found: 'Nenhum resultado encontrado',
@ -328,6 +338,7 @@ export default {
Settings: 'Configurações', Settings: 'Configurações',
Settings_succesfully_changed: 'Configurações salvas com sucesso!', Settings_succesfully_changed: 'Configurações salvas com sucesso!',
Share: 'Compartilhar', Share: 'Compartilhar',
Share_Link: 'Share Link',
Sign_in_your_server: 'Entrar no seu servidor', Sign_in_your_server: 'Entrar no seu servidor',
Sign_Up: 'Registrar', Sign_Up: 'Registrar',
Some_field_is_invalid_or_empty: 'Algum campo está inválido ou vazio', 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_were_mentioned: 'você foi mencionado',
you: 'você', you: 'você',
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!', 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_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria', Write_External_Permission: 'Acesso à Galeria',

View File

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

View File

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

View File

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

View File

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

View File

@ -1098,6 +1098,23 @@ const RocketChat = {
}, },
translateMessage(message, targetLanguage) { translateMessage(message, targetLanguage) {
return this.sdk.methodCall('autoTranslate.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 customEmojis from './customEmojis';
import activeUsers from './activeUsers'; import activeUsers from './activeUsers';
import usersTyping from './usersTyping'; import usersTyping from './usersTyping';
import inviteLinks from './inviteLinks';
export default combineReducers({ export default combineReducers({
settings, settings,
@ -32,5 +33,6 @@ export default combineReducers({
crashReport, crashReport,
customEmojis, customEmojis,
activeUsers, 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 Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks';
import database from '../lib/database'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
@ -15,6 +16,17 @@ const roomTypes = {
channel: 'c', direct: 'd', group: 'p' 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 }) { const navigate = function* navigate({ params }) {
yield put(appStart('inside')); yield put(appStart('inside'));
if (params.rid) { if (params.rid) {
@ -24,6 +36,8 @@ const navigate = function* navigate({ params }) {
yield Navigation.navigate('RoomsListView'); yield Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { rid: params.rid, name, t: roomTypes[type] }); 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); const servers = yield serversCollection.find(host);
if (servers && user) { if (servers && user) {
yield put(selectServerRequest(host)); yield put(selectServerRequest(host));
yield take(types.SERVER.SELECT_SUCCESS); yield take(types.LOGIN.SUCCESS);
yield navigate({ params }); yield navigate({ params });
return; return;
} }
@ -82,6 +96,8 @@ const handleOpen = function* handleOpen({ params }) {
if (params.token) { if (params.token) {
yield take(types.SERVER.SELECT_SUCCESS); yield take(types.SERVER.SELECT_SUCCESS);
yield RocketChat.connect({ server: host, user: { token: params.token } }); 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 init from './init';
import state from './state'; import state from './state';
import deepLinking from './deepLinking'; import deepLinking from './deepLinking';
import inviteLinks from './inviteLinks';
const root = function* root() { const root = function* root() {
yield all([ yield all([
@ -19,7 +20,8 @@ const root = function* root() {
messages(), messages(),
selectServer(), selectServer(),
state(), 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 database from '../lib/database';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { inviteLinksRequest } from '../actions/inviteLinks';
const getServer = state => state.server.server; const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -115,17 +116,27 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield put(setUser(user)); yield put(setUser(user));
EventEmitter.emit('connected'); EventEmitter.emit('connected');
let currentRoot;
if (!user.username) { if (!user.username) {
yield put(appStart('setUsername')); yield put(appStart('setUsername'));
} else if (adding) { } else if (adding) {
yield put(serverFinishAdd()); yield put(serverFinishAdd());
yield put(appStart('inside')); yield put(appStart('inside'));
} else { } else {
const currentRoot = yield select(state => state.app.root); currentRoot = yield select(state => state.app.root);
if (currentRoot !== 'inside') { if (currentRoot !== 'inside') {
yield put(appStart('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) { } catch (e) {
log(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, joined: !!room,
canViewMembers: false, canViewMembers: false,
canAutoTranslate: false, canAutoTranslate: false,
canAddUser: false canAddUser: false,
canInviteUser: false
}; };
if (room && room.observe && room.rid) { if (room && room.observe && room.rid) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
@ -108,6 +109,7 @@ class RoomActionsView extends React.Component {
this.setState({ canAutoTranslate }); this.setState({ canAutoTranslate });
this.canAddUser(); this.canAddUser();
this.canInviteUser();
} }
componentWillUnmount() { componentWillUnmount() {
@ -126,7 +128,6 @@ class RoomActionsView extends React.Component {
} }
} }
// TODO: move to componentDidMount
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
canAddUser = async() => { canAddUser = async() => {
const { room, joined } = this.state; const { room, joined } = this.state;
@ -150,8 +151,15 @@ class RoomActionsView extends React.Component {
this.setState({ canAddUser: canAdd }); this.setState({ canAddUser: canAdd });
} }
// TODO: move to componentDidMount canInviteUser = async() => {
// eslint-disable-next-line react/sort-comp 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() => { canViewMembers = async() => {
const { room } = this.state; const { room } = this.state;
const { rid, t, broadcast } = room; const { rid, t, broadcast } = room;
@ -172,7 +180,7 @@ class RoomActionsView extends React.Component {
get sections() { get sections() {
const { const {
room, membersCount, canViewMembers, canAddUser, joined, canAutoTranslate room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
} = this.state; } = this.state;
const { jitsiEnabled } = this.props; const { jitsiEnabled } = this.props;
const { const {
@ -302,17 +310,28 @@ class RoomActionsView extends React.Component {
if (canAddUser) { if (canAddUser) {
actions.push({ actions.push({
icon: 'user-plus', icon: 'plus',
name: I18n.t('Add_user'), name: I18n.t('Add_users'),
route: 'SelectedUsersView', route: 'SelectedUsersView',
params: { params: {
nextActionID: 'ADD_USER', nextActionID: 'ADD_USER',
rid, rid,
title: I18n.t('Add_user') title: I18n.t('Add_users')
}, },
testID: 'room-actions-add-user' 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]; sections[2].data = [...actions, ...sections[2].data];
if (joined) { if (joined) {

View File

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