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 (
+