Add scene creator
parent
93b64cf474
commit
e7536289cb
|
@ -9,14 +9,14 @@ import {createContext, useContext} from 'react'
|
|||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {SessionModel} from './session'
|
||||
import {NavigationModel} from './navigation'
|
||||
import {ShellModel} from './shell'
|
||||
import {ShellUiModel} from './shell-ui'
|
||||
import {MeModel} from './me'
|
||||
import {OnboardModel} from './onboard'
|
||||
|
||||
export class RootStoreModel {
|
||||
session = new SessionModel(this)
|
||||
nav = new NavigationModel()
|
||||
shell = new ShellModel()
|
||||
shell = new ShellUiModel()
|
||||
me = new MeModel(this)
|
||||
onboard = new OnboardModel()
|
||||
|
||||
|
|
|
@ -35,14 +35,27 @@ export class EditProfileModel {
|
|||
}
|
||||
}
|
||||
|
||||
export class CreateSceneModel {
|
||||
name = 'create-scene'
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComposerOpts {
|
||||
replyTo?: Post.PostRef
|
||||
onPost?: () => void
|
||||
}
|
||||
|
||||
export class ShellModel {
|
||||
export class ShellUiModel {
|
||||
isModalActive = false
|
||||
activeModal: LinkActionsModel | SharePostModel | EditProfileModel | undefined
|
||||
activeModal:
|
||||
| LinkActionsModel
|
||||
| SharePostModel
|
||||
| EditProfileModel
|
||||
| CreateSceneModel
|
||||
| undefined
|
||||
isComposerActive = false
|
||||
composerOpts: ComposerOpts | undefined
|
||||
|
||||
|
@ -50,7 +63,13 @@ export class ShellModel {
|
|||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
openModal(modal: LinkActionsModel | SharePostModel | EditProfileModel) {
|
||||
openModal(
|
||||
modal:
|
||||
| LinkActionsModel
|
||||
| SharePostModel
|
||||
| EditProfileModel
|
||||
| CreateSceneModel,
|
||||
) {
|
||||
this.isModalActive = true
|
||||
this.activeModal = modal
|
||||
}
|
|
@ -8,7 +8,7 @@ import Toast from '../util/Toast'
|
|||
import ProgressCircle from '../util/ProgressCircle'
|
||||
import {useStores} from '../../../state'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
import {ComposerOpts} from '../../../state/models/shell'
|
||||
import {ComposerOpts} from '../../../state/models/shell-ui'
|
||||
import {s, colors, gradients} from '../../lib/styles'
|
||||
|
||||
const MAX_TEXT_LENGTH = 256
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
import React, {useState} from 'react'
|
||||
import Toast from '../util/Toast'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {ErrorMessage} from '../util/ErrorMessage'
|
||||
import {useStores} from '../../../state'
|
||||
import {s, colors, gradients} from '../../lib/styles'
|
||||
import {makeValidHandle, createFullHandle} from '../../lib/strings'
|
||||
import {AppBskyActorCreateScene} from '../../../third-party/api/index'
|
||||
|
||||
export const snapPoints = ['70%']
|
||||
|
||||
export function Component({}: {}) {
|
||||
const store = useStores()
|
||||
const [error, setError] = useState<string>('')
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [handle, setHandle] = useState<string>('')
|
||||
const [displayName, setDisplayName] = useState<string>('')
|
||||
const [description, setDescription] = useState<string>('')
|
||||
const onPressSave = async () => {
|
||||
setIsProcessing(true)
|
||||
if (error) {
|
||||
setError('')
|
||||
}
|
||||
try {
|
||||
if (!store.me.did) {
|
||||
return
|
||||
}
|
||||
const desc = await store.api.com.atproto.server.getAccountsConfig()
|
||||
const fullHandle = createFullHandle(
|
||||
handle,
|
||||
desc.data.availableUserDomains[0],
|
||||
)
|
||||
// create scene actor
|
||||
const createSceneRes = await store.api.app.bsky.actor.createScene({
|
||||
handle: fullHandle,
|
||||
})
|
||||
// set the scene profile
|
||||
// TODO
|
||||
// follow the scene
|
||||
await store.api.app.bsky.graph.follow
|
||||
.create(
|
||||
{
|
||||
did: store.me.did,
|
||||
},
|
||||
{
|
||||
subject: {
|
||||
did: createSceneRes.data.did,
|
||||
declarationCid: createSceneRes.data.declarationCid,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
.catch(e => console.error(e)) // an error here is not critical
|
||||
Toast.show('Scene created', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
store.shell.closeModal()
|
||||
store.nav.navigate(`/profile/${fullHandle}`)
|
||||
} catch (e: any) {
|
||||
if (e instanceof AppBskyActorCreateScene.InvalidHandleError) {
|
||||
setError(
|
||||
'The handle can only contain letters, numbers, and dashes, and must start with a letter.',
|
||||
)
|
||||
} else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
|
||||
setError(`The handle "${handle}" is not available.`)
|
||||
} else {
|
||||
console.error(e)
|
||||
setError(
|
||||
'Failed to create the scene. Check your internet connection and try again.',
|
||||
)
|
||||
}
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.flex1}>
|
||||
<Text style={styles.title}>Create a scene</Text>
|
||||
<Text style={styles.description}>
|
||||
Scenes are invite-only groups which aggregate what's popular with
|
||||
members.
|
||||
</Text>
|
||||
<View style={styles.inner}>
|
||||
<View style={styles.group}>
|
||||
<Text style={styles.label}>Scene Handle</Text>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder="e.g. alices-friends"
|
||||
value={handle}
|
||||
onChangeText={str => setHandle(makeValidHandle(str))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.group}>
|
||||
<Text style={styles.label}>Scene Display Name</Text>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder="e.g. Alice's Friends"
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.group}>
|
||||
<Text style={styles.label}>Scene Description</Text>
|
||||
<TextInput
|
||||
style={[styles.textArea]}
|
||||
placeholder="e.g. Artists, dog-lovers, and memelords."
|
||||
multiline
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.errorContainer}>
|
||||
{error !== '' && (
|
||||
<View style={s.mb10}>
|
||||
<ErrorMessage message={error} numberOfLines={3} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{handle.length >= 2 && !isProcessing ? (
|
||||
<TouchableOpacity style={s.mt10} onPress={onPressSave}>
|
||||
<LinearGradient
|
||||
colors={[gradients.primary.start, gradients.primary.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>Create Scene</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={s.mt10}>
|
||||
<View style={[styles.btn]}>
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text style={[s.gray4, s.bold, s.f18]}>Create Scene</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
fontSize: 17,
|
||||
paddingHorizontal: 22,
|
||||
color: colors.gray5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
inner: {
|
||||
padding: 14,
|
||||
},
|
||||
group: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.gray3,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
textArea: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.gray3,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 10,
|
||||
fontSize: 16,
|
||||
height: 100,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
errorContainer: {
|
||||
height: 80,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.gray1,
|
||||
},
|
||||
})
|
|
@ -68,7 +68,7 @@ export function Component({profileView}: {profileView: ProfileViewModel}) {
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.group}>
|
||||
<Text style={styles.label}>Biography</Text>
|
||||
<Text style={styles.label}>Description</Text>
|
||||
<TextInput
|
||||
style={[styles.textArea]}
|
||||
placeholder="e.g. Artist, dog-lover, and memelord."
|
||||
|
|
|
@ -5,11 +5,12 @@ import BottomSheet from '@gorhom/bottom-sheet'
|
|||
import {useStores} from '../../../state'
|
||||
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||
|
||||
import * as models from '../../../state/models/shell'
|
||||
import * as models from '../../../state/models/shell-ui'
|
||||
|
||||
import * as LinkActionsModal from './LinkActions'
|
||||
import * as SharePostModal from './SharePost.native'
|
||||
import * as EditProfile from './EditProfile'
|
||||
import * as CreateScene from './CreateScene'
|
||||
|
||||
const CLOSED_SNAPPOINTS = ['10%']
|
||||
|
||||
|
@ -57,6 +58,9 @@ export const Modal = observer(function Modal() {
|
|||
{...(store.shell.activeModal as models.EditProfileModel)}
|
||||
/>
|
||||
)
|
||||
} else if (store.shell.activeModal?.name === 'create-scene') {
|
||||
snapPoints = CreateScene.snapPoints
|
||||
element = <CreateScene.Component />
|
||||
} else {
|
||||
element = <View />
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
PostThreadViewPostModel,
|
||||
} from '../../../state/models/post-thread-view'
|
||||
import {useStores} from '../../../state'
|
||||
import {SharePostModel} from '../../../state/models/shell'
|
||||
import {SharePostModel} from '../../../state/models/shell-ui'
|
||||
import {PostThreadItem} from './PostThreadItem'
|
||||
|
||||
export const PostThread = observer(function PostThread({uri}: {uri: string}) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import {AtUri} from '../../../third-party/uri'
|
|||
import * as PostType from '../../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FeedItemModel} from '../../../state/models/feed-view'
|
||||
import {SharePostModel} from '../../../state/models/shell'
|
||||
import {SharePostModel} from '../../../state/models/shell-ui'
|
||||
import {Link} from '../util/Link'
|
||||
import {PostDropdownBtn} from '../util/DropdownBtn'
|
||||
import {UserInfoText} from '../util/UserInfoText'
|
||||
|
|
|
@ -11,7 +11,7 @@ import LinearGradient from 'react-native-linear-gradient'
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ProfileViewModel} from '../../../state/models/profile-view'
|
||||
import {useStores} from '../../../state'
|
||||
import {EditProfileModel} from '../../../state/models/shell'
|
||||
import {EditProfileModel} from '../../../state/models/shell-ui'
|
||||
import {pluralize} from '../../lib/strings'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {getGradient} from '../../lib/asset-gen'
|
||||
|
|
|
@ -13,7 +13,7 @@ import RootSiblings from 'react-native-root-siblings'
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {colors} from '../../lib/styles'
|
||||
import {useStores} from '../../../state'
|
||||
import {SharePostModel} from '../../../state/models/shell'
|
||||
import {SharePostModel} from '../../../state/models/shell-ui'
|
||||
|
||||
export interface DropdownItem {
|
||||
icon?: IconProp
|
||||
|
|
|
@ -5,9 +5,11 @@ import {colors} from '../../lib/styles'
|
|||
|
||||
export function ErrorMessage({
|
||||
message,
|
||||
numberOfLines,
|
||||
onPressTryAgain,
|
||||
}: {
|
||||
message: string
|
||||
numberOfLines?: number
|
||||
onPressTryAgain?: () => void
|
||||
}) {
|
||||
return (
|
||||
|
@ -19,7 +21,9 @@ export function ErrorMessage({
|
|||
size={16}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.message}>{message}</Text>
|
||||
<Text style={styles.message} numberOfLines={numberOfLines}>
|
||||
{message}
|
||||
</Text>
|
||||
{onPressTryAgain && (
|
||||
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
|
||||
<FontAwesomeIcon
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleProp, Text, TouchableOpacity, ViewStyle} from 'react-native'
|
||||
import {useStores} from '../../../state'
|
||||
import {LinkActionsModel} from '../../../state/models/shell'
|
||||
import {LinkActionsModel} from '../../../state/models/shell-ui'
|
||||
|
||||
export const Link = observer(function Link({
|
||||
style,
|
||||
|
|
|
@ -71,3 +71,17 @@ export function extractEntities(text: string): Entity[] | undefined {
|
|||
}
|
||||
return ents.length > 0 ? ents : undefined
|
||||
}
|
||||
|
||||
export function makeValidHandle(str: string): string {
|
||||
if (str.length > 20) {
|
||||
str = str.slice(0, 20)
|
||||
}
|
||||
str = str.toLowerCase()
|
||||
return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '')
|
||||
}
|
||||
|
||||
export function createFullHandle(name: string, domain: string): string {
|
||||
name = name.replace(/[\.]+$/, '')
|
||||
domain = domain.replace(/^[\.]+/, '')
|
||||
return `${name}.${domain}`
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons'
|
||||
import {ComposePost} from '../../com/composer/ComposePost'
|
||||
import {useStores} from '../../../state'
|
||||
import {ComposerOpts} from '../../../state/models/shell'
|
||||
import {ComposerOpts} from '../../../state/models/shell-ui'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
|
||||
export const Composer = observer(
|
||||
|
|
|
@ -20,6 +20,7 @@ import _chunk from 'lodash.chunk'
|
|||
import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons'
|
||||
import {UserAvatar} from '../../com/util/UserAvatar'
|
||||
import {useStores} from '../../../state'
|
||||
import {CreateSceneModel} from '../../../state/models/shell-ui'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
|
||||
export const MainMenu = observer(
|
||||
|
@ -54,6 +55,10 @@ export const MainMenu = observer(
|
|||
store.nav.navigate(url)
|
||||
onClose()
|
||||
}
|
||||
const onPressCreateScene = () => {
|
||||
store.shell.openModal(new CreateSceneModel())
|
||||
onClose()
|
||||
}
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
@ -65,17 +70,19 @@ export const MainMenu = observer(
|
|||
const MenuItem = ({
|
||||
icon,
|
||||
label,
|
||||
url,
|
||||
count,
|
||||
url,
|
||||
onPress,
|
||||
}: {
|
||||
icon: IconProp
|
||||
label: string
|
||||
url: string
|
||||
count?: number
|
||||
url?: string
|
||||
onPress?: () => void
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, styles.menuItemMargin]}
|
||||
onPress={() => onNavigate(url)}>
|
||||
onPress={onPress ? onPress : () => onNavigate(url || '/')}>
|
||||
<View style={[styles.menuItemIconWrapper]}>
|
||||
{icon === 'home' ? (
|
||||
<HomeIcon style={styles.menuItemIcon} size="32" />
|
||||
|
@ -209,7 +216,7 @@ export const MainMenu = observer(
|
|||
<MenuItem
|
||||
icon={'user-group'}
|
||||
label="Create Scene"
|
||||
url="/contacts"
|
||||
onPress={onPressCreateScene}
|
||||
/>
|
||||
{store.me.memberships ? (
|
||||
store.me.memberships.memberships.map((membership, i) => (
|
||||
|
|
|
@ -19,7 +19,7 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'
|
|||
import {useStores} from '../../../state'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {match} from '../../routes'
|
||||
import {LinkActionsModel} from '../../../state/models/shell'
|
||||
import {LinkActionsModel} from '../../../state/models/shell-ui'
|
||||
|
||||
const TAB_HEIGHT = 42
|
||||
|
||||
|
|
|
@ -230,11 +230,11 @@ export const MobileShell: React.FC = observer(() => {
|
|||
/>
|
||||
<Btn icon={['far', 'clone']} onPress={onPressTabs} />
|
||||
</View>
|
||||
<Modal />
|
||||
<MainMenu
|
||||
active={isMainMenuActive}
|
||||
onClose={() => setMainMenuActive(false)}
|
||||
/>
|
||||
<Modal />
|
||||
<TabsSelector
|
||||
active={isTabsSelectorActive}
|
||||
onClose={() => setTabsSelectorActive(false)}
|
||||
|
|
Loading…
Reference in New Issue