diff --git a/native/avatars/emoji-avatar-creation.react.js b/native/avatars/emoji-avatar-creation.react.js index 6e3910e9e..f74cb24b6 100644 --- a/native/avatars/emoji-avatar-creation.react.js +++ b/native/avatars/emoji-avatar-creation.react.js @@ -1,254 +1,215 @@ // @flow import * as React from 'react'; import { View, Text, TouchableWithoutFeedback, ActivityIndicator, } from 'react-native'; import EmojiPicker from 'rn-emoji-keyboard'; -import { changeThreadSettingsActionTypes } from 'lib/actions/thread-actions.js'; -import { updateUserAvatarActionTypes } from 'lib/actions/user-actions.js'; -import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; -import { savedEmojiAvatarSelectorForThread } from 'lib/selectors/thread-selectors.js'; -import { savedEmojiAvatarSelectorForCurrentUser } from 'lib/selectors/user-selectors.js'; -import type { ClientEmojiAvatar } from 'lib/types/avatar-types.js'; +import type { + UpdateUserAvatarRequest, + ClientEmojiAvatar, +} from 'lib/types/avatar-types'; import Avatar from './avatar.react.js'; import Button from '../components/button.react.js'; import ColorRows from '../components/color-rows.react.js'; import { useSelector } from '../redux/redux-utils.js'; +import type { AppState } from '../redux/state-types.js'; import { useStyles } from '../themes/colors.js'; -import { - useSaveUserAvatar, - useSaveThreadAvatar, -} from '../utils/avatar-utils.js'; - -const userAvatarLoadingStatusSelector = createLoadingStatusSelector( - updateUserAvatarActionTypes, -); - -const threadAvatarLoadingStatusSelector = createLoadingStatusSelector( - changeThreadSettingsActionTypes, - `${changeThreadSettingsActionTypes.started}:avatar`, -); - -export type EmojiAvatarCreationParams = { - +threadID?: string, - +containingThreadID?: ?string, -}; type Props = { - +threadID?: string, - +containingThreadID?: ?string, + +saveAvatarCall: (newAvatarRequest: UpdateUserAvatarRequest) => mixed, + +saveAvatarCallLoading: boolean, + +savedEmojiAvatarSelector: (state: AppState) => () => ClientEmojiAvatar, }; function EmojiAvatarCreation(props: Props): React.Node { - const { threadID, containingThreadID } = props; + const { saveAvatarCall, saveAvatarCallLoading, savedEmojiAvatarSelector } = + props; - const selector = threadID - ? savedEmojiAvatarSelectorForThread(threadID, containingThreadID) - : savedEmojiAvatarSelectorForCurrentUser; - - const savedEmojiAvatarFunc = useSelector(selector); + const savedEmojiAvatarFunc = useSelector(savedEmojiAvatarSelector); const [pendingEmoji, setPendingEmoji] = React.useState( () => savedEmojiAvatarFunc().emoji, ); const [pendingColor, setPendingColor] = React.useState( () => savedEmojiAvatarFunc().color, ); const [emojiKeyboardOpen, setEmojiKeyboardOpen] = React.useState(false); const styles = useStyles(unboundStyles); - const saveUserAvatar = useSaveUserAvatar(); - const saveThreadAvatar = useSaveThreadAvatar(); - - const saveUserAvatarCallLoading = useSelector( - state => userAvatarLoadingStatusSelector(state) === 'loading', - ); - const saveThreadAvatarCallLoading = useSelector( - state => threadAvatarLoadingStatusSelector(state) === 'loading', - ); - const onPressEditEmoji = React.useCallback(() => { setEmojiKeyboardOpen(true); }, []); const onPressSetAvatar = React.useCallback(() => { const newEmojiAvatarRequest = { type: 'emoji', emoji: pendingEmoji, color: pendingColor, }; - if (!threadID) { - saveUserAvatar(newEmojiAvatarRequest); - } else { - saveThreadAvatar(newEmojiAvatarRequest, threadID); - } - }, [pendingColor, pendingEmoji, saveThreadAvatar, saveUserAvatar, threadID]); + saveAvatarCall(newEmojiAvatarRequest); + }, [pendingColor, pendingEmoji, saveAvatarCall]); const onPressReset = React.useCallback(() => { const resetEmojiAvatar = savedEmojiAvatarFunc(); setPendingEmoji(resetEmojiAvatar.emoji); setPendingColor(resetEmojiAvatar.color); }, [savedEmojiAvatarFunc]); const onEmojiSelected = React.useCallback(emoji => { setPendingEmoji(emoji.emoji); }, []); const onEmojiKeyboardClose = React.useCallback( () => setEmojiKeyboardOpen(false), [], ); const stagedAvatarInfo: ClientEmojiAvatar = React.useMemo( () => ({ type: 'emoji', emoji: pendingEmoji, color: pendingColor, }), [pendingColor, pendingEmoji], ); const loadingContainer = React.useMemo(() => { - if (!saveUserAvatarCallLoading && !saveThreadAvatarCallLoading) { + if (!saveAvatarCallLoading) { return null; } return ( ); - }, [ - saveThreadAvatarCallLoading, - saveUserAvatarCallLoading, - styles.loadingContainer, - ]); + }, [saveAvatarCallLoading, styles.loadingContainer]); return ( {loadingContainer} Edit Emoji ); } const unboundStyles = { container: { flex: 1, justifyContent: 'space-between', }, emojiAvatarCreationContainer: { paddingTop: 16, }, stagedAvatarSection: { backgroundColor: 'panelForeground', paddingVertical: 24, alignItems: 'center', }, editEmojiText: { color: 'purpleLink', marginTop: 16, fontWeight: '500', fontSize: 16, lineHeight: 24, textAlign: 'center', }, colorRowsSection: { paddingVertical: 24, marginTop: 24, backgroundColor: 'panelForeground', alignItems: 'center', }, selectedColorOuterRing: { backgroundColor: 'modalSubtext', }, buttonsContainer: { paddingHorizontal: 16, paddingBottom: 8, }, saveButton: { backgroundColor: 'purpleButton', paddingVertical: 12, borderRadius: 8, }, saveButtonText: { color: 'whiteText', textAlign: 'center', fontWeight: '500', fontSize: 16, lineHeight: 24, }, resetButton: { padding: 12, borderRadius: 8, marginTop: 8, alignSelf: 'center', }, resetButtonText: { color: 'redText', textAlign: 'center', fontWeight: '500', fontSize: 16, lineHeight: 24, }, loadingContainer: { position: 'absolute', backgroundColor: 'black', width: 112, height: 112, borderRadius: 56, opacity: 0.6, justifyContent: 'center', }, }; export default EmojiAvatarCreation; diff --git a/native/chat/settings/emoji-thread-avatar-creation.react.js b/native/chat/settings/emoji-thread-avatar-creation.react.js index 3efff6c5c..93fa3dcd2 100644 --- a/native/chat/settings/emoji-thread-avatar-creation.react.js +++ b/native/chat/settings/emoji-thread-avatar-creation.react.js @@ -1,256 +1,57 @@ // @flow import * as React from 'react'; -import { - View, - Text, - TouchableWithoutFeedback, - ActivityIndicator, -} from 'react-native'; -import EmojiPicker from 'rn-emoji-keyboard'; import { changeThreadSettingsActionTypes } from 'lib/actions/thread-actions.js'; -import { updateUserAvatarActionTypes } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { savedEmojiAvatarSelectorForThread } from 'lib/selectors/thread-selectors.js'; -import { savedEmojiAvatarSelectorForCurrentUser } from 'lib/selectors/user-selectors.js'; -import type { ClientEmojiAvatar } from 'lib/types/avatar-types.js'; -import Avatar from '../../avatars/avatar.react.js'; +import EmojiAvatarCreation from '../../avatars/emoji-avatar-creation.react.js'; import type { ChatNavigationProp } from '../../chat/chat.react.js'; -import Button from '../../components/button.react.js'; -import ColorRows from '../../components/color-rows.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; -import { useStyles } from '../../themes/colors.js'; -import { - useSaveUserAvatar, - useSaveThreadAvatar, -} from '../../utils/avatar-utils.js'; - -const userAvatarLoadingStatusSelector = createLoadingStatusSelector( - updateUserAvatarActionTypes, -); +import { useSaveThreadAvatar } from '../../utils/avatar-utils.js'; const threadAvatarLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:avatar`, ); export type EmojiThreadAvatarCreationParams = { - +threadID?: string, + +threadID: string, +containingThreadID?: ?string, }; type Props = { +navigation: ChatNavigationProp<'EmojiThreadAvatarCreation'>, +route: NavigationRoute<'EmojiThreadAvatarCreation'>, }; function EmojiThreadAvatarCreation(props: Props): React.Node { const { threadID, containingThreadID } = props.route.params; - const selector = threadID - ? savedEmojiAvatarSelectorForThread(threadID, containingThreadID) - : savedEmojiAvatarSelectorForCurrentUser; - - const savedEmojiAvatarFunc = useSelector(selector); - - const [pendingEmoji, setPendingEmoji] = React.useState( - () => savedEmojiAvatarFunc().emoji, - ); - const [pendingColor, setPendingColor] = React.useState( - () => savedEmojiAvatarFunc().color, + const selector = savedEmojiAvatarSelectorForThread( + threadID, + containingThreadID, ); - const [emojiKeyboardOpen, setEmojiKeyboardOpen] = - React.useState(false); - const styles = useStyles(unboundStyles); - - const saveUserAvatar = useSaveUserAvatar(); const saveThreadAvatar = useSaveThreadAvatar(); - - const saveUserAvatarCallLoading = useSelector( - state => userAvatarLoadingStatusSelector(state) === 'loading', - ); const saveThreadAvatarCallLoading = useSelector( state => threadAvatarLoadingStatusSelector(state) === 'loading', ); - const onPressEditEmoji = React.useCallback(() => { - setEmojiKeyboardOpen(true); - }, []); - - const onPressSetAvatar = React.useCallback(() => { - const newEmojiAvatarRequest = { - type: 'emoji', - emoji: pendingEmoji, - color: pendingColor, - }; - - if (!threadID) { - saveUserAvatar(newEmojiAvatarRequest); - } else { - saveThreadAvatar(newEmojiAvatarRequest, threadID); - } - }, [pendingColor, pendingEmoji, saveThreadAvatar, saveUserAvatar, threadID]); - - const onPressReset = React.useCallback(() => { - const resetEmojiAvatar = savedEmojiAvatarFunc(); - - setPendingEmoji(resetEmojiAvatar.emoji); - setPendingColor(resetEmojiAvatar.color); - }, [savedEmojiAvatarFunc]); - - const onEmojiSelected = React.useCallback(emoji => { - setPendingEmoji(emoji.emoji); - }, []); - - const onEmojiKeyboardClose = React.useCallback( - () => setEmojiKeyboardOpen(false), - [], - ); - - const stagedAvatarInfo: ClientEmojiAvatar = React.useMemo( - () => ({ - type: 'emoji', - emoji: pendingEmoji, - color: pendingColor, - }), - [pendingColor, pendingEmoji], + const saveThreadAvatarCallback = React.useCallback( + newAvatarRequest => saveThreadAvatar(newAvatarRequest, threadID), + [saveThreadAvatar, threadID], ); - const loadingContainer = React.useMemo(() => { - if (!saveUserAvatarCallLoading && !saveThreadAvatarCallLoading) { - return null; - } - - return ( - - - - ); - }, [ - saveThreadAvatarCallLoading, - saveUserAvatarCallLoading, - styles.loadingContainer, - ]); - return ( - - - - - - - - {loadingContainer} - - Edit Emoji - - - - - - - - - - - - - + ); } -const unboundStyles = { - container: { - flex: 1, - justifyContent: 'space-between', - }, - emojiAvatarCreationContainer: { - paddingTop: 16, - }, - stagedAvatarSection: { - backgroundColor: 'panelForeground', - paddingVertical: 24, - alignItems: 'center', - }, - editEmojiText: { - color: 'purpleLink', - marginTop: 16, - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - textAlign: 'center', - }, - colorRowsSection: { - paddingVertical: 24, - marginTop: 24, - backgroundColor: 'panelForeground', - alignItems: 'center', - }, - selectedColorOuterRing: { - backgroundColor: 'modalSubtext', - }, - buttonsContainer: { - paddingHorizontal: 16, - paddingBottom: 8, - }, - saveButton: { - backgroundColor: 'purpleButton', - paddingVertical: 12, - borderRadius: 8, - }, - saveButtonText: { - color: 'whiteText', - textAlign: 'center', - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - }, - resetButton: { - padding: 12, - borderRadius: 8, - marginTop: 8, - alignSelf: 'center', - }, - resetButtonText: { - color: 'redText', - textAlign: 'center', - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - }, - loadingContainer: { - position: 'absolute', - backgroundColor: 'black', - width: 112, - height: 112, - borderRadius: 56, - opacity: 0.6, - justifyContent: 'center', - }, -}; - export default EmojiThreadAvatarCreation; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js index d85576e2b..e41d1ee86 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,197 +1,196 @@ // @flow import type { RouteProp } from '@react-navigation/native'; import type { ActionResultModalParams } from './action-result-modal.react.js'; import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react.js'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react.js'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react.js'; import type { FullScreenThreadMediaGalleryParams } from '../chat/fullscreen-thread-media-gallery.react.js'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import type { MessageReactionsModalParams } from '../chat/message-reactions-modal.react.js'; import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react.js'; import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react.js'; import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react.js'; import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react.js'; import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react.js'; import type { DeleteThreadParams } from '../chat/settings/delete-thread.react.js'; import type { EmojiThreadAvatarCreationParams } from '../chat/settings/emoji-thread-avatar-creation.react.js'; import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react.js'; import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react.js'; import type { SubchannelListModalParams } from '../chat/subchannels-list-modal.react.js'; import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react.js'; import type { CameraModalParams } from '../media/camera-modal.react.js'; import type { ImageModalParams } from '../media/image-modal.react.js'; import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js'; -import type { EmojiUserAvatarCreationParams } from '../profile/emoji-user-avatar-creation.react.js'; import type { RelationshipListItemTooltipModalParams } from '../profile/relationship-list-item-tooltip-modal.react.js'; export const ActionResultModalRouteName = 'ActionResultModal'; export const AddUsersModalRouteName = 'AddUsersModal'; export const AppearancePreferencesRouteName = 'AppearancePreferences'; export const AppRouteName = 'App'; export const AppsRouteName = 'Apps'; export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList'; export const BlockListRouteName = 'BlockList'; export const BuildInfoRouteName = 'BuildInfo'; export const CalendarRouteName = 'Calendar'; export const CameraModalRouteName = 'CameraModal'; export const ChatRouteName = 'Chat'; export const ChatThreadListRouteName = 'ChatThreadList'; export const ColorSelectorModalRouteName = 'ColorSelectorModal'; export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal'; export const ComposeSubchannelRouteName = 'ComposeSubchannel'; export const CommunityDrawerNavigatorRouteName = 'CommunityDrawerNavigator'; export const CustomServerModalRouteName = 'CustomServerModal'; export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications'; export const DeleteAccountRouteName = 'DeleteAccount'; export const DeleteThreadRouteName = 'DeleteThread'; export const DevToolsRouteName = 'DevTools'; export const EditPasswordRouteName = 'EditPassword'; export const EmojiThreadAvatarCreationRouteName = 'EmojiThreadAvatarCreation'; export const EmojiUserAvatarCreationRouteName = 'EmojiUserAvatarCreation'; export const FriendListRouteName = 'FriendList'; export const FullScreenThreadMediaGalleryRouteName = 'FullScreenThreadMediaGallery'; export const HomeChatThreadListRouteName = 'HomeChatThreadList'; export const ImageModalRouteName = 'ImageModal'; export const ImagePasteModalRouteName = 'ImagePasteModal'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const MessageListRouteName = 'MessageList'; export const MessageReactionsModalRouteName = 'MessageReactionsModal'; export const MultimediaMessageTooltipModalRouteName = 'MultimediaMessageTooltipModal'; export const PrivacyPreferencesRouteName = 'PrivacyPreferences'; export const ProfileRouteName = 'Profile'; export const ProfileScreenRouteName = 'ProfileScreen'; export const RelationshipListItemTooltipModalRouteName = 'RelationshipListItemTooltipModal'; export const RobotextMessageTooltipModalRouteName = 'RobotextMessageTooltipModal'; export const SidebarListModalRouteName = 'SidebarListModal'; export const SubchannelsListModalRouteName = 'SubchannelsListModal'; export const TabNavigatorRouteName = 'TabNavigator'; export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export type RootParamList = { +LoggedOutModal: void, +App: void, +ThreadPickerModal: ThreadPickerModalParams, +AddUsersModal: AddUsersModalParams, +CustomServerModal: CustomServerModalParams, +ColorSelectorModal: ColorSelectorModalParams, +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, +TermsAndPrivacyModal: TermsAndPrivacyModalParams, +SubchannelsListModal: SubchannelListModalParams, +MessageReactionsModal: MessageReactionsModalParams, }; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +RelationshipListItemTooltipModal: RelationshipListItemTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +CommunityDrawerNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +CameraModal: CameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, ...TooltipModalParamList, }; export type TabParamList = { +Calendar: void, +Chat: void, +Profile: void, +Apps: void, }; export type ChatParamList = { +ChatThreadList: void, +MessageList: MessageListParams, +ComposeSubchannel: ComposeSubchannelParams, +ThreadSettings: ThreadSettingsParams, +EmojiThreadAvatarCreation: EmojiThreadAvatarCreationParams, +DeleteThread: DeleteThreadParams, +FullScreenThreadMediaGallery: FullScreenThreadMediaGalleryParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, - +EmojiUserAvatarCreation: EmojiUserAvatarCreationParams, + +EmojiUserAvatarCreation: void, +EditPassword: void, +DeleteAccount: void, +BuildInfo: void, +DevTools: void, +AppearancePreferences: void, +PrivacyPreferences: void, +DefaultNotifications: void, +FriendList: void, +BlockList: void, }; export type CommunityDrawerParamList = { +TabNavigator: void }; export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, ...CommunityDrawerParamList, }; export type NavigationRoute> = RouteProp; export const accountModals = [LoggedOutModalRouteName]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, FullScreenThreadMediaGalleryRouteName, ]; diff --git a/native/profile/emoji-user-avatar-creation.react.js b/native/profile/emoji-user-avatar-creation.react.js index 0633bb9d6..9b68b077b 100644 --- a/native/profile/emoji-user-avatar-creation.react.js +++ b/native/profile/emoji-user-avatar-creation.react.js @@ -1,256 +1,33 @@ // @flow import * as React from 'react'; -import { - View, - Text, - TouchableWithoutFeedback, - ActivityIndicator, -} from 'react-native'; -import EmojiPicker from 'rn-emoji-keyboard'; -import { changeThreadSettingsActionTypes } from 'lib/actions/thread-actions.js'; import { updateUserAvatarActionTypes } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; -import { savedEmojiAvatarSelectorForThread } from 'lib/selectors/thread-selectors.js'; import { savedEmojiAvatarSelectorForCurrentUser } from 'lib/selectors/user-selectors.js'; -import type { ClientEmojiAvatar } from 'lib/types/avatar-types.js'; -import Avatar from '../avatars/avatar.react.js'; -import Button from '../components/button.react.js'; -import ColorRows from '../components/color-rows.react.js'; -import type { NavigationRoute } from '../navigation/route-names.js'; -import type { ProfileNavigationProp } from '../profile/profile.react.js'; +import EmojiAvatarCreation from '../avatars/emoji-avatar-creation.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { useStyles } from '../themes/colors.js'; -import { - useSaveUserAvatar, - useSaveThreadAvatar, -} from '../utils/avatar-utils.js'; +import { useSaveUserAvatar } from '../utils/avatar-utils.js'; const userAvatarLoadingStatusSelector = createLoadingStatusSelector( updateUserAvatarActionTypes, ); -const threadAvatarLoadingStatusSelector = createLoadingStatusSelector( - changeThreadSettingsActionTypes, - `${changeThreadSettingsActionTypes.started}:avatar`, -); - -export type EmojiUserAvatarCreationParams = { - +threadID?: string, - +containingThreadID?: ?string, -}; - -type Props = { - +navigation: ProfileNavigationProp<'EmojiUserAvatarCreation'>, - +route: NavigationRoute<'EmojiUserAvatarCreation'>, -}; - -function EmojiUserAvatarCreation(props: Props): React.Node { - const { threadID, containingThreadID } = props.route.params; - - const selector = threadID - ? savedEmojiAvatarSelectorForThread(threadID, containingThreadID) - : savedEmojiAvatarSelectorForCurrentUser; - - const savedEmojiAvatarFunc = useSelector(selector); - - const [pendingEmoji, setPendingEmoji] = React.useState( - () => savedEmojiAvatarFunc().emoji, - ); - const [pendingColor, setPendingColor] = React.useState( - () => savedEmojiAvatarFunc().color, - ); - const [emojiKeyboardOpen, setEmojiKeyboardOpen] = - React.useState(false); - - const styles = useStyles(unboundStyles); - +// eslint-disable-next-line no-unused-vars +function EmojiUserAvatarCreation(props: { ... }): React.Node { const saveUserAvatar = useSaveUserAvatar(); - const saveThreadAvatar = useSaveThreadAvatar(); - const saveUserAvatarCallLoading = useSelector( state => userAvatarLoadingStatusSelector(state) === 'loading', ); - const saveThreadAvatarCallLoading = useSelector( - state => threadAvatarLoadingStatusSelector(state) === 'loading', - ); - - const onPressEditEmoji = React.useCallback(() => { - setEmojiKeyboardOpen(true); - }, []); - - const onPressSetAvatar = React.useCallback(() => { - const newEmojiAvatarRequest = { - type: 'emoji', - emoji: pendingEmoji, - color: pendingColor, - }; - - if (!threadID) { - saveUserAvatar(newEmojiAvatarRequest); - } else { - saveThreadAvatar(newEmojiAvatarRequest, threadID); - } - }, [pendingColor, pendingEmoji, saveThreadAvatar, saveUserAvatar, threadID]); - - const onPressReset = React.useCallback(() => { - const resetEmojiAvatar = savedEmojiAvatarFunc(); - - setPendingEmoji(resetEmojiAvatar.emoji); - setPendingColor(resetEmojiAvatar.color); - }, [savedEmojiAvatarFunc]); - - const onEmojiSelected = React.useCallback(emoji => { - setPendingEmoji(emoji.emoji); - }, []); - - const onEmojiKeyboardClose = React.useCallback( - () => setEmojiKeyboardOpen(false), - [], - ); - - const stagedAvatarInfo: ClientEmojiAvatar = React.useMemo( - () => ({ - type: 'emoji', - emoji: pendingEmoji, - color: pendingColor, - }), - [pendingColor, pendingEmoji], - ); - - const loadingContainer = React.useMemo(() => { - if (!saveUserAvatarCallLoading && !saveThreadAvatarCallLoading) { - return null; - } - - return ( - - - - ); - }, [ - saveThreadAvatarCallLoading, - saveUserAvatarCallLoading, - styles.loadingContainer, - ]); return ( - - - - - - - - {loadingContainer} - - Edit Emoji - - - - - - - - - - - - - + ); } -const unboundStyles = { - container: { - flex: 1, - justifyContent: 'space-between', - }, - emojiAvatarCreationContainer: { - paddingTop: 16, - }, - stagedAvatarSection: { - backgroundColor: 'panelForeground', - paddingVertical: 24, - alignItems: 'center', - }, - editEmojiText: { - color: 'purpleLink', - marginTop: 16, - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - textAlign: 'center', - }, - colorRowsSection: { - paddingVertical: 24, - marginTop: 24, - backgroundColor: 'panelForeground', - alignItems: 'center', - }, - selectedColorOuterRing: { - backgroundColor: 'modalSubtext', - }, - buttonsContainer: { - paddingHorizontal: 16, - paddingBottom: 8, - }, - saveButton: { - backgroundColor: 'purpleButton', - paddingVertical: 12, - borderRadius: 8, - }, - saveButtonText: { - color: 'whiteText', - textAlign: 'center', - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - }, - resetButton: { - padding: 12, - borderRadius: 8, - marginTop: 8, - alignSelf: 'center', - }, - resetButtonText: { - color: 'redText', - textAlign: 'center', - fontWeight: '500', - fontSize: 16, - lineHeight: 24, - }, - loadingContainer: { - position: 'absolute', - backgroundColor: 'black', - width: 112, - height: 112, - borderRadius: 56, - opacity: 0.6, - justifyContent: 'center', - }, -}; - export default EmojiUserAvatarCreation; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index c026b4c92..7b9d02514 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,435 +1,432 @@ // @flow import * as React from 'react'; import { View, Text, Alert, Platform, ScrollView } from 'react-native'; import { logOutActionTypes, logOut } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import { type PreRequestUserState } from 'lib/types/session-types.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import { SingleLine } from '../components/single-line.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { EditPasswordRouteName, EmojiUserAvatarCreationRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, PrivacyPreferencesRouteName, DefaultNotificationsPreferencesRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, }; function ProfileRow(props: ProfileRowProps): React.Node { const { content, onPress, danger } = props; return ( ); } type BaseProps = { +navigation: ProfileNavigationProp<'ProfileScreen'>, +route: NavigationRoute<'ProfileScreen'>, }; type Props = { ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +preRequestUserState: PreRequestUserState, +logOutLoading: boolean, +colors: Colors, +styles: typeof unboundStyles, +dispatchActionPromise: DispatchActionPromise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, +shouldRenderAvatars: boolean, }; class ProfileScreen extends React.PureComponent { get loggedOutOrLoggingOut() { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render() { let developerTools, defaultNotifications; const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( ); defaultNotifications = ( ); } let passwordEditionUI; if (accountHasPassword(this.props.currentUserInfo)) { passwordEditionUI = ( Password •••••••••••••••• ); } let avatarSection; if (this.props.shouldRenderAvatars) { avatarSection = ( <> USER AVATAR ); } return ( {avatarSection} ACCOUNT Logged in as {this.props.stringForUser} {passwordEditionUI} PREFERENCES {defaultNotifications} {developerTools} ); } onPressEmojiAvatarFlow = () => { - this.props.navigation.navigate<'EmojiUserAvatarCreation'>({ - name: EmojiUserAvatarCreationRouteName, - params: {}, - }); + this.props.navigation.navigate(EmojiUserAvatarCreationRouteName); }; onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } if (!this.props.isAccountWithPassword) { Alert.alert( 'Log out', 'Are you sure you want to log out?', [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const alertDescription = 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; logOutWithoutDeletingNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.logOut(); }; logOutAndDeleteNativeCredentialsWrapper = async () => { if (this.loggedOutOrLoggingOut) { return; } await this.deleteNativeCredentials(); this.logOut(); }; logOut() { this.props.dispatchActionPromise( logOutActionTypes, this.props.logOut(this.props.preRequestUserState), ); } async deleteNativeCredentials() { await deleteNativeCredentialsFor(); } navigateIfActive(name) { this.props.navigation.navigate({ name }); } onPressEditPassword = () => { this.navigateIfActive(EditPasswordRouteName); }; onPressDeleteAccount = () => { this.navigateIfActive(DeleteAccountRouteName); }; onPressBuildInfo = () => { this.navigateIfActive(BuildInfoRouteName); }; onPressDevTools = () => { this.navigateIfActive(DevToolsRouteName); }; onPressAppearance = () => { this.navigateIfActive(AppearancePreferencesRouteName); }; onPressPrivacy = () => { this.navigateIfActive(PrivacyPreferencesRouteName); }; onPressDefaultNotifications = () => { this.navigateIfActive(DefaultNotificationsPreferencesRouteName); }; onPressFriendList = () => { this.navigateIfActive(FriendListRouteName); }; onPressBlockList = () => { this.navigateIfActive(BlockListRouteName); }; } const unboundStyles = { avatarSection: { alignItems: 'center', paddingVertical: 16, }, container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, loggedInLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, paddedRow: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 1, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', flex: 1, }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, }; const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType = React.memo(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const preRequestUserState = useSelector(preRequestUserStateSelector); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); const callLogOut = useServerCall(logOut); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedProfileScreen;