diff --git a/native/avatars/avatar.react.js b/native/avatars/avatar.react.js index 95beba2f9..9e70c6aba 100644 --- a/native/avatars/avatar.react.js +++ b/native/avatars/avatar.react.js @@ -1,130 +1,127 @@ // @flow import * as React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import type { ResolvedClientAvatar } from 'lib/types/avatar-types.js'; import Multimedia from '../media/multimedia.react.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +avatarInfo: ResolvedClientAvatar, +size: 'micro' | 'small' | 'large' | 'profile', }; function Avatar(props: Props): React.Node { const { avatarInfo, size } = props; - const shouldRenderAvatars = useShouldRenderAvatars(); - const containerSizeStyle = React.useMemo(() => { if (size === 'micro') { return styles.micro; } else if (size === 'small') { return styles.small; } else if (size === 'large') { return styles.large; } return styles.profile; }, [size]); const emojiContainerStyle = React.useMemo(() => { const containerStyles = [styles.emojiContainer, containerSizeStyle]; if (avatarInfo.type === 'emoji') { const backgroundColor = { backgroundColor: `#${avatarInfo.color}` }; containerStyles.push(backgroundColor); } return containerStyles; }, [avatarInfo, containerSizeStyle]); const emojiSizeStyle = React.useMemo(() => { if (size === 'micro') { return styles.emojiMicro; } else if (size === 'small') { return styles.emojiSmall; } else if (size === 'large') { return styles.emojiLarge; } return styles.emojiProfile; }, [size]); const avatar = React.useMemo(() => { if (avatarInfo.type === 'image') { const avatarMediaInfo = { type: 'photo', uri: avatarInfo.uri, }; return ( ); } return ( {avatarInfo.emoji} ); }, [ avatarInfo.emoji, avatarInfo.type, avatarInfo.uri, containerSizeStyle, emojiContainerStyle, emojiSizeStyle, ]); - return shouldRenderAvatars ? avatar : null; + return avatar; } const styles = StyleSheet.create({ emojiContainer: { alignItems: 'center', justifyContent: 'center', }, emojiLarge: { fontSize: 28, textAlign: 'center', }, emojiMicro: { fontSize: 9, textAlign: 'center', }, emojiProfile: { fontSize: 80, textAlign: 'center', }, emojiSmall: { fontSize: 14, textAlign: 'center', }, imageContainer: { overflow: 'hidden', }, large: { borderRadius: 20, height: 40, width: 40, }, micro: { borderRadius: 8, height: 16, width: 16, }, profile: { borderRadius: 56, height: 112, width: 112, }, small: { borderRadius: 12, height: 24, width: 24, }, }); export default Avatar; diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js index 768ac856e..b9fa15bae 100644 --- a/native/chat/chat-thread-list-item.react.js +++ b/native/chat/chat-thread-list-item.react.js @@ -1,230 +1,221 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js'; import MessagePreview from './message-preview.react.js'; import SwipeableThread from './swipeable-thread.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import Button from '../components/button.react.js'; -import ColorSplotch from '../components/color-splotch.react.js'; import { SingleLine } from '../components/single-line.react.js'; import ThreadAncestorsLabel from '../components/thread-ancestors-label.react.js'; import UnreadDot from '../components/unread-dot.react.js'; import { useColors, useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +data: ChatThreadItem, +onPressItem: ( threadInfo: ThreadInfo, pendingPersonalThreadUserInfo?: UserInfo, ) => void, +onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId: string, }; function ChatThreadListItem({ data, onPressItem, onPressSeeMoreSidebars, onSwipeableWillOpen, currentlyOpenedSwipeableId, }: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const lastMessage = React.useMemo(() => { const mostRecentMessageInfo = data.mostRecentMessageInfo; if (!mostRecentMessageInfo) { return ( No messages ); } return ( ); }, [data.mostRecentMessageInfo, data.threadInfo, styles]); const numOfSidebarsWithExtendedArrow = data.sidebars.filter(sidebarItem => sidebarItem.type === 'sidebar').length - 1; const sidebars = data.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return ; } }); const onPress = React.useCallback(() => { onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo); }, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]); const threadNameStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.threadName; } return [styles.threadName, styles.unreadThreadName]; }, [ data.threadInfo.currentUser.unread, styles.threadName, styles.unreadThreadName, ]); const lastActivity = shortAbsoluteDate(data.lastUpdatedTime); const lastActivityStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.lastActivity; } return [styles.lastActivity, styles.unreadLastActivity]; }, [ data.threadInfo.currentUser.unread, styles.lastActivity, styles.unreadLastActivity, ]); const resolvedThreadInfo = useResolvedThreadInfo(data.threadInfo); - const shouldRenderAvatars = useShouldRenderAvatars(); - - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return ; - } - - return ; - }, [data.threadInfo, shouldRenderAvatars]); return ( <> {sidebars} ); } const chatThreadListItemHeight = 70; const spacerHeight = 6; const unboundStyles = { container: { height: chatThreadListItemHeight, justifyContent: 'center', backgroundColor: 'listBackground', }, content: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, - colorSplotch: { + avatarContainer: { marginLeft: 6, marginBottom: 12, }, threadDetails: { paddingLeft: 12, paddingRight: 18, justifyContent: 'center', flex: 1, marginTop: 5, }, lastActivity: { color: 'listForegroundTertiaryLabel', fontSize: 14, marginLeft: 10, }, unreadLastActivity: { color: 'listForegroundLabel', fontWeight: 'bold', }, noMessages: { color: 'listForegroundTertiaryLabel', flex: 1, fontSize: 14, fontStyle: 'italic', }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, threadName: { color: 'listForegroundSecondaryLabel', flex: 1, fontSize: 21, }, unreadThreadName: { color: 'listForegroundLabel', fontWeight: '500', }, spacer: { height: spacerHeight, }, }; export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight }; diff --git a/native/chat/composed-message-width.js b/native/chat/composed-message-width.js index b2884010d..fa0f452ff 100644 --- a/native/chat/composed-message-width.js +++ b/native/chat/composed-message-width.js @@ -1,26 +1,20 @@ // @flow import { avatarOffset } from './chat-constants.js'; import { useSelector } from '../redux/redux-utils.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; function useMessageListScreenWidth(): number { return useSelector(state => { const { dimensions } = state; return dimensions.rotated ? dimensions.height : dimensions.width; }); } // Keep sorta synced with styles.alignment/styles.messageBox in ComposedMessage function useComposedMessageMaxWidth(): number { const messageListScreenWidth = useMessageListScreenWidth(); - const shouldRenderAvatars = useShouldRenderAvatars(); - if (shouldRenderAvatars) { - return (messageListScreenWidth - 24 - avatarOffset) * 0.8; - } - - return (messageListScreenWidth - 24) * 0.8; + return (messageListScreenWidth - 24 - avatarOffset) * 0.8; } export { useMessageListScreenWidth, useComposedMessageMaxWidth }; diff --git a/native/chat/composed-message.react.js b/native/chat/composed-message.react.js index edcaa6ce0..e01805735 100644 --- a/native/chat/composed-message.react.js +++ b/native/chat/composed-message.react.js @@ -1,341 +1,335 @@ // @flow import Icon from '@expo/vector-icons/Feather.js'; import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { useDerivedValue, withTiming, interpolateColor, useAnimatedStyle, } from 'react-native-reanimated'; import { getMessageLabel } from 'lib/shared/edit-messages-utils.js'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { assertComposableMessageType } from 'lib/types/message-types.js'; import { clusterEndHeight, composedMessageStyle, avatarOffset, } from './chat-constants.js'; import { useComposedMessageMaxWidth } from './composed-message-width.js'; import { FailedSend } from './failed-send.react.js'; import { InlineEngagement } from './inline-engagement.react.js'; import { MessageHeader } from './message-header.react.js'; import { useNavigateToSidebar } from './sidebar-navigation.js'; import SwipeableMessage from './swipeable-message.react.js'; import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import CommIcon from '../components/comm-icon.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import { type Colors, useColors } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; /* eslint-disable import/no-named-as-default-member */ const { Node } = Animated; /* eslint-enable import/no-named-as-default-member */ type SwipeOptions = 'reply' | 'sidebar' | 'both' | 'none'; type BaseProps = { ...React.ElementConfig, +item: ChatMessageInfoItemWithHeight, +sendFailed: boolean, +focused: boolean, +swipeOptions: SwipeOptions, +shouldDisplayPinIndicator: boolean, +children: React.Node, }; type Props = { ...BaseProps, // Redux state +composedMessageMaxWidth: number, +colors: Colors, +contentAndHeaderOpacity: number | Node, +deliveryIconOpacity: number | Node, // withInputState +inputState: ?InputState, +navigateToSidebar: () => mixed, - +shouldRenderAvatars: boolean, +editedMessageStyle: AnimatedStyleObj, }; class ComposedMessage extends React.PureComponent { render() { assertComposableMessageType(this.props.item.messageInfo.type); const { item, sendFailed, focused, swipeOptions, shouldDisplayPinIndicator, children, composedMessageMaxWidth, colors, inputState, navigateToSidebar, contentAndHeaderOpacity, deliveryIconOpacity, - shouldRenderAvatars, editedMessageStyle, ...viewProps } = this.props; const { id, creator } = item.messageInfo; const { hasBeenEdited, isPinned } = item; const { isViewer } = creator; const alignStyle = isViewer ? styles.rightChatBubble : styles.leftChatBubble; let containerMarginBottom = 5; if (item.endsCluster) { containerMarginBottom += clusterEndHeight; } const containerStyle = { marginBottom: containerMarginBottom }; const messageBoxContainerStyle = [styles.messageBoxContainer]; const positioningStyle = isViewer ? styles.rightChatContainer : styles.leftChatContainer; messageBoxContainerStyle.push(positioningStyle); let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { let deliveryIconName; let deliveryIconColor = `#${item.threadInfo.color}`; if (id !== null && id !== undefined) { deliveryIconName = 'check-circle'; } else if (sendFailed) { deliveryIconName = 'x-circle'; deliveryIconColor = colors.redText; failedSendInfo = ; } else { deliveryIconName = 'circle'; } const animatedStyle: AnimatedStyleObj = { opacity: deliveryIconOpacity }; deliveryIcon = ( ); } const triggerReply = swipeOptions === 'reply' || swipeOptions === 'both' ? this.reply : undefined; const triggerSidebar = swipeOptions === 'sidebar' || swipeOptions === 'both' ? navigateToSidebar : undefined; let avatar; - if (!isViewer && item.endsCluster && shouldRenderAvatars) { + if (!isViewer && item.endsCluster) { avatar = ( ); - } else if (!isViewer && shouldRenderAvatars) { + } else if (!isViewer) { avatar = ; } const pinIconPositioning = isViewer ? 'left' : 'right'; const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin'; const messageBoxTopLevelContainerStyle = pinIconPositioning === 'left' ? styles.rightMessageBoxTopLevelContainerStyle : styles.leftMessageBoxTopLevelContainerStyle; let pinIcon; if (isPinned && shouldDisplayPinIndicator) { pinIcon = ( ); } const messageBoxStyle = { opacity: contentAndHeaderOpacity, maxWidth: composedMessageMaxWidth, }; const messageBox = ( {pinIcon} {avatar} {children} ); let inlineEngagement = null; const label = getMessageLabel(hasBeenEdited, item.threadInfo); if ( item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0 || label ) { const positioning = isViewer ? 'right' : 'left'; inlineEngagement = ( ); } return ( {deliveryIcon} {messageBox} {failedSendInfo} {inlineEngagement} ); } reply = () => { const { inputState, item } = this.props; invariant(inputState, 'inputState should be set in reply'); invariant(item.messageInfo.text, 'text should be set in reply'); inputState.editInputMessage({ message: createMessageReply(item.messageInfo.text), mode: 'prepend', }); }; } const styles = StyleSheet.create({ alignment: { marginLeft: composedMessageStyle.marginLeft, marginRight: composedMessageStyle.marginRight, }, avatarContainer: { marginRight: 8, }, avatarOffset: { width: avatarOffset, }, content: { alignItems: 'center', flexDirection: 'row-reverse', }, icon: { fontSize: 16, textAlign: 'center', }, iconContainer: { marginLeft: 2, width: 16, }, leftChatBubble: { justifyContent: 'flex-end', }, leftChatContainer: { alignItems: 'flex-start', }, leftMessageBoxTopLevelContainerStyle: { flexDirection: 'row-reverse', }, messageBoxContainer: { marginRight: 5, }, pinIconContainer: { marginRight: 4, marginTop: 4, }, rightChatBubble: { justifyContent: 'flex-start', }, rightChatContainer: { alignItems: 'flex-end', }, rightMessageBoxTopLevelContainerStyle: { flexDirection: 'row', }, swipeableContainer: { alignItems: 'flex-end', flexDirection: 'row', }, }); const ConnectedComposedMessage: React.ComponentType = React.memo(function ConnectedComposedMessage(props: BaseProps) { const composedMessageMaxWidth = useComposedMessageMaxWidth(); const colors = useColors(); const inputState = React.useContext(InputStateContext); const navigateToSidebar = useNavigateToSidebar(props.item); const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); const deliveryIconOpacity = useDeliveryIconOpacity(props.item); - const shouldRenderAvatars = useShouldRenderAvatars(); const progress = useDerivedValue(() => { const isThisThread = inputState?.editState.editedMessage?.threadID === props.item.threadInfo.id; const isHighlighted = inputState?.editState.editedMessage?.id === props.item.messageInfo.id && isThisThread; return withTiming(isHighlighted ? 1 : 0); }); const editedMessageStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( progress.value, [0, 1], ['transparent', `#${props.item.threadInfo.color}40`], ); return { backgroundColor, }; }); return ( ); }); export default ConnectedComposedMessage; diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index 4b0c51e7b..44d410ef2 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,349 +1,339 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import Animated, { Extrapolate, interpolateNode, } from 'react-native-reanimated'; import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { stringForReactionList } from 'lib/shared/reaction-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { inlineEngagementLabelStyle, inlineEngagementStyle, inlineEngagementCenterStyle, inlineEngagementRightStyle, inlineEngagementLeftStyle, composedMessageStyle, avatarOffset, } from './chat-constants.js'; import { useNavigateToThread } from './message-list-types.js'; import CommIcon from '../components/comm-icon.react.js'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js'; import { MessageReactionsModalRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +disabled?: boolean, +positioning?: 'left' | 'right', +label?: ?string, - +shouldRenderAvatars?: boolean, }; function InlineEngagement(props: Props): React.Node { - const { - disabled = false, - reactions, - threadInfo, - positioning, - shouldRenderAvatars, - label, - } = props; + const { disabled = false, reactions, threadInfo, positioning, label } = props; const repliesText = useInlineEngagementText(threadInfo); const navigateToThread = useNavigateToThread(); const { navigate } = useNavigation(); const styles = useStyles(unboundStyles); const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null; const repliesStyles = React.useMemo( () => [styles.repliesText, unreadStyle], [styles.repliesText, unreadStyle], ); const onPressThread = React.useCallback(() => { if (threadInfo && !disabled) { navigateToThread({ threadInfo }); } }, [disabled, navigateToThread, threadInfo]); const sidebarItem = React.useMemo(() => { if (!threadInfo) { return null; } return ( {repliesText} ); }, [ threadInfo, onPressThread, styles.sidebar, styles.icon, repliesStyles, repliesText, ]); const onPressReactions = React.useCallback(() => { navigate<'MessageReactionsModal'>({ name: MessageReactionsModalRouteName, params: { reactions }, }); }, [navigate, reactions]); const marginLeft = React.useMemo( () => (sidebarItem ? styles.reactionMarginLeft : null), [sidebarItem, styles.reactionMarginLeft], ); const reactionList = React.useMemo(() => { if (!reactions || Object.keys(reactions).length === 0) { return null; } const reactionText = stringForReactionList(reactions); const reactionItems = {reactionText}; return ( {reactionItems} ); }, [ marginLeft, onPressReactions, reactions, styles.reaction, styles.reactionsContainer, ]); const isLeft = positioning === 'left'; const editedLabel = React.useMemo(() => { if (!label) { return null; } const labelLeftRight = isLeft ? styles.messageLabelLeft : styles.messageLabelRight; return {label}; }, [isLeft, label, styles]); const container = React.useMemo(() => { if (!sidebarItem && !reactionList) { return null; } return ( {sidebarItem} {reactionList} ); }, [reactionList, sidebarItem, styles.container]); const inlineEngagementPositionStyle = [styles.inlineEngagement]; if (isLeft) { inlineEngagementPositionStyle.push(styles.leftInlineEngagement); } else { inlineEngagementPositionStyle.push(styles.rightInlineEngagement); } - if (shouldRenderAvatars) { - inlineEngagementPositionStyle.push({ marginLeft: avatarOffset }); - } let body; if (isLeft) { body = ( <> {editedLabel} {container} ); } else { body = ( <> {container} {editedLabel} ); } return {body}; } const unboundStyles = { container: { flexDirection: 'row', height: inlineEngagementStyle.height, borderRadius: 16, backgroundColor: 'inlineEngagementBackground', alignSelf: 'baseline', alignItems: 'center', padding: 8, }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, rightInlineEngagement: { alignSelf: 'flex-end', position: 'relative', right: inlineEngagementRightStyle.marginRight, top: inlineEngagementRightStyle.topOffset, }, leftInlineEngagement: { justifyContent: 'flex-start', position: 'relative', top: inlineEngagementLeftStyle.topOffset, }, sidebar: { flexDirection: 'row', alignItems: 'center', }, inlineEngagement: { flexDirection: 'row', marginBottom: inlineEngagementStyle.marginBottom, marginTop: inlineEngagementStyle.marginTop, alignItems: 'center', + marginLeft: avatarOffset, }, icon: { color: 'inlineEngagementLabel', marginRight: 4, }, repliesText: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reaction: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reactionMarginLeft: { marginLeft: 12, }, reactionsContainer: { display: 'flex', flexDirection: 'row', alignItems: 'center', }, messageLabel: { color: 'messageLabel', paddingHorizontal: 3, fontSize: 13, top: inlineEngagementLabelStyle.topOffset, height: inlineEngagementLabelStyle.height, }, messageLabelLeft: { marginLeft: 9, marginRight: 4, }, messageLabelRight: { marginRight: 10, marginLeft: 4, }, avatarOffset: { width: avatarOffset, }, }; type TooltipInlineEngagementProps = { +item: ChatMessageInfoItemWithHeight, +isOpeningSidebar: boolean, +progress: Animated.Node, +windowWidth: number, +positioning: 'left' | 'right' | 'center', +initialCoordinates: { +x: number, +y: number, +width: number, +height: number, }, }; function TooltipInlineEngagement( props: TooltipInlineEngagementProps, ): React.Node { const { item, isOpeningSidebar, progress, windowWidth, initialCoordinates, positioning, } = props; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return const inlineEngagementStyles = React.useMemo(() => { if (positioning === 'left') { return { position: 'absolute', top: inlineEngagementStyle.marginTop + inlineEngagementLeftStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: inlineEngagementRightStyle.marginRight + composedMessageStyle.marginRight, top: inlineEngagementStyle.marginTop + inlineEngagementRightStyle.topOffset, }; } else if (positioning === 'center') { return { alignSelf: 'center', top: inlineEngagementCenterStyle.topOffset, }; } invariant( false, `${positioning} is not a valid positioning value for InlineEngagement`, ); }, [positioning]); const inlineEngagementContainer = React.useMemo(() => { const opacity = isOpeningSidebar ? 0 : interpolateNode(progress, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }); return { position: 'absolute', width: windowWidth, top: initialCoordinates.height, left: -initialCoordinates.x, opacity, }; }, [ initialCoordinates.height, initialCoordinates.x, isOpeningSidebar, progress, windowWidth, ]); return ( ); } export { InlineEngagement, TooltipInlineEngagement }; diff --git a/native/chat/message-header.react.js b/native/chat/message-header.react.js index 9b1acc7a9..095f4a3b1 100644 --- a/native/chat/message-header.react.js +++ b/native/chat/message-header.react.js @@ -1,105 +1,97 @@ // @flow import { useRoute } from '@react-navigation/native'; import * as React from 'react'; import { View } from 'react-native'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { clusterEndHeight, avatarOffset } from './chat-constants.js'; import type { DisplayType } from './timestamp.react.js'; import { Timestamp, timestampHeight } from './timestamp.react.js'; import { SingleLine } from '../components/single-line.react.js'; import { MessageListRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +item: ChatMessageInfoItemWithHeight, +focused: boolean, +display: DisplayType, }; function MessageHeader(props: Props): React.Node { const styles = useStyles(unboundStyles); const { item, focused, display } = props; const { creator, time } = item.messageInfo; const { isViewer } = creator; const route = useRoute(); const modalDisplay = display === 'modal'; const shouldShowUsername = !isViewer && (modalDisplay || item.startsCluster); const stringForUser = useStringForUser(shouldShowUsername ? creator : null); - const shouldRenderAvatars = useShouldRenderAvatars(); - let authorName = null; if (stringForUser) { const style = [styles.authorName]; if (modalDisplay) { style.push(styles.modal); } - if (shouldRenderAvatars) { - style.push({ marginLeft: 12 + avatarOffset }); - } else { - style.push({ marginLeft: 12 }); - } - authorName = {stringForUser}; } // We only want to render the top-placed timestamp for a message if it's // rendered in the message list, and not any separate screens (i.e. // the MessageResultsScreen). const presentedFromMessageList = typeof route.params?.presentedFrom === 'string' && route.params.presentedFrom.startsWith(MessageListRouteName); const messageInMessageList = route.name === MessageListRouteName || presentedFromMessageList; const timestamp = messageInMessageList && (modalDisplay || item.startsConversation) ? ( ) : null; let style = null; if (focused && !modalDisplay) { let topMargin = 0; if (!item.startsCluster && !item.messageInfo.creator.isViewer) { topMargin += authorNameHeight + clusterEndHeight; } if (!item.startsConversation) { topMargin += timestampHeight; } style = { marginTop: topMargin }; } return ( {timestamp} {authorName} ); } const authorNameHeight = 25; const unboundStyles = { authorName: { bottom: 0, color: 'listBackgroundSecondaryLabel', fontSize: 14, height: authorNameHeight, marginRight: 7, paddingHorizontal: 12, paddingVertical: 4, + marginLeft: 12 + avatarOffset, }, modal: { // high contrast framed against OverlayNavigator-dimmed background color: 'white', }, }; export { MessageHeader, authorNameHeight }; diff --git a/native/chat/message-list-header-title.react.js b/native/chat/message-list-header-title.react.js index de10bd5c8..6c3162236 100644 --- a/native/chat/message-list-header-title.react.js +++ b/native/chat/message-list-header-title.react.js @@ -1,119 +1,107 @@ // @flow import { HeaderTitle, type HeaderTitleInputProps, } from '@react-navigation/elements'; import * as React from 'react'; import { View } from 'react-native'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { firstLine } from 'lib/utils/string-utils.js'; import type { ChatNavigationProp } from './chat.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import Button from '../components/button.react.js'; import { ThreadSettingsRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type BaseProps = { +threadInfo: ThreadInfo, +navigate: $PropertyType, 'navigate'>, +isSearchEmpty: boolean, +areSettingsEnabled: boolean, ...HeaderTitleInputProps, }; type Props = { ...BaseProps, +styles: typeof unboundStyles, +title: string, - +shouldRenderAvatars: boolean, }; class MessageListHeaderTitle extends React.PureComponent { render() { const { threadInfo, navigate, isSearchEmpty, areSettingsEnabled, styles, title, - shouldRenderAvatars, ...rest } = this.props; let avatar; - if (!isSearchEmpty && shouldRenderAvatars) { + if (!isSearchEmpty) { avatar = ( ); } return ( ); } onPress = () => { const { threadInfo } = this.props; this.props.navigate<'ThreadSettings'>({ name: ThreadSettingsRouteName, params: { threadInfo }, key: `${ThreadSettingsRouteName}${threadInfo.id}`, }); }; } const unboundStyles = { avatarContainer: { marginRight: 8, }, button: { flex: 1, }, container: { flex: 1, flexDirection: 'row', alignItems: 'center', }, }; const ConnectedMessageListHeaderTitle: React.ComponentType = React.memo(function ConnectedMessageListHeaderTitle( props: BaseProps, ) { const styles = useStyles(unboundStyles); - const shouldRenderAvatars = useShouldRenderAvatars(); - const { uiName } = useResolvedThreadInfo(props.threadInfo); const { isSearchEmpty } = props; const title = isSearchEmpty ? 'New Message' : uiName; - return ( - - ); + return ; }); export default ConnectedMessageListHeaderTitle; diff --git a/native/chat/message-reactions-modal.react.js b/native/chat/message-reactions-modal.react.js index f71f0deca..91c7767e3 100644 --- a/native/chat/message-reactions-modal.react.js +++ b/native/chat/message-reactions-modal.react.js @@ -1,162 +1,151 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { View, Text, FlatList, TouchableHighlight } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { useMessageReactionsList } from 'lib/shared/reaction-utils.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import Modal from '../components/modal.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; export type MessageReactionsModalParams = { +reactions: ReactionInfo, }; type Props = { +navigation: RootNavigationProp<'MessageReactionsModal'>, +route: NavigationRoute<'MessageReactionsModal'>, }; function MessageReactionsModal(props: Props): React.Node { const { reactions } = props.route.params; const styles = useStyles(unboundStyles); const colors = useColors(); const navigation = useNavigation(); const modalSafeAreaEdges = React.useMemo(() => ['top'], []); const modalContainerSafeAreaEdges = React.useMemo(() => ['bottom'], []); const close = React.useCallback(() => navigation.goBack(), [navigation]); const reactionsListData = useMessageReactionsList(reactions); - const shouldRenderAvatars = useShouldRenderAvatars(); - const marginLeftStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [shouldRenderAvatars], - ); - const renderItem = React.useCallback( ({ item }) => ( - - {item.username} - + {item.username} {item.reaction} ), [ - marginLeftStyle, styles.reactionsListReactionText, styles.reactionsListRowContainer, styles.reactionsListUserInfoContainer, styles.reactionsListUsernameText, ], ); const itemSeperator = React.useCallback(() => { return ; }, [styles.reactionsListItemSeperator]); return ( Reactions ); } const unboundStyles = { modalStyle: { // we need to set each margin property explicitly to override marginLeft: 0, marginRight: 0, marginBottom: 0, marginTop: 0, justifyContent: 'flex-end', flex: 0, borderWidth: 0, borderTopLeftRadius: 10, borderTopRightRadius: 10, }, modalContainerStyle: { justifyContent: 'flex-end', }, modalContentContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24, marginTop: 8, }, reactionsListContentContainer: { paddingBottom: 16, }, reactionsListTitleText: { color: 'modalForegroundLabel', fontSize: 18, }, reactionsListRowContainer: { flexDirection: 'row', justifyContent: 'space-between', }, reactionsListUserInfoContainer: { flex: 1, flexDirection: 'row', alignItems: 'center', }, reactionsListUsernameText: { color: 'modalForegroundLabel', fontSize: 18, + marginLeft: 8, }, reactionsListReactionText: { fontSize: 18, }, reactionsListItemSeperator: { height: 16, }, closeButton: { borderRadius: 4, width: 18, height: 18, alignItems: 'center', }, closeIcon: { color: 'modalBackgroundSecondaryLabel', }, }; export default MessageReactionsModal; diff --git a/native/chat/message-tooltip-button-avatar.react.js b/native/chat/message-tooltip-button-avatar.react.js index f0e97d521..d20452c4e 100644 --- a/native/chat/message-tooltip-button-avatar.react.js +++ b/native/chat/message-tooltip-button-avatar.react.js @@ -1,41 +1,38 @@ // @flow import * as React from 'react'; import { View, StyleSheet } from 'react-native'; import { avatarOffset } from './chat-constants.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type Props = { +item: ChatMessageInfoItemWithHeight, }; function MessageTooltipButtonAvatar(props: Props): React.Node { const { item } = props; - const shouldRenderAvatars = useShouldRenderAvatars(); - - if (item.messageInfo.creator.isViewer || !shouldRenderAvatars) { + if (item.messageInfo.creator.isViewer) { return null; } return ( ); } const styles = StyleSheet.create({ avatarContainer: { bottom: 0, left: -avatarOffset, position: 'absolute', }, }); const MemoizedMessageTooltipButtonAvatar: React.ComponentType = React.memo(MessageTooltipButtonAvatar); export default MemoizedMessageTooltipButtonAvatar; diff --git a/native/chat/settings/thread-settings-child-thread.react.js b/native/chat/settings/thread-settings-child-thread.react.js index cce5255bd..5e6a0b053 100644 --- a/native/chat/settings/thread-settings-child-thread.react.js +++ b/native/chat/settings/thread-settings-child-thread.react.js @@ -1,95 +1,83 @@ // @flow import * as React from 'react'; import { View, Platform } from 'react-native'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import ThreadAvatar from '../../avatars/thread-avatar.react.js'; import Button from '../../components/button.react.js'; import ThreadIcon from '../../components/thread-icon.react.js'; import ThreadPill from '../../components/thread-pill.react.js'; import { useColors, useStyles } from '../../themes/colors.js'; -import { useShouldRenderAvatars } from '../../utils/avatar-utils.js'; import { useNavigateToThread } from '../message-list-types.js'; type Props = { +threadInfo: ThreadInfo, +firstListItem: boolean, +lastListItem: boolean, }; function ThreadSettingsChildThread(props: Props): React.Node { const { threadInfo } = props; const navigateToThread = useNavigateToThread(); const onPress = React.useCallback(() => { navigateToThread({ threadInfo }); }, [threadInfo, navigateToThread]); const styles = useStyles(unboundStyles); const colors = useColors(); - const shouldRenderAvatars = useShouldRenderAvatars(); - - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return null; - } - return ( - - - - ); - }, [shouldRenderAvatars, styles.avatarContainer, threadInfo]); - const firstItem = props.firstListItem ? null : styles.topBorder; const lastItem = props.lastListItem ? styles.lastButton : null; return ( ); } const unboundStyles = { avatarContainer: { marginRight: 8, }, button: { flex: 1, flexDirection: 'row', paddingVertical: 8, paddingLeft: 12, paddingRight: 10, alignItems: 'center', }, topBorder: { borderColor: 'panelForegroundBorder', borderTopWidth: 1, }, container: { backgroundColor: 'panelForeground', flex: 1, paddingHorizontal: 12, }, lastButton: { paddingBottom: Platform.OS === 'ios' ? 12 : 10, paddingTop: 8, }, leftSide: { flex: 1, flexDirection: 'row', alignItems: 'center', }, }; export default ThreadSettingsChildThread; diff --git a/native/chat/settings/thread-settings-member.react.js b/native/chat/settings/thread-settings-member.react.js index e281a3c16..9d49c89c4 100644 --- a/native/chat/settings/thread-settings-member.react.js +++ b/native/chat/settings/thread-settings-member.react.js @@ -1,304 +1,291 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform, ActivityIndicator, TouchableOpacity, } from 'react-native'; import { removeUsersFromThreadActionTypes, changeThreadMemberRolesActionTypes, } from 'lib/actions/thread-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { getAvailableThreadMemberActions } from 'lib/shared/thread-utils.js'; import { stringForUser } from 'lib/shared/user-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { type ThreadInfo, type RelativeMemberInfo, } from 'lib/types/thread-types.js'; import type { ThreadSettingsNavigate } from './thread-settings.react.js'; import UserAvatar from '../../avatars/user-avatar.react.js'; import PencilIcon from '../../components/pencil-icon.react.js'; import { SingleLine } from '../../components/single-line.react.js'; import { type KeyboardState, KeyboardContext, } from '../../keyboard/keyboard-state.js'; import { OverlayContext, type OverlayContextType, } from '../../navigation/overlay-context.js'; import { ThreadSettingsMemberTooltipModalRouteName } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; import type { VerticalBounds } from '../../types/layout-types.js'; -import { useShouldRenderAvatars } from '../../utils/avatar-utils.js'; type BaseProps = { +memberInfo: RelativeMemberInfo, +threadInfo: ThreadInfo, +canEdit: boolean, +navigate: ThreadSettingsNavigate, +firstListItem: boolean, +lastListItem: boolean, +verticalBounds: ?VerticalBounds, +threadSettingsRouteKey: string, }; type Props = { ...BaseProps, // Redux state +removeUserLoadingStatus: LoadingStatus, +changeRoleLoadingStatus: LoadingStatus, +colors: Colors, +styles: typeof unboundStyles, // withKeyboardState +keyboardState: ?KeyboardState, // withOverlayContext +overlayContext: ?OverlayContextType, - +shouldRenderAvatars: boolean, }; class ThreadSettingsMember extends React.PureComponent { editButton: ?React.ElementRef; render() { const userText = stringForUser(this.props.memberInfo); - const marginLeftStyle = { - marginLeft: this.props.shouldRenderAvatars ? 8 : 0, - }; - let usernameInfo = null; if (this.props.memberInfo.username) { usernameInfo = ( - - {userText} - + {userText} ); } else { usernameInfo = ( {userText} ); } let editButton = null; if ( this.props.removeUserLoadingStatus === 'loading' || this.props.changeRoleLoadingStatus === 'loading' ) { editButton = ( ); } else if ( getAvailableThreadMemberActions( this.props.memberInfo, this.props.threadInfo, this.props.canEdit, ).length !== 0 ) { editButton = ( ); } const roleName = this.props.memberInfo.role && this.props.threadInfo.roles[this.props.memberInfo.role].name; const roleInfo = ( {roleName} ); const firstItem = this.props.firstListItem ? null : this.props.styles.topBorder; const lastItem = this.props.lastListItem ? this.props.styles.lastInnerContainer : null; return ( {usernameInfo} {editButton} {roleInfo} ); } editButtonRef = (editButton: ?React.ElementRef) => { this.editButton = editButton; }; onEditButtonLayout = () => {}; onPressEdit = () => { if (this.dismissKeyboardIfShowing()) { return; } const { editButton, props: { verticalBounds }, } = this; if (!editButton || !verticalBounds) { return; } const { overlayContext } = this.props; invariant( overlayContext, 'ThreadSettingsMember should have OverlayContext', ); overlayContext.setScrollBlockingModalStatus('open'); editButton.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; this.props.navigate<'ThreadSettingsMemberTooltipModal'>({ name: ThreadSettingsMemberTooltipModalRouteName, params: { presentedFrom: this.props.threadSettingsRouteKey, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs: getAvailableThreadMemberActions( this.props.memberInfo, this.props.threadInfo, this.props.canEdit, ), memberInfo: this.props.memberInfo, threadInfo: this.props.threadInfo, }, }); }); }; dismissKeyboardIfShowing = () => { const { keyboardState } = this.props; return !!(keyboardState && keyboardState.dismissKeyboardIfShowing()); }; } const unboundStyles = { anonymous: { color: 'panelForegroundTertiaryLabel', fontStyle: 'italic', }, container: { backgroundColor: 'panelForeground', flex: 1, paddingHorizontal: 12, }, editButton: { paddingLeft: 10, }, topBorder: { borderColor: 'panelForegroundBorder', borderTopWidth: 1, }, innerContainer: { flex: 1, paddingHorizontal: 12, paddingVertical: 8, }, lastInnerContainer: { paddingBottom: Platform.OS === 'ios' ? 12 : 10, }, role: { color: 'panelForegroundTertiaryLabel', flex: 1, fontSize: 14, paddingTop: 4, }, row: { flex: 1, flexDirection: 'row', }, userInfoContainer: { flex: 1, flexDirection: 'row', alignItems: 'center', }, username: { color: 'panelForegroundSecondaryLabel', flex: 1, fontSize: 16, lineHeight: 20, + marginLeft: 8, }, }; const ConnectedThreadSettingsMember: React.ComponentType = React.memo(function ConnectedThreadSettingsMember( props: BaseProps, ) { const memberID = props.memberInfo.id; const removeUserLoadingStatus = useSelector(state => createLoadingStatusSelector( removeUsersFromThreadActionTypes, `${removeUsersFromThreadActionTypes.started}:${memberID}`, )(state), ); const changeRoleLoadingStatus = useSelector(state => createLoadingStatusSelector( changeThreadMemberRolesActionTypes, `${changeThreadMemberRolesActionTypes.started}:${memberID}`, )(state), ); const [memberInfo] = useENSNames([props.memberInfo]); const colors = useColors(); const styles = useStyles(unboundStyles); const keyboardState = React.useContext(KeyboardContext); const overlayContext = React.useContext(OverlayContext); - const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedThreadSettingsMember; diff --git a/native/chat/settings/thread-settings-parent.react.js b/native/chat/settings/thread-settings-parent.react.js index 07334fc08..bad3f1e32 100644 --- a/native/chat/settings/thread-settings-parent.react.js +++ b/native/chat/settings/thread-settings-parent.react.js @@ -1,128 +1,115 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import ThreadAvatar from '../../avatars/thread-avatar.react.js'; import Button from '../../components/button.react.js'; import ThreadPill from '../../components/thread-pill.react.js'; import { useStyles } from '../../themes/colors.js'; -import { useShouldRenderAvatars } from '../../utils/avatar-utils.js'; import { useNavigateToThread } from '../message-list-types.js'; type ParentButtonProps = { +parentThreadInfo: ThreadInfo, }; function ParentButton(props: ParentButtonProps): React.Node { const styles = useStyles(unboundStyles); const navigateToThread = useNavigateToThread(); const onPressParentThread = React.useCallback(() => { navigateToThread({ threadInfo: props.parentThreadInfo }); }, [props.parentThreadInfo, navigateToThread]); - const shouldRenderAvatars = useShouldRenderAvatars(); - - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return null; - } - - return ( + return ( + ); } type ThreadSettingsParentProps = { +threadInfo: ThreadInfo, +parentThreadInfo: ?ThreadInfo, }; function ThreadSettingsParent(props: ThreadSettingsParentProps): React.Node { const { threadInfo, parentThreadInfo } = props; const styles = useStyles(unboundStyles); let parent; if (parentThreadInfo) { parent = ; } else if (threadInfo.parentThreadID) { parent = ( Secret parent ); } else { parent = ( No parent ); } return ( Parent {parent} ); } const unboundStyles = { avatarContainer: { marginRight: 8, }, currentValue: { flex: 1, }, currentValueText: { color: 'panelForegroundSecondaryLabel', fontFamily: 'Arial', fontSize: 16, margin: 0, paddingRight: 0, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, width: 96, }, noParent: { fontStyle: 'italic', paddingLeft: 2, }, parentContainer: { flexDirection: 'row', }, row: { backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 4, alignItems: 'center', }, }; const ConnectedThreadSettingsParent: React.ComponentType = React.memo(ThreadSettingsParent); export default ConnectedThreadSettingsParent; diff --git a/native/chat/settings/thread-settings.react.js b/native/chat/settings/thread-settings.react.js index a495b0c3e..79a27b399 100644 --- a/native/chat/settings/thread-settings.react.js +++ b/native/chat/settings/thread-settings.react.js @@ -1,1265 +1,1259 @@ // @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 { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadInfo, type ResolvedThreadInfo, type RelativeMemberInfo, } 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 ThreadSettingsAvatar from './thread-settings-avatar.react.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 { useShouldRenderAvatars } from '../../utils/avatar-utils.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: 'avatar', +key: string, +threadInfo: ResolvedThreadInfo, +canChangeSettings: boolean, } | { +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, - +shouldRenderAvatars: 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 canEditThreadAvatar = threadHasPermission( threadInfo, threadPermissions.EDIT_THREAD_AVATAR, ); 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 canChangeAvatar = canEditThreadAvatar && canStartEditing; const canChangeName = canEditThreadName && canStartEditing; const canChangeDescription = canEditThreadDescription && canStartEditing; const canChangeColor = canEditThreadColor && canStartEditing; const listData: ChatSettingsItem[] = []; - if (this.props.shouldRenderAvatars) { - listData.push({ - itemType: 'header', - key: 'avatarHeader', - title: 'Channel Avatar', - categoryType: 'unpadded', - }); - listData.push({ - itemType: 'avatar', - key: 'avatar', - threadInfo, - canChangeSettings: canChangeAvatar, - }); - listData.push({ - itemType: 'footer', - key: 'avatarFooter', - categoryType: 'outline', - }); - } + listData.push({ + itemType: 'header', + key: 'avatarHeader', + title: 'Channel Avatar', + categoryType: 'unpadded', + }); + listData.push({ + itemType: 'avatar', + key: 'avatar', + threadInfo, + canChangeSettings: canChangeAvatar, + }); + listData.push({ + itemType: 'footer', + key: 'avatarFooter', + categoryType: 'outline', + }); 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, (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 } }); }); }; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return 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 === 'avatar') { 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 threadMembersChangeIsSaving = ( state: AppState, threadMembers: $ReadOnlyArray, ) => { 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 somethingIsSaving = useSelector(state => { const editNameLoadingStatus = createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:${threadID}:name`, )(state); const editColorLoadingStatus = createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:${threadID}:color`, )(state); const editDescriptionLoadingStatus = createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:${threadID}:description`, )(state); const leaveThreadLoadingStatus = createLoadingStatusSelector( leaveThreadActionTypes, `${leaveThreadActionTypes.started}:${threadID}`, )(state); const boundThreadMembersChangeIsSaving = threadMembersChangeIsSaving( state, threadMembers, ); return ( boundThreadMembersChangeIsSaving || editNameLoadingStatus === 'loading' || editColorLoadingStatus === 'loading' || editDescriptionLoadingStatus === 'loading' || leaveThreadLoadingStatus === 'loading' ); }); const { navigation } = props; React.useEffect(() => { const tabNavigation: ?TabNavigationProp<'Chat'> = navigation.getParent(); invariant(tabNavigation, 'ChatNavigator should be within TabNavigator'); const onTabPress = () => { if (navigation.isFocused() && !somethingIsSaving) { navigation.popToTop(); } }; tabNavigation.addListener('tabPress', onTabPress); return () => tabNavigation.removeListener('tabPress', onTabPress); }, [navigation, somethingIsSaving]); const styles = useStyles(unboundStyles); const indicatorStyle = useIndicatorStyle(); const overlayContext = React.useContext(OverlayContext); const keyboardState = React.useContext(KeyboardContext); const { canPromoteSidebar } = usePromoteSidebar(threadInfo); - const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedThreadSettings; diff --git a/native/chat/typeahead-tooltip.react.js b/native/chat/typeahead-tooltip.react.js index 581c839b9..612970955 100644 --- a/native/chat/typeahead-tooltip.react.js +++ b/native/chat/typeahead-tooltip.react.js @@ -1,162 +1,152 @@ // @flow import * as React from 'react'; import { Platform, Text } from 'react-native'; import { PanGestureHandler, FlatList } from 'react-native-gesture-handler'; import { type TypeaheadMatchedStrings, type Selection, getNewTextAndSelection, } from 'lib/shared/mention-utils.js'; import type { RelativeMemberInfo } from 'lib/types/thread-types.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import Button from '../components/button.react.js'; import { useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; export type TypeaheadTooltipProps = { +text: string, +matchedStrings: TypeaheadMatchedStrings, +suggestedUsers: $ReadOnlyArray, +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void, }; function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node { const { text, matchedStrings, suggestedUsers, focusAndUpdateTextAndSelection, } = props; - const shouldRenderAvatars = useShouldRenderAvatars(); - const { textBeforeAtSymbol, usernamePrefix } = matchedStrings; const styles = useStyles(unboundStyles); - const marginLeftStyle = React.useMemo( - () => ({ - marginLeft: shouldRenderAvatars ? 8 : 0, - }), - [shouldRenderAvatars], - ); - const renderTypeaheadButton = React.useCallback( ({ item }: { item: RelativeMemberInfo, ... }) => { const onPress = () => { const { newText, newSelectionStart } = getNewTextAndSelection( textBeforeAtSymbol, text, usernamePrefix, item, ); focusAndUpdateTextAndSelection(newText, { start: newSelectionStart, end: newSelectionStart, }); }; return ( ); }, [ styles.button, styles.buttonLabel, - marginLeftStyle, textBeforeAtSymbol, text, usernamePrefix, focusAndUpdateTextAndSelection, ], ); // This is a hack that was introduced due to a buggy behavior of a // absolutely positioned FlatList on Android. // There was a bug that was present when there were too few items in a // FlatList and it wasn't scrollable. It was only present on Android as // iOS has a default "bounce" animation, even if the list is too short. // The bug manifested itself when we tried to scroll the FlatList. // Because it was unscrollable we were really scrolling FlatList // below it (in the ChatList) as FlatList here has "position: absolute" // and is positioned over the other FlatList. // The hack here solves it by using a PanGestureHandler. This way Pan events // on TypeaheadTooltip FlatList are always caught by handler. // When the FlatList is scrollable it scrolls normally, because handler // passes those events down to it. // If it's not scrollable, the PanGestureHandler "swallows" them. // Normally it would trigger onGestureEvent callback, but we don't need to // handle those events. We just want them to be ignored // and that's what's actually happening. const flatList = React.useMemo( () => ( ), [ renderTypeaheadButton, styles.container, styles.contentContainer, suggestedUsers, ], ); const listWithConditionalHandler = React.useMemo(() => { if (Platform.OS === 'android') { return {flatList}; } return flatList; }, [flatList]); return listWithConditionalHandler; } const unboundStyles = { container: { position: 'absolute', maxHeight: 200, left: 0, right: 0, bottom: '100%', backgroundColor: 'typeaheadTooltipBackground', borderBottomWidth: 1, borderTopWidth: 1, borderColor: 'typeaheadTooltipBorder', borderStyle: 'solid', }, contentContainer: { padding: 8, }, button: { alignItems: 'center', flexDirection: 'row', innerHeight: 24, padding: 8, color: 'typeaheadTooltipText', }, buttonLabel: { color: 'white', fontSize: 16, fontWeight: '400', + marginLeft: 8, }, }; export default TypeaheadTooltip; diff --git a/native/components/thread-list-thread.react.js b/native/components/thread-list-thread.react.js index efd1c5c22..34be24323 100644 --- a/native/components/thread-list-thread.react.js +++ b/native/components/thread-list-thread.react.js @@ -1,98 +1,86 @@ // @flow import * as React from 'react'; import type { ThreadInfo, ResolvedThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import Button from './button.react.js'; -import ColorSplotch from './color-splotch.react.js'; import { SingleLine } from './single-line.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import { type Colors, useStyles, useColors } from '../themes/colors.js'; import type { ViewStyle, TextStyle } from '../types/styles.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type SharedProps = { +onSelect: (threadID: string) => void, +style?: ViewStyle, +textStyle?: TextStyle, }; type BaseProps = { ...SharedProps, +threadInfo: ThreadInfo, }; type Props = { ...SharedProps, +threadInfo: ResolvedThreadInfo, - +shouldRenderAvatars: boolean, +colors: Colors, +styles: typeof unboundStyles, }; class ThreadListThread extends React.PureComponent { render() { const { modalIosHighlightUnderlay: underlayColor } = this.props.colors; - let avatar; - if (this.props.shouldRenderAvatars) { - avatar = ; - } else { - avatar = ; - } - return ( ); } onSelect = () => { this.props.onSelect(this.props.threadInfo.id); }; } const unboundStyles = { button: { alignItems: 'center', flexDirection: 'row', paddingLeft: 13, }, text: { color: 'modalForegroundLabel', fontSize: 16, paddingLeft: 9, paddingRight: 12, paddingVertical: 6, }, }; const ConnectedThreadListThread: React.ComponentType = React.memo(function ConnectedThreadListThread(props: BaseProps) { const { threadInfo, ...rest } = props; const styles = useStyles(unboundStyles); const colors = useColors(); const resolvedThreadInfo = useResolvedThreadInfo(threadInfo); - const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedThreadListThread; diff --git a/native/navigation/community-drawer-item.react.js b/native/navigation/community-drawer-item.react.js index b3bf0298d..d093daa0b 100644 --- a/native/navigation/community-drawer-item.react.js +++ b/native/navigation/community-drawer-item.react.js @@ -1,155 +1,142 @@ // @flow import * as React from 'react'; import { View, TouchableOpacity } from 'react-native'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { ExpandButton, ExpandButtonDisabled } from './expand-buttons.react.js'; import SubchannelsButton from './subchannels-button.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import { SingleLine } from '../components/single-line.react.js'; import InviteLinksButton from '../invite-links/invite-links-button.react.js'; import { useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; import type { CommunityDrawerItemDataFlattened } from '../utils/drawer-utils.react.js'; export type DrawerItemProps = { +itemData: CommunityDrawerItemDataFlattened, +toggleExpanded: (threadID: string) => void, +isExpanded: boolean, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { itemData: { threadInfo, labelStyle, hasSubchannelsButton, hasChildren, itemStyle, }, navigateToThread, isExpanded, toggleExpanded, } = props; const styles = useStyles(unboundStyles); const subchannelsButton = React.useMemo(() => { if (isExpanded && hasSubchannelsButton) { return ( ); } return null; }, [isExpanded, hasSubchannelsButton, styles.subchannelsButton, threadInfo]); const onExpandToggled = React.useCallback(() => { toggleExpanded(threadInfo.id); }, [toggleExpanded, threadInfo.id]); const itemExpandButton = React.useMemo(() => { if (!hasChildren && !hasSubchannelsButton) { return ; } return ; }, [hasChildren, hasSubchannelsButton, onExpandToggled, isExpanded]); const onPress = React.useCallback(() => { navigateToThread({ threadInfo }); }, [navigateToThread, threadInfo]); const { uiName } = useResolvedThreadInfo(threadInfo); - const shouldRenderAvatars = useShouldRenderAvatars(); - - const avatar = React.useMemo(() => { - if (!shouldRenderAvatars) { - return null; - } - - return ( - - - - ); - }, [shouldRenderAvatars, styles.avatarContainer, threadInfo]); - const containerStyle = React.useMemo( () => [ styles.container, { paddingLeft: itemStyle.indentation, }, styles[itemStyle.background], ], [itemStyle.indentation, itemStyle.background, styles], ); return ( {itemExpandButton} - {avatar} + + + {uiName} {subchannelsButton} ); } const unboundStyles = { container: { paddingRight: 24, }, none: { paddingVertical: 2, }, beginning: { backgroundColor: 'drawerOpenCommunityBackground', borderTopRightRadius: 8, paddingTop: 2, }, middle: { backgroundColor: 'drawerOpenCommunityBackground', paddingRight: 24, }, end: { backgroundColor: 'drawerOpenCommunityBackground', borderBottomRightRadius: 8, paddingBottom: 2, }, avatarContainer: { marginRight: 8, }, threadEntry: { flexDirection: 'row', marginVertical: 6, }, textTouchableWrapper: { flex: 1, flexDirection: 'row', alignItems: 'center', }, subchannelsButton: { marginLeft: 24, marginBottom: 6, }, }; const MemoizedCommunityDrawerItem: React.ComponentType = React.memo(CommunityDrawerItem); export default MemoizedCommunityDrawerItem; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index 92931efd6..68ef52122 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,424 +1,411 @@ // @flow import * as React from 'react'; import { View, Text, Alert, Platform, ScrollView } from 'react-native'; import { logOutActionTypes, logOut } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import { type PreRequestUserState } from 'lib/types/session-types.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import { SingleLine } from '../components/single-line.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { EditPasswordRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, PrivacyPreferencesRouteName, DefaultNotificationsPreferencesRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, }; function ProfileRow(props: ProfileRowProps): React.Node { const { content, onPress, danger } = props; return ( ); } type BaseProps = { +navigation: ProfileNavigationProp<'ProfileScreen'>, +route: NavigationRoute<'ProfileScreen'>, }; type Props = { ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +preRequestUserState: PreRequestUserState, +logOutLoading: boolean, +colors: Colors, +styles: typeof unboundStyles, +dispatchActionPromise: DispatchActionPromise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, - +shouldRenderAvatars: boolean, }; class ProfileScreen extends React.PureComponent { get loggedOutOrLoggingOut() { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render() { let developerTools, defaultNotifications; const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( ); defaultNotifications = ( ); } let passwordEditionUI; if (accountHasPassword(this.props.currentUserInfo)) { passwordEditionUI = ( Password •••••••••••••••• ); } - let avatarSection; - if (this.props.shouldRenderAvatars) { - avatarSection = ( - <> - USER AVATAR - - - - - ); - } - return ( - {avatarSection} + USER AVATAR + + + ACCOUNT Logged in as {this.props.stringForUser} {passwordEditionUI} PREFERENCES {defaultNotifications} {developerTools} ); } onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } if (!this.props.isAccountWithPassword) { Alert.alert( 'Log out', 'Are you sure you want to log out?', [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const alertDescription = 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; logOutWithoutDeletingNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.logOut(); }; logOutAndDeleteNativeCredentialsWrapper = async () => { if (this.loggedOutOrLoggingOut) { return; } await this.deleteNativeCredentials(); this.logOut(); }; logOut() { this.props.dispatchActionPromise( logOutActionTypes, this.props.logOut(this.props.preRequestUserState), ); } async deleteNativeCredentials() { await deleteNativeCredentialsFor(); } navigateIfActive(name) { this.props.navigation.navigate({ name }); } onPressEditPassword = () => { this.navigateIfActive(EditPasswordRouteName); }; onPressDeleteAccount = () => { this.navigateIfActive(DeleteAccountRouteName); }; onPressBuildInfo = () => { this.navigateIfActive(BuildInfoRouteName); }; onPressDevTools = () => { this.navigateIfActive(DevToolsRouteName); }; onPressAppearance = () => { this.navigateIfActive(AppearancePreferencesRouteName); }; onPressPrivacy = () => { this.navigateIfActive(PrivacyPreferencesRouteName); }; onPressDefaultNotifications = () => { this.navigateIfActive(DefaultNotificationsPreferencesRouteName); }; onPressFriendList = () => { this.navigateIfActive(FriendListRouteName); }; onPressBlockList = () => { this.navigateIfActive(BlockListRouteName); }; } const unboundStyles = { avatarSection: { alignItems: 'center', paddingVertical: 16, }, container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, loggedInLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, paddedRow: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 1, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', flex: 1, }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, }; const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType = React.memo(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const preRequestUserState = useSelector(preRequestUserStateSelector); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); const callLogOut = useServerCall(logOut); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); - const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedProfileScreen; diff --git a/native/profile/relationship-list-item.react.js b/native/profile/relationship-list-item.react.js index 69c3830c8..8a88fc849 100644 --- a/native/profile/relationship-list-item.react.js +++ b/native/profile/relationship-list-item.react.js @@ -1,357 +1,349 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Alert, View, Text, TouchableOpacity, ActivityIndicator, } from 'react-native'; import { updateRelationshipsActionTypes, updateRelationships, } from 'lib/actions/relationship-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { type RelationshipRequest, type RelationshipAction, type RelationshipErrors, userRelationshipStatus, relationshipActions, } from 'lib/types/relationship-types.js'; import type { AccountUserInfo, GlobalAccountUserInfo, } from 'lib/types/user-types.js'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import type { RelationshipListNavigate } from './relationship-list.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import PencilIcon from '../components/pencil-icon.react.js'; import { SingleLine } from '../components/single-line.react.js'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state.js'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RelationshipListItemTooltipModalRouteName, FriendListRouteName, BlockListRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import type { VerticalBounds } from '../types/layout-types.js'; -import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; type BaseProps = { +userInfo: AccountUserInfo, +lastListItem: boolean, +verticalBounds: ?VerticalBounds, +relationshipListRoute: NavigationRoute<'FriendList' | 'BlockList'>, +navigate: RelationshipListNavigate, +onSelect: (selectedUser: GlobalAccountUserInfo) => void, }; type Props = { ...BaseProps, // Redux state +removeUserLoadingStatus: LoadingStatus, +colors: Colors, +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +updateRelationships: ( request: RelationshipRequest, ) => Promise, // withOverlayContext +overlayContext: ?OverlayContextType, // withKeyboardState +keyboardState: ?KeyboardState, - +shouldRenderAvatars: boolean, }; class RelationshipListItem extends React.PureComponent { editButton = React.createRef>(); render() { const { lastListItem, removeUserLoadingStatus, userInfo, relationshipListRoute, } = this.props; const relationshipsToEdit = { [FriendListRouteName]: [userRelationshipStatus.FRIEND], [BlockListRouteName]: [ userRelationshipStatus.BOTH_BLOCKED, userRelationshipStatus.BLOCKED_BY_VIEWER, ], }[relationshipListRoute.name]; const canEditFriendRequest = { [FriendListRouteName]: true, [BlockListRouteName]: false, }[relationshipListRoute.name]; const borderBottom = lastListItem ? null : this.props.styles.borderBottom; let editButton = null; if (removeUserLoadingStatus === 'loading') { editButton = ( ); } else if (relationshipsToEdit.includes(userInfo.relationshipStatus)) { editButton = ( ); } else if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED && canEditFriendRequest ) { editButton = ( Accept Reject ); } else if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT && canEditFriendRequest ) { editButton = ( Cancel request ); } else { editButton = ( Add ); } - const marginLeftStyle = { - marginLeft: this.props.shouldRenderAvatars ? 8 : 0, - }; - return ( - + {this.props.userInfo.username} {editButton} ); } onSelect = () => { const { id, username } = this.props.userInfo; this.props.onSelect({ id, username }); }; visibleEntryIDs() { const { relationshipListRoute } = this.props; const id = { [FriendListRouteName]: 'unfriend', [BlockListRouteName]: 'unblock', }[relationshipListRoute.name]; return [id]; } onPressEdit = () => { if (this.props.keyboardState?.dismissKeyboardIfShowing()) { return; } const { editButton, props: { verticalBounds }, } = this; const { overlayContext, userInfo } = this.props; invariant( overlayContext, 'RelationshipListItem should have OverlayContext', ); overlayContext.setScrollBlockingModalStatus('open'); if (!editButton.current || !verticalBounds) { return; } const { relationshipStatus, ...restUserInfo } = userInfo; const relativeUserInfo = { ...restUserInfo, isViewer: false, }; editButton.current.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; this.props.navigate<'RelationshipListItemTooltipModal'>({ name: RelationshipListItemTooltipModalRouteName, params: { presentedFrom: this.props.relationshipListRoute.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs: this.visibleEntryIDs(), relativeUserInfo, }, }); }); }; // We need to set onLayout in order to allow .measure() to be on the ref onLayout = () => {}; onPressFriendUser = () => { this.onPressUpdateFriendship(relationshipActions.FRIEND); }; onPressUnfriendUser = () => { this.onPressUpdateFriendship(relationshipActions.UNFRIEND); }; onPressUpdateFriendship(action: RelationshipAction) { const { id } = this.props.userInfo; const customKeyName = `${updateRelationshipsActionTypes.started}:${id}`; this.props.dispatchActionPromise( updateRelationshipsActionTypes, this.updateFriendship(action), { customKeyName }, ); } async updateFriendship(action: RelationshipAction) { try { return await this.props.updateRelationships({ action, userIDs: [this.props.userInfo.id], }); } catch (e) { Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], { cancelable: true, }); throw e; } } } const unboundStyles = { editButton: { paddingLeft: 10, }, container: { flex: 1, paddingHorizontal: 12, backgroundColor: 'panelForeground', }, innerContainer: { paddingVertical: 10, paddingHorizontal: 12, borderColor: 'panelForegroundBorder', flexDirection: 'row', }, borderBottom: { borderBottomWidth: 1, }, buttonContainer: { flexDirection: 'row', }, editButtonWithMargin: { marginLeft: 15, }, username: { color: 'panelForegroundSecondaryLabel', flex: 1, fontSize: 16, lineHeight: 20, marginLeft: 8, }, blueAction: { color: 'link', fontSize: 16, paddingLeft: 6, }, redAction: { color: 'redText', fontSize: 16, paddingLeft: 6, }, }; const ConnectedRelationshipListItem: React.ComponentType = React.memo(function ConnectedRelationshipListItem( props: BaseProps, ) { const removeUserLoadingStatus = useSelector(state => createLoadingStatusSelector( updateRelationshipsActionTypes, `${updateRelationshipsActionTypes.started}:${props.userInfo.id}`, )(state), ); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const boundUpdateRelationships = useServerCall(updateRelationships); const overlayContext = React.useContext(OverlayContext); const keyboardState = React.useContext(KeyboardContext); - const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedRelationshipListItem; diff --git a/native/utils/avatar-utils.js b/native/utils/avatar-utils.js deleted file mode 100644 index 006d66fcd..000000000 --- a/native/utils/avatar-utils.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow - -function useShouldRenderAvatars(): boolean { - return true; -} - -export { useShouldRenderAvatars };