Implement basic web composer
parent
5961c26800
commit
99360f7bd9
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -48,9 +48,6 @@ export const Composer = observer(
|
|||
],
|
||||
}
|
||||
|
||||
// events
|
||||
// =
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue