diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js
index b92339f73..177d33892 100644
--- a/web/chat/chat-thread-list-item.react.js
+++ b/web/chat/chat-thread-list-item.react.js
@@ -1,151 +1,151 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import { useAncestorThreads } from 'lib/shared/ancestor-threads';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
import { useSelector } from '../redux/redux-utils';
import {
useOnClickThread,
useThreadIsActive,
} from '../selectors/nav-selectors';
import SWMansionIcon from '../SWMansionIcon.react';
import ChatThreadListItemMenu from './chat-thread-list-item-menu.react';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react';
import css from './chat-thread-list.css';
import MessagePreview from './message-preview.react';
type Props = {
+item: ChatThreadItem,
+setModal: (modal: ?React.Node) => void,
};
function ChatThreadListItem(props: Props): React.Node {
const { item, setModal } = props;
const { threadInfo, lastUpdatedTimeIncludingSidebars } = item;
const threadID = item.threadInfo.id;
const ancestorThreads = useAncestorThreads(threadInfo);
- const onClick = useOnClickThread(threadID);
+ const onClick = useOnClickThread(item.threadInfo);
const timeZone = useSelector(state => state.timeZone);
const lastActivity = shortAbsoluteDate(
lastUpdatedTimeIncludingSidebars,
timeZone,
);
const active = useThreadIsActive(threadID);
const containerClassName = React.useMemo(
() =>
classNames({
[css.thread]: true,
[css.activeThread]: active,
}),
[active],
);
const { unread } = item.threadInfo.currentUser;
const titleClassName = React.useMemo(
() =>
classNames({
[css.title]: true,
[css.unread]: unread,
}),
[unread],
);
const lastActivityClassName = React.useMemo(
() =>
classNames({
[css.lastActivity]: true,
[css.unread]: unread,
[css.dark]: !unread,
}),
[unread],
);
const breadCrumbsClassName = React.useMemo(
() =>
classNames(css.breadCrumbs, {
[css.unread]: unread,
}),
[unread],
);
const { color } = item.threadInfo;
const colorSplotchStyle = React.useMemo(
() => ({ backgroundColor: `#${color}` }),
[color],
);
const sidebars = item.sidebars.map(sidebarItem => {
if (sidebarItem.type === 'sidebar') {
const { type, ...sidebarInfo } = sidebarItem;
return (
);
} else if (sidebarItem.type === 'seeMore') {
return (
);
} else {
return
;
}
});
const ancestorPath = ancestorThreads.map((thread, idx) => {
const isNotLast = idx !== ancestorThreads.length - 1;
const chevron = isNotLast && (
);
return (
{thread.uiName}
{chevron}
);
});
return (
<>
{sidebars}
>
);
}
export default ChatThreadListItem;
diff --git a/web/chat/inline-sidebar.react.js b/web/chat/inline-sidebar.react.js
index f7f9ae10b..d9ef13e83 100644
--- a/web/chat/inline-sidebar.react.js
+++ b/web/chat/inline-sidebar.react.js
@@ -1,78 +1,78 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import {
CornerDownRight as CornerDownRightIcon,
CornerDownLeft as CornerDownLeftIcon,
} from 'react-feather';
import { relativeMemberInfoSelectorForMembersOfThread } from 'lib/selectors/user-selectors';
import { stringForUser } from 'lib/shared/user-utils';
import type { ThreadInfo } from 'lib/types/thread-types';
import { pluralizeAndTrim } from 'lib/utils/text-utils';
import { useSelector } from '../redux/redux-utils';
import { useOnClickThread } from '../selectors/nav-selectors';
import css from './inline-sidebar.css';
type Props = {
+threadInfo: ThreadInfo,
+positioning: 'left' | 'center' | 'right',
};
function InlineSidebar(props: Props): React.Node {
const { threadInfo } = props;
- const onClick = useOnClickThread(threadInfo.id);
+ const onClick = useOnClickThread(threadInfo);
let viewerIcon, nonViewerIcon, alignStyle;
if (props.positioning === 'right') {
viewerIcon = (
);
alignStyle = css.viewerMessageBoxContainer;
} else if (props.positioning === 'left') {
nonViewerIcon = (
);
alignStyle = css.nonViewerMessageBoxContainer;
} else {
nonViewerIcon = (
);
alignStyle = css.centerContainer;
}
const unreadStyle = threadInfo.currentUser.unread ? css.unread : null;
const repliesCount = threadInfo.repliesCount || 1;
const repliesText = `${repliesCount} ${
repliesCount > 1 ? 'replies' : 'reply'
}`;
const threadMembers = useSelector(
relativeMemberInfoSelectorForMembersOfThread(threadInfo.id),
);
const sendersText = React.useMemo(() => {
const senders = threadMembers
.filter(member => member.isSender)
.map(stringForUser);
return senders.length > 0 ? `${pluralizeAndTrim(senders, 25)} sent ` : '';
}, [threadMembers]);
return (
{nonViewerIcon}
{sendersText}
{repliesText}
{viewerIcon}
);
}
const inlineSidebarHeight = 20;
export { InlineSidebar, inlineSidebarHeight };
diff --git a/web/chat/message-action-button.js b/web/chat/message-action-button.js
index e2e3ef309..57e977e16 100644
--- a/web/chat/message-action-button.js
+++ b/web/chat/message-action-button.js
@@ -1,168 +1,168 @@
// @flow
import classNames from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import type { ThreadInfo } from 'lib/types/thread-types';
import {
useOnClickThread,
useOnClickPendingSidebar,
} from '../selectors/nav-selectors';
import SWMansionIcon from '../SWMansionIcon.react';
import css from './message-action-button.css';
import type {
ItemAndContainerPositionInfo,
PositionInfo,
} from './position-types';
import { tooltipPositions, type TooltipPosition } from './tooltip-utils';
import { TooltipMenu, type TooltipStyle, TooltipButton } from './tooltip.react';
const ellipsisIconExcessVerticalWhitespace = 10;
const openSidebarText = 'Go to sidebar';
const createSidebarText = 'Create sidebar';
type MessageActionTooltipProps = {
+threadInfo: ThreadInfo,
+item: ChatMessageInfoItem,
+containerPosition: PositionInfo,
+availableTooltipPositions: $ReadOnlyArray,
};
function MessageActionButton(props: MessageActionTooltipProps): React.Node {
const {
threadInfo,
item,
containerPosition,
availableTooltipPositions,
} = props;
const [tooltipVisible, setTooltipVisible] = React.useState(false);
const [pointingTo, setPointingTo] = React.useState();
const toggleTooltip = React.useCallback(
(event: SyntheticEvent) => {
setTooltipVisible(!tooltipVisible);
if (tooltipVisible) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const { top, bottom, left, right, width, height } = rect;
const dotsPosition: ItemAndContainerPositionInfo = {
containerPosition,
itemPosition: {
top:
top - containerPosition.top + ellipsisIconExcessVerticalWhitespace,
bottom:
bottom -
containerPosition.top -
ellipsisIconExcessVerticalWhitespace,
left: left - containerPosition.left,
right: right - containerPosition.left,
width,
height: height - ellipsisIconExcessVerticalWhitespace * 2,
},
};
setPointingTo(dotsPosition);
},
[containerPosition, tooltipVisible],
);
const hideTooltip = React.useCallback(() => {
setTooltipVisible(false);
}, []);
const { threadCreatedFromMessage, messageInfo } = item;
- const onThreadOpen = useOnClickThread(threadCreatedFromMessage?.id);
+ const onThreadOpen = useOnClickThread(threadCreatedFromMessage);
const onPendingSidebarOpen = useOnClickPendingSidebar(
messageInfo,
threadInfo,
);
const onSidebarButtonClick = React.useCallback(
(event: SyntheticEvent) => {
if (threadCreatedFromMessage) {
onThreadOpen(event);
} else {
onPendingSidebarOpen(event);
}
},
[onPendingSidebarOpen, onThreadOpen, threadCreatedFromMessage],
);
const sidebarTooltipButtonText = threadCreatedFromMessage
? openSidebarText
: createSidebarText;
let tooltipMenu = null;
if (pointingTo && tooltipVisible) {
tooltipMenu = (
);
}
return (
);
}
function getMessageActionTooltipStyle(
tooltipPosition: TooltipPosition,
): TooltipStyle {
let className;
if (tooltipPosition === tooltipPositions.TOP_RIGHT) {
className = classNames(
css.messageActionTopRightTooltip,
css.messageTopRightTooltip,
css.messageActionExtraAreaTop,
css.messageActionExtraAreaTopRight,
);
} else if (tooltipPosition === tooltipPositions.TOP_LEFT) {
className = classNames(
css.messageActionTopLeftTooltip,
css.messageTopLeftTooltip,
css.messageActionExtraAreaTop,
css.messageActionExtraAreaTopLeft,
);
} else if (tooltipPosition === tooltipPositions.RIGHT) {
className = classNames(
css.messageActionRightTooltip,
css.messageRightTooltip,
css.messageActionExtraArea,
css.messageActionExtraAreaRight,
);
} else if (tooltipPosition === tooltipPositions.LEFT) {
className = classNames(
css.messageActionLeftTooltip,
css.messageLeftTooltip,
css.messageActionExtraArea,
css.messageActionExtraAreaLeft,
);
}
invariant(className, `${tooltipPosition} is not valid for message tooltip`);
return { className };
}
export default MessageActionButton;
diff --git a/web/chat/sidebar-item.react.js b/web/chat/sidebar-item.react.js
index a0193f713..f44e8adbe 100644
--- a/web/chat/sidebar-item.react.js
+++ b/web/chat/sidebar-item.react.js
@@ -1,35 +1,35 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import type { SidebarInfo } from 'lib/types/thread-types';
import { useOnClickThread } from '../selectors/nav-selectors';
import SWMansionIcon from '../SWMansionIcon.react';
import css from './chat-thread-list.css';
type Props = {
+sidebarInfo: SidebarInfo,
};
function SidebarItem(props: Props): React.Node {
const { threadInfo } = props.sidebarInfo;
- const threadID = threadInfo.id;
- const onClick = useOnClickThread(threadID);
+ const onClick = useOnClickThread(threadInfo);
+
const { unread } = threadInfo.currentUser;
const unreadCls = classNames(css.sidebarTitle, { [css.unread]: unread });
return (
<>
>
);
}
export default SidebarItem;
diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js
index 525ed040a..3e71299a2 100644
--- a/web/selectors/nav-selectors.js
+++ b/web/selectors/nav-selectors.js
@@ -1,194 +1,205 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { useDispatch } from 'react-redux';
import { createSelector } from 'reselect';
import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors';
import { currentCalendarQuery } from 'lib/selectors/nav-selectors';
import { createPendingSidebar } from 'lib/shared/thread-utils';
import type { CalendarQuery } from 'lib/types/entry-types';
import type { CalendarFilter } from 'lib/types/filter-types';
import type {
ComposableMessageInfo,
RobotextMessageInfo,
} from 'lib/types/message-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { getDefaultTextMessageRules } from '../markdown/rules.react';
import type { AppState } from '../redux/redux-setup';
import { useSelector } from '../redux/redux-utils';
import { updateNavInfoActionType } from '../types/nav-types';
const dateExtractionRegex = /^([0-9]{4})-([0-9]{2})-[0-9]{2}$/;
function yearExtractor(startDate: string, endDate: string): ?number {
const startDateResults = dateExtractionRegex.exec(startDate);
const endDateResults = dateExtractionRegex.exec(endDate);
if (
!startDateResults ||
!startDateResults[1] ||
!endDateResults ||
!endDateResults[1] ||
startDateResults[1] !== endDateResults[1]
) {
return null;
}
return parseInt(startDateResults[1], 10);
}
function yearAssertingExtractor(startDate: string, endDate: string): number {
const result = yearExtractor(startDate, endDate);
invariant(
result !== null && result !== undefined,
`${startDate} and ${endDate} aren't in the same year`,
);
return result;
}
const yearAssertingSelector: (state: AppState) => number = createSelector(
(state: AppState) => state.navInfo.startDate,
(state: AppState) => state.navInfo.endDate,
yearAssertingExtractor,
);
// 1-indexed
function monthExtractor(startDate: string, endDate: string): ?number {
const startDateResults = dateExtractionRegex.exec(startDate);
const endDateResults = dateExtractionRegex.exec(endDate);
if (
!startDateResults ||
!startDateResults[1] ||
!startDateResults[2] ||
!endDateResults ||
!endDateResults[1] ||
!endDateResults[2] ||
startDateResults[1] !== endDateResults[1] ||
startDateResults[2] !== endDateResults[2]
) {
return null;
}
return parseInt(startDateResults[2], 10);
}
// 1-indexed
function monthAssertingExtractor(startDate: string, endDate: string): number {
const result = monthExtractor(startDate, endDate);
invariant(
result !== null && result !== undefined,
`${startDate} and ${endDate} aren't in the same month`,
);
return result;
}
// 1-indexed
const monthAssertingSelector: (state: AppState) => number = createSelector(
(state: AppState) => state.navInfo.startDate,
(state: AppState) => state.navInfo.endDate,
monthAssertingExtractor,
);
function activeThreadSelector(state: AppState): ?string {
return state.navInfo.tab === 'chat' ? state.navInfo.activeChatThreadID : null;
}
const webCalendarQuery: (
state: AppState,
) => () => CalendarQuery = createSelector(
currentCalendarQuery,
(state: AppState) => state.navInfo.tab === 'calendar',
(
calendarQuery: (calendarActive: boolean) => CalendarQuery,
calendarActive: boolean,
) => () => calendarQuery(calendarActive),
);
const nonThreadCalendarQuery: (
state: AppState,
) => () => CalendarQuery = createSelector(
webCalendarQuery,
nonThreadCalendarFiltersSelector,
(
calendarQuery: () => CalendarQuery,
filters: $ReadOnlyArray,
) => {
return (): CalendarQuery => {
const query = calendarQuery();
return {
startDate: query.startDate,
endDate: query.endDate,
filters,
};
};
},
);
function useOnClickThread(
- threadID: ?string,
+ thread: ?ThreadInfo,
): (event: SyntheticEvent) => void {
const dispatch = useDispatch();
return React.useCallback(
(event: SyntheticEvent) => {
invariant(
- threadID,
+ thread?.id,
'useOnClickThread should be called with threadID set',
);
event.preventDefault();
- dispatch({
- type: updateNavInfoActionType,
- payload: {
- activeChatThreadID: threadID,
- },
- });
+ const { id: threadID } = thread;
+ if (threadID.includes('pending')) {
+ dispatch({
+ type: updateNavInfoActionType,
+ payload: {
+ activeChatThreadID: threadID,
+ pendingThread: thread,
+ },
+ });
+ } else {
+ dispatch({
+ type: updateNavInfoActionType,
+ payload: {
+ activeChatThreadID: threadID,
+ },
+ });
+ }
},
- [dispatch, threadID],
+ [dispatch, thread],
);
}
function useThreadIsActive(threadID: string): boolean {
return useSelector(state => threadID === state.navInfo.activeChatThreadID);
}
function useOnClickPendingSidebar(
messageInfo: ComposableMessageInfo | RobotextMessageInfo,
threadInfo: ThreadInfo,
): (event: SyntheticEvent) => void {
const dispatch = useDispatch();
const viewerID = useSelector(state => state.currentUserInfo?.id);
return React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
if (!viewerID) {
return;
}
const pendingSidebarInfo = createPendingSidebar(
messageInfo,
threadInfo,
viewerID,
getDefaultTextMessageRules().simpleMarkdownRules,
);
dispatch({
type: updateNavInfoActionType,
payload: {
activeChatThreadID: pendingSidebarInfo.id,
pendingThread: pendingSidebarInfo,
},
});
},
[viewerID, messageInfo, threadInfo, dispatch],
);
}
export {
yearExtractor,
yearAssertingSelector,
monthExtractor,
monthAssertingSelector,
activeThreadSelector,
webCalendarQuery,
nonThreadCalendarQuery,
useOnClickThread,
useThreadIsActive,
useOnClickPendingSidebar,
};