diff --git a/native/chat/settings/thread-settings-media-gallery.react.js b/native/chat/settings/thread-settings-media-gallery.react.js
index 44d5d70ab..0cb3f985e 100644
--- a/native/chat/settings/thread-settings-media-gallery.react.js
+++ b/native/chat/settings/thread-settings-media-gallery.react.js
@@ -1,127 +1,194 @@
// @flow
+import { useNavigation, useRoute } from '@react-navigation/native';
import * as React from 'react';
import { View, useWindowDimensions } from 'react-native';
+import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
import { FlatList } from 'react-native-gesture-handler';
import { fetchThreadMedia } from 'lib/actions/thread-actions.js';
-import type { MediaInfo } from 'lib/types/media-types';
+import type { MediaInfo, Media } from 'lib/types/media-types';
import { useServerCall } from 'lib/utils/action-utils.js';
import GestureTouchableOpacity from '../../components/gesture-touchable-opacity.react.js';
import Multimedia from '../../media/multimedia.react.js';
+import {
+ ImageModalRouteName,
+ VideoPlaybackModalRouteName,
+} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
+import type {
+ LayoutCoordinates,
+ VerticalBounds,
+} from '../../types/layout-types.js';
const galleryItemGap = 8;
const numColumns = 3;
type ThreadSettingsMediaGalleryProps = {
+threadID: string,
+limit: number,
+ +verticalBounds: ?VerticalBounds,
};
function ThreadSettingsMediaGallery(
props: ThreadSettingsMediaGalleryProps,
): React.Node {
const styles = useStyles(unboundStyles);
const { width } = useWindowDimensions();
// Explanation of galleryItemWidth:
// The FlatList has a horizontal padding of 16px on each side,
// and so the width of the actual FlatList is `width - 32px`.
// With three columns, there will be two gaps in between the items,
// so the width of each item (with the gaps) will be
// (width - 32px - (numColumns-1) * galleryItemGap) / numColumns.
// E.g. 16px, media, galleryItemGap, media, galleryItemGap, media, 16px
const galleryItemWidth =
(width - 32 - (numColumns - 1) * galleryItemGap) / numColumns;
- const { threadID, limit } = props;
+ const { threadID, limit, verticalBounds } = props;
const [mediaInfos, setMediaInfos] = React.useState([]);
const callFetchThreadMedia = useServerCall(fetchThreadMedia);
React.useEffect(() => {
const fetchData = async () => {
const result = await callFetchThreadMedia({
threadID,
limit,
offset: 0,
});
setMediaInfos(result.media);
};
fetchData();
}, [callFetchThreadMedia, threadID, limit]);
const memoizedStyles = React.useMemo(() => {
return {
mediaContainer: {
marginTop: galleryItemGap,
width: galleryItemWidth,
...styles.mediaContainer,
},
mediaContainerWithMargin: {
marginTop: galleryItemGap,
marginLeft: galleryItemGap,
width: galleryItemWidth,
...styles.mediaContainer,
},
media: {
width: galleryItemWidth,
...styles.media,
},
};
}, [galleryItemWidth, styles.media, styles.mediaContainer]);
const renderItem = React.useCallback(
- ({ item, index }) => {
- const containerStyle =
- index % numColumns === 0
- ? memoizedStyles.mediaContainer
- : memoizedStyles.mediaContainerWithMargin;
-
- const mediaInfoItem: MediaInfo = {
- ...item,
- index,
- };
-
- return (
-
-
-
-
-
- );
- },
- [
- memoizedStyles.media,
- memoizedStyles.mediaContainer,
- memoizedStyles.mediaContainerWithMargin,
- ],
+ ({ item, index }) => (
+
+ ),
+ [threadID, verticalBounds, memoizedStyles],
);
return (
);
}
+type MediaGalleryItemProps = {
+ +item: Media,
+ +index: number,
+ +memoizedStyles: {
+ +mediaContainer: ViewStyleProp,
+ +mediaContainerWithMargin: ViewStyleProp,
+ +media: ViewStyleProp,
+ },
+ +threadID: string,
+ +verticalBounds: ?VerticalBounds,
+};
+
+function MediaGalleryItem(props: MediaGalleryItemProps): React.Node {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const ref = React.useRef(null);
+ const onLayout = React.useCallback(() => {}, []);
+ const { threadID, verticalBounds, memoizedStyles, item, index } = props;
+
+ const mediaInfo: MediaInfo = React.useMemo(
+ () => ({
+ ...(item: Media),
+ index,
+ }),
+ [item, index],
+ );
+
+ const navigateToMedia = React.useCallback(() => {
+ ref.current?.measure((x, y, width, height, pageX, pageY) => {
+ const initialCoordinates: LayoutCoordinates = {
+ x: pageX,
+ y: pageY,
+ width,
+ height,
+ };
+
+ navigation.navigate<'VideoPlaybackModal' | 'ImageModal'>({
+ name:
+ mediaInfo.type === 'video'
+ ? VideoPlaybackModalRouteName
+ : ImageModalRouteName,
+ key: `multimedia|${threadID}|${mediaInfo.id}`,
+ params: {
+ presentedFrom: route.key,
+ mediaInfo,
+ item,
+ initialCoordinates,
+ verticalBounds,
+ },
+ });
+ });
+ }, [navigation, route, threadID, mediaInfo, item, verticalBounds]);
+
+ const containerStyle =
+ index % numColumns === 0
+ ? memoizedStyles.mediaContainer
+ : memoizedStyles.mediaContainerWithMargin;
+
+ return (
+
+
+
+
+
+ );
+}
+
const unboundStyles = {
flatListContainer: {
paddingHorizontal: 16,
},
mediaContainer: {
height: 180,
justifyContent: 'center',
alignItems: 'center',
},
media: {
height: 180,
},
};
export default ThreadSettingsMediaGallery;
diff --git a/native/chat/settings/thread-settings.react.js b/native/chat/settings/thread-settings.react.js
index e45bd5c5b..4864cde6a 100644
--- a/native/chat/settings/thread-settings.react.js
+++ b/native/chat/settings/thread-settings.react.js
@@ -1,1206 +1,1210 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Platform } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { createSelector } from 'reselect';
import tinycolor from 'tinycolor2';
import {
changeThreadSettingsActionTypes,
leaveThreadActionTypes,
removeUsersFromThreadActionTypes,
changeThreadMemberRolesActionTypes,
} from 'lib/actions/thread-actions.js';
import { usePromoteSidebar } from 'lib/hooks/promote-sidebar.react.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import {
threadInfoSelector,
childThreadInfos,
} from 'lib/selectors/thread-selectors.js';
import { getAvailableRelationshipButtons } from 'lib/shared/relationship-utils.js';
import {
threadHasPermission,
viewerIsMember,
threadInChatList,
getSingleOtherUser,
threadIsChannel,
} from 'lib/shared/thread-utils.js';
import threadWatcher from 'lib/shared/thread-watcher.js';
import type { RelationshipButton } from 'lib/types/relationship-types.js';
import {
type ThreadInfo,
type ResolvedThreadInfo,
type RelativeMemberInfo,
threadPermissions,
threadTypes,
} from 'lib/types/thread-types.js';
import type { UserInfos } from 'lib/types/user-types.js';
import {
useResolvedThreadInfo,
useResolvedOptionalThreadInfo,
useResolvedOptionalThreadInfos,
} from 'lib/utils/entity-helpers.js';
import type { CategoryType } from './thread-settings-category.react.js';
import {
ThreadSettingsCategoryHeader,
ThreadSettingsCategoryActionHeader,
ThreadSettingsCategoryFooter,
} from './thread-settings-category.react.js';
import ThreadSettingsChildThread from './thread-settings-child-thread.react.js';
import ThreadSettingsColor from './thread-settings-color.react.js';
import ThreadSettingsDeleteThread from './thread-settings-delete-thread.react.js';
import ThreadSettingsDescription from './thread-settings-description.react.js';
import ThreadSettingsEditRelationship from './thread-settings-edit-relationship.react.js';
import ThreadSettingsHomeNotifs from './thread-settings-home-notifs.react.js';
import ThreadSettingsLeaveThread from './thread-settings-leave-thread.react.js';
import {
ThreadSettingsSeeMore,
ThreadSettingsAddMember,
ThreadSettingsAddSubchannel,
} from './thread-settings-list-action.react.js';
import ThreadSettingsMediaGallery from './thread-settings-media-gallery.react.js';
import ThreadSettingsMember from './thread-settings-member.react.js';
import ThreadSettingsName from './thread-settings-name.react.js';
import ThreadSettingsParent from './thread-settings-parent.react.js';
import ThreadSettingsPromoteSidebar from './thread-settings-promote-sidebar.react.js';
import ThreadSettingsPushNotifs from './thread-settings-push-notifs.react.js';
import ThreadSettingsVisibility from './thread-settings-visibility.react.js';
import ThreadAncestors from '../../components/thread-ancestors.react.js';
import {
type KeyboardState,
KeyboardContext,
} from '../../keyboard/keyboard-state.js';
import { defaultStackScreenOptions } from '../../navigation/options.js';
import {
OverlayContext,
type OverlayContextType,
} from '../../navigation/overlay-context.js';
import {
AddUsersModalRouteName,
ComposeSubchannelModalRouteName,
FullScreenThreadMediaGalleryRouteName,
} from '../../navigation/route-names.js';
import type { NavigationRoute } from '../../navigation/route-names.js';
import type { TabNavigationProp } from '../../navigation/tab-navigator.react.js';
import { useSelector } from '../../redux/redux-utils.js';
import type { AppState } from '../../redux/state-types.js';
import {
useStyles,
type IndicatorStyle,
useIndicatorStyle,
} from '../../themes/colors.js';
import type { VerticalBounds } from '../../types/layout-types.js';
import type { ViewStyle } from '../../types/styles.js';
import type { ChatNavigationProp } from '../chat.react.js';
const itemPageLength = 5;
export type ThreadSettingsParams = {
+threadInfo: ThreadInfo,
};
export type ThreadSettingsNavigate = $PropertyType<
ChatNavigationProp<'ThreadSettings'>,
'navigate',
>;
type ChatSettingsItem =
| {
+itemType: 'header',
+key: string,
+title: string,
+categoryType: CategoryType,
}
| {
+itemType: 'actionHeader',
+key: string,
+title: string,
+actionText: string,
+onPress: () => void,
}
| {
+itemType: 'footer',
+key: string,
+categoryType: CategoryType,
}
| {
+itemType: 'name',
+key: string,
+threadInfo: ResolvedThreadInfo,
+nameEditValue: ?string,
+canChangeSettings: boolean,
}
| {
+itemType: 'color',
+key: string,
+threadInfo: ResolvedThreadInfo,
+colorEditValue: string,
+canChangeSettings: boolean,
+navigate: ThreadSettingsNavigate,
+threadSettingsRouteKey: string,
}
| {
+itemType: 'description',
+key: string,
+threadInfo: ResolvedThreadInfo,
+descriptionEditValue: ?string,
+descriptionTextHeight: ?number,
+canChangeSettings: boolean,
}
| {
+itemType: 'parent',
+key: string,
+threadInfo: ResolvedThreadInfo,
+parentThreadInfo: ?ResolvedThreadInfo,
}
| {
+itemType: 'visibility',
+key: string,
+threadInfo: ResolvedThreadInfo,
}
| {
+itemType: 'pushNotifs',
+key: string,
+threadInfo: ResolvedThreadInfo,
}
| {
+itemType: 'homeNotifs',
+key: string,
+threadInfo: ResolvedThreadInfo,
}
| {
+itemType: 'seeMore',
+key: string,
+onPress: () => void,
}
| {
+itemType: 'childThread',
+key: string,
+threadInfo: ResolvedThreadInfo,
+firstListItem: boolean,
+lastListItem: boolean,
}
| {
+itemType: 'addSubchannel',
+key: string,
}
| {
+itemType: 'member',
+key: string,
+memberInfo: RelativeMemberInfo,
+threadInfo: ResolvedThreadInfo,
+canEdit: boolean,
+navigate: ThreadSettingsNavigate,
+firstListItem: boolean,
+lastListItem: boolean,
+verticalBounds: ?VerticalBounds,
+threadSettingsRouteKey: string,
}
| {
+itemType: 'addMember',
+key: string,
}
| {
+itemType: 'mediaGallery',
+key: string,
+threadInfo: ThreadInfo,
+limit: number,
+ +verticalBounds: ?VerticalBounds,
}
| {
+itemType: 'promoteSidebar' | 'leaveThread' | 'deleteThread',
+key: string,
+threadInfo: ResolvedThreadInfo,
+navigate: ThreadSettingsNavigate,
+buttonStyle: ViewStyle,
}
| {
+itemType: 'editRelationship',
+key: string,
+threadInfo: ResolvedThreadInfo,
+navigate: ThreadSettingsNavigate,
+buttonStyle: ViewStyle,
+relationshipButton: RelationshipButton,
};
type BaseProps = {
+navigation: ChatNavigationProp<'ThreadSettings'>,
+route: NavigationRoute<'ThreadSettings'>,
};
type Props = {
...BaseProps,
// Redux state
+userInfos: UserInfos,
+viewerID: ?string,
+threadInfo: ResolvedThreadInfo,
+parentThreadInfo: ?ResolvedThreadInfo,
+childThreadInfos: ?$ReadOnlyArray,
+somethingIsSaving: boolean,
+styles: typeof unboundStyles,
+indicatorStyle: IndicatorStyle,
// withOverlayContext
+overlayContext: ?OverlayContextType,
// withKeyboardState
+keyboardState: ?KeyboardState,
+canPromoteSidebar: boolean,
};
type State = {
+numMembersShowing: number,
+numSubchannelsShowing: number,
+numSidebarsShowing: number,
+nameEditValue: ?string,
+descriptionEditValue: ?string,
+descriptionTextHeight: ?number,
+colorEditValue: string,
+verticalBounds: ?VerticalBounds,
};
type PropsAndState = { ...Props, ...State };
class ThreadSettings extends React.PureComponent {
flatListContainer: ?React.ElementRef;
constructor(props: Props) {
super(props);
this.state = {
numMembersShowing: itemPageLength,
numSubchannelsShowing: itemPageLength,
numSidebarsShowing: itemPageLength,
nameEditValue: null,
descriptionEditValue: null,
descriptionTextHeight: null,
colorEditValue: props.threadInfo.color,
verticalBounds: null,
};
}
static scrollDisabled(props: Props) {
const { overlayContext } = props;
invariant(overlayContext, 'ThreadSettings should have OverlayContext');
return overlayContext.scrollBlockingModalStatus !== 'closed';
}
componentDidUpdate(prevProps: Props) {
const prevThreadInfo = prevProps.threadInfo;
const newThreadInfo = this.props.threadInfo;
if (
!tinycolor.equals(newThreadInfo.color, prevThreadInfo.color) &&
tinycolor.equals(this.state.colorEditValue, prevThreadInfo.color)
) {
this.setState({ colorEditValue: newThreadInfo.color });
}
if (defaultStackScreenOptions.gestureEnabled) {
const scrollIsDisabled = ThreadSettings.scrollDisabled(this.props);
const scrollWasDisabled = ThreadSettings.scrollDisabled(prevProps);
if (!scrollWasDisabled && scrollIsDisabled) {
this.props.navigation.setOptions({ gestureEnabled: false });
} else if (scrollWasDisabled && !scrollIsDisabled) {
this.props.navigation.setOptions({ gestureEnabled: true });
}
}
}
threadBasicsListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.threadInfo,
(propsAndState: PropsAndState) => propsAndState.parentThreadInfo,
(propsAndState: PropsAndState) => propsAndState.nameEditValue,
(propsAndState: PropsAndState) => propsAndState.colorEditValue,
(propsAndState: PropsAndState) => propsAndState.descriptionEditValue,
(propsAndState: PropsAndState) => propsAndState.descriptionTextHeight,
(propsAndState: PropsAndState) => !propsAndState.somethingIsSaving,
(propsAndState: PropsAndState) => propsAndState.navigation.navigate,
(propsAndState: PropsAndState) => propsAndState.route.key,
(
threadInfo: ResolvedThreadInfo,
parentThreadInfo: ?ResolvedThreadInfo,
nameEditValue: ?string,
colorEditValue: string,
descriptionEditValue: ?string,
descriptionTextHeight: ?number,
canStartEditing: boolean,
navigate: ThreadSettingsNavigate,
routeKey: string,
) => {
const canEditThreadName = threadHasPermission(
threadInfo,
threadPermissions.EDIT_THREAD_NAME,
);
const canEditThreadDescription = threadHasPermission(
threadInfo,
threadPermissions.EDIT_THREAD_DESCRIPTION,
);
const canEditThreadColor = threadHasPermission(
threadInfo,
threadPermissions.EDIT_THREAD_COLOR,
);
const canChangeName = canEditThreadName && canStartEditing;
const canChangeDescription = canEditThreadDescription && canStartEditing;
const canChangeColor = canEditThreadColor && canStartEditing;
const listData: ChatSettingsItem[] = [];
listData.push({
itemType: 'header',
key: 'basicsHeader',
title: 'Basics',
categoryType: 'full',
});
listData.push({
itemType: 'name',
key: 'name',
threadInfo,
nameEditValue,
canChangeSettings: canChangeName,
});
listData.push({
itemType: 'color',
key: 'color',
threadInfo,
colorEditValue,
canChangeSettings: canChangeColor,
navigate,
threadSettingsRouteKey: routeKey,
});
listData.push({
itemType: 'footer',
key: 'basicsFooter',
categoryType: 'full',
});
if (
(descriptionEditValue !== null && descriptionEditValue !== undefined) ||
threadInfo.description ||
canEditThreadDescription
) {
listData.push({
itemType: 'description',
key: 'description',
threadInfo,
descriptionEditValue,
descriptionTextHeight,
canChangeSettings: canChangeDescription,
});
}
const isMember = viewerIsMember(threadInfo);
if (isMember) {
listData.push({
itemType: 'header',
key: 'subscriptionHeader',
title: 'Subscription',
categoryType: 'full',
});
listData.push({
itemType: 'pushNotifs',
key: 'pushNotifs',
threadInfo,
});
if (threadInfo.type !== threadTypes.SIDEBAR) {
listData.push({
itemType: 'homeNotifs',
key: 'homeNotifs',
threadInfo,
});
}
listData.push({
itemType: 'footer',
key: 'subscriptionFooter',
categoryType: 'full',
});
}
listData.push({
itemType: 'header',
key: 'privacyHeader',
title: 'Privacy',
categoryType: 'full',
});
listData.push({
itemType: 'visibility',
key: 'visibility',
threadInfo,
});
listData.push({
itemType: 'parent',
key: 'parent',
threadInfo,
parentThreadInfo,
});
listData.push({
itemType: 'footer',
key: 'privacyFooter',
categoryType: 'full',
});
return listData;
},
);
subchannelsListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.threadInfo,
(propsAndState: PropsAndState) => propsAndState.navigation.navigate,
(propsAndState: PropsAndState) => propsAndState.childThreadInfos,
(propsAndState: PropsAndState) => propsAndState.numSubchannelsShowing,
(
threadInfo: ResolvedThreadInfo,
navigate: ThreadSettingsNavigate,
childThreads: ?$ReadOnlyArray,
numSubchannelsShowing: number,
) => {
const listData: ChatSettingsItem[] = [];
const subchannels = childThreads?.filter(threadIsChannel) ?? [];
const canCreateSubchannels = threadHasPermission(
threadInfo,
threadPermissions.CREATE_SUBCHANNELS,
);
if (subchannels.length === 0 && !canCreateSubchannels) {
return listData;
}
listData.push({
itemType: 'header',
key: 'subchannelHeader',
title: 'Subchannels',
categoryType: 'unpadded',
});
if (canCreateSubchannels) {
listData.push({
itemType: 'addSubchannel',
key: 'addSubchannel',
});
}
const numItems = Math.min(numSubchannelsShowing, subchannels.length);
for (let i = 0; i < numItems; i++) {
const subchannelInfo = subchannels[i];
listData.push({
itemType: 'childThread',
key: `childThread${subchannelInfo.id}`,
threadInfo: subchannelInfo,
firstListItem: i === 0 && !canCreateSubchannels,
lastListItem: i === numItems - 1 && numItems === subchannels.length,
});
}
if (numItems < subchannels.length) {
listData.push({
itemType: 'seeMore',
key: 'seeMoreSubchannels',
onPress: this.onPressSeeMoreSubchannels,
});
}
listData.push({
itemType: 'footer',
key: 'subchannelFooter',
categoryType: 'unpadded',
});
return listData;
},
);
sidebarsListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.navigation.navigate,
(propsAndState: PropsAndState) => propsAndState.childThreadInfos,
(propsAndState: PropsAndState) => propsAndState.numSidebarsShowing,
(
navigate: ThreadSettingsNavigate,
childThreads: ?$ReadOnlyArray,
numSidebarsShowing: number,
) => {
const listData: ChatSettingsItem[] = [];
const sidebars =
childThreads?.filter(
childThreadInfo => childThreadInfo.type === threadTypes.SIDEBAR,
) ?? [];
if (sidebars.length === 0) {
return listData;
}
listData.push({
itemType: 'header',
key: 'sidebarHeader',
title: 'Threads',
categoryType: 'unpadded',
});
const numItems = Math.min(numSidebarsShowing, sidebars.length);
for (let i = 0; i < numItems; i++) {
const sidebarInfo = sidebars[i];
listData.push({
itemType: 'childThread',
key: `childThread${sidebarInfo.id}`,
threadInfo: sidebarInfo,
firstListItem: i === 0,
lastListItem: i === numItems - 1 && numItems === sidebars.length,
});
}
if (numItems < sidebars.length) {
listData.push({
itemType: 'seeMore',
key: 'seeMoreSidebars',
onPress: this.onPressSeeMoreSidebars,
});
}
listData.push({
itemType: 'footer',
key: 'sidebarFooter',
categoryType: 'unpadded',
});
return listData;
},
);
threadMembersListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.threadInfo,
(propsAndState: PropsAndState) => !propsAndState.somethingIsSaving,
(propsAndState: PropsAndState) => propsAndState.navigation.navigate,
(propsAndState: PropsAndState) => propsAndState.route.key,
(propsAndState: PropsAndState) => propsAndState.numMembersShowing,
(propsAndState: PropsAndState) => propsAndState.verticalBounds,
(
threadInfo: ResolvedThreadInfo,
canStartEditing: boolean,
navigate: ThreadSettingsNavigate,
routeKey: string,
numMembersShowing: number,
verticalBounds: ?VerticalBounds,
) => {
const listData: ChatSettingsItem[] = [];
const canAddMembers = threadHasPermission(
threadInfo,
threadPermissions.ADD_MEMBERS,
);
if (threadInfo.members.length === 0 && !canAddMembers) {
return listData;
}
listData.push({
itemType: 'header',
key: 'memberHeader',
title: 'Members',
categoryType: 'unpadded',
});
if (canAddMembers) {
listData.push({
itemType: 'addMember',
key: 'addMember',
});
}
const numItems = Math.min(numMembersShowing, threadInfo.members.length);
for (let i = 0; i < numItems; i++) {
const memberInfo = threadInfo.members[i];
listData.push({
itemType: 'member',
key: `member${memberInfo.id}`,
memberInfo,
threadInfo,
canEdit: canStartEditing,
navigate,
firstListItem: i === 0 && !canAddMembers,
lastListItem:
i === numItems - 1 && numItems === threadInfo.members.length,
verticalBounds,
threadSettingsRouteKey: routeKey,
});
}
if (numItems < threadInfo.members.length) {
listData.push({
itemType: 'seeMore',
key: 'seeMoreMembers',
onPress: this.onPressSeeMoreMembers,
});
}
listData.push({
itemType: 'footer',
key: 'memberFooter',
categoryType: 'unpadded',
});
return listData;
},
);
mediaGalleryListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.threadInfo,
- (threadInfo: ThreadInfo) => {
+ (propsAndState: PropsAndState) => propsAndState.verticalBounds,
+ (threadInfo: ThreadInfo, verticalBounds: ?VerticalBounds) => {
const listData: ChatSettingsItem[] = [];
const limit = 6;
listData.push({
itemType: 'actionHeader',
key: 'mediaGalleryHeader',
title: 'Media Gallery',
actionText: 'See more',
onPress: this.onPressSeeMoreMediaGallery,
});
listData.push({
itemType: 'mediaGallery',
key: 'mediaGallery',
threadInfo,
limit,
+ verticalBounds,
});
listData.push({
itemType: 'footer',
key: 'mediaGalleryFooter',
categoryType: 'outline',
});
return listData;
},
);
actionsListDataSelector = createSelector(
(propsAndState: PropsAndState) => propsAndState.threadInfo,
(propsAndState: PropsAndState) => propsAndState.parentThreadInfo,
(propsAndState: PropsAndState) => propsAndState.navigation.navigate,
(propsAndState: PropsAndState) => propsAndState.styles,
(propsAndState: PropsAndState) => propsAndState.userInfos,
(propsAndState: PropsAndState) => propsAndState.viewerID,
(
threadInfo: ResolvedThreadInfo,
parentThreadInfo: ?ResolvedThreadInfo,
navigate: ThreadSettingsNavigate,
styles: typeof unboundStyles,
userInfos: UserInfos,
viewerID: ?string,
) => {
const buttons = [];
if (this.props.canPromoteSidebar) {
buttons.push({
itemType: 'promoteSidebar',
key: 'promoteSidebar',
threadInfo,
navigate,
});
}
const canLeaveThread = threadHasPermission(
threadInfo,
threadPermissions.LEAVE_THREAD,
);
if (viewerIsMember(threadInfo) && canLeaveThread) {
buttons.push({
itemType: 'leaveThread',
key: 'leaveThread',
threadInfo,
navigate,
});
}
const canDeleteThread = threadHasPermission(
threadInfo,
threadPermissions.DELETE_THREAD,
);
if (canDeleteThread) {
buttons.push({
itemType: 'deleteThread',
key: 'deleteThread',
threadInfo,
navigate,
});
}
const threadIsPersonal = threadInfo.type === threadTypes.PERSONAL;
if (threadIsPersonal && viewerID) {
const otherMemberID = getSingleOtherUser(threadInfo, viewerID);
if (otherMemberID) {
const otherUserInfo = userInfos[otherMemberID];
const availableRelationshipActions =
getAvailableRelationshipButtons(otherUserInfo);
for (const action of availableRelationshipActions) {
buttons.push({
itemType: 'editRelationship',
key: action,
threadInfo,
navigate,
relationshipButton: action,
});
}
}
}
const listData: ChatSettingsItem[] = [];
if (buttons.length === 0) {
return listData;
}
listData.push({
itemType: 'header',
key: 'actionsHeader',
title: 'Actions',
categoryType: 'unpadded',
});
for (let i = 0; i < buttons.length; i++) {
// Necessary for Flow...
if (buttons[i].itemType === 'editRelationship') {
listData.push({
...buttons[i],
buttonStyle: [
i === 0 ? null : styles.nonTopButton,
i === buttons.length - 1 ? styles.lastButton : null,
],
});
} else {
listData.push({
...buttons[i],
buttonStyle: [
i === 0 ? null : styles.nonTopButton,
i === buttons.length - 1 ? styles.lastButton : null,
],
});
}
}
listData.push({
itemType: 'footer',
key: 'actionsFooter',
categoryType: 'unpadded',
});
return listData;
},
);
listDataSelector = createSelector(
this.threadBasicsListDataSelector,
this.subchannelsListDataSelector,
this.sidebarsListDataSelector,
this.threadMembersListDataSelector,
this.mediaGalleryListDataSelector,
this.actionsListDataSelector,
(
threadBasicsListData: ChatSettingsItem[],
subchannelsListData: ChatSettingsItem[],
sidebarsListData: ChatSettingsItem[],
threadMembersListData: ChatSettingsItem[],
mediaGalleryListData: ChatSettingsItem[],
actionsListData: ChatSettingsItem[],
) => [
...threadBasicsListData,
...subchannelsListData,
...sidebarsListData,
...threadMembersListData,
...mediaGalleryListData,
...actionsListData,
],
);
get listData() {
return this.listDataSelector({ ...this.props, ...this.state });
}
render() {
return (
);
}
flatListContainerRef = (
flatListContainer: ?React.ElementRef,
) => {
this.flatListContainer = flatListContainer;
};
onFlatListContainerLayout = () => {
const { flatListContainer } = this;
if (!flatListContainer) {
return;
}
const { keyboardState } = this.props;
if (!keyboardState || keyboardState.keyboardShowing) {
return;
}
flatListContainer.measure((x, y, width, height, pageX, pageY) => {
if (
height === null ||
height === undefined ||
pageY === null ||
pageY === undefined
) {
return;
}
this.setState({ verticalBounds: { height, y: pageY } });
});
};
renderItem = (row: { item: ChatSettingsItem, ... }) => {
const item = row.item;
if (item.itemType === 'header') {
return (
);
} else if (item.itemType === 'actionHeader') {
return (
);
} else if (item.itemType === 'footer') {
return ;
} else if (item.itemType === 'name') {
return (
);
} else if (item.itemType === 'color') {
return (
);
} else if (item.itemType === 'description') {
return (
);
} else if (item.itemType === 'parent') {
return (
);
} else if (item.itemType === 'visibility') {
return ;
} else if (item.itemType === 'pushNotifs') {
return ;
} else if (item.itemType === 'homeNotifs') {
return ;
} else if (item.itemType === 'seeMore') {
return ;
} else if (item.itemType === 'childThread') {
return (
);
} else if (item.itemType === 'addSubchannel') {
return (
);
} else if (item.itemType === 'member') {
return (
);
} else if (item.itemType === 'addMember') {
return ;
} else if (item.itemType === 'mediaGallery') {
return (
);
} else if (item.itemType === 'leaveThread') {
return (
);
} else if (item.itemType === 'deleteThread') {
return (
);
} else if (item.itemType === 'promoteSidebar') {
return (
);
} else if (item.itemType === 'editRelationship') {
return (
);
} else {
invariant(false, `unexpected ThreadSettings item type ${item.itemType}`);
}
};
setNameEditValue = (value: ?string, callback?: () => void) => {
this.setState({ nameEditValue: value }, callback);
};
setColorEditValue = (color: string) => {
this.setState({ colorEditValue: color });
};
setDescriptionEditValue = (value: ?string, callback?: () => void) => {
this.setState({ descriptionEditValue: value }, callback);
};
setDescriptionTextHeight = (height: number) => {
this.setState({ descriptionTextHeight: height });
};
onPressComposeSubchannel = () => {
this.props.navigation.navigate(ComposeSubchannelModalRouteName, {
presentedFrom: this.props.route.key,
threadInfo: this.props.threadInfo,
});
};
onPressAddMember = () => {
this.props.navigation.navigate(AddUsersModalRouteName, {
presentedFrom: this.props.route.key,
threadInfo: this.props.threadInfo,
});
};
onPressSeeMoreMembers = () => {
this.setState(prevState => ({
numMembersShowing: prevState.numMembersShowing + itemPageLength,
}));
};
onPressSeeMoreSubchannels = () => {
this.setState(prevState => ({
numSubchannelsShowing: prevState.numSubchannelsShowing + itemPageLength,
}));
};
onPressSeeMoreSidebars = () => {
this.setState(prevState => ({
numSidebarsShowing: prevState.numSidebarsShowing + itemPageLength,
}));
};
onPressSeeMoreMediaGallery = () => {
this.props.navigation.navigate(FullScreenThreadMediaGalleryRouteName, {
threadInfo: this.props.threadInfo,
});
};
}
const unboundStyles = {
container: {
backgroundColor: 'panelBackground',
flex: 1,
},
flatList: {
paddingVertical: 16,
},
nonTopButton: {
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
lastButton: {
paddingBottom: Platform.OS === 'ios' ? 14 : 12,
},
};
const editNameLoadingStatusSelector = createLoadingStatusSelector(
changeThreadSettingsActionTypes,
`${changeThreadSettingsActionTypes.started}:name`,
);
const editColorLoadingStatusSelector = createLoadingStatusSelector(
changeThreadSettingsActionTypes,
`${changeThreadSettingsActionTypes.started}:color`,
);
const editDescriptionLoadingStatusSelector = createLoadingStatusSelector(
changeThreadSettingsActionTypes,
`${changeThreadSettingsActionTypes.started}:description`,
);
const leaveThreadLoadingStatusSelector = createLoadingStatusSelector(
leaveThreadActionTypes,
);
const somethingIsSaving = (
state: AppState,
threadMembers: $ReadOnlyArray,
) => {
if (
editNameLoadingStatusSelector(state) === 'loading' ||
editColorLoadingStatusSelector(state) === 'loading' ||
editDescriptionLoadingStatusSelector(state) === 'loading' ||
leaveThreadLoadingStatusSelector(state) === 'loading'
) {
return true;
}
for (const threadMember of threadMembers) {
const removeUserLoadingStatus = createLoadingStatusSelector(
removeUsersFromThreadActionTypes,
`${removeUsersFromThreadActionTypes.started}:${threadMember.id}`,
)(state);
if (removeUserLoadingStatus === 'loading') {
return true;
}
const changeRoleLoadingStatus = createLoadingStatusSelector(
changeThreadMemberRolesActionTypes,
`${changeThreadMemberRolesActionTypes.started}:${threadMember.id}`,
)(state);
if (changeRoleLoadingStatus === 'loading') {
return true;
}
}
return false;
};
const ConnectedThreadSettings: React.ComponentType =
React.memo(function ConnectedThreadSettings(props: BaseProps) {
const userInfos = useSelector(state => state.userStore.userInfos);
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const threadID = props.route.params.threadInfo.id;
const reduxThreadInfo: ?ThreadInfo = useSelector(
state => threadInfoSelector(state)[threadID],
);
React.useEffect(() => {
invariant(
reduxThreadInfo,
'ReduxThreadInfo should exist when ThreadSettings is opened',
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { setParams } = props.navigation;
React.useEffect(() => {
if (reduxThreadInfo) {
setParams({ threadInfo: reduxThreadInfo });
}
}, [reduxThreadInfo, setParams]);
const threadInfo: ThreadInfo =
reduxThreadInfo ?? props.route.params.threadInfo;
const resolvedThreadInfo = useResolvedThreadInfo(threadInfo);
React.useEffect(() => {
if (threadInChatList(threadInfo)) {
return undefined;
}
threadWatcher.watchID(threadInfo.id);
return () => {
threadWatcher.removeID(threadInfo.id);
};
}, [threadInfo]);
const parentThreadID = threadInfo.parentThreadID;
const parentThreadInfo: ?ThreadInfo = useSelector(state =>
parentThreadID ? threadInfoSelector(state)[parentThreadID] : null,
);
const resolvedParentThreadInfo =
useResolvedOptionalThreadInfo(parentThreadInfo);
const threadMembers = threadInfo.members;
const boundChildThreadInfos = useSelector(
state => childThreadInfos(state)[threadID],
);
const resolvedChildThreadInfos = useResolvedOptionalThreadInfos(
boundChildThreadInfos,
);
const boundSomethingIsSaving = useSelector(state =>
somethingIsSaving(state, threadMembers),
);
const { navigation } = props;
React.useEffect(() => {
const tabNavigation: ?TabNavigationProp<'Chat'> = navigation.getParent();
invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
const onTabPress = () => {
if (navigation.isFocused() && !boundSomethingIsSaving) {
navigation.popToTop();
}
};
tabNavigation.addListener('tabPress', onTabPress);
return () => tabNavigation.removeListener('tabPress', onTabPress);
}, [navigation, boundSomethingIsSaving]);
const styles = useStyles(unboundStyles);
const indicatorStyle = useIndicatorStyle();
const overlayContext = React.useContext(OverlayContext);
const keyboardState = React.useContext(KeyboardContext);
const { canPromoteSidebar } = usePromoteSidebar(threadInfo);
return (
);
});
export default ConnectedThreadSettings;