Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement basic web composer

+282 -82
+2 -21
public/index.html
··· 13 13 #app-root { display:flex; height:100%; } 14 14 15 15 /* Remove focus state on inputs */ 16 - input:focus { 16 + input:focus, 17 + textarea:focus { 17 18 outline: 0; 18 - } 19 - 20 - /* These styles are for src/view/com/modals/WebModal */ 21 - div[data-modal-overlay] { 22 - position: fixed; 23 - top: 0; 24 - left: 0; 25 - background: #0004; 26 - width: 100vw; 27 - height: 100vh; 28 - } 29 - div[data-modal-container] { 30 - position: fixed; 31 - top: 20vh; 32 - left: calc(50vw - 300px); 33 - width: 600px; 34 - padding: 20px; 35 - background: #fff; 36 - border-radius: 10px; 37 - box-shadow: 0 5px 10px #0005; 38 19 } 39 20 </style> 40 21 </head>
+19 -56
src/view/com/composer/ComposePost.tsx
··· 11 11 TouchableWithoutFeedback, 12 12 View, 13 13 } from 'react-native' 14 - import PasteInput, { 15 - PastedFile, 16 - PasteInputRef, 17 - } from '@mattermost/react-native-paste-input' 18 14 import LinearGradient from 'react-native-linear-gradient' 19 15 import { 20 16 FontAwesomeIcon, 21 17 FontAwesomeIconStyle, 22 18 } from '@fortawesome/react-native-fontawesome' 23 - import {useAnalytics} from '@segment/analytics-react-native' 19 + // import {useAnalytics} from '@segment/analytics-react-native' TODO 24 20 import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' 25 21 import {Autocomplete} from './Autocomplete' 26 22 import {ExternalEmbed} from './ExternalEmbed' 27 23 import {Text} from '../util/text/Text' 28 24 import * as Toast from '../util/Toast' 29 - // @ts-ignore no type definition -prf 30 - import ProgressCircle from 'react-native-progress/Circle' 31 - // @ts-ignore no type definition -prf 32 - import ProgressPie from 'react-native-progress/Pie' 25 + import {TextInput, TextInputRef} from './text-input/TextInput' 26 + import {CharProgress} from './char-progress/CharProgress' 33 27 import {TextLink} from '../util/Link' 34 28 import {UserAvatar} from '../util/UserAvatar' 35 29 import {useStores} from '../../../state' ··· 49 43 import {usePalette} from '../../lib/hooks/usePalette' 50 44 51 45 const MAX_TEXT_LENGTH = 256 52 - const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH 53 46 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} 54 47 55 48 export const ComposePost = observer(function ComposePost({ ··· 63 56 onPost?: ComposerOpts['onPost'] 64 57 onClose: () => void 65 58 }) { 66 - const {track} = useAnalytics() 59 + // const {track} = useAnalytics() TODO 67 60 const pal = usePalette('default') 68 61 const store = useStores() 69 - const textInput = useRef<PasteInputRef>(null) 62 + const textInput = useRef<TextInputRef>(null) 70 63 const [isProcessing, setIsProcessing] = useState(false) 71 64 const [processingState, setProcessingState] = useState('') 72 65 const [error, setError] = useState('') ··· 80 73 ) 81 74 const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]) 82 75 83 - // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment 84 76 const autocompleteView = React.useMemo<UserAutocompleteViewModel>( 85 77 () => new UserAutocompleteViewModel(store), 86 78 [store], ··· 219 211 } 220 212 } 221 213 } 222 - const onPaste = async (err: string | undefined, files: PastedFile[]) => { 214 + const onPaste = async (err: string | undefined, uris: string[]) => { 223 215 if (err) { 224 216 return setError(cleanError(err)) 225 217 } 226 218 if (selectedPhotos.length >= 4) { 227 219 return 228 220 } 229 - const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName)) 230 - if (!imgFile) { 231 - return 221 + const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri)) 222 + if (imgUri) { 223 + const finalImgPath = await cropPhoto(imgUri) 224 + onSelectPhotos([...selectedPhotos, finalImgPath]) 232 225 } 233 - const finalImgPath = await cropPhoto(imgFile.uri) 234 - onSelectPhotos([...selectedPhotos, finalImgPath]) 235 226 } 236 227 const onPressCancel = () => hackfixOnClose() 237 228 const onPressPublish = async () => { ··· 257 248 autocompleteView.knownHandles, 258 249 setProcessingState, 259 250 ) 260 - track('Create Post', { 261 - imageCount: selectedPhotos.length, 262 - }) 251 + // TODO 252 + // track('Create Post', { 253 + // imageCount: selectedPhotos.length, 254 + // }) 263 255 } catch (e: any) { 264 256 setError(cleanError(e.message)) 265 257 setIsProcessing(false) ··· 276 268 } 277 269 278 270 const canPost = text.length <= MAX_TEXT_LENGTH 279 - const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined 280 271 281 272 const selectTextInputLayout = 282 273 selectedPhotos.length !== 0 ··· 311 302 <KeyboardAvoidingView 312 303 testID="composePostView" 313 304 behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 314 - style={[pal.view, styles.outer]}> 305 + style={styles.outer}> 315 306 <TouchableWithoutFeedback onPressIn={onPressContainer}> 316 307 <SafeAreaView style={s.flex1}> 317 308 <View style={styles.topbar}> ··· 396 387 avatar={store.me.avatar} 397 388 size={50} 398 389 /> 399 - <PasteInput 390 + <TextInput 400 391 testID="composerTextInput" 401 - ref={textInput} 402 - multiline 403 - scrollEnabled 392 + innerRef={textInput} 404 393 onChangeText={(str: string) => onChangeText(str)} 405 394 onPaste={onPaste} 406 395 placeholder={selectTextInputPlaceholder} 407 - placeholderTextColor={pal.colors.textLight} 408 396 style={[ 409 397 pal.text, 410 398 styles.textInput, 411 399 styles.textInputFormatting, 412 400 ]}> 413 401 {textDecorated} 414 - </PasteInput> 402 + </TextInput> 415 403 </View> 416 404 <SelectedPhoto 417 405 selectedPhotos={selectedPhotos} ··· 450 438 /> 451 439 </TouchableOpacity> 452 440 <View style={s.flex1} /> 453 - <Text style={[s.mr10, {color: progressColor}]}> 454 - {MAX_TEXT_LENGTH - text.length} 455 - </Text> 456 - <View> 457 - {text.length > DANGER_TEXT_LENGTH ? ( 458 - <ProgressPie 459 - size={30} 460 - borderWidth={4} 461 - borderColor={progressColor} 462 - color={progressColor} 463 - progress={Math.min( 464 - (text.length - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 465 - 1, 466 - )} 467 - /> 468 - ) : ( 469 - <ProgressCircle 470 - size={30} 471 - borderWidth={1} 472 - borderColor={colors.gray2} 473 - color={progressColor} 474 - progress={text.length / MAX_TEXT_LENGTH} 475 - /> 476 - )} 477 - </View> 441 + <CharProgress count={text.length} /> 478 442 </View> 479 443 <Autocomplete 480 444 active={autocompleteView.isActive} ··· 504 468 flexDirection: 'column', 505 469 flex: 1, 506 470 padding: 15, 507 - paddingBottom: Platform.OS === 'ios' ? 0 : 50, 508 471 height: '100%', 509 472 }, 510 473 topbar: {
+41
src/view/com/composer/char-progress/CharProgress.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {Text} from '../../util/text/Text' 4 + // @ts-ignore no type definition -prf 5 + import ProgressCircle from 'react-native-progress/Circle' 6 + // @ts-ignore no type definition -prf 7 + import ProgressPie from 'react-native-progress/Pie' 8 + import {s, colors} from '../../../lib/styles' 9 + 10 + const MAX_TEXT_LENGTH = 256 11 + const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH 12 + 13 + export function CharProgress({count}: {count: number}) { 14 + const progressColor = count > DANGER_TEXT_LENGTH ? '#e60000' : undefined 15 + return ( 16 + <> 17 + <Text style={[s.mr10, {color: progressColor}]}> 18 + {MAX_TEXT_LENGTH - count} 19 + </Text> 20 + <View> 21 + {count > DANGER_TEXT_LENGTH ? ( 22 + <ProgressPie 23 + size={30} 24 + borderWidth={4} 25 + borderColor={progressColor} 26 + color={progressColor} 27 + progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)} 28 + /> 29 + ) : ( 30 + <ProgressCircle 31 + size={30} 32 + borderWidth={1} 33 + borderColor={colors.gray2} 34 + color={progressColor} 35 + progress={count / MAX_TEXT_LENGTH} 36 + /> 37 + )} 38 + </View> 39 + </> 40 + ) 41 + }
+39
src/view/com/composer/char-progress/CharProgress.web.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {Text} from '../util/text/Text' 4 + import {s} from '../../lib/styles' 5 + 6 + const MAX_TEXT_LENGTH = 256 7 + const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH 8 + 9 + export function CharProgress({count}: {count: number}) { 10 + const progressColor = count > DANGER_TEXT_LENGTH ? '#e60000' : undefined 11 + return ( 12 + <> 13 + <Text style={[s.mr10, {color: progressColor}]}> 14 + {MAX_TEXT_LENGTH - count} 15 + </Text> 16 + <View> 17 + { 18 + null /* TODO count > DANGER_TEXT_LENGTH ? ( 19 + <ProgressPie 20 + size={30} 21 + borderWidth={4} 22 + borderColor={progressColor} 23 + color={progressColor} 24 + progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)} 25 + /> 26 + ) : ( 27 + <ProgressCircle 28 + size={30} 29 + borderWidth={1} 30 + borderColor={colors.gray2} 31 + color={progressColor} 32 + progress={count / MAX_TEXT_LENGTH} 33 + /> 34 + )*/ 35 + } 36 + </View> 37 + </> 38 + ) 39 + }
+54
src/view/com/composer/text-input/TextInput.tsx
··· 1 + import React from 'react' 2 + import {StyleProp, TextStyle} from 'react-native' 3 + import PasteInput, { 4 + PastedFile, 5 + PasteInputRef, 6 + } from '@mattermost/react-native-paste-input' 7 + import {usePalette} from '../../../lib/hooks/usePalette' 8 + 9 + export type TextInputRef = PasteInputRef 10 + 11 + interface TextInputProps { 12 + testID: string 13 + innerRef: React.Ref<TextInputRef> 14 + placeholder: string 15 + style: StyleProp<TextStyle> 16 + onChangeText: (str: string) => void 17 + onPaste: (err: string | undefined, uris: string[]) => void 18 + } 19 + 20 + export function TextInput({ 21 + testID, 22 + innerRef, 23 + placeholder, 24 + style, 25 + onChangeText, 26 + onPaste, 27 + children, 28 + }: React.PropsWithChildren<TextInputProps>) { 29 + const pal = usePalette('default') 30 + const onPasteInner = (err: string | undefined, files: PastedFile[]) => { 31 + if (err) { 32 + onPaste(err, []) 33 + } else { 34 + onPaste( 35 + undefined, 36 + files.map(f => f.uri), 37 + ) 38 + } 39 + } 40 + return ( 41 + <PasteInput 42 + testID={testID} 43 + ref={innerRef} 44 + multiline 45 + scrollEnabled 46 + onChangeText={(str: string) => onChangeText(str)} 47 + onPaste={onPasteInner} 48 + placeholder={placeholder} 49 + placeholderTextColor={pal.colors.textLight} 50 + style={style}> 51 + {children} 52 + </PasteInput> 53 + ) 54 + }
+51
src/view/com/composer/text-input/TextInput.web.tsx
··· 1 + import React from 'react' 2 + import { 3 + StyleProp, 4 + StyleSheet, 5 + TextInput as RNTextInput, 6 + TextStyle, 7 + } from 'react-native' 8 + import {usePalette} from '../../lib/hooks/usePalette' 9 + import {addStyle} from '../../lib/addStyle' 10 + 11 + export type TextInputRef = RNTextInput 12 + 13 + interface TextInputProps { 14 + testID: string 15 + innerRef: React.Ref<TextInputRef> 16 + placeholder: string 17 + style: StyleProp<TextStyle> 18 + onChangeText: (str: string) => void 19 + onPaste: (err: string | undefined, uris: string[]) => void 20 + } 21 + 22 + export function TextInput({ 23 + testID, 24 + innerRef, 25 + placeholder, 26 + style, 27 + onChangeText, 28 + children, 29 + }: React.PropsWithChildren<TextInputProps>) { 30 + const pal = usePalette('default') 31 + style = addStyle(style, styles.input) 32 + return ( 33 + <RNTextInput 34 + testID={testID} 35 + ref={innerRef} 36 + multiline 37 + scrollEnabled 38 + onChangeText={(str: string) => onChangeText(str)} 39 + placeholder={placeholder} 40 + placeholderTextColor={pal.colors.textLight} 41 + style={style}> 42 + {children} 43 + </RNTextInput> 44 + ) 45 + } 46 + 47 + const styles = StyleSheet.create({ 48 + input: { 49 + minHeight: 140, 50 + }, 51 + })
-3
src/view/shell/mobile/Composer.tsx
··· 48 48 ], 49 49 } 50 50 51 - // events 52 - // = 53 - 54 51 // rendering 55 52 // = 56 53
+65
src/view/shell/web/Composer.tsx
··· 1 + import React from 'react' 2 + import {observer} from 'mobx-react-lite' 3 + import {StyleSheet, View} from 'react-native' 4 + import {ComposePost} from '../../com/composer/ComposePost' 5 + import {ComposerOpts} from '../../../state/models/shell-ui' 6 + import {usePalette} from '../../lib/hooks/usePalette' 7 + 8 + export const Composer = observer( 9 + ({ 10 + active, 11 + replyTo, 12 + imagesOpen, 13 + onPost, 14 + onClose, 15 + }: { 16 + active: boolean 17 + winHeight: number 18 + replyTo?: ComposerOpts['replyTo'] 19 + imagesOpen?: ComposerOpts['imagesOpen'] 20 + onPost?: ComposerOpts['onPost'] 21 + onClose: () => void 22 + }) => { 23 + const pal = usePalette('default') 24 + 25 + // rendering 26 + // = 27 + 28 + if (!active) { 29 + return <View /> 30 + } 31 + 32 + return ( 33 + <View style={styles.mask}> 34 + <View style={[styles.container, pal.view]}> 35 + <ComposePost 36 + replyTo={replyTo} 37 + imagesOpen={imagesOpen} 38 + onPost={onPost} 39 + onClose={onClose} 40 + /> 41 + </View> 42 + </View> 43 + ) 44 + }, 45 + ) 46 + 47 + const styles = StyleSheet.create({ 48 + mask: { 49 + position: 'absolute', 50 + top: 0, 51 + left: 0, 52 + width: '100%', 53 + height: '100%', 54 + backgroundColor: '#000c', 55 + alignItems: 'center', 56 + justifyContent: 'center', 57 + }, 58 + container: { 59 + maxWidth: 600, 60 + width: '100%', 61 + paddingVertical: 0, 62 + paddingHorizontal: 2, 63 + borderRadius: 8, 64 + }, 65 + })
+11 -2
src/view/shell/web/index.tsx
··· 3 3 import {View, StyleSheet} from 'react-native' 4 4 import {useStores} from '../../../state' 5 5 import {match, MatchResult} from '../../routes' 6 - import {DesktopLeftColumn} from './left-column' 7 - import {DesktopRightColumn} from './right-column' 6 + import {DesktopLeftColumn} from './DesktopLeftColumn' 7 + import {DesktopRightColumn} from './DesktopRightColumn' 8 8 import {Onboard} from '../../screens/Onboard' 9 9 import {Login} from '../../screens/Login' 10 10 import {ErrorBoundary} from '../../com/util/ErrorBoundary' 11 11 import {Lightbox} from '../../com/lightbox/Lightbox' 12 12 import {Modal} from '../../com/modals/Modal' 13 + import {Composer} from './Composer' 13 14 import {usePalette} from '../../lib/hooks/usePalette' 14 15 import {s} from '../../lib/styles' 15 16 ··· 49 50 ))} 50 51 <DesktopLeftColumn /> 51 52 <DesktopRightColumn /> 53 + <Composer 54 + active={store.shell.isComposerActive} 55 + onClose={() => store.shell.closeComposer()} 56 + winHeight={0} 57 + replyTo={store.shell.composerOpts?.replyTo} 58 + imagesOpen={store.shell.composerOpts?.imagesOpen} 59 + onPost={store.shell.composerOpts?.onPost} 60 + /> 52 61 <Modal /> 53 62 <Lightbox /> 54 63 </View>
src/view/shell/web/left-column.tsx src/view/shell/web/DesktopLeftColumn.tsx
src/view/shell/web/right-column.tsx src/view/shell/web/DesktopRightColumn.tsx