Move to expo and react-navigation (#288)
* WIP - adding expo * WIP - adding expo 2 * Fix tsc * Finish adding expo * Disable the 'require cycle' warning * Tweak plist * Modify some dependency versions to make expo happy * Fix icon fill * Get Web compiling for expo * 1.7 * Switch to react-navigation in expo2 (#287) * WIP Switch to react-navigation * WIP Switch to react-navigation 2 * WIP Switch to react-navigation 3 * Convert all screens to react navigation * Update BottomBar for react navigation * Update mobile menu to be react-native drawer * Fixes to drawer and bottombar * Factor out some helpers * Replace the navigation model with react-navigation * Restructure the shell folder and fix the header positioning * Restore the error boundary * Fix tsc * Implement not-found page * Remove react-native-gesture-handler (no longer used) * Handle notifee card presses * Handle all navigations from the state layer * Fix drawer behaviors * Fix two linking issues * Switch to our react-native-progress fork to fix an svg rendering issue * Get Web working with react-navigation * Refactor routes and navigation for a bit more clarity * Remove dead code * Rework Web shell to left/right nav to make this easier * Fix ViewHeader for desktop web * Hide profileheader back btn on desktop web * Move the compose button to the left nav * Implement reply prompt in threads for desktop web * Composer refactors * Factor out all platform-specific text input behaviors from the composer * Small fix * Update the web build to use tiptap for the composer * Tune up the mention autocomplete dropdown * Simplify the default avatar and banner * Fixes to link cards in web composer * Fix dropdowns on web * Tweak load latest on desktop * Add web beta message and feedback link * Fix up links in desktop web
This commit is contained in:
parent
503e03d91e
commit
56cf890deb
222 changed files with 8705 additions and 6338 deletions
|
@ -16,7 +16,7 @@ export function init(store: RootStoreModel) {
|
|||
// this method is a copy of segment's own lifecycle event tracking
|
||||
// we handle it manually to ensure that it never fires while the app is backgrounded
|
||||
// -prf
|
||||
segmentClient.onContextLoaded(() => {
|
||||
segmentClient.isReady.onChange(() => {
|
||||
if (AppState.currentState !== 'active') {
|
||||
store.log.debug('Prevented a metrics ping while the app was backgrounded')
|
||||
return
|
||||
|
|
|
@ -117,7 +117,9 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
|||
if (opts.extLink.localThumb) {
|
||||
opts.onStateChange?.('Uploading link thumbnail...')
|
||||
let encoding
|
||||
if (opts.extLink.localThumb.path.endsWith('.png')) {
|
||||
if (opts.extLink.localThumb.mime) {
|
||||
encoding = opts.extLink.localThumb.mime
|
||||
} else if (opts.extLink.localThumb.path.endsWith('.png')) {
|
||||
encoding = 'image/png'
|
||||
} else if (
|
||||
opts.extLink.localThumb.path.endsWith('.jpeg') ||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ImageRequireSource} from 'react-native'
|
||||
|
||||
export const DEF_AVATAR: ImageRequireSource = require('../../public/img/default-avatar.jpg')
|
||||
export const TABS_EXPLAINER: ImageRequireSource = require('../../public/img/tabs-explainer.jpg')
|
||||
export const CLOUD_SPLASH: ImageRequireSource = require('../../public/img/cloud-splash.png')
|
||||
export const DEF_AVATAR: ImageRequireSource = require('../../assets/default-avatar.jpg')
|
||||
export const TABS_EXPLAINER: ImageRequireSource = require('../../assets/tabs-explainer.jpg')
|
||||
export const CLOUD_SPLASH: ImageRequireSource = require('../../assets/cloud-splash.png')
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||
export const TABS_ENABLED = false
|
||||
|
|
|
@ -166,5 +166,3 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) {
|
|||
export const POST_IMG_MAX_WIDTH = 2000
|
||||
export const POST_IMG_MAX_HEIGHT = 2000
|
||||
export const POST_IMG_MAX_SIZE = 1000000
|
||||
|
||||
export const DESKTOP_HEADER_HEIGHT = 57
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {useColorScheme} from 'react-native'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
|
||||
export function useColorSchemeStyle(lightStyle: any, darkStyle: any) {
|
||||
const colorScheme = useColorScheme()
|
||||
const colorScheme = useTheme().colorScheme
|
||||
return colorScheme === 'dark' ? darkStyle : lightStyle
|
||||
}
|
||||
|
|
50
src/lib/hooks/usePermissions.ts
Normal file
50
src/lib/hooks/usePermissions.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import {Alert} from 'react-native'
|
||||
import {Camera} from 'expo-camera'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import {Linking} from 'react-native'
|
||||
|
||||
const openSettings = () => {
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
|
||||
const openPermissionAlert = (perm: string) => {
|
||||
Alert.alert(
|
||||
'Permission needed',
|
||||
`Bluesky does not have permission to access your ${perm}.`,
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{text: 'Open Settings', onPress: () => openSettings()},
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
export function usePhotoLibraryPermission() {
|
||||
const [mediaLibraryPermissions] = MediaLibrary.usePermissions()
|
||||
const requestPhotoAccessIfNeeded = async () => {
|
||||
if (mediaLibraryPermissions?.status === 'granted') {
|
||||
return true
|
||||
} else {
|
||||
openPermissionAlert('photo library')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return {requestPhotoAccessIfNeeded}
|
||||
}
|
||||
|
||||
export function useCameraPermission() {
|
||||
const [cameraPermissionStatus] = Camera.useCameraPermissions()
|
||||
|
||||
const requestCameraAccessIfNeeded = async () => {
|
||||
if (cameraPermissionStatus?.granted) {
|
||||
return true
|
||||
} else {
|
||||
openPermissionAlert('camera')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {requestCameraAccessIfNeeded}
|
||||
}
|
|
@ -73,12 +73,10 @@ export function HomeIconSolid({
|
|||
style,
|
||||
size,
|
||||
strokeWidth = 4,
|
||||
fillOpacity = 1,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
fillOpacity?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
|
@ -89,11 +87,6 @@ export function HomeIconSolid({
|
|||
style={style}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
opacity={fillOpacity}
|
||||
d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z"
|
||||
/>
|
||||
<Path
|
||||
strokeWidth={strokeWidth}
|
||||
d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z"
|
||||
/>
|
||||
|
@ -158,12 +151,10 @@ export function MagnifyingGlassIcon2Solid({
|
|||
style,
|
||||
size,
|
||||
strokeWidth = 2,
|
||||
fillOpacity = 1,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
fillOpacity?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
|
@ -181,7 +172,6 @@ export function MagnifyingGlassIcon2Solid({
|
|||
ry="7"
|
||||
stroke="none"
|
||||
fill="currentColor"
|
||||
opacity={fillOpacity}
|
||||
/>
|
||||
<Ellipse cx="12" cy="11" rx="9" ry="9" />
|
||||
<Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" />
|
||||
|
@ -219,12 +209,10 @@ export function BellIconSolid({
|
|||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
fillOpacity = 1,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
fillOpacity?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
|
@ -237,10 +225,7 @@ export function BellIconSolid({
|
|||
<Path
|
||||
d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
opacity={fillOpacity}
|
||||
/>
|
||||
<Path d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" />
|
||||
<Line x1="9" y1="22" x2="15" y2="22" />
|
||||
</Svg>
|
||||
)
|
||||
|
@ -278,6 +263,34 @@ export function CogIcon({
|
|||
)
|
||||
}
|
||||
|
||||
export function CogIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 9.594 3.94 C 9.684 3.398 10.154 3 10.704 3 L 13.297 3 C 13.847 3 14.317 3.398 14.407 3.94 L 14.62 5.221 C 14.683 5.595 14.933 5.907 15.265 6.091 C 15.339 6.131 15.412 6.174 15.485 6.218 C 15.809 6.414 16.205 6.475 16.56 6.342 L 17.777 5.886 C 18.292 5.692 18.872 5.9 19.147 6.376 L 20.443 8.623 C 20.718 9.099 20.608 9.705 20.183 10.054 L 19.18 10.881 C 18.887 11.121 18.742 11.494 18.749 11.873 C 18.751 11.958 18.751 12.043 18.749 12.128 C 18.742 12.506 18.887 12.878 19.179 13.118 L 20.184 13.946 C 20.608 14.296 20.718 14.9 20.444 15.376 L 19.146 17.623 C 18.871 18.099 18.292 18.307 17.777 18.114 L 16.56 17.658 C 16.205 17.525 15.81 17.586 15.484 17.782 C 15.412 17.826 15.338 17.869 15.264 17.91 C 14.933 18.093 14.683 18.405 14.62 18.779 L 14.407 20.059 C 14.317 20.602 13.847 21 13.297 21 L 10.703 21 C 10.153 21 9.683 20.602 9.593 20.06 L 9.38 18.779 C 9.318 18.405 9.068 18.093 8.736 17.909 C 8.662 17.868 8.589 17.826 8.516 17.782 C 8.191 17.586 7.796 17.525 7.44 17.658 L 6.223 18.114 C 5.708 18.307 5.129 18.1 4.854 17.624 L 3.557 15.377 C 3.282 14.901 3.392 14.295 3.817 13.946 L 4.821 13.119 C 5.113 12.879 5.258 12.506 5.251 12.127 C 5.249 12.042 5.249 11.957 5.251 11.872 C 5.258 11.494 5.113 11.122 4.821 10.882 L 3.817 10.054 C 3.393 9.705 3.283 9.1 3.557 8.624 L 4.854 6.377 C 5.129 5.9 5.709 5.692 6.224 5.886 L 7.44 6.342 C 7.796 6.475 8.191 6.414 8.516 6.218 C 8.588 6.174 8.662 6.131 8.736 6.09 C 9.068 5.907 9.318 5.595 9.38 5.221 Z M 13.5 9.402 C 11.5 8.247 9 9.691 9 12 C 9 13.072 9.572 14.062 10.5 14.598 C 12.5 15.753 15 14.309 15 12 C 15 10.928 14.428 9.938 13.5 9.402 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function MoonIcon({
|
||||
|
@ -336,6 +349,45 @@ export function UserIcon({
|
|||
)
|
||||
}
|
||||
|
||||
export function UserIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="currentColor"
|
||||
d="M 15 9.75 C 15 12.059 12.5 13.503 10.5 12.348 C 9.572 11.812 9 10.822 9 9.75 C 9 7.441 11.5 5.997 13.5 7.152 C 14.428 7.688 15 8.678 15 9.75 Z"
|
||||
/>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="currentColor"
|
||||
d="M 17.982 18.725 C 16.565 16.849 14.35 15.748 12 15.75 C 9.65 15.748 7.435 16.849 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
|
||||
/>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 17.981 18.725 C 23.158 14.12 21.409 5.639 14.833 3.458 C 8.257 1.277 1.786 7.033 3.185 13.818 C 3.576 15.716 4.57 17.437 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function UserGroupIcon({
|
||||
|
@ -674,6 +726,7 @@ export function ComposeIcon2({
|
|||
<Svg
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import {LikelyType, LinkMeta} from './link-meta'
|
||||
import {match as matchRoute} from 'view/routes'
|
||||
// import {match as matchRoute} from 'view/routes'
|
||||
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {PostThreadViewModel} from 'state/models/post-thread-view'
|
||||
import {ComposerOptsQuote} from 'state/models/shell-ui'
|
||||
|
||||
import {Home} from 'view/screens/Home'
|
||||
import {Search} from 'view/screens/Search'
|
||||
import {Notifications} from 'view/screens/Notifications'
|
||||
import {PostThread} from 'view/screens/PostThread'
|
||||
import {PostUpvotedBy} from 'view/screens/PostUpvotedBy'
|
||||
import {PostRepostedBy} from 'view/screens/PostRepostedBy'
|
||||
import {Profile} from 'view/screens/Profile'
|
||||
import {ProfileFollowers} from 'view/screens/ProfileFollowers'
|
||||
import {ProfileFollows} from 'view/screens/ProfileFollows'
|
||||
// TODO
|
||||
// import {Home} from 'view/screens/Home'
|
||||
// import {Search} from 'view/screens/Search'
|
||||
// import {Notifications} from 'view/screens/Notifications'
|
||||
// import {PostThread} from 'view/screens/PostThread'
|
||||
// import {PostUpvotedBy} from 'view/screens/PostUpvotedBy'
|
||||
// import {PostRepostedBy} from 'view/screens/PostRepostedBy'
|
||||
// import {Profile} from 'view/screens/Profile'
|
||||
// import {ProfileFollowers} from 'view/screens/ProfileFollowers'
|
||||
// import {ProfileFollows} from 'view/screens/ProfileFollows'
|
||||
|
||||
// NOTE
|
||||
// this is a hack around the lack of hosted social metadata
|
||||
|
@ -24,77 +25,77 @@ export async function extractBskyMeta(
|
|||
url: string,
|
||||
): Promise<LinkMeta> {
|
||||
url = convertBskyAppUrlIfNeeded(url)
|
||||
const route = matchRoute(url)
|
||||
// const route = matchRoute(url)
|
||||
let meta: LinkMeta = {
|
||||
likelyType: LikelyType.AtpData,
|
||||
url,
|
||||
title: route.defaultTitle,
|
||||
// title: route.defaultTitle,
|
||||
}
|
||||
|
||||
if (route.Com === Home) {
|
||||
meta = {
|
||||
...meta,
|
||||
title: 'Bluesky',
|
||||
description: 'A new kind of social network',
|
||||
}
|
||||
} else if (route.Com === Search) {
|
||||
meta = {
|
||||
...meta,
|
||||
title: 'Search - Bluesky',
|
||||
description: 'A new kind of social network',
|
||||
}
|
||||
} else if (route.Com === Notifications) {
|
||||
meta = {
|
||||
...meta,
|
||||
title: 'Notifications - Bluesky',
|
||||
description: 'A new kind of social network',
|
||||
}
|
||||
} else if (
|
||||
route.Com === PostThread ||
|
||||
route.Com === PostUpvotedBy ||
|
||||
route.Com === PostRepostedBy
|
||||
) {
|
||||
// post and post-related screens
|
||||
const threadUri = makeRecordUri(
|
||||
route.params.name,
|
||||
'app.bsky.feed.post',
|
||||
route.params.rkey,
|
||||
)
|
||||
const threadView = new PostThreadViewModel(store, {
|
||||
uri: threadUri,
|
||||
depth: 0,
|
||||
})
|
||||
await threadView.setup().catch(_err => undefined)
|
||||
const title = [
|
||||
route.Com === PostUpvotedBy
|
||||
? 'Likes on a post by'
|
||||
: route.Com === PostRepostedBy
|
||||
? 'Reposts of a post by'
|
||||
: 'Post by',
|
||||
threadView.thread?.post.author.displayName ||
|
||||
threadView.thread?.post.author.handle ||
|
||||
'a bluesky user',
|
||||
].join(' ')
|
||||
meta = {
|
||||
...meta,
|
||||
title,
|
||||
description: threadView.thread?.postRecord?.text,
|
||||
}
|
||||
} else if (
|
||||
route.Com === Profile ||
|
||||
route.Com === ProfileFollowers ||
|
||||
route.Com === ProfileFollows
|
||||
) {
|
||||
// profile and profile-related screens
|
||||
const profile = await store.profiles.getProfile(route.params.name)
|
||||
if (profile?.data) {
|
||||
meta = {
|
||||
...meta,
|
||||
title: profile.data.displayName || profile.data.handle,
|
||||
description: profile.data.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (route.Com === Home) {
|
||||
// meta = {
|
||||
// ...meta,
|
||||
// title: 'Bluesky',
|
||||
// description: 'A new kind of social network',
|
||||
// }
|
||||
// } else if (route.Com === Search) {
|
||||
// meta = {
|
||||
// ...meta,
|
||||
// title: 'Search - Bluesky',
|
||||
// description: 'A new kind of social network',
|
||||
// }
|
||||
// } else if (route.Com === Notifications) {
|
||||
// meta = {
|
||||
// ...meta,
|
||||
// title: 'Notifications - Bluesky',
|
||||
// description: 'A new kind of social network',
|
||||
// }
|
||||
// } else if (
|
||||
// route.Com === PostThread ||
|
||||
// route.Com === PostUpvotedBy ||
|
||||
// route.Com === PostRepostedBy
|
||||
// ) {
|
||||
// // post and post-related screens
|
||||
// const threadUri = makeRecordUri(
|
||||
// route.params.name,
|
||||
// 'app.bsky.feed.post',
|
||||
// route.params.rkey,
|
||||
// )
|
||||
// const threadView = new PostThreadViewModel(store, {
|
||||
// uri: threadUri,
|
||||
// depth: 0,
|
||||
// })
|
||||
// await threadView.setup().catch(_err => undefined)
|
||||
// const title = [
|
||||
// route.Com === PostUpvotedBy
|
||||
// ? 'Likes on a post by'
|
||||
// : route.Com === PostRepostedBy
|
||||
// ? 'Reposts of a post by'
|
||||
// : 'Post by',
|
||||
// threadView.thread?.post.author.displayName ||
|
||||
// threadView.thread?.post.author.handle ||
|
||||
// 'a bluesky user',
|
||||
// ].join(' ')
|
||||
// meta = {
|
||||
// ...meta,
|
||||
// title,
|
||||
// description: threadView.thread?.postRecord?.text,
|
||||
// }
|
||||
// } else if (
|
||||
// route.Com === Profile ||
|
||||
// route.Com === ProfileFollowers ||
|
||||
// route.Com === ProfileFollows
|
||||
// ) {
|
||||
// // profile and profile-related screens
|
||||
// const profile = await store.profiles.getProfile(route.params.name)
|
||||
// if (profile?.data) {
|
||||
// meta = {
|
||||
// ...meta,
|
||||
// title: profile.data.displayName || profile.data.handle,
|
||||
// description: profile.data.description,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return meta
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// import {Share} from 'react-native'
|
||||
// import * as Toast from 'view/com/util/Toast'
|
||||
import {extractDataUriMime, getDataUriSize} from './util'
|
||||
|
||||
export interface DownloadAndResizeOpts {
|
||||
uri: string
|
||||
|
@ -18,9 +19,15 @@ export interface Image {
|
|||
height: number
|
||||
}
|
||||
|
||||
export async function downloadAndResize(_opts: DownloadAndResizeOpts) {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
const controller = new AbortController()
|
||||
const to = setTimeout(() => controller.abort(), opts.timeout || 5e3)
|
||||
const res = await fetch(opts.uri)
|
||||
const resBody = await res.blob()
|
||||
clearTimeout(to)
|
||||
|
||||
const dataUri = await blobToDataUri(resBody)
|
||||
return await resize(dataUri, opts)
|
||||
}
|
||||
|
||||
export interface ResizeOpts {
|
||||
|
@ -31,11 +38,18 @@ export interface ResizeOpts {
|
|||
}
|
||||
|
||||
export async function resize(
|
||||
_localUri: string,
|
||||
dataUri: string,
|
||||
_opts: ResizeOpts,
|
||||
): Promise<Image> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
const dim = await getImageDim(dataUri)
|
||||
// TODO -- need to resize
|
||||
return {
|
||||
path: dataUri,
|
||||
mime: extractDataUriMime(dataUri),
|
||||
size: getDataUriSize(dataUri),
|
||||
width: dim.width,
|
||||
height: dim.height,
|
||||
}
|
||||
}
|
||||
|
||||
export async function compressIfNeeded(
|
||||
|
@ -86,3 +100,18 @@ export async function getImageDim(path: string): Promise<Dim> {
|
|||
await promise
|
||||
return {width: img.width, height: img.height}
|
||||
}
|
||||
|
||||
function blobToDataUri(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result)
|
||||
} else {
|
||||
reject(new Error('Failed to read blob'))
|
||||
}
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
compressIfNeeded,
|
||||
moveToPremanantPath,
|
||||
} from 'lib/media/manip'
|
||||
import {extractDataUriMime} from './util'
|
||||
|
||||
interface PickedFile {
|
||||
uri: string
|
||||
|
@ -138,7 +139,3 @@ function selectFile(opts: PickerOpts): Promise<PickedFile> {
|
|||
input.click()
|
||||
})
|
||||
}
|
||||
|
||||
function extractDataUriMime(uri: string): string {
|
||||
return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';'))
|
||||
}
|
||||
|
|
7
src/lib/media/util.ts
Normal file
7
src/lib/media/util.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function extractDataUriMime(uri: string): string {
|
||||
return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';'))
|
||||
}
|
||||
|
||||
export function getDataUriSize(uri: string): number {
|
||||
return Math.round((uri.length * 3) / 4) // very rough estimate
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import notifee, {EventType} from '@notifee/react-native'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {RootStoreModel} from 'state/models/root-store'
|
||||
import {TabPurpose} from 'state/models/navigation'
|
||||
import {NotificationsViewItemModel} from 'state/models/notifications-view'
|
||||
import {enforceLen} from 'lib/strings/helpers'
|
||||
import {resetToTab} from '../Navigation'
|
||||
|
||||
export function init(store: RootStoreModel) {
|
||||
store.onUnreadNotifications(count => notifee.setBadgeCount(count))
|
||||
|
@ -16,7 +16,7 @@ export function init(store: RootStoreModel) {
|
|||
store.log.debug('Notifee foreground event', {type})
|
||||
if (type === EventType.PRESS) {
|
||||
store.log.debug('User pressed a notifee, opening notifications')
|
||||
store.nav.switchTo(TabPurpose.Notifs, true)
|
||||
resetToTab('NotificationsTab')
|
||||
}
|
||||
})
|
||||
notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import {Alert} from 'react-native'
|
||||
import {
|
||||
check,
|
||||
openSettings,
|
||||
Permission,
|
||||
PermissionStatus,
|
||||
PERMISSIONS,
|
||||
RESULTS,
|
||||
} from 'react-native-permissions'
|
||||
|
||||
export const PHOTO_LIBRARY = PERMISSIONS.IOS.PHOTO_LIBRARY
|
||||
export const CAMERA = PERMISSIONS.IOS.CAMERA
|
||||
|
||||
/**
|
||||
* Returns `true` if the user has granted permission or hasn't made
|
||||
* a decision yet. Returns `false` if unavailable or not granted.
|
||||
*/
|
||||
export async function hasAccess(perm: Permission): Promise<boolean> {
|
||||
const status = await check(perm)
|
||||
return isntANo(status)
|
||||
}
|
||||
|
||||
export async function requestAccessIfNeeded(
|
||||
perm: Permission,
|
||||
): Promise<boolean> {
|
||||
if (await hasAccess(perm)) {
|
||||
return true
|
||||
}
|
||||
let permDescription
|
||||
if (perm === PHOTO_LIBRARY) {
|
||||
permDescription = 'photo library'
|
||||
} else if (perm === CAMERA) {
|
||||
permDescription = 'camera'
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
Alert.alert(
|
||||
'Permission needed',
|
||||
`Bluesky does not have permission to access your ${permDescription}.`,
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{text: 'Open Settings', onPress: () => openSettings()},
|
||||
],
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
export async function requestPhotoAccessIfNeeded() {
|
||||
return requestAccessIfNeeded(PHOTO_LIBRARY)
|
||||
}
|
||||
|
||||
export async function requestCameraAccessIfNeeded() {
|
||||
return requestAccessIfNeeded(CAMERA)
|
||||
}
|
||||
|
||||
function isntANo(status: PermissionStatus): boolean {
|
||||
return status !== RESULTS.UNAVAILABLE && status !== RESULTS.BLOCKED
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
At the moment, Web doesn't have any equivalence for these.
|
||||
*/
|
||||
|
||||
export const PHOTO_LIBRARY = ''
|
||||
export const CAMERA = ''
|
||||
|
||||
export async function hasAccess(_perm: any): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
export async function requestAccessIfNeeded(_perm: any): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
export async function requestPhotoAccessIfNeeded() {
|
||||
return requestAccessIfNeeded(PHOTO_LIBRARY)
|
||||
}
|
||||
|
||||
export async function requestCameraAccessIfNeeded() {
|
||||
return requestAccessIfNeeded(CAMERA)
|
||||
}
|
77
src/lib/routes/helpers.ts
Normal file
77
src/lib/routes/helpers.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import {State, RouteParams} from './types'
|
||||
|
||||
export function getCurrentRoute(state: State) {
|
||||
let node = state.routes[state.index || 0]
|
||||
while (node.state?.routes && typeof node.state?.index === 'number') {
|
||||
node = node.state?.routes[node.state?.index]
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
export function isStateAtTabRoot(state: State | undefined) {
|
||||
if (!state) {
|
||||
// NOTE
|
||||
// if state is not defined it's because init is occuring
|
||||
// and therefore we can safely assume we're at root
|
||||
// -prf
|
||||
return true
|
||||
}
|
||||
const currentRoute = getCurrentRoute(state)
|
||||
return (
|
||||
isTab(currentRoute.name, 'Home') ||
|
||||
isTab(currentRoute.name, 'Search') ||
|
||||
isTab(currentRoute.name, 'Notifications')
|
||||
)
|
||||
}
|
||||
|
||||
export function isTab(current: string, route: string) {
|
||||
// NOTE
|
||||
// our tab routes can be variously referenced by 3 different names
|
||||
// this helper deals with that weirdness
|
||||
// -prf
|
||||
return (
|
||||
current === route ||
|
||||
current === `${route}Tab` ||
|
||||
current === `${route}Inner`
|
||||
)
|
||||
}
|
||||
|
||||
export enum TabState {
|
||||
InsideAtRoot,
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
export function getTabState(state: State | undefined, tab: string): TabState {
|
||||
if (!state) {
|
||||
return TabState.Outside
|
||||
}
|
||||
const currentRoute = getCurrentRoute(state)
|
||||
if (isTab(currentRoute.name, tab)) {
|
||||
return TabState.InsideAtRoot
|
||||
} else if (isTab(state.routes[state.index || 0].name, tab)) {
|
||||
return TabState.Inside
|
||||
}
|
||||
return TabState.Outside
|
||||
}
|
||||
|
||||
export function buildStateObject(
|
||||
stack: string,
|
||||
route: string,
|
||||
params: RouteParams,
|
||||
) {
|
||||
if (stack === 'Flat') {
|
||||
return {
|
||||
routes: [{name: route, params}],
|
||||
}
|
||||
}
|
||||
return {
|
||||
routes: [
|
||||
{
|
||||
name: stack,
|
||||
state: {
|
||||
routes: [{name: route, params}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
55
src/lib/routes/router.ts
Normal file
55
src/lib/routes/router.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import {RouteParams, Route} from './types'
|
||||
|
||||
export class Router {
|
||||
routes: [string, Route][] = []
|
||||
constructor(description: Record<string, string>) {
|
||||
for (const [screen, pattern] of Object.entries(description)) {
|
||||
this.routes.push([screen, createRoute(pattern)])
|
||||
}
|
||||
}
|
||||
|
||||
matchName(name: string): Route | undefined {
|
||||
for (const [screenName, route] of this.routes) {
|
||||
if (screenName === name) {
|
||||
return route
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchPath(path: string): [string, RouteParams] {
|
||||
let name = 'NotFound'
|
||||
let params: RouteParams = {}
|
||||
for (const [screenName, route] of this.routes) {
|
||||
const res = route.match(path)
|
||||
if (res) {
|
||||
name = screenName
|
||||
params = res.params
|
||||
break
|
||||
}
|
||||
}
|
||||
return [name, params]
|
||||
}
|
||||
}
|
||||
|
||||
function createRoute(pattern: string): Route {
|
||||
let matcherReInternal = pattern.replace(
|
||||
/:([\w]+)/g,
|
||||
(_m, name) => `(?<${name}>[^/]+)`,
|
||||
)
|
||||
const matcherRe = new RegExp(`^${matcherReInternal}([?]|$)`, 'i')
|
||||
return {
|
||||
match(path: string) {
|
||||
const res = matcherRe.exec(path)
|
||||
if (res) {
|
||||
return {params: res.groups || {}}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
build(params: Record<string, string>) {
|
||||
return pattern.replace(
|
||||
/:([\w]+)/g,
|
||||
(_m, name) => params[name] || 'undefined',
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
61
src/lib/routes/types.ts
Normal file
61
src/lib/routes/types.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
export type CommonNavigatorParams = {
|
||||
NotFound: undefined
|
||||
Settings: undefined
|
||||
Profile: {name: string}
|
||||
ProfileFollowers: {name: string}
|
||||
ProfileFollows: {name: string}
|
||||
PostThread: {name: string; rkey: string}
|
||||
PostUpvotedBy: {name: string; rkey: string}
|
||||
PostRepostedBy: {name: string; rkey: string}
|
||||
Debug: undefined
|
||||
Log: undefined
|
||||
}
|
||||
|
||||
export type HomeTabNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
}
|
||||
|
||||
export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
Search: undefined
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: undefined
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
HomeTab: undefined
|
||||
Home: undefined
|
||||
SearchTab: undefined
|
||||
Search: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
// NOTE
|
||||
// this isn't strictly correct but it should be close enough
|
||||
// a TS wizard might be able to get this 100%
|
||||
// -prf
|
||||
export type NavigationProp = NativeStackNavigationProp<AllNavigatorParams>
|
||||
|
||||
export type State =
|
||||
| NavigationState
|
||||
| Omit<PartialState<NavigationState>, 'stale'>
|
||||
|
||||
export type RouteParams = Record<string, string>
|
||||
export type MatchResult = {params: RouteParams}
|
||||
export type Route = {
|
||||
match: (path: string) => MatchResult | undefined
|
||||
build: (params: RouteParams) => string
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
|
||||
import {Theme, TypographyVariant} from './ThemeContext'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {DESKTOP_HEADER_HEIGHT} from './constants'
|
||||
|
||||
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
|
||||
export const colors = {
|
||||
|
@ -161,9 +159,7 @@ export const s = StyleSheet.create({
|
|||
// dimensions
|
||||
w100pct: {width: '100%'},
|
||||
h100pct: {height: '100%'},
|
||||
hContentRegion: isDesktopWeb
|
||||
? {height: `calc(100vh - ${DESKTOP_HEADER_HEIGHT}px)`}
|
||||
: {height: '100%'},
|
||||
hContentRegion: {height: '100%'},
|
||||
|
||||
// text align
|
||||
textLeft: {textAlign: 'left'},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue