From 7e31645e9a355f2a0b3c8d62430a53dbb4714674 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Dec 2022 14:06:01 -0600 Subject: Add a design system (#34) * Add theming system * Add standard Button control and update RadioButtons * Unify radiobutton with design system * Update debug screen to have multiple views * Add ToggleButton * Update error controls to use design system * Add typography to element * Move DropdownButton into the design system * Clean out old code * Move Text into design system * Add 'inverted' color palette * Move LoadingPlaceholder into the design system --- src/App.native.tsx | 9 +- src/lib/functions.ts | 6 + src/view/com/composer/Autocomplete.tsx | 4 +- src/view/com/composer/ComposePost.tsx | 2 +- src/view/com/composer/Prompt.tsx | 2 +- src/view/com/discover/SuggestedFollows.tsx | 4 +- src/view/com/lightbox/Lightbox.tsx | 2 +- src/view/com/login/CreateAccount.tsx | 2 +- src/view/com/login/Signin.tsx | 2 +- src/view/com/modals/Confirm.tsx | 4 +- src/view/com/modals/CreateScene.tsx | 4 +- src/view/com/modals/EditProfile.tsx | 4 +- src/view/com/modals/InviteToScene.tsx | 4 +- src/view/com/modals/ReportAccount.tsx | 4 +- src/view/com/modals/ReportPost.tsx | 4 +- src/view/com/modals/ServerInput.tsx | 2 +- src/view/com/notifications/Feed.tsx | 5 +- src/view/com/notifications/FeedItem.tsx | 4 +- src/view/com/notifications/InviteAccepter.tsx | 2 +- src/view/com/onboard/FeatureExplainer.tsx | 2 +- src/view/com/onboard/Follows.tsx | 2 +- src/view/com/post-thread/PostRepostedBy.tsx | 5 +- src/view/com/post-thread/PostThread.tsx | 3 +- src/view/com/post-thread/PostThreadItem.tsx | 6 +- src/view/com/post-thread/PostVotedBy.tsx | 5 +- src/view/com/post/Post.tsx | 4 +- src/view/com/post/PostText.tsx | 4 +- src/view/com/posts/Feed.tsx | 5 +- src/view/com/posts/FeedItem.tsx | 4 +- src/view/com/profile/ProfileCard.tsx | 2 +- src/view/com/profile/ProfileFollowers.tsx | 5 +- src/view/com/profile/ProfileFollows.tsx | 5 +- src/view/com/profile/ProfileHeader.tsx | 10 +- src/view/com/profile/ProfileMembers.tsx | 3 +- src/view/com/util/DropdownBtn.tsx | 223 ------------- src/view/com/util/EmptyState.tsx | 18 +- src/view/com/util/ErrorMessage.tsx | 102 ------ src/view/com/util/ErrorScreen.tsx | 112 ------- src/view/com/util/FloatingActionButton.tsx | 57 ---- src/view/com/util/Link.tsx | 9 +- src/view/com/util/LoadingPlaceholder.tsx | 25 +- src/view/com/util/Picker.tsx | 4 +- src/view/com/util/PostCtrls.tsx | 4 +- src/view/com/util/PostEmbeds.tsx | 2 +- src/view/com/util/PostMeta.tsx | 4 +- src/view/com/util/RichText.tsx | 108 ------- src/view/com/util/Selector.tsx | 2 +- src/view/com/util/Text.tsx | 15 - src/view/com/util/UserInfoText.tsx | 2 +- src/view/com/util/ViewHeader.tsx | 2 +- src/view/com/util/ViewSelector.tsx | 4 +- src/view/com/util/error/ErrorMessage.tsx | 76 +++++ src/view/com/util/error/ErrorScreen.tsx | 113 +++++++ src/view/com/util/forms/Button.tsx | 120 +++++++ src/view/com/util/forms/DropdownButton.tsx | 238 ++++++++++++++ src/view/com/util/forms/RadioButton.tsx | 135 ++++++-- src/view/com/util/forms/RadioGroup.tsx | 11 +- src/view/com/util/forms/ToggleButton.tsx | 165 ++++++++++ src/view/com/util/images/AutoSizedImage.tsx | 2 +- src/view/com/util/text/RichText.tsx | 115 +++++++ src/view/com/util/text/Text.tsx | 23 ++ src/view/lib/ThemeContext.tsx | 70 +++++ src/view/lib/hooks/useAnimatedValue.ts | 12 + src/view/lib/hooks/useOnMainScroll.ts | 25 ++ src/view/lib/hooks/usePalette.ts | 41 +++ src/view/lib/themes.ts | 163 ++++++++++ src/view/lib/useAnimatedValue.ts | 12 - src/view/lib/useOnMainScroll.ts | 25 -- src/view/lib/z-index.ts | 2 - src/view/routes.ts | 2 + src/view/screens/Contacts.tsx | 4 +- src/view/screens/Debug.tsx | 432 ++++++++++++++++++++++++++ src/view/screens/Home.tsx | 4 +- src/view/screens/Login.tsx | 2 +- src/view/screens/NotFound.tsx | 2 +- src/view/screens/Notifications.tsx | 2 +- src/view/screens/Profile.tsx | 9 +- src/view/screens/Search.tsx | 2 +- src/view/screens/Settings.tsx | 5 +- src/view/shell/mobile/Composer.tsx | 2 +- src/view/shell/mobile/Menu.tsx | 2 +- src/view/shell/mobile/TabsSelector.tsx | 4 +- src/view/shell/mobile/index.tsx | 4 +- 83 files changed, 1854 insertions(+), 798 deletions(-) create mode 100644 src/lib/functions.ts delete mode 100644 src/view/com/util/DropdownBtn.tsx delete mode 100644 src/view/com/util/ErrorMessage.tsx delete mode 100644 src/view/com/util/ErrorScreen.tsx delete mode 100644 src/view/com/util/FloatingActionButton.tsx delete mode 100644 src/view/com/util/RichText.tsx delete mode 100644 src/view/com/util/Text.tsx create mode 100644 src/view/com/util/error/ErrorMessage.tsx create mode 100644 src/view/com/util/error/ErrorScreen.tsx create mode 100644 src/view/com/util/forms/Button.tsx create mode 100644 src/view/com/util/forms/DropdownButton.tsx create mode 100644 src/view/com/util/forms/ToggleButton.tsx create mode 100644 src/view/com/util/text/RichText.tsx create mode 100644 src/view/com/util/text/Text.tsx create mode 100644 src/view/lib/ThemeContext.tsx create mode 100644 src/view/lib/hooks/useAnimatedValue.ts create mode 100644 src/view/lib/hooks/useOnMainScroll.ts create mode 100644 src/view/lib/hooks/usePalette.ts create mode 100644 src/view/lib/themes.ts delete mode 100644 src/view/lib/useAnimatedValue.ts delete mode 100644 src/view/lib/useOnMainScroll.ts delete mode 100644 src/view/lib/z-index.ts create mode 100644 src/view/screens/Debug.tsx (limited to 'src') diff --git a/src/App.native.tsx b/src/App.native.tsx index a532a08d4..fa523cd81 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -5,6 +5,7 @@ import {RootSiblingParent} from 'react-native-root-siblings' import {GestureHandlerRootView} from 'react-native-gesture-handler' import SplashScreen from 'react-native-splash-screen' import {SafeAreaProvider} from 'react-native-safe-area-context' +import {ThemeProvider} from './view/lib/ThemeContext' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import {MobileShell} from './view/shell/mobile' @@ -40,9 +41,11 @@ function App() { - - - + + + + + diff --git a/src/lib/functions.ts b/src/lib/functions.ts new file mode 100644 index 000000000..d6fbf5b92 --- /dev/null +++ b/src/lib/functions.ts @@ -0,0 +1,6 @@ +export function choose>( + value: keyof T, + choices: T, +): U { + return choices[value] +} diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx index 1637108f8..b151e0d9e 100644 --- a/src/view/com/composer/Autocomplete.tsx +++ b/src/view/com/composer/Autocomplete.tsx @@ -5,8 +5,8 @@ import { StyleSheet, useWindowDimensions, } from 'react-native' -import {useAnimatedValue} from '../../lib/useAnimatedValue' -import {Text} from '../util/Text' +import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +import {Text} from '../util/text/Text' import {colors} from '../../lib/styles' interface AutocompleteItem { diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index baa931105..fe310f19b 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -15,7 +15,7 @@ import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' import {Autocomplete} from './Autocomplete' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import * as Toast from '../util/Toast' // @ts-ignore no type definition -prf import ProgressCircle from 'react-native-progress/Circle' diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 2b1559df4..ec63d9501 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {colors} from '../../lib/styles' import {useStores} from '../../../state' import {UserAvatar} from '../util/UserAvatar' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' export function ComposePrompt({ noAvi = false, diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index b78bae889..77bd94d5a 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -10,9 +10,9 @@ import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {observer} from 'mobx-react-lite' import _omit from 'lodash.omit' -import {ErrorScreen} from '../util/ErrorScreen' +import {ErrorScreen} from '../util/error/ErrorScreen' import {Link} from '../util/Link' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import * as Toast from '../util/Toast' import {useStores} from '../../../state' diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 36c51764f..849354aea 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -10,7 +10,7 @@ import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {SwipeAndZoom, Dir} from '../util/gestures/SwipeAndZoom' import {useStores} from '../../../state' -import {useAnimatedValue} from '../../lib/useAnimatedValue' +import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' import * as models from '../../../state/models/shell-ui' diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx index f97eb7a0f..689a4f384 100644 --- a/src/view/com/login/CreateAccount.tsx +++ b/src/view/com/login/CreateAccount.tsx @@ -15,7 +15,7 @@ import * as EmailValidator from 'email-validator' import {Logo} from './Logo' import {Picker} from '../util/Picker' import {TextLink} from '../util/Link' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import {s, colors} from '../../lib/styles' import { makeValidHandle, diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx index 45728d3b3..f76507d71 100644 --- a/src/view/com/login/Signin.tsx +++ b/src/view/com/login/Signin.tsx @@ -12,7 +12,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as EmailValidator from 'email-validator' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {Logo} from './Logo' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import {s, colors} from '../../lib/styles' import {createFullHandle, toNiceDomain} from '../../../lib/strings' import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state' diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index a18043f1a..7545e36a6 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -6,10 +6,10 @@ import { View, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import {useStores} from '../../../state' import {s, colors, gradients} from '../../lib/styles' -import {ErrorMessage} from '../util/ErrorMessage' +import {ErrorMessage} from '../util/error/ErrorMessage' export const snapPoints = ['50%'] diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx index 0d47aa4bd..60c240546 100644 --- a/src/view/com/modals/CreateScene.tsx +++ b/src/view/com/modals/CreateScene.tsx @@ -9,8 +9,8 @@ import { import LinearGradient from 'react-native-linear-gradient' import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' import {AppBskyActorCreateScene} from '@atproto/api' -import {ErrorMessage} from '../util/ErrorMessage' -import {Text} from '../util/Text' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {Text} from '../util/text/Text' import {useStores} from '../../../state' import {s, colors, gradients} from '../../lib/styles' import { diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index bd97ced53..8a3f016ad 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -9,8 +9,8 @@ import { import LinearGradient from 'react-native-linear-gradient' import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' import {Image as PickedImage} from 'react-native-image-crop-picker' -import {Text} from '../util/Text' -import {ErrorMessage} from '../util/ErrorMessage' +import {Text} from '../util/text/Text' +import {ErrorMessage} from '../util/error/ErrorMessage' import {useStores} from '../../../state' import {ProfileViewModel} from '../../../state/models/profile-view' import {s, colors, gradients} from '../../lib/styles' diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx index 28380b6a8..a73440179 100644 --- a/src/view/com/modals/InviteToScene.tsx +++ b/src/view/com/modals/InviteToScene.tsx @@ -19,8 +19,8 @@ import { import _omit from 'lodash.omit' import {AtUri} from '../../../third-party/uri' import {ProfileCard} from '../profile/ProfileCard' -import {ErrorMessage} from '../util/ErrorMessage' -import {Text} from '../util/Text' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {Text} from '../util/text/Text' import {useStores} from '../../../state' import * as apilib from '../../../state/lib/api' import {ProfileViewModel} from '../../../state/models/profile-view' diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx index 582e24238..bf4d5f5a0 100644 --- a/src/view/com/modals/ReportAccount.tsx +++ b/src/view/com/modals/ReportAccount.tsx @@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient' import {useStores} from '../../../state' import {s, colors, gradients} from '../../lib/styles' import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' -import {Text} from '../util/Text' -import {ErrorMessage} from '../util/ErrorMessage' +import {Text} from '../util/text/Text' +import {ErrorMessage} from '../util/error/ErrorMessage' const ITEMS: RadioGroupItem[] = [ {key: 'spam', label: 'Spam or excessive repeat posts'}, diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 6f134032a..d4684069a 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient' import {useStores} from '../../../state' import {s, colors, gradients} from '../../lib/styles' import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' -import {Text} from '../util/Text' -import {ErrorMessage} from '../util/ErrorMessage' +import {Text} from '../util/text/Text' +import {ErrorMessage} from '../util/error/ErrorMessage' const ITEMS: RadioGroupItem[] = [ {key: 'spam', label: 'Spam or excessive repeat posts'}, diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index 0d1e0e911..884fb91e6 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react' import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' -import {Text} from '../util/Text' +import {Text} from '../util/text/Text' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' import { diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index c986bca57..91a01db4d 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -4,9 +4,9 @@ import {View, FlatList} from 'react-native' import {NotificationsViewModel} from '../../../state/models/notifications-view' import {FeedItem} from './FeedItem' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' -import {ErrorMessage} from '../util/ErrorMessage' +import {ErrorMessage} from '../util/error/ErrorMessage' import {EmptyState} from '../util/EmptyState' -import {OnScrollCb} from '../../lib/useOnMainScroll' +import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -54,7 +54,6 @@ export const Feed = observer(function Feed({ {view.isLoading && !data && } {view.hasError && ( } {feed.hasError && ( )} {dropdownItems?.length ? ( - - + ) : undefined} diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx index 251ece41a..0e34865b9 100644 --- a/src/view/com/profile/ProfileMembers.tsx +++ b/src/view/com/profile/ProfileMembers.tsx @@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, View} from 'react-native' import {MembersViewModel, MemberItem} from '../../../state/models/members-view' import {ProfileCard} from './ProfileCard' -import {ErrorMessage} from '../util/ErrorMessage' +import {ErrorMessage} from '../util/error/ErrorMessage' import {useStores} from '../../../state' export const ProfileMembers = observer(function ProfileMembers({ @@ -49,7 +49,6 @@ export const ProfileMembers = observer(function ProfileMembers({ return ( void -} - -export function DropdownBtn({ - style, - items, - menuWidth, - children, -}: { - style?: StyleProp - items: DropdownItem[] - menuWidth?: number - children?: React.ReactNode -}) { - const ref = useRef(null) - - const onPress = () => { - ref.current?.measure( - ( - _x: number, - _y: number, - width: number, - height: number, - pageX: number, - pageY: number, - ) => { - if (!menuWidth) { - menuWidth = 200 - } - createDropdownMenu( - pageX + width - menuWidth, - pageY + height, - menuWidth, - items, - ) - }, - ) - } - - return ( - - {children} - - ) -} - -export function PostDropdownBtn({ - style, - children, - itemHref, - itemTitle, - isAuthor, - onCopyPostText, - onDeletePost, -}: { - style?: StyleProp - children?: React.ReactNode - itemHref: string - itemTitle: string - isAuthor: boolean - onCopyPostText: () => void - onDeletePost: () => void -}) { - const store = useStores() - - const dropdownItems: DropdownItem[] = [ - TABS_ENABLED - ? { - icon: ['far', 'clone'], - label: 'Open in new tab', - onPress() { - store.nav.newTab(itemHref) - }, - } - : undefined, - { - icon: ['far', 'paste'], - label: 'Copy post text', - onPress() { - onCopyPostText() - }, - }, - { - icon: 'share', - label: 'Share...', - onPress() { - Share.share({url: toShareUrl(itemHref)}) - }, - }, - { - icon: 'circle-exclamation', - label: 'Report post', - onPress() { - store.shell.openModal(new ReportPostModal(itemHref)) - }, - }, - isAuthor - ? { - icon: ['far', 'trash-can'], - label: 'Delete post', - onPress() { - store.shell.openModal( - new ConfirmModal( - 'Delete this post?', - 'Are you sure? This can not be undone.', - onDeletePost, - ), - ) - }, - } - : undefined, - ].filter(Boolean) as DropdownItem[] - - return ( - - {children} - - ) -} - -function createDropdownMenu( - x: number, - y: number, - width: number, - items: DropdownItem[], -): RootSiblings { - const onPressItem = (index: number) => { - sibling.destroy() - items[index].onPress() - } - const onOuterPress = () => sibling.destroy() - const sibling = new RootSiblings( - ( - <> - - - - - {items.map((item, index) => ( - onPressItem(index)}> - {item.icon && ( - - )} - {item.label} - - ))} - - - ), - ) - return sibling -} - -const styles = StyleSheet.create({ - bg: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - backgroundColor: '#000', - opacity: 0.1, - }, - menu: { - position: 'absolute', - backgroundColor: '#fff', - borderRadius: 14, - opacity: 1, - paddingVertical: 6, - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingLeft: 15, - paddingRight: 40, - }, - menuItemBorder: { - borderTopWidth: 1, - borderTopColor: colors.gray1, - marginTop: 4, - paddingTop: 12, - }, - icon: { - marginLeft: 6, - marginRight: 8, - }, - label: { - fontSize: 18, - }, -}) diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index 8d98807e3..d9a317fae 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -2,9 +2,9 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from './Text' +import {Text} from './text/Text' import {UserGroupIcon} from '../../lib/icons' -import {colors} from '../../lib/styles' +import {usePalette} from '../../lib/hooks/usePalette' export function EmptyState({ icon, @@ -15,16 +15,23 @@ export function EmptyState({ message: string style?: StyleProp }) { + const pal = usePalette('default') return ( {icon === 'user-group' ? ( ) : ( - + )} - {message} + + {message} + ) } @@ -40,12 +47,9 @@ const styles = StyleSheet.create({ icon: { marginLeft: 'auto', marginRight: 'auto', - color: colors.gray3, }, text: { textAlign: 'center', - color: colors.gray5, paddingTop: 16, - fontSize: 16, }, }) diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx deleted file mode 100644 index b87b77baa..000000000 --- a/src/view/com/util/ErrorMessage.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react' -import { - StyleSheet, - TouchableOpacity, - StyleProp, - View, - ViewStyle, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import LinearGradient from 'react-native-linear-gradient' -import {Text} from './Text' -import {colors, gradients} from '../../lib/styles' - -export function ErrorMessage({ - message, - numberOfLines, - dark, - style, - onPressTryAgain, -}: { - message: string - numberOfLines?: number - dark?: boolean - style?: StyleProp - onPressTryAgain?: () => void -}) { - const inner = ( - <> - - - - - {message} - - {onPressTryAgain && ( - - - - )} - - ) - if (dark) { - return ( - - {inner} - - ) - } - return {inner} -} - -const styles = StyleSheet.create({ - outer: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.red1, - borderWidth: 1, - borderColor: colors.red3, - borderRadius: 6, - paddingVertical: 8, - paddingHorizontal: 8, - }, - errorIcon: { - backgroundColor: colors.red4, - borderRadius: 12, - width: 24, - height: 24, - alignItems: 'center', - justifyContent: 'center', - marginRight: 8, - }, - darkErrorIcon: { - backgroundColor: colors.white, - }, - message: { - flex: 1, - color: colors.red4, - paddingRight: 10, - }, - darkMessage: { - color: colors.white, - fontWeight: '600', - }, - btn: { - paddingHorizontal: 4, - paddingVertical: 4, - }, -}) diff --git a/src/view/com/util/ErrorScreen.tsx b/src/view/com/util/ErrorScreen.tsx deleted file mode 100644 index d0e1e2755..000000000 --- a/src/view/com/util/ErrorScreen.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from './Text' -import {colors} from '../../lib/styles' - -export function ErrorScreen({ - title, - message, - details, - onPressTryAgain, -}: { - title: string - message: string - details?: string - onPressTryAgain?: () => void -}) { - return ( - - - - - - - {title} - {message} - {details && {details}} - {onPressTryAgain && ( - - - - Try again - - - )} - - ) -} - -const styles = StyleSheet.create({ - outer: { - flex: 1, - backgroundColor: colors.red1, - borderWidth: 1, - borderColor: colors.red3, - borderRadius: 6, - paddingVertical: 30, - paddingHorizontal: 14, - margin: 10, - }, - title: { - textAlign: 'center', - color: colors.red4, - fontSize: 24, - marginBottom: 10, - }, - message: { - textAlign: 'center', - color: colors.red4, - marginBottom: 20, - }, - details: { - textAlign: 'center', - color: colors.black, - backgroundColor: colors.white, - borderWidth: 1, - borderColor: colors.gray5, - borderRadius: 6, - paddingVertical: 10, - paddingHorizontal: 14, - overflow: 'hidden', - marginBottom: 20, - }, - btnContainer: { - alignItems: 'center', - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.red4, - borderRadius: 6, - paddingHorizontal: 16, - paddingVertical: 10, - }, - btnText: { - marginLeft: 5, - color: colors.white, - fontSize: 16, - fontWeight: 'bold', - }, - errorIconContainer: { - alignItems: 'center', - marginBottom: 10, - }, - errorIcon: { - backgroundColor: colors.red4, - borderRadius: 30, - width: 50, - height: 50, - alignItems: 'center', - justifyContent: 'center', - marginRight: 5, - }, -}) diff --git a/src/view/com/util/FloatingActionButton.tsx b/src/view/com/util/FloatingActionButton.tsx deleted file mode 100644 index 21c4fba6c..000000000 --- a/src/view/com/util/FloatingActionButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import { - GestureResponderEvent, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {colors, gradients} from '../../lib/styles' -import * as zIndex from '../../lib/z-index' - -type OnPress = ((event: GestureResponderEvent) => void) | undefined -export function FAB({icon, onPress}: {icon: IconProp; onPress: OnPress}) { - return ( - - - - - - - - ) -} - -const styles = StyleSheet.create({ - outer: { - position: 'absolute', - zIndex: zIndex.FAB, - right: 22, - bottom: 14, - width: 60, - height: 60, - borderRadius: 30, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowOffset: {width: 0, height: 1}, - }, - inner: { - width: 60, - height: 60, - borderRadius: 30, - justifyContent: 'center', - alignItems: 'center', - }, - icon: {}, -}) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 2bb553575..05573d999 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -9,7 +9,8 @@ import { View, ViewStyle, } from 'react-native' -import {Text} from './Text' +import {Text} from './text/Text' +import {TypographyVariant} from '../../lib/ThemeContext' import {useStores, RootStoreModel} from '../../../state' import {convertBskyAppUrlIfNeeded} from '../../../lib/strings' @@ -57,14 +58,14 @@ export const Link = observer(function Link({ }) export const TextLink = observer(function Link({ + type = 'body1', style, href, - title, text, }: { + type: TypographyVariant style?: StyleProp href: string - title?: string text: string }) { const store = useStores() @@ -75,7 +76,7 @@ export const TextLink = observer(function Link({ handleLink(store, href, true) } return ( - + {text} ) diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 9c2d0398f..15488167f 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -3,6 +3,7 @@ import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UpIcon} from '../../lib/icons' import {s, colors} from '../../lib/styles' +import {useTheme} from '../../lib/ThemeContext' export function LoadingPlaceholder({ width, @@ -13,13 +14,14 @@ export function LoadingPlaceholder({ height: string | number style?: StyleProp }) { + const theme = useTheme() return ( @@ -41,6 +43,7 @@ export function PostLoadingPlaceholder({ }: { style?: StyleProp }) { + const theme = useTheme() return ( @@ -52,16 +55,24 @@ export function PostLoadingPlaceholder({ - + - + @@ -125,8 +136,6 @@ export function NotificationFeedLoadingPlaceholder() { const styles = StyleSheet.create({ post: { flexDirection: 'row', - backgroundColor: colors.white, - borderRadius: 6, padding: 10, margin: 1, }, @@ -135,8 +144,6 @@ const styles = StyleSheet.create({ marginRight: 10, }, notification: { - backgroundColor: colors.white, - borderRadius: 6, padding: 10, paddingLeft: 46, margin: 1, diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx index 84a627b6d..208ec0492 100644 --- a/src/view/com/util/Picker.tsx +++ b/src/view/com/util/Picker.tsx @@ -1,3 +1,5 @@ +// TODO: replaceme with something in the design system + import React, {useRef} from 'react' import { StyleProp, @@ -13,7 +15,7 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import RootSiblings from 'react-native-root-siblings' -import {Text} from './Text' +import {Text} from './text/Text' import {colors} from '../../lib/styles' interface PickerItem { diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 264210768..ac10e92fe 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -2,10 +2,10 @@ import React from 'react' import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import ReactNativeHapticFeedback from 'react-native-haptic-feedback' -import {Text} from './Text' +import {Text} from './text/Text' import {UpIcon, UpIconSolid} from '../../lib/icons' import {s, colors} from '../../lib/styles' -import {useAnimatedValue} from '../../lib/useAnimatedValue' +import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' interface PostCtrlsOpts { big?: boolean diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx index 1c980465a..839110a21 100644 --- a/src/view/com/util/PostEmbeds.tsx +++ b/src/view/com/util/PostEmbeds.tsx @@ -2,7 +2,7 @@ import React from 'react' import {ImageStyle, StyleSheet, StyleProp, View, ViewStyle} from 'react-native' import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api' import {Link} from '../util/Link' -import {Text} from '../util/Text' +import {Text} from './text/Text' import {colors} from '../../lib/styles' import {AutoSizedImage} from './images/AutoSizedImage' import {ImagesLightbox} from '../../../state/models/shell-ui' diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 77dfbb485..fae3a4c83 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -2,8 +2,8 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Link} from '../util/Link' -import {Text} from '../util/Text' -import {PostDropdownBtn} from '../util/DropdownBtn' +import {Text} from './text/Text' +import {PostDropdownBtn} from './forms/DropdownButton' import {s} from '../../lib/styles' import {ago} from '../../../lib/strings' diff --git a/src/view/com/util/RichText.tsx b/src/view/com/util/RichText.tsx deleted file mode 100644 index d6f193f9d..000000000 --- a/src/view/com/util/RichText.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react' -import {TextStyle, StyleProp} from 'react-native' -import {TextLink} from './Link' -import {Text} from './Text' -import {s} from '../../lib/styles' -import {toShortUrl} from '../../../lib/strings' - -type TextSlice = {start: number; end: number} -type Entity = { - index: TextSlice - type: string - value: string -} - -export function RichText({ - text, - entities, - style, - numberOfLines, -}: { - text: string - entities?: Entity[] - style?: StyleProp - numberOfLines?: number -}) { - if (!entities?.length) { - if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { - style = { - fontSize: 26, - lineHeight: 30, - } - return {text} - } - return {text} - } - if (!style) style = [] - else if (!Array.isArray(style)) style = [style] - entities.sort(sortByIndex) - const segments = Array.from(toSegments(text, entities)) - const els = [] - let key = 0 - for (const segment of segments) { - if (typeof segment === 'string') { - els.push(segment) - } else { - if (segment.entity.type === 'mention') { - els.push( - , - ) - } else if (segment.entity.type === 'link') { - els.push( - , - ) - } - } - key++ - } - return ( - - {els} - - ) -} - -function sortByIndex(a: Entity, b: Entity) { - return a.index.start - b.index.start -} - -function* toSegments(text: string, entities: Entity[]) { - let cursor = 0 - let i = 0 - do { - let currEnt = entities[i] - if (cursor < currEnt.index.start) { - yield text.slice(cursor, currEnt.index.start) - } else if (cursor > currEnt.index.start) { - i++ - continue - } - if (currEnt.index.start < currEnt.index.end) { - let subtext = text.slice(currEnt.index.start, currEnt.index.end) - if (!subtext.trim()) { - // dont yield links to empty strings - yield subtext - } else { - yield { - entity: currEnt, - text: subtext, - } - } - } - cursor = currEnt.index.end - i++ - } while (i < entities.length) - if (cursor < text.length) { - yield text.slice(cursor, text.length) - } -} diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx index 954360b32..211c5b902 100644 --- a/src/view/com/util/Selector.tsx +++ b/src/view/com/util/Selector.tsx @@ -5,7 +5,7 @@ import { TouchableWithoutFeedback, View, } from 'react-native' -import {Text} from './Text' +import {Text} from './text/Text' import {colors} from '../../lib/styles' interface Layout { diff --git a/src/view/com/util/Text.tsx b/src/view/com/util/Text.tsx deleted file mode 100644 index acf7589e0..000000000 --- a/src/view/com/util/Text.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import {Text as RNText, TextProps} from 'react-native' -import {s} from '../../lib/styles' - -export function Text({ - children, - style, - ...props -}: React.PropsWithChildren) { - return ( - - {children} - - ) -} diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index cdd1f4d91..f5ed07d63 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react' import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {StyleProp, TextStyle} from 'react-native' import {Link} from './Link' -import {Text} from './Text' +import {Text} from './text/Text' import {LoadingPlaceholder} from './LoadingPlaceholder' import {useStores} from '../../../state' diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index e14c2412d..c6eaba5dd 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -8,7 +8,7 @@ import { } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserAvatar} from './UserAvatar' -import {Text} from './Text' +import {Text} from './text/Text' import {s, colors} from '../../lib/styles' import {MagnifyingGlassIcon} from '../../lib/icons' import {useStores} from '../../../state' diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index e436e41b2..a29ee9d26 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -7,8 +7,8 @@ import { } from 'react-native' import {Selector} from './Selector' import {HorzSwipe} from './gestures/HorzSwipe' -import {useAnimatedValue} from '../../lib/useAnimatedValue' -import {OnScrollCb} from '../../lib/useOnMainScroll' +import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' +import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' const HEADER_ITEM = {_reactKey: '__header__'} const SELECTOR_ITEM = {_reactKey: '__selector__'} diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx new file mode 100644 index 000000000..905268d3e --- /dev/null +++ b/src/view/com/util/error/ErrorMessage.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { + StyleSheet, + TouchableOpacity, + StyleProp, + View, + ViewStyle, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from '../text/Text' +import {colors} from '../../../lib/styles' +import {useTheme} from '../../../lib/ThemeContext' +import {usePalette} from '../../../lib/hooks/usePalette' + +export function ErrorMessage({ + message, + numberOfLines, + style, + onPressTryAgain, +}: { + message: string + numberOfLines?: number + style?: StyleProp + onPressTryAgain?: () => void +}) { + const theme = useTheme() + const pal = usePalette('error') + return ( + + + + + + {message} + + {onPressTryAgain && ( + + + + )} + + ) +} + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 8, + }, + errorIcon: { + borderRadius: 12, + width: 24, + height: 24, + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + message: { + flex: 1, + paddingRight: 10, + }, + btn: { + paddingHorizontal: 4, + paddingVertical: 4, + }, +}) diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx new file mode 100644 index 000000000..6db54a9f2 --- /dev/null +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -0,0 +1,113 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from '../text/Text' +import {colors} from '../../../lib/styles' +import {useTheme} from '../../../lib/ThemeContext' +import {usePalette} from '../../../lib/hooks/usePalette' + +export function ErrorScreen({ + title, + message, + details, + onPressTryAgain, +}: { + title: string + message: string + details?: string + onPressTryAgain?: () => void +}) { + const theme = useTheme() + const pal = usePalette('error') + return ( + + + + + + + + {title} + + {message} + {details && ( + + {details} + + )} + {onPressTryAgain && ( + + + + + Try again + + + + )} + + ) +} + +const styles = StyleSheet.create({ + outer: { + flex: 1, + paddingVertical: 30, + paddingHorizontal: 14, + }, + title: { + textAlign: 'center', + marginBottom: 10, + }, + message: { + textAlign: 'center', + marginBottom: 20, + }, + details: { + textAlign: 'center', + paddingVertical: 10, + paddingHorizontal: 14, + overflow: 'hidden', + marginBottom: 20, + }, + btnContainer: { + alignItems: 'center', + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + }, + btnText: { + marginLeft: 5, + }, + errorIconContainer: { + alignItems: 'center', + marginBottom: 10, + }, + errorIcon: { + borderRadius: 30, + width: 50, + height: 50, + alignItems: 'center', + justifyContent: 'center', + marginRight: 5, + }, +}) diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx new file mode 100644 index 000000000..b5c4da19d --- /dev/null +++ b/src/view/com/util/forms/Button.tsx @@ -0,0 +1,120 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + TextStyle, + TouchableOpacity, + ViewStyle, +} from 'react-native' +import {Text} from '../text/Text' +import {useTheme} from '../../../lib/ThemeContext' +import {choose} from '../../../../lib/functions' + +export type ButtonType = + | 'primary' + | 'secondary' + | 'inverted' + | 'primary-outline' + | 'secondary-outline' + | 'primary-light' + | 'secondary-light' + | 'default-light' + +export function Button({ + type = 'primary', + label, + style, + onPress, + children, +}: React.PropsWithChildren<{ + type?: ButtonType + label?: string + style?: StyleProp + onPress?: () => void +}>) { + const theme = useTheme() + const outerStyle = choose>(type, { + primary: { + backgroundColor: theme.palette.primary.background, + }, + secondary: { + backgroundColor: theme.palette.secondary.background, + }, + inverted: { + backgroundColor: theme.palette.inverted.background, + }, + 'primary-outline': { + backgroundColor: theme.palette.default.background, + borderWidth: 1, + borderColor: theme.palette.primary.border, + }, + 'secondary-outline': { + backgroundColor: theme.palette.default.background, + borderWidth: 1, + borderColor: theme.palette.secondary.border, + }, + 'primary-light': { + backgroundColor: theme.palette.default.background, + }, + 'secondary-light': { + backgroundColor: theme.palette.default.background, + }, + 'default-light': { + backgroundColor: theme.palette.default.background, + }, + }) + const labelStyle = choose>(type, { + primary: { + color: theme.palette.primary.text, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + secondary: { + color: theme.palette.secondary.text, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + inverted: { + color: theme.palette.inverted.text, + fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined, + }, + 'primary-outline': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-outline': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'primary-light': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-light': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'default-light': { + color: theme.palette.default.text, + fontWeight: theme.palette.default.isLowContrast ? '500' : undefined, + }, + }) + return ( + + {label ? ( + + {label} + + ) : ( + children + )} + + ) +} + +const styles = StyleSheet.create({ + outer: { + paddingHorizontal: 10, + paddingVertical: 8, + }, +}) diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx new file mode 100644 index 000000000..c81ccf6c5 --- /dev/null +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -0,0 +1,238 @@ +import React, {useRef} from 'react' +import { + Share, + StyleProp, + StyleSheet, + TouchableOpacity, + TouchableWithoutFeedback, + View, + ViewStyle, +} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import RootSiblings from 'react-native-root-siblings' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from '../text/Text' +import {Button, ButtonType} from './Button' +import {colors} from '../../../lib/styles' +import {toShareUrl} from '../../../../lib/strings' +import {useStores} from '../../../../state' +import {ReportPostModal, ConfirmModal} from '../../../../state/models/shell-ui' +import {TABS_ENABLED} from '../../../../build-flags' + +const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} + +export interface DropdownItem { + icon?: IconProp + label: string + onPress: () => void +} + +export type DropdownButtonType = ButtonType | 'bare' + +export function DropdownButton({ + type = 'bare', + style, + items, + label, + menuWidth, + children, +}: { + type: DropdownButtonType + style?: StyleProp + items: DropdownItem[] + label?: string + menuWidth?: number + children?: React.ReactNode +}) { + const ref = useRef(null) + + const onPress = () => { + ref.current?.measure( + ( + _x: number, + _y: number, + width: number, + height: number, + pageX: number, + pageY: number, + ) => { + if (!menuWidth) { + menuWidth = 200 + } + createDropdownMenu( + pageX + width - menuWidth, + pageY + height, + menuWidth, + items, + ) + }, + ) + } + + if (type === 'bare') { + return ( + + {children} + + ) + } + return ( + + + + ) +} + +export function PostDropdownBtn({ + style, + children, + itemHref, + isAuthor, + onCopyPostText, + onDeletePost, +}: { + style?: StyleProp + children?: React.ReactNode + itemHref: string + itemTitle: string + isAuthor: boolean + onCopyPostText: () => void + onDeletePost: () => void +}) { + const store = useStores() + + const dropdownItems: DropdownItem[] = [ + TABS_ENABLED + ? { + icon: ['far', 'clone'], + label: 'Open in new tab', + onPress() { + store.nav.newTab(itemHref) + }, + } + : undefined, + { + icon: ['far', 'paste'], + label: 'Copy post text', + onPress() { + onCopyPostText() + }, + }, + { + icon: 'share', + label: 'Share...', + onPress() { + Share.share({url: toShareUrl(itemHref)}) + }, + }, + { + icon: 'circle-exclamation', + label: 'Report post', + onPress() { + store.shell.openModal(new ReportPostModal(itemHref)) + }, + }, + isAuthor + ? { + icon: ['far', 'trash-can'], + label: 'Delete post', + onPress() { + store.shell.openModal( + new ConfirmModal( + 'Delete this post?', + 'Are you sure? This can not be undone.', + onDeletePost, + ), + ) + }, + } + : undefined, + ].filter(Boolean) as DropdownItem[] + + return ( + + {children} + + ) +} + +function createDropdownMenu( + x: number, + y: number, + width: number, + items: DropdownItem[], +): RootSiblings { + const onPressItem = (index: number) => { + sibling.destroy() + items[index].onPress() + } + const onOuterPress = () => sibling.destroy() + const sibling = new RootSiblings( + ( + <> + + + + + {items.map((item, index) => ( + onPressItem(index)}> + {item.icon && ( + + )} + {item.label} + + ))} + + + ), + ) + return sibling +} + +const styles = StyleSheet.create({ + bg: { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + backgroundColor: '#000', + opacity: 0.1, + }, + menu: { + position: 'absolute', + backgroundColor: '#fff', + borderRadius: 14, + opacity: 1, + paddingVertical: 6, + }, + menuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingLeft: 15, + paddingRight: 40, + }, + menuItemBorder: { + borderTopWidth: 1, + borderTopColor: colors.gray1, + marginTop: 4, + paddingTop: 12, + }, + icon: { + marginLeft: 6, + marginRight: 8, + }, + label: { + fontSize: 18, + }, +}) diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx index 9da404bea..81489c447 100644 --- a/src/view/com/util/forms/RadioButton.tsx +++ b/src/view/com/util/forms/RadioButton.tsx @@ -1,24 +1,126 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Text} from '../Text' -import {colors} from '../../../lib/styles' +import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' +import {Text} from '../text/Text' +import {Button, ButtonType} from './Button' +import {useTheme} from '../../../lib/ThemeContext' +import {choose} from '../../../../lib/functions' export function RadioButton({ + type = 'default-light', label, isSelected, + style, onPress, }: { + type?: ButtonType label: string isSelected: boolean + style?: StyleProp onPress: () => void }) { + const theme = useTheme() + const circleStyle = choose>(type, { + primary: { + borderColor: theme.palette.primary.text, + }, + secondary: { + borderColor: theme.palette.secondary.text, + }, + inverted: { + borderColor: theme.palette.inverted.text, + }, + 'primary-outline': { + borderColor: theme.palette.primary.border, + }, + 'secondary-outline': { + borderColor: theme.palette.secondary.border, + }, + 'primary-light': { + borderColor: theme.palette.primary.border, + }, + 'secondary-light': { + borderColor: theme.palette.secondary.border, + }, + 'default-light': { + borderColor: theme.palette.default.border, + }, + }) + const circleFillStyle = choose>( + type, + { + primary: { + backgroundColor: theme.palette.primary.text, + }, + secondary: { + backgroundColor: theme.palette.secondary.text, + }, + inverted: { + backgroundColor: theme.palette.inverted.text, + }, + 'primary-outline': { + backgroundColor: theme.palette.primary.background, + }, + 'secondary-outline': { + backgroundColor: theme.palette.secondary.background, + }, + 'primary-light': { + backgroundColor: theme.palette.primary.background, + }, + 'secondary-light': { + backgroundColor: theme.palette.secondary.background, + }, + 'default-light': { + backgroundColor: theme.palette.primary.background, + }, + }, + ) + const labelStyle = choose>(type, { + primary: { + color: theme.palette.primary.text, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + secondary: { + color: theme.palette.secondary.text, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + inverted: { + color: theme.palette.inverted.text, + fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined, + }, + 'primary-outline': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-outline': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'primary-light': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-light': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'default-light': { + color: theme.palette.default.text, + fontWeight: theme.palette.default.isLowContrast ? '500' : undefined, + }, + }) return ( - - - {isSelected ? : undefined} + ) } @@ -26,30 +128,21 @@ const styles = StyleSheet.create({ outer: { flexDirection: 'row', alignItems: 'center', - marginBottom: 5, - borderRadius: 8, - borderWidth: 1, - borderColor: colors.gray2, - paddingHorizontal: 10, - paddingVertical: 8, }, circle: { - width: 30, - height: 30, + width: 26, + height: 26, borderRadius: 15, padding: 4, borderWidth: 1, - borderColor: colors.gray3, marginRight: 10, }, circleFill: { - width: 20, - height: 20, + width: 16, + height: 16, borderRadius: 10, - backgroundColor: colors.blue3, }, label: { flex: 1, - fontSize: 17, }, }) diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx index 6684cde5c..9abc2345f 100644 --- a/src/view/com/util/forms/RadioGroup.tsx +++ b/src/view/com/util/forms/RadioGroup.tsx @@ -1,6 +1,7 @@ import React, {useState} from 'react' import {View} from 'react-native' import {RadioButton} from './RadioButton' +import {ButtonType} from './Button' export interface RadioGroupItem { label: string @@ -8,22 +9,28 @@ export interface RadioGroupItem { } export function RadioGroup({ + type, items, + initialSelection = '', onSelect, }: { + type?: ButtonType items: RadioGroupItem[] + initialSelection?: string onSelect: (key: string) => void }) { - const [selection, setSelection] = useState('') + const [selection, setSelection] = useState(initialSelection) const onSelectInner = (key: string) => { setSelection(key) onSelect(key) } return ( - {items.map(item => ( + {items.map((item, i) => ( onSelectInner(item.key)} diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx new file mode 100644 index 000000000..77e8fa203 --- /dev/null +++ b/src/view/com/util/forms/ToggleButton.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' +import {Text} from '../text/Text' +import {Button, ButtonType} from './Button' +import {useTheme} from '../../../lib/ThemeContext' +import {choose} from '../../../../lib/functions' +import {colors} from '../../../lib/styles' + +export function ToggleButton({ + type = 'default-light', + label, + isSelected, + style, + onPress, +}: { + type?: ButtonType + label: string + isSelected: boolean + style?: StyleProp + onPress: () => void +}) { + const theme = useTheme() + const circleStyle = choose>(type, { + primary: { + borderColor: theme.palette.primary.text, + }, + secondary: { + borderColor: theme.palette.secondary.text, + }, + inverted: { + borderColor: theme.palette.inverted.text, + }, + 'primary-outline': { + borderColor: theme.palette.primary.border, + }, + 'secondary-outline': { + borderColor: theme.palette.secondary.border, + }, + 'primary-light': { + borderColor: theme.palette.primary.border, + }, + 'secondary-light': { + borderColor: theme.palette.secondary.border, + }, + 'default-light': { + borderColor: theme.palette.default.border, + }, + }) + const circleFillStyle = choose>( + type, + { + primary: { + backgroundColor: theme.palette.primary.text, + opacity: isSelected ? 1 : 0.33, + }, + secondary: { + backgroundColor: theme.palette.secondary.text, + opacity: isSelected ? 1 : 0.33, + }, + inverted: { + backgroundColor: theme.palette.inverted.text, + opacity: isSelected ? 1 : 0.33, + }, + 'primary-outline': { + backgroundColor: theme.palette.primary.background, + opacity: isSelected ? 1 : 0.5, + }, + 'secondary-outline': { + backgroundColor: theme.palette.secondary.background, + opacity: isSelected ? 1 : 0.5, + }, + 'primary-light': { + backgroundColor: theme.palette.primary.background, + opacity: isSelected ? 1 : 0.5, + }, + 'secondary-light': { + backgroundColor: theme.palette.secondary.background, + opacity: isSelected ? 1 : 0.5, + }, + 'default-light': { + backgroundColor: isSelected + ? theme.palette.primary.background + : colors.gray3, + }, + }, + ) + const labelStyle = choose>(type, { + primary: { + color: theme.palette.primary.text, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + secondary: { + color: theme.palette.secondary.text, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + inverted: { + color: theme.palette.inverted.text, + fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined, + }, + 'primary-outline': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-outline': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'primary-light': { + color: theme.palette.primary.textInverted, + fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined, + }, + 'secondary-light': { + color: theme.palette.secondary.textInverted, + fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined, + }, + 'default-light': { + color: theme.palette.default.text, + fontWeight: theme.palette.default.isLowContrast ? '500' : undefined, + }, + }) + return ( + + ) +} + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'row', + alignItems: 'center', + }, + circle: { + width: 42, + height: 26, + borderRadius: 15, + padding: 4, + borderWidth: 1, + marginRight: 10, + }, + circleFill: { + width: 16, + height: 16, + borderRadius: 10, + }, + circleFillSelected: { + marginLeft: 16, + }, + label: { + flex: 1, + }, +}) diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 05425eb31..9de443b7f 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -9,7 +9,7 @@ import { View, ViewStyle, } from 'react-native' -import {Text} from '../Text' +import {Text} from '../text/Text' import {colors} from '../../../lib/styles' const MAX_HEIGHT = 300 diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx new file mode 100644 index 000000000..c9ed4b58e --- /dev/null +++ b/src/view/com/util/text/RichText.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import {TextStyle, StyleProp} from 'react-native' +import {TextLink} from '../Link' +import {Text} from './Text' +import {s} from '../../../lib/styles' +import {toShortUrl} from '../../../../lib/strings' +import {TypographyVariant} from '../../../lib/ThemeContext' +import {usePalette} from '../../../lib/hooks/usePalette' + +type TextSlice = {start: number; end: number} +type Entity = { + index: TextSlice + type: string + value: string +} + +export function RichText({ + type = 'body1', + text, + entities, + style, + numberOfLines, +}: { + type: TypographyVariant + text: string + entities?: Entity[] + style?: StyleProp + numberOfLines?: number +}) { + const pal = usePalette('default') + if (!entities?.length) { + if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { + style = { + fontSize: 26, + lineHeight: 30, + } + return {text} + } + return {text} + } + if (!style) style = [] + else if (!Array.isArray(style)) style = [style] + entities.sort(sortByIndex) + const segments = Array.from(toSegments(text, entities)) + const els = [] + let key = 0 + for (const segment of segments) { + if (typeof segment === 'string') { + els.push(segment) + } else { + if (segment.entity.type === 'mention') { + els.push( + , + ) + } else if (segment.entity.type === 'link') { + els.push( + , + ) + } + } + key++ + } + return ( + + {els} + + ) +} + +function sortByIndex(a: Entity, b: Entity) { + return a.index.start - b.index.start +} + +function* toSegments(text: string, entities: Entity[]) { + let cursor = 0 + let i = 0 + do { + let currEnt = entities[i] + if (cursor < currEnt.index.start) { + yield text.slice(cursor, currEnt.index.start) + } else if (cursor > currEnt.index.start) { + i++ + continue + } + if (currEnt.index.start < currEnt.index.end) { + let subtext = text.slice(currEnt.index.start, currEnt.index.end) + if (!subtext.trim()) { + // dont yield links to empty strings + yield subtext + } else { + yield { + entity: currEnt, + text: subtext, + } + } + } + cursor = currEnt.index.end + i++ + } while (i < entities.length) + if (cursor < text.length) { + yield text.slice(cursor, text.length) + } +} diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx new file mode 100644 index 000000000..549eb2901 --- /dev/null +++ b/src/view/com/util/text/Text.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import {Text as RNText, TextProps} from 'react-native' +import {s} from '../../../lib/styles' +import {useTheme, TypographyVariant} from '../../../lib/ThemeContext' + +export type CustomTextProps = TextProps & { + type?: TypographyVariant +} + +export function Text({ + type = 'body1', + children, + style, + ...props +}: React.PropsWithChildren) { + const theme = useTheme() + const typography = theme.typography[type] + return ( + + {children} + + ) +} diff --git a/src/view/lib/ThemeContext.tsx b/src/view/lib/ThemeContext.tsx new file mode 100644 index 000000000..57f758c53 --- /dev/null +++ b/src/view/lib/ThemeContext.tsx @@ -0,0 +1,70 @@ +import React, {createContext, useContext, useMemo} from 'react' +import {TextStyle, useColorScheme, ViewStyle} from 'react-native' +import {darkTheme, defaultTheme} from './themes' + +export type ColorScheme = 'light' | 'dark' + +export type PaletteColorName = + | 'default' + | 'primary' + | 'secondary' + | 'inverted' + | 'error' +export type PaletteColor = { + isLowContrast: boolean + background: string + backgroundLight: string + text: string + textLight: string + textInverted: string + link: string + border: string + icon: string +} +export type Palette = Record + +export type ShapeName = 'button' | 'bigButton' | 'smallButton' +export type Shapes = Record + +export type TypographyVariant = + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'subtitle1' + | 'subtitle2' + | 'body1' + | 'body2' + | 'button' + | 'caption' + | 'overline' +export type Typography = Record + +export interface Theme { + colorScheme: ColorScheme + palette: Palette + shapes: Shapes + typography: Typography +} + +export interface ThemeProviderProps { + theme?: ColorScheme +} + +export const ThemeContext = createContext(defaultTheme) + +export const useTheme = () => useContext(ThemeContext) + +export const ThemeProvider: React.FC = ({ + theme, + children, +}) => { + const colorScheme = useColorScheme() + + const value = useMemo( + () => ((theme || colorScheme) === 'dark' ? darkTheme : defaultTheme), + [colorScheme, theme], + ) + + return {children} +} diff --git a/src/view/lib/hooks/useAnimatedValue.ts b/src/view/lib/hooks/useAnimatedValue.ts new file mode 100644 index 000000000..1307ef952 --- /dev/null +++ b/src/view/lib/hooks/useAnimatedValue.ts @@ -0,0 +1,12 @@ +import * as React from 'react' +import {Animated} from 'react-native' + +export function useAnimatedValue(initialValue: number) { + const lazyRef = React.useRef() + + if (lazyRef.current === undefined) { + lazyRef.current = new Animated.Value(initialValue) + } + + return lazyRef.current as Animated.Value +} diff --git a/src/view/lib/hooks/useOnMainScroll.ts b/src/view/lib/hooks/useOnMainScroll.ts new file mode 100644 index 000000000..c3c16ff83 --- /dev/null +++ b/src/view/lib/hooks/useOnMainScroll.ts @@ -0,0 +1,25 @@ +import {useState} from 'react' +import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' +import {RootStoreModel} from '../../../state' + +export type OnScrollCb = ( + event: NativeSyntheticEvent, +) => void + +export function useOnMainScroll(store: RootStoreModel) { + let [lastY, setLastY] = useState(0) + let isMinimal = store.shell.minimalShellMode + return function onMainScroll(event: NativeSyntheticEvent) { + const y = event.nativeEvent.contentOffset.y + const dy = y - (lastY || 0) + setLastY(y) + + if (!isMinimal && y > 10 && dy > 10) { + store.shell.setMinimalShellMode(true) + isMinimal = true + } else if (isMinimal && (y <= 10 || dy < -10)) { + store.shell.setMinimalShellMode(false) + isMinimal = false + } + } +} diff --git a/src/view/lib/hooks/usePalette.ts b/src/view/lib/hooks/usePalette.ts new file mode 100644 index 000000000..e9af4ae16 --- /dev/null +++ b/src/view/lib/hooks/usePalette.ts @@ -0,0 +1,41 @@ +import {TextStyle, ViewStyle} from 'react-native' +import {useTheme, PaletteColorName, PaletteColor} from '../ThemeContext' + +export interface UsePaletteValue { + colors: PaletteColor + view: ViewStyle + border: ViewStyle + text: TextStyle + textLight: TextStyle + textInverted: TextStyle + link: TextStyle +} +export function usePalette(color: PaletteColorName): UsePaletteValue { + const palette = useTheme().palette[color] + return { + colors: palette, + view: { + backgroundColor: palette.background, + }, + border: { + borderWidth: 1, + borderColor: palette.border, + }, + text: { + color: palette.text, + fontWeight: palette.isLowContrast ? '500' : undefined, + }, + textLight: { + color: palette.textLight, + fontWeight: palette.isLowContrast ? '500' : undefined, + }, + textInverted: { + color: palette.textInverted, + fontWeight: palette.isLowContrast ? '500' : undefined, + }, + link: { + color: palette.link, + fontWeight: palette.isLowContrast ? '500' : undefined, + }, + } +} diff --git a/src/view/lib/themes.ts b/src/view/lib/themes.ts new file mode 100644 index 000000000..3851ee9d0 --- /dev/null +++ b/src/view/lib/themes.ts @@ -0,0 +1,163 @@ +import type {Theme} from './ThemeContext' +import {colors} from './styles' + +export const defaultTheme: Theme = { + colorScheme: 'light', + palette: { + default: { + isLowContrast: false, + background: colors.white, + backgroundLight: colors.gray2, + text: colors.black, + textLight: colors.gray5, + textInverted: colors.white, + link: colors.blue3, + border: colors.gray3, + icon: colors.gray2, + }, + primary: { + isLowContrast: true, + background: colors.blue3, + backgroundLight: colors.blue2, + text: colors.white, + textLight: colors.blue0, + textInverted: colors.blue3, + link: colors.blue0, + border: colors.blue4, + icon: colors.blue4, + }, + secondary: { + isLowContrast: true, + background: colors.green3, + backgroundLight: colors.green2, + text: colors.white, + textLight: colors.green1, + textInverted: colors.green4, + link: colors.green1, + border: colors.green4, + icon: colors.green4, + }, + inverted: { + isLowContrast: true, + background: colors.black, + backgroundLight: colors.gray6, + text: colors.white, + textLight: colors.gray3, + textInverted: colors.black, + link: colors.blue2, + border: colors.gray3, + icon: colors.gray5, + }, + error: { + isLowContrast: true, + background: colors.red3, + backgroundLight: colors.red2, + text: colors.white, + textLight: colors.red1, + textInverted: colors.red3, + link: colors.red1, + border: colors.red4, + icon: colors.red4, + }, + }, + shapes: { + button: { + // TODO + }, + bigButton: { + // TODO + }, + smallButton: { + // TODO + }, + }, + typography: { + h1: { + fontSize: 48, + fontWeight: '500', + }, + h2: { + fontSize: 34, + letterSpacing: 0.25, + fontWeight: '500', + }, + h3: { + fontSize: 24, + fontWeight: '500', + }, + h4: { + fontWeight: '500', + fontSize: 20, + letterSpacing: 0.15, + }, + subtitle1: { + fontSize: 16, + letterSpacing: 0.15, + }, + subtitle2: { + fontWeight: '500', + fontSize: 14, + letterSpacing: 0.1, + }, + body1: { + fontSize: 16, + letterSpacing: 0.5, + }, + body2: { + fontSize: 14, + letterSpacing: 0.25, + }, + button: { + fontWeight: '500', + fontSize: 14, + letterSpacing: 0.5, + }, + caption: { + fontSize: 12, + letterSpacing: 0.4, + }, + overline: { + fontSize: 10, + letterSpacing: 1.5, + textTransform: 'uppercase', + }, + }, +} + +export const darkTheme: Theme = { + ...defaultTheme, + colorScheme: 'dark', + palette: { + ...defaultTheme.palette, + default: { + isLowContrast: true, + background: colors.black, + backgroundLight: colors.gray6, + text: colors.white, + textLight: colors.gray3, + textInverted: colors.black, + link: colors.blue2, + border: colors.gray3, + icon: colors.gray5, + }, + primary: { + ...defaultTheme.palette.primary, + textInverted: colors.blue2, + }, + secondary: { + ...defaultTheme.palette.secondary, + textInverted: colors.green2, + }, + inverted: { + isLowContrast: false, + background: colors.white, + backgroundLight: colors.gray2, + text: colors.black, + textLight: colors.gray5, + textInverted: colors.white, + link: colors.blue3, + border: colors.gray3, + icon: colors.gray1, + }, + }, +} diff --git a/src/view/lib/useAnimatedValue.ts b/src/view/lib/useAnimatedValue.ts deleted file mode 100644 index 1307ef952..000000000 --- a/src/view/lib/useAnimatedValue.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react' -import {Animated} from 'react-native' - -export function useAnimatedValue(initialValue: number) { - const lazyRef = React.useRef() - - if (lazyRef.current === undefined) { - lazyRef.current = new Animated.Value(initialValue) - } - - return lazyRef.current as Animated.Value -} diff --git a/src/view/lib/useOnMainScroll.ts b/src/view/lib/useOnMainScroll.ts deleted file mode 100644 index ee0081226..000000000 --- a/src/view/lib/useOnMainScroll.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {useState} from 'react' -import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' -import {RootStoreModel} from '../../state' - -export type OnScrollCb = ( - event: NativeSyntheticEvent, -) => void - -export function useOnMainScroll(store: RootStoreModel) { - let [lastY, setLastY] = useState(0) - let isMinimal = store.shell.minimalShellMode - return function onMainScroll(event: NativeSyntheticEvent) { - const y = event.nativeEvent.contentOffset.y - const dy = y - (lastY || 0) - setLastY(y) - - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { - store.shell.setMinimalShellMode(false) - isMinimal = false - } - } -} diff --git a/src/view/lib/z-index.ts b/src/view/lib/z-index.ts deleted file mode 100644 index 872027d3f..000000000 --- a/src/view/lib/z-index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const FAB = 1 -export const BASE = 0 diff --git a/src/view/routes.ts b/src/view/routes.ts index 272a1b096..3717e0f05 100644 --- a/src/view/routes.ts +++ b/src/view/routes.ts @@ -15,6 +15,7 @@ import {ProfileFollowers} from './screens/ProfileFollowers' import {ProfileFollows} from './screens/ProfileFollows' import {ProfileMembers} from './screens/ProfileMembers' import {Settings} from './screens/Settings' +import {Debug} from './screens/Debug' export type ScreenParams = { navIdx: [number, number] @@ -71,6 +72,7 @@ export const routes: Route[] = [ 'retweet', r('/profile/(?[^/]+)/post/(?[^/]+)/reposted-by'), ], + [Debug, 'Debug', 'house', r('/debug')], ] export function match(url: string): MatchResult { diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index bcfb47782..8de56d79a 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -3,11 +3,11 @@ import {StyleSheet, TextInput, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {Selector} from '../com/util/Selector' -import {Text} from '../com/util/Text' +import {Text} from '../com/util/text/Text' import {colors} from '../lib/styles' import {ScreenParams} from '../routes' import {useStores} from '../../state' -import {useAnimatedValue} from '../lib/useAnimatedValue' +import {useAnimatedValue} from '../lib/hooks/useAnimatedValue' export const Contacts = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx new file mode 100644 index 000000000..f34bcc17b --- /dev/null +++ b/src/view/screens/Debug.tsx @@ -0,0 +1,432 @@ +import React from 'react' +import {ScrollView, View} from 'react-native' +import {ViewHeader} from '../com/util/ViewHeader' +import {ThemeProvider} from '../lib/ThemeContext' +import {PaletteColorName} from '../lib/ThemeContext' +import {usePalette} from '../lib/hooks/usePalette' + +import {Text} from '../com/util/text/Text' +import {ViewSelector} from '../com/util/ViewSelector' +import {EmptyState} from '../com/util/EmptyState' +import * as LoadingPlaceholder from '../com/util/LoadingPlaceholder' +import {Button} from '../com/util/forms/Button' +import {DropdownButton, DropdownItem} from '../com/util/forms/DropdownButton' +import {ToggleButton} from '../com/util/forms/ToggleButton' +import {RadioGroup} from '../com/util/forms/RadioGroup' +import {ErrorScreen} from '../com/util/error/ErrorScreen' +import {ErrorMessage} from '../com/util/error/ErrorMessage' + +const MAIN_VIEWS = ['Base', 'Controls', 'Error'] + +export const Debug = () => { + const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>( + 'light', + ) + const onToggleColorScheme = () => { + setColorScheme(colorScheme === 'light' ? 'dark' : 'light') + } + return ( + + + + ) +} + +function DebugInner({ + colorScheme, + onToggleColorScheme, +}: { + colorScheme: 'light' | 'dark' + onToggleColorScheme: () => void +}) { + const [currentView, setCurrentView] = React.useState(0) + const pal = usePalette('default') + + const renderItem = item => { + return ( + + + + + {item.currentView === 2 ? ( + + ) : item.currentView === 1 ? ( + + ) : ( + + )} + + ) + } + + const items = [{currentView}] + + return ( + + + + + ) +} + +function Heading({label}: {label: string}) { + const pal = usePalette('default') + return ( + + + {label} + + + ) +} + +function BaseView() { + return ( + + + + + + + + + + + + + + + + ) +} + +function ControlsView() { + return ( + + + + + + + + + + + + ) +} + +function ErrorView() { + return ( + + + {}} + /> + + + + + + + + + {}} + /> + + + {}} + numberOfLines={1} + /> + + + ) +} + +function PaletteView({palette}: {palette: PaletteColorName}) { + const defaultPal = usePalette('default') + const pal = usePalette(palette) + return ( + + {palette} colors + Light text + Link text + {palette !== 'default' && ( + + Inverted text + + )} + + ) +} + +function TypographyView() { + const pal = usePalette('default') + return ( + + + Heading 1 + + + Heading 2 + + + Heading 3 + + + Heading 4 + + + Subtitle 1 + + + Subtitle 2 + + + Body 1 + + + Body 2 + + + Button + + + Caption + + + Overline + + + ) +} + +function EmptyStateView() { + return +} + +function LoadingPlaceholderView() { + return ( + <> + + + + ) +} + +function ButtonsView() { + const defaultPal = usePalette('default') + const buttonStyles = {marginRight: 5} + return ( + + +