From 99360f7bd90480b0c382014fa7d889aef8c433a4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 27 Jan 2023 00:16:07 -0600 Subject: Implement basic web composer --- src/view/com/composer/ComposePost.tsx | 75 +++------ .../com/composer/char-progress/CharProgress.tsx | 41 +++++ .../composer/char-progress/CharProgress.web.tsx | 39 +++++ src/view/com/composer/text-input/TextInput.tsx | 54 +++++++ src/view/com/composer/text-input/TextInput.web.tsx | 51 ++++++ src/view/shell/mobile/Composer.tsx | 3 - src/view/shell/web/Composer.tsx | 65 ++++++++ src/view/shell/web/DesktopLeftColumn.tsx | 173 +++++++++++++++++++++ src/view/shell/web/DesktopRightColumn.tsx | 52 +++++++ src/view/shell/web/index.tsx | 13 +- src/view/shell/web/left-column.tsx | 173 --------------------- src/view/shell/web/right-column.tsx | 52 ------- 12 files changed, 505 insertions(+), 286 deletions(-) create mode 100644 src/view/com/composer/char-progress/CharProgress.tsx create mode 100644 src/view/com/composer/char-progress/CharProgress.web.tsx create mode 100644 src/view/com/composer/text-input/TextInput.tsx create mode 100644 src/view/com/composer/text-input/TextInput.web.tsx create mode 100644 src/view/shell/web/Composer.tsx create mode 100644 src/view/shell/web/DesktopLeftColumn.tsx create mode 100644 src/view/shell/web/DesktopRightColumn.tsx delete mode 100644 src/view/shell/web/left-column.tsx delete mode 100644 src/view/shell/web/right-column.tsx (limited to 'src') diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index 64e75328a..2f30a1cf4 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -11,25 +11,19 @@ import { TouchableWithoutFeedback, View, } from 'react-native' -import PasteInput, { - PastedFile, - PasteInputRef, -} from '@mattermost/react-native-paste-input' import LinearGradient from 'react-native-linear-gradient' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {useAnalytics} from '@segment/analytics-react-native' +// import {useAnalytics} from '@segment/analytics-react-native' TODO import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' import {Autocomplete} from './Autocomplete' import {ExternalEmbed} from './ExternalEmbed' 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' -// @ts-ignore no type definition -prf -import ProgressPie from 'react-native-progress/Pie' +import {TextInput, TextInputRef} from './text-input/TextInput' +import {CharProgress} from './char-progress/CharProgress' import {TextLink} from '../util/Link' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' @@ -49,7 +43,6 @@ import {SelectedPhoto} from './SelectedPhoto' import {usePalette} from '../../lib/hooks/usePalette' const MAX_TEXT_LENGTH = 256 -const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} export const ComposePost = observer(function ComposePost({ @@ -63,10 +56,10 @@ export const ComposePost = observer(function ComposePost({ onPost?: ComposerOpts['onPost'] onClose: () => void }) { - const {track} = useAnalytics() + // const {track} = useAnalytics() TODO const pal = usePalette('default') const store = useStores() - const textInput = useRef(null) + const textInput = useRef(null) const [isProcessing, setIsProcessing] = useState(false) const [processingState, setProcessingState] = useState('') const [error, setError] = useState('') @@ -80,7 +73,6 @@ export const ComposePost = observer(function ComposePost({ ) const [selectedPhotos, setSelectedPhotos] = useState([]) - // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment const autocompleteView = React.useMemo( () => new UserAutocompleteViewModel(store), [store], @@ -219,19 +211,18 @@ export const ComposePost = observer(function ComposePost({ } } } - const onPaste = async (err: string | undefined, files: PastedFile[]) => { + const onPaste = async (err: string | undefined, uris: string[]) => { if (err) { return setError(cleanError(err)) } if (selectedPhotos.length >= 4) { return } - const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName)) - if (!imgFile) { - return + const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri)) + if (imgUri) { + const finalImgPath = await cropPhoto(imgUri) + onSelectPhotos([...selectedPhotos, finalImgPath]) } - const finalImgPath = await cropPhoto(imgFile.uri) - onSelectPhotos([...selectedPhotos, finalImgPath]) } const onPressCancel = () => hackfixOnClose() const onPressPublish = async () => { @@ -257,9 +248,10 @@ export const ComposePost = observer(function ComposePost({ autocompleteView.knownHandles, setProcessingState, ) - track('Create Post', { - imageCount: selectedPhotos.length, - }) + // TODO + // track('Create Post', { + // imageCount: selectedPhotos.length, + // }) } catch (e: any) { setError(cleanError(e.message)) setIsProcessing(false) @@ -276,7 +268,6 @@ export const ComposePost = observer(function ComposePost({ } const canPost = text.length <= MAX_TEXT_LENGTH - const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined const selectTextInputLayout = selectedPhotos.length !== 0 @@ -311,7 +302,7 @@ export const ComposePost = observer(function ComposePost({ + style={styles.outer}> @@ -396,22 +387,19 @@ export const ComposePost = observer(function ComposePost({ avatar={store.me.avatar} size={50} /> - onChangeText(str)} onPaste={onPaste} placeholder={selectTextInputPlaceholder} - placeholderTextColor={pal.colors.textLight} style={[ pal.text, styles.textInput, styles.textInputFormatting, ]}> {textDecorated} - + - - {MAX_TEXT_LENGTH - text.length} - - - {text.length > DANGER_TEXT_LENGTH ? ( - - ) : ( - - )} - + DANGER_TEXT_LENGTH ? '#e60000' : undefined + return ( + <> + + {MAX_TEXT_LENGTH - count} + + + {count > DANGER_TEXT_LENGTH ? ( + + ) : ( + + )} + + + ) +} diff --git a/src/view/com/composer/char-progress/CharProgress.web.tsx b/src/view/com/composer/char-progress/CharProgress.web.tsx new file mode 100644 index 000000000..6bdcc1392 --- /dev/null +++ b/src/view/com/composer/char-progress/CharProgress.web.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import {View} from 'react-native' +import {Text} from '../util/text/Text' +import {s} from '../../lib/styles' + +const MAX_TEXT_LENGTH = 256 +const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH + +export function CharProgress({count}: {count: number}) { + const progressColor = count > DANGER_TEXT_LENGTH ? '#e60000' : undefined + return ( + <> + + {MAX_TEXT_LENGTH - count} + + + { + null /* TODO count > DANGER_TEXT_LENGTH ? ( + + ) : ( + + )*/ + } + + + ) +} diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx new file mode 100644 index 000000000..3c5dacf80 --- /dev/null +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import {StyleProp, TextStyle} from 'react-native' +import PasteInput, { + PastedFile, + PasteInputRef, +} from '@mattermost/react-native-paste-input' +import {usePalette} from '../../../lib/hooks/usePalette' + +export type TextInputRef = PasteInputRef + +interface TextInputProps { + testID: string + innerRef: React.Ref + placeholder: string + style: StyleProp + onChangeText: (str: string) => void + onPaste: (err: string | undefined, uris: string[]) => void +} + +export function TextInput({ + testID, + innerRef, + placeholder, + style, + onChangeText, + onPaste, + children, +}: React.PropsWithChildren) { + const pal = usePalette('default') + const onPasteInner = (err: string | undefined, files: PastedFile[]) => { + if (err) { + onPaste(err, []) + } else { + onPaste( + undefined, + files.map(f => f.uri), + ) + } + } + return ( + onChangeText(str)} + onPaste={onPasteInner} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + style={style}> + {children} + + ) +} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx new file mode 100644 index 000000000..6960bf7ab --- /dev/null +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + TextInput as RNTextInput, + TextStyle, +} from 'react-native' +import {usePalette} from '../../lib/hooks/usePalette' +import {addStyle} from '../../lib/addStyle' + +export type TextInputRef = RNTextInput + +interface TextInputProps { + testID: string + innerRef: React.Ref + placeholder: string + style: StyleProp + onChangeText: (str: string) => void + onPaste: (err: string | undefined, uris: string[]) => void +} + +export function TextInput({ + testID, + innerRef, + placeholder, + style, + onChangeText, + children, +}: React.PropsWithChildren) { + const pal = usePalette('default') + style = addStyle(style, styles.input) + return ( + onChangeText(str)} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + style={style}> + {children} + + ) +} + +const styles = StyleSheet.create({ + input: { + minHeight: 140, + }, +}) diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx index a19a4704c..c93931ab4 100644 --- a/src/view/shell/mobile/Composer.tsx +++ b/src/view/shell/mobile/Composer.tsx @@ -48,9 +48,6 @@ export const Composer = observer( ], } - // events - // = - // rendering // = diff --git a/src/view/shell/web/Composer.tsx b/src/view/shell/web/Composer.tsx new file mode 100644 index 000000000..639040097 --- /dev/null +++ b/src/view/shell/web/Composer.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import {observer} from 'mobx-react-lite' +import {StyleSheet, View} from 'react-native' +import {ComposePost} from '../../com/composer/ComposePost' +import {ComposerOpts} from '../../../state/models/shell-ui' +import {usePalette} from '../../lib/hooks/usePalette' + +export const Composer = observer( + ({ + active, + replyTo, + imagesOpen, + onPost, + onClose, + }: { + active: boolean + winHeight: number + replyTo?: ComposerOpts['replyTo'] + imagesOpen?: ComposerOpts['imagesOpen'] + onPost?: ComposerOpts['onPost'] + onClose: () => void + }) => { + const pal = usePalette('default') + + // rendering + // = + + if (!active) { + return + } + + return ( + + + + + + ) + }, +) + +const styles = StyleSheet.create({ + mask: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: '#000c', + alignItems: 'center', + justifyContent: 'center', + }, + container: { + maxWidth: 600, + width: '100%', + paddingVertical: 0, + paddingHorizontal: 2, + borderRadius: 8, + }, +}) diff --git a/src/view/shell/web/DesktopLeftColumn.tsx b/src/view/shell/web/DesktopLeftColumn.tsx new file mode 100644 index 000000000..44559b6ad --- /dev/null +++ b/src/view/shell/web/DesktopLeftColumn.tsx @@ -0,0 +1,173 @@ +import React from 'react' +import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import LinearGradient from 'react-native-linear-gradient' +import {Link} from '../../com/util/Link' +import {Text} from '../../com/util/text/Text' +import {UserAvatar} from '../../com/util/UserAvatar' +import {s, colors, gradients} from '../../lib/styles' +import {useStores} from '../../../state' +import {usePalette} from '../../lib/hooks/usePalette' +import { + HomeIcon, + HomeIconSolid, + BellIcon, + BellIconSolid, + MagnifyingGlassIcon, + CogIcon, +} from '../../lib/icons' + +interface NavItemProps { + label: string + count?: number + href: string + icon: JSX.Element + iconFilled: JSX.Element + isProfile?: boolean +} +export const NavItem = observer( + ({label, count, href, icon, iconFilled, isProfile}: NavItemProps) => { + const store = useStores() + const pal = usePalette('default') + const isCurrent = store.nav.tab.current.url === href + return ( + [ + // @ts-ignore Pressable state differs for RNW -prf + state.hovered && {backgroundColor: pal.colors.backgroundLight}, + ]}> + + + {isCurrent ? iconFilled : icon} + {typeof count === 'number' && count > 0 && ( + + {count} + + )} + + + {label} + + + + ) + }, +) + +export const DesktopLeftColumn = observer(() => { + const store = useStores() + const pal = usePalette('default') + const onPressCompose = () => store.shell.openComposer({}) + const avi = ( + + ) + return ( + + + } + iconFilled={} + /> + } + iconFilled={} + /> + } + iconFilled={} + /> + } + iconFilled={} + /> + + + + New Post + + + + + ) +}) + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + left: 'calc(50vw - 530px)', + width: '230px', + height: '100%', + borderRightWidth: 1, + paddingTop: 5, + }, + navItem: { + paddingVertical: 10, + paddingHorizontal: 10, + flexDirection: 'row', + alignItems: 'center', + }, + navItemIconWrapper: { + flexDirection: 'row', + width: 30, + justifyContent: 'center', + marginRight: 5, + }, + navItemProfile: { + width: 40, + marginRight: 10, + }, + navItemCount: { + position: 'absolute', + top: -5, + left: 15, + backgroundColor: colors.red3, + color: colors.white, + fontSize: 12, + fontWeight: 'bold', + paddingHorizontal: 4, + borderRadius: 6, + }, + navItemLabel: { + fontSize: 19, + }, + composeBtn: { + marginTop: 20, + marginBottom: 10, + marginLeft: 10, + marginRight: 20, + borderRadius: 30, + paddingHorizontal: 20, + paddingVertical: 12, + }, +}) diff --git a/src/view/shell/web/DesktopRightColumn.tsx b/src/view/shell/web/DesktopRightColumn.tsx new file mode 100644 index 000000000..2daa16f6a --- /dev/null +++ b/src/view/shell/web/DesktopRightColumn.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import {View, StyleSheet} from 'react-native' +import {Link} from '../../com/util/Link' +import {Text} from '../../com/util/text/Text' +import {usePalette} from '../../lib/hooks/usePalette' +import {MagnifyingGlassIcon} from '../../lib/icons' +import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows' +import {s} from '../../lib/styles' + +export const DesktopRightColumn: React.FC = () => { + const pal = usePalette('default') + return ( + + + + + + + Search + + + + Suggested Follows + + + + ) +} + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + right: 'calc(50vw - 650px)', + width: '350px', + height: '100%', + borderLeftWidth: 1, + overscrollBehavior: 'auto', + paddingLeft: 30, + paddingTop: 10, + }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 20, + marginBottom: 20, + }, + searchIcon: { + marginRight: 5, + }, +}) diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx index a4232eab2..0eb5cf75c 100644 --- a/src/view/shell/web/index.tsx +++ b/src/view/shell/web/index.tsx @@ -3,13 +3,14 @@ import {observer} from 'mobx-react-lite' import {View, StyleSheet} from 'react-native' import {useStores} from '../../../state' import {match, MatchResult} from '../../routes' -import {DesktopLeftColumn} from './left-column' -import {DesktopRightColumn} from './right-column' +import {DesktopLeftColumn} from './DesktopLeftColumn' +import {DesktopRightColumn} from './DesktopRightColumn' import {Onboard} from '../../screens/Onboard' import {Login} from '../../screens/Login' import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {Lightbox} from '../../com/lightbox/Lightbox' import {Modal} from '../../com/modals/Modal' +import {Composer} from './Composer' import {usePalette} from '../../lib/hooks/usePalette' import {s} from '../../lib/styles' @@ -49,6 +50,14 @@ export const WebShell: React.FC = observer(() => { ))} + store.shell.closeComposer()} + winHeight={0} + replyTo={store.shell.composerOpts?.replyTo} + imagesOpen={store.shell.composerOpts?.imagesOpen} + onPost={store.shell.composerOpts?.onPost} + /> diff --git a/src/view/shell/web/left-column.tsx b/src/view/shell/web/left-column.tsx deleted file mode 100644 index 44559b6ad..000000000 --- a/src/view/shell/web/left-column.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react' -import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import LinearGradient from 'react-native-linear-gradient' -import {Link} from '../../com/util/Link' -import {Text} from '../../com/util/text/Text' -import {UserAvatar} from '../../com/util/UserAvatar' -import {s, colors, gradients} from '../../lib/styles' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' -import { - HomeIcon, - HomeIconSolid, - BellIcon, - BellIconSolid, - MagnifyingGlassIcon, - CogIcon, -} from '../../lib/icons' - -interface NavItemProps { - label: string - count?: number - href: string - icon: JSX.Element - iconFilled: JSX.Element - isProfile?: boolean -} -export const NavItem = observer( - ({label, count, href, icon, iconFilled, isProfile}: NavItemProps) => { - const store = useStores() - const pal = usePalette('default') - const isCurrent = store.nav.tab.current.url === href - return ( - [ - // @ts-ignore Pressable state differs for RNW -prf - state.hovered && {backgroundColor: pal.colors.backgroundLight}, - ]}> - - - {isCurrent ? iconFilled : icon} - {typeof count === 'number' && count > 0 && ( - - {count} - - )} - - - {label} - - - - ) - }, -) - -export const DesktopLeftColumn = observer(() => { - const store = useStores() - const pal = usePalette('default') - const onPressCompose = () => store.shell.openComposer({}) - const avi = ( - - ) - return ( - - - } - iconFilled={} - /> - } - iconFilled={} - /> - } - iconFilled={} - /> - } - iconFilled={} - /> - - - - New Post - - - - - ) -}) - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - left: 'calc(50vw - 530px)', - width: '230px', - height: '100%', - borderRightWidth: 1, - paddingTop: 5, - }, - navItem: { - paddingVertical: 10, - paddingHorizontal: 10, - flexDirection: 'row', - alignItems: 'center', - }, - navItemIconWrapper: { - flexDirection: 'row', - width: 30, - justifyContent: 'center', - marginRight: 5, - }, - navItemProfile: { - width: 40, - marginRight: 10, - }, - navItemCount: { - position: 'absolute', - top: -5, - left: 15, - backgroundColor: colors.red3, - color: colors.white, - fontSize: 12, - fontWeight: 'bold', - paddingHorizontal: 4, - borderRadius: 6, - }, - navItemLabel: { - fontSize: 19, - }, - composeBtn: { - marginTop: 20, - marginBottom: 10, - marginLeft: 10, - marginRight: 20, - borderRadius: 30, - paddingHorizontal: 20, - paddingVertical: 12, - }, -}) diff --git a/src/view/shell/web/right-column.tsx b/src/view/shell/web/right-column.tsx deleted file mode 100644 index 2daa16f6a..000000000 --- a/src/view/shell/web/right-column.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import {View, StyleSheet} from 'react-native' -import {Link} from '../../com/util/Link' -import {Text} from '../../com/util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' -import {MagnifyingGlassIcon} from '../../lib/icons' -import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows' -import {s} from '../../lib/styles' - -export const DesktopRightColumn: React.FC = () => { - const pal = usePalette('default') - return ( - - - - - - - Search - - - - Suggested Follows - - - - ) -} - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - right: 'calc(50vw - 650px)', - width: '350px', - height: '100%', - borderLeftWidth: 1, - overscrollBehavior: 'auto', - paddingLeft: 30, - paddingTop: 10, - }, - searchContainer: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 14, - paddingVertical: 10, - borderRadius: 20, - marginBottom: 20, - }, - searchIcon: { - marginRight: 5, - }, -}) -- cgit 1.4.1