diff --git a/native/invite-links/invite-links-navigator.react.js b/native/invite-links/invite-links-navigator.react.js new file mode 100644 index 000000000..41ce33269 --- /dev/null +++ b/native/invite-links/invite-links-navigator.react.js @@ -0,0 +1,83 @@ +// @flow + +import { + createStackNavigator, + type StackNavigationHelpers, + type StackNavigationProp, +} from '@react-navigation/stack'; +import * as React from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import ViewInviteLinksHeaderLeftButton from './view-invite-links-header-left-button.react.js'; +import ViewInviteLinksHeaderTitle from './view-invite-links-header-title.react.js'; +import ViewInviteLinksScreen from './view-invite-links-screen.react.js'; +import { defaultStackScreenOptions } from '../navigation/options.js'; +import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; +import { + type InviteLinkParamList, + ViewInviteLinksRouteName, + type ScreenParamList, +} from '../navigation/route-names.js'; +import { useColors, useStyles } from '../themes/colors.js'; + +const safeAreaEdges = ['bottom']; + +export type InviteLinksNavigationProps< + RouteName: $Keys = $Keys, +> = StackNavigationProp; + +const InviteLinksStack = createStackNavigator< + ScreenParamList, + InviteLinkParamList, + StackNavigationHelpers, +>(); + +const viewInviteLinksOptions = ({ route }) => ({ + // eslint-disable-next-line react/display-name + headerTitle: props => ( + + ), + headerLeft: ViewInviteLinksHeaderLeftButton, + headerBackImage: () => null, + headerBackTitleStyle: { marginLeft: 20 }, +}); + +type Props = { + +navigation: RootNavigationProp<'InviteLinkNavigator'>, + ... +}; +// eslint-disable-next-line no-unused-vars +function InviteLinksNavigator(props: Props): React.Node { + const styles = useStyles(unboundStyles); + const colors = useColors(); + const screenOptions = React.useMemo( + () => ({ + ...defaultStackScreenOptions, + headerStyle: { + backgroundColor: colors.modalBackground, + borderBottomWidth: 1, + }, + }), + [colors.modalBackground], + ); + return ( + + + + + + ); +} + +const unboundStyles = { + container: { + flex: 1, + backgroundColor: 'modalBackground', + }, +}; + +export default InviteLinksNavigator; diff --git a/native/invite-links/view-invite-links-header-left-button.react.js b/native/invite-links/view-invite-links-header-left-button.react.js new file mode 100644 index 000000000..95a5fccfe --- /dev/null +++ b/native/invite-links/view-invite-links-header-left-button.react.js @@ -0,0 +1,19 @@ +// @flow + +import { HeaderBackButton as BaseHeaderBackButton } from '@react-navigation/elements'; +import * as React from 'react'; + +import { useColors } from '../themes/colors.js'; + +type Props = React.ElementConfig; +function ViewInviteLinksHeaderLeftButton(props: Props): React.Node { + const { headerChevron } = useColors(); + if (!props.canGoBack) { + return null; + } + return ( + + ); +} + +export default ViewInviteLinksHeaderLeftButton; diff --git a/native/invite-links/view-invite-links-header-title.react.js b/native/invite-links/view-invite-links-header-title.react.js new file mode 100644 index 000000000..112b292e8 --- /dev/null +++ b/native/invite-links/view-invite-links-header-title.react.js @@ -0,0 +1,25 @@ +// @flow + +import type { HeaderTitleInputProps } from '@react-navigation/elements'; +import { HeaderTitle } from '@react-navigation/elements'; +import * as React from 'react'; + +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; +import { firstLine } from 'lib/utils/string-utils.js'; + +type Props = { + +community: ThreadInfo, + ...HeaderTitleInputProps, +}; +function ViewInviteLinksHeaderTitle(props: Props): React.Node { + const { community, ...rest } = props; + const { uiName } = useResolvedThreadInfo(community); + const title = `Invite people to ${firstLine(uiName)}`; + return {title}; +} + +const MemoizedViewInviteLinksHeaderTitle: React.ComponentType = + React.memo(ViewInviteLinksHeaderTitle); + +export default MemoizedViewInviteLinksHeaderTitle; diff --git a/native/invite-links/view-invite-links-screen.react.js b/native/invite-links/view-invite-links-screen.react.js new file mode 100644 index 000000000..981089678 --- /dev/null +++ b/native/invite-links/view-invite-links-screen.react.js @@ -0,0 +1,126 @@ +// @flow + +import Clipboard from '@react-native-clipboard/clipboard'; +import * as React from 'react'; +import { Text, View } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { primaryInviteLinksSelector } from 'lib/selectors/invite-links-selectors.js'; +import type { InviteLink } from 'lib/types/link-types.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; + +import SWMansionIcon from '../components/swmansion-icon.react.js'; +import { displayActionResultModal } from '../navigation/action-result-modal.js'; +import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; +import type { NavigationRoute } from '../navigation/route-names.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { useStyles, useColors } from '../themes/colors.js'; + +export type ViewInviteLinksScreenParams = { + +community: ThreadInfo, +}; + +type Props = { + +navigation: RootNavigationProp<'ViewInviteLinks'>, + +route: NavigationRoute<'ViewInviteLinks'>, +}; + +const confirmCopy = () => displayActionResultModal('copied!'); + +function ViewInviteLinksScreen(props: Props): React.Node { + const { community } = props.route.params; + const inviteLink: ?InviteLink = useSelector(primaryInviteLinksSelector)[ + community.id + ]; + + const styles = useStyles(unboundStyles); + const { modalForegroundLabel } = useColors(); + const linkUrl = `https://comm.app/invite/${inviteLink?.name ?? ''}`; + const onPressCopy = React.useCallback(() => { + Clipboard.setString(linkUrl); + setTimeout(confirmCopy); + }, [linkUrl]); + + let publicLinkSection = null; + if (inviteLink) { + publicLinkSection = ( + <> + PUBLIC LINK + + + {linkUrl} + + + Copy + + + + Use this public link to invite your friends into the community! + + + + ); + } + return {publicLinkSection}; +} + +const unboundStyles = { + container: { + flex: 1, + paddingTop: 24, + }, + sectionTitle: { + fontSize: 12, + fontWeight: '400', + lineHeight: 18, + color: 'modalBackgroundLabel', + paddingHorizontal: 16, + paddingBottom: 4, + }, + section: { + borderBottomColor: 'modalSeparator', + borderBottomWidth: 1, + borderTopColor: 'modalSeparator', + borderTopWidth: 1, + backgroundColor: 'modalForeground', + padding: 16, + }, + link: { + paddingHorizontal: 16, + paddingVertical: 9, + backgroundColor: 'inviteLinkButtonBackground', + borderRadius: 20, + flexDirection: 'row', + justifyContent: 'space-between', + }, + linkText: { + fontSize: 14, + fontWeight: '400', + lineHeight: 22, + color: 'inviteLinkLinkColor', + }, + button: { + flexDirection: 'row', + alignItems: 'center', + }, + copy: { + fontSize: 12, + fontWeight: '400', + lineHeight: 18, + color: 'modalForegroundLabel', + paddingLeft: 8, + }, + details: { + fontSize: 12, + fontWeight: '400', + lineHeight: 18, + color: 'modalForegroundLabel', + paddingTop: 16, + }, +}; + +export default ViewInviteLinksScreen; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js index 48a63404e..e7e1b37ed 100644 --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -1,261 +1,268 @@ // @flow import { createNavigatorFactory, useNavigationBuilder, type StackNavigationState, type StackOptions, type StackNavigationEventMap, type StackNavigatorProps, type ExtraStackNavigatorProps, type ParamListBase, type StackNavigationHelpers, type StackNavigationProp, } from '@react-navigation/native'; import { StackView, TransitionPresets } from '@react-navigation/stack'; import * as React from 'react'; import { Platform } from 'react-native'; import { enableScreens } from 'react-native-screens'; import AppNavigator from './app-navigator.react.js'; import InviteLinkModal from './invite-link-modal.react.js'; import { defaultStackScreenOptions } from './options.js'; import { RootNavigatorContext } from './root-navigator-context.js'; import RootRouter, { type RootRouterExtraNavigationHelpers, } from './root-router.js'; import { LoggedOutModalRouteName, AppRouteName, ThreadPickerModalRouteName, ImagePasteModalRouteName, AddUsersModalRouteName, CustomServerModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, SidebarListModalRouteName, SubchannelsListModalRouteName, MessageReactionsModalRouteName, type ScreenParamList, type RootParamList, TermsAndPrivacyRouteName, RegistrationRouteName, InviteLinkModalRouteName, + InviteLinkNavigatorRouteName, } from './route-names.js'; import LoggedOutModal from '../account/logged-out-modal.react.js'; import RegistrationNavigator from '../account/registration/registration-navigator.react.js'; import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react.js'; import ThreadPickerModal from '../calendar/thread-picker-modal.react.js'; import ImagePasteModal from '../chat/image-paste-modal.react.js'; import MessageReactionsModal from '../chat/message-reactions-modal.react.js'; import AddUsersModal from '../chat/settings/add-users-modal.react.js'; import ColorSelectorModal from '../chat/settings/color-selector-modal.react.js'; import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react.js'; import SidebarListModal from '../chat/sidebar-list-modal.react.js'; import SubchannelsListModal from '../chat/subchannels-list-modal.react.js'; +import InviteLinksNavigator from '../invite-links/invite-links-navigator.react.js'; import CustomServerModal from '../profile/custom-server-modal.react.js'; enableScreens(); export type RootNavigationHelpers = { ...$Exact>, ...RootRouterExtraNavigationHelpers, ... }; type RootNavigatorProps = StackNavigatorProps>; function RootNavigator({ initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, id, ...rest }: RootNavigatorProps) { const [keyboardHandlingEnabled, setKeyboardHandlingEnabled] = React.useState(true); const mergedScreenOptions = React.useMemo(() => { if (typeof screenOptions === 'function') { return input => ({ ...screenOptions(input), keyboardHandlingEnabled, }); } return { ...screenOptions, keyboardHandlingEnabled, }; }, [screenOptions, keyboardHandlingEnabled]); const { state, descriptors, navigation } = useNavigationBuilder(RootRouter, { id, initialRouteName, children, screenOptions: mergedScreenOptions, defaultScreenOptions, screenListeners, }); const rootNavigationContext = React.useMemo( () => ({ setKeyboardHandlingEnabled }), [setKeyboardHandlingEnabled], ); return ( ); } const createRootNavigator = createNavigatorFactory< StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationHelpers<>, ExtraStackNavigatorProps, >(RootNavigator); const baseTransitionPreset = Platform.select({ ios: TransitionPresets.ModalSlideFromBottomIOS, default: TransitionPresets.FadeFromBottomAndroid, }); const transitionPreset = { ...baseTransitionPreset, cardStyleInterpolator: interpolatorProps => { const baseCardStyleInterpolator = baseTransitionPreset.cardStyleInterpolator(interpolatorProps); const overlayOpacity = interpolatorProps.current.progress.interpolate({ inputRange: [0, 1], outputRange: ([0, 0.7]: number[]), // Flow... extrapolate: 'clamp', }); return { ...baseCardStyleInterpolator, overlayStyle: [ baseCardStyleInterpolator.overlayStyle, { opacity: overlayOpacity }, ], }; }, }; const defaultScreenOptions = { ...defaultStackScreenOptions, ...transitionPreset, cardStyle: { backgroundColor: 'transparent' }, presentation: 'modal', headerShown: false, }; const disableGesturesScreenOptions = { gestureEnabled: false, }; const modalOverlayScreenOptions = { cardOverlayEnabled: true, presentation: 'transparentModal', }; const termsAndPrivacyModalScreenOptions = { gestureEnabled: false, cardOverlayEnabled: true, presentation: 'transparentModal', }; export type RootRouterNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; export type RootNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; const Root = createRootNavigator< ScreenParamList, RootParamList, RootNavigationHelpers, >(); function RootComponent(): React.Node { return ( + ); } export default RootComponent; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js index 0f0660bfa..00f4cb82e 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,240 +1,249 @@ // @flow import type { RouteProp } from '@react-navigation/native'; import type { ActionResultModalParams } from './action-result-modal.react.js'; import type { InviteLinkModalParams } from './invite-link-modal.react'; import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js'; import type { ExistingEthereumAccountParams } from '../account/registration/existing-ethereum-account.react.js'; import type { KeyserverSelectionParams } from '../account/registration/keyserver-selection.react.js'; import type { UsernameSelectionParams } from '../account/registration/username-selection.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 { MessageResultsScreenParams } from '../chat/message-results-screen.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 { TogglePinModalParams } from '../chat/toggle-pin-modal.react.js'; +import type { ViewInviteLinksScreenParams } from '../invite-links/view-invite-links-screen.react.js'; import type { ChatCameraModalParams } from '../media/chat-camera-modal.react.js'; import type { ImageModalParams } from '../media/image-modal.react.js'; import type { ThreadAvatarCameraModalParams } from '../media/thread-avatar-camera-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 { RelationshipListItemTooltipModalParams } from '../profile/relationship-list-item-tooltip-modal.react.js'; import type { MessageSearchParams } from '../search/message-search.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 ChatCameraModalRouteName = 'ChatCameraModal'; 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 InviteLinkModalRouteName = 'InviteLinkModal'; +export const InviteLinkNavigatorRouteName = 'InviteLinkNavigator'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const MessageListRouteName = 'MessageList'; export const MessageReactionsModalRouteName = 'MessageReactionsModal'; export const MessageResultsScreenRouteName = 'MessageResultsScreen'; 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 ThreadAvatarCameraModalRouteName = 'ThreadAvatarCameraModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const UserAvatarCameraModalRouteName = 'UserAvatarCameraModal'; export const TogglePinModalRouteName = 'TogglePinModal'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; +export const ViewInviteLinksRouteName = 'ViewInviteLinks'; export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export const RegistrationRouteName = 'Registration'; export const KeyserverSelectionRouteName = 'KeyserverSelection'; export const CoolOrNerdModeSelectionRouteName = 'CoolOrNerdModeSelection'; export const ConnectEthereumRouteName = 'ConnectEthereum'; export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount'; export const UsernameSelectionRouteName = 'UsernameSelection'; export const MessageSearchRouteName = 'MessageSearch'; 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, +Registration: void, +InviteLinkModal: InviteLinkModalParams, + +InviteLinkNavigator: void, }; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; export const PinnableMessageTooltipRouteNames = [ TextMessageTooltipModalRouteName, MultimediaMessageTooltipModalRouteName, ]; export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +RelationshipListItemTooltipModal: RelationshipListItemTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +CommunityDrawerNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +ChatCameraModal: ChatCameraModalParams, +UserAvatarCameraModal: void, +ThreadAvatarCameraModal: ThreadAvatarCameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, +TogglePinModal: TogglePinModalParams, ...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, +MessageResultsScreen: MessageResultsScreenParams, +MessageSearch: MessageSearchParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, +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 RegistrationParamList = { +CoolOrNerdModeSelection: void, +KeyserverSelection: KeyserverSelectionParams, +ConnectEthereum: ConnectEthereumParams, +ExistingEthereumAccount: ExistingEthereumAccountParams, +UsernameSelection: UsernameSelectionParams, }; +export type InviteLinkParamList = { + +ViewInviteLinks: ViewInviteLinksScreenParams, +}; + export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, ...CommunityDrawerParamList, ...RegistrationParamList, + ...InviteLinkParamList, }; export type NavigationRoute> = RouteProp; export const accountModals = [LoggedOutModalRouteName, RegistrationRouteName]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, FullScreenThreadMediaGalleryRouteName, MessageResultsScreenRouteName, MessageSearchRouteName, ]; diff --git a/native/themes/colors.js b/native/themes/colors.js index 7ece5777f..abd952929 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,371 +1,375 @@ // @flow import * as React from 'react'; import { StyleSheet } from 'react-native'; import { createSelector } from 'reselect'; import { selectBackgroundIsDark } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import type { AppState } from '../redux/state-types.js'; import type { GlobalTheme } from '../types/themes.js'; const designSystemColors = Object.freeze({ shadesWhite100: '#ffffff', shadesWhite90: '#f5f5f5', shadesWhite80: '#ebebeb', shadesWhite70: '#e0e0e0', shadesWhite60: '#cccccc', shadesBlack100: '#0a0a0a', shadesBlack90: '#1f1f1f', shadesBlack80: '#404040', shadesBlack70: '#666666', shadesBlack60: '#808080', violetDark100: '#7e57c2', violetDark80: '#6d49ab', violetDark60: '#563894', violetDark40: '#44297a', violetDark20: '#331f5c', violetLight100: '#ae94db', violetLight80: '#b9a4df', violetLight60: '#d3c6ec', violetLight40: '#e8e0f5', violetLight20: '#f3f0fa', successLight10: '#d5f6e3', successLight50: '#6cdf9c', successPrimary: '#00c853', successDark50: '#029841', successDark90: '#034920', errorLight10: '#feebe6', errorLight50: '#f9947b', errorPrimary: '#f53100', errorDark50: '#b62602', errorDark90: '#4f1203', spoilerColor: '#33332c', }); const light = Object.freeze({ blockQuoteBackground: designSystemColors.shadesWhite70, blockQuoteBorder: designSystemColors.shadesWhite60, codeBackground: designSystemColors.shadesWhite70, disabledButton: designSystemColors.shadesWhite70, disabledButtonText: designSystemColors.shadesBlack60, disconnectedBarBackground: designSystemColors.shadesWhite90, editButton: '#A4A4A2', floatingButtonBackground: '#999999', floatingButtonLabel: designSystemColors.shadesWhite80, headerChevron: designSystemColors.shadesBlack100, inlineEngagementBackground: designSystemColors.shadesWhite70, inlineEngagementLabel: designSystemColors.shadesBlack100, link: designSystemColors.violetDark100, listBackground: designSystemColors.shadesWhite100, listBackgroundLabel: designSystemColors.shadesBlack100, listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listChatBubble: '#F1F0F5', listForegroundLabel: designSystemColors.shadesBlack100, listForegroundSecondaryLabel: '#333333', listForegroundTertiaryLabel: designSystemColors.shadesBlack70, listInputBackground: designSystemColors.shadesWhite90, listInputBar: '#E2E2E2', listInputButton: '#8E8D92', listIosHighlightUnderlay: '#DDDDDDDD', listSearchBackground: designSystemColors.shadesWhite90, listSearchIcon: '#8E8D92', listSeparatorLabel: designSystemColors.shadesBlack70, modalBackground: designSystemColors.shadesWhite80, modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalButton: '#BBBBBB', modalButtonLabel: designSystemColors.shadesBlack100, modalContrastBackground: designSystemColors.shadesBlack100, modalContrastForegroundLabel: designSystemColors.shadesWhite100, modalContrastOpacity: 0.7, modalForeground: designSystemColors.shadesWhite100, modalForegroundBorder: designSystemColors.shadesWhite60, modalForegroundLabel: designSystemColors.shadesBlack100, modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: designSystemColors.shadesWhite60, modalSubtextLabel: designSystemColors.shadesBlack70, navigationCard: designSystemColors.shadesWhite100, navigationChevron: designSystemColors.shadesWhite60, panelBackground: designSystemColors.shadesWhite90, panelBackgroundLabel: '#888888', panelButton: designSystemColors.shadesWhite70, panelForeground: designSystemColors.shadesWhite100, panelForegroundBorder: designSystemColors.shadesWhite60, panelForegroundLabel: designSystemColors.shadesBlack100, panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', panelInputBackground: designSystemColors.shadesWhite60, panelInputSecondaryForeground: designSystemColors.shadesBlack60, panelIosHighlightUnderlay: '#EBEBEBDD', panelSecondaryForeground: designSystemColors.shadesWhite80, panelSecondaryForegroundBorder: designSystemColors.shadesWhite70, panelSeparator: designSystemColors.shadesWhite60, purpleLink: designSystemColors.violetDark100, purpleButton: designSystemColors.violetDark100, reactionSelectionPopoverItemBackground: designSystemColors.shadesBlack80, redText: designSystemColors.errorPrimary, spoiler: designSystemColors.spoilerColor, tabBarAccent: designSystemColors.violetDark100, tabBarBackground: designSystemColors.shadesWhite90, tabBarActiveTintColor: designSystemColors.violetDark100, vibrantGreenButton: designSystemColors.successPrimary, vibrantRedButton: designSystemColors.errorPrimary, whiteText: designSystemColors.shadesWhite100, tooltipBackground: designSystemColors.shadesWhite70, logInSpacer: '#FFFFFF33', siweButton: designSystemColors.shadesWhite100, siweButtonText: designSystemColors.shadesBlack90, drawerExpandButton: designSystemColors.shadesBlack60, drawerExpandButtonDisabled: designSystemColors.shadesWhite60, drawerItemLabelLevel0: designSystemColors.shadesBlack100, drawerItemLabelLevel1: designSystemColors.shadesBlack100, drawerItemLabelLevel2: designSystemColors.shadesBlack90, drawerOpenCommunityBackground: designSystemColors.shadesWhite90, drawerBackground: designSystemColors.shadesWhite100, subthreadsModalClose: designSystemColors.shadesBlack60, subthreadsModalBackground: designSystemColors.shadesWhite80, subthreadsModalSearch: '#00000008', messageLabel: designSystemColors.shadesBlack100, modalSeparator: designSystemColors.shadesWhite60, secondaryButtonBorder: designSystemColors.shadesWhite100, + inviteLinkLinkColor: designSystemColors.shadesBlack100, + inviteLinkButtonBackground: designSystemColors.shadesWhite60, }); export type Colors = $Exact; const dark: Colors = Object.freeze({ blockQuoteBackground: '#A9A9A9', blockQuoteBorder: designSystemColors.shadesBlack60, codeBackground: designSystemColors.shadesBlack100, disabledButton: designSystemColors.shadesBlack80, disabledButtonText: designSystemColors.shadesBlack60, disconnectedBarBackground: designSystemColors.shadesBlack90, editButton: designSystemColors.shadesBlack70, floatingButtonBackground: designSystemColors.shadesBlack70, floatingButtonLabel: designSystemColors.shadesWhite100, headerChevron: designSystemColors.shadesWhite100, inlineEngagementBackground: designSystemColors.shadesBlack70, inlineEngagementLabel: designSystemColors.shadesWhite100, link: designSystemColors.violetLight100, listBackground: designSystemColors.shadesBlack100, listBackgroundLabel: designSystemColors.shadesWhite60, listBackgroundSecondaryLabel: '#BBBBBB', listBackgroundTernaryLabel: designSystemColors.shadesBlack60, listChatBubble: '#26252A', listForegroundLabel: designSystemColors.shadesWhite100, listForegroundSecondaryLabel: designSystemColors.shadesWhite60, listForegroundTertiaryLabel: designSystemColors.shadesBlack60, listInputBackground: designSystemColors.shadesBlack90, listInputBar: designSystemColors.shadesBlack70, listInputButton: designSystemColors.shadesWhite60, listIosHighlightUnderlay: '#BBBBBB88', listSearchBackground: designSystemColors.shadesBlack90, listSearchIcon: designSystemColors.shadesWhite60, listSeparatorLabel: designSystemColors.shadesWhite80, modalBackground: designSystemColors.shadesBlack100, modalBackgroundLabel: designSystemColors.shadesWhite60, modalBackgroundSecondaryLabel: designSystemColors.shadesBlack70, modalButton: designSystemColors.shadesBlack70, modalButtonLabel: designSystemColors.shadesWhite100, modalContrastBackground: designSystemColors.shadesWhite100, modalContrastForegroundLabel: designSystemColors.shadesBlack100, modalContrastOpacity: 0.85, modalForeground: designSystemColors.shadesBlack90, modalForegroundBorder: designSystemColors.shadesBlack90, modalForegroundLabel: designSystemColors.shadesWhite100, modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: designSystemColors.shadesBlack70, modalIosHighlightUnderlay: '#AAAAAA88', modalSubtext: designSystemColors.shadesBlack80, modalSubtextLabel: '#AAAAAA', navigationCard: '#2A2A2A', navigationChevron: designSystemColors.shadesBlack70, panelBackground: designSystemColors.shadesBlack100, panelBackgroundLabel: designSystemColors.shadesWhite60, panelButton: designSystemColors.shadesBlack70, panelForeground: designSystemColors.shadesBlack90, panelForegroundBorder: '#2C2C2E', panelForegroundLabel: designSystemColors.shadesWhite100, panelForegroundSecondaryLabel: designSystemColors.shadesWhite60, panelForegroundTertiaryLabel: '#AAAAAA', panelInputBackground: designSystemColors.shadesBlack80, panelInputSecondaryForeground: designSystemColors.shadesBlack60, panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: designSystemColors.shadesBlack80, panelSecondaryForegroundBorder: designSystemColors.shadesBlack70, panelSeparator: designSystemColors.shadesBlack80, purpleLink: designSystemColors.violetLight100, purpleButton: designSystemColors.violetDark100, reactionSelectionPopoverItemBackground: designSystemColors.shadesBlack80, redText: designSystemColors.errorPrimary, spoiler: designSystemColors.spoilerColor, tabBarAccent: designSystemColors.violetLight100, tabBarBackground: designSystemColors.shadesBlack100, tabBarActiveTintColor: designSystemColors.violetLight100, vibrantGreenButton: designSystemColors.successPrimary, vibrantRedButton: designSystemColors.errorPrimary, whiteText: designSystemColors.shadesWhite100, tooltipBackground: designSystemColors.shadesBlack90, logInSpacer: '#FFFFFF33', siweButton: designSystemColors.shadesWhite100, siweButtonText: designSystemColors.shadesBlack90, drawerExpandButton: designSystemColors.shadesBlack60, drawerExpandButtonDisabled: designSystemColors.shadesBlack80, drawerItemLabelLevel0: designSystemColors.shadesWhite60, drawerItemLabelLevel1: designSystemColors.shadesWhite60, drawerItemLabelLevel2: designSystemColors.shadesWhite90, drawerOpenCommunityBackground: '#191919', drawerBackground: designSystemColors.shadesBlack90, subthreadsModalClose: designSystemColors.shadesBlack60, subthreadsModalBackground: designSystemColors.shadesBlack90, subthreadsModalSearch: '#FFFFFF04', typeaheadTooltipBackground: '#1F1F1f', typeaheadTooltipBorder: designSystemColors.shadesBlack80, typeaheadTooltipText: 'white', messageLabel: designSystemColors.shadesWhite60, modalSeparator: designSystemColors.shadesBlack80, secondaryButtonBorder: designSystemColors.shadesWhite100, + inviteLinkLinkColor: designSystemColors.shadesWhite80, + inviteLinkButtonBackground: designSystemColors.shadesBlack80, }); const colors = { light, dark }; const colorsSelector: (state: AppState) => Colors = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { const explicitTheme = theme ? theme : 'light'; return colors[explicitTheme]; }, ); const magicStrings = new Set(); for (const theme in colors) { for (const magicString in colors[theme]) { magicStrings.add(magicString); } } type Styles = { [name: string]: { [field: string]: mixed } }; type ReplaceField = (input: any) => any; export type StyleSheetOf = $ObjMap; function stylesFromColors( obj: IS, themeColors: Colors, ): StyleSheetOf { const result = {}; for (const key in obj) { const style = obj[key]; const filledInStyle = { ...style }; for (const styleKey in style) { const styleValue = style[styleKey]; if (typeof styleValue !== 'string') { continue; } if (magicStrings.has(styleValue)) { const mapped = themeColors[styleValue]; if (mapped) { filledInStyle[styleKey] = mapped; } } } result[key] = filledInStyle; } return StyleSheet.create(result); } function styleSelector( obj: IS, ): (state: AppState) => StyleSheetOf { return createSelector(colorsSelector, (themeColors: Colors) => stylesFromColors(obj, themeColors), ); } function useStyles(obj: IS): StyleSheetOf { const ourColors = useColors(); return React.useMemo( () => stylesFromColors(obj, ourColors), [obj, ourColors], ); } function useOverlayStyles(obj: IS): StyleSheetOf { const navContext = React.useContext(NavContext); const navigationState = navContext && navContext.state; const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); const backgroundIsDark = React.useMemo( () => selectBackgroundIsDark(navigationState, theme), [navigationState, theme], ); const syntheticTheme = backgroundIsDark ? 'dark' : 'light'; return React.useMemo( () => stylesFromColors(obj, colors[syntheticTheme]), [obj, syntheticTheme], ); } function useColors(): Colors { return useSelector(colorsSelector); } function getStylesForTheme( obj: IS, theme: GlobalTheme, ): StyleSheetOf { return stylesFromColors(obj, colors[theme]); } export type IndicatorStyle = 'white' | 'black'; function useIndicatorStyle(): IndicatorStyle { const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); return theme && theme === 'dark' ? 'white' : 'black'; } const indicatorStyleSelector: (state: AppState) => IndicatorStyle = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'white' : 'black'; }, ); export type KeyboardAppearance = 'default' | 'light' | 'dark'; const keyboardAppearanceSelector: (state: AppState) => KeyboardAppearance = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'dark' : 'light'; }, ); function useKeyboardAppearance(): KeyboardAppearance { return useSelector(keyboardAppearanceSelector); } export { colors, colorsSelector, styleSelector, useStyles, useOverlayStyles, useColors, getStylesForTheme, useIndicatorStyle, indicatorStyleSelector, useKeyboardAppearance, };