diff --git a/packages/common/src/store/pages/chat/selectors.ts b/packages/common/src/store/pages/chat/selectors.ts index be773e66f4..f88986f5be 100644 --- a/packages/common/src/store/pages/chat/selectors.ts +++ b/packages/common/src/store/pages/chat/selectors.ts @@ -7,8 +7,8 @@ import { User } from 'models/User' import { accountSelectors } from 'store/account' import { cacheUsersSelectors } from 'store/cache' import { CommonState } from 'store/reducers' -import { decodeHashId } from 'utils/hashIds' -import { Maybe } from 'utils/typeUtils' +import { decodeHashId, encodeHashId } from 'utils/hashIds' +import { Maybe, removeNullable } from 'utils/typeUtils' import { chatMessagesAdapter, chatsAdapter } from './slice' import { ChatPermissionAction } from './types' @@ -168,6 +168,30 @@ export const getSingleOtherChatUser = ( return getOtherChatUsers(state, chatId)[0] } +/** + * Gets a list of the users the current user has chats with. + * Note that this only takes the first user of each chat that doesn't match the current one, + * so this will need to be adjusted when we do group chats. + */ +export const getUserList = createSelector( + [getUserId, getChats, getHasMoreChats, getChatsStatus], + (currentUserId, chats, hasMore, chatsStatus) => { + const chatUserListIds = chats + .map( + (c) => + c.chat_members + .filter((u) => decodeHashId(u.user_id) !== currentUserId) + .map((u) => decodeHashId(u.user_id))[0] + ) + .filter(removeNullable) + return { + userIds: chatUserListIds, + hasMore, + loading: chatsStatus === Status.LOADING + } + } +) + export const getChatMessageByIndex = ( state: CommonState, chatId: string, @@ -228,6 +252,7 @@ export const getCanCreateChat = createSelector( getBlockees, getBlockers, getChatPermissions, + getChats, (state: CommonState, { userId }: { userId: Maybe }) => { if (!userId) return null const usersMap = getUsers(state, { ids: [userId] }) @@ -239,6 +264,7 @@ export const getCanCreateChat = createSelector( blockees, blockers, chatPermissions, + chats, user ): { canCreateChat: boolean; callToAction: ChatPermissionAction } => { if (!currentUserId) { @@ -254,13 +280,24 @@ export const getCanCreateChat = createSelector( } } + // Check for existing chat, since unblocked users with existing chats + // don't need permission to continue chatting. + // Use a callback fn to prevent iteration until necessary to improve perf + // Note: this only works if the respective chat has been fetched already, like in chatsUserList + const encodedUserId = encodeHashId(user.user_id) + const hasExistingChat = () => + !!chats.find((c) => + c.chat_members.find((u) => u.user_id === encodedUserId) + ) + const userPermissions = chatPermissions[user.user_id] const isBlockee = blockees.includes(user.user_id) const isBlocker = blockers.includes(user.user_id) const canCreateChat = !isBlockee && !isBlocker && - (userPermissions?.current_user_has_permission ?? true) + ((userPermissions?.current_user_has_permission ?? true) || + hasExistingChat()) let action = ChatPermissionAction.NOT_APPLICABLE if (!canCreateChat) { diff --git a/packages/common/src/store/ui/create-chat-modal/index.ts b/packages/common/src/store/ui/create-chat-modal/index.ts index 7393586a4f..1f632f5e34 100644 --- a/packages/common/src/store/ui/create-chat-modal/index.ts +++ b/packages/common/src/store/ui/create-chat-modal/index.ts @@ -3,6 +3,7 @@ import { Action } from '@reduxjs/toolkit' import { createModal } from '../modals/createModal' export type CreateChatModalState = { + defaultUserList?: 'followers' | 'chats' presetMessage?: string onCancelAction?: Action } @@ -10,7 +11,8 @@ export type CreateChatModalState = { const createChatModal = createModal({ reducerPath: 'createChatModal', initialState: { - isOpen: false + isOpen: false, + defaultUserList: 'followers' }, sliceSelector: (state) => state.ui.modalsWithState }) diff --git a/packages/mobile/src/components/share-drawer/ShareDrawer.tsx b/packages/mobile/src/components/share-drawer/ShareDrawer.tsx index 442a17db14..a94f67e3db 100644 --- a/packages/mobile/src/components/share-drawer/ShareDrawer.tsx +++ b/packages/mobile/src/components/share-drawer/ShareDrawer.tsx @@ -109,7 +109,8 @@ export const ShareDrawer = () => { const handleShareToDirectMessage = useCallback(async () => { if (!content) return navigation.navigate('ChatUserList', { - presetMessage: getContentUrl(content) + presetMessage: getContentUrl(content), + defaultUserList: 'chats' }) track(make({ eventName: Name.CHAT_ENTRY_POINT, source: 'share' })) }, [content, navigation]) diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index b3c21e84e4..2c78702ebc 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -7,7 +7,8 @@ import type { NotificationType, RepostType, SearchPlaylist, - SearchTrack + SearchTrack, + CreateChatModalState } from '@audius/common' import { FeatureFlags } from '@audius/common' import type { EventArg, NavigationState } from '@react-navigation/native' @@ -110,6 +111,7 @@ export type AppTabScreenParamList = { ChatUserList: | { presetMessage?: string + defaultUserList?: CreateChatModalState['defaultUserList'] } | undefined Chat: { diff --git a/packages/mobile/src/screens/chat-screen/ChatUserListScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatUserListScreen.tsx index d15e4c8bb2..26fe90fa2a 100644 --- a/packages/mobile/src/screens/chat-screen/ChatUserListScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatUserListScreen.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react' -import type { User } from '@audius/common' +import type { CreateChatModalState, User } from '@audius/common' import { FOLLOWERS_USER_LIST_TAG, Status, @@ -12,6 +12,7 @@ import { searchUsersModalActions, searchUsersModalSelectors, statusIsNotFinalized, + chatSelectors, useProxySelector, userListActions } from '@audius/common' @@ -36,6 +37,8 @@ const { searchUsers } = searchUsersModalActions const { getUserList } = searchUsersModalSelectors const { getUsers } = cacheUsersSelectors const { fetchBlockees, fetchBlockers, fetchPermissions } = chatActions +const { getUserList: getChatsUserList } = chatSelectors +const { getUserList: getFollowersUserList } = followersUserListSelectors const DEBOUNCE_MS = 150 @@ -141,12 +144,17 @@ const ListEmpty = () => { ) } -const useDefaultUserList = () => { +const useDefaultUserList = ( + defaultUserList: CreateChatModalState['defaultUserList'] +) => { const dispatch = useDispatch() const currentUser = useSelector(getAccountUser) - const { hasMore, loading, userIds } = useSelector( - followersUserListSelectors.getUserList - ) + const followersUserList = useSelector(getFollowersUserList) + const chatsUserList = useSelector(getChatsUserList) + + const { hasMore, loading, userIds } = + defaultUserList === 'chats' ? chatsUserList : followersUserList + const loadMore = useCallback(() => { if (currentUser) { dispatch(followersUserListActions.setFollowers(currentUser?.user_id)) @@ -190,7 +198,8 @@ export const ChatUserListScreen = () => { const dispatch = useDispatch() const { params } = useRoute<'ChatUserList'>() const presetMessage = params?.presetMessage - const defaultUserList = useDefaultUserList() + const defaultUserListType = params?.defaultUserList + const defaultUserList = useDefaultUserList(defaultUserListType) const queryUserList = useQueryUserList(query) const hasQuery = query.length > 0 diff --git a/packages/web/src/components/share-modal/ShareModal.tsx b/packages/web/src/components/share-modal/ShareModal.tsx index f09667daa4..2a87e4f4fa 100644 --- a/packages/web/src/components/share-modal/ShareModal.tsx +++ b/packages/web/src/components/share-modal/ShareModal.tsx @@ -60,7 +60,8 @@ export const ShareModal = () => { openCreateChatModal({ // Just care about the link presetMessage: (await getTwitterShareText(content, false)).link, - onCancelAction: setVisibility({ modal: 'Share', visible: true }) + onCancelAction: setVisibility({ modal: 'Share', visible: true }), + defaultUserList: 'chats' }) dispatch(make(Name.CHAT_ENTRY_POINT, { source: 'share' })) }, [openCreateChatModal, dispatch, onClose, content]) diff --git a/packages/web/src/pages/chat-page/components/CreateChatModal.tsx b/packages/web/src/pages/chat-page/components/CreateChatModal.tsx index f0f32ba62b..8024245f9e 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatModal.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatModal.tsx @@ -11,7 +11,8 @@ import { useCreateChatModal, useInboxUnavailableModal, createChatModalActions, - searchUsersModalActions + searchUsersModalActions, + chatSelectors } from '@audius/common' import { IconCompose } from '@audius/stems' import { useDispatch } from 'react-redux' @@ -27,18 +28,21 @@ const messages = { } const { getAccountUser } = accountSelectors -const { fetchBlockers } = chatActions +const { getUserList: getFollowersUserList } = followersUserListSelectors +const { getUserList: getChatsUserList } = chatSelectors +const { fetchBlockers, fetchMoreChats } = chatActions export const CreateChatModal = () => { const dispatch = useDispatch() const currentUser = useSelector(getAccountUser) const { isOpen, onClose, onClosed, data } = useCreateChatModal() const { onOpen: openInboxUnavailableModal } = useInboxUnavailableModal() - const { onCancelAction, presetMessage } = data + const { onCancelAction, presetMessage, defaultUserList } = data - const { userIds, loading, hasMore } = useSelector( - followersUserListSelectors.getUserList - ) + const followersUserList = useSelector(getFollowersUserList) + const chatsUserList = useSelector(getChatsUserList) + const { userIds, hasMore, loading } = + defaultUserList === 'chats' ? chatsUserList : followersUserList const handleCancel = useCallback(() => { if (onCancelAction) { @@ -48,10 +52,14 @@ export const CreateChatModal = () => { const loadMore = useCallback(() => { if (currentUser) { - dispatch(followersUserListActions.setFollowers(currentUser?.user_id)) - dispatch(userListActions.loadMore(FOLLOWERS_USER_LIST_TAG)) + if (defaultUserList === 'chats') { + dispatch(fetchMoreChats()) + } else { + dispatch(followersUserListActions.setFollowers(currentUser?.user_id)) + dispatch(userListActions.loadMore(FOLLOWERS_USER_LIST_TAG)) + } } - }, [dispatch, currentUser]) + }, [dispatch, defaultUserList, currentUser]) const handleOpenInboxUnavailableModal = useCallback( (user: User) => {