Rocket.Chat.ReactNative/app/views/TeamChannelsView.tsx

582 lines
16 KiB
TypeScript

import React from 'react';
import { Q } from '@nozbe/watermelondb';
import { HeaderBackButton, StackNavigationOptions } from '@react-navigation/stack';
import { Alert, FlatList, Keyboard } from 'react-native';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { deleteRoom } from '../actions/room';
import { DisplayMode, themes } from '../lib/constants';
import { TActionSheetOptions, TActionSheetOptionsItem, withActionSheet } from '../containers/ActionSheet';
import ActivityIndicator from '../containers/ActivityIndicator';
import BackgroundContainer from '../containers/BackgroundContainer';
import { getHeaderTitlePosition } from '../containers/Header';
import * as HeaderButton from '../containers/HeaderButton';
import RoomHeader from '../containers/RoomHeader';
import SafeAreaView from '../containers/SafeAreaView';
import SearchHeader from '../containers/SearchHeader';
import StatusBar from '../containers/StatusBar';
import { IApplicationState, IBaseScreen, TSubscriptionModel } from '../definitions';
import { ERoomType } from '../definitions/ERoomType';
import { withDimensions } from '../dimensions';
import I18n from '../i18n';
import database from '../lib/database';
import { CustomIcon } from '../containers/CustomIcon';
import RoomItem, { ROW_HEIGHT } from '../containers/RoomItem';
import { getUserSelector } from '../selectors/login';
import { ChatsStackParamList } from '../stacks/types';
import { withTheme } from '../theme';
import debounce from '../utils/debounce';
import { isIOS } from '../utils/deviceInfo';
import { goRoom } from '../utils/goRoom';
import { showErrorAlert } from '../utils/info';
import log, { events, logEvent } from '../utils/log';
import { getRoomAvatar, getRoomTitle, hasPermission } from '../lib/methods';
import { Services } from '../lib/services';
const API_FETCH_COUNT = 25;
const getItemLayout = (data: IItem[] | null | undefined, index: number) => ({
length: data?.length || 0,
offset: ROW_HEIGHT * index,
index
});
const keyExtractor = (item: IItem) => item._id;
export interface IItem {
_id: ERoomType;
fname: string;
customFields: object;
broadcast: boolean;
encrypted: boolean;
name: string;
t: string;
msgs: number;
usersCount: number;
u: { _id: string; name: string };
ts: string;
ro: boolean;
teamId: string;
default: boolean;
sysMes: boolean;
_updatedAt: string;
teamDefault: boolean;
}
interface ITeamChannelsViewState {
loading: boolean;
loadingMore: boolean;
data: IItem[];
isSearching: boolean;
searchText: string | null;
search: IItem[];
end: boolean;
showCreate: boolean;
}
interface ITeamChannelsViewProps extends IBaseScreen<ChatsStackParamList, 'TeamChannelsView'> {
insets: EdgeInsets;
useRealName: boolean;
width: number;
StoreLastMessage: boolean;
addTeamChannelPermission: string[];
editTeamChannelPermission: string[];
removeTeamChannelPermission: string[];
deleteCPermission: string[];
deletePPermission: string[];
showActionSheet: (options: TActionSheetOptions) => void;
showAvatar: boolean;
displayMode: DisplayMode;
}
class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChannelsViewState> {
private teamId: string;
private joined: boolean;
private teamChannels: TSubscriptionModel[];
private team: TSubscriptionModel;
constructor(props: ITeamChannelsViewProps) {
super(props);
this.teamChannels = [];
this.team = {} as TSubscriptionModel;
this.joined = props.route.params?.joined;
this.teamId = props.route.params?.teamId;
this.state = {
loading: true,
loadingMore: false,
data: [],
isSearching: false,
searchText: '',
search: [],
end: false,
showCreate: false
};
this.loadTeam();
this.setHeader();
}
componentDidMount() {
this.load();
}
loadTeam = async () => {
const { addTeamChannelPermission } = this.props;
const { loading, data } = this.state;
const db = database.active;
try {
const subCollection = db.get('subscriptions');
this.teamChannels = await subCollection.query(Q.where('team_id', Q.eq(this.teamId))).fetch();
this.team = this.teamChannels?.find((channel: TSubscriptionModel) => channel.teamMain) as TSubscriptionModel;
this.setHeader();
if (!this.team) {
throw new Error();
}
const permissions = await hasPermission([addTeamChannelPermission], this.team.rid);
if (permissions[0]) {
this.setState({ showCreate: true }, () => this.setHeader());
}
if (loading && data.length) {
this.setState({ loading: false });
}
} catch {
const { navigation } = this.props;
navigation.pop();
showErrorAlert(I18n.t('Team_not_found'));
}
};
load = debounce(async () => {
const { loadingMore, data, search, isSearching, searchText, end } = this.state;
const length = isSearching ? search.length : data.length;
if (loadingMore || end) {
return;
}
this.setState({ loadingMore: true });
try {
const result = await Services.getTeamListRoom({
teamId: this.teamId,
offset: length,
count: API_FETCH_COUNT,
type: 'all',
filter: searchText
});
if (result.success) {
const newState = {
loading: false,
loadingMore: false,
end: result.rooms.length < API_FETCH_COUNT
} as ITeamChannelsViewState;
if (isSearching) {
newState.search = [...search, ...result.rooms] as IItem[];
} else {
newState.data = [...data, ...result.rooms] as IItem[];
}
this.setState(newState);
} else {
this.setState({ loading: false, loadingMore: false });
}
} catch (e) {
log(e);
this.setState({ loading: false, loadingMore: false });
}
}, 300);
setHeader = () => {
const { isSearching, showCreate, data } = this.state;
const { navigation, isMasterDetail, insets, theme } = this.props;
const { team } = this;
if (!team) {
return;
}
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) {
const options: StackNavigationOptions = {
headerTitleAlign: 'left',
headerLeft: () => (
<HeaderButton.Container left>
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
</HeaderButton.Container>
),
headerTitle: () => (
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='team-channels-view-search-header' />
),
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerRight: () => null
};
return navigation.setOptions(options);
}
const options: StackNavigationOptions = {
headerShown: true,
headerTitleAlign: 'left',
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerLeft: () => (
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
),
headerTitle: () => (
<RoomHeader title={getRoomTitle(team)} subtitle={team.topic} type={team.t} onPress={this.goRoomActionsView} teamMain />
)
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
options.headerRight = () => (
<HeaderButton.Container>
{showCreate ? (
<HeaderButton.Item
iconName='create'
testID='team-channels-view-create'
onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })}
/>
) : null}
<HeaderButton.Item iconName='search' testID='team-channels-view-search' onPress={this.onSearchPress} />
</HeaderButton.Container>
);
navigation.setOptions(options);
};
onSearchPress = () => {
logEvent(events.TC_SEARCH);
this.setState({ isSearching: true }, () => this.setHeader());
};
onSearchChangeText = debounce((searchText: string) => {
this.setState(
{
searchText,
search: [],
loading: !!searchText,
loadingMore: false,
end: false
},
() => {
if (searchText) {
this.load();
}
}
);
}, 300);
onCancelSearchPress = () => {
logEvent(events.TC_CANCEL_SEARCH);
const { isSearching } = this.state;
if (!isSearching) {
return;
}
Keyboard.dismiss();
this.setState(
{
searchText: null,
isSearching: false,
search: [],
loadingMore: false,
end: false
},
() => {
this.setHeader();
}
);
};
goRoomActionsView = (screen: string) => {
logEvent(events.TC_GO_ACTIONS);
const { team, joined } = this;
const { navigation, isMasterDetail } = this.props;
if (!team) {
return;
}
if (isMasterDetail && screen) {
navigation.navigate('ModalStackNavigator', {
screen: 'RoomActionsView',
params: {
rid: team.rid,
t: team.t,
room: team,
joined
}
});
} else {
navigation.navigate('RoomActionsView', {
rid: team.rid,
t: team.t,
room: team,
joined
});
}
};
onPressItem = debounce(
async (item: IItem) => {
logEvent(events.TC_GO_ROOM);
const { navigation, isMasterDetail } = this.props;
try {
let params = {};
const result = await Services.getRoomInfo(item._id);
if (result.success) {
params = {
rid: item._id,
name: getRoomTitle(result.room),
joinCodeRequired: result.room.joinCodeRequired,
t: result.room.t,
teamId: result.room.teamId
};
}
if (isMasterDetail) {
navigation.pop();
}
goRoom({ item: params, isMasterDetail, navigationMethod: navigation.push });
} catch (e: any) {
if (e.data.error === 'not-allowed') {
showErrorAlert(I18n.t('error-not-allowed'));
} else {
showErrorAlert(e.data.error);
}
}
},
1000,
true
);
toggleAutoJoin = async (item: IItem) => {
logEvent(events.TC_TOGGLE_AUTOJOIN);
try {
const { data } = this.state;
const result = await Services.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault });
if (result.success) {
const newData = data.map(i => {
if (i._id === item._id) {
i.teamDefault = !i.teamDefault;
}
return i;
});
this.setState({ data: newData });
}
} catch (e) {
logEvent(events.TC_TOGGLE_AUTOJOIN_F);
log(e);
}
};
remove = (item: IItem) => {
Alert.alert(
I18n.t('Confirmation'),
I18n.t('Remove_Team_Room_Warning'),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
style: 'destructive',
onPress: () => this.removeRoom(item)
}
],
{ cancelable: false }
);
};
removeRoom = async (item: IItem) => {
logEvent(events.TC_DELETE_ROOM);
try {
const { data } = this.state;
const result = await Services.removeTeamRoom({ roomId: item._id, teamId: this.team.teamId as string });
if (result.success) {
const newData = data.filter(room => result.room._id !== room._id);
this.setState({ data: newData });
}
} catch (e) {
logEvent(events.TC_DELETE_ROOM_F);
log(e);
}
};
delete = (item: IItem) => {
logEvent(events.TC_DELETE_ROOM);
const { dispatch } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
I18n.t('Delete_Room_Warning'),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
style: 'destructive',
onPress: () => dispatch(deleteRoom(ERoomType.c, item))
}
],
{ cancelable: false }
);
};
showChannelActions = async (item: IItem) => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const {
showActionSheet,
editTeamChannelPermission,
deleteCPermission,
deletePPermission,
theme,
removeTeamChannelPermission
} = this.props;
const isAutoJoinChecked = item.teamDefault;
const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked';
const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor;
const options: TActionSheetOptionsItem[] = [];
const permissionsTeam = await hasPermission([editTeamChannelPermission], this.team.rid);
if (permissionsTeam[0]) {
options.push({
title: I18n.t('Auto-join'),
icon: item.t === 'p' ? 'channel-private' : 'channel-public',
onPress: () => this.toggleAutoJoin(item),
right: () => (
<CustomIcon
testID={isAutoJoinChecked ? 'auto-join-checked' : 'auto-join-unchecked'}
name={autoJoinIcon}
size={20}
color={autoJoinIconColor}
/>
),
testID: 'action-sheet-auto-join'
});
}
const permissionsRemoveTeam = await hasPermission([removeTeamChannelPermission], this.team.rid);
if (permissionsRemoveTeam[0]) {
options.push({
title: I18n.t('Remove_from_Team'),
icon: 'close',
danger: true,
onPress: () => this.remove(item),
testID: 'action-sheet-remove-from-team'
});
}
const permissionsChannel = await hasPermission([item.t === 'c' ? deleteCPermission : deletePPermission], item._id);
if (permissionsChannel[0]) {
options.push({
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => this.delete(item),
testID: 'action-sheet-delete'
});
}
if (options.length === 0) {
return;
}
showActionSheet({ options });
};
renderItem = ({ item }: { item: IItem }) => {
const { StoreLastMessage, useRealName, theme, width, showAvatar, displayMode } = this.props;
return (
<RoomItem
item={item}
theme={theme}
type={item.t}
showLastMessage={StoreLastMessage}
onPress={this.onPressItem}
width={width}
onLongPress={this.showChannelActions}
useRealName={useRealName}
getRoomTitle={getRoomTitle}
getRoomAvatar={getRoomAvatar}
swipeEnabled={false}
autoJoin={item.teamDefault}
showAvatar={showAvatar}
displayMode={displayMode}
/>
);
};
renderFooter = () => {
const { loadingMore } = this.state;
if (loadingMore) {
return <ActivityIndicator />;
}
return null;
};
renderScroll = () => {
const { loading, data, search, isSearching, searchText } = this.state;
if (loading) {
return <BackgroundContainer loading />;
}
if (isSearching && !search.length) {
return <BackgroundContainer text={searchText ? I18n.t('No_team_channels_found') : ''} />;
}
if (!isSearching && !data.length) {
return <BackgroundContainer text={I18n.t('No_team_channels_found')} />;
}
return (
<FlatList
data={isSearching ? search : data}
extraData={isSearching ? search : data}
keyExtractor={keyExtractor}
renderItem={this.renderItem}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
keyboardShouldPersistTaps='always'
onEndReached={() => this.load()}
onEndReachedThreshold={0.5}
ListFooterComponent={this.renderFooter}
/>
);
};
render() {
console.count(`${this.constructor.name}.render calls`);
return (
<SafeAreaView testID='team-channels-view'>
<StatusBar />
{this.renderScroll()}
</SafeAreaView>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server,
user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name,
isMasterDetail: state.app.isMasterDetail,
StoreLastMessage: state.settings.Store_Last_Message,
addTeamChannelPermission: state.permissions['add-team-channel'],
editTeamChannelPermission: state.permissions['edit-team-channel'],
removeTeamChannelPermission: state.permissions['remove-team-channel'],
deleteCPermission: state.permissions['delete-c'],
deletePPermission: state.permissions['delete-p'],
showAvatar: state.sortPreferences.showAvatar,
displayMode: state.sortPreferences.displayMode
});
export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(withActionSheet(TeamChannelsView)))));