Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509284
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/native/navigation/tooltip.react.js b/native/navigation/tooltip.react.js
index 76f7b6d11..dc52ab36f 100644
--- a/native/navigation/tooltip.react.js
+++ b/native/navigation/tooltip.react.js
@@ -1,621 +1,621 @@
// @flow
import type { RouteProp } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import invariant from 'invariant';
import * as React from 'react';
import {
View,
StyleSheet,
TouchableWithoutFeedback,
Platform,
TouchableOpacity,
} from 'react-native';
import Animated from 'react-native-reanimated';
import { useDispatch } from 'react-redux';
import {
type ServerCallState,
serverCallStateSelector,
} from 'lib/selectors/server-calls';
import type { Dispatch } from 'lib/types/redux-types';
import {
createBoundServerCallsSelector,
useDispatchActionPromise,
type DispatchActionPromise,
type ActionFunc,
type BindServerCall,
type DispatchFunctions,
} from 'lib/utils/action-utils';
import { ChatContext, type ChatContextType } from '../chat/chat-context';
import CommIcon from '../components/comm-icon.react';
import { SingleLine } from '../components/single-line.react';
import SWMansionIcon from '../components/swmansion-icon.react';
import { type InputState, InputStateContext } from '../input/input-state';
import { type DimensionsInfo } from '../redux/dimensions-updater.react';
import { useSelector } from '../redux/redux-utils';
import {
type VerticalBounds,
type LayoutCoordinates,
} from '../types/layout-types';
import type { LayoutEvent } from '../types/react-native';
import { AnimatedView, type ViewStyle, type TextStyle } from '../types/styles';
import type { AppNavigationProp } from './app-navigator.react';
import { OverlayContext, type OverlayContextType } from './overlay-context';
import type { TooltipModalParamList } from './route-names';
/* eslint-disable import/no-named-as-default-member */
const { Value, Node, Extrapolate, add, multiply, interpolateNode } = Animated;
/* eslint-enable import/no-named-as-default-member */
export type TooltipEntry<RouteName: $Keys<TooltipModalParamList>> = {
+id: string,
+text: string,
+onPress: (
route: TooltipRoute<RouteName>,
dispatchFunctions: DispatchFunctions,
bindServerCall: BindServerCall,
inputState: ?InputState,
navigation: AppNavigationProp<RouteName>,
viewerID: ?string,
chatContext: ?ChatContextType,
) => mixed,
};
type TooltipItemProps<RouteName> = {
+spec: TooltipEntry<RouteName>,
+onPress: (entry: TooltipEntry<RouteName>) => void,
+containerStyle?: ViewStyle,
+labelStyle?: TextStyle,
};
type TooltipSpec<RouteName> = {
+entries: $ReadOnlyArray<TooltipEntry<RouteName>>,
+labelStyle?: ViewStyle,
};
export type TooltipParams<CustomProps> = {
...CustomProps,
+presentedFrom: string,
+initialCoordinates: LayoutCoordinates,
+verticalBounds: VerticalBounds,
+location?: 'above' | 'below' | 'fixed',
+margin?: number,
+visibleEntryIDs?: $ReadOnlyArray<string>,
};
export type TooltipRoute<RouteName: $Keys<TooltipModalParamList>> = RouteProp<
TooltipModalParamList,
RouteName,
>;
export type BaseTooltipProps<RouteName> = {
+navigation: AppNavigationProp<RouteName>,
+route: TooltipRoute<RouteName>,
};
type ButtonProps<Base> = {
...Base,
+progress: Node,
+isOpeningSidebar: boolean,
};
type TooltipProps<Base> = {
...Base,
// Redux state
+dimensions: DimensionsInfo,
+serverCallState: ServerCallState,
+viewerID: ?string,
// Redux dispatch functions
+dispatch: Dispatch,
+dispatchActionPromise: DispatchActionPromise,
// withOverlayContext
+overlayContext: ?OverlayContextType,
// withInputState
+inputState: ?InputState,
+chatContext: ?ChatContextType,
};
function createTooltip<
RouteName: $Keys<TooltipModalParamList>,
BaseTooltipPropsType: BaseTooltipProps<RouteName> = BaseTooltipProps<RouteName>,
>(
ButtonComponent: React.ComponentType<ButtonProps<BaseTooltipPropsType>>,
tooltipSpec: TooltipSpec<RouteName>,
): React.ComponentType<BaseTooltipPropsType> {
class TooltipItem extends React.PureComponent<TooltipItemProps<RouteName>> {
render() {
let icon;
if (this.props.spec.id === 'copy') {
icon = <SWMansionIcon name="copy" style={styles.icon} size={16} />;
} else if (this.props.spec.id === 'reply') {
icon = <CommIcon name="reply" style={styles.icon} size={12} />;
} else if (this.props.spec.id === 'report') {
icon = (
<SWMansionIcon name="warning-circle" style={styles.icon} size={16} />
);
} else if (this.props.spec.id === 'sidebar') {
icon = (
<SWMansionIcon
name="message-circle-lines"
style={styles.icon}
size={16}
/>
);
}
return (
<TouchableOpacity
onPress={this.onPress}
style={this.props.containerStyle}
>
{icon}
<SingleLine style={[styles.label, this.props.labelStyle]}>
{this.props.spec.text}
</SingleLine>
</TouchableOpacity>
);
}
onPress = () => {
this.props.onPress(this.props.spec);
};
}
class Tooltip extends React.PureComponent<
TooltipProps<BaseTooltipPropsType>,
> {
backdropOpacity: Node;
tooltipContainerOpacity: Node;
tooltipVerticalAbove: Node;
tooltipVerticalBelow: Node;
tooltipHorizontalOffset: Value = new Value(0);
tooltipHorizontal: Node;
tooltipScale: Node;
constructor(props: TooltipProps<BaseTooltipPropsType>) {
super(props);
const { overlayContext } = props;
invariant(overlayContext, 'Tooltip should have OverlayContext');
const { position } = overlayContext;
this.backdropOpacity = interpolateNode(position, {
inputRange: [0, 1],
outputRange: [0, 0.7],
extrapolate: Extrapolate.CLAMP,
});
this.tooltipContainerOpacity = interpolateNode(position, {
inputRange: [0, 0.1],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
});
const { margin } = this;
this.tooltipVerticalAbove = interpolateNode(position, {
inputRange: [0, 1],
outputRange: [margin + this.tooltipHeight / 2, 0],
extrapolate: Extrapolate.CLAMP,
});
this.tooltipVerticalBelow = interpolateNode(position, {
inputRange: [0, 1],
outputRange: [-margin - this.tooltipHeight / 2, 0],
extrapolate: Extrapolate.CLAMP,
});
this.tooltipHorizontal = multiply(
add(1, multiply(-1, position)),
this.tooltipHorizontalOffset,
);
this.tooltipScale = interpolateNode(position, {
inputRange: [0, 0.2, 0.8, 1],
outputRange: [0, 0, 1, 1],
extrapolate: Extrapolate.CLAMP,
});
}
componentDidMount() {
Haptics.impactAsync();
}
get entries(): $ReadOnlyArray<TooltipEntry<RouteName>> {
const { entries } = tooltipSpec;
const { visibleEntryIDs } = this.props.route.params;
if (!visibleEntryIDs) {
return entries;
}
const visibleSet = new Set(visibleEntryIDs);
return entries.filter(entry => visibleSet.has(entry.id));
}
get tooltipHeight(): number {
if (this.props.route.params.location === 'fixed') {
return fixedTooltipHeight;
} else {
return tooltipHeight(this.entries.length);
}
}
get location(): 'above' | 'below' | 'fixed' {
const { params } = this.props.route;
const { location } = params;
if (location) {
return location;
}
const { initialCoordinates, verticalBounds } = params;
const { y, height } = initialCoordinates;
const contentTop = y;
const contentBottom = y + height;
const boundsTop = verticalBounds.y;
const boundsBottom = verticalBounds.y + verticalBounds.height;
const { margin, tooltipHeight: curTooltipHeight } = this;
const fullHeight = curTooltipHeight + margin;
if (
contentBottom + fullHeight > boundsBottom &&
contentTop - fullHeight > boundsTop
) {
return 'above';
}
return 'below';
}
get opacityStyle() {
return {
...styles.backdrop,
opacity: this.backdropOpacity,
};
}
get contentContainerStyle() {
const { verticalBounds } = this.props.route.params;
const fullScreenHeight = this.props.dimensions.height;
const top = verticalBounds.y;
const bottom =
fullScreenHeight - verticalBounds.y - verticalBounds.height;
return {
...styles.contentContainer,
marginTop: top,
marginBottom: bottom,
};
}
get buttonStyle() {
const { params } = this.props.route;
const { initialCoordinates, verticalBounds } = params;
const { x, y, width, height } = initialCoordinates;
return {
width: Math.ceil(width),
height: Math.ceil(height),
marginTop: y - verticalBounds.y,
marginLeft: x,
};
}
get margin() {
const customMargin = this.props.route.params.margin;
return customMargin !== null && customMargin !== undefined
? customMargin
: 20;
}
get tooltipContainerStyle() {
const { dimensions, route } = this.props;
const { initialCoordinates, verticalBounds } = route.params;
const { x, y, width, height } = initialCoordinates;
const { margin, location } = this;
const style = {};
style.position = 'absolute';
(style.alignItems = 'center'),
(style.opacity = this.tooltipContainerOpacity);
style.transform = [{ translateX: this.tooltipHorizontal }];
const extraLeftSpace = x;
const extraRightSpace = dimensions.width - width - x;
if (extraLeftSpace < extraRightSpace) {
style.left = 0;
style.minWidth = width + 2 * extraLeftSpace;
} else {
style.right = 0;
style.minWidth = width + 2 * extraRightSpace;
}
if (location === 'fixed') {
style.minWidth = dimensions.width - 16;
style.left = 8;
style.right = 8;
}
if (location === 'above') {
const fullScreenHeight = dimensions.height;
style.bottom =
fullScreenHeight - Math.max(y, verticalBounds.y) + margin;
style.transform.push({ translateY: this.tooltipVerticalAbove });
} else {
style.top =
Math.min(y + height, verticalBounds.y + verticalBounds.height) +
margin;
style.transform.push({ translateY: this.tooltipVerticalBelow });
}
style.transform.push({ scale: this.tooltipScale });
return style;
}
render() {
const {
dimensions,
serverCallState,
viewerID,
dispatch,
dispatchActionPromise,
overlayContext,
inputState,
chatContext,
...navAndRouteForFlow
} = this.props;
const tooltipContainerStyle = [styles.itemContainer];
if (this.location === 'fixed') {
tooltipContainerStyle.push(styles.itemContainerFixed);
}
const { entries } = this;
const items = entries.map((entry, index) => {
let style;
if (this.location === 'fixed') {
style = index !== entries.length - 1 ? styles.itemMarginFixed : null;
} else {
style = index !== entries.length - 1 ? styles.itemMargin : null;
}
return (
<TooltipItem
key={index}
spec={entry}
onPress={this.onPressEntry}
containerStyle={[...tooltipContainerStyle, style]}
labelStyle={tooltipSpec.labelStyle}
/>
);
});
let triangleStyle;
const { route } = this.props;
const { initialCoordinates } = route.params;
const { x, width } = initialCoordinates;
const extraLeftSpace = x;
const extraRightSpace = dimensions.width - width - x;
if (extraLeftSpace < extraRightSpace) {
triangleStyle = {
alignSelf: 'flex-start',
left: extraLeftSpace + (width - 20) / 2,
};
} else {
triangleStyle = {
alignSelf: 'flex-end',
right: extraRightSpace + (width - 20) / 2,
};
}
let triangleDown = null;
let triangleUp = null;
const { location } = this;
if (location === 'above') {
triangleDown = <View style={[styles.triangleDown, triangleStyle]} />;
} else if (location === 'below') {
triangleUp = <View style={[styles.triangleUp, triangleStyle]} />;
}
invariant(overlayContext, 'Tooltip should have OverlayContext');
const { position } = overlayContext;
const isOpeningSidebar = !!chatContext?.currentTransitionSidebarSourceID;
const buttonProps: ButtonProps<BaseTooltipPropsType> = {
...navAndRouteForFlow,
progress: position,
isOpeningSidebar,
};
const itemsStyle = [styles.items];
if (this.location === 'fixed') {
itemsStyle.push(styles.itemsFixed);
}
return (
<TouchableWithoutFeedback onPress={this.onPressBackdrop}>
<View style={styles.container}>
<AnimatedView style={this.opacityStyle} />
<View style={this.contentContainerStyle}>
<View style={this.buttonStyle}>
<ButtonComponent {...buttonProps} />
</View>
</View>
<AnimatedView
style={this.tooltipContainerStyle}
onLayout={this.onTooltipContainerLayout}
>
{triangleUp}
<View style={itemsStyle}>{items}</View>
{triangleDown}
</AnimatedView>
</View>
</TouchableWithoutFeedback>
);
}
onPressBackdrop = () => {
this.props.navigation.goBackOnce();
};
onPressEntry = (entry: TooltipEntry<RouteName>) => {
this.props.navigation.goBackOnce();
const dispatchFunctions = {
dispatch: this.props.dispatch,
dispatchActionPromise: this.props.dispatchActionPromise,
};
entry.onPress(
this.props.route,
dispatchFunctions,
this.bindServerCall,
this.props.inputState,
this.props.navigation,
this.props.viewerID,
this.props.chatContext,
);
};
bindServerCall = <F>(serverCall: ActionFunc<F>): F => {
const {
cookie,
urlPrefix,
sessionID,
currentUserInfo,
connectionStatus,
} = this.props.serverCallState;
return createBoundServerCallsSelector(serverCall)({
dispatch: this.props.dispatch,
cookie,
urlPrefix,
sessionID,
currentUserInfo,
connectionStatus,
});
};
onTooltipContainerLayout = (event: LayoutEvent) => {
const { route, dimensions } = this.props;
const { x, width } = route.params.initialCoordinates;
const extraLeftSpace = x;
const extraRightSpace = dimensions.width - width - x;
const actualWidth = event.nativeEvent.layout.width;
if (extraLeftSpace < extraRightSpace) {
const minWidth = width + 2 * extraLeftSpace;
this.tooltipHorizontalOffset.setValue((minWidth - actualWidth) / 2);
} else {
const minWidth = width + 2 * extraRightSpace;
this.tooltipHorizontalOffset.setValue((actualWidth - minWidth) / 2);
}
};
}
return React.memo<BaseTooltipPropsType>(function ConnectedTooltip(
props: BaseTooltipPropsType,
) {
const dimensions = useSelector(state => state.dimensions);
const serverCallState = useSelector(serverCallStateSelector);
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const dispatch = useDispatch();
const dispatchActionPromise = useDispatchActionPromise();
const overlayContext = React.useContext(OverlayContext);
const inputState = React.useContext(InputStateContext);
const chatContext = React.useContext(ChatContext);
return (
<Tooltip
{...props}
dimensions={dimensions}
serverCallState={serverCallState}
viewerID={viewerID}
dispatch={dispatch}
dispatchActionPromise={dispatchActionPromise}
overlayContext={overlayContext}
inputState={inputState}
chatContext={chatContext}
/>
);
});
}
const styles = StyleSheet.create({
backdrop: {
backgroundColor: 'black',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
container: {
flex: 1,
},
contentContainer: {
flex: 1,
overflow: 'hidden',
},
icon: {
- marginRight: 4,
+ color: '#FFFFFF',
},
itemContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
padding: 10,
},
itemContainerFixed: {
flexDirection: 'column',
},
itemMargin: {
- borderBottomColor: '#E1E1E1',
+ borderBottomColor: '#404040',
borderBottomWidth: 1,
},
itemMarginFixed: {
- borderRightColor: '#E1E1E1',
+ borderRightColor: '#404040',
borderRightWidth: 1,
},
items: {
- backgroundColor: 'white',
+ backgroundColor: '#1F1F1F',
borderRadius: 5,
overflow: 'hidden',
},
itemsFixed: {
flex: 1,
flexDirection: 'row',
},
label: {
- color: '#444',
+ color: '#FFFFFF',
fontSize: 14,
lineHeight: 17,
textAlign: 'center',
},
triangleDown: {
borderBottomColor: 'transparent',
borderBottomWidth: 0,
borderLeftColor: 'transparent',
borderLeftWidth: 10,
borderRightColor: 'transparent',
borderRightWidth: 10,
borderStyle: 'solid',
- borderTopColor: 'white',
+ borderTopColor: '#1F1F1F',
borderTopWidth: 10,
height: 10,
top: Platform.OS === 'android' ? -1 : 0,
width: 10,
},
triangleUp: {
- borderBottomColor: 'white',
+ borderBottomColor: '#1F1F1F',
borderBottomWidth: 10,
borderLeftColor: 'transparent',
borderLeftWidth: 10,
borderRightColor: 'transparent',
borderRightWidth: 10,
borderStyle: 'solid',
borderTopColor: 'transparent',
borderTopWidth: 0,
bottom: Platform.OS === 'android' ? -1 : 0,
height: 10,
width: 10,
},
});
function tooltipHeight(numEntries: number): number {
// 10 (triangle) + 37 * numEntries (entries) + numEntries - 1 (padding)
return 9 + 38 * numEntries;
}
const fixedTooltipHeight: number = 53;
export { createTooltip, fixedTooltipHeight };
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 3:09 AM (10 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690259
Default Alt Text
(18 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment