diff --git a/web/app-list/app-list-item.react.js b/web/app-list/app-list-item.react.js
index 4cdb67477..a912c13f0 100644
--- a/web/app-list/app-list-item.react.js
+++ b/web/app-list/app-list-item.react.js
@@ -1,39 +1,39 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import SWMansionIcon, {
type Icon,
} from 'lib/components/SWMansionIcon.react.js';
import css from './app-list-item.css';
import { useSelector } from '../redux/redux-utils.js';
import { navTabSelector } from '../selectors/nav-selectors.js';
-import type { NavigationTab } from '../types/nav-types.js';
+import type { WebNavigationTab } from '../types/nav-types.js';
type Props = {
- +id: NavigationTab,
+ +id: WebNavigationTab,
+name: string,
+icon: Icon,
+onClick: () => mixed,
};
function AppListItem(props: Props): React.Node {
const { id, name, icon, onClick } = props;
const currentSelectedApp = useSelector(navTabSelector);
const className = classNames(css.container, {
[css.selected]: currentSelectedApp === id,
});
return (
);
}
export default AppListItem;
diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js
index 571f2b4cb..c86996cd5 100644
--- a/web/selectors/nav-selectors.js
+++ b/web/selectors/nav-selectors.js
@@ -1,140 +1,140 @@
// @flow
import invariant from 'invariant';
import { createSelector } from 'reselect';
import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js';
import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import type { CalendarFilter } from 'lib/types/filter-types.js';
import type { AppState } from '../redux/redux-setup.js';
import {
- type NavigationTab,
+ type WebNavigationTab,
type NavigationSettingsSection,
} from '../types/nav-types.js';
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 navTabSelector(state: AppState): NavigationTab {
+function navTabSelector(state: AppState): WebNavigationTab {
return state.navInfo.tab;
}
function navSettingsSectionSelector(
state: AppState,
): ?NavigationSettingsSection {
return state.navInfo.settingsSection;
}
export {
yearExtractor,
yearAssertingSelector,
monthExtractor,
monthAssertingSelector,
activeThreadSelector,
webCalendarQuery,
nonThreadCalendarQuery,
navTabSelector,
navSettingsSectionSelector,
};
diff --git a/web/sidebar/community-drawer-item-community-handlers.react.js b/web/sidebar/community-drawer-item-community-handlers.react.js
index d3082a8b2..5da84351f 100644
--- a/web/sidebar/community-drawer-item-community-handlers.react.js
+++ b/web/sidebar/community-drawer-item-community-handlers.react.js
@@ -1,116 +1,116 @@
// @flow
import * as React from 'react';
import {
clearChatCommunityFilter,
updateCalendarCommunityFilter,
updateChatCommunityFilter,
} from 'lib/actions/community-actions.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import type { CommunityDrawerItemCommunityHandler } from './community-drawer-item-handler.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useCommunityIsPickedCalendar } from '../selectors/calendar-selectors.js';
import {
useOnClickThread,
useThreadIsActive,
} from '../selectors/thread-selectors.js';
-import type { NavigationTab } from '../types/nav-types.js';
+import type { WebNavigationTab } from '../types/nav-types.js';
export type HandlerProps = {
+setHandler: (handler: CommunityDrawerItemCommunityHandler) => void,
+threadInfo: ThreadInfo,
};
function ChatDrawerItemCommunityHandler(props: HandlerProps): React.Node {
const { setHandler, threadInfo } = props;
const onClickThread = useOnClickThread(threadInfo);
const isActive = useThreadIsActive(threadInfo.id);
const dispatch = useDispatch();
const openCommunityID = useSelector(state => state.communityPickerStore.chat);
const expanded = openCommunityID === threadInfo.id;
const onClick = React.useCallback(
(event: SyntheticEvent) => {
if (!isActive) {
onClickThread(event);
}
if (openCommunityID === threadInfo.id && isActive) {
dispatch({
type: clearChatCommunityFilter,
});
return;
}
const community = threadInfo.community ?? threadInfo.id;
dispatch({
type: updateChatCommunityFilter,
payload: community,
});
},
[
dispatch,
isActive,
onClickThread,
openCommunityID,
threadInfo.community,
threadInfo.id,
],
);
const handler = React.useMemo(
() => ({ onClick, isActive, expanded }),
[expanded, isActive, onClick],
);
React.useEffect(() => {
setHandler(handler);
}, [handler, setHandler]);
return null;
}
function CalendarDrawerItemCommunityHandler(props: HandlerProps): React.Node {
const { setHandler, threadInfo } = props;
const dispatch = useDispatch();
const onClick = React.useCallback(() => {
dispatch({
type: updateCalendarCommunityFilter,
payload: threadInfo.id,
});
}, [dispatch, threadInfo.id]);
const isActive = useCommunityIsPickedCalendar(threadInfo.id);
const expanded = false;
const handler = React.useMemo(
() => ({ onClick, isActive, expanded }),
[onClick, isActive, expanded],
);
React.useEffect(() => {
setHandler(handler);
}, [handler, setHandler]);
return null;
}
const communityDrawerItemCommunityHandlers: {
- +[tab: NavigationTab]: React.ComponentType,
+ +[tab: WebNavigationTab]: React.ComponentType,
} = Object.freeze({
chat: ChatDrawerItemCommunityHandler,
calendar: CalendarDrawerItemCommunityHandler,
});
function getCommunityDrawerItemCommunityHandler(
- tab: NavigationTab,
+ tab: WebNavigationTab,
): React.ComponentType {
return (
communityDrawerItemCommunityHandlers[tab] ?? ChatDrawerItemCommunityHandler
);
}
export { getCommunityDrawerItemCommunityHandler };
diff --git a/web/sidebar/community-drawer-item-handlers.react.js b/web/sidebar/community-drawer-item-handlers.react.js
index 97c312567..99941e084 100644
--- a/web/sidebar/community-drawer-item-handlers.react.js
+++ b/web/sidebar/community-drawer-item-handlers.react.js
@@ -1,74 +1,74 @@
// @flow
import * as React from 'react';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { CommunityDrawerItemHandler } from './community-drawer-item-handler.react.js';
import { useCommunityIsPickedCalendar } from '../selectors/calendar-selectors.js';
import {
useOnClickThread,
useThreadIsActive,
} from '../selectors/thread-selectors.js';
-import type { NavigationTab } from '../types/nav-types.js';
+import type { WebNavigationTab } from '../types/nav-types.js';
export type HandlerProps = {
+setHandler: (handler: CommunityDrawerItemHandler) => void,
+threadInfo: ThreadInfo,
};
function ChatDrawerItemHandler(props: HandlerProps): React.Node {
const { setHandler, threadInfo } = props;
const onClick = useOnClickThread(threadInfo);
const isActive = useThreadIsActive(threadInfo.id);
const [expanded, setExpanded] = React.useState(false);
const toggleExpanded = React.useCallback(() => {
setExpanded(isExpanded => !isExpanded);
}, []);
const handler = React.useMemo(
() => ({ onClick, isActive, expanded, toggleExpanded }),
[expanded, isActive, onClick, toggleExpanded],
);
React.useEffect(() => {
setHandler(handler);
}, [handler, setHandler]);
return null;
}
const onClick = () => {};
const expanded = false;
const toggleExpanded = () => {};
function CalendarDrawerItemHandler(props: HandlerProps): React.Node {
const { setHandler, threadInfo } = props;
const isActive = useCommunityIsPickedCalendar(threadInfo.id);
const handler = React.useMemo(
() => ({ onClick, isActive, expanded, toggleExpanded }),
[isActive],
);
React.useEffect(() => {
setHandler(handler);
}, [handler, setHandler]);
return null;
}
const communityDrawerItemHandlers: {
- +[tab: NavigationTab]: React.ComponentType,
+ +[tab: WebNavigationTab]: React.ComponentType,
} = Object.freeze({
chat: ChatDrawerItemHandler,
calendar: CalendarDrawerItemHandler,
});
function getCommunityDrawerItemHandler(
- tab: NavigationTab,
+ tab: WebNavigationTab,
): React.ComponentType {
return communityDrawerItemHandlers[tab] ?? ChatDrawerItemHandler;
}
export { getCommunityDrawerItemHandler };
diff --git a/web/sidebar/community-drawer-item.react.js b/web/sidebar/community-drawer-item.react.js
index 3b5982241..d4205cb3a 100644
--- a/web/sidebar/community-drawer-item.react.js
+++ b/web/sidebar/community-drawer-item.react.js
@@ -1,118 +1,118 @@
// @flow
import classnames from 'classnames';
import * as React from 'react';
import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import type { CommunityDrawerItemHandler } from './community-drawer-item-handler.react.js';
import type { HandlerProps } from './community-drawer-item-handlers.react.js';
import { getCommunityDrawerItemHandler } from './community-drawer-item-handlers.react.js';
import css from './community-drawer-item.css';
import {
getChildren,
getExpandButton,
} from './community-drawer-utils.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
-import type { NavigationTab } from '../types/nav-types.js';
+import type { WebNavigationTab } from '../types/nav-types.js';
export type DrawerItemProps = {
+itemData: CommunityDrawerItemData,
+paddingLeft: number,
+expandable?: boolean,
- +handlerType: NavigationTab,
+ +handlerType: WebNavigationTab,
};
function CommunityDrawerItem(props: DrawerItemProps): React.Node {
const {
itemData: { threadInfo, itemChildren, hasSubchannelsButton, labelStyle },
paddingLeft,
expandable = true,
handlerType,
} = props;
const [handler, setHandler] = React.useState({
onClick: () => {},
expanded: false,
toggleExpanded: () => {},
isActive: false,
});
const Handler = getCommunityDrawerItemHandler(handlerType);
const children = React.useMemo(
() =>
getChildren({
expanded: handler.expanded,
hasSubchannelsButton,
itemChildren,
paddingLeft,
threadInfo,
expandable,
handlerType,
}),
[
handler.expanded,
hasSubchannelsButton,
itemChildren,
paddingLeft,
threadInfo,
expandable,
handlerType,
],
);
const itemExpandButton = React.useMemo(
() =>
getExpandButton({
expandable,
childrenLength: itemChildren.length,
hasSubchannelsButton,
onExpandToggled: handler.toggleExpanded,
expanded: handler.expanded,
}),
[
expandable,
itemChildren.length,
hasSubchannelsButton,
handler.toggleExpanded,
handler.expanded,
],
);
const { uiName } = useResolvedThreadInfo(threadInfo);
const titleLabel = classnames({
[css[labelStyle]]: true,
[css.activeTitle]: handler.isActive,
});
const style = React.useMemo(() => ({ paddingLeft }), [paddingLeft]);
return (
<>
{children}
>
);
}
export type CommunityDrawerItemChatProps = {
+itemData: CommunityDrawerItemData,
+paddingLeft: number,
+expandable?: boolean,
+handler: React.ComponentType,
};
const MemoizedCommunityDrawerItem: React.ComponentType =
React.memo(CommunityDrawerItem);
export default MemoizedCommunityDrawerItem;
diff --git a/web/sidebar/community-drawer-utils.react.js b/web/sidebar/community-drawer-utils.react.js
index fec27951a..8645ff2f8 100644
--- a/web/sidebar/community-drawer-utils.react.js
+++ b/web/sidebar/community-drawer-utils.react.js
@@ -1,89 +1,89 @@
// @flow
import * as React from 'react';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react';
import css from './community-drawer-item.css';
import CommunityDrawerItemChat from './community-drawer-item.react.js';
import { ExpandButton } from './expand-buttons.react.js';
import SubchannelsButton from './subchannels-button.react.js';
-import type { NavigationTab } from '../types/nav-types.js';
+import type { WebNavigationTab } from '../types/nav-types.js';
const indentation = 14;
const subchannelsButtonIndentation = 24;
function getChildren({
expanded,
hasSubchannelsButton,
itemChildren,
paddingLeft,
threadInfo,
expandable,
handlerType,
}: {
expanded: boolean,
hasSubchannelsButton: boolean,
itemChildren: $ReadOnlyArray>,
paddingLeft: number,
threadInfo: ThreadInfo,
expandable: boolean,
- handlerType: NavigationTab,
+ handlerType: WebNavigationTab,
}): React.Node {
if (!expanded) {
return null;
}
if (hasSubchannelsButton) {
const buttonPaddingLeft = paddingLeft + subchannelsButtonIndentation;
return (
);
}
return itemChildren.map(item => (
));
}
function getExpandButton({
expandable,
childrenLength,
hasSubchannelsButton,
onExpandToggled,
expanded,
}: {
+expandable: boolean,
+childrenLength: ?number,
+hasSubchannelsButton: boolean,
+onExpandToggled?: ?() => ?void,
+expanded: boolean,
}): React.Node {
if (!expandable) {
return null;
}
if (childrenLength === 0 && !hasSubchannelsButton) {
return (
);
}
return (
);
}
export { getChildren, getExpandButton };
diff --git a/web/types/nav-types.js b/web/types/nav-types.js
index 689a2f34e..04401dca6 100644
--- a/web/types/nav-types.js
+++ b/web/types/nav-types.js
@@ -1,60 +1,60 @@
// @flow
import type { TInterface } from 'tcomb';
import t from 'tcomb';
import { threadInfoValidator } from 'lib/permissions/minimally-encoded-thread-permissions-validators.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import { type BaseNavInfo } from 'lib/types/nav-types.js';
import {
type AccountUserInfo,
accountUserInfoValidator,
} from 'lib/types/user-types.js';
import { tID, tShape } from 'lib/utils/validation-utils.js';
-export type NavigationTab = 'calendar' | 'chat' | 'settings';
-const navigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']);
+export type WebNavigationTab = 'calendar' | 'chat' | 'settings';
+const webNavigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']);
export type LoginMethod = 'form' | 'qr-code';
const loginMethodValidator = t.enums.of(['form', 'qr-code']);
export type NavigationSettingsSection =
| 'account'
| 'friend-list'
| 'block-list'
| 'keyservers'
| 'danger-zone';
const navigationSettingsSectionValidator = t.enums.of([
'account',
'friend-list',
'block-list',
'keyservers',
'danger-zone',
]);
export type NavigationChatMode = 'view' | 'create';
const navigationChatModeValidator = t.enums.of(['view', 'create']);
export type NavInfo = {
...$Exact,
- +tab: NavigationTab,
+ +tab: WebNavigationTab,
+activeChatThreadID: ?string,
+pendingThread?: ThreadInfo,
+settingsSection?: NavigationSettingsSection,
+selectedUserList?: $ReadOnlyArray,
+chatMode?: NavigationChatMode,
+inviteSecret?: ?string,
+loginMethod?: LoginMethod,
};
export const navInfoValidator: TInterface = tShape<$Exact>({
startDate: t.String,
endDate: t.String,
- tab: navigationTabValidator,
+ tab: webNavigationTabValidator,
activeChatThreadID: t.maybe(tID),
pendingThread: t.maybe(threadInfoValidator),
settingsSection: t.maybe(navigationSettingsSectionValidator),
selectedUserList: t.maybe(t.list(accountUserInfoValidator)),
chatMode: t.maybe(navigationChatModeValidator),
inviteSecret: t.maybe(t.String),
loginMethod: t.maybe(loginMethodValidator),
});