Merge main into the Web PR (#230)

* Update to RN 71.1.0 (#100)

* Update to RN 71

* Adds missing lint plugin

* Add missing native changes

* Bump @atproto/api@0.0.7 (#112)

* Image not loading on swipe (#114)

* Adds prefetching to images

* Adds image prefetch

* bugfix for images not showing on swipe

* Fixes prefetch bug

* Update src/view/com/util/PostEmbeds.tsx

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Fixes to session management (#117)

* Update session-management to solve incorrectly dropped sessions

* Reset the nav on account switch

* Reset the feed on me.load()

* Update tests to reflect new account-switching behavior

* Increase max image resolutions and sizes (#118)

* Slightly increase the hitslop for post controls

* Fix character counter color in dark mode

* Update login to use new session.create api, which enables email login (close #93) (#119)

* Replaces the alert with dropdown for profile image and banner (#123)

* replaces the alert with dropdown for profile image and banner

* lint

* Fix to ordering of images in the embed grid (#121)

* Add explicit link-embed controls to the composer (#120)

* Add explicit link-embed controls

* Update the target rez/size of link embed thumbs

* Remove the alert before publishing without a link card

* [Draft] Fixes image failing on reupload issue (#128)

* Fixes image failing on reupload issue

* Use tmp folder instead of documents

* lint

* Image performance improvements (#126)

* Switch out most images for FastImage

* Add image loading placeholders

* Fix tests

* Collection of fixes to list rendering (#127)

* Fix bug that caused endless spinners in profile feeds

* Bundle fetches of suggested actors into one update

* Fixes to suggested follow rendering

* Fix missing replacement of flex:1 to height:100

* Fixes to navigation swipes (#129)

* Nav swipe: increase the distance traveled in response to gesture movement.

This causes swipes to feel faster and more responsive.

* Fix: fully clamp the swipe against the edge

* Improve the performance of swipes by skipping the interaction manager

* Adds dark mode to the edit screen (#130)

* Adds dark mode to edit screen

* lint

* lint

* lint

* Reduce render cost of post controls and improve perceived responsiveness (#132)

* Move post control animations into conditional render and increase perceived responsiveness

* Remove log

* Adds dark mode to the dropdown (#131)

* Adds dark mode to the bottom sheet

* Make background button lighter (like before)

* lint

* Fix bug in lightbox rendering (#133)

* Fix layout in onboarding to not overflow the footer

* Configure feed FlatList (removeClippedSubviews=true) to improve scroll performance (#136)

* Disable like/repost animations to see if theyre causing #135 (#137)

* Composer: mention tagging now works in middle of text (close #105) (#139)

* Implement account deletion (#141)

* Fix photo & camera permission management (#140)

* Check photo & camera perms and alert the user if not available (close #64)

- Adds perms checks with a prompt to update settings if needed
- Moves initial access of photos in the composer so that the initial prompt
  occurs at an intuitive time.

* Add react-native-permissions test mock

* Fix issue causing multiple access requests

* Use longer var names

* Update podfile.lock

* Lint fix

* Move photo perm request in composer to the gallery btn instead of when the carousel is opened

* Adds more tracking all around the app (#142)

* Adds more tracking all around the app

* more events

* lint

* using better analytics naming

* missed file

* more fixes

* Calculate image aspect ratio on load (#146)

* Calculate image aspect ratio on load

* Move aspect ratio bounds to constants

* Adds detox testing and instructions (#147)

* Adds detox testing and instructions

* lint

* lint

* Error cleanup (close #79) (#148)

* Avoid surfacing errors to the user when it's not critical

* Remove now-unused GetAssertionsView

* Apply cleanError() consistently

* Give a better error message for Upstream Failures (http status 502)

* Hide errors in notifications because they're not useful

* More e2e tests (create account) (#150)

* Adds respots under the 'post' tab under profile (#158)

* Adds dark mode to delete account screen (#159)

* 87 dark mode edit profile (#162)

* Adds dark mode to delete account screen

* Adds one more missed darkmode

* more fixes

* Remove fallback gradient on external links without thumbs (#164)

* Remove fallback gradient on external links without thumbs

* Remove fallback gradient on external links without thumbs in the composer preview

* Fix refresh behavior around a series of models (repost, graph, vote) (#163)

* Fix refresh behavior around a series of models (repost, graph, vote)

* Fix cursor behavior in reposted-by view

* Fixes issue where retrying on image upload fails (#166)

* Fixes issue where retrying on image upload fails

* Lint, longer test time

* Longer waitfor time in tests

* even longer timeout

* longer timeout

* missed file

* Update src/view/com/composer/ComposePost.tsx

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Update src/view/com/composer/ComposePost.tsx

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* 154 cached image profile (#167)

* Fixes issue where retrying on image upload fails

* Lint, longer test time

* Longer waitfor time in tests

* even longer timeout

* longer timeout

* missed file

* Fixes image cache error on second try for profile screen

* lint

* lint

* lint

* Refactor session management to use a new "Agent" API (#165)

* Add the atp-agent implementation (temporarily in this repo)

* Rewrite all session & API management to use the new atp-agent

* Update tests for the atp-agent refactor

* Refactor management of session-related state. Includes:
- More careful management of when state is cleared or fetched
- Debug logging to help trace future issues
- Clearer APIs overall

* Bubble session-expiration events to the user and display a toast to explain

* Switch to the new @atproto/api@0.1.0

* Minor aesthetic cleanup in SessionModel

* Wire up ReportAccount and ReportPost (#168)

* Fixes embeds for youtube channels (#169)

* Bump app ios version to 1.1 (needed after app store submission)

* Fix potential issues with promise guards when an error occurs (#170)

* Refactor models to use bundleAsync and lock regions (#171)

* Fix to an edge case with feed re-ordering for threads (#172)

* 151 fix youtube channel embed (#173)

* Fixes embeds for youtube channels

* Tests for youtube extract meta

* lint

* Add 'doesnt use non-exempt encryption' to ios config

* Rework the search UI and add  (#174)

* Add search tab and move icon to footer

* Remove subtitles from view header

* Remove unused code

* Clean up UI of search screen

* Search: give better user feedback to UI state and add a cancel button

* Add WhoToFollow section to search

* Add a temporary SuggestedPosts solution using the patented 'bsky team algo'

* Trigger reload of suggested content in search on open

* Wait five min between reloading discovery content

* Reduce weight of solid search icon in footer

* Fix lint

* Fix tests

* 151 feat youtube embed iframe (#176)

* youtube embed iframe temp commit

* Fixes styling and code cleanup

* lint

* Now clicking between the pause and settings button doesn't trigger the parent

* use modest branding (less yt logos)

* Stop playing the video once there's a navigation event

* Make sure the iframe is unmounted on any navigation event

* fixes tests

* lint

* Add scroll-to-top for all screens (#177)

* Adds hardcoded suggested list (#178)

* Adds hardcoded suggested list

* Update suggested-actors-view to support page sizes smaller than the hardcoded list

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* more robust centering of the play button (#181)

Co-authored-by: Aryan Goharzad <arrygoo@gmail.com>

* Bundle of UI modifications (#175)

* Adjust visual balance of SuggestedPosts and WhoToFollow

* Fix bug in the discovery load trigger

* Adjust search header aesthetic and have it scroll away

* More visual balance tweaks on the search page

* Even more visual balance tweaks on the search page

* Hide the footer on scroll in search

* Ditch the composer prompt buttons in the home feed

* Center the view header title

* Hide header on scroll on the home feed

* Fix e2e tests

* Fix home feed positioning (closes #189) (#195)

* Fix home feed positioning for floating header

* Fix positioning of errors in home feed

* Fix lint

* Don't show new-content notification for reposts (close #179) (#197)

* Show the splash screen during session resumption (close #186) (#199)

* Fix to suggested follows: chunk the hardcoded fetches to 25 at a time (close #196) (#198)

* UI updates to the floating action button (#201)

* Update FAB to use a plus icon and not drop shadow

* Update FAB positioning to be more consistent in different shell modes

* Animate the FAB's repositioning

* Remove the 'loading' placeholder from images as it degraded feed perf (#202)

* Remove the 'loading' placeholder from images as it degraded feed perf

* Remove references

* Fix RN bug that causes home feed not to load more; also fix home feed load view. (#208)

RN has a bug where rendering a flatlist with an empty array appears to break its
virtual list windowing behaviors. See https://stackoverflow.com/a/67873596

* Only give the loading spinner on the home feed during PTR (#207)

(cherry picked from commit b7a5da12fdfacef74873b5cf6d75f20d259bde0e)

* Implement our own lifecycle tracking to ensure it never fires while the app is backgrounded (close #193) (#211)

* Push notification fixes (#210)

* Fix to when screen analytics events are firing

* Fix: dont trigger update state when backgrounded

* Small fix to notifee API usage

* Fix: properly load notification info for push card

* Add feedback link to main menu (close #191) (#212)

* Add "follows you" information and sync follow state between views (#215)

* Bump @atproto/api@0.1.2 and update API usage

* Add 'follows you' pill to profile header (close #110)

* Add 'follows you' to followers and follows (close #103)

* Update reposted-by and liked-by views to use the same components as followers and following

* Create a local follows cache MyFollowsModel to keep views in sync (close #205)

* Add incremental hydration to the MyFollows model

* Fix tests

* Update deps

* Fix lint

* Fix to paginated fetches

* Fix reference

* Fix potential state-desync issue

* Fixes to notifications (#216)

* Improve push-notification for follows

* Refresh notifications on screen open (close #214)

* Avoid showing loader more than needed in post threads

* Refactor notification polling to handle view-state more effectively

* Delete a bunch of tests taht werent adding value

* Remove the accounts integration test; we'll use the e2e test instead

* Load latest in notifications when the screen is open rather than full refresh

* Randomize hard-coded suggested follows (#226)

* Ensure follows are loaded before filtering hardcoded suggestions

* Randomize hard-coded suggested profiles (close #219)

* Sanitizes posts on publish and render (#217)

* Sanatizes posts on publish and render

* lint

* lint and added sanitize to thread view as well

* adjusts indices based on replaced text

* Woops, fixes a bug

* bugfix + cleanup

* comment

* lint

* move sanitize text to later in the flow

* undo changes to compose post

* Add RichText library building upon the sanitizePost library method

* Add lodash.clonedeep dep

* Switch to RichText processing on record load & render

* Fix lint

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* A group of notifications fixes (#227)

* Fix: don't group together notifications that can't visually be grouped (close #221)

* Mark all notifications read on PTR

* Small optimization: useCallback and useMemo in posts feed

* Add loading spinner to footer of notifications (close #222)

* Fix to scrolling to posts within a thread (#228)

* Fix: render the entire thread at start so that scrollToIndex works always (close #270)

* Visual fixes to thread 'load more'

* A few small perf improvements to thread rendering

* Fix lint

* 1.2

* Remove unused logger lib

* Remove state-mock

* Type fixes

* Reorganize the folder structure for lib and switch to typescript path aliases

* Move build-flags into lib

* Move to the state path alias

* Add view path alias

* Fix lint

* iOS build fixes

* Wrap analytics in native/web splitter and re-enable in all view code

* Add web version of react-native-webview

* Add web split for version number

* Fix BlurView import for web

* Add web split for fastimage

* Create web split for permissions lib

* Fix for web high priority images

---------

Co-authored-by: Aryan Goharzad <arrygoo@gmail.com>
This commit is contained in:
Paul Frazee 2023-02-22 14:23:57 -06:00 committed by GitHub
parent 7916b26aad
commit f28334739b
242 changed files with 8400 additions and 7454 deletions

View file

@ -1,6 +1,6 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from '../../lib/addStyle'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'

View file

@ -6,8 +6,8 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from './text/Text'
import {UserGroupIcon} from '../../lib/icons'
import {usePalette} from '../../lib/hooks/usePalette'
import {UserGroupIcon} from 'lib/icons'
import {usePalette} from 'lib/hooks/usePalette'
export function EmptyState({
icon,

View file

@ -1,41 +1,53 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
GestureResponderEvent,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {colors, gradients} from '../../lib/styles'
import {useStores} from '../../../state'
import {colors, gradients} from 'lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useStores} from 'state/index'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export const FAB = observer(
({icon, onPress}: {icon: IconProp; onPress: OnPress}) => {
({
testID,
icon,
onPress,
}: {
testID?: string
icon: IconProp
onPress: OnPress
}) => {
const store = useStores()
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 1 : 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, 60)}],
}
return (
<TouchableWithoutFeedback onPress={onPress}>
<View
style={[
styles.outer,
store.shell.minimalShellMode ? styles.lower : undefined,
]}>
<TouchableWithoutFeedback testID={testID} onPress={onPress}>
<Animated.View style={[styles.outer, transform]}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.inner}>
<FontAwesomeIcon
size={24}
icon={icon}
color={colors.white}
style={styles.icon}
/>
<FontAwesomeIcon size={24} icon={icon} color={colors.white} />
</LinearGradient>
</View>
</Animated.View>
</TouchableWithoutFeedback>
)
},
@ -46,16 +58,10 @@ const styles = StyleSheet.create({
position: 'absolute',
zIndex: 1,
right: 22,
bottom: 84,
bottom: 94,
width: 60,
height: 60,
borderRadius: 30,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowOffset: {width: 0, height: 1},
},
lower: {
bottom: 34,
},
inner: {
width: 60,
@ -64,5 +70,4 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
icon: {},
})

View file

@ -10,9 +10,9 @@ import {
ViewStyle,
} from 'react-native'
import {Text} from './text/Text'
import {TypographyVariant} from '../../lib/ThemeContext'
import {useStores, RootStoreModel} from '../../../state'
import {convertBskyAppUrlIfNeeded} from '../../../lib/strings'
import {TypographyVariant} from 'lib/ThemeContext'
import {useStores, RootStoreModel} from 'state/index'
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
export const Link = observer(function Link({
style,
@ -22,17 +22,21 @@ export const Link = observer(function Link({
noFeedback,
}: {
style?: StyleProp<ViewStyle>
href: string
href?: string
title?: string
children?: React.ReactNode
noFeedback?: boolean
}) {
const store = useStores()
const onPress = () => {
handleLink(store, href, false)
if (href) {
handleLink(store, href, false)
}
}
const onLongPress = () => {
handleLink(store, href, true)
if (href) {
handleLink(store, href, true)
}
}
if (noFeedback) {
return (

View file

@ -4,9 +4,9 @@ import {observer} from 'mobx-react-lite'
import LinearGradient from 'react-native-linear-gradient'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Text} from './text/Text'
import {colors, gradients} from '../../lib/styles'
import {colors, gradients} from 'lib/styles'
import {clamp} from 'lodash'
import {useStores} from '../../../state'
import {useStores} from 'state/index'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}

View file

@ -1,7 +1,7 @@
import React from 'react'
import {StyleSheet, TouchableOpacity} from 'react-native'
import {Text} from './text/Text'
import {usePalette} from '../../lib/hooks/usePalette'
import {usePalette} from 'lib/hooks/usePalette'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}

View file

@ -1,10 +1,10 @@
import React from 'react'
import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {HeartIcon} from '../../lib/icons'
import {s} from '../../lib/styles'
import {useTheme} from '../../lib/ThemeContext'
import {usePalette} from '../../lib/hooks/usePalette'
import {HeartIcon} from 'lib/icons'
import {s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
export function LoadingPlaceholder({
width,

View file

@ -16,7 +16,7 @@ import {
} from '@fortawesome/react-native-fontawesome'
import RootSiblings from 'react-native-root-siblings'
import {Text} from './text/Text'
import {colors} from '../../lib/styles'
import {colors} from 'lib/styles'
interface PickerItem {
value: string

View file

@ -1,6 +1,5 @@
import React from 'react'
import {
Animated,
StyleProp,
StyleSheet,
TouchableOpacity,
@ -12,6 +11,11 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'
// DISABLED see #135
// import {
// TriggerableAnimated,
// TriggerableAnimatedRef,
// } from './anim/TriggerableAnimated'
import {Text} from './text/Text'
import {PostDropdownBtn} from './forms/DropdownButton'
import {
@ -19,12 +23,13 @@ import {
HeartIconSolid,
RepostIcon,
CommentBottomArrow,
} from '../../lib/icons'
import {s, colors} from '../../lib/styles'
import {useTheme} from '../../lib/ThemeContext'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
interface PostCtrlsOpts {
itemUri: string
itemCid: string
itemHref: string
itemTitle: string
isAuthor: boolean
@ -36,13 +41,49 @@ interface PostCtrlsOpts {
isReposted: boolean
isUpvoted: boolean
onPressReply: () => void
onPressToggleRepost: () => void
onPressToggleUpvote: () => void
onPressToggleRepost: () => Promise<void>
onPressToggleUpvote: () => Promise<void>
onCopyPostText: () => void
onDeletePost: () => void
}
const HITSLOP = {top: 2, left: 2, bottom: 2, right: 2}
const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
// DISABLED see #135
/*
function ctrlAnimStart(interp: Animated.Value) {
return Animated.sequence([
Animated.timing(interp, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.delay(50),
Animated.timing(interp, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
])
}
function ctrlAnimStyle(interp: Animated.Value) {
return {
transform: [
{
scale: interp.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
}
*/
export function PostCtrls(opts: PostCtrlsOpts) {
const theme = useTheme()
@ -51,76 +92,59 @@ export function PostCtrls(opts: PostCtrlsOpts) {
color: theme.palette.default.postCtrl,
}),
[theme],
)
const interp1 = useAnimatedValue(0)
const interp2 = useAnimatedValue(0)
const anim1Style = {
transform: [
{
scale: interp1.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp1.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
const anim2Style = {
transform: [
{
scale: interp2.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp2.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
) as StyleProp<ViewStyle>
const [repostMod, setRepostMod] = React.useState<number>(0)
const [likeMod, setLikeMod] = React.useState<number>(0)
// DISABLED see #135
// const repostRef = React.useRef<TriggerableAnimatedRef | null>(null)
// const likeRef = React.useRef<TriggerableAnimatedRef | null>(null)
const onPressToggleRepostWrapper = () => {
if (!opts.isReposted) {
ReactNativeHapticFeedback.trigger('impactMedium')
Animated.sequence([
Animated.timing(interp1, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.delay(100),
Animated.timing(interp1, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
]).start()
setRepostMod(1)
opts
.onPressToggleRepost()
.catch(_e => undefined)
.then(() => setRepostMod(0))
// DISABLED see #135
// repostRef.current?.trigger(
// {start: ctrlAnimStart, style: ctrlAnimStyle},
// async () => {
// await opts.onPressToggleRepost().catch(_e => undefined)
// setRepostMod(0)
// },
// )
} else {
setRepostMod(-1)
opts
.onPressToggleRepost()
.catch(_e => undefined)
.then(() => setRepostMod(0))
}
opts.onPressToggleRepost()
}
const onPressToggleUpvoteWrapper = () => {
if (!opts.isUpvoted) {
ReactNativeHapticFeedback.trigger('impactMedium')
Animated.sequence([
Animated.timing(interp2, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.delay(100),
Animated.timing(interp2, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
]).start()
setLikeMod(1)
opts
.onPressToggleUpvote()
.catch(_e => undefined)
.then(() => setLikeMod(0))
// DISABLED see #135
// likeRef.current?.trigger(
// {start: ctrlAnimStart, style: ctrlAnimStyle},
// async () => {
// await opts.onPressToggleUpvote().catch(_e => undefined)
// setLikeMod(0)
// },
// )
} else {
setLikeMod(-1)
opts
.onPressToggleUpvote()
.catch(_e => undefined)
.then(() => setLikeMod(0))
}
opts.onPressToggleUpvote()
}
return (
@ -147,7 +171,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
hitSlop={HITSLOP}
onPress={onPressToggleRepostWrapper}
style={styles.ctrl}>
<Animated.View style={anim1Style}>
<RepostIcon
style={
opts.isReposted || repostMod > 0
? (styles.ctrlIconReposted as StyleProp<ViewStyle>)
: defaultCtrlColor
}
strokeWidth={2.4}
size={opts.big ? 24 : 20}
/>
{
undefined /*DISABLED see #135 <TriggerableAnimated ref={repostRef}>
<RepostIcon
style={
(opts.isReposted
@ -157,15 +191,16 @@ export function PostCtrls(opts: PostCtrlsOpts) {
strokeWidth={2.4}
size={opts.big ? 24 : 20}
/>
</Animated.View>
</TriggerableAnimated>*/
}
{typeof opts.repostCount !== 'undefined' ? (
<Text
style={
opts.isReposted
opts.isReposted || repostMod > 0
? [s.bold, s.green3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.repostCount}
{opts.repostCount + repostMod}
</Text>
) : undefined}
</TouchableOpacity>
@ -175,8 +210,21 @@ export function PostCtrls(opts: PostCtrlsOpts) {
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={onPressToggleUpvoteWrapper}>
<Animated.View style={anim2Style}>
{opts.isUpvoted ? (
{opts.isUpvoted || likeMod > 0 ? (
<HeartIconSolid
style={styles.ctrlIconUpvoted as StyleProp<ViewStyle>}
size={opts.big ? 22 : 16}
/>
) : (
<HeartIcon
style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
strokeWidth={3}
size={opts.big ? 20 : 16}
/>
)}
{
undefined /*DISABLED see #135 <TriggerableAnimated ref={likeRef}>
{opts.isUpvoted || likeMod > 0 ? (
<HeartIconSolid
style={styles.ctrlIconUpvoted as ViewStyle}
size={opts.big ? 22 : 16}
@ -191,15 +239,16 @@ export function PostCtrls(opts: PostCtrlsOpts) {
size={opts.big ? 20 : 16}
/>
)}
</Animated.View>
</TriggerableAnimated>*/
}
{typeof opts.upvoteCount !== 'undefined' ? (
<Text
style={
opts.isUpvoted
opts.isUpvoted || likeMod > 0
? [s.bold, s.red3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.upvoteCount}
{opts.upvoteCount + likeMod}
</Text>
) : undefined}
</TouchableOpacity>
@ -208,6 +257,8 @@ export function PostCtrls(opts: PostCtrlsOpts) {
{opts.big ? undefined : (
<PostDropdownBtn
style={styles.ctrl}
itemUri={opts.itemUri}
itemCid={opts.itemCid}
itemHref={opts.itemHref}
itemTitle={opts.itemTitle}
isAuthor={opts.isAuthor}

View file

@ -0,0 +1,69 @@
import React from 'react'
import {Text} from '../text/Text'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
const ExternalLinkEmbed = ({
link,
onImagePress,
imageChild,
}: {
link: PresentedExternal
onImagePress?: () => void
imageChild?: React.ReactNode
}) => {
const pal = usePalette('default')
return (
<>
{link.thumb ? (
<AutoSizedImage
uri={link.thumb}
style={styles.extImage}
onPress={onImagePress}>
{imageChild}
</AutoSizedImage>
) : undefined}
<View style={styles.extInner}>
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
{link.title || link.uri}
</Text>
<Text
type="sm"
numberOfLines={1}
style={[pal.textLight, styles.extUri]}>
{link.uri}
</Text>
{link.description ? (
<Text
type="sm"
numberOfLines={2}
style={[pal.text, styles.extDescription]}>
{link.description}
</Text>
) : undefined}
</View>
</>
)
}
const styles = StyleSheet.create({
extInner: {
padding: 10,
},
extImage: {
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
width: '100%',
maxHeight: 200,
},
extUri: {
marginTop: 2,
},
extDescription: {
marginTop: 4,
},
})
export default ExternalLinkEmbed

View file

@ -0,0 +1,119 @@
import React, {useEffect} from 'react'
import {useState} from 'react'
import {
View,
StyleSheet,
Pressable,
TouchableWithoutFeedback,
EmitterSubscription,
} from 'react-native'
import YoutubePlayer from 'react-native-youtube-iframe'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ExternalLinkEmbed from './ExternalLinkEmbed'
import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
import {useStores} from 'state/index'
const YoutubeEmbed = ({
link,
videoId,
}: {
videoId: string
link: PresentedExternal
}) => {
const store = useStores()
const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false)
const [playerDimensions, setPlayerDimensions] = useState({
width: 0,
height: 0,
})
const pal = usePalette('default')
const handlePlayButtonPressed = () => {
setDisplayVideoPlayer(true)
}
const handleOnLayout = (event: {
nativeEvent: {layout: {width: any; height: any}}
}) => {
setPlayerDimensions({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
})
}
useEffect(() => {
let sub: EmitterSubscription
if (displayVideoPlayer) {
sub = store.onNavigation(() => {
setDisplayVideoPlayer(false)
})
}
return () => sub && sub.remove()
}, [displayVideoPlayer, store])
const imageChild = (
<Pressable onPress={handlePlayButtonPressed} style={styles.playButton}>
<FontAwesomeIcon icon="play" size={24} color="white" />
</Pressable>
)
if (!displayVideoPlayer) {
return (
<View
style={[styles.extOuter, pal.view, pal.border]}
onLayout={handleOnLayout}>
<ExternalLinkEmbed
link={link}
onImagePress={handlePlayButtonPressed}
imageChild={imageChild}
/>
</View>
)
}
const height = (playerDimensions.width / 16) * 9
const noop = () => {}
return (
<TouchableWithoutFeedback onPress={noop}>
<View>
{/* Removing the outter View will make tap events propagate to parents */}
<YoutubePlayer
initialPlayerParams={{
modestbranding: true,
}}
webViewProps={{
startInLoadingState: true,
}}
height={height}
videoId={videoId}
webViewStyle={styles.webView}
/>
</View>
</TouchableWithoutFeedback>
)
}
const styles = StyleSheet.create({
extOuter: {
borderWidth: 1,
borderRadius: 8,
marginTop: 4,
},
playButton: {
position: 'absolute',
alignSelf: 'center',
alignItems: 'center',
top: '44%',
justifyContent: 'center',
backgroundColor: 'black',
padding: 10,
borderRadius: 50,
opacity: 0.8,
},
webView: {
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
},
})
export default YoutubeEmbed

View file

@ -1,16 +1,22 @@
import React from 'react'
import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
import {
StyleSheet,
StyleProp,
View,
ViewStyle,
Image as RNImage,
} from 'react-native'
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
import LinearGradient from 'react-native-linear-gradient'
import {Link} from '../util/Link'
import {Text} from './text/Text'
import {AutoSizedImage} from './images/AutoSizedImage'
import {ImageLayoutGrid} from './images/ImageLayoutGrid'
import {ImagesLightbox} from '../../../state/models/shell-ui'
import {useStores} from '../../../state'
import {usePalette} from '../../lib/hooks/usePalette'
import {gradients} from '../../lib/styles'
import {saveImageModal} from '../../../lib/images'
import {Link} from '../Link'
import {AutoSizedImage} from '../images/AutoSizedImage'
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 YoutubeEmbed from './YoutubeEmbed'
import ExternalLinkEmbed from './ExternalLinkEmbed'
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
type Embed =
| AppBskyEmbedImages.Presented
@ -35,6 +41,16 @@ export function PostEmbeds({
const onLongPress = (index: number) => {
saveImageModal({uri: uris[index]})
}
const onPressIn = (index: number) => {
const firstImageToShow = uris[index]
RNImage.prefetch(firstImageToShow)
uris.forEach(uri => {
if (firstImageToShow !== uri) {
// First image already prefeched above
RNImage.prefetch(uri)
}
})
}
if (embed.images.length === 4) {
return (
@ -44,6 +60,7 @@ export function PostEmbeds({
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
@ -55,6 +72,7 @@ export function PostEmbeds({
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
@ -66,6 +84,7 @@ export function PostEmbeds({
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
@ -76,7 +95,8 @@ export function PostEmbeds({
uri={embed.images[0].thumb}
onPress={() => openLightbox(0)}
onLongPress={() => onLongPress(0)}
containerStyle={styles.singleImage}
onPressIn={() => onPressIn(0)}
style={styles.singleImage}
/>
</View>
)
@ -85,40 +105,18 @@ export function PostEmbeds({
}
if (AppBskyEmbedExternal.isPresented(embed)) {
const link = embed.external
const youtubeVideoId = getYoutubeVideoId(link.uri)
if (youtubeVideoId) {
return <YoutubeEmbed videoId={youtubeVideoId} link={link} />
}
return (
<Link
style={[styles.extOuter, pal.view, pal.border, style]}
href={link.uri}
noFeedback>
{link.thumb ? (
<AutoSizedImage uri={link.thumb} containerStyle={styles.extImage} />
) : (
<LinearGradient
colors={[gradients.blueDark.start, gradients.blueDark.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.extImage, styles.extImageFallback]}
/>
)}
<View style={styles.extInner}>
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
{link.title || link.uri}
</Text>
<Text
type="sm"
numberOfLines={1}
style={[pal.textLight, styles.extUri]}>
{link.uri}
</Text>
{link.description ? (
<Text
type="sm"
numberOfLines={2}
style={[pal.text, styles.extDescription]}>
{link.description}
</Text>
) : undefined}
</View>
<ExternalLinkEmbed link={link} />
</Link>
)
}
@ -131,28 +129,11 @@ const styles = StyleSheet.create({
},
singleImage: {
borderRadius: 8,
maxHeight: 500,
},
extOuter: {
borderWidth: 1,
borderRadius: 8,
marginTop: 4,
},
extInner: {
padding: 10,
},
extImage: {
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
width: '100%',
maxHeight: 200,
},
extImageFallback: {
height: 160,
},
extUri: {
marginTop: 2,
},
extDescription: {
marginTop: 4,
},
})

View file

@ -1,8 +1,8 @@
import React from 'react'
import {Platform, StyleSheet, View} from 'react-native'
import {Text} from './text/Text'
import {ago} from '../../../lib/strings'
import {usePalette} from '../../lib/hooks/usePalette'
import {ago} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette'
interface PostMetaOpts {
authorHandle: string

View file

@ -6,7 +6,7 @@ import {
View,
} from 'react-native'
import {Text} from './text/Text'
import {usePalette} from '../../lib/hooks/usePalette'
import {usePalette} from 'lib/hooks/usePalette'
interface Layout {
x: number

View file

@ -1,15 +1,23 @@
import React, {useCallback} from 'react'
import {Alert, Image, StyleSheet, TouchableOpacity, View} from 'react-native'
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {HighPriorityImage} from 'view/com/util/images/Image'
import {
openCamera,
openCropper,
openPicker,
PickedMedia,
} from './images/image-crop-picker/ImageCropPicker'
import {useStores} from '../../../state'
import {colors, gradients} from '../../lib/styles'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
} from 'lib/permissions'
import {useStores} from 'state/index'
import {colors, gradients} from 'lib/styles'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
export function UserAvatar({
size,
@ -25,40 +33,9 @@ export function UserAvatar({
onSelectNewAvatar?: (img: PickedMedia) => void
}) {
const store = useStores()
const pal = usePalette('default')
const initials = getInitials(displayName || handle)
const handleEditAvatar = useCallback(() => {
Alert.alert('Select upload method', '', [
{
text: 'Take a new photo',
onPress: () => {
openCamera(store, {
mediaType: 'photo',
width: 1000,
height: 1000,
cropperCircleOverlay: true,
}).then(onSelectNewAvatar)
},
},
{
text: 'Select from gallery',
onPress: () => {
openPicker(store, {
mediaType: 'photo',
}).then(async items => {
await openCropper(store, {
mediaType: 'photo',
path: items[0].path,
width: 1000,
height: 1000,
cropperCircleOverlay: true,
}).then(onSelectNewAvatar)
})
},
},
])
}, [store, onSelectNewAvatar])
const renderSvg = (svgSize: number, svgInitials: string) => (
<Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
<Defs>
@ -80,11 +57,65 @@ export function UserAvatar({
</Svg>
)
const dropdownItems = [
{
label: 'Camera',
icon: 'camera' as IconProp,
onPress: async () => {
if (!(await requestCameraAccessIfNeeded())) {
return
}
onSelectNewAvatar?.(
await openCamera(store, {
mediaType: 'photo',
width: 1000,
height: 1000,
cropperCircleOverlay: true,
}),
)
},
},
{
label: 'Library',
icon: 'image' as IconProp,
onPress: async () => {
if (!(await requestPhotoAccessIfNeeded())) {
return
}
const items = await openPicker(store, {
mediaType: 'photo',
})
onSelectNewAvatar?.(
await openCropper(store, {
mediaType: 'photo',
path: items[0].path,
width: 1000,
height: 1000,
cropperCircleOverlay: true,
}),
)
},
},
// TODO: Remove avatar https://github.com/bluesky-social/social-app/issues/122
// {
// label: 'Remove',
// icon: ['far', 'trash-can'],
// onPress: () => {
// // Remove avatar API call
// },
// },
]
// onSelectNewAvatar is only passed as prop on the EditProfile component
return onSelectNewAvatar ? (
<TouchableOpacity onPress={handleEditAvatar}>
<DropdownButton
type="bare"
items={dropdownItems}
openToRight
rightOffset={-10}
bottomOffset={-10}
menuWidth={170}>
{avatar ? (
<Image
<HighPriorityImage
style={{
width: size,
height: size,
@ -95,16 +126,16 @@ export function UserAvatar({
) : (
renderSvg(size, initials)
)}
<View style={styles.editButtonContainer}>
<View style={[styles.editButtonContainer, pal.btn]}>
<FontAwesomeIcon
icon="camera"
size={12}
style={{color: colors.white}}
color={pal.text.color as string}
/>
</View>
</TouchableOpacity>
</DropdownButton>
) : avatar ? (
<Image
<HighPriorityImage
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
resizeMode="stretch"
source={{uri: avatar}}

View file

@ -1,15 +1,23 @@
import React, {useCallback} from 'react'
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors, gradients} from '../../lib/styles'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import Image from 'view/com/util/images/Image'
import {colors, gradients} from 'lib/styles'
import {
openCamera,
openCropper,
openPicker,
PickedMedia,
} from './images/image-crop-picker/ImageCropPicker'
import {useStores} from '../../../state'
import {useStores} from 'state/index'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
} from 'lib/permissions'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
export function UserBanner({
banner,
@ -19,39 +27,57 @@ export function UserBanner({
onSelectNewBanner?: (img: PickedMedia) => void
}) {
const store = useStores()
const handleEditBanner = useCallback(() => {
Alert.alert('Select upload method', '', [
{
text: 'Take a new photo',
onPress: () => {
openCamera(store, {
const pal = usePalette('default')
const dropdownItems = [
{
label: 'Camera',
icon: 'camera' as IconProp,
onPress: async () => {
if (!(await requestCameraAccessIfNeeded())) {
return
}
onSelectNewBanner?.(
await openCamera(store, {
mediaType: 'photo',
// compressImageMaxWidth: 3000, TODO needed?
width: 3000,
// compressImageMaxHeight: 1000, TODO needed?
height: 1000,
}).then(onSelectNewBanner)
},
}),
)
},
{
text: 'Select from gallery',
onPress: () => {
openPicker(store, {
},
{
label: 'Library',
icon: 'image' as IconProp,
onPress: async () => {
if (!(await requestPhotoAccessIfNeeded())) {
return
}
const items = await openPicker(store, {
mediaType: 'photo',
})
onSelectNewBanner?.(
await openCropper(store, {
mediaType: 'photo',
}).then(async items => {
await openCropper(store, {
mediaType: 'photo',
path: items[0].path,
// compressImageMaxWidth: 3000, TODO needed?
width: 3000,
// compressImageMaxHeight: 1000, TODO needed?
height: 1000,
}).then(onSelectNewBanner)
})
},
path: items[0].path,
// compressImageMaxWidth: 3000, TODO needed?
width: 3000,
// compressImageMaxHeight: 1000, TODO needed?
height: 1000,
}),
)
},
])
}, [store, onSelectNewBanner])
},
// TODO: Remove banner https://github.com/bluesky-social/social-app/issues/122
// {
// label: 'Remove',
// icon: ['far', 'trash-can'],
// onPress: () => {
// // Remove banner api call
// },
// },
]
const renderSvg = () => (
<Svg width="100%" height="150" viewBox="50 0 200 100">
@ -72,20 +98,27 @@ export function UserBanner({
// setUserBanner is only passed as prop on the EditProfile component
return onSelectNewBanner ? (
<TouchableOpacity onPress={handleEditBanner}>
<DropdownButton
type="bare"
items={dropdownItems}
openToRight
rightOffset={-200}
bottomOffset={-10}
menuWidth={170}>
{banner ? (
<Image style={styles.bannerImage} source={{uri: banner}} />
) : (
renderSvg()
)}
<View style={styles.editButtonContainer}>
<View style={[styles.editButtonContainer, pal.btn]}>
<FontAwesomeIcon
icon="camera"
size={12}
style={{color: colors.white}}
color={pal.text.color as string}
/>
</View>
</TouchableOpacity>
</DropdownButton>
) : banner ? (
<Image
style={styles.bannerImage}

View file

@ -4,8 +4,8 @@ import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {Link} from './Link'
import {Text} from './text/Text'
import {LoadingPlaceholder} from './LoadingPlaceholder'
import {useStores} from '../../../state'
import {TypographyVariant} from '../../lib/ThemeContext'
import {useStores} from 'state/index'
import {TypographyVariant} from 'lib/ThemeContext'
export function UserInfoText({
type = 'md',

View file

@ -1,56 +1,40 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {CenteredView} from './Views'
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAvatar} from './UserAvatar'
import {Text} from './text/Text'
import {MagnifyingGlassIcon} from '../../lib/icons'
import {useStores} from '../../../state'
import {usePalette} from '../../lib/hooks/usePalette'
import {colors} from '../../lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useAnalytics} from 'lib/analytics'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
export const ViewHeader = observer(function ViewHeader({
title,
subtitle,
canGoBack,
hideOnScroll,
}: {
title: string
subtitle?: string
canGoBack?: boolean
hideOnScroll?: boolean
}) {
const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics()
const onPressBack = () => {
store.nav.tab.goBack()
}
const onPressMenu = () => {
track('ViewHeader:MenuButtonClicked')
store.shell.setMainMenuOpen(true)
}
const onPressSearch = () => {
store.nav.navigate('/search')
}
const onPressReconnect = () => {
store.session.connect().catch(e => {
store.log.warn('Failed to reconnect to server', e)
})
}
if (typeof canGoBack === 'undefined') {
canGoBack = store.nav.tab.canGoBack
}
return (
<CenteredView style={[styles.header, pal.view]}>
<Container hideOnScroll={hideOnScroll || false}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
@ -75,48 +59,57 @@ export const ViewHeader = observer(function ViewHeader({
<Text type="title" style={[pal.text, styles.title]}>
{title}
</Text>
{subtitle ? (
<Text
type="title-sm"
style={[styles.subtitle, pal.textLight]}
numberOfLines={1}>
{subtitle}
</Text>
) : undefined}
</View>
<TouchableOpacity
onPress={onPressSearch}
hitSlop={HITSLOP}
style={styles.btn}>
<MagnifyingGlassIcon size={21} strokeWidth={3} style={pal.text} />
</TouchableOpacity>
{!store.session.online ? (
<TouchableOpacity style={styles.btn} onPress={onPressReconnect}>
{store.session.attemptingConnect ? (
<ActivityIndicator />
) : (
<>
<FontAwesomeIcon
icon="signal"
style={pal.text as FontAwesomeIconStyle}
size={16}
/>
<FontAwesomeIcon
icon="x"
style={[
styles.littleXIcon,
{backgroundColor: pal.colors.background},
]}
size={8}
/>
</>
)}
</TouchableOpacity>
) : undefined}
</CenteredView>
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
</Container>
)
})
const Container = observer(
({
children,
hideOnScroll,
}: {
children: React.ReactNode
hideOnScroll: boolean
}) => {
const store = useStores()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
React.useEffect(() => {
if (store.shell.minimalShellMode) {
Animated.timing(interp, {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(interp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, -100)}],
}
if (!hideOnScroll) {
return <View style={[styles.header, pal.view]}>{children}</View>
}
return (
<Animated.View
style={[styles.header, pal.view, styles.headerFloating, transform]}>
{children}
</Animated.View>
)
},
)
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
@ -125,20 +118,20 @@ const styles = StyleSheet.create({
paddingTop: 6,
paddingBottom: 6,
},
headerFloating: {
position: 'absolute',
top: 0,
width: '100%',
},
titleContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginLeft: 'auto',
marginRight: 'auto',
paddingRight: 10,
},
title: {
fontWeight: 'bold',
},
subtitle: {
marginLeft: 4,
maxWidth: 200,
fontWeight: 'normal',
},
backBtn: {
width: 30,
@ -152,19 +145,4 @@ const styles = StyleSheet.create({
backIcon: {
marginTop: 6,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: 36,
height: 36,
borderRadius: 20,
marginLeft: 4,
},
littleXIcon: {
color: colors.red3,
position: 'absolute',
right: 7,
bottom: 7,
},
})

View file

@ -1,20 +1,12 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CenteredView} from './Views'
import {Text} from './text/Text'
import {useStores} from '../../../state'
import {usePalette} from '../../lib/hooks/usePalette'
import {colors} from '../../lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {colors} from 'lib/styles'
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
@ -32,11 +24,6 @@ export const ViewHeader = observer(function ViewHeader({
const onPressBack = () => {
store.nav.tab.goBack()
}
const onPressReconnect = () => {
store.session.connect().catch(e => {
store.log.warn('Failed to reconnect to server', e)
})
}
if (typeof canGoBack === 'undefined') {
canGoBack = store.nav.tab.canGoBack
}
@ -76,29 +63,6 @@ export const ViewHeader = observer(function ViewHeader({
</Text>
</View>
)}
{!store.session.online ? (
<TouchableOpacity style={styles.btn} onPress={onPressReconnect}>
{store.session.attemptingConnect ? (
<ActivityIndicator />
) : (
<>
<FontAwesomeIcon
icon="signal"
style={pal.text as FontAwesomeIconStyle}
size={16}
/>
<FontAwesomeIcon
icon="x"
style={[
styles.littleXIcon,
{backgroundColor: pal.colors.background},
]}
size={8}
/>
</>
)}
</TouchableOpacity>
) : undefined}
</CenteredView>
)
})

View file

@ -3,10 +3,10 @@ import {View} from 'react-native'
import {Selector} from './Selector'
import {HorzSwipe} from './gestures/HorzSwipe'
import {FlatList} from './Views'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
import {clamp} from '../../../lib/numbers'
import {s} from '../../lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {clamp} from 'lib/numbers'
import {s} from 'lib/styles'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}
@ -101,6 +101,7 @@ export function ViewSelector({
onRefresh={onRefresh}
onEndReached={onEndReached}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
/>
</HorzSwipe>
)

View file

@ -22,9 +22,8 @@ import {
View,
ViewProps,
} from 'react-native'
import {useTheme} from '../../lib/ThemeContext'
import {addStyle} from '../../lib/addStyle'
import {colors} from '../../lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {addStyle, colors} from 'lib/styles'
export function CenteredView({
style,

View file

@ -0,0 +1,73 @@
import React from 'react'
import {Animated, StyleProp, View, ViewStyle} from 'react-native'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
type CreateAnimFn = (interp: Animated.Value) => Animated.CompositeAnimation
type FinishCb = () => void
interface TriggeredAnimation {
start: CreateAnimFn
style: (
interp: Animated.Value,
) => Animated.WithAnimatedValue<StyleProp<ViewStyle>>
}
export interface TriggerableAnimatedRef {
trigger: (anim: TriggeredAnimation, onFinish?: FinishCb) => void
}
type TriggerableAnimatedProps = React.PropsWithChildren<{}>
type PropsInner = TriggerableAnimatedProps & {
anim: TriggeredAnimation
onFinish: () => void
}
export const TriggerableAnimated = React.forwardRef<
TriggerableAnimatedRef,
TriggerableAnimatedProps
>(({children, ...props}, ref) => {
const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
undefined,
)
const [finishCb, setFinishCb] = React.useState<FinishCb | undefined>(
undefined,
)
React.useImperativeHandle(ref, () => ({
trigger(v: TriggeredAnimation, cb?: FinishCb) {
setFinishCb(() => cb) // note- wrap in function due to react behaviors around setstate
setAnim(v)
},
}))
const onFinish = () => {
finishCb?.()
setAnim(undefined)
setFinishCb(undefined)
}
return (
<View key="triggerable">
{anim ? (
<AnimatingView anim={anim} onFinish={onFinish} {...props}>
{children}
</AnimatingView>
) : (
children
)}
</View>
)
})
function AnimatingView({
anim,
onFinish,
children,
}: React.PropsWithChildren<PropsInner>) {
const interp = useAnimatedValue(0)
React.useEffect(() => {
anim?.start(interp).start(() => {
onFinish()
})
})
const animStyle = anim?.style(interp)
return <Animated.View style={animStyle}>{children}</Animated.View>
}

View file

@ -11,8 +11,8 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
export function ErrorMessage({
message,

View file

@ -5,9 +5,9 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {colors} from '../../../lib/styles'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
export function ErrorScreen({
title,

View file

@ -7,8 +7,8 @@ import {
ViewStyle,
} from 'react-native'
import {Text} from '../text/Text'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
import {useTheme} from 'lib/ThemeContext'
import {choose} from 'lib/functions'
export type ButtonType =
| 'primary'

View file

@ -13,11 +13,13 @@ import RootSiblings from 'react-native-root-siblings'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {colors} from '../../../lib/styles'
import {toShareUrl} from '../../../../lib/strings'
import {useStores} from '../../../../state'
import {ReportPostModal, ConfirmModal} from '../../../../state/models/shell-ui'
import {TABS_ENABLED} from '../../../../build-flags'
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'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
@ -36,6 +38,9 @@ export function DropdownButton({
label,
menuWidth,
children,
openToRight = false,
rightOffset = 0,
bottomOffset = 0,
}: {
type?: DropdownButtonType
style?: StyleProp<ViewStyle>
@ -43,6 +48,9 @@ export function DropdownButton({
label?: string
menuWidth?: number
children?: React.ReactNode
openToRight?: boolean
rightOffset?: number
bottomOffset?: number
}) {
const ref = useRef<TouchableOpacity>(null)
@ -59,12 +67,11 @@ export function DropdownButton({
if (!menuWidth) {
menuWidth = 200
}
createDropdownMenu(
pageX + width - menuWidth,
pageY + height,
menuWidth,
items,
)
const newX = openToRight
? pageX + width + rightOffset
: pageX + width - menuWidth
const newY = pageY + height + bottomOffset
createDropdownMenu(newX, newY, menuWidth, items)
},
)
}
@ -97,6 +104,8 @@ export function DropdownButton({
export function PostDropdownBtn({
style,
children,
itemUri,
itemCid,
itemHref,
isAuthor,
onCopyPostText,
@ -104,6 +113,8 @@ export function PostDropdownBtn({
}: {
style?: StyleProp<ViewStyle>
children?: React.ReactNode
itemUri: string
itemCid: string
itemHref: string
itemTitle: string
isAuthor: boolean
@ -140,7 +151,7 @@ export function PostDropdownBtn({
icon: 'circle-exclamation',
label: 'Report post',
onPress() {
store.shell.openModal(new ReportPostModal(itemHref))
store.shell.openModal(new ReportPostModal(itemUri, itemCid))
},
},
isAuthor
@ -180,24 +191,14 @@ function createDropdownMenu(
const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings(
(
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={styles.bg} />
</TouchableWithoutFeedback>
<View style={[styles.menu, {left: x, top: y, width}]}>
{items.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.menuItem]}
onPress={() => onPressItem(index)}>
{item.icon && (
<FontAwesomeIcon style={styles.icon} icon={item.icon} />
)}
<Text style={styles.label}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
</>
<DropdownItems
onOuterPress={onOuterPress}
x={x}
y={y}
width={width}
items={items}
onPressItem={onPressItem}
/>
),
)
return sibling
@ -241,3 +242,55 @@ const styles = StyleSheet.create({
fontSize: 18,
},
})
type DropDownItemProps = {
onOuterPress: () => void
x: number
y: number
width: number
items: DropdownItem[]
onPressItem: (index: number) => void
}
const DropdownItems = ({
onOuterPress,
x,
y,
width,
items,
onPressItem,
}: DropDownItemProps) => {
const pal = usePalette('default')
const theme = useTheme()
const dropDownBackgroundColor =
theme.colorScheme === 'dark' ? pal.btn : pal.view
return (
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={[styles.bg]} />
</TouchableWithoutFeedback>
<View
style={[
styles.menu,
{left: x, top: y, width},
dropDownBackgroundColor,
]}>
{items.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.menuItem]}
onPress={() => onPressItem(index)}>
{item.icon && (
<FontAwesomeIcon
style={styles.icon}
icon={item.icon}
color={pal.text.color as string}
/>
)}
<Text style={[styles.label, pal.text]}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
</>
)
}

View file

@ -2,8 +2,8 @@ import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
import {useTheme} from 'lib/ThemeContext'
import {choose} from 'lib/functions'
export function RadioButton({
type = 'default-light',

View file

@ -2,7 +2,7 @@ import React, {useState} from 'react'
import {View} from 'react-native'
import {RadioButton} from './RadioButton'
import {ButtonType} from './Button'
import {s} from '../../../lib/styles'
import {s} from 'lib/styles'
export interface RadioGroupItem {
label: string

View file

@ -2,9 +2,9 @@ import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
import {colors} from '../../../lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {choose} from 'lib/functions'
import {colors} from 'lib/styles'
export function ToggleButton({
type = 'default-light',

View file

@ -9,7 +9,7 @@ import {
View,
} from 'react-native'
import {clamp} from 'lodash'
import {s} from '../../../lib/styles'
import {s} from 'lib/styles'
interface Props {
panX: Animated.Value
@ -90,6 +90,7 @@ export function HorzSwipe({
// swiping right
(diffX < 0 && !canSwipeRight)
) {
panX.setValue(0)
return
}
@ -119,6 +120,7 @@ export function HorzSwipe({
toValue: final,
duration: 100,
useNativeDriver,
isInteraction: false,
}).start(() => {
onSwipeEnd?.(final)
panX.flattenOffset()
@ -130,6 +132,7 @@ export function HorzSwipe({
toValue: 0,
duration: 100,
useNativeDriver,
isInteraction: false,
}).start(() => {
panX.flattenOffset()
panX.setValue(0)

View file

@ -9,7 +9,7 @@ import {
View,
} from 'react-native'
import {clamp} from 'lodash'
import {s} from '../../../lib/styles'
import {s} from 'lib/styles'
export enum Dir {
None,

View file

@ -1,125 +1,59 @@
import React, {useState, useEffect} from 'react'
import {
Image,
ImageStyle,
LayoutChangeEvent,
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {Text} from '../text/Text'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
import {DELAY_PRESS_IN} from './constants'
import React from 'react'
import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
import Image, {OnLoadEvent} from 'view/com/util/images/Image'
import {clamp} from 'lib/numbers'
const MAX_HEIGHT = 300
interface Dim {
width: number
height: number
}
export const DELAY_PRESS_IN = 500
const MIN_ASPECT_RATIO = 0.33 // 1/3
const MAX_ASPECT_RATIO = 5 // 5/1
export function AutoSizedImage({
uri,
onPress,
onLongPress,
onPressIn,
style,
containerStyle,
children = null,
}: {
uri: string
onPress?: () => void
onLongPress?: () => void
style?: StyleProp<ImageStyle>
containerStyle?: StyleProp<ViewStyle>
onPressIn?: () => void
style?: StyleProp<ViewStyle>
children?: React.ReactNode
}) {
const theme = useTheme()
const errPal = usePalette('error')
const [error, setError] = useState<string | undefined>('')
const [imgInfo, setImgInfo] = useState<Dim | undefined>()
const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
useEffect(() => {
let aborted = false
if (!imgInfo) {
Image.getSize(
uri,
(width: number, height: number) => {
if (!aborted) {
setImgInfo({width, height})
}
},
(err: any) => {
if (!aborted) {
setError(String(err))
}
},
)
}
return () => {
aborted = true
}
}, [uri, imgInfo])
const onLayout = (evt: LayoutChangeEvent) => {
setContainerInfo({
width: evt.nativeEvent.layout.width,
height: evt.nativeEvent.layout.height,
})
}
let calculatedStyle: StyleProp<ImageStyle> | undefined
if (imgInfo && containerInfo) {
// imgInfo.height / imgInfo.width = x / containerInfo.width
// x = imgInfo.height / imgInfo.width * containerInfo.width
calculatedStyle = {
height: Math.min(
MAX_HEIGHT,
(imgInfo.height / imgInfo.width) * containerInfo.width,
const [aspectRatio, setAspectRatio] = React.useState<number>(1)
const onLoad = (e: OnLoadEvent) => {
setAspectRatio(
clamp(
e.nativeEvent.width / e.nativeEvent.height,
MIN_ASPECT_RATIO,
MAX_ASPECT_RATIO,
),
}
)
}
return (
<View style={style}>
<TouchableOpacity
onPress={onPress}
onLongPress={onLongPress}
delayPressIn={DELAY_PRESS_IN}>
{error ? (
<View style={[styles.errorContainer, errPal.view, containerStyle]}>
<Text style={errPal.text}>{error}</Text>
</View>
) : calculatedStyle ? (
<View style={[styles.container, containerStyle]}>
<Image style={calculatedStyle} source={{uri}} />
</View>
) : (
<View
style={[
style,
styles.placeholder,
{backgroundColor: theme.palette.default.backgroundLight},
]}
onLayout={onLayout}
/>
)}
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
style={[styles.container, style]}>
<Image
style={[styles.image, {aspectRatio}]}
source={{uri}}
onLoad={onLoad}
/>
{children}
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
placeholder: {
width: '100%',
aspectRatio: 1,
},
errorContainer: {
paddingHorizontal: 12,
paddingVertical: 8,
},
container: {
overflow: 'hidden',
},
image: {
width: '100%',
},
})

View file

@ -0,0 +1,12 @@
import React from 'react'
import FastImage, {FastImageProps, Source} from 'react-native-fast-image'
export default FastImage
export type {OnLoadEvent, ImageStyle, Source} from 'react-native-fast-image'
export function HighPriorityImage({source, ...props}: FastImageProps) {
const updatedSource = {
uri: typeof source === 'object' && source ? source.uri : '',
priority: FastImage.priority.high,
} as Source
return <FastImage source={updatedSource} {...props} />
}

View file

@ -0,0 +1,11 @@
import {
Image,
NativeSyntheticEvent,
ImageLoadEventData,
ImageSourcePropType,
} from 'react-native'
export default Image
export const HighPriorityImage = Image
export type OnLoadEvent = NativeSyntheticEvent<ImageLoadEventData>
export type Source = ImageSourcePropType
export type {ImageStyle} from 'react-native'

View file

@ -1,12 +1,12 @@
import React from 'react'
import {
Image,
StyleProp,
StyleSheet,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import Image from 'view/com/util/images/Image'
export function ImageHorzList({
uris,

View file

@ -1,7 +1,5 @@
import React from 'react'
import {
Image,
ImageStyle,
LayoutChangeEvent,
StyleProp,
StyleSheet,
@ -9,7 +7,9 @@ import {
View,
ViewStyle,
} from 'react-native'
import {DELAY_PRESS_IN} from './constants'
import Image, {ImageStyle} from 'view/com/util/images/Image'
export const DELAY_PRESS_IN = 500
interface Dim {
width: number
@ -23,12 +23,14 @@ export function ImageLayoutGrid({
uris,
onPress,
onLongPress,
onPressIn,
style,
}: {
type: ImageLayoutGridType
uris: string[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
style?: StyleProp<ViewStyle>
}) {
const [containerInfo, setContainerInfo] = React.useState<Dim | undefined>()
@ -47,6 +49,7 @@ export function ImageLayoutGrid({
type={type}
uris={uris}
onPress={onPress}
onPressIn={onPressIn}
onLongPress={onLongPress}
containerInfo={containerInfo}
/>
@ -60,15 +63,17 @@ function ImageLayoutGridInner({
uris,
onPress,
onLongPress,
onPressIn,
containerInfo,
}: {
type: ImageLayoutGridType
uris: string[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
containerInfo: Dim
}) {
const size1 = React.useMemo<ImageStyle>(() => {
const size1 = React.useMemo<StyleProp<ImageStyle>>(() => {
if (type === 'three') {
const size = (containerInfo.width - 10) / 3
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
@ -77,7 +82,7 @@ function ImageLayoutGridInner({
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [type, containerInfo])
const size2 = React.useMemo<ImageStyle>(() => {
const size2 = React.useMemo<StyleProp<ImageStyle>>(() => {
if (type === 'three') {
const size = ((containerInfo.width - 10) / 3) * 2 + 5
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
@ -93,6 +98,7 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size1} />
</TouchableOpacity>
@ -100,6 +106,7 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
</TouchableOpacity>
@ -112,6 +119,7 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size2} />
</TouchableOpacity>
@ -120,6 +128,7 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
</TouchableOpacity>
@ -127,6 +136,7 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} />
</TouchableOpacity>
@ -141,29 +151,33 @@ function ImageLayoutGridInner({
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size1} />
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} />
</TouchableOpacity>
</View>
<View style={styles.wSpace} />
<View>
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(2)}
onLongPress={() => onLongPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} />
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
delayPressIn={DELAY_PRESS_IN}
onPress={() => onPress?.(3)}
onPressIn={() => onPressIn?.(3)}
onLongPress={() => onLongPress?.(3)}>
<Image source={{uri: uris[3]}} style={size1} />
</TouchableOpacity>

View file

@ -1 +0,0 @@
export const DELAY_PRESS_IN = 500

View file

@ -4,7 +4,7 @@ import {
openCropper as openCropperFn,
ImageOrVideo,
} from 'react-native-image-crop-picker'
import {RootStoreModel} from '../../../../../state'
import {RootStoreModel} from 'state/index'
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
export type {PickedMedia} from './types'

View file

@ -1,9 +1,9 @@
/// <reference lib="dom" />
import {CropImageModal} from '../../../../../state/models/shell-ui'
import {CropImageModal} from 'state/models/shell-ui'
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
export type {PickedMedia} from './types'
import {RootStoreModel} from '../../../../../state'
import {RootStoreModel} from 'state/index'
interface PickedFile {
uri: string
@ -31,17 +31,17 @@ export async function openPicker(
export async function openCamera(
_store: RootStoreModel,
opts: CameraOpts,
_opts: CameraOpts,
): Promise<PickedMedia> {
const mediaType = opts.mediaType || 'photo'
// const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO')
}
export async function openCropper(
_store: RootStoreModel,
opts: CropperOpts,
_opts: CropperOpts,
): Promise<PickedMedia> {
const mediaType = opts.mediaType || 'photo'
// const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO')
}

View file

@ -2,29 +2,21 @@ import React from 'react'
import {TextStyle, StyleProp} from 'react-native'
import {TextLink} from '../Link'
import {Text} from './Text'
import {lh} from '../../../lib/styles'
import {toShortUrl} from '../../../../lib/strings'
import {useTheme, TypographyVariant} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
type TextSlice = {start: number; end: number}
type Entity = {
index: TextSlice
type: string
value: string
}
import {lh} from 'lib/styles'
import {toShortUrl} from 'lib/strings/url-helpers'
import {RichText as RichTextObj, Entity} from 'lib/strings/rich-text'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
export function RichText({
type = 'md',
text,
entities,
richText,
lineHeight = 1.2,
style,
numberOfLines,
}: {
type?: TypographyVariant
text: string
entities?: Entity[]
richText?: RichTextObj
lineHeight?: number
style?: StyleProp<TextStyle>
numberOfLines?: number
@ -32,6 +24,12 @@ export function RichText({
const theme = useTheme()
const pal = usePalette('default')
const lineHeightStyle = lh(theme, type, lineHeight)
if (!richText) {
return null
}
const {text, entities} = richText
if (!entities?.length) {
if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
style = {

View file

@ -1,7 +1,7 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {s} from '../../../lib/styles'
import {useTheme, TypographyVariant} from '../../../lib/ThemeContext'
import {s} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
export type CustomTextProps = TextProps & {
type?: TypographyVariant