Implement basic web composer

zio/stable
Paul Frazee 2023-01-27 00:16:07 -06:00
parent 5961c26800
commit 99360f7bd9
11 changed files with 282 additions and 82 deletions

View File

@ -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>

View File

@ -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: {

View File

@ -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>
</>
)
}

View File

@ -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>
</>
)
}

View File

@ -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>
)
}

View File

@ -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,
},
})

View File

@ -48,9 +48,6 @@ export const Composer = observer(
],
}
// events
// =
// rendering
// =

View File

@ -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,
},
})

View File

@ -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>