diff --git a/public/index.html b/public/index.html index edc9d8e1..59487592 100644 --- a/public/index.html +++ b/public/index.html @@ -13,29 +13,10 @@ #app-root { display:flex; height:100%; } /* Remove focus state on inputs */ - input:focus { + input:focus, + textarea:focus { outline: 0; } - - /* These styles are for src/view/com/modals/WebModal */ - div[data-modal-overlay] { - position: fixed; - top: 0; - left: 0; - background: #0004; - width: 100vw; - height: 100vh; - } - div[data-modal-container] { - position: fixed; - top: 20vh; - left: calc(50vw - 300px); - width: 600px; - padding: 20px; - background: #fff; - border-radius: 10px; - box-shadow: 0 5px 10px #0005; - } </style> </head> <body> diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index 64e75328..2f30a1cf 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<PasteInputRef>(null) + const textInput = useRef<TextInputRef>(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<string[]>([]) - // 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<UserAutocompleteViewModel>( () => 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({ <KeyboardAvoidingView testID="composePostView" behavior={Platform.OS === 'ios' ? 'padding' : 'height'} - style={[pal.view, styles.outer]}> + style={styles.outer}> <TouchableWithoutFeedback onPressIn={onPressContainer}> <SafeAreaView style={s.flex1}> <View style={styles.topbar}> @@ -396,22 +387,19 @@ export const ComposePost = observer(function ComposePost({ avatar={store.me.avatar} size={50} /> - <PasteInput + <TextInput testID="composerTextInput" - ref={textInput} - multiline - scrollEnabled + innerRef={textInput} onChangeText={(str: string) => onChangeText(str)} onPaste={onPaste} placeholder={selectTextInputPlaceholder} - placeholderTextColor={pal.colors.textLight} style={[ pal.text, styles.textInput, styles.textInputFormatting, ]}> {textDecorated} - </PasteInput> + </TextInput> </View> <SelectedPhoto selectedPhotos={selectedPhotos} @@ -450,31 +438,7 @@ export const ComposePost = observer(function ComposePost({ /> </TouchableOpacity> <View style={s.flex1} /> - <Text style={[s.mr10, {color: progressColor}]}> - {MAX_TEXT_LENGTH - text.length} - </Text> - <View> - {text.length > DANGER_TEXT_LENGTH ? ( - <ProgressPie - size={30} - borderWidth={4} - borderColor={progressColor} - color={progressColor} - progress={Math.min( - (text.length - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, - 1, - )} - /> - ) : ( - <ProgressCircle - size={30} - borderWidth={1} - borderColor={colors.gray2} - color={progressColor} - progress={text.length / MAX_TEXT_LENGTH} - /> - )} - </View> + <CharProgress count={text.length} /> </View> <Autocomplete active={autocompleteView.isActive} @@ -504,7 +468,6 @@ const styles = StyleSheet.create({ flexDirection: 'column', flex: 1, padding: 15, - paddingBottom: Platform.OS === 'ios' ? 0 : 50, height: '100%', }, topbar: { diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx new file mode 100644 index 00000000..d4093064 --- /dev/null +++ b/src/view/com/composer/char-progress/CharProgress.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import {View} from 'react-native' +import {Text} from '../../util/text/Text' +// @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 {s, colors} 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 ( + <> + <Text style={[s.mr10, {color: progressColor}]}> + {MAX_TEXT_LENGTH - count} + </Text> + <View> + {count > DANGER_TEXT_LENGTH ? ( + <ProgressPie + size={30} + borderWidth={4} + borderColor={progressColor} + color={progressColor} + progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)} + /> + ) : ( + <ProgressCircle + size={30} + borderWidth={1} + borderColor={colors.gray2} + color={progressColor} + progress={count / MAX_TEXT_LENGTH} + /> + )} + </View> + </> + ) +} 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 00000000..6bdcc139 --- /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 ( + <> + <Text style={[s.mr10, {color: progressColor}]}> + {MAX_TEXT_LENGTH - count} + </Text> + <View> + { + null /* TODO count > DANGER_TEXT_LENGTH ? ( + <ProgressPie + size={30} + borderWidth={4} + borderColor={progressColor} + color={progressColor} + progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)} + /> + ) : ( + <ProgressCircle + size={30} + borderWidth={1} + borderColor={colors.gray2} + color={progressColor} + progress={count / MAX_TEXT_LENGTH} + /> + )*/ + } + </View> + </> + ) +} 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 00000000..3c5dacf8 --- /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<TextInputRef> + placeholder: string + style: StyleProp<TextStyle> + onChangeText: (str: string) => void + onPaste: (err: string | undefined, uris: string[]) => void +} + +export function TextInput({ + testID, + innerRef, + placeholder, + style, + onChangeText, + onPaste, + children, +}: React.PropsWithChildren<TextInputProps>) { + const pal = usePalette('default') + const onPasteInner = (err: string | undefined, files: PastedFile[]) => { + if (err) { + onPaste(err, []) + } else { + onPaste( + undefined, + files.map(f => f.uri), + ) + } + } + return ( + <PasteInput + testID={testID} + ref={innerRef} + multiline + scrollEnabled + onChangeText={(str: string) => onChangeText(str)} + onPaste={onPasteInner} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + style={style}> + {children} + </PasteInput> + ) +} 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 00000000..6960bf7a --- /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<TextInputRef> + placeholder: string + style: StyleProp<TextStyle> + onChangeText: (str: string) => void + onPaste: (err: string | undefined, uris: string[]) => void +} + +export function TextInput({ + testID, + innerRef, + placeholder, + style, + onChangeText, + children, +}: React.PropsWithChildren<TextInputProps>) { + const pal = usePalette('default') + style = addStyle(style, styles.input) + return ( + <RNTextInput + testID={testID} + ref={innerRef} + multiline + scrollEnabled + onChangeText={(str: string) => onChangeText(str)} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + style={style}> + {children} + </RNTextInput> + ) +} + +const styles = StyleSheet.create({ + input: { + minHeight: 140, + }, +}) diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx index a19a4704..c93931ab 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 00000000..63904009 --- /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 <View /> + } + + return ( + <View style={styles.mask}> + <View style={[styles.container, pal.view]}> + <ComposePost + replyTo={replyTo} + imagesOpen={imagesOpen} + onPost={onPost} + onClose={onClose} + /> + </View> + </View> + ) + }, +) + +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/left-column.tsx b/src/view/shell/web/DesktopLeftColumn.tsx similarity index 100% rename from src/view/shell/web/left-column.tsx rename to src/view/shell/web/DesktopLeftColumn.tsx diff --git a/src/view/shell/web/right-column.tsx b/src/view/shell/web/DesktopRightColumn.tsx similarity index 100% rename from src/view/shell/web/right-column.tsx rename to src/view/shell/web/DesktopRightColumn.tsx diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx index a4232eab..0eb5cf75 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(() => { ))} <DesktopLeftColumn /> <DesktopRightColumn /> + <Composer + active={store.shell.isComposerActive} + onClose={() => store.shell.closeComposer()} + winHeight={0} + replyTo={store.shell.composerOpts?.replyTo} + imagesOpen={store.shell.composerOpts?.imagesOpen} + onPost={store.shell.composerOpts?.onPost} + /> <Modal /> <Lightbox /> </View>