diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index 36ddc2dc65b..57a1d526578 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -856,6 +856,7 @@ export enum FavoriteSource { NAVIGATOR = 'navigator' } export enum FollowSource { + INBOX_UNAVAILABLE_MODAL = 'inbox unavailable modal', PROFILE_PAGE = 'profile page', TRACK_PAGE = 'track page', COLLECTION_PAGE = 'collection page', diff --git a/packages/common/src/store/pages/chat/selectors.ts b/packages/common/src/store/pages/chat/selectors.ts index 2c64a0100f4..05eb4b345f5 100644 --- a/packages/common/src/store/pages/chat/selectors.ts +++ b/packages/common/src/store/pages/chat/selectors.ts @@ -311,8 +311,12 @@ export const getCanCreateChat = createSelector( action = ChatPermissionAction.WAIT } else if (blockees.includes(user.user_id)) { action = ChatPermissionAction.UNBLOCK - } else if (userPermissions.permits === ChatPermission.TIPPERS) { + } else if (userPermissions.permit_list.includes(ChatPermission.TIPPERS)) { action = ChatPermissionAction.TIP + } else if ( + userPermissions.permit_list.includes(ChatPermission.FOLLOWERS) + ) { + action = ChatPermissionAction.FOLLOW } else { action = ChatPermissionAction.NONE } diff --git a/packages/common/src/store/pages/chat/types.ts b/packages/common/src/store/pages/chat/types.ts index cc7723ba5ff..53b61587048 100644 --- a/packages/common/src/store/pages/chat/types.ts +++ b/packages/common/src/store/pages/chat/types.ts @@ -10,6 +10,8 @@ export enum ChatPermissionAction { NONE, /** Current user can tip user */ TIP, + /** Current user can follow user */ + FOLLOW, /** Current user can unblock user */ UNBLOCK, /** User is signed out and needs to sign in */ diff --git a/packages/common/src/store/social/users/actions.ts b/packages/common/src/store/social/users/actions.ts index 14ea315824d..a13aa07e2c8 100644 --- a/packages/common/src/store/social/users/actions.ts +++ b/packages/common/src/store/social/users/actions.ts @@ -1,3 +1,4 @@ +import { Action } from '@reduxjs/toolkit' import { createCustomAction } from 'typesafe-actions' import { FollowSource, ShareSource } from '../../../models/Analytics' @@ -22,13 +23,14 @@ export const followUser = createCustomAction( ( userId: ID, source: FollowSource, - trackId?: ID // in case the user is following the artist from a gated track page / modal - ) => ({ userId, source, trackId }) + trackId?: ID, // in case the user is following the artist from a gated track page / modal + onSuccessActions?: Action[] + ) => ({ userId, source, trackId, onSuccessActions }) ) export const followUserSucceeded = createCustomAction( FOLLOW_USER_SUCCEEDED, - (userId: ID) => ({ userId }) + (userId: ID, onSuccessActions?: Action[]) => ({ userId, onSuccessActions }) ) export const followUserFailed = createCustomAction( diff --git a/packages/harmony/src/assets/icons/MessageSlash.svg b/packages/harmony/src/assets/icons/MessageSlash.svg new file mode 100644 index 00000000000..597d42e705c --- /dev/null +++ b/packages/harmony/src/assets/icons/MessageSlash.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/harmony/src/icons/utilityIcons.ts b/packages/harmony/src/icons/utilityIcons.ts index c3768b39021..cbbe304b5ef 100644 --- a/packages/harmony/src/icons/utilityIcons.ts +++ b/packages/harmony/src/icons/utilityIcons.ts @@ -65,6 +65,7 @@ import IconMerchSVG from '../assets/icons/Merch.svg' import IconMessageSVG from '../assets/icons/Message.svg' import IconMessageBlockSVG from '../assets/icons/MessageBlock.svg' import IconMessageLockedSVG from '../assets/icons/MessageLocked.svg' +import IconMessageSlashSVG from '../assets/icons/MessageSlash.svg' import IconMessageUnblockSVG from '../assets/icons/MessageUnblock.svg' import IconMessagesSVG from '../assets/icons/Messages.svg' import IconMinusSVG from '../assets/icons/Minus.svg' @@ -204,6 +205,7 @@ export const IconMessage = IconMessageSVG as IconComponent export const IconStar = IconStarSVG as IconComponent export const IconCastAirplay = IconCastAirplaySVG as IconComponent export const IconMessageBlock = IconMessageBlockSVG as IconComponent +export const IconMessageSlash = IconMessageSlashSVG as IconComponent export const IconStars = IconStarsSVG as IconComponent export const IconCastChromecast = IconCastChromecastSVG as IconComponent export const IconMessageLocked = IconMessageLockedSVG as IconComponent diff --git a/packages/mobile/src/components/block-messages-drawer/BlockMessagesDrawer.tsx b/packages/mobile/src/components/block-messages-drawer/BlockMessagesDrawer.tsx index 35e9fca1e32..2f73e1499e4 100644 --- a/packages/mobile/src/components/block-messages-drawer/BlockMessagesDrawer.tsx +++ b/packages/mobile/src/components/block-messages-drawer/BlockMessagesDrawer.tsx @@ -8,7 +8,7 @@ import { import { View } from 'react-native' import { useDispatch, useSelector } from 'react-redux' -import { IconMessageBlock, IconInfo, Button } from '@audius/harmony-native' +import { IconMessageSlash, IconInfo, Button } from '@audius/harmony-native' import { Text } from 'app/components/core' import { NativeDrawer } from 'app/components/drawer' import { useDrawer } from 'app/hooks/useDrawer' @@ -158,7 +158,7 @@ export const BlockMessagesDrawer = () => { - + {messages.title} diff --git a/packages/mobile/src/components/inbox-unavailable-drawer/InboxUnavailableDrawer.tsx b/packages/mobile/src/components/inbox-unavailable-drawer/InboxUnavailableDrawer.tsx index fa6ceb8201e..c3fcee688f7 100644 --- a/packages/mobile/src/components/inbox-unavailable-drawer/InboxUnavailableDrawer.tsx +++ b/packages/mobile/src/components/inbox-unavailable-drawer/InboxUnavailableDrawer.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import { useCallback } from 'react' +import { FollowSource } from '@audius/common/models' import { accountSelectors, cacheUsersSelectors, @@ -9,9 +10,11 @@ import { makeChatId, ChatPermissionAction, tippingActions, - useInboxUnavailableModal + useInboxUnavailableModal, + usersSocialActions } from '@audius/common/store' import { CHAT_BLOG_POST_URL } from '@audius/common/utils' +import type { Action } from '@reduxjs/toolkit' import { View } from 'react-native' import { useDispatch, useSelector } from 'react-redux' @@ -23,6 +26,7 @@ import { makeStyles, flexRowCentered } from 'app/styles' import { useColor } from 'app/utils/theme' import { UserBadges } from '../user-badges' +const { followUser } = usersSocialActions const { unblockUser, createChat } = chatActions const { getCanCreateChat } = chatSelectors @@ -38,11 +42,19 @@ const messages = { {' a tip before you can send them messages.'} ), + followRequired: (displayName: ReactNode) => ( + <> + {'You must follow '} + {displayName} + {' before you can send them messages.'} + + ), noAction: "You can't send messages to ", info: 'This will not affect their ability to view your profile or interact with your content.', unblockUser: 'Unblock User', learnMore: 'Learn More', sendAudio: 'Send $AUDIO', + follow: 'Follow', cancel: 'Cancel' } @@ -157,6 +169,25 @@ const DrawerContent = ({ data, onClose }: DrawerContentProps) => { onClose() }, [onClose, currentUserId, dispatch, navigation, user, presetMessage]) + const handleFollowPress = useCallback(() => { + if (userId) { + const followSuccessActions: Action[] = [ + chatActions.createChat({ + userIds: [userId] + }) + ] + dispatch( + followUser( + userId, + FollowSource.INBOX_UNAVAILABLE_MODAL, + undefined, + followSuccessActions + ) + ) + } + onClose() + }, [userId, dispatch, onClose]) + switch (callToAction) { case ChatPermissionAction.NONE: return ( @@ -206,6 +237,30 @@ const DrawerContent = ({ data, onClose }: DrawerContentProps) => { ) + case ChatPermissionAction.FOLLOW: + return ( + <> + + {messages.followRequired( + user ? ( + + ) : null + )} + + + + ) case ChatPermissionAction.UNBLOCK: return ( <> diff --git a/packages/mobile/src/harmony-native/icons.ts b/packages/mobile/src/harmony-native/icons.ts index 75f1e11f20b..87ea8faa346 100644 --- a/packages/mobile/src/harmony-native/icons.ts +++ b/packages/mobile/src/harmony-native/icons.ts @@ -60,6 +60,7 @@ export { default as IconMerch } from '@audius/harmony/src/assets/icons/Merch.svg export { default as IconStar } from '@audius/harmony/src/assets/icons/Star.svg' export { default as IconCastAirplay } from '@audius/harmony/src/assets/icons/CastAirplay.svg' export { default as IconMessageBlock } from '@audius/harmony/src/assets/icons/MessageBlock.svg' +export { default as IconMessageSlash } from '@audius/harmony/src/assets/icons/MessageSlash.svg' export { default as IconStars } from '@audius/harmony/src/assets/icons/Stars.svg' export { default as IconCastChromecast } from '@audius/harmony/src/assets/icons/CastChromecast.svg' export { default as IconMessageLocked } from '@audius/harmony/src/assets/icons/MessageLocked.svg' diff --git a/packages/mobile/src/screens/chat-screen/ChatUserListItem.tsx b/packages/mobile/src/screens/chat-screen/ChatUserListItem.tsx index 182e1fda7a7..640386d3d0e 100644 --- a/packages/mobile/src/screens/chat-screen/ChatUserListItem.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatUserListItem.tsx @@ -15,7 +15,7 @@ import { View, TouchableOpacity, Keyboard } from 'react-native' import { useDispatch } from 'react-redux' import { - IconMessageBlock, + IconMessageSlash, IconKebabHorizontal, IconUser } from '@audius/harmony-native' @@ -34,6 +34,7 @@ const messages = { followers: 'Followers', ctaNone: 'Cannot Be Messaged', ctaTip: 'Send a Tip To Message', + ctaFollow: 'Follow to Message', ctaBlock: 'Blocked' } @@ -134,6 +135,7 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({ const ctaToTextMap: Record = { [ChatPermissionAction.TIP]: messages.ctaTip, + [ChatPermissionAction.FOLLOW]: messages.ctaFollow, [ChatPermissionAction.UNBLOCK]: messages.ctaBlock, [ChatPermissionAction.NONE]: messages.ctaNone, [ChatPermissionAction.WAIT]: messages.ctaNone, @@ -252,7 +254,7 @@ export const ChatUserListItem = ({ ) : ( - +) { + const { onSuccessActions } = action + // Do any callbacks + if (onSuccessActions) { + // Spread here to unfreeze the action + // Redux sagas can't "put" frozen actions + for (const onSuccessAction of onSuccessActions) { + yield* put({ ...onSuccessAction }) + } + } +} + export function* watchUnfollowUser() { yield* takeEvery(socialActions.UNFOLLOW_USER, unfollowUser) } @@ -434,7 +462,13 @@ export function* watchShareUser() { } const sagas = () => { - return [watchFollowUser, watchUnfollowUser, watchShareUser, errorSagas] + return [ + watchFollowUser, + watchUnfollowUser, + watchFollowUserSucceeded, + watchShareUser, + errorSagas + ] } export default sagas diff --git a/packages/web/src/common/store/social/users/store.test.ts b/packages/web/src/common/store/social/users/store.test.ts index 3b14fecf1fd..c5a0a7c98b0 100644 --- a/packages/web/src/common/store/social/users/store.test.ts +++ b/packages/web/src/common/store/social/users/store.test.ts @@ -46,7 +46,7 @@ describe('follow', () => { fieldName: 'followee_count', delta: 1 }) - .call(sagas.confirmFollowUser, 2, 1) + .call(sagas.confirmFollowUser, 2, 1, undefined) .put( cacheActions.update(Kind.USERS, [ { diff --git a/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx b/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx index 62351b13b55..d96f4c15be9 100644 --- a/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx +++ b/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback } from 'react' -import { User } from '@audius/common/models' +import { FollowSource, User } from '@audius/common/models' import { accountSelectors, cacheUsersSelectors, @@ -9,7 +9,8 @@ import { makeChatId, ChatPermissionAction, tippingActions, - useInboxUnavailableModal + useInboxUnavailableModal, + usersSocialActions } from '@audius/common/store' import { CHAT_BLOG_POST_URL } from '@audius/common/utils' import { @@ -31,6 +32,7 @@ import { UserNameAndBadges } from 'components/user-name-and-badges/UserNameAndBa import { useSelector } from 'utils/reducer' const { unblockUser, createChat } = chatActions +const { followUser } = usersSocialActions const messages = { title: 'Inbox Unavailable', @@ -43,7 +45,15 @@ const messages = { {' a tip before you can send them messages.'} ), + followRequired: (displayName: ReactNode) => ( + <> + {'You must follow '} + {displayName} + {' before you can send them messages.'} + + ), tipButton: 'Send $AUDIO', + follow: 'Follow', unblockContent: 'You cannot send messages to users you have blocked.', unblockButton: 'Unblock', defaultUsername: 'this user' @@ -77,6 +87,18 @@ const actionToContent = ({ buttonText: messages.tipButton, buttonIcon: IconTipping } + case ChatPermissionAction.FOLLOW: + return { + content: messages.followRequired( + user ? ( + + ) : ( + messages.defaultUsername + ) + ), + buttonText: messages.follow, + buttonIcon: null + } case ChatPermissionAction.UNBLOCK: return { content: messages.unblockContent, @@ -108,6 +130,7 @@ export const InboxUnavailableModal = () => { ) const hasAction = callToAction === ChatPermissionAction.TIP || + callToAction === ChatPermissionAction.FOLLOW || callToAction === ChatPermissionAction.UNBLOCK const handleClick = useCallback(() => { @@ -141,6 +164,23 @@ export const InboxUnavailableModal = () => { ] }) ) + } else if (callToAction === ChatPermissionAction.FOLLOW && currentUserId) { + const followSuccessActions: Action[] = [ + chatActions.createChat({ + userIds: [userId] + }) + ] + if (onSuccessAction) { + followSuccessActions.push(onSuccessAction) + } + dispatch( + followUser( + userId, + FollowSource.INBOX_UNAVAILABLE_MODAL, + undefined, + followSuccessActions + ) + ) } else if (callToAction === ChatPermissionAction.UNBLOCK) { dispatch(unblockUser({ userId })) dispatch(createChat({ userIds: [userId], presetMessage })) diff --git a/packages/web/src/pages/chat-page/components/BlockUserConfirmationModal.tsx b/packages/web/src/pages/chat-page/components/BlockUserConfirmationModal.tsx index be4f355c6aa..64da224aee3 100644 --- a/packages/web/src/pages/chat-page/components/BlockUserConfirmationModal.tsx +++ b/packages/web/src/pages/chat-page/components/BlockUserConfirmationModal.tsx @@ -8,7 +8,7 @@ import { ModalHeader, ModalTitle, ModalFooter, - IconMessageBlock, + IconMessageSlash, IconInfo, Button, Hint @@ -70,7 +70,7 @@ export const BlockUserConfirmationModal = ({ return ( - } /> + } />
{messages.content(user, isReportAbuse)}
diff --git a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx index de856488009..0a92d4751d4 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx @@ -14,7 +14,7 @@ import { IconButton, IconKebabHorizontal, IconMessage, - IconMessageBlock, + IconMessageSlash, IconMessageUnblock as IconUnblockMessages, IconUser, PopupMenu @@ -39,7 +39,8 @@ const messages = { unblock: 'Unblock Messages', notPermitted: 'Cannot Be Messaged', sendTipRequired: 'Send a Tip to Message', - unblockRequired: 'Blocked' + unblockRequired: 'Blocked', + followRequired: 'Follow to Message' } type UserResultComposeProps = { @@ -74,21 +75,28 @@ const renderCustomChip = (callToAction: ChatPermissionAction) => { case ChatPermissionAction.TIP: return (
- + {messages.sendTipRequired}
) + case ChatPermissionAction.FOLLOW: + return ( +
+ + {messages.followRequired} +
+ ) case ChatPermissionAction.UNBLOCK: return (
- + {messages.unblockRequired}
) default: return (
- + {messages.notPermitted}
) @@ -145,7 +153,7 @@ export const CreateChatUserResult = (props: UserResultComposeProps) => { onClick: handleUnblockClicked } : { - icon: , + icon: , text: messages.block, onClick: handleBlockClicked } diff --git a/packages/web/src/pages/chat-page/components/UserChatHeader.tsx b/packages/web/src/pages/chat-page/components/UserChatHeader.tsx index f4a214dc88d..a5673c635ae 100644 --- a/packages/web/src/pages/chat-page/components/UserChatHeader.tsx +++ b/packages/web/src/pages/chat-page/components/UserChatHeader.tsx @@ -7,7 +7,7 @@ import { route } from '@audius/common/utils' import { IconButton, PopupMenu, - IconMessageBlock, + IconMessageSlash, IconMessageUnblock, IconUser, IconTrash, @@ -94,7 +94,7 @@ export const UserChatHeader = ({ chatId }: { chatId?: string }) => { } : { text: messages.block, - icon: , + icon: , onClick: handleBlockClicked }, {