Page MenuHomePhabricator

D6549.id21969.diff
No OneTemporary

D6549.id21969.diff

diff --git a/native/chat/message-report-utils.js b/native/chat/message-report-utils.js
--- a/native/chat/message-report-utils.js
+++ b/native/chat/message-report-utils.js
@@ -1,52 +1,32 @@
// @flow
-import Alert from 'react-native/Libraries/Alert/Alert';
+import * as React from 'react';
+import { Alert } from 'react-native';
import {
sendMessageReport,
sendMessageReportActionTypes,
} from 'lib/actions/message-report-actions';
-import type { BindServerCall, DispatchFunctions } from 'lib/utils/action-utils';
+import {
+ useServerCall,
+ useDispatchActionPromise,
+} from 'lib/utils/action-utils';
import { displayActionResultModal } from '../navigation/action-result-modal';
-import type { TooltipRoute } from '../navigation/tooltip.react';
+import type { TooltipRoute } from '../tooltip/tooltip.react';
const confirmReport = () => displayActionResultModal('reported to admin');
-function onPressReport(
+function useOnPressReport(
route:
| TooltipRoute<'TextMessageTooltipModal'>
| TooltipRoute<'MultimediaMessageTooltipModal'>,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
-) {
+): () => mixed {
const messageID = route.params.item.messageInfo.id;
- if (!messageID) {
- Alert.alert(
- 'Couldn’t send the report',
- 'Uhh... try again?',
- [{ text: 'OK' }],
- {
- cancelable: false,
- },
- );
- return;
- }
- reportMessage(messageID, dispatchFunctions, bindServerCall);
-}
-
-function reportMessage(
- messageID: string,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
-) {
- const callSendMessageReport = bindServerCall(sendMessageReport);
- const messageReportPromise = (async () => {
- try {
- const result = await callSendMessageReport({ messageID });
- confirmReport();
- return result;
- } catch (e) {
+ const dispatchActionPromise = useDispatchActionPromise();
+ const callSendMessageReport = useServerCall(sendMessageReport);
+ return React.useCallback(() => {
+ if (!messageID) {
Alert.alert(
'Couldn’t send the report',
'Uhh... try again?',
@@ -55,14 +35,27 @@
cancelable: false,
},
);
- throw e;
+ return;
}
- })();
-
- dispatchFunctions.dispatchActionPromise(
- sendMessageReportActionTypes,
- messageReportPromise,
- );
+ const messageReportPromise = (async () => {
+ try {
+ const result = await callSendMessageReport({ messageID });
+ confirmReport();
+ return result;
+ } catch (e) {
+ Alert.alert(
+ 'Couldn’t send the report',
+ 'Uhh... try again?',
+ [{ text: 'OK' }],
+ {
+ cancelable: false,
+ },
+ );
+ throw e;
+ }
+ })();
+ dispatchActionPromise(sendMessageReportActionTypes, messageReportPromise);
+ }, [callSendMessageReport, messageID, dispatchActionPromise]);
}
-export { onPressReport };
+export { useOnPressReport };
diff --git a/native/chat/multimedia-message-tooltip-button.react.js b/native/chat/multimedia-message-tooltip-button.react.js
--- a/native/chat/multimedia-message-tooltip-button.react.js
+++ b/native/chat/multimedia-message-tooltip-button.react.js
@@ -9,8 +9,8 @@
import type { SetState } from 'lib/types/hook-types';
import type { AppNavigationProp } from '../navigation/app-navigator.react';
-import type { TooltipRoute } from '../navigation/tooltip.react';
import { useSelector } from '../redux/redux-utils';
+import type { TooltipRoute } from '../tooltip/tooltip.react';
import { TooltipInlineEngagement } from './inline-engagement.react';
import { InnerMultimediaMessage } from './inner-multimedia-message.react';
import { MessageHeader } from './message-header.react';
diff --git a/native/chat/multimedia-message-tooltip-modal.react.js b/native/chat/multimedia-message-tooltip-modal.react.js
--- a/native/chat/multimedia-message-tooltip-modal.react.js
+++ b/native/chat/multimedia-message-tooltip-modal.react.js
@@ -2,42 +2,68 @@
import * as React from 'react';
+import SWMansionIcon from '../components/swmansion-icon.react';
import {
createTooltip,
type TooltipParams,
type BaseTooltipProps,
-} from '../navigation/tooltip.react';
+ type TooltipMenuProps,
+} from '../tooltip/tooltip.react';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types';
import type { VerticalBounds } from '../types/layout-types';
-import { onPressReport } from './message-report-utils';
+import { useOnPressReport } from './message-report-utils';
import MultimediaMessageTooltipButton from './multimedia-message-tooltip-button.react';
-import { navigateToSidebar } from './sidebar-navigation';
+import { useAnimatedNavigateToSidebar } from './sidebar-navigation';
export type MultimediaMessageTooltipModalParams = TooltipParams<{
+item: ChatMultimediaMessageInfoItem,
+verticalBounds: VerticalBounds,
}>;
-const spec = {
- entries: [
- {
- id: 'sidebar',
- text: 'Thread',
- onPress: navigateToSidebar,
- },
- {
- id: 'report',
- text: 'Report',
- onPress: onPressReport,
- },
- ],
-};
+function TooltipMenu(
+ props: TooltipMenuProps<'MultimediaMessageTooltipModal'>,
+): React.Node {
+ const { route, tooltipItem: TooltipItem } = props;
+
+ const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item);
+ const renderSidebarIcon = React.useCallback(
+ style => (
+ <SWMansionIcon name="message-circle-lines" style={style} size={16} />
+ ),
+ [],
+ );
+
+ const onPressReport = useOnPressReport(route);
+ const renderReportIcon = React.useCallback(
+ style => <SWMansionIcon name="warning-circle" style={style} size={16} />,
+ [],
+ );
+
+ return (
+ <>
+ <TooltipItem
+ id="sidebar"
+ text="Thread"
+ onPress={onPressSidebar}
+ renderIcon={renderSidebarIcon}
+ key="sidebar"
+ />
+ <TooltipItem
+ id="report"
+ text="Report"
+ onPress={onPressReport}
+ renderIcon={renderReportIcon}
+ key="report"
+ />
+ </>
+ );
+}
const MultimediaMessageTooltipModal: React.ComponentType<
BaseTooltipProps<'MultimediaMessageTooltipModal'>,
> = createTooltip<'MultimediaMessageTooltipModal'>(
MultimediaMessageTooltipButton,
- spec,
+ TooltipMenu,
);
export default MultimediaMessageTooltipModal;
diff --git a/native/chat/multimedia-message.react.js b/native/chat/multimedia-message.react.js
--- a/native/chat/multimedia-message.react.js
+++ b/native/chat/multimedia-message.react.js
@@ -21,7 +21,7 @@
MultimediaMessageTooltipModalRouteName,
VideoPlaybackModalRouteName,
} from '../navigation/route-names';
-import { fixedTooltipHeight } from '../navigation/tooltip.react';
+import { fixedTooltipHeight } from '../tooltip/tooltip.react';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types';
import { type VerticalBounds } from '../types/layout-types';
import type { LayoutCoordinates } from '../types/layout-types';
diff --git a/native/chat/robotext-message-tooltip-button.react.js b/native/chat/robotext-message-tooltip-button.react.js
--- a/native/chat/robotext-message-tooltip-button.react.js
+++ b/native/chat/robotext-message-tooltip-button.react.js
@@ -9,8 +9,8 @@
import type { SetState } from 'lib/types/hook-types';
import type { AppNavigationProp } from '../navigation/app-navigator.react';
-import type { TooltipRoute } from '../navigation/tooltip.react';
import { useSelector } from '../redux/redux-utils';
+import type { TooltipRoute } from '../tooltip/tooltip.react';
import { TooltipInlineEngagement } from './inline-engagement.react';
import { InnerRobotextMessage } from './inner-robotext-message.react';
import {
diff --git a/native/chat/robotext-message-tooltip-modal.react.js b/native/chat/robotext-message-tooltip-modal.react.js
--- a/native/chat/robotext-message-tooltip-modal.react.js
+++ b/native/chat/robotext-message-tooltip-modal.react.js
@@ -2,34 +2,52 @@
import * as React from 'react';
+import SWMansionIcon from '../components/swmansion-icon.react';
import {
createTooltip,
type TooltipParams,
type BaseTooltipProps,
-} from '../navigation/tooltip.react';
+ type TooltipMenuProps,
+} from '../tooltip/tooltip.react';
import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types';
import RobotextMessageTooltipButton from './robotext-message-tooltip-button.react';
-import { navigateToSidebar } from './sidebar-navigation';
+import { useAnimatedNavigateToSidebar } from './sidebar-navigation';
export type RobotextMessageTooltipModalParams = TooltipParams<{
+item: ChatRobotextMessageInfoItemWithHeight,
}>;
-const spec = {
- entries: [
- {
- id: 'sidebar',
- text: 'Thread',
- onPress: navigateToSidebar,
- },
- ],
-};
+function TooltipMenu(
+ props: TooltipMenuProps<'RobotextMessageTooltipModal'>,
+): React.Node {
+ const { route, tooltipItem: TooltipItem } = props;
+
+ const onPress = useAnimatedNavigateToSidebar(route.params.item);
+ const renderIcon = React.useCallback(
+ style => (
+ <SWMansionIcon name="message-circle-lines" style={style} size={16} />
+ ),
+ [],
+ );
+
+ return (
+ <>
+ <TooltipItem
+ id="sidebar"
+ text="Thread"
+ onPress={onPress}
+ renderIcon={renderIcon}
+ key="sidebar"
+ />
+ </>
+ );
+}
const RobotextMessageTooltipModal: React.ComponentType<
BaseTooltipProps<'RobotextMessageTooltipModal'>,
> = createTooltip<'RobotextMessageTooltipModal'>(
RobotextMessageTooltipButton,
- spec,
+ TooltipMenu,
);
export default RobotextMessageTooltipModal;
diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js
--- a/native/chat/robotext-message.react.js
+++ b/native/chat/robotext-message.react.js
@@ -12,8 +12,8 @@
import { OverlayContext } from '../navigation/overlay-context';
import { RobotextMessageTooltipModalRouteName } from '../navigation/route-names';
import type { NavigationRoute } from '../navigation/route-names';
-import { fixedTooltipHeight } from '../navigation/tooltip.react';
import { useStyles } from '../themes/colors';
+import { fixedTooltipHeight } from '../tooltip/tooltip.react';
import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types';
import type { VerticalBounds } from '../types/layout-types';
import { AnimatedView } from '../types/styles';
diff --git a/native/chat/settings/thread-settings-member-tooltip-modal.react.js b/native/chat/settings/thread-settings-member-tooltip-modal.react.js
--- a/native/chat/settings/thread-settings-member-tooltip-modal.react.js
+++ b/native/chat/settings/thread-settings-member-tooltip-modal.react.js
@@ -14,14 +14,18 @@
} from 'lib/shared/thread-utils';
import { stringForUser } from 'lib/shared/user-utils';
import type { ThreadInfo, RelativeMemberInfo } from 'lib/types/thread-types';
-import type { DispatchFunctions, BindServerCall } from 'lib/utils/action-utils';
+import {
+ useDispatchActionPromise,
+ useServerCall,
+} from 'lib/utils/action-utils';
import {
createTooltip,
type TooltipParams,
type TooltipRoute,
type BaseTooltipProps,
-} from '../../navigation/tooltip.react';
+ type TooltipMenuProps,
+} from '../../tooltip/tooltip.react';
import ThreadSettingsMemberTooltipButton from './thread-settings-member-tooltip-button.react';
export type ThreadSettingsMemberTooltipModalParams = TooltipParams<{
@@ -29,78 +33,118 @@
+threadInfo: ThreadInfo,
}>;
-function onRemoveUser(
+function useOnRemoveUser(
route: TooltipRoute<'ThreadSettingsMemberTooltipModal'>,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
) {
const { memberInfo, threadInfo } = route.params;
- const boundRemoveUsersFromThread = bindServerCall(removeUsersFromThread);
- const onConfirmRemoveUser = () =>
- removeMemberFromThread(
- threadInfo,
- memberInfo,
- dispatchFunctions.dispatchActionPromise,
- boundRemoveUsersFromThread,
- );
+ const boundRemoveUsersFromThread = useServerCall(removeUsersFromThread);
+ const dispatchActionPromise = useDispatchActionPromise();
- const userText = stringForUser(memberInfo);
- Alert.alert(
- 'Confirm removal',
- `Are you sure you want to remove ${userText} from this chat?`,
- [
- { text: 'Cancel', style: 'cancel' },
- { text: 'OK', onPress: onConfirmRemoveUser },
- ],
- { cancelable: true },
+ const onConfirmRemoveUser = React.useCallback(
+ () =>
+ removeMemberFromThread(
+ threadInfo,
+ memberInfo,
+ dispatchActionPromise,
+ boundRemoveUsersFromThread,
+ ),
+ [threadInfo, memberInfo, dispatchActionPromise, boundRemoveUsersFromThread],
);
+
+ const userText = stringForUser(memberInfo);
+ return React.useCallback(() => {
+ Alert.alert(
+ 'Confirm removal',
+ `Are you sure you want to remove ${userText} from this chat?`,
+ [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'OK', onPress: onConfirmRemoveUser },
+ ],
+ { cancelable: true },
+ );
+ }, [onConfirmRemoveUser, userText]);
}
-function onToggleAdmin(
+function useOnToggleAdmin(
route: TooltipRoute<'ThreadSettingsMemberTooltipModal'>,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
) {
const { memberInfo, threadInfo } = route.params;
+ const boundChangeThreadMemberRoles = useServerCall(changeThreadMemberRoles);
+ const dispatchActionPromise = useDispatchActionPromise();
+
const isCurrentlyAdmin = memberIsAdmin(memberInfo, threadInfo);
- const boundChangeThreadMemberRoles = bindServerCall(changeThreadMemberRoles);
- const onConfirmMakeAdmin = () =>
- switchMemberAdminRoleInThread(
+ const onConfirmMakeAdmin = React.useCallback(
+ () =>
+ switchMemberAdminRoleInThread(
+ threadInfo,
+ memberInfo,
+ isCurrentlyAdmin,
+ dispatchActionPromise,
+ boundChangeThreadMemberRoles,
+ ),
+ [
threadInfo,
memberInfo,
isCurrentlyAdmin,
- dispatchFunctions.dispatchActionPromise,
+ dispatchActionPromise,
boundChangeThreadMemberRoles,
- );
+ ],
+ );
const userText = stringForUser(memberInfo);
const actionClause = isCurrentlyAdmin
? `remove ${userText} as an admin`
: `make ${userText} an admin`;
- Alert.alert(
- 'Confirm action',
- `Are you sure you want to ${actionClause} of this chat?`,
- [
- { text: 'Cancel', style: 'cancel' },
- { text: 'OK', onPress: onConfirmMakeAdmin },
- ],
- { cancelable: true },
- );
+ return React.useCallback(() => {
+ Alert.alert(
+ 'Confirm action',
+ `Are you sure you want to ${actionClause} of this chat?`,
+ [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'OK', onPress: onConfirmMakeAdmin },
+ ],
+ { cancelable: true },
+ );
+ }, [onConfirmMakeAdmin, actionClause]);
}
-const spec = {
- entries: [
- { id: 'remove_user', text: 'Remove user', onPress: onRemoveUser },
- { id: 'remove_admin', text: 'Remove admin', onPress: onToggleAdmin },
- { id: 'make_admin', text: 'Make admin', onPress: onToggleAdmin },
- ],
-};
+function TooltipMenu(
+ props: TooltipMenuProps<'ThreadSettingsMemberTooltipModal'>,
+): React.Node {
+ const { route, tooltipItem: TooltipItem } = props;
+
+ const onRemoveUser = useOnRemoveUser(route);
+ const onToggleAdmin = useOnToggleAdmin(route);
+
+ return (
+ <>
+ <TooltipItem
+ id="remove_user"
+ text="Remove user"
+ onPress={onRemoveUser}
+ key="remove_user"
+ />
+ <TooltipItem
+ id="remove_admin"
+ text="Remove admin"
+ onPress={onToggleAdmin}
+ key="remove_admin"
+ />
+ <TooltipItem
+ id="make_admin"
+ text="Make admin"
+ onPress={onToggleAdmin}
+ key="make_admin"
+ />
+ </>
+ );
+}
const ThreadSettingsMemberTooltipModal: React.ComponentType<
BaseTooltipProps<'ThreadSettingsMemberTooltipModal'>,
> = createTooltip<'ThreadSettingsMemberTooltipModal'>(
ThreadSettingsMemberTooltipButton,
- spec,
+ TooltipMenu,
);
export default ThreadSettingsMemberTooltipModal;
diff --git a/native/chat/sidebar-navigation.js b/native/chat/sidebar-navigation.js
--- a/native/chat/sidebar-navigation.js
+++ b/native/chat/sidebar-navigation.js
@@ -5,20 +5,12 @@
import { createPendingSidebar } from 'lib/shared/thread-utils';
import type { ThreadInfo } from 'lib/types/thread-types';
-import type { DispatchFunctions, BindServerCall } from 'lib/utils/action-utils';
-import type { InputState } from '../input/input-state';
import { getDefaultTextMessageRules } from '../markdown/rules.react';
-import type { AppNavigationProp } from '../navigation/app-navigator.react';
-import type { MessageTooltipRouteNames } from '../navigation/route-names';
-import type { TooltipRoute } from '../navigation/tooltip.react';
import { useSelector } from '../redux/redux-utils';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types';
-import type { ChatContextType } from './chat-context';
-import {
- createNavigateToThreadAction,
- useNavigateToThread,
-} from './message-list-types';
+import { ChatContext } from './chat-context';
+import { useNavigateToThread } from './message-list-types';
function getSidebarThreadInfo(
sourceMessage: ChatMessageInfoItemWithHeight,
@@ -42,27 +34,6 @@
);
}
-function navigateToSidebar<RouteName: MessageTooltipRouteNames>(
- route: TooltipRoute<RouteName>,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
- inputState: ?InputState,
- navigation: AppNavigationProp<RouteName>,
- viewerID: ?string,
- chatContext: ?ChatContextType,
-) {
- invariant(viewerID, 'viewerID should be set');
- const threadInfo = getSidebarThreadInfo(route.params.item, viewerID);
- invariant(threadInfo, 'threadInfo should be set');
-
- chatContext?.setCurrentTransitionSidebarSourceID(
- route.params.item.messageInfo.id,
- );
- navigation.navigate<'MessageList'>(
- createNavigateToThreadAction({ threadInfo }),
- );
-}
-
function useNavigateToSidebar(item: ChatMessageInfoItemWithHeight): () => void {
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
@@ -78,4 +49,21 @@
}, [navigateToThread, threadInfo]);
}
-export { navigateToSidebar, getSidebarThreadInfo, useNavigateToSidebar };
+function useAnimatedNavigateToSidebar(
+ item: ChatMessageInfoItemWithHeight,
+): () => void {
+ const chatContext = React.useContext(ChatContext);
+ const setSidebarSourceID = chatContext?.setCurrentTransitionSidebarSourceID;
+ const navigateToSidebar = useNavigateToSidebar(item);
+ const messageID = item.messageInfo.id;
+ return React.useCallback(() => {
+ setSidebarSourceID && setSidebarSourceID(messageID);
+ navigateToSidebar();
+ }, [setSidebarSourceID, messageID, navigateToSidebar]);
+}
+
+export {
+ getSidebarThreadInfo,
+ useNavigateToSidebar,
+ useAnimatedNavigateToSidebar,
+};
diff --git a/native/chat/text-message-tooltip-button.react.js b/native/chat/text-message-tooltip-button.react.js
--- a/native/chat/text-message-tooltip-button.react.js
+++ b/native/chat/text-message-tooltip-button.react.js
@@ -9,8 +9,8 @@
import type { SetState } from 'lib/types/hook-types';
import type { AppNavigationProp } from '../navigation/app-navigator.react';
-import type { TooltipRoute } from '../navigation/tooltip.react';
import { useSelector } from '../redux/redux-utils';
+import type { TooltipRoute } from '../tooltip/tooltip.react';
import { TooltipInlineEngagement } from './inline-engagement.react';
import { InnerTextMessage } from './inner-text-message.react';
import { MessageHeader } from './message-header.react';
diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js
--- a/native/chat/text-message-tooltip-modal.react.js
+++ b/native/chat/text-message-tooltip-modal.react.js
@@ -5,19 +5,20 @@
import * as React from 'react';
import { createMessageReply } from 'lib/shared/message-utils';
-import type { DispatchFunctions, BindServerCall } from 'lib/utils/action-utils';
-import type { InputState } from '../input/input-state';
+import CommIcon from '../components/comm-icon.react';
+import SWMansionIcon from '../components/swmansion-icon.react';
+import { InputStateContext } from '../input/input-state';
import { displayActionResultModal } from '../navigation/action-result-modal';
import {
createTooltip,
type TooltipParams,
- type TooltipRoute,
type BaseTooltipProps,
-} from '../navigation/tooltip.react';
+ type TooltipMenuProps,
+} from '../tooltip/tooltip.react';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types';
-import { onPressReport } from './message-report-utils';
-import { navigateToSidebar } from './sidebar-navigation';
+import { useOnPressReport } from './message-report-utils';
+import { useAnimatedNavigateToSidebar } from './sidebar-navigation';
import TextMessageTooltipButton from './text-message-tooltip-button.react';
export type TextMessageTooltipModalParams = TooltipParams<{
@@ -26,43 +27,87 @@
const confirmCopy = () => displayActionResultModal('copied!');
-function onPressCopy(route: TooltipRoute<'TextMessageTooltipModal'>) {
- Clipboard.setString(route.params.item.messageInfo.text);
- setTimeout(confirmCopy);
-}
+function TooltipMenu(
+ props: TooltipMenuProps<'TextMessageTooltipModal'>,
+): React.Node {
+ const { route, tooltipItem: TooltipItem } = props;
-function onPressReply(
- route: TooltipRoute<'TextMessageTooltipModal'>,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
- inputState: ?InputState,
-) {
- invariant(
- inputState,
- 'inputState should be set in TextMessageTooltipModal.onPressReply',
+ const inputState = React.useContext(InputStateContext);
+ const { text } = route.params.item.messageInfo;
+ const onPressReply = React.useCallback(() => {
+ invariant(
+ inputState,
+ 'inputState should be set in TextMessageTooltipModal.onPressReply',
+ );
+ inputState.addReply(createMessageReply(text));
+ }, [inputState, text]);
+ const renderReplyIcon = React.useCallback(
+ style => <CommIcon name="reply" style={style} size={12} />,
+ [],
+ );
+
+ const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item);
+ const renderSidebarIcon = React.useCallback(
+ style => (
+ <SWMansionIcon name="message-circle-lines" style={style} size={16} />
+ ),
+ [],
+ );
+
+ const onPressCopy = React.useCallback(() => {
+ Clipboard.setString(text);
+ setTimeout(confirmCopy);
+ }, [text]);
+ const renderCopyIcon = React.useCallback(
+ style => <SWMansionIcon name="copy" style={style} size={16} />,
+ [],
+ );
+
+ const onPressReport = useOnPressReport(route);
+ const renderReportIcon = React.useCallback(
+ style => <SWMansionIcon name="warning-circle" style={style} size={16} />,
+ [],
);
- inputState.addReply(createMessageReply(route.params.item.messageInfo.text));
-}
-const spec = {
- entries: [
- { id: 'reply', text: 'Reply', onPress: onPressReply },
- {
- id: 'sidebar',
- text: 'Thread',
- onPress: navigateToSidebar,
- },
- { id: 'copy', text: 'Copy', onPress: onPressCopy },
- {
- id: 'report',
- text: 'Report',
- onPress: onPressReport,
- },
- ],
-};
+ return (
+ <>
+ <TooltipItem
+ id="reply"
+ text="Reply"
+ onPress={onPressReply}
+ renderIcon={renderReplyIcon}
+ key="reply"
+ />
+ <TooltipItem
+ id="sidebar"
+ text="Thread"
+ onPress={onPressSidebar}
+ renderIcon={renderSidebarIcon}
+ key="sidebar"
+ />
+ <TooltipItem
+ id="copy"
+ text="Copy"
+ onPress={onPressCopy}
+ renderIcon={renderCopyIcon}
+ key="copy"
+ />
+ <TooltipItem
+ id="report"
+ text="Report"
+ onPress={onPressReport}
+ renderIcon={renderReportIcon}
+ key="report"
+ />
+ </>
+ );
+}
const TextMessageTooltipModal: React.ComponentType<
BaseTooltipProps<'TextMessageTooltipModal'>,
-> = createTooltip<'TextMessageTooltipModal'>(TextMessageTooltipButton, spec);
+> = createTooltip<'TextMessageTooltipModal'>(
+ TextMessageTooltipButton,
+ TooltipMenu,
+);
export default TextMessageTooltipModal;
diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js
--- a/native/chat/text-message.react.js
+++ b/native/chat/text-message.react.js
@@ -19,7 +19,7 @@
} from '../navigation/overlay-context';
import type { NavigationRoute } from '../navigation/route-names';
import { TextMessageTooltipModalRouteName } from '../navigation/route-names';
-import { fixedTooltipHeight } from '../navigation/tooltip.react';
+import { fixedTooltipHeight } from '../tooltip/tooltip.react';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types';
import type { VerticalBounds } from '../types/layout-types';
import type { ChatNavigationProp } from './chat.react';
diff --git a/native/profile/relationship-list-item-tooltip-modal.react.js b/native/profile/relationship-list-item-tooltip-modal.react.js
--- a/native/profile/relationship-list-item-tooltip-modal.react.js
+++ b/native/profile/relationship-list-item-tooltip-modal.react.js
@@ -9,7 +9,10 @@
} from 'lib/actions/relationship-actions';
import { stringForUser } from 'lib/shared/user-utils';
import type { RelativeUserInfo } from 'lib/types/user-types';
-import type { DispatchFunctions, BindServerCall } from 'lib/utils/action-utils';
+import {
+ useDispatchActionPromise,
+ useServerCall,
+} from 'lib/utils/action-utils';
import PencilIcon from '../components/pencil-icon.react';
import type { AppNavigationProp } from '../navigation/app-navigator.react';
@@ -17,7 +20,8 @@
createTooltip,
type TooltipParams,
type BaseTooltipProps,
-} from '../navigation/tooltip.react';
+ type TooltipMenuProps,
+} from '../tooltip/tooltip.react';
type Action = 'unfriend' | 'unblock';
@@ -29,79 +33,85 @@
...RelationshipListItemTooltipModalParams,
+action: Action,
};
-function onRemoveUser(
- props: OnRemoveUserProps,
- dispatchFunctions: DispatchFunctions,
- bindServerCall: BindServerCall,
-) {
- const boundRemoveRelationships = bindServerCall(updateRelationships);
- const callRemoveRelationships = async () => {
- try {
- return await boundRemoveRelationships({
- action: props.action,
- userIDs: [props.relativeUserInfo.id],
- });
- } catch (e) {
- Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], {
- cancelable: true,
- });
- throw e;
- }
- };
- const onConfirmRemoveUser = () => {
- const customKeyName = `${updateRelationshipsActionTypes.started}:${props.relativeUserInfo.id}`;
- dispatchFunctions.dispatchActionPromise(
- updateRelationshipsActionTypes,
- callRemoveRelationships(),
- { customKeyName },
+function useRelationshipAction(input: OnRemoveUserProps) {
+ const boundRemoveRelationships = useServerCall(updateRelationships);
+ const dispatchActionPromise = useDispatchActionPromise();
+ const userText = stringForUser(input.relativeUserInfo);
+
+ return React.useCallback(() => {
+ const callRemoveRelationships = async () => {
+ try {
+ return await boundRemoveRelationships({
+ action: input.action,
+ userIDs: [input.relativeUserInfo.id],
+ });
+ } catch (e) {
+ Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], {
+ cancelable: true,
+ });
+ throw e;
+ }
+ };
+ const onConfirmRemoveUser = () => {
+ const customKeyName = `${updateRelationshipsActionTypes.started}:${input.relativeUserInfo.id}`;
+ dispatchActionPromise(
+ updateRelationshipsActionTypes,
+ callRemoveRelationships(),
+ { customKeyName },
+ );
+ };
+ const action = {
+ unfriend: 'removal',
+ unblock: 'unblock',
+ }[input.action];
+ const message = {
+ unfriend: `remove ${userText} from friends?`,
+ unblock: `unblock ${userText}?`,
+ }[input.action];
+ Alert.alert(
+ `Confirm ${action}`,
+ `Are you sure you want to ${message}`,
+ [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'OK', onPress: onConfirmRemoveUser },
+ ],
+ { cancelable: true },
);
- };
+ }, [boundRemoveRelationships, dispatchActionPromise, userText, input]);
+}
- const userText = stringForUser(props.relativeUserInfo);
- const action = {
- unfriend: 'removal',
- unblock: 'unblock',
- }[props.action];
- const message = {
- unfriend: `remove ${userText} from friends?`,
- unblock: `unblock ${userText}?`,
- }[props.action];
- Alert.alert(
- `Confirm ${action}`,
- `Are you sure you want to ${message}`,
- [
- { text: 'Cancel', style: 'cancel' },
- { text: 'OK', onPress: onConfirmRemoveUser },
- ],
- { cancelable: true },
+function TooltipMenu(
+ props: TooltipMenuProps<'RelationshipListItemTooltipModal'>,
+): React.Node {
+ const { route, tooltipItem: TooltipItem } = props;
+
+ const onRemoveUser = useRelationshipAction({
+ ...route.params,
+ action: 'unfriend',
+ });
+ const onUnblockUser = useRelationshipAction({
+ ...route.params,
+ action: 'unblock',
+ });
+
+ return (
+ <>
+ <TooltipItem
+ id="unfriend"
+ text="Unfriend"
+ onPress={onRemoveUser}
+ key="unfriend"
+ />
+ <TooltipItem
+ id="unblock"
+ text="Unblock"
+ onPress={onUnblockUser}
+ key="unblock"
+ />
+ </>
);
}
-const spec = {
- entries: [
- {
- id: 'unfriend',
- text: 'Unfriend',
- onPress: (route, dispatchFunctions, bindServerCall) =>
- onRemoveUser(
- { ...route.params, action: 'unfriend' },
- dispatchFunctions,
- bindServerCall,
- ),
- },
- {
- id: 'unblock',
- text: 'Unblock',
- onPress: (route, dispatchFunctions, bindServerCall) =>
- onRemoveUser(
- { ...route.params, action: 'unblock' },
- dispatchFunctions,
- bindServerCall,
- ),
- },
- ],
-};
-
type Props = {
+navigation: AppNavigationProp<'RelationshipListItemTooltipModal'>,
...
@@ -124,7 +134,7 @@
BaseTooltipProps<'RelationshipListItemTooltipModal'>,
> = createTooltip<'RelationshipListItemTooltipModal'>(
RelationshipListItemTooltipButton,
- spec,
+ TooltipMenu,
);
export default RelationshipListItemTooltipModal;
diff --git a/native/tooltip/tooltip-context.react.js b/native/tooltip/tooltip-context.react.js
new file mode 100644
--- /dev/null
+++ b/native/tooltip/tooltip-context.react.js
@@ -0,0 +1,186 @@
+// @flow
+
+import { useActionSheet } from '@expo/react-native-action-sheet';
+import * as React from 'react';
+import { Platform, StyleSheet } from 'react-native';
+
+import type { TooltipItemBaseProps } from './tooltip-item.react';
+
+type RegisterOptionInput = {
+ ...TooltipItemBaseProps,
+ +symbol: symbol,
+};
+type RegisterOptionOutput = { +shouldRender: boolean };
+export type TooltipContextType = {
+ +registerOption: (input: RegisterOptionInput) => RegisterOptionOutput,
+ +unregisterOption: (id: string) => void,
+ +showActionSheet: () => void,
+ +shouldShowMore: () => boolean,
+ +getNumVisibleEntries: () => number,
+};
+const TooltipContext: React.Context<?TooltipContextType> = React.createContext<?TooltipContextType>();
+
+type ProviderProps = {
+ +maxOptionsToDisplay: number,
+ +visibleEntryIDs: ?$ReadOnlyArray<string>,
+ +cancel: () => mixed,
+ +children: React.Node,
+};
+function TooltipContextProvider(props: ProviderProps): React.Node {
+ const optionsRef = React.useRef<RegisterOptionInput[]>([]);
+
+ const { visibleEntryIDs } = props;
+ const visibleEntryIDsSet = React.useMemo(
+ () => new Set(visibleEntryIDs ?? []),
+ [visibleEntryIDs],
+ );
+
+ const { maxOptionsToDisplay } = props;
+ const registerOption = React.useCallback(
+ (input: RegisterOptionInput) => {
+ const options = optionsRef.current;
+
+ const existingIndex = options.findIndex(option => option.id === input.id);
+ if (existingIndex === -1) {
+ options.push(input);
+ } else {
+ if (options[existingIndex].symbol !== input.symbol) {
+ console.warn(`multiple TooltipItems registered with ID ${input.id}`);
+ }
+ options[existingIndex] = input;
+ }
+
+ const optionsToDisplay = options.filter(option =>
+ visibleEntryIDsSet.has(option.id),
+ );
+ const displayIndex = optionsToDisplay.findIndex(
+ option => option.id === input.id,
+ );
+ const cutoff =
+ optionsToDisplay.length === maxOptionsToDisplay
+ ? maxOptionsToDisplay
+ : maxOptionsToDisplay - 1;
+
+ const shouldRender =
+ input.id === 'more' || (displayIndex >= 0 && displayIndex < cutoff);
+ return { shouldRender };
+ },
+ [maxOptionsToDisplay, visibleEntryIDsSet],
+ );
+
+ const unregisterOption = React.useCallback((id: string) => {
+ optionsRef.current = optionsRef.current.filter(option => option.id !== id);
+ }, []);
+
+ const { cancel } = props;
+ const { showActionSheetWithOptions } = useActionSheet();
+ const showActionSheet = React.useCallback(() => {
+ const options = optionsRef.current;
+
+ const optionsToDisplay = options.filter(option =>
+ visibleEntryIDsSet.has(option.id),
+ );
+
+ const filteredOptions = optionsToDisplay.slice(maxOptionsToDisplay - 1);
+
+ const cancelButtonExists = options.some(option => option.isCancel);
+ if (Platform.OS === 'ios' && !cancelButtonExists) {
+ filteredOptions.push({
+ id: 'cancel',
+ text: 'Cancel',
+ onPress: cancel,
+ isCancel: true,
+ symbol: Symbol(),
+ });
+ }
+
+ // We're reversing options to populate the action sheet from bottom to
+ // top instead of the default (top to bottom) ordering.
+ filteredOptions.reverse();
+
+ const texts = filteredOptions.map(option => option.text);
+
+ const destructiveButtonIndices = filteredOptions
+ .filter(option => option.isDestructive)
+ .map((_, i) => i);
+
+ const cancelButtonIndex = filteredOptions.findIndex(
+ option => option.isCancel,
+ );
+
+ const icons = filteredOptions.map(option =>
+ option.renderIcon ? option.renderIcon(styles.bottomSheetIcon) : undefined,
+ );
+
+ const onPressAction = (selectedIndex: ?number) => {
+ const index = selectedIndex ?? 0;
+ filteredOptions[index].onPress();
+ };
+
+ const containerStyle = {
+ paddingBottom: 24,
+ };
+ showActionSheetWithOptions(
+ {
+ options: texts,
+ cancelButtonIndex,
+ destructiveButtonIndex: destructiveButtonIndices,
+ containerStyle,
+ icons,
+ },
+ onPressAction,
+ );
+ }, [
+ maxOptionsToDisplay,
+ visibleEntryIDsSet,
+ cancel,
+ showActionSheetWithOptions,
+ ]);
+
+ const shouldShowMore = React.useCallback(() => {
+ const options = optionsRef.current;
+ const optionsToDisplay = options.filter(option =>
+ visibleEntryIDsSet.has(option.id),
+ );
+ return optionsToDisplay.length > maxOptionsToDisplay;
+ }, [maxOptionsToDisplay, visibleEntryIDsSet]);
+
+ const getNumVisibleEntries = React.useCallback(() => {
+ const options = optionsRef.current;
+ const optionsToDisplay = options.filter(option =>
+ visibleEntryIDsSet.has(option.id),
+ );
+ return Math.min(optionsToDisplay.length, maxOptionsToDisplay);
+ }, [maxOptionsToDisplay, visibleEntryIDsSet]);
+
+ const context = React.useMemo(
+ () => ({
+ registerOption,
+ unregisterOption,
+ showActionSheet,
+ shouldShowMore,
+ getNumVisibleEntries,
+ }),
+ [
+ registerOption,
+ unregisterOption,
+ showActionSheet,
+ shouldShowMore,
+ getNumVisibleEntries,
+ ],
+ );
+ const { children } = props;
+ return (
+ <TooltipContext.Provider value={context}>
+ {children}
+ </TooltipContext.Provider>
+ );
+}
+
+const styles = StyleSheet.create({
+ bottomSheetIcon: {
+ color: '#000000',
+ },
+});
+
+export { TooltipContext, TooltipContextProvider };
diff --git a/native/tooltip/tooltip-item.react.js b/native/tooltip/tooltip-item.react.js
new file mode 100644
--- /dev/null
+++ b/native/tooltip/tooltip-item.react.js
@@ -0,0 +1,77 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+import { TouchableOpacity } from 'react-native';
+
+import { SingleLine } from '../components/single-line.react';
+import { useStyles } from '../themes/colors';
+import type { ViewStyle, TextStyle } from '../types/styles';
+import { TooltipContext } from './tooltip-context.react';
+
+export type TooltipItemBaseProps = {
+ +id: string,
+ +text: string,
+ +onPress: () => mixed,
+ +renderIcon?: (iconStyle: TextStyle) => React.Node,
+ +isDestructive?: boolean,
+ +isCancel?: boolean,
+};
+type Props = {
+ ...TooltipItemBaseProps,
+ +containerStyle?: ViewStyle,
+ +closeTooltip: () => mixed,
+};
+function TooltipItem(props: Props): React.Node {
+ const tooltipContext = React.useContext(TooltipContext);
+ invariant(tooltipContext, 'TooltipContext should be set in TooltipItem');
+ const { registerOption, unregisterOption } = tooltipContext;
+
+ const symbolRef = React.useRef(Symbol());
+
+ const { containerStyle, closeTooltip, ...contextRegistrationInput } = props;
+ const { shouldRender } = registerOption({
+ ...contextRegistrationInput,
+ symbol: symbolRef.current,
+ });
+
+ const { id, text, renderIcon, onPress: onPressItem } = props;
+
+ React.useEffect(() => {
+ return () => unregisterOption(id);
+ }, [unregisterOption, id]);
+
+ const styles = useStyles(unboundStyles);
+
+ const onPress = React.useCallback(() => {
+ onPressItem();
+ closeTooltip();
+ }, [onPressItem, closeTooltip]);
+
+ if (!shouldRender) {
+ return null;
+ }
+
+ const icon = renderIcon ? renderIcon(styles.icon) : null;
+
+ return (
+ <TouchableOpacity onPress={onPress} style={containerStyle}>
+ {icon}
+ <SingleLine style={styles.label}>{text}</SingleLine>
+ </TouchableOpacity>
+ );
+}
+
+const unboundStyles = {
+ label: {
+ color: 'modalForegroundLabel',
+ fontSize: 14,
+ lineHeight: 17,
+ textAlign: 'center',
+ },
+ icon: {
+ color: 'modalForegroundLabel',
+ },
+};
+
+export default TooltipItem;
diff --git a/native/navigation/tooltip.react.js b/native/tooltip/tooltip.react.js
rename from native/navigation/tooltip.react.js
rename to native/tooltip/tooltip.react.js
--- a/native/navigation/tooltip.react.js
+++ b/native/tooltip/tooltip.react.js
@@ -1,9 +1,5 @@
// @flow
-import {
- useActionSheet,
- type ShowActionSheetWithOptions,
-} from '@expo/react-native-action-sheet';
import type { RouteProp } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import invariant from 'invariant';
@@ -12,7 +8,6 @@
View,
TouchableWithoutFeedback,
Platform,
- TouchableOpacity,
Keyboard,
} from 'react-native';
import Animated, {
@@ -22,28 +17,17 @@
useSharedValue,
type SharedValue,
} from 'react-native-reanimated';
-import { useDispatch } from 'react-redux';
-import {
- type ServerCallState,
- serverCallStateSelector,
-} from 'lib/selectors/server-calls';
import type { SetState } from 'lib/types/hook-types';
-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 { AppNavigationProp } from '../navigation/app-navigator.react';
+import {
+ OverlayContext,
+ type OverlayContextType,
+} from '../navigation/overlay-context';
+import type { TooltipModalParamList } from '../navigation/route-names';
import { type DimensionsInfo } from '../redux/dimensions-updater.react';
import { useSelector } from '../redux/redux-utils';
import { useStyles } from '../themes/colors';
@@ -52,40 +36,20 @@
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';
+import { AnimatedView } from '../types/styles';
+import {
+ TooltipContextProvider,
+ TooltipContext,
+ type TooltipContextType,
+} from './tooltip-context.react';
+import BaseTooltipItem, {
+ type TooltipItemBaseProps,
+} from './tooltip-item.react';
/* 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,
- +styles: typeof unboundStyles,
-};
-type TooltipSpec<RouteName> = {
- +entries: $ReadOnlyArray<TooltipEntry<RouteName>>,
- +labelStyle?: ViewStyle,
-};
-
export type TooltipParams<CustomProps> = {
...CustomProps,
+presentedFrom: string,
@@ -116,24 +80,22 @@
...Base,
// Redux state
+dimensions: DimensionsInfo,
- +serverCallState: ServerCallState,
- +viewerID: ?string,
- +nextReactionMessageLocalID: number,
- // Redux dispatch functions
- +dispatch: Dispatch,
- +dispatchActionPromise: DispatchActionPromise,
- // withOverlayContext
+overlayContext: ?OverlayContextType,
- // withInputState
- +inputState: ?InputState,
+chatContext: ?ChatContextType,
- +showActionSheetWithOptions: ShowActionSheetWithOptions,
+actionSheetShown: SharedValue<boolean>,
+hideTooltip: boolean,
+setHideTooltip: SetState<boolean>,
+showEmojiKeyboard: SharedValue<boolean>,
+exitAnimationWorklet: (finished: boolean) => void,
+styles: typeof unboundStyles,
+ +tooltipContext: TooltipContextType,
+ +closeTooltip: () => mixed,
+ +boundTooltipItem: React.ComponentType<TooltipItemBaseProps>,
+};
+
+export type TooltipMenuProps<RouteName> = {
+ ...BaseTooltipProps<RouteName>,
+ +tooltipItem: React.ComponentType<TooltipItemBaseProps>,
};
function createTooltip<
@@ -141,51 +103,8 @@
BaseTooltipPropsType: BaseTooltipProps<RouteName> = BaseTooltipProps<RouteName>,
>(
ButtonComponent: React.ComponentType<ButtonProps<BaseTooltipPropsType>>,
- tooltipSpec: TooltipSpec<RouteName>,
+ MenuComponent: React.ComponentType<TooltipMenuProps<RouteName>>,
): React.ComponentType<BaseTooltipPropsType> {
- class TooltipItem extends React.PureComponent<TooltipItemProps<RouteName>> {
- render() {
- let icon;
- const { styles } = this.props;
- 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}
- />
- );
- } else if (this.props.spec.id === 'more') {
- icon = (
- <SWMansionIcon name="menu-vertical" 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>,
> {
@@ -243,21 +162,11 @@
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.tooltipLocation === 'fixed') {
return fixedTooltipHeight;
} else {
- return tooltipHeight(this.entries.length);
+ return tooltipHeight(this.props.tooltipContext.getNumVisibleEntries());
}
}
@@ -390,21 +299,17 @@
render() {
const {
dimensions,
- serverCallState,
- viewerID,
- nextReactionMessageLocalID,
- dispatch,
- dispatchActionPromise,
overlayContext,
- inputState,
chatContext,
- showActionSheetWithOptions,
actionSheetShown,
hideTooltip,
setHideTooltip,
showEmojiKeyboard,
exitAnimationWorklet,
styles,
+ tooltipContext,
+ closeTooltip,
+ boundTooltipItem,
...navAndRouteForFlow
} = this.props;
@@ -414,46 +319,26 @@
tooltipContainerStyle.push(styles.itemContainerFixed);
}
- const { entries } = this;
- const items = entries.map((entry, index) => {
- let style;
- if (this.tooltipLocation === '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}
- styles={styles}
- />
- );
- });
-
- if (this.tooltipLocation === 'fixed' && entries.length > 3) {
- items.splice(3);
-
- const moreSpec = {
- id: 'more',
- text: 'More',
- onPress: this.onPressMore,
- };
+ const items = [
+ <MenuComponent
+ {...navAndRouteForFlow}
+ tooltipItem={this.getTooltipItem()}
+ key="menu"
+ />,
+ ];
- const moreTooltipItem = (
- <TooltipItem
- key={entries.length}
- spec={moreSpec}
- onPress={moreSpec.onPress}
+ if (this.props.tooltipContext.shouldShowMore()) {
+ items.push(
+ <BaseTooltipItem
+ id="more"
+ text="More"
+ onPress={this.onPressMore}
+ renderIcon={this.renderMoreIcon}
containerStyle={tooltipContainerStyle}
- styles={styles}
- />
+ closeTooltip={this.props.closeTooltip}
+ key="more"
+ />,
);
-
- items.push(moreTooltipItem);
}
let triangleStyle;
@@ -532,7 +417,7 @@
}
return (
- <TouchableWithoutFeedback onPress={this.onPressBackdrop}>
+ <TouchableWithoutFeedback onPress={this.props.closeTooltip}>
<View style={styles.container}>
<AnimatedView style={this.opacityStyle} />
<View style={this.contentContainerStyle}>
@@ -546,147 +431,25 @@
);
}
- onPressBackdrop = () => {
- if (this.tooltipLocation !== 'fixed') {
- this.props.navigation.goBackOnce();
- } else {
- this.props.setHideTooltip(true);
- }
- this.props.showEmojiKeyboard.value = false;
- };
-
- onPressEntry = (entry: TooltipEntry<RouteName>) => {
- if (
- this.tooltipLocation !== 'fixed' ||
- this.props.actionSheetShown.value
- ) {
- this.props.navigation.goBackOnce();
- } else {
- this.props.setHideTooltip(true);
- }
-
- 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,
- );
- };
+ getTooltipItem() {
+ const BoundTooltipItem = this.props.boundTooltipItem;
+ return BoundTooltipItem;
+ }
onPressMore = () => {
Keyboard.dismiss();
this.props.actionSheetShown.value = true;
this.props.setHideTooltip(true);
+ this.props.tooltipContext.showActionSheet();
+ };
- const { entries } = this;
- const options = entries.map(entry => entry.text);
-
- const {
- destructiveButtonIndex,
- cancelButtonIndex,
- } = this.getPlatformSpecificButtonIndices(options);
-
- // We're reversing options to populate the action sheet from bottom to
- // top instead of the default (top to bottom) ordering.
- options.reverse();
-
- const containerStyle = {
- paddingBottom: 24,
- };
-
+ renderMoreIcon = () => {
const { styles } = this.props;
- const icons = [
- <SWMansionIcon
- key="report"
- name="warning-circle"
- style={styles.bottomSheetIcon}
- size={16}
- />,
- <SWMansionIcon
- key="copy"
- name="copy"
- style={styles.bottomSheetIcon}
- size={16}
- />,
- <SWMansionIcon
- key="thread"
- name="message-circle-lines"
- style={styles.bottomSheetIcon}
- size={16}
- />,
- <CommIcon
- key="reply"
- name="reply"
- style={styles.bottomSheetIcon}
- size={12}
- />,
- ];
-
- const onPressAction = (selectedIndex?: number) => {
- if (selectedIndex === cancelButtonIndex) {
- this.props.navigation.goBackOnce();
- return;
- }
- const index = entries.length - (selectedIndex ?? 0);
- const entry = entries[Platform.OS === 'ios' ? index : index - 1];
- this.onPressEntry(entry);
- };
-
- this.props.showActionSheetWithOptions(
- {
- options,
- cancelButtonIndex,
- destructiveButtonIndex,
- containerStyle,
- icons,
- },
- onPressAction,
+ return (
+ <SWMansionIcon name="menu-vertical" style={styles.icon} size={16} />
);
};
- getPlatformSpecificButtonIndices = (options: Array<string>) => {
- let destructiveButtonIndex;
- if (Platform.OS === 'ios') {
- const reportIndex = options.findIndex(option => option === 'Report');
- destructiveButtonIndex =
- reportIndex !== -1 ? options.length - reportIndex : undefined;
- }
-
- const cancelButtonIndex = Platform.OS === 'ios' ? 0 : -1;
-
- // The "Cancel" action is iOS-specific
- if (Platform.OS === 'ios') {
- options.push('Cancel');
- }
-
- return { destructiveButtonIndex, cancelButtonIndex };
- };
-
- 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;
@@ -704,21 +467,9 @@
}
};
}
- return React.memo<BaseTooltipPropsType>(function ConnectedTooltip(
- props: BaseTooltipPropsType,
- ) {
- const { showActionSheetWithOptions } = useActionSheet();
-
+ function ConnectedTooltip(props: BaseTooltipPropsType) {
const dimensions = useSelector(state => state.dimensions);
- const serverCallState = useSelector(serverCallStateSelector);
- const viewerID = useSelector(
- state => state.currentUserInfo && state.currentUserInfo.id,
- );
- const nextReactionMessageLocalID = useSelector(state => state.nextLocalID);
- const dispatch = useDispatch();
- const dispatchActionPromise = useDispatchActionPromise();
const overlayContext = React.useContext(OverlayContext);
- const inputState = React.useContext(InputStateContext);
const chatContext = React.useContext(ChatContext);
const actionSheetShown = useSharedValue(false);
@@ -726,11 +477,12 @@
const showEmojiKeyboard = useSharedValue(false);
+ const { goBackOnce } = props.navigation;
const goBackCallback = React.useCallback(() => {
- if (!actionSheetShown.value && !showEmojiKeyboard.value) {
- props.navigation.goBackOnce();
+ if (!actionSheetShown.value) {
+ goBackOnce();
}
- }, [actionSheetShown.value, props.navigation, showEmojiKeyboard.value]);
+ }, [actionSheetShown.value, goBackOnce]);
const exitAnimationWorklet = React.useCallback(
finished => {
@@ -742,30 +494,70 @@
[goBackCallback],
);
+ const { params } = props.route;
+ const { tooltipLocation } = params;
+ const isFixed = tooltipLocation === 'fixed';
+
+ const closeTooltip = React.useCallback(() => {
+ if (isFixed && !actionSheetShown.value) {
+ setHideTooltip(true);
+ } else {
+ goBackOnce();
+ }
+ showEmojiKeyboard.value = false;
+ }, [isFixed, actionSheetShown.value, goBackOnce, showEmojiKeyboard]);
+
const styles = useStyles(unboundStyles);
+ const boundTooltipItem = React.useCallback(
+ innerProps => {
+ const containerStyle = isFixed
+ ? [styles.itemContainer, styles.itemContainerFixed]
+ : styles.itemContainer;
+ return (
+ <BaseTooltipItem
+ {...innerProps}
+ containerStyle={containerStyle}
+ closeTooltip={closeTooltip}
+ />
+ );
+ },
+ [isFixed, styles, closeTooltip],
+ );
+ const tooltipContext = React.useContext(TooltipContext);
+ invariant(tooltipContext, 'TooltipContext should be set in Tooltip');
return (
<Tooltip
{...props}
dimensions={dimensions}
- serverCallState={serverCallState}
- viewerID={viewerID}
- nextReactionMessageLocalID={nextReactionMessageLocalID}
- dispatch={dispatch}
- dispatchActionPromise={dispatchActionPromise}
overlayContext={overlayContext}
- inputState={inputState}
chatContext={chatContext}
- showActionSheetWithOptions={showActionSheetWithOptions}
actionSheetShown={actionSheetShown}
hideTooltip={hideTooltip}
setHideTooltip={setHideTooltip}
showEmojiKeyboard={showEmojiKeyboard}
exitAnimationWorklet={exitAnimationWorklet}
styles={styles}
+ tooltipContext={tooltipContext}
+ closeTooltip={closeTooltip}
+ boundTooltipItem={boundTooltipItem}
/>
);
- });
+ }
+ function MemoizedTooltip(props: BaseTooltipPropsType) {
+ const { visibleEntryIDs } = props.route.params;
+ const { goBackOnce } = props.navigation;
+ return (
+ <TooltipContextProvider
+ maxOptionsToDisplay={4}
+ visibleEntryIDs={visibleEntryIDs}
+ cancel={goBackOnce}
+ >
+ <ConnectedTooltip {...props} />
+ </TooltipContextProvider>
+ );
+ }
+ return React.memo<BaseTooltipPropsType>(MemoizedTooltip);
}
const unboundStyles = {
@@ -777,9 +569,6 @@
right: 0,
top: 0,
},
- bottomSheetIcon: {
- color: '#000000',
- },
container: {
flex: 1,
},
@@ -800,14 +589,6 @@
itemContainerFixed: {
flexDirection: 'column',
},
- itemMargin: {
- borderBottomColor: '#404040',
- borderBottomWidth: 1,
- },
- itemMarginFixed: {
- borderRightColor: 'panelForegroundBorder',
- borderRightWidth: 1,
- },
items: {
backgroundColor: 'tooltipBackground',
borderRadius: 5,
@@ -817,12 +598,6 @@
flex: 1,
flexDirection: 'row',
},
- label: {
- color: 'modalForegroundLabel',
- fontSize: 14,
- lineHeight: 17,
- textAlign: 'center',
- },
triangleDown: {
borderBottomColor: 'transparent',
borderBottomWidth: 0,

File Metadata

Mime Type
text/plain
Expires
Fri, Dec 20, 5:34 PM (17 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2681498
Default Alt Text
D6549.id21969.diff (56 KB)

Event Timeline