Merge branch 'rnw' of github.com:bluesky-social/social-app into rnw

This commit is contained in:
Michael Staub 2023-02-23 16:34:25 -08:00
commit 693cbb9f18
35 changed files with 573 additions and 535 deletions

View file

@ -36,11 +36,18 @@ import {s, colors, gradients} from 'lib/styles'
import {cleanError} from 'lib/strings/errors'
import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {downloadAndResize} from 'lib/images'
import {PhotoCarouselPicker, cropPhoto} from './photos/PhotoCarouselPicker'
import {getImageDim, downloadAndResize} from 'lib/media/manip'
import {PhotoCarouselPicker} from './photos/PhotoCarouselPicker'
import {cropAndCompressFlow, pickImagesFlow} from '../../../lib/media/picker'
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
import {SelectedPhoto} from './SelectedPhoto'
import {usePalette} from 'lib/hooks/usePalette'
import {
POST_IMG_MAX_WIDTH,
POST_IMG_MAX_HEIGHT,
POST_IMG_MAX_SIZE,
} from 'lib/constants'
import {isWeb} from 'platform/detection'
const MAX_TEXT_LENGTH = 256
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
@ -61,7 +68,7 @@ export const ComposePost = observer(function ComposePost({
onPost?: ComposerOpts['onPost']
onClose: () => void
}) {
const {track, screen} = useAnalytics()
const {track} = useAnalytics()
const pal = usePalette('default')
const store = useStores()
const textInput = useRef<TextInputRef>(null)
@ -174,12 +181,24 @@ export const ComposePost = observer(function ComposePost({
const onPressContainer = () => {
textInput.current?.focus()
}
const onPressSelectPhotos = () => {
const onPressSelectPhotos = async () => {
track('ComposePost:SelectPhotos')
if (isSelectingPhotos) {
setIsSelectingPhotos(false)
} else if (selectedPhotos.length < 4) {
setIsSelectingPhotos(true)
if (isWeb) {
if (selectedPhotos.length < 4) {
const images = await pickImagesFlow(
store,
4 - selectedPhotos.length,
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
POST_IMG_MAX_SIZE,
)
setSelectedPhotos([...selectedPhotos, ...images])
}
} else {
if (isSelectingPhotos) {
setIsSelectingPhotos(false)
} else if (selectedPhotos.length < 4) {
setIsSelectingPhotos(true)
}
}
}
const onSelectPhotos = (photos: string[]) => {
@ -220,7 +239,19 @@ export const ComposePost = observer(function ComposePost({
}
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
if (imgUri) {
const finalImgPath = await cropPhoto(store, imgUri)
let imgDim
try {
imgDim = await getImageDim(imgUri)
} catch (e) {
imgDim = {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}
}
const finalImgPath = await cropAndCompressFlow(
store,
imgUri,
imgDim,
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
POST_IMG_MAX_SIZE,
)
onSelectPhotos([...selectedPhotos, finalImgPath])
}
}

View file

@ -9,57 +9,24 @@ import {
openPicker,
openCamera,
openCropper,
} from '../../util/images/image-crop-picker/ImageCropPicker'
cropAndCompressFlow,
} from '../../../../lib/media/picker'
import {
UserLocalPhotosModel,
PhotoIdentifier,
} from 'state/models/user-local-photos'
import {
compressIfNeeded,
moveToPremanantPath,
scaleDownDimensions,
} from 'lib/images'
import {compressIfNeeded} from 'lib/media/manip'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores, RootStoreModel} from 'state/index'
import {useStores} from 'state/index'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
} from 'lib/permissions'
const MAX_WIDTH = 2000
const MAX_HEIGHT = 2000
const MAX_SIZE = 1000000
const IMAGE_PARAMS = {
width: 2000,
height: 2000,
freeStyleCropEnabled: true,
}
export async function cropPhoto(
store: RootStoreModel,
path: string,
imgWidth = MAX_WIDTH,
imgHeight = MAX_HEIGHT,
) {
// choose target dimensions based on the original
// this causes the photo cropper to start with the full image "selected"
const {width, height} = scaleDownDimensions(
{width: imgWidth, height: imgHeight},
{width: MAX_WIDTH, height: MAX_HEIGHT},
)
const cropperRes = await openCropper(store, {
mediaType: 'photo',
path,
freeStyleCropEnabled: true,
width,
height,
})
const img = await compressIfNeeded(cropperRes, MAX_SIZE)
const permanentPath = await moveToPremanantPath(img.path)
return permanentPath
}
import {
POST_IMG_MAX_WIDTH,
POST_IMG_MAX_HEIGHT,
POST_IMG_MAX_SIZE,
} from 'lib/constants'
export const PhotoCarouselPicker = ({
selectedPhotos,
@ -92,9 +59,11 @@ export const PhotoCarouselPicker = ({
}
const cameraRes = await openCamera(store, {
mediaType: 'photo',
...IMAGE_PARAMS,
width: POST_IMG_MAX_WIDTH,
height: POST_IMG_MAX_HEIGHT,
freeStyleCropEnabled: true,
})
const img = await compressIfNeeded(cameraRes, MAX_SIZE)
const img = await compressIfNeeded(cameraRes, POST_IMG_MAX_SIZE)
onSelectPhotos([...selectedPhotos, img.path])
} catch (err: any) {
// ignore
@ -106,11 +75,15 @@ export const PhotoCarouselPicker = ({
async (item: PhotoIdentifier) => {
track('PhotoCarouselPicker:PhotoSelected')
try {
const imgPath = await cropPhoto(
const imgPath = await cropAndCompressFlow(
store,
item.node.image.uri,
item.node.image.width,
item.node.image.height,
{
width: item.node.image.width,
height: item.node.image.height,
},
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
POST_IMG_MAX_SIZE,
)
onSelectPhotos([...selectedPhotos, imgPath])
} catch (err: any) {
@ -132,24 +105,16 @@ export const PhotoCarouselPicker = ({
mediaType: 'photo',
})
const result = []
for (const image of items) {
// choose target dimensions based on the original
// this causes the photo cropper to start with the full image "selected"
const {width, height} = scaleDownDimensions(
{width: image.width, height: image.height},
{width: MAX_WIDTH, height: MAX_HEIGHT},
result.push(
await cropAndCompressFlow(
store,
image.path,
image,
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
POST_IMG_MAX_SIZE,
),
)
const cropperRes = await openCropper(store, {
mediaType: 'photo',
path: image.path,
...IMAGE_PARAMS,
width,
height,
})
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
const permanentPath = await moveToPremanantPath(finalImg.path)
result.push(permanentPath)
}
onSelectPhotos([...selectedPhotos, ...result])
}, [track, store, selectedPhotos, onSelectPhotos])

View file

@ -1,158 +1,10 @@
import React, {useCallback} from 'react'
import {StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {
openPicker,
openCamera,
openCropper,
} from '../../util/images/image-crop-picker/ImageCropPicker'
import {compressIfNeeded, scaleDownDimensions} from 'lib/images'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores, RootStoreModel} from 'state/index'
import React from 'react'
const MAX_WIDTH = 1000
const MAX_HEIGHT = 1000
const MAX_SIZE = 300000
// Not used on Web
const IMAGE_PARAMS = {
width: 1000,
height: 1000,
freeStyleCropEnabled: true,
}
export async function cropPhoto(
store: RootStoreModel,
path: string,
imgWidth = MAX_WIDTH,
imgHeight = MAX_HEIGHT,
) {
// choose target dimensions based on the original
// this causes the photo cropper to start with the full image "selected"
const {width, height} = scaleDownDimensions(
{width: imgWidth, height: imgHeight},
{width: MAX_WIDTH, height: MAX_HEIGHT},
)
const cropperRes = await openCropper(store, {
mediaType: 'photo',
path,
freeStyleCropEnabled: true,
width,
height,
})
const img = await compressIfNeeded(cropperRes, MAX_SIZE)
return img.path
}
export const PhotoCarouselPicker = ({
selectedPhotos,
onSelectPhotos,
}: {
export const PhotoCarouselPicker = (_opts: {
selectedPhotos: string[]
onSelectPhotos: (v: string[]) => void
}) => {
const pal = usePalette('default')
const store = useStores()
const handleOpenCamera = useCallback(async () => {
try {
const cameraRes = await openCamera(store, {
mediaType: 'photo',
...IMAGE_PARAMS,
})
const img = await compressIfNeeded(cameraRes, MAX_SIZE)
onSelectPhotos([...selectedPhotos, img.path])
} catch (err: any) {
// ignore
store.log.warn('Error using camera', err)
}
}, [store, selectedPhotos, onSelectPhotos])
const handleOpenGallery = useCallback(() => {
openPicker(store, {
multiple: true,
maxFiles: 4 - selectedPhotos.length,
mediaType: 'photo',
}).then(async items => {
const result = []
for (const image of items) {
// choose target dimensions based on the original
// this causes the photo cropper to start with the full image "selected"
const {width, height} = scaleDownDimensions(
{width: image.width, height: image.height},
{width: MAX_WIDTH, height: MAX_HEIGHT},
)
const cropperRes = await openCropper(store, {
mediaType: 'photo',
path: image.path,
freeStyleCropEnabled: true,
width,
height,
})
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
result.push(finalImg.path)
}
onSelectPhotos([...selectedPhotos, ...result])
})
}, [store, selectedPhotos, onSelectPhotos])
return (
<ScrollView
testID="photoCarouselPickerView"
horizontal
style={[pal.view, styles.photosContainer]}
keyboardShouldPersistTaps="always"
showsHorizontalScrollIndicator={false}>
<TouchableOpacity
testID="openCameraButton"
style={[styles.galleryButton, pal.border, styles.photo]}
onPress={handleOpenCamera}>
<FontAwesomeIcon
icon="camera"
size={24}
style={pal.link as FontAwesomeIconStyle}
/>
</TouchableOpacity>
<TouchableOpacity
testID="openGalleryButton"
style={[styles.galleryButton, pal.border, styles.photo]}
onPress={handleOpenGallery}>
<FontAwesomeIcon
icon="image"
style={pal.link as FontAwesomeIconStyle}
size={24}
/>
</TouchableOpacity>
</ScrollView>
)
return <></>
}
const styles = StyleSheet.create({
photosContainer: {
width: '100%',
maxHeight: 96,
padding: 8,
overflow: 'hidden',
},
galleryButton: {
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
photoButton: {
width: 75,
height: 75,
marginRight: 8,
borderWidth: 1,
borderRadius: 16,
},
photo: {
width: 75,
height: 75,
marginRight: 8,
borderRadius: 16,
},
})

View file

@ -17,7 +17,7 @@ import {
VirtualizedList,
ModalProps,
} from 'react-native'
import {Modal} from '../../modals/Modal'
import {ModalsContainer} from '../../modals/Modal'
import ImageItem from './components/ImageItem/ImageItem'
import ImageDefaultHeader from './components/ImageDefaultHeader'
@ -98,7 +98,7 @@ function ImageViewing({
return (
<View style={styles.screen} onLayout={onLayout}>
<Modal />
<ModalsContainer />
<View style={[styles.container, {opacity, backgroundColor}]}>
<Animated.View style={[styles.header, {transform: headerTransform}]}>
{typeof HeaderComponent !== 'undefined' ? (

View file

@ -4,7 +4,7 @@ import {observer} from 'mobx-react-lite'
import ImageView from './ImageViewing'
import {useStores} from 'state/index'
import * as models from 'state/models/shell-ui'
import {saveImageModal} from 'lib/images'
import {saveImageModal} from 'lib/media/manip'
import {ImageSource} from './ImageViewing/@types'
export const Lightbox = observer(function Lightbox() {

View file

@ -25,7 +25,6 @@ import {makeValidHandle, createFullHandle} from 'lib/strings/handles'
import {toNiceDomain} from 'lib/strings/url-helpers'
import {useStores, DEFAULT_SERVICE} from 'state/index'
import {ServiceDescription} from 'state/models/session'
import {ServerInputModal} from 'state/models/shell-ui'
import {usePalette} from 'lib/hooks/usePalette'
import {cleanError} from 'lib/strings/errors'
@ -84,7 +83,11 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
const onPressRetryConnect = () => setRetryDescribeTrigger({})
const onPressSelectService = () => {
store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl))
store.shell.openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
Keyboard.dismiss()
}

View file

@ -279,7 +279,11 @@ const LoginForm = ({
const [password, setPassword] = useState<string>('')
const onPressSelectService = () => {
store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl))
store.shell.openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
Keyboard.dismiss()
track('Signin:PressedSelectService')
}

View file

@ -8,7 +8,7 @@ import {
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {ScrollView, TextInput} from './util'
import {PickedMedia} from '../util/images/image-crop-picker/ImageCropPicker'
import {PickedMedia} from '../../../lib/media/picker'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from 'state/index'
@ -16,7 +16,7 @@ import {ProfileViewModel} from 'state/models/profile-view'
import {s, colors, gradients} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers'
import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
import {compressIfNeeded} from 'lib/images'
import {compressIfNeeded} from 'lib/media/manip'
import {UserBanner} from '../util/UserBanner'
import {UserAvatar} from '../util/UserAvatar'
import {usePalette} from 'lib/hooks/usePalette'

View file

@ -5,8 +5,6 @@ import BottomSheet from '@gorhom/bottom-sheet'
import {useStores} from 'state/index'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as models from 'state/models/shell-ui'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
@ -18,7 +16,7 @@ import {StyleSheet} from 'react-native'
const CLOSED_SNAPPOINTS = ['10%']
export const Modal = observer(function Modal() {
export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores()
const bottomSheetRef = useRef<BottomSheet>(null)
const pal = usePalette('default')
@ -32,52 +30,37 @@ export const Modal = observer(function Modal() {
store.shell.closeModal()
}
const activeModal = React.useMemo(
() => store.shell.activeModals.at(-1),
[store.shell.activeModals],
)
useEffect(() => {
if (store.shell.isModalActive) {
bottomSheetRef.current?.expand()
} else {
bottomSheetRef.current?.close()
}
}, [store.shell.isModalActive, bottomSheetRef, store.shell.activeModal?.name])
}, [store.shell.isModalActive, bottomSheetRef, activeModal?.name])
let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS
let element
if (store.shell.activeModal?.name === 'confirm') {
if (activeModal?.name === 'confirm') {
snapPoints = ConfirmModal.snapPoints
element = (
<ConfirmModal.Component
{...(store.shell.activeModal as models.ConfirmModal)}
/>
)
} else if (store.shell.activeModal?.name === 'edit-profile') {
element = <ConfirmModal.Component {...activeModal} />
} else if (activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints
element = (
<EditProfileModal.Component
{...(store.shell.activeModal as models.EditProfileModal)}
/>
)
} else if (store.shell.activeModal?.name === 'server-input') {
element = <EditProfileModal.Component {...activeModal} />
} else if (activeModal?.name === 'server-input') {
snapPoints = ServerInputModal.snapPoints
element = (
<ServerInputModal.Component
{...(store.shell.activeModal as models.ServerInputModal)}
/>
)
} else if (store.shell.activeModal?.name === 'report-post') {
element = <ServerInputModal.Component {...activeModal} />
} else if (activeModal?.name === 'report-post') {
snapPoints = ReportPostModal.snapPoints
element = (
<ReportPostModal.Component
{...(store.shell.activeModal as models.ReportPostModal)}
/>
)
} else if (store.shell.activeModal?.name === 'report-account') {
element = <ReportPostModal.Component {...activeModal} />
} else if (activeModal?.name === 'report-account') {
snapPoints = ReportAccountModal.snapPoints
element = (
<ReportAccountModal.Component
{...(store.shell.activeModal as models.ReportAccountModal)}
/>
)
} else if (store.shell.activeModal?.name === 'delete-account') {
element = <ReportAccountModal.Component {...activeModal} />
} else if (activeModal?.name === 'delete-account') {
snapPoints = DeleteAccountModal.snapPoints
element = <DeleteAccountModal.Component />
} else {

View file

@ -3,8 +3,7 @@ import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import * as models from 'state/models/shell-ui'
import type {Modal as ModalIface} from 'state/models/shell-ui'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
@ -13,7 +12,23 @@ import * as ReportPostModal from './ReportPost'
import * as ReportAccountModal from './ReportAccount'
import * as CropImageModal from './crop-image/CropImage.web'
export const Modal = observer(function Modal() {
export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores()
if (!store.shell.isModalActive) {
return null
}
return (
<>
{store.shell.activeModals.map((modal, i) => (
<Modal key={`modal-${i}`} modal={modal} />
))}
</>
)
})
function Modal({modal}: {modal: ModalIface}) {
const store = useStores()
const pal = usePalette('default')
@ -21,7 +36,10 @@ export const Modal = observer(function Modal() {
return null
}
const onClose = () => {
const onPressMask = () => {
if (modal.name === 'crop-image') {
return // dont close on mask presses during crop
}
store.shell.closeModal()
}
const onInnerPress = () => {
@ -29,48 +47,24 @@ export const Modal = observer(function Modal() {
}
let element
if (store.shell.activeModal?.name === 'confirm') {
element = (
<ConfirmModal.Component
{...(store.shell.activeModal as models.ConfirmModal)}
/>
)
} else if (store.shell.activeModal?.name === 'edit-profile') {
element = (
<EditProfileModal.Component
{...(store.shell.activeModal as models.EditProfileModal)}
/>
)
} else if (store.shell.activeModal?.name === 'server-input') {
element = (
<ServerInputModal.Component
{...(store.shell.activeModal as models.ServerInputModal)}
/>
)
} else if (store.shell.activeModal?.name === 'report-post') {
element = (
<ReportPostModal.Component
{...(store.shell.activeModal as models.ReportPostModal)}
/>
)
} else if (store.shell.activeModal?.name === 'report-account') {
element = (
<ReportAccountModal.Component
{...(store.shell.activeModal as models.ReportAccountModal)}
/>
)
} else if (store.shell.activeModal?.name === 'crop-image') {
element = (
<CropImageModal.Component
{...(store.shell.activeModal as models.CropImageModal)}
/>
)
if (modal.name === 'confirm') {
element = <ConfirmModal.Component {...modal} />
} else if (modal.name === 'edit-profile') {
element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'server-input') {
element = <ServerInputModal.Component {...modal} />
} else if (modal.name === 'report-post') {
element = <ReportPostModal.Component {...modal} />
} else if (modal.name === 'report-account') {
element = <ReportAccountModal.Component {...modal} />
} else if (modal.name === 'crop-image') {
element = <CropImageModal.Component {...modal} />
} else {
return null
}
return (
<TouchableWithoutFeedback onPress={onClose}>
<TouchableWithoutFeedback onPress={onPressMask}>
<View style={styles.mask}>
<TouchableWithoutFeedback onPress={onInnerPress}>
<View style={[styles.container, pal.view]}>{element}</View>
@ -78,7 +72,7 @@ export const Modal = observer(function Modal() {
</View>
</TouchableWithoutFeedback>
)
})
}
const styles = StyleSheet.create({
mask: {

View file

@ -38,6 +38,7 @@ export function Component({
const pal = usePalette('default')
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
const [scale, setScale] = React.useState<number>(1)
const editorRef = React.useRef<ImageEditor>(null)
const doSetAs = (v: AspectRatio) => () => setAs(v)
@ -46,8 +47,20 @@ export function Component({
store.shell.closeModal()
}
const onPressDone = () => {
console.log('TODO')
onSelect(undefined) // TODO
const canvas = editorRef.current?.getImageScaledToCanvas()
if (canvas) {
const dataUri = canvas.toDataURL('image/jpeg')
onSelect({
mediaType: 'photo',
path: dataUri,
mime: 'image/jpeg',
size: Math.round((dataUri.length * 3) / 4), // very rough estimate
width: DIMS[as].width,
height: DIMS[as].height,
})
} else {
onSelect(undefined)
}
store.shell.closeModal()
}
@ -61,13 +74,15 @@ export function Component({
}
return (
<View>
<View style={[styles.cropper, cropperStyle]}>
<View style={[styles.cropper, pal.borderDark, cropperStyle]}>
<ImageEditor
ref={editorRef}
style={styles.imageEditor}
image={uri}
width={DIMS[as].width}
height={DIMS[as].height}
scale={scale}
border={0}
/>
</View>
<View style={styles.ctrls}>
@ -126,6 +141,9 @@ const styles = StyleSheet.create({
cropper: {
marginLeft: 'auto',
marginRight: 'auto',
borderWidth: 1,
borderRadius: 4,
overflow: 'hidden',
},
cropperSquare: {
width: 400,

View file

@ -15,11 +15,7 @@ import {
import {BlurView} from '../util/BlurView'
import {ProfileViewModel} from 'state/models/profile-view'
import {useStores} from 'state/index'
import {
EditProfileModal,
ReportAccountModal,
ProfileImageLightbox,
} from 'state/models/shell-ui'
import {ProfileImageLightbox} from 'state/models/shell-ui'
import {pluralize} from 'lib/strings/helpers'
import {toShareUrl} from 'lib/strings/url-helpers'
import {s, gradients} from 'lib/styles'
@ -65,7 +61,11 @@ export const ProfileHeader = observer(function ProfileHeader({
}
const onPressEditProfile = () => {
track('ProfileHeader:EditProfileButtonClicked')
store.shell.openModal(new EditProfileModal(view, onRefreshAll))
store.shell.openModal({
name: 'edit-profile',
profileView: view,
onUpdate: onRefreshAll,
})
}
const onPressFollowers = () => {
track('ProfileHeader:FollowersButtonClicked')
@ -101,7 +101,10 @@ export const ProfileHeader = observer(function ProfileHeader({
}
const onPressReportAccount = () => {
track('ProfileHeader:ReportAccountButtonClicked')
store.shell.openModal(new ReportAccountModal(view.did))
store.shell.openModal({
name: 'report-account',
did: view.did,
})
}
// loading

View file

@ -13,7 +13,7 @@ import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ImagesLightbox} from 'state/models/shell-ui'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {saveImageModal} from 'lib/images'
import {saveImageModal} from 'lib/media/manip'
import YoutubeEmbed from './YoutubeEmbed'
import ExternalLinkEmbed from './ExternalLinkEmbed'
import {getYoutubeVideoId} from 'lib/strings/url-helpers'

View file

@ -9,7 +9,7 @@ import {
openCropper,
openPicker,
PickedMedia,
} from './images/image-crop-picker/ImageCropPicker'
} from '../../../lib/media/picker'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
@ -18,6 +18,7 @@ import {useStores} from 'state/index'
import {colors, gradients} from 'lib/styles'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
export function UserAvatar({
size,
@ -58,7 +59,7 @@ export function UserAvatar({
)
const dropdownItems = [
{
!isWeb && {
label: 'Camera',
icon: 'camera' as IconProp,
onPress: async () => {

View file

@ -10,7 +10,7 @@ import {
openCropper,
openPicker,
PickedMedia,
} from './images/image-crop-picker/ImageCropPicker'
} from '../../../lib/media/picker'
import {useStores} from 'state/index'
import {
requestPhotoAccessIfNeeded,
@ -18,6 +18,7 @@ import {
} from 'lib/permissions'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
export function UserBanner({
banner,
@ -29,7 +30,7 @@ export function UserBanner({
const store = useStores()
const pal = usePalette('default')
const dropdownItems = [
{
!isWeb && {
label: 'Camera',
icon: 'camera' as IconProp,
onPress: async () => {

View file

@ -35,7 +35,7 @@ export const ViewHeader = observer(function ViewHeader({
canGoBack = store.nav.tab.canGoBack
}
if (isDesktopWeb) {
return undefined
return <></>
}
return (
<Container hideOnScroll={hideOnScroll || false}>

View file

@ -16,7 +16,6 @@ import {Button, ButtonType} from './Button'
import {colors} from 'lib/styles'
import {toShareUrl} from 'lib/strings/url-helpers'
import {useStores} from 'state/index'
import {ReportPostModal, ConfirmModal} from 'state/models/shell-ui'
import {TABS_ENABLED} from 'lib/build-flags'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
@ -28,6 +27,7 @@ export interface DropdownItem {
label: string
onPress: () => void
}
type MaybeDropdownItem = DropdownItem | false | undefined
export type DropdownButtonType = ButtonType | 'bare'
@ -44,7 +44,7 @@ export function DropdownButton({
}: {
type?: DropdownButtonType
style?: StyleProp<ViewStyle>
items: DropdownItem[]
items: MaybeDropdownItem[]
label?: string
menuWidth?: number
children?: React.ReactNode
@ -71,7 +71,12 @@ export function DropdownButton({
? pageX + width + rightOffset
: pageX + width - menuWidth
const newY = pageY + height + bottomOffset
createDropdownMenu(newX, newY, menuWidth, items)
createDropdownMenu(
newX,
newY,
menuWidth,
items.filter(v => !!v) as DropdownItem[],
)
},
)
}
@ -151,7 +156,11 @@ export function PostDropdownBtn({
icon: 'circle-exclamation',
label: 'Report post',
onPress() {
store.shell.openModal(new ReportPostModal(itemUri, itemCid))
store.shell.openModal({
name: 'report-post',
postUri: itemUri,
postCid: itemCid,
})
},
},
isAuthor
@ -159,13 +168,12 @@ export function PostDropdownBtn({
icon: ['far', 'trash-can'],
label: 'Delete post',
onPress() {
store.shell.openModal(
new ConfirmModal(
'Delete this post?',
'Are you sure? This can not be undone.',
onDeletePost,
),
)
store.shell.openModal({
name: 'confirm',
title: 'Delete this post?',
message: 'Are you sure? This can not be undone.',
onPressConfirm: onDeletePost,
})
},
}
: undefined,

View file

@ -1,92 +0,0 @@
import {
openPicker as openPickerFn,
openCamera as openCameraFn,
openCropper as openCropperFn,
ImageOrVideo,
} from 'react-native-image-crop-picker'
import {RootStoreModel} from 'state/index'
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
export type {PickedMedia} from './types'
/**
* NOTE
* These methods all include the RootStoreModel as the first param
* because the web versions require it. The signatures have to remain
* equivalent between the different forms, but the store param is not
* used here.
* -prf
*/
export async function openPicker(
_store: RootStoreModel,
opts: PickerOpts,
): Promise<PickedMedia[]> {
const mediaType = opts.mediaType || 'photo'
const items = await openPickerFn({
mediaType,
multiple: opts.multiple,
maxFiles: opts.maxFiles,
})
const toMedia = (item: ImageOrVideo) => ({
mediaType,
path: item.path,
mime: item.mime,
size: item.size,
width: item.width,
height: item.height,
})
if (Array.isArray(items)) {
return items.map(toMedia)
}
return [toMedia(items)]
}
export async function openCamera(
_store: RootStoreModel,
opts: CameraOpts,
): Promise<PickedMedia> {
const mediaType = opts.mediaType || 'photo'
const item = await openCameraFn({
mediaType,
width: opts.width,
height: opts.height,
freeStyleCropEnabled: opts.freeStyleCropEnabled,
cropperCircleOverlay: opts.cropperCircleOverlay,
cropping: true,
forceJpg: true, // ios only
compressImageQuality: 1.0,
})
return {
mediaType,
path: item.path,
mime: item.mime,
size: item.size,
width: item.width,
height: item.height,
}
}
export async function openCropper(
_store: RootStoreModel,
opts: CropperOpts,
): Promise<PickedMedia> {
const mediaType = opts.mediaType || 'photo'
const item = await openCropperFn({
path: opts.path,
mediaType: opts.mediaType || 'photo',
width: opts.width,
height: opts.height,
freeStyleCropEnabled: opts.freeStyleCropEnabled,
cropperCircleOverlay: opts.cropperCircleOverlay,
forceJpg: true, // ios only
compressImageQuality: 1.0,
})
return {
mediaType,
path: item.path,
mime: item.mime,
size: item.size,
width: item.width,
height: item.height,
}
}

View file

@ -1,75 +0,0 @@
/// <reference lib="dom" />
import {CropImageModal} from 'state/models/shell-ui'
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
export type {PickedMedia} from './types'
import {RootStoreModel} from 'state/index'
interface PickedFile {
uri: string
path: string
size: number
}
export async function openPicker(
store: RootStoreModel,
opts: PickerOpts,
): Promise<PickedMedia[] | PickedMedia> {
const res = await selectFile(opts)
return new Promise((resolve, reject) => {
store.shell.openModal(
new CropImageModal(res.uri, (img?: PickedMedia) => {
if (img) {
resolve(img)
} else {
reject(new Error('Canceled'))
}
}),
)
})
}
export async function openCamera(
_store: RootStoreModel,
_opts: CameraOpts,
): Promise<PickedMedia> {
// const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO')
}
export async function openCropper(
_store: RootStoreModel,
_opts: CropperOpts,
): Promise<PickedMedia> {
// const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO')
}
function selectFile(opts: PickerOpts): Promise<PickedFile> {
return new Promise((resolve, reject) => {
var input = document.createElement('input')
input.type = 'file'
input.accept = opts.mediaType === 'photo' ? 'image/*' : '*/*'
input.onchange = e => {
const target = e.target as HTMLInputElement
const file = target?.files?.[0]
if (!file) {
return reject(new Error('Canceled'))
}
var reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = readerEvent => {
if (!readerEvent.target) {
return reject(new Error('Canceled'))
}
resolve({
uri: readerEvent.target.result as string,
path: file.name,
size: file.size,
})
}
}
input.click()
})
}

View file

@ -1,31 +0,0 @@
export interface PickerOpts {
mediaType?: 'photo'
multiple?: boolean
maxFiles?: number
}
export interface CameraOpts {
mediaType?: 'photo'
width: number
height: number
freeStyleCropEnabled?: boolean
cropperCircleOverlay?: boolean
}
export interface CropperOpts {
path: string
mediaType?: 'photo'
width: number
height: number
freeStyleCropEnabled?: boolean
cropperCircleOverlay?: boolean
}
export interface PickedMedia {
mediaType: 'photo'
path: string
mime: string
size: number
width: number
height: number
}

View file

@ -28,7 +28,7 @@ import {Login} from '../../screens/Login'
import {Menu} from './Menu'
import {Onboard} from '../../screens/Onboard'
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
import {Modal} from '../../com/modals/Modal'
import {ModalsContainer} from '../../com/modals/Modal'
import {Lightbox} from '../../com/lightbox/Lightbox'
import {Text} from '../../com/util/text/Text'
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
@ -366,7 +366,7 @@ export const MobileShell: React.FC = observer(() => {
return (
<View style={styles.outerContainer}>
<Login />
<Modal />
<ModalsContainer />
</View>
)
}
@ -515,7 +515,7 @@ export const MobileShell: React.FC = observer(() => {
notificationCount={store.me.notifications.unreadCount}
/>
</Animated.View>
<Modal />
<ModalsContainer />
<Lightbox />
<Composer
active={store.shell.isComposerActive}

View file

@ -11,21 +11,28 @@ 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 {ModalsContainer} from '../../com/modals/Modal'
import {Text} from 'view/com/util/text/Text'
import {Composer} from './Composer'
import {usePalette} from 'lib/hooks/usePalette'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {s, colors} from 'lib/styles'
import {isMobileWeb} from 'platform/detection'
export const WebShell: React.FC = observer(() => {
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores()
const screenRenderDesc = constructScreenRenderDesc(store.nav)
if (isMobileWeb) {
return <NoMobileWeb />
}
if (!store.session.hasSession) {
return (
<View style={styles.outerContainer}>
<Login />
<Modal />
<ModalsContainer />
</View>
)
}
@ -60,21 +67,10 @@ export const WebShell: React.FC = observer(() => {
imagesOpen={store.shell.composerOpts?.imagesOpen}
onPost={store.shell.composerOpts?.onPost}
/>
<Modal />
<ModalsContainer />
<Lightbox />
</View>
)
// TODO
// <Modal />
// <Lightbox />
// <Composer
// active={store.shell.isComposerActive}
// onClose={() => store.shell.closeComposer()}
// winHeight={winDim.height}
// replyTo={store.shell.composerOpts?.replyTo}
// imagesOpen={store.shell.composerOpts?.imagesOpen}
// onPost={store.shell.composerOpts?.onPost}
// />
})
/**
@ -126,6 +122,21 @@ function constructScreenRenderDesc(nav: NavigationModel): {
}
}
function NoMobileWeb() {
const pal = usePalette('default')
return (
<View style={[pal.view, styles.noMobileWeb]}>
<Text type="title-2xl" style={{paddingBottom: 20}}>
We're so sorry!
</Text>
<Text type="lg">
This app is not available for mobile Web yet. Please open it on your
desktop or download the iOS app.
</Text>
</View>
)
}
const styles = StyleSheet.create({
outerContainer: {
height: '100%',
@ -142,4 +153,10 @@ const styles = StyleSheet.create({
hidden: {
display: 'none',
},
noMobileWeb: {
height: '100%',
justifyContent: 'center',
paddingHorizontal: 20,
paddingBottom: 40,
},
})