diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js
index 7eeeda098..0bbe08130 100644
--- a/native/chat/chat-thread-list-item.react.js
+++ b/native/chat/chat-thread-list-item.react.js
@@ -1,297 +1,297 @@
// @flow
import * as React from 'react';
import { Text, View } from 'react-native';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
-import type { LegacyThreadInfo } from 'lib/types/thread-types.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { shortAbsoluteDate } from 'lib/utils/date-utils.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js';
import MessagePreview from './message-preview.react.js';
import SwipeableThread from './swipeable-thread.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import Button from '../components/button.react.js';
import SingleLine from '../components/single-line.react.js';
import ThreadAncestorsLabel from '../components/thread-ancestors-label.react.js';
import UnreadDot from '../components/unread-dot.react.js';
import { useColors, useStyles } from '../themes/colors.js';
type Props = {
+data: ChatThreadItem,
+onPressItem: (
- threadInfo: LegacyThreadInfo,
+ threadInfo: ThreadInfo,
pendingPersonalThreadUserInfo?: UserInfo,
) => void,
- +onPressSeeMoreSidebars: (threadInfo: LegacyThreadInfo) => void,
- +onSwipeableWillOpen: (threadInfo: LegacyThreadInfo) => void,
+ +onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void,
+ +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
+currentlyOpenedSwipeableId: string,
};
function ChatThreadListItem({
data,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
currentlyOpenedSwipeableId,
}: Props): React.Node {
const styles = useStyles(unboundStyles);
const colors = useColors();
const lastMessage = React.useMemo(() => {
const mostRecentMessageInfo = data.mostRecentMessageInfo;
if (!mostRecentMessageInfo) {
return (
No messages
);
}
return (
);
}, [data.mostRecentMessageInfo, data.threadInfo, styles]);
const numOfSidebarsWithExtendedArrow =
data.sidebars.filter(sidebarItem => sidebarItem.type === 'sidebar').length -
1;
const sidebars = React.useMemo(
() =>
data.sidebars.map((sidebarItem, index) => {
if (sidebarItem.type === 'sidebar') {
const { type, ...sidebarInfo } = sidebarItem;
return (
);
} else if (sidebarItem.type === 'seeMore') {
return (
);
} else {
return ;
}
}),
[
currentlyOpenedSwipeableId,
data.sidebars,
data.threadInfo,
numOfSidebarsWithExtendedArrow,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
styles.spacer,
],
);
const onPress = React.useCallback(() => {
onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo);
}, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]);
const threadNameStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.threadName;
}
return [styles.threadName, styles.unreadThreadName];
}, [
data.threadInfo.currentUser.unread,
styles.threadName,
styles.unreadThreadName,
]);
const lastActivity = shortAbsoluteDate(data.lastUpdatedTime);
const lastActivityStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.lastActivity;
}
return [styles.lastActivity, styles.unreadLastActivity];
}, [
data.threadInfo.currentUser.unread,
styles.lastActivity,
styles.unreadLastActivity,
]);
const resolvedThreadInfo = useResolvedThreadInfo(data.threadInfo);
const unreadDot = React.useMemo(
() => (
),
[data.threadInfo.currentUser.unread, styles.avatarContainer],
);
const threadAvatar = React.useMemo(
() => (
),
[data.threadInfo, styles.avatarContainer],
);
const threadDetails = React.useMemo(
() => (
{resolvedThreadInfo.uiName}
{lastMessage}
{lastActivity}
),
[
data.threadInfo,
lastActivity,
lastActivityStyle,
lastMessage,
resolvedThreadInfo.uiName,
styles.row,
styles.threadDetails,
threadNameStyle,
],
);
const swipeableThreadContent = React.useMemo(
() => (
),
[
colors.listIosHighlightUnderlay,
onPress,
styles.container,
styles.content,
threadAvatar,
threadDetails,
unreadDot,
],
);
const swipeableThread = React.useMemo(
() => (
{swipeableThreadContent}
),
[
currentlyOpenedSwipeableId,
data.mostRecentNonLocalMessage,
data.threadInfo,
onSwipeableWillOpen,
swipeableThreadContent,
],
);
const chatThreadListItem = React.useMemo(
() => (
<>
{swipeableThread}
{sidebars}
>
),
[sidebars, swipeableThread],
);
return chatThreadListItem;
}
const chatThreadListItemHeight = 70;
const spacerHeight = 6;
const unboundStyles = {
container: {
height: chatThreadListItemHeight,
justifyContent: 'center',
backgroundColor: 'listBackground',
},
content: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
avatarContainer: {
marginLeft: 6,
marginBottom: 12,
},
threadDetails: {
paddingLeft: 12,
paddingRight: 18,
justifyContent: 'center',
flex: 1,
marginTop: 5,
},
lastActivity: {
color: 'listForegroundTertiaryLabel',
fontSize: 14,
marginLeft: 10,
},
unreadLastActivity: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
noMessages: {
color: 'listForegroundTertiaryLabel',
flex: 1,
fontSize: 14,
fontStyle: 'italic',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
threadName: {
color: 'listForegroundSecondaryLabel',
flex: 1,
fontSize: 21,
},
unreadThreadName: {
color: 'listForegroundLabel',
fontWeight: '500',
},
spacer: {
height: spacerHeight,
},
};
export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight };
diff --git a/native/chat/chat-thread-list-see-more-sidebars.react.js b/native/chat/chat-thread-list-see-more-sidebars.react.js
index 4b6d45e29..111a3df31 100644
--- a/native/chat/chat-thread-list-see-more-sidebars.react.js
+++ b/native/chat/chat-thread-list-see-more-sidebars.react.js
@@ -1,70 +1,70 @@
// @flow
import Icon from '@expo/vector-icons/Ionicons.js';
import * as React from 'react';
import { Text } from 'react-native';
-import type { LegacyThreadInfo } from 'lib/types/thread-types.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import { sidebarHeight } from './sidebar-item.react.js';
import Button from '../components/button.react.js';
import { useColors, useStyles } from '../themes/colors.js';
type Props = {
- +threadInfo: LegacyThreadInfo,
+ +threadInfo: ThreadInfo,
+unread: boolean,
- +onPress: (threadInfo: LegacyThreadInfo) => void,
+ +onPress: (threadInfo: ThreadInfo) => void,
};
function ChatThreadListSeeMoreSidebars(props: Props): React.Node {
const { onPress, threadInfo, unread } = props;
const onPressButton = React.useCallback(
() => onPress(threadInfo),
[onPress, threadInfo],
);
const colors = useColors();
const styles = useStyles(unboundStyles);
const unreadStyle = unread ? styles.unread : null;
return (
);
}
const unboundStyles = {
unread: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
button: {
height: sidebarHeight,
flexDirection: 'row',
display: 'flex',
paddingLeft: 28,
paddingRight: 18,
alignItems: 'center',
backgroundColor: 'listBackground',
},
icon: {
paddingLeft: 5,
color: 'listForegroundSecondaryLabel',
width: 35,
},
text: {
color: 'listForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
paddingLeft: 3,
paddingBottom: 2,
},
};
export default ChatThreadListSeeMoreSidebars;
diff --git a/native/chat/chat-thread-list-sidebar.react.js b/native/chat/chat-thread-list-sidebar.react.js
index 6e22a8542..aae0fc121 100644
--- a/native/chat/chat-thread-list-sidebar.react.js
+++ b/native/chat/chat-thread-list-sidebar.react.js
@@ -1,158 +1,158 @@
// @flow
import * as React from 'react';
import { View } from 'react-native';
-import type { LegacyThreadInfo, SidebarInfo } from 'lib/types/thread-types.js';
+import type { SidebarInfo, ThreadInfo } from 'lib/types/thread-types.js';
import { SidebarItem, sidebarHeight } from './sidebar-item.react.js';
import SwipeableThread from './swipeable-thread.react.js';
import Button from '../components/button.react.js';
import UnreadDot from '../components/unread-dot.react.js';
import { useColors, useStyles } from '../themes/colors.js';
import ExtendedArrow from '../vectors/arrow-extended.react.js';
import Arrow from '../vectors/arrow.react.js';
type Props = {
+sidebarInfo: SidebarInfo,
- +onPressItem: (threadInfo: LegacyThreadInfo) => void,
- +onSwipeableWillOpen: (threadInfo: LegacyThreadInfo) => void,
+ +onPressItem: (threadInfo: ThreadInfo) => void,
+ +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
+currentlyOpenedSwipeableId: string,
+extendArrow: boolean,
};
function ChatThreadListSidebar(props: Props): React.Node {
const colors = useColors();
const styles = useStyles(unboundStyles);
const {
sidebarInfo,
onSwipeableWillOpen,
currentlyOpenedSwipeableId,
onPressItem,
extendArrow = false,
} = props;
const { threadInfo } = sidebarInfo;
const onPress = React.useCallback(
() => onPressItem(threadInfo),
[threadInfo, onPressItem],
);
const arrow = React.useMemo(() => {
if (extendArrow) {
return (
);
}
return (
);
}, [extendArrow, styles.arrow, styles.extendedArrow]);
const unreadIndicator = React.useMemo(
() => (
),
[
sidebarInfo.threadInfo.currentUser.unread,
styles.unreadIndicatorContainer,
],
);
const sidebarItem = React.useMemo(
() => ,
[sidebarInfo],
);
const swipeableThread = React.useMemo(
() => (
{sidebarItem}
),
[
currentlyOpenedSwipeableId,
onSwipeableWillOpen,
sidebarInfo.mostRecentNonLocalMessage,
sidebarInfo.threadInfo,
sidebarItem,
styles.swipeableThreadContainer,
],
);
const chatThreadListSidebar = React.useMemo(
() => (
),
[
arrow,
colors.listIosHighlightUnderlay,
onPress,
styles.sidebar,
swipeableThread,
unreadIndicator,
],
);
return chatThreadListSidebar;
}
const unboundStyles = {
arrow: {
left: 28,
position: 'absolute',
top: -12,
},
extendedArrow: {
left: 28,
position: 'absolute',
top: -6,
},
sidebar: {
alignItems: 'center',
flexDirection: 'row',
width: '100%',
height: sidebarHeight,
paddingLeft: 6,
paddingRight: 18,
backgroundColor: 'listBackground',
},
swipeableThreadContainer: {
flex: 1,
height: '100%',
},
unreadIndicatorContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-start',
paddingLeft: 6,
width: 56,
},
};
export default ChatThreadListSidebar;
diff --git a/native/chat/chat-thread-list.react.js b/native/chat/chat-thread-list.react.js
index ee9001ca6..9ea756624 100644
--- a/native/chat/chat-thread-list.react.js
+++ b/native/chat/chat-thread-list.react.js
@@ -1,494 +1,494 @@
// @flow
import IonIcon from '@expo/vector-icons/Ionicons.js';
import type {
TabNavigationState,
BottomTabOptions,
BottomTabNavigationEventMap,
StackNavigationState,
StackOptions,
StackNavigationEventMap,
} from '@react-navigation/core';
import invariant from 'invariant';
import * as React from 'react';
import {
View,
FlatList,
Platform,
TouchableWithoutFeedback,
BackHandler,
TextInput,
} from 'react-native';
import { FloatingAction } from 'react-native-floating-action';
import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js';
import {
type ChatThreadItem,
useFlattenedChatListData,
} from 'lib/selectors/chat-selectors.js';
import {
createPendingThread,
getThreadListSearchResults,
useThreadListSearch,
} from 'lib/shared/thread-utils.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
-import type { LegacyThreadInfo, ThreadInfo } from 'lib/types/thread-types.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { ChatThreadListItem } from './chat-thread-list-item.react.js';
import ChatThreadListSearch from './chat-thread-list-search.react.js';
import { getItemLayout, keyExtractor } from './chat-thread-list-utils.js';
import type {
ChatTopTabsNavigationProp,
ChatNavigationProp,
} from './chat.react.js';
import { useNavigateToThread } from './message-list-types.js';
import {
SidebarListModalRouteName,
HomeChatThreadListRouteName,
BackgroundChatThreadListRouteName,
type NavigationRoute,
type ScreenParamList,
} from '../navigation/route-names.js';
import type { TabNavigationProp } from '../navigation/tab-navigator.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { indicatorStyleSelector, useStyles } from '../themes/colors.js';
import type { ScrollEvent } from '../types/react-native.js';
const floatingActions = [
{
text: 'Compose',
icon: ,
name: 'compose',
position: 1,
},
];
export type Item =
| ChatThreadItem
| { +type: 'search', +searchText: string }
| { +type: 'empty', +emptyItem: React.ComponentType<{}> };
type BaseProps = {
+navigation:
| ChatTopTabsNavigationProp<'HomeChatThreadList'>
| ChatTopTabsNavigationProp<'BackgroundChatThreadList'>,
+route:
| NavigationRoute<'HomeChatThreadList'>
| NavigationRoute<'BackgroundChatThreadList'>,
+filterThreads: (threadItem: ThreadInfo) => boolean,
+emptyItem?: React.ComponentType<{}>,
};
export type SearchStatus = 'inactive' | 'activating' | 'active';
function ChatThreadList(props: BaseProps): React.Node {
const boundChatListData = useFlattenedChatListData();
const loggedInUserInfo = useLoggedInUserInfo();
const styles = useStyles(unboundStyles);
const indicatorStyle = useSelector(indicatorStyleSelector);
const navigateToThread = useNavigateToThread();
const { navigation, route, filterThreads, emptyItem } = props;
const [searchText, setSearchText] = React.useState('');
const [searchStatus, setSearchStatus] =
React.useState('inactive');
const { threadSearchResults, usersSearchResults } = useThreadListSearch(
searchText,
loggedInUserInfo?.id,
);
const [openedSwipeableID, setOpenedSwipeableID] = React.useState('');
const [numItemsToDisplay, setNumItemsToDisplay] = React.useState(25);
const onChangeSearchText = React.useCallback((updatedSearchText: string) => {
setSearchText(updatedSearchText);
setNumItemsToDisplay(25);
}, []);
const scrollPos = React.useRef(0);
const flatListRef = React.useRef>();
const onScroll = React.useCallback(
(event: ScrollEvent) => {
const oldScrollPos = scrollPos.current;
scrollPos.current = event.nativeEvent.contentOffset.y;
if (scrollPos.current !== 0 || oldScrollPos === 0) {
return;
}
if (searchStatus === 'activating') {
setSearchStatus('active');
}
},
[searchStatus],
);
const onSwipeableWillOpen = React.useCallback(
(threadInfo: ThreadInfo) => setOpenedSwipeableID(threadInfo.id),
[],
);
const composeThread = React.useCallback(() => {
if (!loggedInUserInfo) {
return;
}
const threadInfo = createPendingThread({
viewerID: loggedInUserInfo.id,
threadType: threadTypes.PRIVATE,
members: [loggedInUserInfo],
});
navigateToThread({ threadInfo, searching: true });
}, [loggedInUserInfo, navigateToThread]);
const onSearchFocus = React.useCallback(() => {
if (searchStatus !== 'inactive') {
return;
}
if (scrollPos.current === 0) {
setSearchStatus('active');
} else {
setSearchStatus('activating');
}
}, [searchStatus]);
const clearSearch = React.useCallback(() => {
if (scrollPos.current > 0 && flatListRef.current) {
flatListRef.current.scrollToOffset({ offset: 0, animated: false });
}
setSearchStatus('inactive');
}, []);
const onSearchBlur = React.useCallback(() => {
if (searchStatus !== 'active') {
return;
}
clearSearch();
}, [clearSearch, searchStatus]);
const onSearchCancel = React.useCallback(() => {
onChangeSearchText('');
clearSearch();
}, [clearSearch, onChangeSearchText]);
const searchInputRef = React.useRef>();
const onPressItem = React.useCallback(
(threadInfo: ThreadInfo, pendingPersonalThreadUserInfo?: UserInfo) => {
onChangeSearchText('');
if (searchInputRef.current) {
searchInputRef.current.blur();
}
navigateToThread({ threadInfo, pendingPersonalThreadUserInfo });
},
[navigateToThread, onChangeSearchText],
);
const onPressSeeMoreSidebars = React.useCallback(
- (threadInfo: LegacyThreadInfo) => {
+ (threadInfo: ThreadInfo) => {
onChangeSearchText('');
if (searchInputRef.current) {
searchInputRef.current.blur();
}
navigation.navigate<'SidebarListModal'>({
name: SidebarListModalRouteName,
params: { threadInfo },
});
},
[navigation, onChangeSearchText],
);
const hardwareBack = React.useCallback(() => {
if (!navigation.isFocused()) {
return false;
}
const isActiveOrActivating =
searchStatus === 'active' || searchStatus === 'activating';
if (!isActiveOrActivating) {
return false;
}
onSearchCancel();
return true;
}, [navigation, onSearchCancel, searchStatus]);
const searchItem = React.useMemo(
() => (
),
[
onChangeSearchText,
onSearchBlur,
onSearchCancel,
onSearchFocus,
searchStatus,
searchText,
styles.searchContainer,
],
);
const renderItem = React.useCallback(
(row: { item: Item, ... }) => {
const item = row.item;
if (item.type === 'search') {
return searchItem;
}
if (item.type === 'empty') {
const EmptyItem = item.emptyItem;
return ;
}
return (
);
},
[
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
openedSwipeableID,
searchItem,
],
);
const listData: $ReadOnlyArray- = React.useMemo(() => {
const chatThreadItems = getThreadListSearchResults(
boundChatListData,
searchText,
filterThreads,
threadSearchResults,
usersSearchResults,
loggedInUserInfo,
);
const chatItems: Item[] = [...chatThreadItems];
if (emptyItem && chatItems.length === 0) {
chatItems.push({ type: 'empty', emptyItem });
}
if (searchStatus === 'inactive' || searchStatus === 'activating') {
chatItems.unshift({ type: 'search', searchText });
}
return chatItems;
}, [
boundChatListData,
emptyItem,
filterThreads,
loggedInUserInfo,
searchStatus,
searchText,
threadSearchResults,
usersSearchResults,
]);
const partialListData: $ReadOnlyArray
- = React.useMemo(
() => listData.slice(0, numItemsToDisplay),
[listData, numItemsToDisplay],
);
const onEndReached = React.useCallback(() => {
if (partialListData.length === listData.length) {
return;
}
setNumItemsToDisplay(prevNumItems => prevNumItems + 25);
}, [listData.length, partialListData.length]);
const floatingAction = React.useMemo(() => {
if (Platform.OS !== 'android') {
return null;
}
return (
);
}, [composeThread]);
const fixedSearch = React.useMemo(() => {
if (searchStatus !== 'active') {
return null;
}
return (
);
}, [
onChangeSearchText,
onSearchBlur,
onSearchCancel,
searchStatus,
searchText,
styles.searchContainer,
]);
const scrollEnabled =
searchStatus === 'inactive' || searchStatus === 'active';
// viewerID is in extraData since it's used by MessagePreview
// within ChatThreadListItem
const viewerID = loggedInUserInfo?.id;
const extraData = `${viewerID || ''} ${openedSwipeableID}`;
const chatThreadList = React.useMemo(
() => (
{fixedSearch}
{floatingAction}
),
[
extraData,
fixedSearch,
floatingAction,
indicatorStyle,
onEndReached,
onScroll,
partialListData,
renderItem,
scrollEnabled,
styles.container,
styles.flatList,
],
);
const onTabPress = React.useCallback(() => {
if (!navigation.isFocused()) {
return;
}
if (scrollPos.current > 0 && flatListRef.current) {
flatListRef.current.scrollToOffset({ offset: 0, animated: true });
} else if (route.name === BackgroundChatThreadListRouteName) {
navigation.navigate({ name: HomeChatThreadListRouteName });
}
}, [navigation, route.name]);
React.useEffect(() => {
const clearNavigationBlurListener = navigation.addListener('blur', () => {
setNumItemsToDisplay(25);
});
return () => {
// `.addListener` returns function that can be called to unsubscribe.
// https://reactnavigation.org/docs/navigation-events/#navigationaddlistener
clearNavigationBlurListener();
};
}, [navigation]);
React.useEffect(() => {
const chatNavigation = navigation.getParent<
ScreenParamList,
'ChatThreadList',
StackNavigationState,
StackOptions,
StackNavigationEventMap,
ChatNavigationProp<'ChatThreadList'>,
>();
invariant(chatNavigation, 'ChatNavigator should be within TabNavigator');
const tabNavigation = chatNavigation.getParent<
ScreenParamList,
'Chat',
TabNavigationState,
BottomTabOptions,
BottomTabNavigationEventMap,
TabNavigationProp<'Chat'>,
>();
invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
tabNavigation.addListener('tabPress', onTabPress);
return () => {
tabNavigation.removeListener('tabPress', onTabPress);
};
}, [navigation, onTabPress]);
React.useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', hardwareBack);
return () => {
BackHandler.removeEventListener('hardwareBackPress', hardwareBack);
};
}, [hardwareBack]);
React.useEffect(() => {
if (scrollPos.current > 0 && flatListRef.current) {
flatListRef.current.scrollToOffset({ offset: 0, animated: false });
}
}, [searchText]);
const isSearchActivating = searchStatus === 'activating';
React.useEffect(() => {
if (isSearchActivating && scrollPos.current > 0 && flatListRef.current) {
flatListRef.current.scrollToOffset({ offset: 0, animated: true });
}
}, [isSearchActivating]);
return chatThreadList;
}
const unboundStyles = {
icon: {
fontSize: 28,
},
container: {
flex: 1,
},
searchContainer: {
backgroundColor: 'listBackground',
display: 'flex',
justifyContent: 'center',
flexDirection: 'row',
},
flatList: {
flex: 1,
backgroundColor: 'listBackground',
},
};
export default ChatThreadList;
diff --git a/native/chat/compose-subchannel.react.js b/native/chat/compose-subchannel.react.js
index 1ed512ab7..be49ed120 100644
--- a/native/chat/compose-subchannel.react.js
+++ b/native/chat/compose-subchannel.react.js
@@ -1,387 +1,386 @@
// @flow
import invariant from 'invariant';
import _filter from 'lodash/fp/filter.js';
import _flow from 'lodash/fp/flow.js';
import _sortBy from 'lodash/fp/sortBy.js';
import * as React from 'react';
import { View, Text } from 'react-native';
import {
newThreadActionTypes,
useNewThread,
} from 'lib/actions/thread-actions.js';
import { useENSNames } from 'lib/hooks/ens-cache.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import {
userInfoSelectorForPotentialMembers,
userSearchIndexForPotentialMembers,
} from 'lib/selectors/user-selectors.js';
import { getPotentialMemberItems } from 'lib/shared/search-utils.js';
import { threadInFilterList, userIsMember } from 'lib/shared/thread-utils.js';
import { type ThreadType, threadTypes } from 'lib/types/thread-types-enum.js';
-import type { ThreadInfo, LegacyThreadInfo } from 'lib/types/thread-types.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import { type AccountUserInfo } from 'lib/types/user-types.js';
import { useDispatchActionPromise } from 'lib/utils/action-utils.js';
import type { ChatNavigationProp } from './chat.react.js';
import { useNavigateToThread } from './message-list-types.js';
import ParentThreadHeader from './parent-thread-header.react.js';
import LinkButton from '../components/link-button.react.js';
import {
createTagInput,
type BaseTagInput,
} from '../components/tag-input.react.js';
import ThreadList from '../components/thread-list.react.js';
import UserList from '../components/user-list.react.js';
import { useCalendarQuery } from '../navigation/nav-selectors.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
import Alert from '../utils/alert.js';
const TagInput = createTagInput();
const tagInputProps = {
placeholder: 'username',
autoFocus: true,
returnKeyType: 'go',
};
const tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username;
export type ComposeSubchannelParams = {
+threadType: ThreadType,
+parentThreadInfo: ThreadInfo,
};
type Props = {
+navigation: ChatNavigationProp<'ComposeSubchannel'>,
+route: NavigationRoute<'ComposeSubchannel'>,
};
function ComposeSubchannel(props: Props): React.Node {
const [usernameInputText, setUsernameInputText] = React.useState('');
const [userInfoInputArray, setUserInfoInputArray] = React.useState<
$ReadOnlyArray,
>([]);
const [createButtonEnabled, setCreateButtonEnabled] =
React.useState(true);
const tagInputRef = React.useRef>();
const onUnknownErrorAlertAcknowledged = React.useCallback(() => {
setUsernameInputText('');
tagInputRef.current?.focus();
}, []);
const waitingOnThreadIDRef = React.useRef();
const { threadType, parentThreadInfo } = props.route.params;
const userInfoInputIDs = userInfoInputArray.map(userInfo => userInfo.id);
const callNewThread = useNewThread();
const calendarQuery = useCalendarQuery();
const newChatThreadAction = React.useCallback(async () => {
try {
const assumedThreadType =
threadType ?? threadTypes.COMMUNITY_SECRET_SUBTHREAD;
const query = calendarQuery();
invariant(
assumedThreadType === 3 ||
assumedThreadType === 4 ||
assumedThreadType === 6 ||
assumedThreadType === 7,
"Sidebars and communities can't be created from the thread composer",
);
const result = await callNewThread({
type: assumedThreadType,
parentThreadID: parentThreadInfo.id,
initialMemberIDs: userInfoInputIDs,
color: parentThreadInfo.color,
calendarQuery: query,
});
waitingOnThreadIDRef.current = result.newThreadID;
return result;
} catch (e) {
setCreateButtonEnabled(true);
Alert.alert(
'Unknown error',
'Uhh... try again?',
[{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }],
{ cancelable: false },
);
throw e;
}
}, [
threadType,
userInfoInputIDs,
calendarQuery,
parentThreadInfo,
callNewThread,
onUnknownErrorAlertAcknowledged,
]);
const dispatchActionPromise = useDispatchActionPromise();
const dispatchNewChatThreadAction = React.useCallback(() => {
setCreateButtonEnabled(false);
dispatchActionPromise(newThreadActionTypes, newChatThreadAction());
}, [dispatchActionPromise, newChatThreadAction]);
const userInfoInputArrayEmpty = userInfoInputArray.length === 0;
const onPressCreateThread = React.useCallback(() => {
if (!createButtonEnabled) {
return;
}
if (userInfoInputArrayEmpty) {
Alert.alert(
'Chatting to yourself?',
'Are you sure you want to create a channel containing only yourself?',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Confirm', onPress: dispatchNewChatThreadAction },
],
{ cancelable: true },
);
} else {
dispatchNewChatThreadAction();
}
}, [
createButtonEnabled,
userInfoInputArrayEmpty,
dispatchNewChatThreadAction,
]);
const { navigation } = props;
const { setOptions } = navigation;
React.useEffect(() => {
setOptions({
// eslint-disable-next-line react/display-name
headerRight: () => (
),
});
}, [setOptions, onPressCreateThread, createButtonEnabled]);
const { setParams } = navigation;
const parentThreadInfoID = parentThreadInfo.id;
const reduxParentThreadInfo = useSelector(
state => threadInfoSelector(state)[parentThreadInfoID],
);
React.useEffect(() => {
if (reduxParentThreadInfo) {
setParams({ parentThreadInfo: reduxParentThreadInfo });
}
}, [reduxParentThreadInfo, setParams]);
const threadInfos = useSelector(threadInfoSelector);
const newlyCreatedThreadInfo = waitingOnThreadIDRef.current
? threadInfos[waitingOnThreadIDRef.current]
: null;
const { pushNewThread } = navigation;
React.useEffect(() => {
if (!newlyCreatedThreadInfo) {
return;
}
const waitingOnThreadID = waitingOnThreadIDRef.current;
if (waitingOnThreadID === null || waitingOnThreadID === undefined) {
return;
}
waitingOnThreadIDRef.current = undefined;
pushNewThread(newlyCreatedThreadInfo);
}, [newlyCreatedThreadInfo, pushNewThread]);
const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers);
const userSearchIndex = useSelector(userSearchIndexForPotentialMembers);
const { community } = parentThreadInfo;
const communityThreadInfo = useSelector(state =>
community ? threadInfoSelector(state)[community] : null,
);
const userSearchResults = React.useMemo(
() =>
getPotentialMemberItems({
text: usernameInputText,
userInfos: otherUserInfos,
searchIndex: userSearchIndex,
excludeUserIDs: userInfoInputIDs,
inputParentThreadInfo: parentThreadInfo,
inputCommunityThreadInfo: communityThreadInfo,
threadType,
}),
[
usernameInputText,
otherUserInfos,
userSearchIndex,
userInfoInputIDs,
parentThreadInfo,
communityThreadInfo,
threadType,
],
);
- const existingThreads: $ReadOnlyArray =
- React.useMemo(() => {
- if (userInfoInputIDs.length === 0) {
- return [];
- }
- return _flow(
- _filter(
- (threadInfo: ThreadInfo) =>
- threadInFilterList(threadInfo) &&
- threadInfo.parentThreadID === parentThreadInfo.id &&
- userInfoInputIDs.every(userID => userIsMember(threadInfo, userID)),
- ),
- _sortBy(
- ([
- 'members.length',
- (threadInfo: ThreadInfo) => (threadInfo.name ? 1 : 0),
- ]: $ReadOnlyArray mixed)>),
- ),
- )(threadInfos);
- }, [userInfoInputIDs, threadInfos, parentThreadInfo]);
+ const existingThreads: $ReadOnlyArray = React.useMemo(() => {
+ if (userInfoInputIDs.length === 0) {
+ return [];
+ }
+ return _flow(
+ _filter(
+ (threadInfo: ThreadInfo) =>
+ threadInFilterList(threadInfo) &&
+ threadInfo.parentThreadID === parentThreadInfo.id &&
+ userInfoInputIDs.every(userID => userIsMember(threadInfo, userID)),
+ ),
+ _sortBy(
+ ([
+ 'members.length',
+ (threadInfo: ThreadInfo) => (threadInfo.name ? 1 : 0),
+ ]: $ReadOnlyArray mixed)>),
+ ),
+ )(threadInfos);
+ }, [userInfoInputIDs, threadInfos, parentThreadInfo]);
const navigateToThread = useNavigateToThread();
const onSelectExistingThread = React.useCallback(
(threadID: string) => {
const threadInfo = threadInfos[threadID];
navigateToThread({ threadInfo });
},
[threadInfos, navigateToThread],
);
const onUserSelect = React.useCallback(
({ id }: AccountUserInfo) => {
if (userInfoInputIDs.some(existingUserID => id === existingUserID)) {
return;
}
setUserInfoInputArray(oldUserInfoInputArray => [
...oldUserInfoInputArray,
otherUserInfos[id],
]);
setUsernameInputText('');
},
[userInfoInputIDs, otherUserInfos],
);
const styles = useStyles(unboundStyles);
let existingThreadsSection = null;
if (existingThreads.length > 0) {
existingThreadsSection = (
Existing channels
);
}
const inputProps = React.useMemo(
() => ({
...tagInputProps,
onSubmitEditing: onPressCreateThread,
}),
[onPressCreateThread],
);
const userSearchResultWithENSNames = useENSNames(userSearchResults);
const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray);
return (
To:
{existingThreadsSection}
);
}
const unboundStyles = {
container: {
flex: 1,
},
existingThreadList: {
backgroundColor: 'modalBackground',
flex: 1,
paddingRight: 12,
},
existingThreads: {
flex: 1,
},
existingThreadsLabel: {
color: 'modalForegroundSecondaryLabel',
fontSize: 16,
paddingLeft: 12,
textAlign: 'center',
},
existingThreadsRow: {
backgroundColor: 'modalForeground',
borderBottomWidth: 1,
borderColor: 'modalForegroundBorder',
borderTopWidth: 1,
paddingVertical: 6,
},
listItem: {
color: 'modalForegroundLabel',
},
tagInputContainer: {
flex: 1,
marginLeft: 8,
paddingRight: 12,
},
tagInputLabel: {
color: 'modalForegroundSecondaryLabel',
fontSize: 16,
paddingLeft: 12,
},
userList: {
backgroundColor: 'modalBackground',
flex: 1,
paddingLeft: 35,
paddingRight: 12,
},
userSelectionRow: {
alignItems: 'center',
backgroundColor: 'modalForeground',
borderBottomWidth: 1,
borderColor: 'modalForegroundBorder',
flexDirection: 'row',
paddingVertical: 6,
},
};
const MemoizedComposeSubchannel: React.ComponentType =
React.memo(ComposeSubchannel);
export default MemoizedComposeSubchannel;
diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js
index 4b8c3bb6b..9972e0d53 100644
--- a/native/chat/sidebar-list-modal.react.js
+++ b/native/chat/sidebar-list-modal.react.js
@@ -1,141 +1,141 @@
// @flow
import * as React from 'react';
import { View } from 'react-native';
import { useSearchSidebars } from 'lib/hooks/search-threads.js';
-import type { LegacyThreadInfo, SidebarInfo } from 'lib/types/thread-types.js';
+import type { SidebarInfo, ThreadInfo } from 'lib/types/thread-types.js';
import { SidebarItem } from './sidebar-item.react.js';
import ThreadListModal from './thread-list-modal.react.js';
import Button from '../components/button.react.js';
import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useColors, useStyles } from '../themes/colors.js';
import ExtendedArrow from '../vectors/arrow-extended.react.js';
import Arrow from '../vectors/arrow.react.js';
export type SidebarListModalParams = {
- +threadInfo: LegacyThreadInfo,
+ +threadInfo: ThreadInfo,
};
type Props = {
+navigation: RootNavigationProp<'SidebarListModal'>,
+route: NavigationRoute<'SidebarListModal'>,
};
function SidebarListModal(props: Props): React.Node {
const { listData, searchState, setSearchState, onChangeSearchInputText } =
useSearchSidebars(props.route.params.threadInfo);
const numOfSidebarsWithExtendedArrow = listData.length - 1;
const createRenderItem = React.useCallback(
- (onPressItem: (threadInfo: LegacyThreadInfo) => void) =>
+ (onPressItem: (threadInfo: ThreadInfo) => void) =>
// eslint-disable-next-line react/display-name
(row: { +item: SidebarInfo, +index: number, ... }) => {
let extendArrow: boolean = false;
if (row.index < numOfSidebarsWithExtendedArrow) {
extendArrow = true;
}
return (
);
},
[numOfSidebarsWithExtendedArrow],
);
return (
);
}
function Item(props: {
item: SidebarInfo,
- onPressItem: (threadInfo: LegacyThreadInfo) => void,
+ onPressItem: (threadInfo: ThreadInfo) => void,
extendArrow: boolean,
}): React.Node {
const { item, onPressItem, extendArrow } = props;
const { threadInfo } = item;
const onPressButton = React.useCallback(
() => onPressItem(threadInfo),
[onPressItem, threadInfo],
);
const colors = useColors();
const styles = useStyles(unboundStyles);
let arrow;
if (extendArrow) {
arrow = (
);
} else {
arrow = (
);
}
return (
);
}
const unboundStyles = {
arrow: {
position: 'absolute',
top: -12,
},
extendedArrow: {
position: 'absolute',
top: -6,
},
sidebar: {
paddingLeft: 0,
paddingRight: 5,
height: 38,
},
sidebarItemContainer: {
flex: 1,
},
sidebarRowContainer: {
flex: 1,
flexDirection: 'row',
},
spacer: {
width: 30,
},
};
export default SidebarListModal;
diff --git a/native/chat/subchannels-list-modal.react.js b/native/chat/subchannels-list-modal.react.js
index eeb68bb5f..4626fb8b9 100644
--- a/native/chat/subchannels-list-modal.react.js
+++ b/native/chat/subchannels-list-modal.react.js
@@ -1,96 +1,96 @@
// @flow
import * as React from 'react';
import { View } from 'react-native';
import { useSearchSubchannels } from 'lib/hooks/search-threads.js';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
-import { type LegacyThreadInfo } from 'lib/types/thread-types.js';
+import { type ThreadInfo } from 'lib/types/thread-types.js';
import SubchannelItem from './subchannel-item.react.js';
import ThreadListModal from './thread-list-modal.react.js';
import Button from '../components/button.react.js';
import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useColors, useStyles } from '../themes/colors.js';
export type SubchannelListModalParams = {
- +threadInfo: LegacyThreadInfo,
+ +threadInfo: ThreadInfo,
};
type Props = {
+navigation: RootNavigationProp<'SubchannelsListModal'>,
+route: NavigationRoute<'SubchannelsListModal'>,
};
function SubchannelListModal(props: Props): React.Node {
const { listData, searchState, setSearchState, onChangeSearchInputText } =
useSearchSubchannels(props.route.params.threadInfo);
return (
);
}
const createRenderItem =
- (onPressItem: (threadInfo: LegacyThreadInfo) => void) =>
+ (onPressItem: (threadInfo: ThreadInfo) => void) =>
// eslint-disable-next-line react/display-name
(row: { +item: ChatThreadItem, +index: number, ... }) => {
return ;
};
function Item(props: {
- onPressItem: (threadInfo: LegacyThreadInfo) => void,
+ onPressItem: (threadInfo: ThreadInfo) => void,
subchannelInfo: ChatThreadItem,
}): React.Node {
const { onPressItem, subchannelInfo } = props;
const { threadInfo } = subchannelInfo;
const onPressButton = React.useCallback(
() => onPressItem(threadInfo),
[onPressItem, threadInfo],
);
const colors = useColors();
const styles = useStyles(unboundStyles);
return (
);
}
const unboundStyles = {
subchannel: {
paddingLeft: 0,
paddingRight: 5,
},
subchannelItemContainer: {
flex: 1,
},
subchannelRowContainer: {
flex: 1,
flexDirection: 'row',
},
};
export default SubchannelListModal;
diff --git a/native/chat/swipeable-thread.react.js b/native/chat/swipeable-thread.react.js
index d3d162891..914a29f29 100644
--- a/native/chat/swipeable-thread.react.js
+++ b/native/chat/swipeable-thread.react.js
@@ -1,100 +1,100 @@
// @flow
import MaterialIcon from '@expo/vector-icons/MaterialCommunityIcons.js';
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
// eslint-disable-next-line import/extensions
import SwipeableComponent from 'react-native-gesture-handler/Swipeable';
import useToggleUnreadStatus from 'lib/hooks/toggle-unread-status.js';
-import type { LegacyThreadInfo } from 'lib/types/thread-types.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import Swipeable from '../components/swipeable.js';
import { useColors } from '../themes/colors.js';
type Props = {
- +threadInfo: LegacyThreadInfo,
+ +threadInfo: ThreadInfo,
+mostRecentNonLocalMessage: ?string,
- +onSwipeableWillOpen: (threadInfo: LegacyThreadInfo) => void,
+ +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
+currentlyOpenedSwipeableId?: string,
+iconSize: number,
+children: React.Node,
};
function SwipeableThread(props: Props): React.Node {
const swipeable = React.useRef();
const navigation = useNavigation();
React.useEffect(() => {
return navigation.addListener('blur', () => {
if (swipeable.current) {
swipeable.current.close();
}
});
}, [navigation, swipeable]);
const { threadInfo, currentlyOpenedSwipeableId } = props;
React.useEffect(() => {
if (swipeable.current && threadInfo.id !== currentlyOpenedSwipeableId) {
swipeable.current.close();
}
}, [currentlyOpenedSwipeableId, swipeable, threadInfo.id]);
const { onSwipeableWillOpen } = props;
const onSwipeableRightWillOpen = React.useCallback(() => {
onSwipeableWillOpen(threadInfo);
}, [onSwipeableWillOpen, threadInfo]);
const colors = useColors();
const { mostRecentNonLocalMessage, iconSize } = props;
const swipeableClose = React.useCallback(() => {
if (swipeable.current) {
swipeable.current.close();
}
}, []);
const toggleUnreadStatus = useToggleUnreadStatus(
threadInfo,
mostRecentNonLocalMessage,
swipeableClose,
);
const swipeableActions = React.useMemo(() => {
const isUnread = threadInfo.currentUser.unread;
return [
{
key: 'action1',
onPress: toggleUnreadStatus,
color: isUnread ? colors.vibrantRedButton : colors.vibrantGreenButton,
content: (
),
},
];
}, [
threadInfo.currentUser.unread,
toggleUnreadStatus,
colors.vibrantRedButton,
colors.vibrantGreenButton,
iconSize,
]);
const swipeableThread = React.useMemo(
() => (
{props.children}
),
[onSwipeableRightWillOpen, props.children, swipeableActions],
);
return swipeableThread;
}
export default SwipeableThread;
diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js
index c8c46cac0..6911eac40 100644
--- a/native/chat/thread-list-modal.react.js
+++ b/native/chat/thread-list-modal.react.js
@@ -1,204 +1,198 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
import {
Text,
TextInput,
FlatList,
View,
TouchableOpacity,
} from 'react-native';
import type { ThreadSearchState } from 'lib/hooks/search-threads.js';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
import type { SetState } from 'lib/types/hook-types.js';
-import type {
- LegacyThreadInfo,
- SidebarInfo,
- ThreadInfo,
-} from 'lib/types/thread-types.js';
+import type { SidebarInfo, ThreadInfo } from 'lib/types/thread-types.js';
import { useNavigateToThread } from './message-list-types.js';
import Modal from '../components/modal.react.js';
import Search from '../components/search.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import ThreadPill from '../components/thread-pill.react.js';
import { useIndicatorStyle, useStyles } from '../themes/colors.js';
import { waitForModalInputFocus } from '../utils/timers.js';
function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) {
return sidebarInfo.threadInfo.id;
}
function getItemLayout(
data: ?$ReadOnlyArray,
index: number,
) {
return { length: 24, offset: 24 * index, index };
}
type Props = {
+threadInfo: ThreadInfo,
- +createRenderItem: (
- onPressItem: (threadInfo: LegacyThreadInfo) => void,
- ) => (row: {
+ +createRenderItem: (onPressItem: (threadInfo: ThreadInfo) => void) => (row: {
+item: U,
+index: number,
...
}) => React.Node,
+listData: $ReadOnlyArray,
+searchState: ThreadSearchState,
+setSearchState: SetState,
+onChangeSearchInputText: (text: string) => mixed,
+searchPlaceholder?: string,
+modalTitle: string,
};
function ThreadListModal(
props: Props,
): React.Node {
const {
threadInfo: parentThreadInfo,
searchState,
setSearchState,
onChangeSearchInputText,
listData,
createRenderItem,
searchPlaceholder,
modalTitle,
} = props;
const searchTextInputRef =
React.useRef>();
const setSearchTextInputRef = React.useCallback(
async (textInput: ?React.ElementRef) => {
searchTextInputRef.current = textInput;
if (!textInput) {
return;
}
await waitForModalInputFocus();
if (searchTextInputRef.current) {
searchTextInputRef.current.focus();
}
},
[],
);
const navigateToThread = useNavigateToThread();
const onPressItem = React.useCallback(
(threadInfo: ThreadInfo) => {
setSearchState({
text: '',
results: new Set(),
});
if (searchTextInputRef.current) {
searchTextInputRef.current.blur();
}
navigateToThread({ threadInfo });
},
[navigateToThread, setSearchState],
);
const renderItem = React.useMemo(
() => createRenderItem(onPressItem),
[createRenderItem, onPressItem],
);
const styles = useStyles(unboundStyles);
const indicatorStyle = useIndicatorStyle();
const navigation = useNavigation();
return (
{modalTitle}
);
}
const unboundStyles = {
parentNameWrapper: {
alignItems: 'flex-start',
},
body: {
paddingHorizontal: 16,
flex: 1,
},
headerTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
height: 32,
alignItems: 'center',
},
header: {
borderBottomColor: 'subthreadsModalSearch',
borderBottomWidth: 1,
height: 94,
padding: 16,
justifyContent: 'space-between',
},
modal: {
borderRadius: 8,
paddingHorizontal: 0,
backgroundColor: 'subthreadsModalBackground',
paddingTop: 0,
justifyContent: 'flex-start',
},
search: {
height: 40,
marginVertical: 16,
backgroundColor: 'subthreadsModalSearch',
},
title: {
color: 'listForegroundLabel',
fontSize: 20,
fontWeight: '500',
lineHeight: 26,
alignSelf: 'center',
marginLeft: 2,
},
closeIcon: {
color: 'subthreadsModalClose',
},
closeButton: {
marginRight: 2,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
};
export default ThreadListModal;