diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js
--- a/web/chat/composed-message.react.js
+++ b/web/chat/composed-message.react.js
@@ -9,7 +9,6 @@
 } from 'react-feather';
 
 import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
-import { useSidebarExistsOrCanBeCreated } from 'lib/shared/thread-utils';
 import { stringForUser } from 'lib/shared/user-utils';
 import { assertComposableMessageType } from 'lib/types/message-types';
 import { type ThreadInfo } from 'lib/types/thread-types';
@@ -18,31 +17,33 @@
 import css from './chat-message-list.css';
 import FailedSend from './failed-send.react';
 import { InlineSidebar } from './inline-sidebar.react';
-import MessageTooltip from './message-tooltip.react';
-import {
-  type OnMessagePositionWithContainerInfo,
-  type MessagePositionInfo,
-} from './position-types';
-import { tooltipPositions } from './tooltip-utils';
+import { tooltipPositions, useMessageTooltip } from './tooltip-utils';
 
 const availableTooltipPositionsForViewerMessage = [
-  tooltipPositions.RIGHT_TOP,
   tooltipPositions.LEFT,
+  tooltipPositions.LEFT_BOTTOM,
+  tooltipPositions.LEFT_TOP,
+  tooltipPositions.RIGHT,
+  tooltipPositions.RIGHT_BOTTOM,
+  tooltipPositions.RIGHT_TOP,
+  tooltipPositions.BOTTOM,
+  tooltipPositions.TOP,
 ];
 const availableTooltipPositionsForNonViewerMessage = [
-  tooltipPositions.LEFT_TOP,
   tooltipPositions.RIGHT,
+  tooltipPositions.RIGHT_BOTTOM,
+  tooltipPositions.RIGHT_TOP,
+  tooltipPositions.LEFT,
+  tooltipPositions.LEFT_BOTTOM,
+  tooltipPositions.LEFT_TOP,
+  tooltipPositions.BOTTOM,
+  tooltipPositions.TOP,
 ];
 
 type BaseProps = {
   +item: ChatMessageInfoItem,
   +threadInfo: ThreadInfo,
   +sendFailed: boolean,
-  +setMouseOverMessagePosition: (
-    messagePositionInfo: MessagePositionInfo,
-  ) => void,
-  +mouseOverMessagePosition?: ?OnMessagePositionWithContainerInfo,
-  +canReply: boolean,
   +children: React.Node,
   +fixedWidth?: boolean,
   +borderRadius: number,
@@ -50,10 +51,11 @@
 type BaseConfig = React.Config<BaseProps, typeof ComposedMessage.defaultProps>;
 type Props = {
   ...BaseProps,
-  // Redux state
-  +sidebarExistsOrCanBeCreated: boolean,
   // withInputState
   +inputState: ?InputState,
+  +onMouseLeave: ?() => mixed,
+  +onMouseEnter: (event: SyntheticEvent<HTMLDivElement>) => mixed,
+  +containsInlineSidebar: boolean,
 };
 class ComposedMessage extends React.PureComponent<Props> {
   static defaultProps: { +borderRadius: number } = {
@@ -118,38 +120,8 @@
       );
     }
 
-    let messageTooltip;
-    if (
-      this.props.mouseOverMessagePosition &&
-      this.props.mouseOverMessagePosition.item.messageInfo.id === id &&
-      (this.props.sidebarExistsOrCanBeCreated || this.props.canReply)
-    ) {
-      // eslint-disable-next-line no-unused-vars
-      const availableTooltipPositions = isViewer
-        ? availableTooltipPositionsForViewerMessage
-        : availableTooltipPositionsForNonViewerMessage;
-
-      messageTooltip = <MessageTooltip messageTimestamp="" actions={[]} />;
-    }
-
-    let messageTooltipLinks;
-    if (messageTooltip) {
-      const tooltipLinksClassName = classNames({
-        [css.messageTooltipActiveArea]: true,
-        [css.viewerMessageTooltipActiveArea]: isViewer,
-        [css.nonViewerMessageActiveArea]: !isViewer,
-      });
-
-      messageTooltipLinks = (
-        <div className={tooltipLinksClassName}>{messageTooltip}</div>
-      );
-    }
-
-    const viewerTooltipLinks = isViewer ? messageTooltipLinks : null;
-    const nonViewerTooltipLinks = !isViewer ? messageTooltipLinks : null;
-
     let inlineSidebar = null;
-    if (item.threadCreatedFromMessage) {
+    if (this.props.containsInlineSidebar && item.threadCreatedFromMessage) {
       const positioning = isViewer ? 'right' : 'left';
       inlineSidebar = (
         <div className={css.sidebarMarginBottom}>
@@ -167,14 +139,12 @@
         <div className={contentClassName}>
           <div
             className={messageBoxContainerClassName}
-            onMouseEnter={this.onMouseEnter}
-            onMouseLeave={this.onMouseLeave}
+            onMouseEnter={this.props.onMouseEnter}
+            onMouseLeave={this.props.onMouseLeave}
           >
-            {viewerTooltipLinks}
             <div className={messageBoxClassName} style={messageBoxStyle}>
               {this.props.children}
             </div>
-            {nonViewerTooltipLinks}
           </div>
           {deliveryIcon}
         </div>
@@ -183,23 +153,6 @@
       </React.Fragment>
     );
   }
-
-  onMouseEnter: (event: SyntheticEvent<HTMLDivElement>) => void = event => {
-    const { item } = this.props;
-    const rect = event.currentTarget.getBoundingClientRect();
-    const { top, bottom, left, right, height, width } = rect;
-    const messagePosition = { top, bottom, left, right, height, width };
-    this.props.setMouseOverMessagePosition({
-      type: 'on',
-      item,
-      messagePosition,
-    });
-  };
-
-  onMouseLeave: () => void = () => {
-    const { item } = this.props;
-    this.props.setMouseOverMessagePosition({ type: 'off', item });
-  };
 }
 
 type ConnectedConfig = React.Config<
@@ -208,16 +161,27 @@
 >;
 const ConnectedComposedMessage: React.ComponentType<ConnectedConfig> = React.memo<BaseConfig>(
   function ConnectedComposedMessage(props) {
-    const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated(
-      props.threadInfo,
-      props.item,
-    );
+    const { item, threadInfo } = props;
     const inputState = React.useContext(InputStateContext);
+    const isViewer = props.item.messageInfo.creator.isViewer;
+    const availablePositions = isViewer
+      ? availableTooltipPositionsForViewerMessage
+      : availableTooltipPositionsForNonViewerMessage;
+    const containsInlineSidebar = !!item.threadCreatedFromMessage;
+
+    const { onMouseLeave, onMouseEnter } = useMessageTooltip({
+      item,
+      threadInfo,
+      availablePositions,
+    });
+
     return (
       <ComposedMessage
         {...props}
-        sidebarExistsOrCanBeCreated={sidebarExistsOrCanBeCreated}
         inputState={inputState}
+        onMouseLeave={onMouseLeave}
+        onMouseEnter={onMouseEnter}
+        containsInlineSidebar={containsInlineSidebar}
       />
     );
   },
diff --git a/web/chat/message.react.js b/web/chat/message.react.js
--- a/web/chat/message.react.js
+++ b/web/chat/message.react.js
@@ -39,26 +39,12 @@
   }
   let message;
   if (item.messageInfo.type === messageTypes.TEXT) {
-    message = (
-      <TextMessage
-        item={item}
-        threadInfo={props.threadInfo}
-        setMouseOverMessagePosition={props.setMouseOverMessagePosition}
-        mouseOverMessagePosition={props.mouseOverMessagePosition}
-      />
-    );
+    message = <TextMessage item={item} threadInfo={props.threadInfo} />;
   } else if (
     item.messageInfo.type === messageTypes.IMAGES ||
     item.messageInfo.type === messageTypes.MULTIMEDIA
   ) {
-    message = (
-      <MultimediaMessage
-        item={item}
-        threadInfo={props.threadInfo}
-        setMouseOverMessagePosition={props.setMouseOverMessagePosition}
-        mouseOverMessagePosition={props.mouseOverMessagePosition}
-      />
-    );
+    message = <MultimediaMessage item={item} threadInfo={props.threadInfo} />;
   } else {
     invariant(item.robotext, "Flow can't handle our fancy types :(");
     message = (
diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js
--- a/web/chat/multimedia-message.react.js
+++ b/web/chat/multimedia-message.react.js
@@ -12,18 +12,9 @@
 import css from './chat-message-list.css';
 import ComposedMessage from './composed-message.react';
 import sendFailed from './multimedia-message-send-failed';
-import type {
-  MessagePositionInfo,
-  OnMessagePositionWithContainerInfo,
-} from './position-types';
-
 type BaseProps = {
   +item: ChatMessageInfoItem,
   +threadInfo: ThreadInfo,
-  +setMouseOverMessagePosition: (
-    messagePositionInfo: MessagePositionInfo,
-  ) => void,
-  +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo,
 };
 type Props = {
   ...BaseProps,
@@ -71,9 +62,6 @@
         item={item}
         threadInfo={this.props.threadInfo}
         sendFailed={sendFailed(item, inputState)}
-        setMouseOverMessagePosition={this.props.setMouseOverMessagePosition}
-        mouseOverMessagePosition={this.props.mouseOverMessagePosition}
-        canReply={false}
         fixedWidth={multimedia.length > 1}
         borderRadius={16}
       >
diff --git a/web/chat/text-message.react.js b/web/chat/text-message.react.js
--- a/web/chat/text-message.react.js
+++ b/web/chat/text-message.react.js
@@ -6,27 +6,19 @@
 
 import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
 import { onlyEmojiRegex } from 'lib/shared/emojis';
-import { colorIsDark, threadHasPermission } from 'lib/shared/thread-utils';
+import { colorIsDark } from 'lib/shared/thread-utils';
 import { messageTypes } from 'lib/types/message-types';
-import { type ThreadInfo, threadPermissions } from 'lib/types/thread-types';
+import { type ThreadInfo } from 'lib/types/thread-types';
 
 import Markdown from '../markdown/markdown.react';
 import css from './chat-message-list.css';
 import ComposedMessage from './composed-message.react';
 import { MessageListContext } from './message-list-types';
-import type {
-  MessagePositionInfo,
-  OnMessagePositionWithContainerInfo,
-} from './position-types';
 import textMessageSendFailed from './text-message-send-failed';
 
 type Props = {
   +item: ChatMessageInfoItem,
   +threadInfo: ThreadInfo,
-  +setMouseOverMessagePosition: (
-    messagePositionInfo: MessagePositionInfo,
-  ) => void,
-  +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo,
 };
 function TextMessage(props: Props): React.Node {
   invariant(
@@ -59,18 +51,11 @@
   const messageListContext = React.useContext(MessageListContext);
   invariant(messageListContext, 'DummyTextNode should have MessageListContext');
   const rules = messageListContext.getTextMessageMarkdownRules(darkColor);
-  const canReply = threadHasPermission(
-    props.threadInfo,
-    threadPermissions.VOICED,
-  );
   return (
     <ComposedMessage
       item={props.item}
       threadInfo={props.threadInfo}
       sendFailed={textMessageSendFailed(props.item)}
-      setMouseOverMessagePosition={props.setMouseOverMessagePosition}
-      mouseOverMessagePosition={props.mouseOverMessagePosition}
-      canReply={canReply}
     >
       <div className={messageClassName} style={messageStyle}>
         <Markdown rules={rules}>{text}</Markdown>
diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js
--- a/web/chat/tooltip-utils.js
+++ b/web/chat/tooltip-utils.js
@@ -12,9 +12,11 @@
 import { isComposableMessageType } from 'lib/types/message-types';
 import type { ThreadInfo } from 'lib/types/thread-types';
 import { threadPermissions } from 'lib/types/thread-types';
+import { longAbsoluteDate } from 'lib/utils/date-utils';
 
 import CommIcon from '../CommIcon.react';
 import { InputStateContext } from '../input/input-state';
+import { useSelector } from '../redux/redux-utils';
 import {
   useOnClickPendingSidebar,
   useOnClickThread,
@@ -25,7 +27,9 @@
   tooltipLabelStyle,
   tooltipStyle,
 } from './chat-constants';
+import MessageTooltip from './message-tooltip.react';
 import type { PositionInfo } from './position-types';
+import { useTooltipContext } from './tooltip-provider';
 
 export const tooltipPositions = Object.freeze({
   LEFT: 'left',
@@ -401,6 +405,111 @@
   ]);
 }
 
+type UseMessageTooltipArgs = {
+  +availablePositions: $ReadOnlyArray<TooltipPosition>,
+  +item: ChatMessageInfoItem,
+  +threadInfo: ThreadInfo,
+};
+
+type UseMessageTooltipResult = {
+  onMouseEnter: (event: SyntheticEvent<HTMLElement>) => void,
+  onMouseLeave: ?() => mixed,
+};
+
+function useMessageTooltip({
+  availablePositions,
+  item,
+  threadInfo,
+}: UseMessageTooltipArgs): UseMessageTooltipResult {
+  const [onMouseLeave, setOnMouseLeave] = React.useState<?() => mixed>(null);
+
+  const { renderTooltip } = useTooltipContext();
+  const tooltipActions = useMessageTooltipActions(item, threadInfo);
+
+  const containsInlineSidebar = !!item.threadCreatedFromMessage;
+
+  const timeZone = useSelector(state => state.timeZone);
+
+  const messageTimestamp = React.useMemo(() => {
+    const time = item.messageInfo.time;
+    return longAbsoluteDate(time, timeZone);
+  }, [item.messageInfo.time, timeZone]);
+
+  const tooltipSize = React.useMemo(() => {
+    if (typeof document === 'undefined') {
+      return {
+        width: 0,
+        height: 0,
+      };
+    }
+    const tooltipLabels = tooltipActions.map(action => action.label);
+    return calculateTooltipSize({
+      tooltipLabels,
+      timestamp: messageTimestamp,
+    });
+  }, [messageTimestamp, tooltipActions]);
+
+  const onMouseEnter = React.useCallback(
+    (event: SyntheticEvent<HTMLElement>) => {
+      if (!renderTooltip) {
+        return;
+      }
+      const rect = event.currentTarget.getBoundingClientRect();
+      const { top, bottom, left, right, height, width } = rect;
+      const messagePosition = { top, bottom, left, right, height, width };
+
+      const tooltipPosition = findTooltipPosition({
+        sourcePositionInfo: messagePosition,
+        tooltipSize,
+        availablePositions,
+        defaultPosition: availablePositions[0],
+        preventDisplayingBelowSource: containsInlineSidebar,
+      });
+      if (!tooltipPosition) {
+        return;
+      }
+
+      const tooltipPositionStyle = getMessageActionTooltipStyle({
+        tooltipPosition,
+        sourcePositionInfo: messagePosition,
+        tooltipSize: tooltipSize,
+      });
+
+      const { alignment } = tooltipPositionStyle;
+
+      const tooltip = (
+        <MessageTooltip
+          actions={tooltipActions}
+          messageTimestamp={messageTimestamp}
+          alignment={alignment}
+        />
+      );
+
+      const renderTooltipResult = renderTooltip({
+        newNode: tooltip,
+        tooltipPositionStyle,
+      });
+      if (renderTooltipResult) {
+        const { onMouseLeaveCallback: callback } = renderTooltipResult;
+        setOnMouseLeave((() => callback: () => () => mixed));
+      }
+    },
+    [
+      availablePositions,
+      containsInlineSidebar,
+      messageTimestamp,
+      renderTooltip,
+      tooltipActions,
+      tooltipSize,
+    ],
+  );
+
+  return {
+    onMouseEnter,
+    onMouseLeave,
+  };
+}
+
 export {
   findTooltipPosition,
   calculateTooltipSize,
@@ -408,5 +517,6 @@
   useMessageTooltipSidebarAction,
   useMessageTooltipReplyAction,
   useMessageTooltipActions,
+  useMessageTooltip,
   sizeOfTooltipArrow,
 };