From ccbc84f9a8a008199ef9f82729b24f04214a6a50 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:53:19 -0300 Subject: [PATCH 001/145] [IMPROVE] Remove show message in main thread preference (#4435) * [IMPROVE] Remove show message in main thread preference * default settings * created the get * fix compare server version * fix E2E tests * settings to logged user * remove constant and get alsosendtochannel from user * fix send to channel first message * fix when the alsoSendThreadToChannel is checked * added list picker user preference * tweaks in messagebox tmid and detox * added pt-br and deleted expectToBeVisible id * reactive alsoSendThreadTOChannel * fix the behavior when press or long press threads messages * remove reply in thread from threads * clean helpers detox * tweak onMessageLongPress and onSubmit * Remove unnecessary calculations inside ListPicker * Fix long press logic * Fix onReplyInit logic * fix data_setup at detox for servers greater than 5.0 Co-authored-by: Diego Mello --- app/containers/MessageActions/index.tsx | 2 +- app/containers/MessageBox/index.tsx | 48 ++++++++++-- app/definitions/ILoggedUser.ts | 1 + app/i18n/locales/en.json | 4 +- app/i18n/locales/pt-BR.json | 4 +- app/lib/methods/subscriptions/rooms.ts | 3 + app/lib/services/connect.ts | 7 +- app/views/RoomView/index.tsx | 8 ++ app/views/UserPreferencesView/ListPicker.tsx | 81 ++++++++++++++++++++ app/views/UserPreferencesView/index.tsx | 27 ++++++- e2e/helpers/data_setup.js | 3 + 11 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 app/views/UserPreferencesView/ListPicker.tsx diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index 1b204e7ac..395f5f87d 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -330,7 +330,7 @@ const MessageActions = React.memo( let options: TActionSheetOptionsItem[] = []; // Reply - if (!isReadOnly) { + if (!isReadOnly && !tmid) { options = [ { title: I18n.t('Reply_in_Thread'), diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 7290c9a6f..067dbb502 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -55,7 +55,7 @@ import { } from '../../definitions'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; -import { hasPermission, debounce, isAndroid, isIOS, isTablet } from '../../lib/methods/helpers'; +import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersion } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; import { TSupportedThemes } from '../../theme'; import { ChatsStackParamList } from '../../stacks/types'; @@ -111,8 +111,8 @@ export interface IMessageBoxProps extends IBaseScreen void | null; + serverVersion: string; } interface IMessageBoxState { @@ -181,7 +181,7 @@ class MessageBox extends Component { commandPreview: [], showCommandPreview: false, command: {}, - tshow: false, + tshow: this.sendThreadToChannel, mentionLoading: false, permissionToUpload: true }; @@ -211,6 +211,23 @@ class MessageBox extends Component { }; } + get sendThreadToChannel() { + const { user, serverVersion, tmid } = this.props; + if (tmid && compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) { + return false; + } + if (tmid && user.alsoSendThreadToChannel === 'default') { + return false; + } + if (user.alsoSendThreadToChannel === 'always') { + return true; + } + if (user.alsoSendThreadToChannel === 'never') { + return false; + } + return true; + } + async componentDidMount() { const db = database.active; const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props; @@ -381,7 +398,12 @@ class MessageBox extends Component { } componentDidUpdate(prevProps: IMessageBoxProps) { - const { uploadFilePermission, goToCannedResponses } = this.props; + const { uploadFilePermission, goToCannedResponses, replyWithMention, threadsEnabled } = this.props; + if (prevProps.replyWithMention !== replyWithMention) { + if (threadsEnabled && replyWithMention) { + this.setState({ tshow: this.sendThreadToChannel }); + } + } if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) { this.setOptions(); } @@ -687,9 +709,13 @@ class MessageBox extends Component { }; clearInput = () => { + const { tshow } = this.state; + const { user, serverVersion } = this.props; this.setInput(''); this.setShowSend(false); - this.setState({ tshow: false }); + if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0') || (tshow && user.alsoSendThreadToChannel === 'default')) { + this.setState({ tshow: false }); + } }; canUploadFile = (file: any) => { @@ -974,7 +1000,7 @@ class MessageBox extends Component { // Normal message } else { // @ts-ignore - onSubmit(message, undefined, tshow); + onSubmit(message, undefined, tmid ? tshow : false); } }; @@ -1044,7 +1070,12 @@ class MessageBox extends Component { onPress={this.onPressSendToChannel} testID='messagebox-send-to-channel' > - + {I18n.t('Messagebox_Send_to_channel')} @@ -1213,7 +1244,8 @@ const mapStateToProps = (state: IApplicationState) => ({ FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled, - uploadFilePermission: state.permissions['mobile-upload-file'] + uploadFilePermission: state.permissions['mobile-upload-file'], + serverVersion: state.server.version }); const dispatchToProps = { diff --git a/app/definitions/ILoggedUser.ts b/app/definitions/ILoggedUser.ts index cf5d65788..9b7193e77 100644 --- a/app/definitions/ILoggedUser.ts +++ b/app/definitions/ILoggedUser.ts @@ -21,6 +21,7 @@ export interface ILoggedUser { showMessageInMainThread?: boolean; isFromWebView?: boolean; enableMessageParserEarlyAdoption: boolean; + alsoSendThreadToChannel: 'default' | 'always' | 'never'; } export interface ILoggedUserResultFromServer diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 187264989..c865bbc37 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -842,5 +842,7 @@ "error-init-video-conf": "Error starting video call", "totp-invalid": "Code or password invalid", "Close_Chat": "Close Chat", - "Select_tags": "Select tags" + "Select_tags": "Select tags", + "Also_send_thread_message_to_channel_behavior": "Also send thread message to channel", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Allow users to select the Also send to channel behavior" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 020de9326..e30e7ad6c 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -795,5 +795,7 @@ "Show_badge_for_mentions_Info": "Mostrar contador somente para menções diretas", "totp-invalid": "Código ou senha inválida", "Close_Chat": "Fechar Conversa", - "Select_tags": "Selecionar tag(s)" + "Select_tags": "Selecionar tag(s)", + "Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Permitir que os usuários selecionem o comportamento Também enviar para o canal" } \ No newline at end of file diff --git a/app/lib/methods/subscriptions/rooms.ts b/app/lib/methods/subscriptions/rooms.ts index bd5f2b425..37505ed63 100644 --- a/app/lib/methods/subscriptions/rooms.ts +++ b/app/lib/methods/subscriptions/rooms.ts @@ -295,6 +295,9 @@ export default function subscribeRooms() { if ((['settings.preferences.showMessageInMainThread'] as any) in diff) { store.dispatch(setUser({ showMessageInMainThread: diff['settings.preferences.showMessageInMainThread'] })); } + if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) { + store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] })); + } } if (/subscriptions/.test(ev)) { if (type === 'removed') { diff --git a/app/lib/services/connect.ts b/app/lib/services/connect.ts index 4d4110b52..41f20cf2e 100644 --- a/app/lib/services/connect.ts +++ b/app/lib/services/connect.ts @@ -268,8 +268,10 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise< const result = sdk.current.currentLogin?.result; let enableMessageParserEarlyAdoption = true; + let showMessageInMainThread = false; if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) { enableMessageParserEarlyAdoption = result.me.settings?.preferences?.enableMessageParserEarlyAdoption ?? true; + showMessageInMainThread = result.me.settings?.preferences?.showMessageInMainThread ?? true; } if (result) { @@ -287,8 +289,9 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise< roles: result.me.roles, avatarETag: result.me.avatarETag, isFromWebView, - showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true, - enableMessageParserEarlyAdoption + showMessageInMainThread, + enableMessageParserEarlyAdoption, + alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel }; return user; } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 11d507eb6..4b7213f80 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -813,6 +813,10 @@ class RoomView extends React.Component { }; onReplyInit = (message: TAnyMessageModel, mention: boolean) => { + // If there's a thread already, we redirect to it + if (mention && !!message.tlm) { + return this.onThreadPress(message); + } this.setState({ selectedMessage: message, replying: true, @@ -833,6 +837,10 @@ class RoomView extends React.Component { }; onMessageLongPress = (message: TAnyMessageModel) => { + // if it's a thread message on main room, we disable the long press + if (message.tmid && !this.tmid) { + return; + } this.messagebox?.current?.closeEmojiAndAction(this.messageActions?.showMessageActions, message); }; diff --git a/app/views/UserPreferencesView/ListPicker.tsx b/app/views/UserPreferencesView/ListPicker.tsx new file mode 100644 index 000000000..1bd557d27 --- /dev/null +++ b/app/views/UserPreferencesView/ListPicker.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { StyleSheet, Text } from 'react-native'; + +import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; +import { CustomIcon } from '../../containers/CustomIcon'; +import * as List from '../../containers/List'; +import I18n from '../../i18n'; +import { useTheme } from '../../theme'; +import sharedStyles from '../Styles'; + +const styles = StyleSheet.create({ + title: { ...sharedStyles.textRegular, fontSize: 16 } +}); + +const OPTIONS = { + alsoSendThreadToChannel: [ + { + label: 'Default', + value: 'default' + }, + { + label: 'Always', + value: 'always' + }, + { + label: 'Never', + value: 'never' + } + ] +}; + +type TOptions = keyof typeof OPTIONS; + +interface IBaseParams { + preference: TOptions; + value: string; + onChangeValue: (param: { [key: string]: string }, onError: () => void) => void; +} + +const ListPicker = ({ + preference, + value, + title, + testID, + onChangeValue +}: { + title: string; + testID: string; +} & IBaseParams) => { + const { showActionSheet, hideActionSheet } = useActionSheet(); + const { colors } = useTheme(); + const [option, setOption] = useState( + value ? OPTIONS[preference].find(option => option.value === value) : OPTIONS[preference][0] + ); + + const getOptions = (): TActionSheetOptionsItem[] => + OPTIONS[preference].map(i => ({ + title: I18n.t(i.label, { defaultValue: i.label }), + onPress: () => { + hideActionSheet(); + onChangeValue({ [preference]: i.value.toString() }, () => setOption(option)); + setOption(i); + }, + right: option?.value === i.value ? () => : undefined + })); + + return ( + showActionSheet({ options: getOptions() })} + right={() => ( + + {option?.label ? I18n.t(option?.label, { defaultValue: option?.label }) : option?.label} + + )} + /> + ); +}; + +export default ListPicker; diff --git a/app/views/UserPreferencesView/index.tsx b/app/views/UserPreferencesView/index.tsx index fed7c5c7c..748e38d21 100644 --- a/app/views/UserPreferencesView/index.tsx +++ b/app/views/UserPreferencesView/index.tsx @@ -15,13 +15,14 @@ import { getUserSelector } from '../../selectors/login'; import { ProfileStackParamList } from '../../stacks/types'; import { Services } from '../../lib/services'; import { useAppSelector } from '../../lib/hooks'; +import ListPicker from './ListPicker'; interface IUserPreferencesViewProps { navigation: StackNavigationProp; } const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Element => { - const { enableMessageParserEarlyAdoption, id } = useAppSelector(state => getUserSelector(state)); + const { enableMessageParserEarlyAdoption, id, alsoSendThreadToChannel } = useAppSelector(state => getUserSelector(state)); const serverVersion = useAppSelector(state => state.server.version); const dispatch = useDispatch(); @@ -45,6 +46,16 @@ const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Ele } }; + const setAlsoSendThreadToChannel = async (param: { [key: string]: string }, onError: () => void) => { + try { + await Services.saveUserPreferences(param); + dispatch(setUser(param)); + } catch (e) { + log(e); + onError(); + } + }; + const renderMessageParserSwitch = (value: boolean) => ( ); @@ -74,6 +85,20 @@ const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Ele ) : null} + {compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0') ? ( + + + + + + + ) : null} ); diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js index 4b038e42e..b8459f189 100644 --- a/e2e/helpers/data_setup.js +++ b/e2e/helpers/data_setup.js @@ -1,6 +1,7 @@ const axios = require('axios').default; const data = require('../data'); +const random = require('./random'); const TEAM_TYPE = { PUBLIC: 0, @@ -106,6 +107,8 @@ const changeChannelJoinCode = async (roomId, joinCode) => { try { await rocketchat.post('method.call/saveRoomSettings', { message: JSON.stringify({ + msg: 'method', + id: random(10), method: 'saveRoomSettings', params: [roomId, { joinCode }] }) From cbc6892084d967224ed2576ea848c63afa6bbad5 Mon Sep 17 00:00:00 2001 From: Gleidson Daniel Silva Date: Fri, 26 Aug 2022 10:21:25 -0300 Subject: [PATCH 002/145] [NEW] Unify members section (#4399) * create useUserPermissions hook * create CheckRadioButton component * fix return * create MembersSection component * apply MembersSection and header filter * fix re-render and testID * fix detox tests * rename to RadioButton * move the component closer to the screen * remove useUserPermissions * remove theme prop * migrate to hooks * fix team permissions * remove theme prop from UserItem * remove options prop * fix Member * remove commented test * fixes * fix for room not joined * add room members events * adds empty option * add members filter and pagination * clear RoomMembersView * remove unused styles * Update app/views/RoomMembersView/index.tsx Co-authored-by: Diego Mello * wip * Temp workaround for SearchBox background color * Rename import * Fix missing params for 5.0 * Fix e2e tests Co-authored-by: Diego Mello --- app/containers/RadioButton/index.tsx | 16 + app/containers/UserItem.tsx | 57 +- app/i18n/locales/en.json | 1 + app/i18n/locales/pt-BR.json | 1 + app/lib/methods/helpers/helpers.ts | 2 +- app/lib/methods/helpers/log/events.ts | 4 + app/lib/navigation/appNavigation.ts | 7 +- app/stacks/InsideStack.tsx | 2 +- app/stacks/MasterDetailStack/index.tsx | 2 +- app/stacks/MasterDetailStack/types.ts | 1 + app/stacks/types.ts | 1 + app/views/CreateChannelView.tsx | 23 +- app/views/NewMessageView.tsx | 1 - app/views/RoomActionsView/index.tsx | 93 +-- .../components/ActionsSection.tsx | 109 +++ app/views/RoomMembersView/helpers.ts | 258 ++++++ app/views/RoomMembersView/index.tsx | 767 ++++++------------ app/views/RoomMembersView/styles.ts | 19 +- app/views/SelectedUsersView.tsx | 23 +- e2e/tests/room/03-roomactions.spec.js | 67 +- e2e/tests/team/02-team.spec.js | 19 +- 21 files changed, 752 insertions(+), 721 deletions(-) create mode 100644 app/containers/RadioButton/index.tsx create mode 100644 app/views/RoomMembersView/components/ActionsSection.tsx create mode 100644 app/views/RoomMembersView/helpers.ts diff --git a/app/containers/RadioButton/index.tsx b/app/containers/RadioButton/index.tsx new file mode 100644 index 000000000..b9c8efe5c --- /dev/null +++ b/app/containers/RadioButton/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { RadioButton as RadioButtonUiLib } from 'react-native-ui-lib'; + +import { useTheme } from '../../theme'; + +export const RadioButton = ({ check, testID, size }: { check: boolean; testID?: string; size?: number }): React.ReactElement => { + const { colors } = useTheme(); + return ( + + ); +}; diff --git a/app/containers/UserItem.tsx b/app/containers/UserItem.tsx index b39948856..e044dd6ba 100644 --- a/app/containers/UserItem.tsx +++ b/app/containers/UserItem.tsx @@ -4,9 +4,8 @@ import { Pressable, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-n import Avatar from './Avatar'; import { CustomIcon, TIconsName } from './CustomIcon'; import sharedStyles from '../views/Styles'; -import { themes } from '../lib/constants'; import { isIOS } from '../lib/methods/helpers'; -import { TSupportedThemes } from '../theme'; +import { useTheme } from '../theme'; const styles = StyleSheet.create({ button: { @@ -47,34 +46,36 @@ interface IUserItem { onLongPress?: () => void; style?: StyleProp; icon?: TIconsName | null; - theme: TSupportedThemes; } -const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon, theme }: IUserItem) => ( - ({ - backgroundColor: isIOS && pressed ? themes[theme].bannerBackground : 'transparent' - })} - > - - - - - {name} - - - @{username} - +const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon }: IUserItem): React.ReactElement => { + const { colors } = useTheme(); + return ( + ({ + backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent' + })} + > + + + + + {name} + + + @{username} + + + {icon ? : null} - {icon ? : null} - - -); + + ); +}; export default UserItem; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index c865bbc37..5f0c0c134 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -356,6 +356,7 @@ "No_mentioned_messages": "No mentioned messages", "No_pinned_messages": "No pinned messages", "No_results_found": "No results found", + "No_members_found": "No members found", "No_starred_messages": "No starred messages", "No_thread_messages": "No thread messages", "No_label_provided": "No {{label}} provided.", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index e30e7ad6c..694aef0d6 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -334,6 +334,7 @@ "No_mentioned_messages": "Não há menções", "No_pinned_messages": "Não há mensagens fixadas", "No_results_found": "Nenhum resultado encontrado", + "No_members_found": "Nenhum usuário encontrado", "No_starred_messages": "Não há mensagens favoritas", "No_thread_messages": "Não há tópicos", "No_label_provided": "Sem {{label}}.", diff --git a/app/lib/methods/helpers/helpers.ts b/app/lib/methods/helpers/helpers.ts index d496b7167..bfacfc015 100644 --- a/app/lib/methods/helpers/helpers.ts +++ b/app/lib/methods/helpers/helpers.ts @@ -85,7 +85,7 @@ export function hasRole(role): boolean { return userRoles.indexOf(role) > -1; } -export async function hasPermission(permissions, rid?: any): boolean[] { +export async function hasPermission(permissions, rid?: any): Promise { let roomRoles = []; if (rid) { const db = database.active; diff --git a/app/lib/methods/helpers/log/events.ts b/app/lib/methods/helpers/log/events.ts index ea4e57f48..b4562b6c5 100644 --- a/app/lib/methods/helpers/log/events.ts +++ b/app/lib/methods/helpers/log/events.ts @@ -272,6 +272,10 @@ export default { RA_MOVE_TO_TEAM_F: 'ra_move_to_team_f', RA_SEARCH_TEAM: 'ra_search_team', + // ROOM MEMBERS ACTIONS VIEW + RM_GO_SELECTEDUSERS: 'rm_go_selected_users', + RM_GO_INVITEUSERS: 'rm_go_invite_users', + // ROOM INFO VIEW RI_GO_RI_EDIT: 'ri_go_ri_edit', RI_GO_LIVECHAT_EDIT: 'ri_go_livechat_edit', diff --git a/app/lib/navigation/appNavigation.ts b/app/lib/navigation/appNavigation.ts index c2ef51c75..7d2f73756 100644 --- a/app/lib/navigation/appNavigation.ts +++ b/app/lib/navigation/appNavigation.ts @@ -17,10 +17,15 @@ function replace(name: string, params: any) { navigationRef.current?.dispatch(StackActions.replace(name, params)); } +function popToTop() { + navigationRef.current?.dispatch(StackActions.popToTop()); +} + export default { navigationRef, routeNameRef, navigate, back, - replace + replace, + popToTop }; diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index 95fdac203..d5938cb58 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -95,7 +95,7 @@ const ChatsStackNavigator = () => { - + { - + { - const { theme } = this.props; - - return ( - this.removeUser(item)} - testID={`create-channel-view-item-${item.name}`} - icon='check' - theme={theme} - /> - ); - }; + renderItem = ({ item }: { item: IOtherUser }) => ( + this.removeUser(item)} + testID={`create-channel-view-item-${item.name}`} + icon='check' + /> + ); renderInvitedList = () => { const { users, theme } = this.props; diff --git a/app/views/NewMessageView.tsx b/app/views/NewMessageView.tsx index c8f70ec93..2b6f65eb6 100644 --- a/app/views/NewMessageView.tsx +++ b/app/views/NewMessageView.tsx @@ -290,7 +290,6 @@ class NewMessageView extends React.Component this.goRoom(itemModel)} testID={`new-message-view-item-${item.name}`} style={style} - theme={theme} /> ); }; diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index df0707a70..f440de18b 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -64,10 +64,6 @@ interface IRoomActionsViewProps extends IActionSheetProvider, IBaseScreen { - const { room, joined } = this.state; - const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props; - const { rid, t } = room; - let canAddUser = false; - - const userInRoom = joined; - const permissions = await hasPermission( - [addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], - rid - ); - - if (userInRoom && permissions[0]) { - canAddUser = true; - } - if (t === 'c' && permissions[1]) { - canAddUser = true; - } - if (t === 'p' && permissions[2]) { - canAddUser = true; - } - return canAddUser; - }; - - canInviteUser = async () => { - const { room } = this.state; - const { createInviteLinksPermission } = this.props; - const { rid } = room; - const permissions = await hasPermission([createInviteLinksPermission], rid); - - const canInviteUser = permissions[0]; - return canInviteUser; - }; - canEdit = async () => { const { room } = this.state; const { editRoomPermission } = this.props; @@ -1135,7 +1089,7 @@ class RoomActionsView extends React.Component 0 ? `${membersCount} ${I18n.t('members')}` : undefined} - onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room } })} + onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room, joined: this.joined } })} testID='room-actions-members' left={() => } showActionIndicator @@ -1164,45 +1118,6 @@ class RoomActionsView extends React.Component ) : null} - {['c', 'p'].includes(t) && canAddUser ? ( - <> - - this.onPressTouchable({ - route: 'SelectedUsersView', - params: { - title: I18n.t('Add_users'), - nextAction: this.addUser - } - }) - } - testID='room-actions-add-user' - left={() => } - showActionIndicator - /> - - - ) : null} - - {['c', 'p'].includes(t) && canInviteUser ? ( - <> - - this.onPressTouchable({ - route: 'InviteUsersView', - params: { rid } - }) - } - testID='room-actions-invite-user' - left={() => } - showActionIndicator - /> - - - ) : null} - {['c', 'p', 'd'].includes(t) && !prid ? ( <> ({ encryptionEnabled: state.encryption.enabled, serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, - addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'], - addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'], - addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'], - createInviteLinksPermission: state.permissions['create-invite-links'], editRoomPermission: state.permissions['edit-room'], toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], diff --git a/app/views/RoomMembersView/components/ActionsSection.tsx b/app/views/RoomMembersView/components/ActionsSection.tsx new file mode 100644 index 000000000..6442db129 --- /dev/null +++ b/app/views/RoomMembersView/components/ActionsSection.tsx @@ -0,0 +1,109 @@ +import { CompositeNavigationProp, useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import React from 'react'; +import { View } from 'react-native'; +import { useDispatch } from 'react-redux'; + +import { setLoading } from '../../../actions/selectedUsers'; +import * as List from '../../../containers/List'; +import { TSubscriptionModel } from '../../../definitions'; +import i18n from '../../../i18n'; +import { usePermissions } from '../../../lib/hooks'; +import log, { events, logEvent } from '../../../lib/methods/helpers/log'; +import { Services } from '../../../lib/services'; +import { MasterDetailInsideStackParamList } from '../../../stacks/MasterDetailStack/types'; +import { ChatsStackParamList } from '../../../stacks/types'; + +type TNavigation = CompositeNavigationProp< + StackNavigationProp, + StackNavigationProp +>; + +interface IActionsSection { + rid: TSubscriptionModel['rid']; + t: TSubscriptionModel['t']; + joined: boolean; +} + +export default function ActionsSection({ rid, t, joined }: IActionsSection): React.ReactElement { + const { navigate, pop } = useNavigation(); + const dispatch = useDispatch(); + const [addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission, createInviteLinksPermission] = + usePermissions(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room', 'create-invite-links'], rid); + + const canAddUser = + (joined && addUserToJoinedRoomPermission) || + (t === 'c' && addUserToAnyCRoomPermission) || + (t === 'p' && addUserToAnyPRoomPermission) || + false; + + const canInviteUser = createInviteLinksPermission; + + const handleOnPress = ({ + route, + params + }: { + route: keyof ChatsStackParamList; + params: ChatsStackParamList[keyof ChatsStackParamList]; + }) => { + navigate(route, params); + // @ts-ignore + logEvent(events[`RM_GO_${route.replace('View', '').toUpperCase()}`]); + }; + + const addUser = async () => { + try { + dispatch(setLoading(true)); + await Services.addUsersToRoom(rid); + pop(); + } catch (e) { + log(e); + } finally { + dispatch(setLoading(false)); + } + }; + + return ( + + {['c', 'p'].includes(t) && canAddUser ? ( + <> + + + handleOnPress({ + route: 'SelectedUsersView', + params: { + title: i18n.t('Add_users'), + nextAction: addUser + } + }) + } + testID='room-actions-add-user' + left={() => } + showActionIndicator + /> + + + ) : null} + + {['c', 'p'].includes(t) && canInviteUser ? ( + <> + + handleOnPress({ + route: 'InviteUsersView', + params: { rid } + }) + } + testID='room-actions-invite-user' + left={() => } + showActionIndicator + /> + + + ) : null} + + ); +} diff --git a/app/views/RoomMembersView/helpers.ts b/app/views/RoomMembersView/helpers.ts new file mode 100644 index 000000000..f7576fb3e --- /dev/null +++ b/app/views/RoomMembersView/helpers.ts @@ -0,0 +1,258 @@ +import { Q } from '@nozbe/watermelondb'; + +import { LISTENER } from '../../containers/Toast'; +import { IUser, SubscriptionType, TSubscriptionModel, TUserModel } from '../../definitions'; +import I18n from '../../i18n'; +import { getRoomTitle, showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers'; +import EventEmitter from '../../lib/methods/helpers/events'; +import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom'; +import log from '../../lib/methods/helpers/log'; +import appNavigation from '../../lib/navigation/appNavigation'; +import { Services } from '../../lib/services'; +import database from '../../lib/database'; +import { RoomTypes } from '../../lib/methods'; + +export type TRoomType = SubscriptionType.CHANNEL | SubscriptionType.GROUP | SubscriptionType.OMNICHANNEL; + +const handleGoRoom = (item: TGoRoomItem, isMasterDetail: boolean): void => { + if (isMasterDetail) { + appNavigation.navigate('DrawerNavigator'); + } else { + appNavigation.popToTop(); + } + goRoom({ item, isMasterDetail }); +}; + +export const fetchRole = (role: string, selectedUser: TUserModel, roomRoles: any): boolean => { + const userRoleResult = roomRoles.find((r: any) => r.u._id === selectedUser._id); + return userRoleResult?.roles.includes(role); +}; + +export const fetchRoomMembersRoles = async (roomType: TRoomType, rid: string, updateState: any): Promise => { + try { + const type = roomType; + const result = await Services.getRoomRoles(rid, type); + if (result?.success) { + updateState({ roomRoles: result.roles }); + } + } catch (e) { + log(e); + } +}; + +export const handleMute = async (user: TUserModel, rid: string) => { + try { + await Services.toggleMuteUserInRoom(rid, user?.username, !user?.muted); + EventEmitter.emit(LISTENER, { + message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') }) + }); + } catch (e) { + log(e); + } +}; + +export const handleModerator = async ( + selectedUser: TUserModel, + isModerator: boolean, + room: TSubscriptionModel, + username: string, + callback: () => Promise +): Promise => { + try { + await Services.toggleRoomModerator({ + roomId: room.rid, + t: room.t, + userId: selectedUser._id, + isModerator + }); + const message = isModerator + ? 'User__username__is_now_a_moderator_of__room_name_' + : 'User__username__removed_from__room_name__moderators'; + EventEmitter.emit(LISTENER, { + message: I18n.t(message, { + username, + room_name: getRoomTitle(room) + }) + }); + callback(); + } catch (e) { + log(e); + } +}; + +export const navToDirectMessage = async (item: IUser, isMasterDetail: boolean): Promise => { + try { + const db = database.active; + const subsCollection = db.get('subscriptions'); + const query = await subsCollection.query(Q.where('name', item.username)).fetch(); + if (query.length) { + const [room] = query; + handleGoRoom(room, isMasterDetail); + } else { + const result = await Services.createDirectMessage(item.username); + if (result.success) { + handleGoRoom({ rid: result.room?._id as string, name: item.username, t: SubscriptionType.DIRECT }, isMasterDetail); + } + } + } catch (e) { + log(e); + } +}; + +const removeFromTeam = async ( + selectedUser: IUser, + updateState: Function, + room: TSubscriptionModel, + members: TUserModel[], + selected?: any +) => { + try { + const userId = selectedUser._id; + const result = await Services.removeTeamMember({ + teamId: room.teamId, + userId, + ...(selected && { rooms: selected }) + }); + if (result.success) { + const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) }); + EventEmitter.emit(LISTENER, { message }); + const newMembers = members.filter(member => member._id !== userId); + updateState({ + members: newMembers + }); + } + } catch (e: any) { + log(e); + showErrorAlert( + e.data.error ? I18n.t(e.data.error) : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }), + I18n.t('Cannot_remove') + ); + } +}; + +export const handleRemoveFromTeam = async ( + selectedUser: TUserModel, + updateState: Function, + room: TSubscriptionModel, + members: TUserModel[] +): Promise => { + try { + const result = await Services.teamListRoomsOfUser({ teamId: room.teamId as string, userId: selectedUser._id }); + + if (result.success) { + if (result.rooms?.length) { + const teamChannels = result.rooms.map((r: any) => ({ + rid: r._id, + name: r.name, + teamId: r.teamId, + alert: r.isLastOwner + })); + appNavigation.navigate('SelectListView', { + title: 'Remove_Member', + infoText: 'Remove_User_Team_Channels', + data: teamChannels, + nextAction: (selected: any) => removeFromTeam(selectedUser, updateState, room, members, selected), + showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove')) + }); + } else { + showConfirmationAlert({ + message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + onPress: () => removeFromTeam(selectedUser, updateState, room, members) + }); + } + } + } catch (e) { + showConfirmationAlert({ + message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + onPress: () => removeFromTeam(selectedUser, updateState, room, members) + }); + } +}; + +export const handleLeader = async ( + selectedUser: TUserModel, + isLeader: boolean, + room: TSubscriptionModel, + username: string, + callback: () => Promise +): Promise => { + try { + await Services.toggleRoomLeader({ + roomId: room.rid, + t: room.t, + userId: selectedUser._id, + isLeader + }); + const message = isLeader + ? 'User__username__is_now_a_leader_of__room_name_' + : 'User__username__removed_from__room_name__leaders'; + EventEmitter.emit(LISTENER, { + message: I18n.t(message, { + username, + room_name: getRoomTitle(room) + }) + }); + callback(); + } catch (e) { + log(e); + } +}; + +export const handleRemoveUserFromRoom = async ( + selectedUser: TUserModel, + room: TSubscriptionModel, + callback: Function +): Promise => { + try { + const userId = selectedUser._id; + await Services.removeUserFromRoom({ roomId: room.rid, t: room.t as RoomTypes, userId }); + const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) }); + EventEmitter.emit(LISTENER, { message }); + callback(); + } catch (e) { + log(e); + } +}; + +export const handleIgnore = async (selectedUser: TUserModel, ignore: boolean, rid: string) => { + try { + await Services.ignoreUser({ + rid, + userId: selectedUser._id, + ignore + }); + const message = I18n.t(ignore ? 'User_has_been_ignored' : 'User_has_been_unignored'); + EventEmitter.emit(LISTENER, { message }); + } catch (e) { + log(e); + } +}; + +export const handleOwner = async ( + selectedUser: TUserModel, + isOwner: boolean, + username: string, + room: TSubscriptionModel, + callback: Function +): Promise => { + try { + await Services.toggleRoomOwner({ + roomId: room.rid, + t: room.t, + userId: selectedUser._id, + isOwner + }); + const message = isOwner ? 'User__username__is_now_a_owner_of__room_name_' : 'User__username__removed_from__room_name__owners'; + EventEmitter.emit(LISTENER, { + message: I18n.t(message, { + username, + room_name: getRoomTitle(room) + }) + }); + } catch (e) { + log(e); + } + callback(); +}; diff --git a/app/views/RoomMembersView/index.tsx b/app/views/RoomMembersView/index.tsx index 2805d8ae8..be7e70d9f 100644 --- a/app/views/RoomMembersView/index.tsx +++ b/app/views/RoomMembersView/index.tsx @@ -1,311 +1,202 @@ -import { Q } from '@nozbe/watermelondb'; -import React from 'react'; -import { FlatList } from 'react-native'; -import { connect } from 'react-redux'; -import { Observable, Subscription } from 'rxjs'; +import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import React, { useEffect, useReducer } from 'react'; +import { FlatList, Text, View } from 'react-native'; -import { themes } from '../../lib/constants'; -import { TActionSheetOptions, TActionSheetOptionsItem, withActionSheet } from '../../containers/ActionSheet'; +import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; import ActivityIndicator from '../../containers/ActivityIndicator'; +import { CustomIcon } from '../../containers/CustomIcon'; import * as HeaderButton from '../../containers/HeaderButton'; import * as List from '../../containers/List'; +import { RadioButton } from '../../containers/RadioButton'; import SafeAreaView from '../../containers/SafeAreaView'; import SearchBox from '../../containers/SearchBox'; import StatusBar from '../../containers/StatusBar'; -import { LISTENER } from '../../containers/Toast'; -import { IApplicationState, IBaseScreen, IUser, SubscriptionType, TSubscriptionModel, TUserModel } from '../../definitions'; -import I18n from '../../i18n'; -import database from '../../lib/database'; -import { CustomIcon } from '../../containers/CustomIcon'; import UserItem from '../../containers/UserItem'; -import { getUserSelector } from '../../selectors/login'; -import { ModalStackParamList } from '../../stacks/MasterDetailStack/types'; -import { TSupportedThemes, withTheme } from '../../theme'; -import EventEmitter from '../../lib/methods/helpers/events'; -import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom'; -import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info'; +import { TSubscriptionModel, TUserModel } from '../../definitions'; +import I18n from '../../i18n'; +import { useAppSelector, usePermissions } from '../../lib/hooks'; +import { getRoomTitle, isGroupChat } from '../../lib/methods/helpers'; +import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import log from '../../lib/methods/helpers/log'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; -import { TSupportedPermissions } from '../../reducers/permissions'; -import { RoomTypes } from '../../lib/methods'; -import { compareServerVersion, debounce, getRoomTitle, hasPermission, isGroupChat } from '../../lib/methods/helpers'; -import styles from './styles'; import { Services } from '../../lib/services'; +import { TSupportedPermissions } from '../../reducers/permissions'; +import { getUserSelector } from '../../selectors/login'; +import { ModalStackParamList } from '../../stacks/MasterDetailStack/types'; +import { useTheme } from '../../theme'; +import ActionsSection from './components/ActionsSection'; +import { + fetchRole, + fetchRoomMembersRoles, + handleIgnore, + handleLeader, + handleModerator, + handleMute, + handleOwner, + handleRemoveFromTeam, + handleRemoveUserFromRoom, + navToDirectMessage, + TRoomType +} from './helpers'; +import styles from './styles'; const PAGE_SIZE = 25; -interface IRoomMembersViewProps extends IBaseScreen { - rid: string; - members: string[]; - baseUrl: string; - room: TSubscriptionModel; - user: { - id: string; - token: string; - roles: string[]; - }; - showActionSheet: (params: TActionSheetOptions) => {}; - theme: TSupportedThemes; - isMasterDetail: boolean; - useRealName: boolean; - muteUserPermission: string[]; - setLeaderPermission: string[]; - setOwnerPermission: string[]; - setModeratorPermission: string[]; - removeUserPermission: string[]; - editTeamMemberPermission: string[]; - viewAllTeamChannelsPermission: string[]; - viewAllTeamsPermission: string[]; - serverVersion: string; -} - interface IRoomMembersViewState { isLoading: boolean; allUsers: boolean; filtering: string; - rid: string; members: TUserModel[]; - membersFiltered: TUserModel[]; room: TSubscriptionModel; end: boolean; + roomRoles: any; + filter: string; page: number; } -class RoomMembersView extends React.Component { - private mounted: boolean; - private permissions: { [key in TSupportedPermissions]?: boolean }; - private roomObservable!: Observable; - private subscription!: Subscription; - private roomRoles: any; +const RightIcon = ({ check, label }: { check: boolean; label: string }) => { + const { colors } = useTheme(); + return ( + + ); +}; - constructor(props: IRoomMembersViewProps) { - super(props); - this.mounted = false; - this.permissions = {}; - const rid = props.route.params?.rid; - const room = props.route.params?.room; - this.state = { +const RoomMembersView = (): React.ReactElement => { + const { showActionSheet } = useActionSheet(); + const { colors } = useTheme(); + + const { params } = useRoute>(); + const navigation = useNavigation>(); + + const isMasterDetail = useAppSelector(state => state.app.isMasterDetail); + + const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name); + const user = useAppSelector(state => getUserSelector(state)); + + const [state, updateState] = useReducer( + (state: IRoomMembersViewState, newState: Partial) => ({ ...state, ...newState }), + { isLoading: false, allUsers: false, filtering: '', - rid, members: [], - membersFiltered: [], - room: room || ({} as TSubscriptionModel), + room: params.room || ({} as TSubscriptionModel), end: false, + roomRoles: null, + filter: '', page: 0 + } + ); + + const teamPermissions: TSupportedPermissions[] = state.room.teamMain + ? ['edit-team-member', 'view-all-team-channels', 'view-all-teams'] + : []; + + const [ + muteUserPermission, + setLeaderPermission, + setOwnerPermission, + setModeratorPermission, + removeUserPermission, + editTeamMemberPermission, + viewAllTeamChannelsPermission, + viewAllTeamsPermission + ] = usePermissions(['mute-user', 'set-leader', 'set-owner', 'set-moderator', 'remove-user', ...teamPermissions], params.rid); + + useEffect(() => { + const subscription = params?.room?.observe && params.room.observe().subscribe(changes => updateState({ room: changes })); + setHeader(true); + fetchMembers(true); + return () => subscription?.unsubscribe(); + }, []); + + useEffect(() => { + const fetchRoles = () => { + if (isGroupChat(state.room)) { + return; + } + if ( + muteUserPermission || + setLeaderPermission || + setOwnerPermission || + setModeratorPermission || + removeUserPermission || + editTeamMemberPermission || + viewAllTeamChannelsPermission || + viewAllTeamsPermission + ) { + fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState); + } }; - if (room && room.observe) { - this.roomObservable = room.observe(); - this.subscription = this.roomObservable.subscribe(changes => { - if (this.mounted) { - this.setState({ room: changes }); - } else { - this.setState({ room: changes }); - } - }); + fetchRoles(); + }, [ + muteUserPermission, + setLeaderPermission, + setOwnerPermission, + setModeratorPermission, + removeUserPermission, + editTeamMemberPermission, + viewAllTeamChannelsPermission, + viewAllTeamsPermission + ]); + + const toggleStatus = (status: boolean) => { + try { + updateState({ members: [], allUsers: status, end: false }); + fetchMembers(status); + setHeader(status); + } catch (e) { + log(e); } - this.setHeader(); - } + }; - async componentDidMount() { - const { room } = this.state; - this.mounted = true; - this.fetchMembers(); - - if (isGroupChat(room)) { - return; - } - - const { - muteUserPermission, - setLeaderPermission, - setOwnerPermission, - setModeratorPermission, - removeUserPermission, - editTeamMemberPermission, - viewAllTeamChannelsPermission, - viewAllTeamsPermission - } = this.props; - - const result = await hasPermission( - [ - muteUserPermission, - setLeaderPermission, - setOwnerPermission, - setModeratorPermission, - removeUserPermission, - ...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : []) - ], - room.rid - ); - - this.permissions = { - 'mute-user': result[0], - 'set-leader': result[1], - 'set-owner': result[2], - 'set-moderator': result[3], - 'remove-user': result[4], - ...(room.teamMain - ? { - 'edit-team-member': result[5], - 'view-all-team-channels': result[6], - 'view-all-teams': result[7] - } - : {}) - }; - - const hasSinglePermission = Object.values(this.permissions).some(p => !!p); - if (hasSinglePermission) { - this.fetchRoomMembersRoles(); - } - } - - componentWillUnmount() { - if (this.subscription && this.subscription.unsubscribe) { - this.subscription.unsubscribe(); - } - } - - setHeader = () => { - const { allUsers } = this.state; - const { navigation } = this.props; - const toggleText = allUsers ? I18n.t('Online') : I18n.t('All'); + const setHeader = (allUsers: boolean) => { navigation.setOptions({ title: I18n.t('Members'), headerRight: () => ( - + + showActionSheet({ + options: [ + { + title: I18n.t('Online'), + onPress: () => toggleStatus(true), + right: () => , + testID: 'room-members-view-toggle-status-online' + }, + { + title: I18n.t('All'), + onPress: () => toggleStatus(false), + right: () => , + testID: 'room-members-view-toggle-status-all' + } + ] + }) + } + testID='room-members-view-filter' + /> ) }); }; - get isServerVersionLowerThan3_16() { - const { serverVersion } = this.props; - return compareServerVersion(serverVersion, 'lowerThan', '3.16.0'); - } + const getUserDisplayName = (user: TUserModel) => (useRealName ? user.name : user.username) || user.username; - onSearchChangeText = debounce((text: string) => { - const { members } = this.state; - text = text.trim(); - if (this.isServerVersionLowerThan3_16) { - let membersFiltered: TUserModel[] = []; - - if (members && members.length > 0 && text) { - membersFiltered = members.filter( - m => m.username.toLowerCase().match(text.toLowerCase()) || m.name?.toLowerCase().match(text.toLowerCase()) - ); - } - return this.setState({ filtering: text, membersFiltered }); - } - - this.setState({ filtering: text, page: 0, members: [], end: false }, () => { - this.fetchMembers(); - }); - }, 500); - - navToDirectMessage = async (item: IUser) => { - try { - const db = database.active; - const subsCollection = db.get('subscriptions'); - const query = await subsCollection.query(Q.where('name', item.username)).fetch(); - if (query.length) { - const [room] = query; - this.goRoom(room); - } else { - const result = await Services.createDirectMessage(item.username); - if (result.success) { - this.goRoom({ rid: result.room?._id as string, name: item.username, t: SubscriptionType.DIRECT }); - } - } - } catch (e) { - log(e); - } - }; - - handleRemoveFromTeam = async (selectedUser: TUserModel) => { - try { - const { navigation } = this.props; - const { room } = this.state; - - const result = await Services.teamListRoomsOfUser({ teamId: room.teamId as string, userId: selectedUser._id }); - - if (result.success) { - if (result.rooms?.length) { - const teamChannels = result.rooms.map((r: any) => ({ - rid: r._id, - name: r.name, - teamId: r.teamId, - alert: r.isLastOwner - })); - navigation.navigate('SelectListView', { - title: 'Remove_Member', - infoText: 'Remove_User_Team_Channels', - data: teamChannels, - nextAction: (selected: any) => this.removeFromTeam(selectedUser, selected), - showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove')) - }); - } else { - showConfirmationAlert({ - message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), - confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), - onPress: () => this.removeFromTeam(selectedUser) - }); - } - } - } catch (e) { - showConfirmationAlert({ - message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), - confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), - onPress: () => this.removeFromTeam(selectedUser) - }); - } - }; - - removeFromTeam = async (selectedUser: IUser, selected?: any) => { - try { - const { members, membersFiltered, room } = this.state; - const { navigation } = this.props; - - const userId = selectedUser._id; - const result = await Services.removeTeamMember({ - teamId: room.teamId, - userId, - ...(selected && { rooms: selected }) - }); - if (result.success) { - const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) }); - EventEmitter.emit(LISTENER, { message }); - const newMembers = members.filter(member => member._id !== userId); - const newMembersFiltered = this.isServerVersionLowerThan3_16 - ? membersFiltered.filter(member => member._id !== userId) - : []; - this.setState({ - members: newMembers, - membersFiltered: newMembersFiltered - }); - // @ts-ignore - This is just to force a reload - navigation.navigate('RoomMembersView'); - } - } catch (e: any) { - log(e); - showErrorAlert( - e.data.error ? I18n.t(e.data.error) : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }), - I18n.t('Cannot_remove') - ); - } - }; - - onPressUser = (selectedUser: TUserModel) => { - const { room } = this.state; - const { showActionSheet, user, theme } = this.props; + const onPressUser = (selectedUser: TUserModel) => { + const { room, roomRoles, members } = state; const options: TActionSheetOptionsItem[] = [ { icon: 'message', title: I18n.t('Direct_message'), - onPress: () => this.navToDirectMessage(selectedUser) + onPress: () => navToDirectMessage(selectedUser, isMasterDetail) } ]; @@ -316,12 +207,12 @@ class RoomMembersView extends React.Component this.handleIgnore(selectedUser, !isIgnored), + onPress: () => handleIgnore(selectedUser, !isIgnored, room.rid), testID: 'action-sheet-ignore-user' }); } - if (this.permissions['mute-user']) { + if (muteUserPermission) { const { muted = [] } = room; const userIsMuted = muted.find?.(m => m === selectedUser.username); selectedUser.muted = !!userIsMuted; @@ -334,7 +225,7 @@ class RoomMembersView extends React.Component this.handleMute(selectedUser) + onPress: () => handleMute(selectedUser, room.rid) }); }, testID: 'action-sheet-mute-user' @@ -342,78 +233,63 @@ class RoomMembersView extends React.Component r.u._id === selectedUser._id); - const isOwner = userRoleResult?.roles.includes('owner'); + if (setOwnerPermission) { + const isOwner = fetchRole('owner', selectedUser, roomRoles); options.push({ icon: 'shield-check', title: I18n.t('Owner'), - onPress: () => this.handleOwner(selectedUser, !isOwner), - right: () => ( - - ), + onPress: () => + handleOwner(selectedUser, !isOwner, getUserDisplayName(selectedUser), room, () => + fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState) + ), + right: () => , testID: 'action-sheet-set-owner' }); } // Leader - if (this.permissions['set-leader']) { - const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id); - const isLeader = userRoleResult?.roles.includes('leader'); + if (setLeaderPermission) { + const isLeader = fetchRole('leader', selectedUser, roomRoles); options.push({ icon: 'shield-alt', title: I18n.t('Leader'), - onPress: () => this.handleLeader(selectedUser, !isLeader), - right: () => ( - - ), + onPress: () => + handleLeader(selectedUser, !isLeader, room, getUserDisplayName(selectedUser), () => + fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState) + ), + right: () => , testID: 'action-sheet-set-leader' }); } // Moderator - if (this.permissions['set-moderator']) { - const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id); - const isModerator = userRoleResult?.roles.includes('moderator'); + if (setModeratorPermission) { + const isModerator = fetchRole('moderator', selectedUser, roomRoles); options.push({ icon: 'shield', title: I18n.t('Moderator'), - onPress: () => this.handleModerator(selectedUser, !isModerator), - right: () => ( - - ), + onPress: () => + handleModerator(selectedUser, !isModerator, room, getUserDisplayName(selectedUser), () => + fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState) + ), + right: () => , testID: 'action-sheet-set-moderator' }); } // Remove from team - if (this.permissions['edit-team-member']) { + if (editTeamMemberPermission) { options.push({ icon: 'logout', danger: true, title: I18n.t('Remove_from_Team'), - onPress: () => this.handleRemoveFromTeam(selectedUser), + onPress: () => handleRemoveFromTeam(selectedUser, updateState, room, members), testID: 'action-sheet-remove-from-team' }); } // Remove from room - if (this.permissions['remove-user'] && !room.teamMain) { + if (removeUserPermission && !room.teamMain) { options.push({ icon: 'logout', title: I18n.t('Remove_from_room'), @@ -422,7 +298,13 @@ class RoomMembersView extends React.Component this.handleRemoveUserFromRoom(selectedUser) + onPress: () => { + handleRemoveUserFromRoom(selectedUser, room, () => + updateState({ + members: members.filter(member => member._id !== selectedUser._id) + }) + ); + } }); }, testID: 'action-sheet-remove-from-room' @@ -435,256 +317,83 @@ class RoomMembersView extends React.Component { - try { - const { allUsers } = this.state; - this.setState({ members: [], allUsers: !allUsers, end: false, page: 0 }, () => { - this.fetchMembers(); - }); - } catch (e) { - log(e); - } - }; - - fetchRoomMembersRoles = async () => { - try { - const { room } = this.state; - const type = room.t as SubscriptionType.CHANNEL | SubscriptionType.GROUP | SubscriptionType.OMNICHANNEL; - const result = await Services.getRoomRoles(room.rid, type); - if (result?.success) { - this.roomRoles = result.roles; - } - } catch (e) { - log(e); - } - }; - - fetchMembers = async () => { - const { rid, members, isLoading, allUsers, end, room, filtering, page } = this.state; + const fetchMembers = async (status: boolean) => { + const { members, isLoading, end, room, filter, page } = state; const { t } = room; if (isLoading || end) { return; } - this.setState({ isLoading: true }); + updateState({ isLoading: true }); try { const membersResult = await Services.getRoomMembers({ - rid, + rid: room.rid, roomType: t, - type: allUsers ? 'all' : 'online', - filter: filtering, + type: !status ? 'all' : 'online', + filter, skip: PAGE_SIZE * page, limit: PAGE_SIZE, - allUsers + allUsers: !status }); const end = membersResult?.length < PAGE_SIZE; const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id)); - this.setState({ - members: members.concat(membersResultFiltered || []), + updateState({ + members: [...members, ...membersResultFiltered], isLoading: false, end, page: page + 1 }); - this.setHeader(); } catch (e) { log(e); - this.setState({ isLoading: false }); + updateState({ isLoading: false }); } }; - goRoom = (item: TGoRoomItem) => { - const { navigation, isMasterDetail } = this.props; - if (isMasterDetail) { - // @ts-ignore - navigation.navigate('DrawerNavigator'); - } else { - navigation.popToTop(); - } - goRoom({ item, isMasterDetail }); - }; + const filteredMembers = + state.members && state.members.length > 0 && state.filter + ? state.members.filter( + m => + m.username.toLowerCase().match(state.filter.toLowerCase()) || m.name?.toLowerCase().match(state.filter.toLowerCase()) + ) + : null; - getUserDisplayName = (user: TUserModel) => { - const { useRealName } = this.props; - return (useRealName ? user.name : user.username) || user.username; - }; - - handleMute = async (user: TUserModel) => { - const { rid } = this.state; - try { - await Services.toggleMuteUserInRoom(rid, user?.username, !user?.muted); - EventEmitter.emit(LISTENER, { - message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') }) - }); - } catch (e) { - log(e); - } - }; - - handleOwner = async (selectedUser: TUserModel, isOwner: boolean) => { - try { - const { room } = this.state; - await Services.toggleRoomOwner({ - roomId: room.rid, - t: room.t, - userId: selectedUser._id, - isOwner - }); - const message = isOwner - ? 'User__username__is_now_a_owner_of__room_name_' - : 'User__username__removed_from__room_name__owners'; - EventEmitter.emit(LISTENER, { - message: I18n.t(message, { - username: this.getUserDisplayName(selectedUser), - room_name: getRoomTitle(room) - }) - }); - } catch (e) { - log(e); - } - this.fetchRoomMembersRoles(); - }; - - handleLeader = async (selectedUser: TUserModel, isLeader: boolean) => { - try { - const { room } = this.state; - await Services.toggleRoomLeader({ - roomId: room.rid, - t: room.t, - userId: selectedUser._id, - isLeader - }); - const message = isLeader - ? 'User__username__is_now_a_leader_of__room_name_' - : 'User__username__removed_from__room_name__leaders'; - EventEmitter.emit(LISTENER, { - message: I18n.t(message, { - username: this.getUserDisplayName(selectedUser), - room_name: getRoomTitle(room) - }) - }); - } catch (e) { - log(e); - } - this.fetchRoomMembersRoles(); - }; - - handleModerator = async (selectedUser: TUserModel, isModerator: boolean) => { - try { - const { room } = this.state; - await Services.toggleRoomModerator({ - roomId: room.rid, - t: room.t, - userId: selectedUser._id, - isModerator - }); - const message = isModerator - ? 'User__username__is_now_a_moderator_of__room_name_' - : 'User__username__removed_from__room_name__moderators'; - EventEmitter.emit(LISTENER, { - message: I18n.t(message, { - username: this.getUserDisplayName(selectedUser), - room_name: getRoomTitle(room) - }) - }); - } catch (e) { - log(e); - } - this.fetchRoomMembersRoles(); - }; - - handleIgnore = async (selectedUser: TUserModel, ignore: boolean) => { - try { - const { room } = this.state; - await Services.ignoreUser({ - rid: room.rid, - userId: selectedUser._id, - ignore - }); - const message = I18n.t(ignore ? 'User_has_been_ignored' : 'User_has_been_unignored'); - EventEmitter.emit(LISTENER, { message }); - } catch (e) { - log(e); - } - }; - - handleRemoveUserFromRoom = async (selectedUser: TUserModel) => { - try { - const { room, members, membersFiltered } = this.state; - const userId = selectedUser._id; - // TODO: interface SubscriptionType on IRoom is wrong - await Services.removeUserFromRoom({ roomId: room.rid, t: room.t as RoomTypes, userId }); - const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) }); - EventEmitter.emit(LISTENER, { message }); - this.setState({ - members: members.filter(member => member._id !== userId), - membersFiltered: this.isServerVersionLowerThan3_16 ? membersFiltered.filter(member => member._id !== userId) : [] - }); - } catch (e) { - log(e); - } - }; - - renderSearchBar = () => this.onSearchChangeText(text)} testID='room-members-view-search' />; - - renderItem = ({ item }: { item: TUserModel }) => { - const { theme } = this.props; - - return ( - this.onPressUser(item)} - testID={`room-members-view-item-${item.username}`} - theme={theme} + return ( + + + ( + + onPressUser(item)} + testID={`room-members-view-item-${item.username}`} + /> + + )} + style={styles.list} + keyExtractor={item => item._id} + ItemSeparatorComponent={List.Separator} + ListHeaderComponent={ + <> + + + updateState({ filter: text.trim() })} testID='room-members-view-search' /> + + + } + ListFooterComponent={() => (state.isLoading ? : null)} + onEndReachedThreshold={0.1} + onEndReached={() => fetchMembers(state.allUsers)} + ListEmptyComponent={() => + state.end ? {I18n.t('No_members_found')} : null + } + {...scrollPersistTaps} /> - ); - }; + + ); +}; - render() { - const { filtering, members, membersFiltered, isLoading } = this.state; - const { theme } = this.props; - return ( - - - item._id} - ItemSeparatorComponent={List.Separator} - ListHeaderComponent={this.renderSearchBar} - ListFooterComponent={() => { - if (isLoading) { - return ; - } - return null; - }} - onEndReachedThreshold={0.1} - onEndReached={this.fetchMembers} - maxToRenderPerBatch={5} - windowSize={10} - {...scrollPersistTaps} - /> - - ); - } -} - -const mapStateToProps = (state: IApplicationState) => ({ - baseUrl: state.server.server, - user: getUserSelector(state), - isMasterDetail: state.app.isMasterDetail, - useRealName: state.settings.UI_Use_Real_Name, - muteUserPermission: state.permissions['mute-user'], - setLeaderPermission: state.permissions['set-leader'], - setOwnerPermission: state.permissions['set-owner'], - setModeratorPermission: state.permissions['set-moderator'], - removeUserPermission: state.permissions['remove-user'], - editTeamMemberPermission: state.permissions['edit-team-member'], - viewAllTeamChannelsPermission: state.permissions['view-all-team-channels'], - viewAllTeamsPermission: state.permissions['view-all-teams'], - serverVersion: state.server.version -}); - -export default connect(mapStateToProps)(withTheme(withActionSheet(RoomMembersView))); +export default RoomMembersView; diff --git a/app/views/RoomMembersView/styles.ts b/app/views/RoomMembersView/styles.ts index d183ebb09..8531b8172 100644 --- a/app/views/RoomMembersView/styles.ts +++ b/app/views/RoomMembersView/styles.ts @@ -1,20 +1,15 @@ import { StyleSheet } from 'react-native'; +import sharedStyles from '../Styles'; + export default StyleSheet.create({ list: { flex: 1 }, - item: { - flexDirection: 'row', - paddingVertical: 10, - paddingHorizontal: 16, - alignItems: 'center' - }, - avatar: { - marginRight: 16 - }, - separator: { - height: StyleSheet.hairlineWidth, - marginLeft: 60 + noResult: { + fontSize: 16, + paddingVertical: 56, + ...sharedStyles.textSemibold, + ...sharedStyles.textAlignCenter } }); diff --git a/app/views/SelectedUsersView.tsx b/app/views/SelectedUsersView.tsx index 08a720836..a651a7178 100644 --- a/app/views/SelectedUsersView.tsx +++ b/app/views/SelectedUsersView.tsx @@ -211,19 +211,15 @@ class SelectedUsersView extends React.Component { - const { theme } = this.props; - return ( - this._onPressSelectedItem(item)} - testID={`selected-user-${item.name}`} - style={{ paddingRight: 15 }} - theme={theme} - /> - ); - }; + renderSelectedItem = ({ item }: { item: ISelectedUser }) => ( + this._onPressSelectedItem(item)} + testID={`selected-user-${item.name}`} + style={{ paddingRight: 15 }} + /> + ); renderItem = ({ item, index }: { item: ISelectedUser; index: number }) => { const { search, chats } = this.state; @@ -249,7 +245,6 @@ class SelectedUsersView extends React.Component ); }; diff --git a/e2e/tests/room/03-roomactions.spec.js b/e2e/tests/room/03-roomactions.spec.js index 74a0fdbf8..5acb56f0a 100644 --- a/e2e/tests/room/03-roomactions.spec.js +++ b/e2e/tests/room/03-roomactions.spec.js @@ -146,10 +146,6 @@ describe('Room actions screen', () => { await expect(element(by.id('room-actions-members'))).toExist(); }); - it('should have add user', async () => { - await expect(element(by.id('room-actions-add-user'))).toExist(); - }); - it('should have files', async () => { await expect(element(by.id('room-actions-files'))).toExist(); }); @@ -303,24 +299,12 @@ describe('Room actions screen', () => { .withTimeout(4000); }); - it('should have notification audio option', async () => { - await waitFor(element(by.id('notification-preference-view-audio'))) - .toExist() - .withTimeout(4000); - }); - it('should have notification sound option', async () => { await waitFor(element(by.id('notification-preference-view-sound'))) .toExist() .withTimeout(4000); }); - it('should have notification duration option', async () => { - await waitFor(element(by.id('notification-preference-view-notification-duration'))) - .toExist() - .withTimeout(4000); - }); - it('should have email alert option', async () => { await waitFor(element(by.id('notification-preference-view-email-alert'))) .toExist() @@ -361,6 +345,14 @@ describe('Room actions screen', () => { }); it('should add users to the room', async () => { + await waitFor(element(by.id('room-actions-members'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-actions-members')).tap(); + await waitFor(element(by.id('room-members-view'))) + .toExist() + .withTimeout(2000); + await waitFor(element(by.id('room-actions-add-user'))) .toExist() .withTimeout(4000); @@ -392,19 +384,14 @@ describe('Room actions screen', () => { await element(by.id('selected-users-view-submit')).tap(); await sleep(300); - await waitFor(element(by.id('room-actions-members'))) - .toExist() - .withTimeout(10000); - await element(by.id('room-actions-members')).tap(); - await element(by.id('room-members-view-toggle-status')).tap(); - await waitFor(element(by.id(`room-members-view-item-${user.username}`))) - .toExist() - .withTimeout(60000); await backToActions(); }); describe('Room Members', () => { before(async () => { + await waitFor(element(by.id('room-actions-members'))) + .toExist() + .withTimeout(2000); await element(by.id('room-actions-members')).tap(); await waitFor(element(by.id('room-members-view'))) .toExist() @@ -442,13 +429,30 @@ describe('Room actions screen', () => { }; it('should show all users', async () => { - await element(by.id('room-members-view-toggle-status')).tap(); + await waitFor(element(by.id('room-members-view-filter'))) + .toExist() + .withTimeout(10000); + await element(by.id('room-members-view-filter')).tap(); + await waitFor(element(by.id('room-members-view-toggle-status-all'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-members-view-toggle-status-all')).tap(); await waitFor(element(by.id(`room-members-view-item-${user.username}`))) .toExist() .withTimeout(60000); + await tapBack(); }); it('should filter user', async () => { + await waitFor(element(by.id('room-actions-members'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-actions-members')).tap(); + await element(by.id('room-members-view-filter')).tap(); + await waitFor(element(by.id('room-members-view-toggle-status-all'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-members-view-toggle-status-all')).tap(); await waitFor(element(by.id(`room-members-view-item-${user.username}`))) .toExist() .withTimeout(60000); @@ -595,11 +599,21 @@ describe('Room actions screen', () => { await waitFor(element(by.id('room-actions-view'))) .toExist() .withTimeout(5000); + await waitFor(element(by.id('room-actions-members'))) + .toExist() + .withTimeout(2000); await element(by.id('room-actions-members')).tap(); await waitFor(element(by.id('room-members-view'))) .toExist() .withTimeout(2000); - await element(by.id('room-members-view-toggle-status')).tap(); + await waitFor(element(by.id('room-members-view-filter'))) + .toExist() + .withTimeout(10000); + await element(by.id('room-members-view-filter')).tap(); + await waitFor(element(by.id('room-members-view-toggle-status-all'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-members-view-toggle-status-all')).tap(); await waitFor(element(by.id(`room-members-view-item-${user.username}`))) .toExist() .withTimeout(60000); @@ -625,6 +639,7 @@ describe('Room actions screen', () => { }); it('should block/unblock user', async () => { + await element(by.id('room-actions-scrollview')).scrollTo('bottom'); await waitFor(element(by.id('room-actions-block-user'))).toExist(); await element(by.id('room-actions-block-user')).tap(); await waitFor(element(by[textMatcher]('Unblock user'))) diff --git a/e2e/tests/team/02-team.spec.js b/e2e/tests/team/02-team.spec.js index 6994703cb..47a3213da 100644 --- a/e2e/tests/team/02-team.spec.js +++ b/e2e/tests/team/02-team.spec.js @@ -266,6 +266,11 @@ describe('Team', () => { }); it('should add users to the team', async () => { + await element(by.id('room-actions-members')).tap(); + await waitFor(element(by.id('room-members-view'))) + .toExist() + .withTimeout(2000); + await waitFor(element(by.id('room-actions-add-user'))) .toExist() .withTimeout(10000); @@ -296,11 +301,17 @@ describe('Team', () => { await element(by.id('selected-users-view-submit')).tap(); await sleep(300); + await tapBack(); + await sleep(300); await waitFor(element(by.id('room-actions-members'))) .toExist() .withTimeout(10000); await element(by.id('room-actions-members')).tap(); - await element(by.id('room-members-view-toggle-status')).tap(); + await element(by.id('room-members-view-filter')).tap(); + await waitFor(element(by.id('room-members-view-toggle-status-all'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-members-view-toggle-status-all')).tap(); await waitFor(element(by.id(`room-members-view-item-${user.username}`))) .toExist() .withTimeout(60000); @@ -358,7 +369,11 @@ describe('Team', () => { }); it('should show all users', async () => { - await element(by.id('room-members-view-toggle-status')).tap(); + await element(by.id('room-members-view-filter')).tap(); + await waitFor(element(by.id('room-members-view-toggle-status-all'))) + .toExist() + .withTimeout(2000); + await element(by.id('room-members-view-toggle-status-all')).tap(); await waitFor(element(by.id(`room-members-view-item-${user.username}`))) .toExist() .withTimeout(60000); From 9cbffff2484a5485b8316db6a6001def52c892f8 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:16:45 -0300 Subject: [PATCH 003/145] [IMPROVE] Redesign create room flow (#4381) Co-authored-by: Diego Mello --- .storybook/storybook.requires.js | 2 + .../Chip/__snapshots__/Chip.stories.storyshot | 11 + .../SwitchItem.stories.storyshot | 3 + app/containers/Chip/Chip.stories.tsx | 32 ++ app/containers/Chip/Chip.test.tsx | 58 +++ app/containers/Chip/index.tsx | 75 +++ .../TextInput/ControlledFormTextInput.tsx | 17 + app/containers/TextInput/index.ts | 1 + app/containers/UserItem.tsx | 17 +- app/i18n/locales/ar.json | 5 +- app/i18n/locales/de.json | 8 +- app/i18n/locales/en.json | 26 +- app/i18n/locales/es-ES.json | 5 +- app/i18n/locales/fr.json | 8 +- app/i18n/locales/it.json | 5 +- app/i18n/locales/ja.json | 5 +- app/i18n/locales/nl.json | 8 +- app/i18n/locales/pt-BR.json | 24 +- app/i18n/locales/pt-PT.json | 5 +- app/i18n/locales/ru.json | 8 +- app/i18n/locales/tr.json | 5 +- app/i18n/locales/zh-CN.json | 5 +- app/i18n/locales/zh-TW.json | 5 +- app/stacks/InsideStack.tsx | 10 +- app/stacks/MasterDetailStack/index.tsx | 4 +- app/views/CreateChannelView.tsx | 427 ------------------ .../RoomSettings/SwitchItem.stories.tsx | 40 ++ .../RoomSettings/SwitchItem.test.tsx | 68 +++ .../RoomSettings/SwitchItem.tsx | 59 +++ .../RoomSettings/SwitchItemEncrypted.test.tsx | 108 +++++ .../RoomSettings/SwitchItemEncrypted.tsx | 48 ++ .../RoomSettings/SwitchItemReadOnly.tsx | 37 ++ .../RoomSettings/SwitchItemType.tsx | 43 ++ .../CreateChannelView/RoomSettings/index.tsx | 78 ++++ app/views/CreateChannelView/index.tsx | 205 +++++++++ app/views/NewMessageView.tsx | 335 -------------- app/views/NewMessageView/ButtonCreate.tsx | 32 ++ app/views/NewMessageView/HeaderNewMessage.tsx | 107 +++++ app/views/NewMessageView/index.tsx | 115 +++++ app/views/RoomInfoEditView/index.tsx | 2 +- app/views/RoomInfoView/Channel.tsx | 4 +- app/views/RoomMembersView/helpers.ts | 1 + app/views/SelectedUsersView.tsx | 291 ------------ app/views/SelectedUsersView/Header.tsx | 74 +++ app/views/SelectedUsersView/index.tsx | 180 ++++++++ e2e/helpers/app.js | 3 +- e2e/tests/assorted/02-broadcast.spec.js | 2 +- e2e/tests/assorted/03-profile.spec.js | 2 +- e2e/tests/room/04-discussion.spec.js | 2 +- e2e/tests/room/06-createdmgroup.spec.js | 2 +- e2e/tests/room/08-roominfo.spec.js | 24 +- e2e/tests/team/01-createteam.spec.js | 3 + package.json | 5 +- yarn.lock | 50 +- 54 files changed, 1532 insertions(+), 1167 deletions(-) create mode 100644 __tests__/containers/Chip/__snapshots__/Chip.stories.storyshot create mode 100644 __tests__/views/CreateChannelView/RoomSettings/__snapshots__/SwitchItem.stories.storyshot create mode 100644 app/containers/Chip/Chip.stories.tsx create mode 100644 app/containers/Chip/Chip.test.tsx create mode 100644 app/containers/Chip/index.tsx create mode 100644 app/containers/TextInput/ControlledFormTextInput.tsx delete mode 100644 app/views/CreateChannelView.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItem.test.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItem.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItemReadOnly.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/SwitchItemType.tsx create mode 100644 app/views/CreateChannelView/RoomSettings/index.tsx create mode 100644 app/views/CreateChannelView/index.tsx delete mode 100644 app/views/NewMessageView.tsx create mode 100644 app/views/NewMessageView/ButtonCreate.tsx create mode 100644 app/views/NewMessageView/HeaderNewMessage.tsx create mode 100644 app/views/NewMessageView/index.tsx delete mode 100644 app/views/SelectedUsersView.tsx create mode 100644 app/views/SelectedUsersView/Header.tsx create mode 100644 app/views/SelectedUsersView/index.tsx diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index 4f02264ec..1baa14ff4 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -22,6 +22,7 @@ const getStories = () => { require("../app/containers/Avatar/Avatar.stories.tsx"), require("../app/containers/BackgroundContainer/index.stories.tsx"), require("../app/containers/Button/Button.stories.tsx"), + require("../app/containers/Chip/Chip.stories.tsx"), require("../app/containers/HeaderButton/HeaderButtons.stories.tsx"), require("../app/containers/List/List.stories.tsx"), require("../app/containers/LoginServices/LoginServices.stories.tsx"), @@ -38,6 +39,7 @@ const getStories = () => { require("../app/containers/UIKit/UiKitModal.stories.tsx"), require("../app/containers/UnreadBadge/UnreadBadge.stories.tsx"), require("../app/views/CannedResponsesListView/CannedResponseItem.stories.tsx"), + require("../app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx"), require("../app/views/DiscussionsView/Item.stories.tsx"), require("../app/views/RoomView/LoadMore/LoadMore.stories.tsx"), require("../app/views/ThreadMessagesView/Item.stories.tsx"), diff --git a/__tests__/containers/Chip/__snapshots__/Chip.stories.storyshot b/__tests__/containers/Chip/__snapshots__/Chip.stories.storyshot new file mode 100644 index 000000000..83a6eb4de --- /dev/null +++ b/__tests__/containers/Chip/__snapshots__/Chip.stories.storyshot @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; + +exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; + +exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; + +exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`; + +exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`; diff --git a/__tests__/views/CreateChannelView/RoomSettings/__snapshots__/SwitchItem.stories.storyshot b/__tests__/views/CreateChannelView/RoomSettings/__snapshots__/SwitchItem.stories.storyshot new file mode 100644 index 000000000..4012a3d13 --- /dev/null +++ b/__tests__/views/CreateChannelView/RoomSettings/__snapshots__/SwitchItem.stories.storyshot @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots SwitchItem Switch 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"minHeight\\":54,\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"space-between\\",\\"flexDirection\\":\\"row\\",\\"maxHeight\\":80,\\"marginBottom\\":12},{\\"backgroundColor\\":\\"#ffffff\\"}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"marginRight\\":8}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Welcome to Rocket.Chat\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"testID\\":\\"create-channel-switch-id-hint\\",\\"style\\":[{\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"Only authorized users can write new messages\\"]}]},{\\"type\\":\\"RCTSwitch\\",\\"props\\":{\\"testID\\":\\"create-channel-switch-id\\",\\"disabled\\":false,\\"onTintColor\\":\\"#2de0a5\\",\\"style\\":{\\"height\\":31,\\"width\\":51},\\"tintColor\\":\\"#f5455c\\",\\"value\\":false,\\"accessibilityRole\\":\\"switch\\"},\\"children\\":null}]}]}"`; diff --git a/app/containers/Chip/Chip.stories.tsx b/app/containers/Chip/Chip.stories.tsx new file mode 100644 index 000000000..f4a83eda1 --- /dev/null +++ b/app/containers/Chip/Chip.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import Chip, { IChip } from './index'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'flex-start', + padding: 16 + } +}); + +export default { + title: 'Chip' +}; + +const ChipWrapped = ({ avatar, text, onPress, testID, style }: IChip) => ( + + + +); + +export const ChipText = () => {}} />; + +export const ChipWithShortText = () => {}} />; + +export const ChipWithoutAvatar = () => {}} />; + +export const ChipWithoutIcon = () => ; + +export const ChipWithoutAvatarAndIcon = () => ; diff --git a/app/containers/Chip/Chip.test.tsx b/app/containers/Chip/Chip.test.tsx new file mode 100644 index 000000000..c222b91c3 --- /dev/null +++ b/app/containers/Chip/Chip.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import Chip, { IChip } from '.'; +import { ISelectedUser } from '../../reducers/selectedUsers'; +import { mockedStore as store } from '../../reducers/mockedStore'; + +const onPressMock = jest.fn((item: any) => item); + +const testChip = { + testID: 'test-chip-id', + item: { fname: 'rocket.chat', name: 'rocket.chat' } as ISelectedUser, + onPress: onPressMock +}; + +const Render = ({ testID, text, avatar, onPress }: IChip) => ( + + + +); + +describe('Chips', () => { + it('should render the Chip component', () => { + const { findByTestId } = render( + testChip.onPress(testChip.item)} + /> + ); + + expect(findByTestId(testChip.testID)).toBeTruthy(); + }); + it("should not call onPress if it's not passed", async () => { + const { findByTestId } = render(); + + const component = await findByTestId(testChip.testID); + fireEvent.press(component); + expect(onPressMock).not.toHaveBeenCalled(); + }); + it('should tap Chip and return item', async () => { + const { findByTestId } = render( + testChip.onPress(testChip.item)} + /> + ); + + const component = await findByTestId(testChip.testID); + fireEvent.press(component); + expect(onPressMock).toHaveBeenCalled(); + expect(onPressMock).toHaveReturnedWith(testChip.item); + }); +}); diff --git a/app/containers/Chip/index.tsx b/app/containers/Chip/index.tsx new file mode 100644 index 000000000..a01dfc258 --- /dev/null +++ b/app/containers/Chip/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Pressable, StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native'; + +import { useTheme } from '../../theme'; +import { CustomIcon } from '../CustomIcon'; +import sharedStyles from '../../views/Styles'; +import Avatar from '../Avatar'; + +const styles = StyleSheet.create({ + pressable: { + paddingHorizontal: 8, + marginRight: 8, + borderRadius: 2, + justifyContent: 'center', + maxWidth: 192 + }, + container: { + flexDirection: 'row', + alignItems: 'center' + }, + avatar: { + marginRight: 8, + marginVertical: 8 + }, + textContainer: { + marginRight: 8, + maxWidth: 120 + }, + name: { + fontSize: 16, + ...sharedStyles.textMedium + } +}); + +export interface IChip { + avatar?: string; + text: string; + onPress?: Function; + testID?: string; + style?: StyleProp; +} + +const Chip = ({ avatar, text, onPress, testID, style }: IChip) => { + const { colors } = useTheme(); + + return ( + [ + styles.pressable, + { + backgroundColor: pressed ? colors.bannerBackground : colors.auxiliaryBackground + }, + style + ]} + disabled={!onPress} + onPress={() => onPress?.()} + android_ripple={{ + color: colors.bannerBackground + }} + > + + {avatar ? : null} + + + {text} + + + {onPress ? : null} + + + ); +}; + +export default Chip; diff --git a/app/containers/TextInput/ControlledFormTextInput.tsx b/app/containers/TextInput/ControlledFormTextInput.tsx new file mode 100644 index 000000000..5b69b0e20 --- /dev/null +++ b/app/containers/TextInput/ControlledFormTextInput.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import { FormTextInput, IRCTextInputProps } from './FormTextInput'; + +interface IControlledFormTextInputProps extends IRCTextInputProps { + control: Control; + name: string; +} + +export const ControlledFormTextInput = ({ control, name, ...props }: IControlledFormTextInputProps) => ( + } + /> +); diff --git a/app/containers/TextInput/index.ts b/app/containers/TextInput/index.ts index 48a640fc2..a9b215823 100644 --- a/app/containers/TextInput/index.ts +++ b/app/containers/TextInput/index.ts @@ -1,2 +1,3 @@ export * from './TextInput'; export * from './FormTextInput'; +export * from './ControlledFormTextInput'; diff --git a/app/containers/UserItem.tsx b/app/containers/UserItem.tsx index e044dd6ba..02739d2f8 100644 --- a/app/containers/UserItem.tsx +++ b/app/containers/UserItem.tsx @@ -25,13 +25,9 @@ const styles = StyleSheet.create({ marginRight: 15 }, name: { - fontSize: 17, + fontSize: 16, ...sharedStyles.textMedium }, - username: { - fontSize: 14, - ...sharedStyles.textRegular - }, icon: { marginHorizontal: 15, alignSelf: 'center' @@ -46,10 +42,12 @@ interface IUserItem { onLongPress?: () => void; style?: StyleProp; icon?: TIconsName | null; + iconColor?: string; } -const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon }: IUserItem): React.ReactElement => { +const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon, iconColor }: IUserItem) => { const { colors } = useTheme(); + return ( - + {name} - - @{username} - - {icon ? : null} + {icon ? : null} ); diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 2315fbf7e..ecfc038dd 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -117,8 +117,7 @@ "Black": "أسود", "Block_user": "حظر المستخدم", "Browser": "المتصفح", - "Broadcast_channel_Description": "يمكن فقط للمستخدمين المصرح لهم كتابة رسائل جديدة، ولكن سيتمكن المستخدمون الآخرون من الرد", - "Broadcast_Channel": "قناة البث", + "Broadcast_hint": "يمكن فقط للمستخدمين المصرح لهم كتابة رسائل جديدة، ولكن سيتمكن المستخدمون الآخرون من الرد", "Busy": "مشغول", "By_proceeding_you_are_agreeing": "من خلال المتابعة، أنت توافق على", "Cancel_editing": "إلغاء التعديل", @@ -389,7 +388,6 @@ "Preferences": "التفضيلات", "Preferences_saved": "تم حفظ التفضيلات", "Privacy_Policy": "سياسة الخصوصية", - "Private_Channel": "قناة خاصة", "Private": "خاص", "Processing": "جار معالجة...", "Profile_saved_successfully": "تم حفظ الملف الشخصي بنجاح!", @@ -403,7 +401,6 @@ "Reactions": "التفاعلات", "Read_External_Permission_Message": "يحتاج Rocket.chat للوصول إلى الصور والملفات الموجودة على الجهاز", "Read_External_Permission": "صلاحية قراءة الوسائط", - "Read_Only_Channel": "قناة للقراءة فقط", "Read_Only": "قراءة فقط", "Read_Receipt": "قراءة المستلم", "Receive_Group_Mentions": "تلقي إشارات المجموعة", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 17e7ad23e..11c5c34c5 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -119,8 +119,7 @@ "Black": "Schwarz", "Block_user": "Benutzer blockieren", "Browser": "Browser", - "Broadcast_channel_Description": "Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten", - "Broadcast_Channel": "Broadcast-Kanal", + "Broadcast_hint": "Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten", "Busy": "Beschäftigt", "By_proceeding_you_are_agreeing": "Indem Sie fortfahren, akzeptieren Sie unsere", "Cancel_editing": "Bearbeitung abbrechen", @@ -394,7 +393,6 @@ "Preferences": "Einstellungen", "Preferences_saved": "Einstellungen gespeichert!", "Privacy_Policy": " Datenschutzbestimmungen", - "Private_Channel": "Privater Kanal", "Private": "Privat", "Processing": "Bearbeite …", "Profile_saved_successfully": "Profil erfolgreich gespeichert!", @@ -409,7 +407,6 @@ "Reactions": "Reaktionen", "Read_External_Permission_Message": "Rocket.Chat benötigt Zugriff auf Ihre Fotos, Medien und Dateien auf Ihrem Gerät", "Read_External_Permission": "Lese-Zugriff auf Medien", - "Read_Only_Channel": "Nur-Lese-Kanal", "Read_Only": "Schreibgeschützt", "Read_Receipt": "Lesebestätigung", "Receive_Group_Mentions": "Gruppen-Benachrichtigungen erhalten", @@ -705,9 +702,6 @@ "Team_not_found": "Team nicht gefunden", "Create_Team": "Team erstellen", "Team_Name": "Team-Name", - "Private_Team": "Privates Team", - "Read_Only_Team": "Nur-Lesen-Team", - "Broadcast_Team": "Broadcast-Team", "creating_team": "Team erstellen", "team-name-already-exists": "Ein Team mit diesem Namen existiert bereits", "Add_Channel_to_Team": "Kanal zum Team hinzufügen", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 5f0c0c134..202a6bceb 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -126,8 +126,6 @@ "Black": "Black", "Block_user": "Block user", "Browser": "Browser", - "Broadcast_channel_Description": "Only authorized users can write new messages, but the other users will be able to reply", - "Broadcast_Channel": "Broadcast Channel", "Busy": "Busy", "By_proceeding_you_are_agreeing": "By proceeding you are agreeing to our", "Cancel_editing": "Cancel editing", @@ -408,7 +406,6 @@ "Preferences": "Preferences", "Preferences_saved": "Preferences saved!", "Privacy_Policy": " Privacy Policy", - "Private_Channel": "Private Channel", "Private": "Private", "Processing": "Processing...", "Profile_saved_successfully": "Profile saved successfully!", @@ -423,7 +420,6 @@ "Reactions": "Reactions", "Read_External_Permission_Message": "Rocket.Chat needs to access photos, media, and files on your device", "Read_External_Permission": "Read Media Permission", - "Read_Only_Channel": "Read Only Channel", "Read_Only": "Read Only", "Read_Receipt": "Read Receipt", "Receive_Group_Mentions": "Receive Group Mentions", @@ -726,9 +722,6 @@ "Team_not_found": "Team not found", "Create_Team": "Create Team", "Team_Name": "Team Name", - "Private_Team": "Private Team", - "Read_Only_Team": "Read Only Team", - "Broadcast_Team": "Broadcast Team", "creating_team": "creating team", "team-name-already-exists": "A team with that name already exists", "Add_Channel_to_Team": "Add Channel to Team", @@ -844,6 +837,25 @@ "totp-invalid": "Code or password invalid", "Close_Chat": "Close Chat", "Select_tags": "Select tags", + "Skip": "Skip", + "N_Selected_members": "{{n}} selected", + "Broadcast": "Broadcast", + "Broadcast_hint": "Only authorized users can write new messages, but the other users will be able to reply", + "Team_hint_private": "Only invited people can join", + "Team_hint_public": "When disabled, anyone can join the team", + "Team_hint_not_read_only": "All users in this team can write messages", + "Team_hint_encrypted": "End to end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.", + "Team_hint_encrypted_not_available": "Only available for private team", + "Channel_hint_private":"Only invited users can access this Channel", + "Channel_hint_public":"Everyone can access this channel", + "Channel_hint_encrypted": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", + "Channel_hint_not_read_only": "All users in the channel can write new messages", + "Channel_hint_encrypted_not_available": "Not available for Public Channels", + "Read_only_hint":"Only authorized users can write new messages", + "Discussion": "Discussion", + "Channel": "Channel", + "Team": "Team", + "Select_Members": "Select Members", "Also_send_thread_message_to_channel_behavior": "Also send thread message to channel", "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Allow users to select the Also send to channel behavior" } \ No newline at end of file diff --git a/app/i18n/locales/es-ES.json b/app/i18n/locales/es-ES.json index 81fa6194e..2a3e5bcf1 100644 --- a/app/i18n/locales/es-ES.json +++ b/app/i18n/locales/es-ES.json @@ -108,8 +108,7 @@ "Back": "Volver", "Black": "Negro", "Block_user": "Bloquear usuario", - "Broadcast_channel_Description": "Sólo los usuarios autorizados pueden escribir nuevos mensajes, el resto podrán responder sobre los mismos.", - "Broadcast_Channel": "Canal de Transmisión", + "Broadcast_hint": "Sólo los usuarios autorizados pueden escribir nuevos mensajes, el resto podrán responder sobre los mismos.", "Busy": "Ocupado", "By_proceeding_you_are_agreeing": "Al proceder estarás de acuerdo", "Cancel_editing": "Cancelar edición", @@ -273,7 +272,6 @@ "Preferences": "Preferencias", "Preferences_saved": "¡Preferencias guardadas!", "Privacy_Policy": "Política de privacidad", - "Private_Channel": "Canal privado", "Private": "Privado", "Processing": "Procesando...", "Profile_saved_successfully": "¡Perfil guardado correctamente!", @@ -286,7 +284,6 @@ "Reactions_are_disabled": "Las reacciones están desactivadas", "Reactions_are_enabled": "Las reacciones están activadas", "Reactions": "Reacciones", - "Read_Only_Channel": "Canal de sólo lectura", "Read_Only": "Sólo lectura ", "Read_Receipt": "Comprobante de lectura", "Receive_Group_Mentions": "Recibir menciones de grupo", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 62e128f49..b30d3ee5d 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -119,8 +119,7 @@ "Black": "Noir", "Block_user": "Bloquer l'utilisateur", "Browser": "Navigateur", - "Broadcast_channel_Description": "Seuls les utilisateurs autorisés peuvent écrire de nouveaux messages, mais les autres utilisateurs pourront répondre.", - "Broadcast_Channel": "Canal de diffusion", + "Broadcast_hint": "Seuls les utilisateurs autorisés peuvent écrire de nouveaux messages, mais les autres utilisateurs pourront répondre.", "Busy": "Occupé", "By_proceeding_you_are_agreeing": "En poursuivant, vous acceptez nos", "Cancel_editing": "Annuler la modification", @@ -398,7 +397,6 @@ "Preferences": "Préférences", "Preferences_saved": "Préférences sauvegardées !", "Privacy_Policy": " Politique de confidentialité", - "Private_Channel": "Canal privé", "Private": "Privé", "Processing": "Traitement...", "Profile_saved_successfully": "Profil enregistré avec succès !", @@ -413,7 +411,6 @@ "Reactions": "Réactions", "Read_External_Permission_Message": "Rocket.Chat doit accéder aux photos, aux médias et aux fichiers sur votre appareil", "Read_External_Permission": "Permission de lecture des fichiers", - "Read_Only_Channel": "Canal en lecture seule", "Read_Only": "Lecture seule", "Read_Receipt": "Accusé de réception", "Receive_Group_Mentions": "Recevoir des mentions de groupe", @@ -715,9 +712,6 @@ "Team_not_found": "Equipe non trouvée", "Create_Team": "Créer une équipe", "Team_Name": "Nom de l'équipe", - "Private_Team": "Equipe privée", - "Read_Only_Team": "Equipe en lecture seule", - "Broadcast_Team": "Equipe de diffusion", "creating_team": "création de l'équipe", "team-name-already-exists": "Une équipe portant ce nom existe déjà", "Add_Channel_to_Team": "Ajouter un canal à l'équipe", diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 9617911ad..9792c34e8 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -115,8 +115,7 @@ "Black": "Nero", "Block_user": "Blocca utente", "Browser": "Browser", - "Broadcast_channel_Description": "Solo gli utenti autorizzati possono scrivere messaggi, ma gli altri utenti saranno in grado di rispondere", - "Broadcast_Channel": "Canale broadcast", + "Broadcast_hint": "Solo gli utenti autorizzati possono scrivere messaggi, ma gli altri utenti saranno in grado di rispondere", "Busy": "Occupato", "By_proceeding_you_are_agreeing": "Procedendo accetti i nostri", "Cancel_editing": "Annulla modifica", @@ -383,7 +382,6 @@ "Preferences": "Impostazioni", "Preferences_saved": "Impostazioni salvate!", "Privacy_Policy": " Privacy Policy", - "Private_Channel": "Canale privato", "Private": "Privato", "Processing": "Elaborazione...", "Profile_saved_successfully": "Profilo salvato correttamente!", @@ -398,7 +396,6 @@ "Reactions": "Reazioni", "Read_External_Permission_Message": "Rocket.Chat deve accedere alle foto, media, e documenti sul tuo dispositivo", "Read_External_Permission": "Permesso di Lettura della Memoria", - "Read_Only_Channel": "Canale in sola lettura", "Read_Only": "Sola lettura", "Read_Receipt": "Conferma di lettura", "Receive_Group_Mentions": "Ricevi menzioni di gruppo", diff --git a/app/i18n/locales/ja.json b/app/i18n/locales/ja.json index 1367a4be5..53c22c5cf 100644 --- a/app/i18n/locales/ja.json +++ b/app/i18n/locales/ja.json @@ -119,8 +119,7 @@ "Black": "ブラック", "Block_user": "ブロックしたユーザー", "Browser": "ブラウザ", - "Broadcast_channel_Description": "許可されたユーザーのみが新しいメッセージを書き込めます。他のユーザーは返信することができます", - "Broadcast_Channel": "配信チャンネル", + "Broadcast_hint": "許可されたユーザーのみが新しいメッセージを書き込めます。他のユーザーは返信することができます", "Busy": "取り込み中", "By_proceeding_you_are_agreeing": "続行することにより、私達を承認します", "Cancel_editing": "編集をキャンセル", @@ -371,7 +370,6 @@ "Preferences": "設定", "Preferences_saved": "設定が保存されました。", "Privacy_Policy": " プライバシーポリシー", - "Private_Channel": "プライベートチャンネル", "Private": "プライベート", "Processing": "処理中...", "Profile_saved_successfully": "プロフィールが保存されました!", @@ -384,7 +382,6 @@ "Reactions_are_disabled": "リアクションは無効化されています", "Reactions_are_enabled": "リアクションは有効化されています", "Reactions": "リアクション", - "Read_Only_Channel": "読み取り専用チャンネル", "Read_Only": "読み取り専用", "Read_Receipt": "レシートを見る", "Receive_Group_Mentions": "グループの通知を受け取る", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index 141638baa..110c743fe 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -119,8 +119,7 @@ "Black": "Zwart", "Block_user": "Blokkeer gebruiker", "Browser": "Browser", - "Broadcast_channel_Description": "Alleen geautoriseerde gebruikers kunnen nieuwe berichten schrijven, maar de andere gebruikers zullen kunnen antwoorden", - "Broadcast_Channel": "Uitzendkanaal", + "Broadcast_hint": "Alleen geautoriseerde gebruikers kunnen nieuwe berichten schrijven, maar de andere gebruikers zullen kunnen antwoorden", "Busy": "Bezig", "By_proceeding_you_are_agreeing": "Door verder te gaan ga je akkoord met onze", "Cancel_editing": "Bewerken annuleren", @@ -398,7 +397,6 @@ "Preferences": "Voorkeuren", "Preferences_saved": "Voorkeuren opgeslagen!", "Privacy_Policy": " Privacybeleid", - "Private_Channel": "Privékanaal", "Private": "Privé", "Processing": "Verwerking...", "Profile_saved_successfully": "Profiel succesvol opgeslagen!", @@ -413,7 +411,6 @@ "Reactions": "Reacties", "Read_External_Permission_Message": "Rocket.Chat heeft toegang nodig tot foto's, media en bestanden op je apparaat", "Read_External_Permission": "Lees toestemming voor media", - "Read_Only_Channel": "Alleen-lezen kanaal", "Read_Only": "Alleen lezen", "Read_Receipt": "Leesbevestiging", "Receive_Group_Mentions": "Groepsvermeldingen ontvangen", @@ -715,9 +712,6 @@ "Team_not_found": "Team niet gevonden", "Create_Team": "Team aanmaken", "Team_Name": "Teamnaam", - "Private_Team": "Privé team", - "Read_Only_Team": "Alleen-lezen team", - "Broadcast_Team": "Broadcast team", "creating_team": "team maken", "team-name-already-exists": "Er bestaat al een team met die naam", "Add_Channel_to_Team": "Kanaal toevoegen aan team", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 694aef0d6..32c5617f1 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -121,8 +121,6 @@ "Black": "Preto", "Block_user": "Bloquear usuário", "Browser": "Navegador", - "Broadcast_channel_Description": "Somente usuários autorizados podem escrever novas mensagens, mas os outros usuários poderão responder", - "Broadcast_Channel": "Canal de Transmissão", "Busy": "Ocupado", "By_proceeding_you_are_agreeing": "Ao prosseguir você está aceitando", "Cancel_editing": "Cancelar edição", @@ -384,7 +382,6 @@ "Preferences": "Preferências", "Preferences_saved": "Preferências salvas!", "Privacy_Policy": " Política de Privacidade", - "Private_Channel": "Canal Privado", "Private": "Privado", "Processing": "Processando...", "Profile_saved_successfully": "Perfil salvo com sucesso!", @@ -399,7 +396,6 @@ "Reactions": "Reações", "Read_External_Permission_Message": "Rocket.Chat precisa acessar fotos, mídia e arquivos no seu dispositivo", "Read_External_Permission": "Permissão de acesso à arquivos", - "Read_Only_Channel": "Canal Somente Leitura", "Read_Only": "Somente Leitura", "Read_Receipt": "Lida por", "Receive_Group_Mentions": "Receber menções de grupo", @@ -683,7 +679,6 @@ "Teams": "Times", "No_team_channels_found": "Nenhum canal encontrado", "Team_not_found": "Time não encontrado", - "Private_Team": "Equipe Privada", "Add_Channel_to_Team": "Adicionar Canal ao Time", "Left_The_Team_Successfully": "Saiu do time com sucesso", "Create_New": "Criar", @@ -797,6 +792,25 @@ "totp-invalid": "Código ou senha inválida", "Close_Chat": "Fechar Conversa", "Select_tags": "Selecionar tag(s)", + "Skip": "Pular", + "N_Selected_members": "{{n}} selecionados", + "Broadcast": "Transmissão", + "Broadcast_hint": "Somente usuários autorizados podem escrever novas mensagens, mas os outros usuários poderão responder", + "Team_hint_private": "Apenas pessoas convidadas podem entrar", + "Team_hint_public": "Quando desativado, qualquer um pode entrar na equipe", + "Team_hint_not_read_only": "Todos os usuários nesta equipe podem escrever mensagens", + "Team_hint_encrypted": "Equipe criptografada de ponta a ponta. A pesquisa não funcionará com equipes criptografadas e as notificações poderão não exibir o conteúdo das mensagens.", + "Team_hint_encrypted_not_available": "Disponível apenas para equipes privadas", + "Channel_hint_private":"Apenas usuários convidados podem acessar este canal", + "Channel_hint_public":"Todos podem acessar este canal", + "Channel_hint_encrypted": "Canal criptografado de ponta a ponta. A pesquisa não funcionará com canais criptografados e as notificações podem não mostrar o conteúdo das mensagens.", + "Channel_hint_not_read_only": "Todos usuários no canal podem enviar mensagens novas", + "Channel_hint_encrypted_not_available": "Indisponível para canais públicos", + "Read_only_hint":"Somente usuários autorizados podem escrever novas mensagens", + "Discussion": "Discussão", + "Channel": "Canal", + "Team": "Time", + "Select_Members": "Selecionar Membros", "Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal", "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Permitir que os usuários selecionem o comportamento Também enviar para o canal" } \ No newline at end of file diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index 9509842ec..fd6f3d5be 100644 --- a/app/i18n/locales/pt-PT.json +++ b/app/i18n/locales/pt-PT.json @@ -118,8 +118,7 @@ "Black": "Preto", "Block_user": "Bloquear utilizador", "Browser": "Navegador", - "Broadcast_channel_Description": "Apenas utilizadores autorizados podem escrever novas mensagens, mas os outros utilizadores poderão responder", - "Broadcast_Channel": "Canal de Transmissão", + "Broadcast_hint": "Apenas utilizadores autorizados podem escrever novas mensagens, mas os outros utilizadores poderão responder", "Busy": "Ocupado", "By_proceeding_you_are_agreeing": "Ao prosseguir você concorda com o(s) nosso(s)", "Cancel_editing": "Cancelar edição", @@ -390,7 +389,6 @@ "Preferences": "Preferências", "Preferences_saved": "Preferências guardadas!", "Privacy_Policy": " Política de Privacidade", - "Private_Channel": "Canal Privado", "Private": "Privado", "Processing": "A processar...", "Profile_saved_successfully": "Perfil actualizado com sucesso!", @@ -405,7 +403,6 @@ "Reactions": "Reacções", "Read_External_Permission_Message": "Rocket.Chat precisa acessar fotos, média e arquivos em seu dispositivo", "Read_External_Permission": "Permissão de leitura da média", - "Read_Only_Channel": "Canal só de leitura", "Read_Only": "Só de Leitura", "Read_Receipt": "Recibos de leitura", "Register": "Registar", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index adb8923f6..01bbc8217 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -119,8 +119,7 @@ "Black": "Черный", "Block_user": "Блокировать пользователя", "Browser": "Браузер", - "Broadcast_channel_Description": "Только авторизованные пользователи могут писать новые сообщения, но другие пользователи смогут ответить", - "Broadcast_Channel": "Широковещательный канал", + "Broadcast_hint": "Только авторизованные пользователи могут писать новые сообщения, но другие пользователи смогут ответить", "Busy": "Занят", "By_proceeding_you_are_agreeing": "Продолжая, вы соглашаетесь с нашими", "Cancel_editing": "Отменить правку", @@ -394,7 +393,6 @@ "Preferences": "Настройки", "Preferences_saved": "Настройки сохранены!", "Privacy_Policy": " Политика конфиденциальности", - "Private_Channel": "Приватный канал", "Private": "Приватный", "Processing": "Обработка...", "Profile_saved_successfully": "Профиль успешно сохранен!", @@ -409,7 +407,6 @@ "Reactions": "Реакции", "Read_External_Permission_Message": "Rocket.Chat необходим доступ к фотографиям, медиа и другим файлам на вашем устройстве", "Read_External_Permission": "Разрешение на Чтение Медиа", - "Read_Only_Channel": "Канал только для чтения", "Read_Only": "Только для чтения", "Read_Receipt": "Уведомление о прочтении", "Receive_Group_Mentions": "Получать групповые уведомления", @@ -706,9 +703,6 @@ "Team_not_found": "Команда не найдена", "Create_Team": "Создать Команду", "Team_Name": "Имя Команды", - "Private_Team": "Приватная Команда", - "Read_Only_Team": "Команда только для чтения", - "Broadcast_Team": "Широковещательная Команда", "creating_team": "создание Команды", "team-name-already-exists": "Команда с таким названием уже существует", "Add_Channel_to_Team": "Добавить канал в Команду", diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index 092dbfda6..e8f26d3bf 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -115,8 +115,7 @@ "Black": "Koyu", "Block_user": "Kullanıcıyı engelle", "Browser": "Tarayıcı", - "Broadcast_channel_Description": "Yalnızca yetkili kullanıcılar yeni ileti yazabilir, ancak diğer kullanıcılar yanıt verebilir", - "Broadcast_Channel": "Kanala Yayınla", + "Broadcast_hint": "Yalnızca yetkili kullanıcılar yeni ileti yazabilir, ancak diğer kullanıcılar yanıt verebilir", "Busy": "Meşgul", "By_proceeding_you_are_agreeing": "Devam ederek kabul ediyorsunuz: ", "Cancel_editing": "Düzenlemeyi iptal et", @@ -384,7 +383,6 @@ "Preferences": "Tercihler", "Preferences_saved": "Tercihler kaydedildi!", "Privacy_Policy": " Privacy Policy", - "Private_Channel": "Özel Kanal", "Private": "Özel", "Processing": "İşleniyor...", "Profile_saved_successfully": "Profil başarıyla kaydedildi!", @@ -399,7 +397,6 @@ "Reactions": "Tepkiler", "Read_External_Permission_Message": "Rocket.Chat'in cihazınızdaki fotoğraflara, medyaya ve dosyalara erişmesi gerekiyor", "Read_External_Permission": "Medya Okuma İzni ", - "Read_Only_Channel": "Yazma Kısıtlı Kanal", "Read_Only": "Yazma Kısıtlı", "Read_Receipt": "Okundu Bilgisi", "Receive_Group_Mentions": "Grup Bahsetmelerini Al", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index 119dfdf6c..6e6065347 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -115,8 +115,7 @@ "Black": "黑色", "Block_user": "屏蔽此用户", "Browser": "浏览器", - "Broadcast_channel_Description": "只有经过授权的用户才能写新信息,但其他用户可以回复", - "Broadcast_Channel": "广播频道", + "Broadcast_hint": "只有经过授权的用户才能写新信息,但其他用户可以回复", "Busy": "忙碌", "By_proceeding_you_are_agreeing": "继续操作,请同意我们的", "Cancel_editing": "取消编辑", @@ -381,7 +380,6 @@ "Preferences": "偏好设置", "Preferences_saved": "偏好已保存!", "Privacy_Policy": "隐私政策", - "Private_Channel": "私人频道", "Private": "私有的", "Processing": "处理中", "Profile_saved_successfully": "个人资料保存成功!", @@ -396,7 +394,6 @@ "Reactions": "表情貼", "Read_External_Permission_Message": "Rocket.Chat 需要存取您装置上的相片、多媒体及文件", "Read_External_Permission": "读取媒体权限", - "Read_Only_Channel": "只读频道", "Read_Only": "只读", "Read_Receipt": "查看已读人员", "Receive_Group_Mentions": "接收群组提及", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index 4e8827ed6..bc9ea3b5c 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -116,8 +116,7 @@ "Black": "黑色", "Block_user": "封鎖此用戶", "Browser": "瀏覽器", - "Broadcast_channel_Description": "只有經過授權的使用者才能發送新訊息,但其他使用者可以回覆", - "Broadcast_Channel": "廣播頻道", + "Broadcast_hint": "只有經過授權的使用者才能發送新訊息,但其他使用者可以回覆", "Busy": "忙碌", "By_proceeding_you_are_agreeing": "若要繼續操作,請同意我們的", "Cancel_editing": "取消編輯", @@ -383,7 +382,6 @@ "Preferences": "偏好設定", "Preferences_saved": "偏好設定已被儲存!", "Privacy_Policy": "隱私政策", - "Private_Channel": "私人頻道", "Private": "私有的", "Processing": "處理中", "Profile_saved_successfully": "個人資料儲存成功!", @@ -398,7 +396,6 @@ "Reactions": "表情貼", "Read_External_Permission_Message": "Rocket.Chat 需要存取您裝置上的相片、多媒體及檔案", "Read_External_Permission": "讀取媒體權限", - "Read_Only_Channel": "唯讀頻道", "Read_Only": "唯讀", "Read_Receipt": "查看已讀人員", "Receive_Group_Mentions": "接收群組提及", diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index d5938cb58..4c7007bd8 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -123,7 +123,7 @@ const ChatsStackNavigator = () => { options={ThreadMessagesView.navigationOptions} /> - + { - + - + ); diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index 80b2637db..37fb43bd9 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -187,9 +187,9 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { - + - + diff --git a/app/views/CreateChannelView.tsx b/app/views/CreateChannelView.tsx deleted file mode 100644 index 9c09c2d2b..000000000 --- a/app/views/CreateChannelView.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { FlatList, ScrollView, StyleSheet, Switch, Text, View, SwitchProps } from 'react-native'; -import { dequal } from 'dequal'; - -import * as List from '../containers/List'; -import { TextInput } from '../containers/TextInput'; -import { sendLoadingEvent } from '../containers/Loading'; -import { createChannelRequest } from '../actions/createChannel'; -import { removeUser } from '../actions/selectedUsers'; -import KeyboardView from '../containers/KeyboardView'; -import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps'; -import I18n from '../i18n'; -import UserItem from '../containers/UserItem'; -import * as HeaderButton from '../containers/HeaderButton'; -import StatusBar from '../containers/StatusBar'; -import { SWITCH_TRACK_COLOR, themes } from '../lib/constants'; -import { withTheme } from '../theme'; -import { Review } from '../lib/methods/helpers/review'; -import { getUserSelector } from '../selectors/login'; -import { events, logEvent } from '../lib/methods/helpers/log'; -import SafeAreaView from '../containers/SafeAreaView'; -import sharedStyles from './Styles'; -import { ChatsStackParamList } from '../stacks/types'; -import { IApplicationState, IBaseScreen, IUser } from '../definitions'; -import { hasPermission } from '../lib/methods/helpers'; - -const styles = StyleSheet.create({ - container: { - flex: 1 - }, - list: { - width: '100%' - }, - input: { - height: 54, - paddingHorizontal: 18, - fontSize: 17, - ...sharedStyles.textRegular - }, - switchContainer: { - height: 54, - alignItems: 'center', - justifyContent: 'space-between', - flexDirection: 'row', - paddingHorizontal: 18 - }, - label: { - fontSize: 17, - ...sharedStyles.textMedium - }, - invitedHeader: { - marginTop: 18, - marginHorizontal: 15, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center' - }, - invitedTitle: { - fontSize: 18, - ...sharedStyles.textSemibold, - lineHeight: 41 - }, - invitedCount: { - fontSize: 14, - ...sharedStyles.textRegular - } -}); - -interface IOtherUser { - _id: string; - name: string; - fname: string; -} - -interface ICreateChannelViewState { - channelName: string; - type: boolean; - readOnly: boolean; - encrypted: boolean; - broadcast: boolean; - isTeam: boolean; - permissions: boolean[]; -} - -interface ICreateChannelViewProps extends IBaseScreen { - baseUrl: string; - error: object; - failure: boolean; - isFetching: boolean; - encryptionEnabled: boolean; - users: IOtherUser[]; - user: IUser; - teamId: string; - createPublicChannelPermission: string[] | undefined; - createPrivateChannelPermission: string[] | undefined; -} - -interface ISwitch extends SwitchProps { - id: string; - label: string; -} - -class CreateChannelView extends React.Component { - private teamId?: string; - - constructor(props: ICreateChannelViewProps) { - super(props); - const { route } = this.props; - const isTeam = route?.params?.isTeam || false; - this.teamId = route?.params?.teamId; - this.state = { - channelName: '', - type: true, - readOnly: false, - encrypted: false, - broadcast: false, - isTeam, - permissions: [] - }; - this.setHeader(); - } - - componentDidMount() { - this.handleHasPermission(); - } - - shouldComponentUpdate(nextProps: ICreateChannelViewProps, nextState: ICreateChannelViewState) { - const { channelName, type, readOnly, broadcast, encrypted, permissions } = this.state; - const { users, isFetching, encryptionEnabled, theme, createPublicChannelPermission, createPrivateChannelPermission } = - this.props; - if (nextProps.theme !== theme) { - return true; - } - if (nextState.channelName !== channelName) { - return true; - } - if (nextState.type !== type) { - return true; - } - if (nextState.readOnly !== readOnly) { - return true; - } - if (nextState.encrypted !== encrypted) { - return true; - } - if (nextState.broadcast !== broadcast) { - return true; - } - if (nextState.permissions !== permissions) { - return true; - } - if (nextProps.isFetching !== isFetching) { - return true; - } - if (nextProps.encryptionEnabled !== encryptionEnabled) { - return true; - } - if (!dequal(nextProps.createPublicChannelPermission, createPublicChannelPermission)) { - return true; - } - if (!dequal(nextProps.createPrivateChannelPermission, createPrivateChannelPermission)) { - return true; - } - if (!dequal(nextProps.users, users)) { - return true; - } - return false; - } - - componentDidUpdate(prevProps: ICreateChannelViewProps) { - const { createPublicChannelPermission, createPrivateChannelPermission, isFetching } = this.props; - if ( - !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || - !dequal(createPrivateChannelPermission, prevProps.createPrivateChannelPermission) - ) { - this.handleHasPermission(); - } - if (isFetching !== prevProps.isFetching) { - sendLoadingEvent({ visible: isFetching }); - } - } - - setHeader = () => { - const { navigation } = this.props; - const { isTeam } = this.state; - - navigation.setOptions({ - title: isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel') - }); - }; - - toggleRightButton = (channelName: string) => { - const { navigation } = this.props; - navigation.setOptions({ - headerRight: () => - channelName.trim().length > 0 && ( - - - - ) - }); - }; - - onChangeText = (channelName: string) => { - this.toggleRightButton(channelName); - this.setState({ channelName }); - }; - - submit = () => { - const { channelName, type, readOnly, broadcast, encrypted, isTeam } = this.state; - const { users: usersProps, isFetching, dispatch } = this.props; - - if (!channelName.trim() || isFetching) { - return; - } - - // transform users object into array of usernames - const users = usersProps.map(user => user.name); - - // create channel or team - const data = { - name: channelName, - users, - type, - readOnly, - broadcast, - encrypted, - isTeam, - teamId: this.teamId - }; - dispatch(createChannelRequest(data)); - Review.pushPositiveEvent(); - }; - - removeUser = (user: IOtherUser) => { - logEvent(events.CR_REMOVE_USER); - const { dispatch } = this.props; - dispatch(removeUser(user)); - }; - - renderSwitch = ({ id, value, label, onValueChange, disabled = false }: ISwitch) => { - const { theme } = this.props; - return ( - - {I18n.t(label)} - - - ); - }; - - handleHasPermission = async () => { - const { createPublicChannelPermission, createPrivateChannelPermission } = this.props; - const permissions = [createPublicChannelPermission, createPrivateChannelPermission]; - const permissionsToCreate = await hasPermission(permissions); - this.setState({ permissions: permissionsToCreate }); - }; - - renderType() { - const { type, isTeam, permissions } = this.state; - const isDisabled = permissions.filter(r => r === true).length <= 1; - - return this.renderSwitch({ - id: 'type', - value: permissions[1] ? type : false, - disabled: isDisabled, - label: isTeam ? 'Private_Team' : 'Private_Channel', - onValueChange: (value: boolean) => { - logEvent(events.CR_TOGGLE_TYPE); - // If we set the channel as public, encrypted status should be false - this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted })); - } - }); - } - - renderReadOnly() { - const { readOnly, broadcast, isTeam } = this.state; - - return this.renderSwitch({ - id: 'readonly', - value: readOnly, - label: isTeam ? 'Read_Only_Team' : 'Read_Only_Channel', - onValueChange: value => { - logEvent(events.CR_TOGGLE_READ_ONLY); - this.setState({ readOnly: value }); - }, - disabled: broadcast - }); - } - - renderEncrypted() { - const { type, encrypted } = this.state; - const { encryptionEnabled } = this.props; - - if (!encryptionEnabled) { - return null; - } - - return this.renderSwitch({ - id: 'encrypted', - value: encrypted, - label: 'Encrypted', - onValueChange: value => { - logEvent(events.CR_TOGGLE_ENCRYPTED); - this.setState({ encrypted: value }); - }, - disabled: !type - }); - } - - renderBroadcast() { - const { broadcast, readOnly, isTeam } = this.state; - - return this.renderSwitch({ - id: 'broadcast', - value: broadcast, - label: isTeam ? 'Broadcast_Team' : 'Broadcast_Channel', - onValueChange: value => { - logEvent(events.CR_TOGGLE_BROADCAST); - this.setState({ - broadcast: value, - readOnly: value ? true : readOnly - }); - } - }); - } - - renderItem = ({ item }: { item: IOtherUser }) => ( - this.removeUser(item)} - testID={`create-channel-view-item-${item.name}`} - icon='check' - /> - ); - - renderInvitedList = () => { - const { users, theme } = this.props; - - return ( - item._id} - style={[ - styles.list, - sharedStyles.separatorVertical, - { - backgroundColor: themes[theme].focusedBackground, - borderColor: themes[theme].separatorColor - } - ]} - renderItem={this.renderItem} - ItemSeparatorComponent={List.Separator} - keyboardShouldPersistTaps='always' - /> - ); - }; - - render() { - const { channelName, isTeam } = this.state; - const { users, theme } = this.props; - const userCount = users.length; - - return ( - - - - - - - - {this.renderType()} - - {this.renderReadOnly()} - - {this.renderEncrypted()} - - {this.renderBroadcast()} - - - {I18n.t('Invite')} - - {userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })} - - - {this.renderInvitedList()} - - - - ); - } -} - -const mapStateToProps = (state: IApplicationState) => ({ - baseUrl: state.server.server, - isFetching: state.createChannel.isFetching, - encryptionEnabled: state.encryption.enabled, - users: state.selectedUsers.users, - user: getUserSelector(state), - createPublicChannelPermission: state.permissions['create-c'], - createPrivateChannelPermission: state.permissions['create-p'] -}); - -export default connect(mapStateToProps)(withTheme(CreateChannelView)); diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx new file mode 100644 index 000000000..09aa2bc2c --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; + +import { SwitchItem } from './SwitchItem'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'flex-start', + padding: 16 + } +}); + +export default { + title: 'SwitchItem' +}; + +const testSwitch = { + id: 'switch-id', + hint: 'Read_only_hint', + label: 'Onboarding_title', + onValueChange: () => {}, + value: false, + testSwitchID: 'create-channel-switch-id', + testLabelID: 'create-channel-switch-id-hint' +}; + +export const Switch = () => ( + <> + + testSwitch.onValueChange()} + value={testSwitch.value} + /> + + +); diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItem.test.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItem.test.tsx new file mode 100644 index 000000000..fa1fc6846 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItem.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import i18n from '../../../i18n'; +import { SwitchItem, ISwitch } from './SwitchItem'; +import { mockedStore as store } from '../../../reducers/mockedStore'; + +const onPressMock = jest.fn((value: boolean) => value); + +const testSwitch = { + id: 'switch-id', + hint: 'Read_only_hint', + label: 'Onboarding_title', + onValueChange: onPressMock, + value: false, + testSwitchID: 'create-channel-switch-id', + testLabelID: 'create-channel-switch-id-hint' +}; + +const Render = ({ hint, id, label, onValueChange, value }: ISwitch) => ( + + + +); + +describe('SwitchItemEncrypted', () => { + it('should not render the Encrypted Switch component', async () => { + const { findByTestId } = render( + testSwitch.onValueChange(value)} + value={testSwitch.value} + /> + ); + const component = await findByTestId(testSwitch.testSwitchID); + expect(component).toBeTruthy(); + }); + it('should change value of switch', async () => { + const { findByTestId } = render( + testSwitch.onValueChange(value)} + value={testSwitch.value} + /> + ); + const component = await findByTestId(testSwitch.testSwitchID); + fireEvent(component, 'valueChange', { value: true }); + expect(onPressMock).toHaveReturnedWith({ value: !testSwitch.value }); + }); + it('check if hint exists and is the same from testSwitch object', async () => { + const { findByTestId } = render( + testSwitch.onValueChange(value)} + value={testSwitch.value} + /> + ); + const component = await findByTestId(testSwitch.testLabelID); + expect(component.props.children).toBe(i18n.t(testSwitch.hint)); + }); +}); diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItem.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItem.tsx new file mode 100644 index 000000000..4e8b548e3 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItem.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { StyleSheet, Switch, Text, View, SwitchProps } from 'react-native'; + +import I18n from '../../../i18n'; +import { SWITCH_TRACK_COLOR } from '../../../lib/constants'; +import { useTheme } from '../../../theme'; +import sharedStyles from '../../Styles'; + +const styles = StyleSheet.create({ + switchContainer: { + minHeight: 54, + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + maxHeight: 80, + marginBottom: 12 + }, + switchTextContainer: { + flex: 1, + marginRight: 8 + }, + label: { + fontSize: 14, + ...sharedStyles.textMedium + }, + hint: { + fontSize: 14, + ...sharedStyles.textRegular + } +}); + +export interface ISwitch extends SwitchProps { + id: string; + label: string; + hint: string; + onValueChange: (value: boolean) => void; +} + +export const SwitchItem = ({ id, value, label, hint, onValueChange, disabled = false }: ISwitch) => { + const { colors } = useTheme(); + + return ( + + + {I18n.t(label)} + + {I18n.t(hint)} + + + + + ); +}; diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx new file mode 100644 index 000000000..f6705b505 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import { SwitchItemEncrypted, ISwitchItemEncrypted } from './SwitchItemEncrypted'; +import { mockedStore as store } from '../../../reducers/mockedStore'; +import i18n from '../../../i18n'; + +const onPressMock = jest.fn((value: boolean) => value); + +const testEncrypted = { + encrypted: false, + encryptionEnabled: false, + isTeam: false, + onValueChangeEncrypted: onPressMock, + type: false, + testSwitchID: 'create-channel-encrypted', + testLabelID: `create-channel-encrypted-hint` +}; + +const Render = ({ encrypted, encryptionEnabled, isTeam, onValueChangeEncrypted, type }: ISwitchItemEncrypted) => ( + + + +); + +describe('SwitchItemEncrypted', () => { + it('should not render the Encrypted Switch component', async () => { + const { findByTestId } = render( + testEncrypted.onValueChangeEncrypted(value)} + type={testEncrypted.type} + /> + ); + try { + await findByTestId(testEncrypted.testSwitchID); + } catch (e) { + expect(e).toBeTruthy(); + } + }); + it('should render the Encrypted Switch component', async () => { + testEncrypted.encryptionEnabled = true; + const { findByTestId } = render( + testEncrypted.onValueChangeEncrypted(value)} + type={testEncrypted.type} + /> + ); + const component = await findByTestId(testEncrypted.testSwitchID); + expect(component).toBeTruthy(); + }); + it('should change value of switch', async () => { + const { findByTestId } = render( + testEncrypted.onValueChangeEncrypted(value)} + type={testEncrypted.type} + /> + ); + + const component = await findByTestId(testEncrypted.testSwitchID); + fireEvent(component, 'valueChange', { value: true }); + expect(onPressMock).toHaveReturnedWith({ value: !testEncrypted.encrypted }); + }); + it('label when encrypted and isTeam are false and is a public channel', async () => { + const { findByTestId } = render( + testEncrypted.onValueChangeEncrypted(value)} + type={testEncrypted.type} + /> + ); + const component = await findByTestId(testEncrypted.testLabelID); + expect(component.props.children).toBe(i18n.t('Channel_hint_encrypted_not_available')); + }); + it('label when encrypted and isTeam are true and is a private team', async () => { + testEncrypted.isTeam = true; + testEncrypted.type = true; + testEncrypted.encrypted = true; + const { findByTestId } = render( + testEncrypted.onValueChangeEncrypted(value)} + type={testEncrypted.type} + /> + ); + const component = await findByTestId(testEncrypted.testLabelID); + expect(component.props.children).toBe(i18n.t('Team_hint_encrypted')); + }); +}); diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.tsx new file mode 100644 index 000000000..9cc277c65 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { SwitchItem } from './SwitchItem'; + +export interface ISwitchItemEncrypted { + encryptionEnabled: boolean; + isTeam: boolean; + type: boolean; + encrypted: boolean; + onValueChangeEncrypted: (value: boolean) => void; +} + +export const SwitchItemEncrypted = ({ + encryptionEnabled, + isTeam, + type, + encrypted, + onValueChangeEncrypted +}: ISwitchItemEncrypted) => { + if (!encryptionEnabled) { + return null; + } + + let hint = ''; + if (isTeam && type) { + hint = 'Team_hint_encrypted'; + } + if (isTeam && !type) { + hint = 'Team_hint_encrypted_not_available'; + } + if (!isTeam && type) { + hint = 'Channel_hint_encrypted'; + } + if (!isTeam && !type) { + hint = 'Channel_hint_encrypted_not_available'; + } + + return ( + + ); +}; diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItemReadOnly.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItemReadOnly.tsx new file mode 100644 index 000000000..7711879cc --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItemReadOnly.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { SwitchItem } from './SwitchItem'; + +export const SwitchItemReadOnly = ({ + readOnly, + isTeam, + onValueChangeReadOnly, + broadcast +}: { + readOnly: boolean; + isTeam: boolean; + onValueChangeReadOnly: (value: boolean) => void; + broadcast: boolean; +}) => { + let hint = ''; + if (readOnly) { + hint = 'Read_only_hint'; + } + if (isTeam && !readOnly) { + hint = 'Team_hint_not_read_only'; + } + if (!isTeam && !readOnly) { + hint = 'Channel_hint_not_read_only'; + } + + return ( + + ); +}; diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItemType.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItemType.tsx new file mode 100644 index 000000000..6299dec97 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/SwitchItemType.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { usePermissions } from '../../../lib/hooks'; +import { SwitchItem } from './SwitchItem'; + +export const SwitchItemType = ({ + isTeam, + type, + onValueChangeType +}: { + isTeam: boolean; + type: boolean; + onValueChangeType: (value: boolean) => void; +}) => { + const [createChannelPermission, createPrivateChannelPermission] = usePermissions(['create-c', 'create-p']); + + const isDisabled = [createChannelPermission, createPrivateChannelPermission].filter(r => r === true).length <= 1; + + let hint = ''; + if (isTeam && type) { + hint = 'Team_hint_private'; + } + if (isTeam && !type) { + hint = 'Team_hint_public'; + } + if (!isTeam && type) { + hint = 'Channel_hint_private'; + } + if (!isTeam && !type) { + hint = 'Channel_hint_public'; + } + + return ( + + ); +}; diff --git a/app/views/CreateChannelView/RoomSettings/index.tsx b/app/views/CreateChannelView/RoomSettings/index.tsx new file mode 100644 index 000000000..5d27ca1e0 --- /dev/null +++ b/app/views/CreateChannelView/RoomSettings/index.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useState } from 'react'; +import { UseFormSetValue } from 'react-hook-form'; + +import { useAppSelector } from '../../../lib/hooks'; +import { events, logEvent } from '../../../lib/methods/helpers/log'; +import { SwitchItem } from './SwitchItem'; +import { SwitchItemType } from './SwitchItemType'; +import { SwitchItemReadOnly } from './SwitchItemReadOnly'; +import { SwitchItemEncrypted } from './SwitchItemEncrypted'; +import { IFormData } from '..'; + +export const RoomSettings = ({ isTeam, setValue }: { isTeam: boolean; setValue: UseFormSetValue }) => { + const [type, setType] = useState(true); + const [readOnly, setReadOnly] = useState(false); + const [encrypted, setEncrypted] = useState(false); + const [broadcast, setBroadcast] = useState(false); + + const { encryptionEnabled } = useAppSelector(state => ({ + encryptionEnabled: state.encryption.enabled + })); + + const onValueChangeType = useCallback( + (value: boolean) => { + logEvent(events.CR_TOGGLE_TYPE); + // If we set the channel as public, encrypted status should be false + setType(value); + setValue('type', value); + setEncrypted(value && encrypted); + setValue('encrypted', value && encrypted); + }, + [encrypted] + ); + + const onValueChangeReadOnly = useCallback((value: boolean) => { + logEvent(events.CR_TOGGLE_READ_ONLY); + setReadOnly(value); + setValue('readOnly', value); + }, []); + + const onValueChangeEncrypted = useCallback((value: boolean) => { + logEvent(events.CR_TOGGLE_ENCRYPTED); + setEncrypted(value); + setValue('encrypted', value); + }, []); + + const onValueChangeBroadcast = (value: boolean) => { + logEvent(events.CR_TOGGLE_BROADCAST); + setBroadcast(value); + setValue('broadcast', value); + setReadOnly(value ? true : readOnly); + setValue('readOnly', value ? true : readOnly); + }; + return ( + <> + + + + + + ); +}; diff --git a/app/views/CreateChannelView/index.tsx b/app/views/CreateChannelView/index.tsx new file mode 100644 index 000000000..be02e0d9f --- /dev/null +++ b/app/views/CreateChannelView/index.tsx @@ -0,0 +1,205 @@ +import React, { useCallback, useEffect, useLayoutEffect } from 'react'; +import { shallowEqual, useDispatch } from 'react-redux'; +import { FlatList, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { useForm } from 'react-hook-form'; + +import { useAppSelector } from '../../lib/hooks'; +import { sendLoadingEvent } from '../../containers/Loading'; +import { createChannelRequest } from '../../actions/createChannel'; +import { removeUser as removeUserAction } from '../../actions/selectedUsers'; +import KeyboardView from '../../containers/KeyboardView'; +import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; +import I18n from '../../i18n'; +import StatusBar from '../../containers/StatusBar'; +import { useTheme } from '../../theme'; +import { Review } from '../../lib/methods/helpers/review'; +import SafeAreaView from '../../containers/SafeAreaView'; +import sharedStyles from '../Styles'; +import { ChatsStackParamList } from '../../stacks/types'; +import Button from '../../containers/Button'; +import { ControlledFormTextInput } from '../../containers/TextInput'; +import Chip from '../../containers/Chip'; +import { RoomSettings } from './RoomSettings'; +import { ISelectedUser } from '../../reducers/selectedUsers'; + +const styles = StyleSheet.create({ + container: { + flex: 1 + }, + containerTextInput: { + paddingHorizontal: 16, + marginTop: 16 + }, + containerStyle: { + marginBottom: 28 + }, + list: { + width: '100%' + }, + invitedHeader: { + marginVertical: 12, + marginHorizontal: 16, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + invitedCount: { + fontSize: 12, + ...sharedStyles.textRegular + }, + invitedList: { + paddingHorizontal: 16 + }, + buttonCreate: { + marginHorizontal: 16, + marginTop: 24 + } +}); + +export interface IFormData { + channelName: string; + type: boolean; + readOnly: boolean; + encrypted: boolean; + broadcast: boolean; +} + +const CreateChannelView = () => { + const { + control, + handleSubmit, + formState: { isDirty }, + setValue + } = useForm({ + defaultValues: { channelName: '', broadcast: false, encrypted: false, readOnly: false, type: false } + }); + + const navigation = useNavigation>(); + const { params } = useRoute>(); + const isTeam = params?.isTeam || false; + const teamId = params?.teamId; + const { colors } = useTheme(); + const dispatch = useDispatch(); + + const { isFetching, useRealName, users } = useAppSelector( + state => ({ + isFetching: state.createChannel.isFetching, + users: state.selectedUsers.users, + useRealName: state.settings.UI_Use_Real_Name as boolean + }), + shallowEqual + ); + + useEffect(() => { + sendLoadingEvent({ visible: isFetching }); + }, [isFetching]); + + useLayoutEffect(() => { + navigation.setOptions({ + title: isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel') + }); + }, [isTeam, navigation]); + + const removeUser = useCallback( + (user: ISelectedUser) => { + dispatch(removeUserAction(user)); + }, + [dispatch] + ); + + const submit = ({ channelName, broadcast, encrypted, readOnly, type }: IFormData) => { + if (!channelName.trim() || isFetching) { + return; + } + // transform users object into array of usernames + const usersMapped = users.map(user => user.name); + // create channel or team + const data = { + name: channelName, + users: usersMapped, + type, + readOnly, + broadcast, + encrypted, + isTeam, + teamId + }; + dispatch(createChannelRequest(data)); + Review.pushPositiveEvent(); + }; + + return ( + + + + + + + + + {users.length > 0 ? ( + <> + + + {I18n.t('N_Selected_members', { n: users.length })} + + + item._id} + style={[ + styles.list, + { + backgroundColor: colors.backgroundColor, + borderColor: colors.separatorColor + } + ]} + contentContainerStyle={styles.invitedList} + renderItem={({ item }) => { + const name = useRealName && item.fname ? item.fname : item.name; + const username = item.name; + + return ( + removeUser(item)} + testID={`create-channel-view-item-${item.name}`} + /> + ); + }} + keyboardShouldPersistTaps='always' + horizontal + /> + + ) : null} +