diff --git a/native/calendar/thread-picker-modal.react.js b/native/calendar/thread-picker-modal.react.js
index 90e482b34..50896aad0 100644
--- a/native/calendar/thread-picker-modal.react.js
+++ b/native/calendar/thread-picker-modal.react.js
@@ -1,99 +1,99 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { StyleSheet } from 'react-native';
import { useDispatch } from 'react-redux';
import {
createLocalEntry,
createLocalEntryActionType,
} from 'lib/actions/entry-actions';
import { threadSearchIndex } from 'lib/selectors/nav-selectors';
import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors';
import Modal from '../components/modal.react';
import ThreadList from '../components/thread-list.react';
import { RootNavigatorContext } from '../navigation/root-navigator-context';
import type { RootNavigationProp } from '../navigation/root-navigator.react';
import type { NavigationRoute } from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import { waitForInteractions } from '../utils/timers';
export type ThreadPickerModalParams = {
- presentedFrom: string,
- dateString: string,
+ +presentedFrom: string,
+ +dateString: string,
};
type Props = {
+navigation: RootNavigationProp<'ThreadPickerModal'>,
+route: NavigationRoute<'ThreadPickerModal'>,
};
function ThreadPickerModal(props: Props): React.Node {
const {
navigation,
route: {
params: { dateString },
},
} = props;
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const nextLocalID = useSelector(state => state.nextLocalID);
const dispatch = useDispatch();
const rootNavigatorContext = React.useContext(RootNavigatorContext);
const threadPicked = React.useCallback(
(threadID: string) => {
invariant(
dateString && viewerID && rootNavigatorContext,
'inputs to threadPicked should be set',
);
rootNavigatorContext.setKeyboardHandlingEnabled(false);
dispatch({
type: createLocalEntryActionType,
payload: createLocalEntry(threadID, nextLocalID, dateString, viewerID),
});
},
[rootNavigatorContext, dispatch, viewerID, nextLocalID, dateString],
);
React.useEffect(
() =>
navigation.addListener('blur', async () => {
await waitForInteractions();
invariant(
rootNavigatorContext,
'RootNavigatorContext should be set in onScreenBlur',
);
rootNavigatorContext.setKeyboardHandlingEnabled(true);
}),
[navigation, rootNavigatorContext],
);
const index = useSelector(state => threadSearchIndex(state));
const onScreenThreadInfos = useSelector(state =>
onScreenEntryEditableThreadInfos(state),
);
return (
);
}
const styles = StyleSheet.create({
threadListItem: {
paddingLeft: 10,
paddingRight: 10,
paddingVertical: 2,
},
});
export default ThreadPickerModal;
diff --git a/native/chat/settings/color-selector-modal.react.js b/native/chat/settings/color-selector-modal.react.js
index 0ccd6d3d4..cb2e2491b 100644
--- a/native/chat/settings/color-selector-modal.react.js
+++ b/native/chat/settings/color-selector-modal.react.js
@@ -1,185 +1,185 @@
// @flow
import * as React from 'react';
import { TouchableHighlight, Alert } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {
changeThreadSettingsActionTypes,
changeThreadSettings,
} from 'lib/actions/thread-actions';
import {
type ThreadInfo,
type ChangeThreadSettingsPayload,
type UpdateThreadRequest,
} from 'lib/types/thread-types';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils';
import ColorSelector from '../../components/color-selector.react';
import Modal from '../../components/modal.react';
import type { RootNavigationProp } from '../../navigation/root-navigator.react';
import type { NavigationRoute } from '../../navigation/route-names';
import { useSelector } from '../../redux/redux-utils';
import { type Colors, useStyles, useColors } from '../../themes/colors';
export type ColorSelectorModalParams = {
- presentedFrom: string,
- color: string,
- threadInfo: ThreadInfo,
- setColor: (color: string) => void,
+ +presentedFrom: string,
+ +color: string,
+ +threadInfo: ThreadInfo,
+ +setColor: (color: string) => void,
};
type BaseProps = {
+navigation: RootNavigationProp<'ColorSelectorModal'>,
+route: NavigationRoute<'ColorSelectorModal'>,
};
type Props = {
...BaseProps,
// Redux state
+colors: Colors,
+styles: typeof unboundStyles,
+windowWidth: number,
// Redux dispatch functions
+dispatchActionPromise: DispatchActionPromise,
// async functions that hit server APIs
+changeThreadSettings: (
request: UpdateThreadRequest,
) => Promise,
};
function ColorSelectorModal(props: Props): React.Node {
const {
changeThreadSettings: updateThreadSettings,
dispatchActionPromise,
windowWidth,
} = props;
const { threadInfo, setColor } = props.route.params;
const close = props.navigation.goBackOnce;
const onErrorAcknowledged = React.useCallback(() => {
setColor(threadInfo.color);
}, [setColor, threadInfo.color]);
const editColor = React.useCallback(
async (newColor: string) => {
const threadID = threadInfo.id;
try {
return await updateThreadSettings({
threadID,
changes: { color: newColor },
});
} catch (e) {
Alert.alert(
'Unknown error',
'Uhh... try again?',
[{ text: 'OK', onPress: onErrorAcknowledged }],
{ cancelable: false },
);
throw e;
}
},
[onErrorAcknowledged, threadInfo.id, updateThreadSettings],
);
const onColorSelected = React.useCallback(
(color: string) => {
const colorEditValue = color.substr(1);
setColor(colorEditValue);
close();
dispatchActionPromise(
changeThreadSettingsActionTypes,
editColor(colorEditValue),
{ customKeyName: `${changeThreadSettingsActionTypes.started}:color` },
);
},
[setColor, close, dispatchActionPromise, editColor],
);
const { colorSelectorContainer, closeButton, closeButtonIcon } = props.styles;
// Based on the assumption we are always in portrait,
// and consequently width is the lowest dimensions
const modalStyle = React.useMemo(
() => [colorSelectorContainer, { height: 0.75 * windowWidth }],
[colorSelectorContainer, windowWidth],
);
const { modalIosHighlightUnderlay } = props.colors;
const { color } = props.route.params;
return (
);
}
const unboundStyles = {
closeButton: {
borderRadius: 3,
height: 18,
position: 'absolute',
right: 5,
top: 5,
width: 18,
},
closeButtonIcon: {
color: 'modalBackgroundSecondaryLabel',
left: 3,
position: 'absolute',
},
colorSelector: {
bottom: 10,
left: 10,
position: 'absolute',
right: 10,
top: 10,
},
colorSelectorContainer: {
backgroundColor: 'modalBackground',
borderColor: 'modalForegroundBorder',
borderRadius: 5,
borderWidth: 2,
flex: 0,
marginHorizontal: 15,
marginVertical: 20,
},
};
const ConnectedColorSelectorModal: React.ComponentType = React.memo(
function ConnectedColorSelectorModal(props: BaseProps) {
const styles = useStyles(unboundStyles);
const colors = useColors();
const windowWidth = useSelector(state => state.dimensions.width);
const dispatchActionPromise = useDispatchActionPromise();
const callChangeThreadSettings = useServerCall(changeThreadSettings);
return (
);
},
);
export default ConnectedColorSelectorModal;
diff --git a/native/chat/settings/compose-subchannel-modal.react.js b/native/chat/settings/compose-subchannel-modal.react.js
index 4ff1c75ae..55ee91626 100644
--- a/native/chat/settings/compose-subchannel-modal.react.js
+++ b/native/chat/settings/compose-subchannel-modal.react.js
@@ -1,150 +1,150 @@
// @flow
import * as React from 'react';
import { Text } from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import { threadTypeDescriptions } from 'lib/shared/thread-utils';
import { type ThreadInfo, threadTypes } from 'lib/types/thread-types';
import Button from '../../components/button.react';
import Modal from '../../components/modal.react';
import SWMansionIcon from '../../components/swmansion-icon.react';
import type { RootNavigationProp } from '../../navigation/root-navigator.react';
import type { NavigationRoute } from '../../navigation/route-names';
import { ComposeSubchannelRouteName } from '../../navigation/route-names';
import { type Colors, useStyles, useColors } from '../../themes/colors';
export type ComposeSubchannelModalParams = {
- presentedFrom: string,
- threadInfo: ThreadInfo,
+ +presentedFrom: string,
+ +threadInfo: ThreadInfo,
};
type BaseProps = {
+navigation: RootNavigationProp<'ComposeSubchannelModal'>,
+route: NavigationRoute<'ComposeSubchannelModal'>,
};
type Props = {
...BaseProps,
+colors: Colors,
+styles: typeof unboundStyles,
};
class ComposeSubchannelModal extends React.PureComponent {
render() {
return (
Chat type
);
}
onPressOpen = () => {
const threadInfo = this.props.route.params.threadInfo;
this.props.navigation.navigate<'ComposeSubchannel'>({
name: ComposeSubchannelRouteName,
params: {
threadType: threadTypes.COMMUNITY_OPEN_SUBTHREAD,
parentThreadInfo: threadInfo,
},
key:
`${ComposeSubchannelRouteName}|` +
`${threadInfo.id}|${threadTypes.COMMUNITY_OPEN_SUBTHREAD}`,
});
};
onPressSecret = () => {
const threadInfo = this.props.route.params.threadInfo;
this.props.navigation.navigate<'ComposeSubchannel'>({
name: ComposeSubchannelRouteName,
params: {
threadType: threadTypes.COMMUNITY_SECRET_SUBTHREAD,
parentThreadInfo: threadInfo,
},
key:
`${ComposeSubchannelRouteName}|` +
`${threadInfo.id}|${threadTypes.COMMUNITY_SECRET_SUBTHREAD}`,
});
};
}
const unboundStyles = {
forwardIcon: {
color: 'modalForegroundSecondaryLabel',
paddingLeft: 10,
},
modal: {
flex: 0,
},
option: {
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 10,
paddingVertical: 20,
},
optionExplanation: {
color: 'modalBackgroundLabel',
flex: 1,
fontSize: 14,
paddingLeft: 20,
textAlign: 'center',
},
optionText: {
color: 'modalBackgroundLabel',
fontSize: 20,
paddingLeft: 5,
},
visibility: {
color: 'modalBackgroundLabel',
fontSize: 24,
textAlign: 'center',
},
visibilityIcon: {
color: 'modalBackgroundLabel',
paddingRight: 3,
},
};
const ConnectedComposeSubchannelModal: React.ComponentType = React.memo(
function ConnectedComposeSubchannelModal(props: BaseProps) {
const styles = useStyles(unboundStyles);
const colors = useColors();
return (
);
},
);
export default ConnectedComposeSubchannelModal;
diff --git a/native/media/image-modal.react.js b/native/media/image-modal.react.js
index 8ab9a8f20..b9132b17e 100644
--- a/native/media/image-modal.react.js
+++ b/native/media/image-modal.react.js
@@ -1,1280 +1,1280 @@
// @flow
import Clipboard from '@react-native-community/clipboard';
import invariant from 'invariant';
import * as React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Platform,
} from 'react-native';
import {
PinchGestureHandler,
PanGestureHandler,
TapGestureHandler,
State as GestureState,
} from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import Animated from 'react-native-reanimated';
import { type MediaInfo, type Dimensions } from 'lib/types/media-types';
import { useIsReportEnabled } from 'lib/utils/report-utils';
import SWMansionIcon from '../components/swmansion-icon.react';
import ConnectedStatusBar from '../connected-status-bar.react';
import { displayActionResultModal } from '../navigation/action-result-modal';
import type { AppNavigationProp } from '../navigation/app-navigator.react';
import {
OverlayContext,
type OverlayContextType,
} from '../navigation/overlay-context';
import type { NavigationRoute } from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import {
type DerivedDimensionsInfo,
derivedDimensionsInfoSelector,
} from '../selectors/dimensions-selectors';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types';
import {
type VerticalBounds,
type LayoutCoordinates,
} from '../types/layout-types';
import type { NativeMethods } from '../types/react-native';
import {
clamp,
gestureJustStarted,
gestureJustEnded,
runTiming,
} from '../utils/animation-utils';
import Multimedia from './multimedia.react';
import { intentionalSaveMedia } from './save-media';
/* eslint-disable import/no-named-as-default-member */
const {
Value,
Node,
Clock,
event,
Extrapolate,
block,
set,
call,
cond,
not,
and,
or,
eq,
neq,
greaterThan,
lessThan,
add,
sub,
multiply,
divide,
pow,
max,
min,
round,
abs,
interpolateNode,
startClock,
stopClock,
clockRunning,
decay,
} = Animated;
/* eslint-enable import/no-named-as-default-member */
function scaleDelta(value: Node, gestureActive: Node): Node {
const diffThisFrame = new Value(1);
const prevValue = new Value(1);
return cond(
gestureActive,
[
set(diffThisFrame, divide(value, prevValue)),
set(prevValue, value),
diffThisFrame,
],
set(prevValue, 1),
);
}
function panDelta(value: Node, gestureActive: Node): Node {
const diffThisFrame = new Value(0);
const prevValue = new Value(0);
return cond(
gestureActive,
[
set(diffThisFrame, sub(value, prevValue)),
set(prevValue, value),
diffThisFrame,
],
set(prevValue, 0),
);
}
function runDecay(
clock: Clock,
velocity: Node,
initialPosition: Node,
startStopClock: boolean = true,
): Node {
const state = {
finished: new Value(0),
velocity: new Value(0),
position: new Value(0),
time: new Value(0),
};
const config = { deceleration: 0.99 };
return block([
cond(not(clockRunning(clock)), [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, initialPosition),
set(state.time, 0),
startStopClock ? startClock(clock) : undefined,
]),
decay(clock, state, config),
cond(state.finished, startStopClock ? stopClock(clock) : undefined),
state.position,
]);
}
export type ImageModalParams = {
- presentedFrom: string,
- mediaInfo: MediaInfo,
- initialCoordinates: LayoutCoordinates,
- verticalBounds: VerticalBounds,
- item: ChatMultimediaMessageInfoItem,
+ +presentedFrom: string,
+ +mediaInfo: MediaInfo,
+ +initialCoordinates: LayoutCoordinates,
+ +verticalBounds: VerticalBounds,
+ +item: ChatMultimediaMessageInfoItem,
};
type TouchableOpacityInstance = React.AbstractComponent<
React.ElementConfig,
NativeMethods,
>;
type BaseProps = {
+navigation: AppNavigationProp<'ImageModal'>,
+route: NavigationRoute<'ImageModal'>,
};
type Props = {
...BaseProps,
// Redux state
+dimensions: DerivedDimensionsInfo,
// withOverlayContext
+overlayContext: ?OverlayContextType,
+mediaReportsEnabled: boolean,
};
type State = {
+closeButtonEnabled: boolean,
+actionLinksEnabled: boolean,
};
class ImageModal extends React.PureComponent {
state: State = {
closeButtonEnabled: true,
actionLinksEnabled: true,
};
closeButton: ?React.ElementRef;
mediaIconsContainer: ?React.ElementRef;
closeButtonX = new Value(-1);
closeButtonY = new Value(-1);
closeButtonWidth = new Value(0);
closeButtonHeight = new Value(0);
closeButtonLastState = new Value(1);
mediaIconsX = new Value(-1);
mediaIconsY = new Value(-1);
mediaIconsWidth = new Value(0);
mediaIconsHeight = new Value(0);
actionLinksLastState = new Value(1);
centerX: Value;
centerY: Value;
frameWidth: Value;
frameHeight: Value;
imageWidth: Value;
imageHeight: Value;
pinchHandler = React.createRef();
panHandler = React.createRef();
singleTapHandler = React.createRef();
doubleTapHandler = React.createRef();
handlerRefs = [
this.pinchHandler,
this.panHandler,
this.singleTapHandler,
this.doubleTapHandler,
];
beforeDoubleTapRefs;
beforeSingleTapRefs;
pinchEvent;
panEvent;
singleTapEvent;
doubleTapEvent;
scale: Node;
x: Node;
y: Node;
backdropOpacity: Node;
imageContainerOpacity: Node;
actionLinksOpacity: Node;
closeButtonOpacity: Node;
constructor(props: Props) {
super(props);
this.updateDimensions();
const { imageWidth, imageHeight } = this;
const left = sub(this.centerX, divide(imageWidth, 2));
const top = sub(this.centerY, divide(imageHeight, 2));
const { initialCoordinates } = props.route.params;
const initialScale = divide(initialCoordinates.width, imageWidth);
const initialTranslateX = sub(
initialCoordinates.x + initialCoordinates.width / 2,
add(left, divide(imageWidth, 2)),
);
const initialTranslateY = sub(
initialCoordinates.y + initialCoordinates.height / 2,
add(top, divide(imageHeight, 2)),
);
const { overlayContext } = props;
invariant(overlayContext, 'ImageModal should have OverlayContext');
const navigationProgress = overlayContext.position;
// The inputs we receive from PanGestureHandler
const panState = new Value(-1);
const panTranslationX = new Value(0);
const panTranslationY = new Value(0);
const panVelocityX = new Value(0);
const panVelocityY = new Value(0);
const panAbsoluteX = new Value(0);
const panAbsoluteY = new Value(0);
this.panEvent = event([
{
nativeEvent: {
state: panState,
translationX: panTranslationX,
translationY: panTranslationY,
velocityX: panVelocityX,
velocityY: panVelocityY,
absoluteX: panAbsoluteX,
absoluteY: panAbsoluteY,
},
},
]);
const curPanActive = new Value(0);
const panActive = block([
cond(
and(
gestureJustStarted(panState),
this.outsideButtons(
sub(panAbsoluteX, panTranslationX),
sub(panAbsoluteY, panTranslationY),
),
),
set(curPanActive, 1),
),
cond(gestureJustEnded(panState), set(curPanActive, 0)),
curPanActive,
]);
const lastPanActive = new Value(0);
const panJustEnded = cond(eq(lastPanActive, panActive), 0, [
set(lastPanActive, panActive),
eq(panActive, 0),
]);
// The inputs we receive from PinchGestureHandler
const pinchState = new Value(-1);
const pinchScale = new Value(1);
const pinchFocalX = new Value(0);
const pinchFocalY = new Value(0);
this.pinchEvent = event([
{
nativeEvent: {
state: pinchState,
scale: pinchScale,
focalX: pinchFocalX,
focalY: pinchFocalY,
},
},
]);
const pinchActive = eq(pinchState, GestureState.ACTIVE);
// The inputs we receive from single TapGestureHandler
const singleTapState = new Value(-1);
const singleTapX = new Value(0);
const singleTapY = new Value(0);
this.singleTapEvent = event([
{
nativeEvent: {
state: singleTapState,
x: singleTapX,
y: singleTapY,
},
},
]);
// The inputs we receive from double TapGestureHandler
const doubleTapState = new Value(-1);
const doubleTapX = new Value(0);
const doubleTapY = new Value(0);
this.doubleTapEvent = event([
{
nativeEvent: {
state: doubleTapState,
x: doubleTapX,
y: doubleTapY,
},
},
]);
// The all-important outputs
const curScale = new Value(1);
const curX = new Value(0);
const curY = new Value(0);
const curBackdropOpacity = new Value(1);
const curCloseButtonOpacity = new Value(1);
const curActionLinksOpacity = new Value(1);
// The centered variables help us know if we need to be recentered
const recenteredScale = max(curScale, 1);
const horizontalPanSpace = this.horizontalPanSpace(recenteredScale);
const verticalPanSpace = this.verticalPanSpace(recenteredScale);
const resetXClock = new Clock();
const resetYClock = new Clock();
const zoomClock = new Clock();
const dismissingFromPan = new Value(0);
const roundedCurScale = divide(round(multiply(curScale, 1000)), 1000);
const gestureActive = or(pinchActive, panActive);
const activeInteraction = or(
gestureActive,
clockRunning(zoomClock),
dismissingFromPan,
);
const updates = [
this.pinchUpdate(
pinchActive,
pinchScale,
pinchFocalX,
pinchFocalY,
curScale,
curX,
curY,
),
this.panUpdate(panActive, panTranslationX, panTranslationY, curX, curY),
this.singleTapUpdate(
singleTapState,
singleTapX,
singleTapY,
roundedCurScale,
curCloseButtonOpacity,
curActionLinksOpacity,
),
this.doubleTapUpdate(
doubleTapState,
doubleTapX,
doubleTapY,
roundedCurScale,
zoomClock,
gestureActive,
curScale,
curX,
curY,
),
this.backdropOpacityUpdate(
panJustEnded,
pinchActive,
panVelocityX,
panVelocityY,
roundedCurScale,
curX,
curY,
curBackdropOpacity,
dismissingFromPan,
),
this.recenter(
resetXClock,
resetYClock,
activeInteraction,
recenteredScale,
horizontalPanSpace,
verticalPanSpace,
curScale,
curX,
curY,
),
this.flingUpdate(
resetXClock,
resetYClock,
activeInteraction,
panJustEnded,
panVelocityX,
panVelocityY,
horizontalPanSpace,
verticalPanSpace,
curX,
curY,
),
];
const updatedScale = [updates, curScale];
const updatedCurX = [updates, curX];
const updatedCurY = [updates, curY];
const updatedBackdropOpacity = [updates, curBackdropOpacity];
const updatedCloseButtonOpacity = [updates, curCloseButtonOpacity];
const updatedActionLinksOpacity = [updates, curActionLinksOpacity];
const reverseNavigationProgress = sub(1, navigationProgress);
this.scale = add(
multiply(reverseNavigationProgress, initialScale),
multiply(navigationProgress, updatedScale),
);
this.x = add(
multiply(reverseNavigationProgress, initialTranslateX),
multiply(navigationProgress, updatedCurX),
);
this.y = add(
multiply(reverseNavigationProgress, initialTranslateY),
multiply(navigationProgress, updatedCurY),
);
this.backdropOpacity = multiply(navigationProgress, updatedBackdropOpacity);
this.imageContainerOpacity = interpolateNode(navigationProgress, {
inputRange: [0, 0.1],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
});
const buttonOpacity = interpolateNode(updatedBackdropOpacity, {
inputRange: [0.95, 1],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
});
this.closeButtonOpacity = multiply(
navigationProgress,
buttonOpacity,
updatedCloseButtonOpacity,
);
this.actionLinksOpacity = multiply(
navigationProgress,
buttonOpacity,
updatedActionLinksOpacity,
);
this.beforeDoubleTapRefs = Platform.select({
android: [],
default: [this.pinchHandler, this.panHandler],
});
this.beforeSingleTapRefs = [
...this.beforeDoubleTapRefs,
this.doubleTapHandler,
];
}
// How much space do we have to pan the image horizontally?
horizontalPanSpace(scale: Node): Node {
const apparentWidth = multiply(this.imageWidth, scale);
const horizPop = divide(sub(apparentWidth, this.frameWidth), 2);
return max(horizPop, 0);
}
// How much space do we have to pan the image vertically?
verticalPanSpace(scale: Node): Node {
const apparentHeight = multiply(this.imageHeight, scale);
const vertPop = divide(sub(apparentHeight, this.frameHeight), 2);
return max(vertPop, 0);
}
pinchUpdate(
// Inputs
pinchActive: Node,
pinchScale: Node,
pinchFocalX: Node,
pinchFocalY: Node,
// Outputs
curScale: Value,
curX: Value,
curY: Value,
): Node {
const deltaScale = scaleDelta(pinchScale, pinchActive);
const deltaPinchX = multiply(
sub(1, deltaScale),
sub(pinchFocalX, curX, this.centerX),
);
const deltaPinchY = multiply(
sub(1, deltaScale),
sub(pinchFocalY, curY, this.centerY),
);
return cond(
[deltaScale, pinchActive],
[
set(curX, add(curX, deltaPinchX)),
set(curY, add(curY, deltaPinchY)),
set(curScale, multiply(curScale, deltaScale)),
],
);
}
outsideButtons(x: Node, y: Node): Node {
const {
closeButtonX,
closeButtonY,
closeButtonWidth,
closeButtonHeight,
closeButtonLastState,
mediaIconsX,
mediaIconsY,
mediaIconsWidth,
mediaIconsHeight,
actionLinksLastState,
} = this;
return and(
or(
eq(closeButtonLastState, 0),
lessThan(x, closeButtonX),
greaterThan(x, add(closeButtonX, closeButtonWidth)),
lessThan(y, closeButtonY),
greaterThan(y, add(closeButtonY, closeButtonHeight)),
),
or(
eq(actionLinksLastState, 0),
lessThan(x, mediaIconsX),
greaterThan(x, add(mediaIconsX, mediaIconsWidth)),
lessThan(y, mediaIconsY),
greaterThan(y, add(mediaIconsY, mediaIconsHeight)),
),
);
}
panUpdate(
// Inputs
panActive: Node,
panTranslationX: Node,
panTranslationY: Node,
// Outputs
curX: Value,
curY: Value,
): Node {
const deltaX = panDelta(panTranslationX, panActive);
const deltaY = panDelta(panTranslationY, panActive);
return cond(
[deltaX, deltaY, panActive],
[set(curX, add(curX, deltaX)), set(curY, add(curY, deltaY))],
);
}
singleTapUpdate(
// Inputs
singleTapState: Node,
singleTapX: Node,
singleTapY: Node,
roundedCurScale: Node,
// Outputs
curCloseButtonOpacity: Value,
curActionLinksOpacity: Value,
): Node {
const lastTapX = new Value(0);
const lastTapY = new Value(0);
const fingerJustReleased = and(
gestureJustEnded(singleTapState),
this.outsideButtons(lastTapX, lastTapY),
);
const wasZoomed = new Value(0);
const isZoomed = greaterThan(roundedCurScale, 1);
const becameUnzoomed = and(wasZoomed, not(isZoomed));
const closeButtonState = cond(
or(
fingerJustReleased,
and(becameUnzoomed, eq(this.closeButtonLastState, 0)),
),
sub(1, this.closeButtonLastState),
this.closeButtonLastState,
);
const actionLinksState = cond(
isZoomed,
0,
cond(
or(fingerJustReleased, becameUnzoomed),
sub(1, this.actionLinksLastState),
this.actionLinksLastState,
),
);
const closeButtonAppearClock = new Clock();
const closeButtonDisappearClock = new Clock();
const actionLinksAppearClock = new Clock();
const actionLinksDisappearClock = new Clock();
return block([
fingerJustReleased,
set(
curCloseButtonOpacity,
cond(
eq(closeButtonState, 1),
[
stopClock(closeButtonDisappearClock),
runTiming(closeButtonAppearClock, curCloseButtonOpacity, 1),
],
[
stopClock(closeButtonAppearClock),
runTiming(closeButtonDisappearClock, curCloseButtonOpacity, 0),
],
),
),
set(
curActionLinksOpacity,
cond(
eq(actionLinksState, 1),
[
stopClock(actionLinksDisappearClock),
runTiming(actionLinksAppearClock, curActionLinksOpacity, 1),
],
[
stopClock(actionLinksAppearClock),
runTiming(actionLinksDisappearClock, curActionLinksOpacity, 0),
],
),
),
set(this.actionLinksLastState, actionLinksState),
set(this.closeButtonLastState, closeButtonState),
set(wasZoomed, isZoomed),
set(lastTapX, singleTapX),
set(lastTapY, singleTapY),
call([eq(curCloseButtonOpacity, 1)], this.setCloseButtonEnabled),
call([eq(curActionLinksOpacity, 1)], this.setActionLinksEnabled),
]);
}
doubleTapUpdate(
// Inputs
doubleTapState: Node,
doubleTapX: Node,
doubleTapY: Node,
roundedCurScale: Node,
zoomClock: Clock,
gestureActive: Node,
// Outputs
curScale: Value,
curX: Value,
curY: Value,
): Node {
const zoomClockRunning = clockRunning(zoomClock);
const zoomActive = and(not(gestureActive), zoomClockRunning);
const targetScale = cond(greaterThan(roundedCurScale, 1), 1, 3);
const tapXDiff = sub(doubleTapX, this.centerX, curX);
const tapYDiff = sub(doubleTapY, this.centerY, curY);
const tapXPercent = divide(tapXDiff, this.imageWidth, curScale);
const tapYPercent = divide(tapYDiff, this.imageHeight, curScale);
const horizPanSpace = this.horizontalPanSpace(targetScale);
const vertPanSpace = this.verticalPanSpace(targetScale);
const horizPanPercent = divide(horizPanSpace, this.imageWidth, targetScale);
const vertPanPercent = divide(vertPanSpace, this.imageHeight, targetScale);
const tapXPercentClamped = clamp(
tapXPercent,
multiply(-1, horizPanPercent),
horizPanPercent,
);
const tapYPercentClamped = clamp(
tapYPercent,
multiply(-1, vertPanPercent),
vertPanPercent,
);
const targetX = multiply(tapXPercentClamped, this.imageWidth, targetScale);
const targetY = multiply(tapYPercentClamped, this.imageHeight, targetScale);
const targetRelativeScale = divide(targetScale, curScale);
const targetRelativeX = multiply(-1, add(targetX, curX));
const targetRelativeY = multiply(-1, add(targetY, curY));
const zoomScale = runTiming(zoomClock, 1, targetRelativeScale);
const zoomX = runTiming(zoomClock, 0, targetRelativeX, false);
const zoomY = runTiming(zoomClock, 0, targetRelativeY, false);
const deltaScale = scaleDelta(zoomScale, zoomActive);
const deltaX = panDelta(zoomX, zoomActive);
const deltaY = panDelta(zoomY, zoomActive);
const fingerJustReleased = and(
gestureJustEnded(doubleTapState),
this.outsideButtons(doubleTapX, doubleTapY),
);
return cond(
[fingerJustReleased, deltaX, deltaY, deltaScale, gestureActive],
stopClock(zoomClock),
cond(or(zoomClockRunning, fingerJustReleased), [
zoomX,
zoomY,
zoomScale,
set(curX, add(curX, deltaX)),
set(curY, add(curY, deltaY)),
set(curScale, multiply(curScale, deltaScale)),
]),
);
}
backdropOpacityUpdate(
// Inputs
panJustEnded: Node,
pinchActive: Node,
panVelocityX: Node,
panVelocityY: Node,
roundedCurScale: Node,
// Outputs
curX: Value,
curY: Value,
curBackdropOpacity: Value,
dismissingFromPan: Value,
): Node {
const progressiveOpacity = max(
min(
sub(1, abs(divide(curX, this.frameWidth))),
sub(1, abs(divide(curY, this.frameHeight))),
),
0,
);
const resetClock = new Clock();
const velocity = pow(add(pow(panVelocityX, 2), pow(panVelocityY, 2)), 0.5);
const shouldGoBack = and(
panJustEnded,
or(greaterThan(velocity, 50), greaterThan(0.7, progressiveOpacity)),
);
const decayClock = new Clock();
const decayItems = [
set(curX, runDecay(decayClock, panVelocityX, curX, false)),
set(curY, runDecay(decayClock, panVelocityY, curY)),
];
return cond(
[panJustEnded, dismissingFromPan],
decayItems,
cond(
or(pinchActive, greaterThan(roundedCurScale, 1)),
set(curBackdropOpacity, runTiming(resetClock, curBackdropOpacity, 1)),
[
stopClock(resetClock),
set(curBackdropOpacity, progressiveOpacity),
set(dismissingFromPan, shouldGoBack),
cond(shouldGoBack, [decayItems, call([], this.close)]),
],
),
);
}
recenter(
// Inputs
resetXClock: Clock,
resetYClock: Clock,
activeInteraction: Node,
recenteredScale: Node,
horizontalPanSpace: Node,
verticalPanSpace: Node,
// Outputs
curScale: Value,
curX: Value,
curY: Value,
): Node {
const resetScaleClock = new Clock();
const recenteredX = clamp(
curX,
multiply(-1, horizontalPanSpace),
horizontalPanSpace,
);
const recenteredY = clamp(
curY,
multiply(-1, verticalPanSpace),
verticalPanSpace,
);
return cond(
activeInteraction,
[
stopClock(resetScaleClock),
stopClock(resetXClock),
stopClock(resetYClock),
],
[
cond(
or(clockRunning(resetScaleClock), neq(recenteredScale, curScale)),
set(curScale, runTiming(resetScaleClock, curScale, recenteredScale)),
),
cond(
or(clockRunning(resetXClock), neq(recenteredX, curX)),
set(curX, runTiming(resetXClock, curX, recenteredX)),
),
cond(
or(clockRunning(resetYClock), neq(recenteredY, curY)),
set(curY, runTiming(resetYClock, curY, recenteredY)),
),
],
);
}
flingUpdate(
// Inputs
resetXClock: Clock,
resetYClock: Clock,
activeInteraction: Node,
panJustEnded: Node,
panVelocityX: Node,
panVelocityY: Node,
horizontalPanSpace: Node,
verticalPanSpace: Node,
// Outputs
curX: Value,
curY: Value,
): Node {
const flingXClock = new Clock();
const flingYClock = new Clock();
const decayX = runDecay(flingXClock, panVelocityX, curX);
const recenteredX = clamp(
decayX,
multiply(-1, horizontalPanSpace),
horizontalPanSpace,
);
const decayY = runDecay(flingYClock, panVelocityY, curY);
const recenteredY = clamp(
decayY,
multiply(-1, verticalPanSpace),
verticalPanSpace,
);
return cond(
activeInteraction,
[stopClock(flingXClock), stopClock(flingYClock)],
[
cond(
clockRunning(resetXClock),
stopClock(flingXClock),
cond(or(panJustEnded, clockRunning(flingXClock)), [
set(curX, recenteredX),
cond(neq(decayX, recenteredX), stopClock(flingXClock)),
]),
),
cond(
clockRunning(resetYClock),
stopClock(flingYClock),
cond(or(panJustEnded, clockRunning(flingYClock)), [
set(curY, recenteredY),
cond(neq(decayY, recenteredY), stopClock(flingYClock)),
]),
),
],
);
}
updateDimensions() {
const { width: frameWidth, height: frameHeight } = this.frame;
const { topInset } = this.props.dimensions;
if (this.frameWidth) {
this.frameWidth.setValue(frameWidth);
} else {
this.frameWidth = new Value(frameWidth);
}
if (this.frameHeight) {
this.frameHeight.setValue(frameHeight);
} else {
this.frameHeight = new Value(frameHeight);
}
const centerX = frameWidth / 2;
const centerY = frameHeight / 2 + topInset;
if (this.centerX) {
this.centerX.setValue(centerX);
} else {
this.centerX = new Value(centerX);
}
if (this.centerY) {
this.centerY.setValue(centerY);
} else {
this.centerY = new Value(centerY);
}
const { width, height } = this.imageDimensions;
if (this.imageWidth) {
this.imageWidth.setValue(width);
} else {
this.imageWidth = new Value(width);
}
if (this.imageHeight) {
this.imageHeight.setValue(height);
} else {
this.imageHeight = new Value(height);
}
}
componentDidMount() {
if (ImageModal.isActive(this.props)) {
Orientation.unlockAllOrientations();
}
}
componentWillUnmount() {
if (ImageModal.isActive(this.props)) {
Orientation.lockToPortrait();
}
}
componentDidUpdate(prevProps: Props) {
if (this.props.dimensions !== prevProps.dimensions) {
this.updateDimensions();
}
const isActive = ImageModal.isActive(this.props);
const wasActive = ImageModal.isActive(prevProps);
if (isActive && !wasActive) {
Orientation.unlockAllOrientations();
} else if (!isActive && wasActive) {
Orientation.lockToPortrait();
}
}
get frame(): Dimensions {
const { width, safeAreaHeight } = this.props.dimensions;
return { width, height: safeAreaHeight };
}
get imageDimensions(): Dimensions {
// Make space for the close button
let { height: maxHeight, width: maxWidth } = this.frame;
if (maxHeight > maxWidth) {
maxHeight -= 100;
} else {
maxWidth -= 100;
}
const { dimensions } = this.props.route.params.mediaInfo;
if (dimensions.height < maxHeight && dimensions.width < maxWidth) {
return dimensions;
}
const heightRatio = maxHeight / dimensions.height;
const widthRatio = maxWidth / dimensions.width;
if (heightRatio < widthRatio) {
return {
height: maxHeight,
width: dimensions.width * heightRatio,
};
} else {
return {
width: maxWidth,
height: dimensions.height * widthRatio,
};
}
}
get imageContainerStyle() {
const { height, width } = this.imageDimensions;
const { height: frameHeight, width: frameWidth } = this.frame;
const top = (frameHeight - height) / 2 + this.props.dimensions.topInset;
const left = (frameWidth - width) / 2;
const { verticalBounds } = this.props.route.params;
return {
height,
width,
marginTop: top - verticalBounds.y,
marginLeft: left,
opacity: this.imageContainerOpacity,
transform: [
{ translateX: this.x },
{ translateY: this.y },
{ scale: this.scale },
],
};
}
static isActive(props) {
const { overlayContext } = props;
invariant(overlayContext, 'ImageModal should have OverlayContext');
return !overlayContext.isDismissing;
}
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;
// margin will clip, but padding won't
const verticalStyle = ImageModal.isActive(this.props)
? { paddingTop: top, paddingBottom: bottom }
: { marginTop: top, marginBottom: bottom };
return [styles.contentContainer, verticalStyle];
}
render() {
const { mediaInfo } = this.props.route.params;
const statusBar = ImageModal.isActive(this.props) ? (
) : null;
const backdropStyle = { opacity: this.backdropOpacity };
const closeButtonStyle = {
opacity: this.closeButtonOpacity,
top: Math.max(this.props.dimensions.topInset - 2, 4),
};
const mediaIconsButtonStyle = {
opacity: this.actionLinksOpacity,
bottom: this.props.dimensions.bottomInset + 8,
};
let copyButton;
if (Platform.OS === 'ios') {
copyButton = (
Copy
);
}
const view = (
{statusBar}
×
Save
{copyButton}
);
return (
{view}
);
}
close = () => {
this.props.navigation.goBackOnce();
};
save = () => {
const { mediaInfo, item } = this.props.route.params;
const { id: uploadID, uri } = mediaInfo;
const { id: messageServerID, localID: messageLocalID } = item.messageInfo;
const ids = { uploadID, messageServerID, messageLocalID };
return intentionalSaveMedia(uri, ids, {
mediaReportsEnabled: this.props.mediaReportsEnabled,
});
};
copy = () => {
const { uri } = this.props.route.params.mediaInfo;
Clipboard.setImageFromURL(uri, success => {
displayActionResultModal(success ? 'copied!' : 'failed to copy :(');
});
};
setCloseButtonEnabled = ([enabledNum]: [number]) => {
const enabled = !!enabledNum;
if (this.state.closeButtonEnabled !== enabled) {
this.setState({ closeButtonEnabled: enabled });
}
};
setActionLinksEnabled = ([enabledNum]: [number]) => {
const enabled = !!enabledNum;
if (this.state.actionLinksEnabled !== enabled) {
this.setState({ actionLinksEnabled: enabled });
}
};
closeButtonRef = (
closeButton: ?React.ElementRef,
) => {
this.closeButton = (closeButton: any);
};
mediaIconsRef = (mediaIconsContainer: ?React.ElementRef) => {
this.mediaIconsContainer = mediaIconsContainer;
};
onCloseButtonLayout = () => {
const { closeButton } = this;
if (!closeButton) {
return;
}
closeButton.measure((x, y, width, height, pageX, pageY) => {
this.closeButtonX.setValue(pageX);
this.closeButtonY.setValue(pageY);
this.closeButtonWidth.setValue(width);
this.closeButtonHeight.setValue(height);
});
};
onMediaIconsLayout = () => {
const { mediaIconsContainer } = this;
if (!mediaIconsContainer) {
return;
}
mediaIconsContainer.measure((x, y, width, height, pageX, pageY) => {
this.mediaIconsX.setValue(pageX);
this.mediaIconsY.setValue(pageY);
this.mediaIconsWidth.setValue(width);
this.mediaIconsHeight.setValue(height);
});
};
}
const styles = StyleSheet.create({
backdrop: {
backgroundColor: 'black',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
closeButton: {
color: 'white',
fontSize: 36,
paddingHorizontal: 8,
paddingVertical: 2,
textShadowColor: '#000',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 1,
},
closeButtonContainer: {
position: 'absolute',
right: 4,
},
container: {
flex: 1,
},
contentContainer: {
flex: 1,
overflow: 'hidden',
},
mediaIcon: {
color: '#D7D7DC',
fontSize: 36,
textShadowColor: '#1C1C1E',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 1,
},
mediaIconButtons: {
alignItems: 'center',
paddingBottom: 2,
paddingLeft: 8,
paddingRight: 8,
paddingTop: 2,
},
mediaIconText: {
color: '#D7D7DC',
fontSize: 14,
textShadowColor: '#1C1C1E',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 1,
},
mediaIconsContainer: {
left: 16,
position: 'absolute',
},
mediaIconsRow: {
flexDirection: 'row',
},
});
const ConnectedImageModal: React.ComponentType = React.memo(
function ConnectedImageModal(props: BaseProps) {
const dimensions = useSelector(derivedDimensionsInfoSelector);
const overlayContext = React.useContext(OverlayContext);
const mediaReportsEnabled = useIsReportEnabled('mediaReports');
return (
);
},
);
export default ConnectedImageModal;
diff --git a/native/navigation/action-result-modal.react.js b/native/navigation/action-result-modal.react.js
index cb96a9efd..db9cb8e40 100644
--- a/native/navigation/action-result-modal.react.js
+++ b/native/navigation/action-result-modal.react.js
@@ -1,80 +1,80 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text } from 'react-native';
import Animated from 'react-native-reanimated';
import { useSelector } from '../redux/redux-utils';
import { useOverlayStyles } from '../themes/colors';
import type { AppNavigationProp } from './app-navigator.react';
import { OverlayContext } from './overlay-context';
import type { NavigationRoute } from './route-names';
export type ActionResultModalParams = {
- message: string,
- preventPresses: true,
+ +message: string,
+ +preventPresses: true,
};
type Props = {
+navigation: AppNavigationProp<'ActionResultModal'>,
+route: NavigationRoute<'ActionResultModal'>,
};
function ActionResultModal(props: Props): React.Node {
const overlayContext = React.useContext(OverlayContext);
invariant(overlayContext, 'ActionResultModal should have OverlayContext');
const { position } = overlayContext;
// Timer resets whenever message updates
const { goBackOnce } = props.navigation;
const { message } = props.route.params;
React.useEffect(() => {
const timeoutID = setTimeout(goBackOnce, 2000);
return () => clearTimeout(timeoutID);
}, [message, goBackOnce]);
const styles = useOverlayStyles(ourStyles);
const bottomInset = useSelector(state => state.dimensions.bottomInset);
const containerStyle = {
...styles.container,
opacity: position,
paddingBottom: bottomInset + 100,
};
return (
{message}
);
}
const ourStyles = {
backdrop: {
backgroundColor: 'modalContrastBackground',
bottom: 0,
left: 0,
opacity: 'modalContrastOpacity',
position: 'absolute',
right: 0,
top: 0,
},
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'flex-end',
},
message: {
borderRadius: 10,
overflow: 'hidden',
padding: 10,
},
text: {
color: 'modalContrastForegroundLabel',
fontSize: 20,
textAlign: 'center',
},
};
export default ActionResultModal;
diff --git a/native/profile/custom-server-modal.react.js b/native/profile/custom-server-modal.react.js
index 8771f297f..d561d1ec3 100644
--- a/native/profile/custom-server-modal.react.js
+++ b/native/profile/custom-server-modal.react.js
@@ -1,137 +1,137 @@
// @flow
import * as React from 'react';
import { Text } from 'react-native';
import { useDispatch } from 'react-redux';
import type { Dispatch } from 'lib/types/redux-types';
import { setURLPrefix } from 'lib/utils/url-utils';
import Button from '../components/button.react';
import Modal from '../components/modal.react';
import TextInput from '../components/text-input.react';
import type { RootNavigationProp } from '../navigation/root-navigator.react';
import type { NavigationRoute } from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import { useStyles } from '../themes/colors';
import { setCustomServer } from '../utils/url-utils';
export type CustomServerModalParams = {
- presentedFrom: string,
+ +presentedFrom: string,
};
type BaseProps = {
+navigation: RootNavigationProp<'CustomServerModal'>,
+route: NavigationRoute<'CustomServerModal'>,
};
type Props = {
...BaseProps,
+urlPrefix: string,
+customServer: ?string,
+styles: typeof unboundStyles,
+dispatch: Dispatch,
};
type State = {
+customServer: string,
};
class CustomServerModal extends React.PureComponent {
constructor(props: Props) {
super(props);
const { customServer } = props;
this.state = {
customServer: customServer ? customServer : '',
};
}
render() {
return (
);
}
onChangeCustomServer = (newCustomServer: string) => {
this.setState({ customServer: newCustomServer });
};
onPressGo = () => {
const { customServer } = this.state;
if (customServer !== this.props.urlPrefix) {
this.props.dispatch({
type: setURLPrefix,
payload: customServer,
});
}
if (customServer && customServer !== this.props.customServer) {
this.props.dispatch({
type: setCustomServer,
payload: customServer,
});
}
this.props.navigation.goBackOnce();
};
}
const unboundStyles = {
button: {
backgroundColor: 'greenButton',
borderRadius: 5,
marginHorizontal: 2,
marginVertical: 2,
paddingHorizontal: 12,
paddingVertical: 4,
},
buttonText: {
color: 'white',
fontSize: 18,
textAlign: 'center',
},
container: {
justifyContent: 'flex-end',
},
modal: {
flex: 0,
flexDirection: 'row',
},
textInput: {
color: 'modalBackgroundLabel',
flex: 1,
fontSize: 16,
margin: 0,
padding: 0,
borderBottomColor: 'transparent',
},
};
const ConnectedCustomServerModal: React.ComponentType = React.memo(
function ConnectedCustomServerModal(props: BaseProps) {
const urlPrefix = useSelector(state => state.urlPrefix);
const customServer = useSelector(state => state.customServer);
const styles = useStyles(unboundStyles);
const dispatch = useDispatch();
return (
);
},
);
export default ConnectedCustomServerModal;