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

zio/stable
Paul Frazee 2023-10-26 10:40:35 -07:00
commit a1a61ef2e5
13 changed files with 117 additions and 83 deletions

View File

@ -51,10 +51,10 @@ export function init(store: RootStoreModel) {
store.onSessionLoaded(() => { store.onSessionLoaded(() => {
const sess = store.session.currentSession const sess = store.session.currentSession
if (sess) { if (sess) {
if (sess.email) { if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash') store.log.debug('Ping w/hash')
const email_hashed = sha256(sess.email)
segmentClient.identify(email_hashed, {email_hashed})
} else { } else {
store.log.debug('Ping w/o hash') store.log.debug('Ping w/o hash')
segmentClient.identify() segmentClient.identify()

View File

@ -46,10 +46,10 @@ export function init(store: RootStoreModel) {
store.onSessionLoaded(() => { store.onSessionLoaded(() => {
const sess = store.session.currentSession const sess = store.session.currentSession
if (sess) { if (sess) {
if (sess.email) { if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash') store.log.debug('Ping w/hash')
const email_hashed = sha256(sess.email)
segmentClient.identify(email_hashed, {email_hashed})
} else { } else {
store.log.debug('Ping w/o hash') store.log.debug('Ping w/o hash')
segmentClient.identify() segmentClient.identify()

View File

@ -116,6 +116,7 @@ export class PostsFeedItemModel {
}, },
() => this.rootStore.agent.deleteLike(url), () => this.rootStore.agent.deleteLike(url),
) )
track('Post:Unlike')
} else { } else {
// like // like
await updateDataOptimistically( await updateDataOptimistically(
@ -129,11 +130,10 @@ export class PostsFeedItemModel {
this.post.viewer!.like = res.uri this.post.viewer!.like = res.uri
}, },
) )
track('Post:Like')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle like', error) this.rootStore.log.error('Failed to toggle like', error)
} finally {
track(this.post.viewer.like ? 'Post:Unlike' : 'Post:Like')
} }
} }
@ -141,6 +141,7 @@ export class PostsFeedItemModel {
this.post.viewer = this.post.viewer || {} this.post.viewer = this.post.viewer || {}
try { try {
if (this.post.viewer?.repost) { if (this.post.viewer?.repost) {
// unrepost
const url = this.post.viewer.repost const url = this.post.viewer.repost
await updateDataOptimistically( await updateDataOptimistically(
this.post, this.post,
@ -150,7 +151,9 @@ export class PostsFeedItemModel {
}, },
() => this.rootStore.agent.deleteRepost(url), () => this.rootStore.agent.deleteRepost(url),
) )
track('Post:Unrepost')
} else { } else {
// repost
await updateDataOptimistically( await updateDataOptimistically(
this.post, this.post,
() => { () => {
@ -162,11 +165,10 @@ export class PostsFeedItemModel {
this.post.viewer!.repost = res.uri this.post.viewer!.repost = res.uri
}, },
) )
track('Post:Repost')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle repost', error) this.rootStore.log.error('Failed to toggle repost', error)
} finally {
track(this.post.viewer.repost ? 'Post:Unrepost' : 'Post:Repost')
} }
} }
@ -174,13 +176,13 @@ export class PostsFeedItemModel {
try { try {
if (this.isThreadMuted) { if (this.isThreadMuted) {
this.rootStore.mutedThreads.uris.delete(this.rootUri) this.rootStore.mutedThreads.uris.delete(this.rootUri)
track('Post:ThreadUnmute')
} else { } else {
this.rootStore.mutedThreads.uris.add(this.rootUri) this.rootStore.mutedThreads.uris.add(this.rootUri)
track('Post:ThreadMute')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle thread mute', error) this.rootStore.log.error('Failed to toggle thread mute', error)
} finally {
track(this.isThreadMuted ? 'Post:ThreadUnmute' : 'Post:ThreadMute')
} }
} }

View File

@ -18,7 +18,7 @@ import {ListModel} from 'state/models/content/list'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers' import {enforceLen} from 'lib/strings/helpers'
import {compressIfNeeded} from 'lib/media/manip' import {compressIfNeeded} from 'lib/media/manip'
import {UserAvatar} from '../util/UserAvatar' import {EditableUserAvatar} from '../util/UserAvatar'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@ -148,7 +148,7 @@ export function Component({
)} )}
<Text style={[styles.label, pal.text]}>List Avatar</Text> <Text style={[styles.label, pal.text]}>List Avatar</Text>
<View style={[styles.avi, {borderColor: pal.colors.background}]}> <View style={[styles.avi, {borderColor: pal.colors.background}]}>
<UserAvatar <EditableUserAvatar
type="list" type="list"
size={80} size={80}
avatar={avatar} avatar={avatar}

View File

@ -20,7 +20,7 @@ import {enforceLen} from 'lib/strings/helpers'
import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants' import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
import {compressIfNeeded} from 'lib/media/manip' import {compressIfNeeded} from 'lib/media/manip'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
import {UserAvatar} from '../util/UserAvatar' import {EditableUserAvatar} from '../util/UserAvatar'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@ -153,7 +153,7 @@ export function Component({
onSelectNewBanner={onSelectNewBanner} onSelectNewBanner={onSelectNewBanner}
/> />
<View style={[styles.avi, {borderColor: pal.colors.background}]}> <View style={[styles.avi, {borderColor: pal.colors.background}]}>
<UserAvatar <EditableUserAvatar
size={80} size={80}
avatar={userAvatar} avatar={userAvatar}
onSelectNewAvatar={onSelectNewAvatar} onSelectNewAvatar={onSelectNewAvatar}

View File

@ -77,6 +77,8 @@ export function Component({}: {}) {
keyboardAppearance={theme.colorScheme} keyboardAppearance={theme.colorScheme}
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
onSubmitEditing={onPressSignup}
enterKeyHint="done"
accessible={true} accessible={true}
accessibilityLabel="Email" accessibilityLabel="Email"
accessibilityHint="Input your email to get on the Bluesky waitlist" accessibilityHint="Input your email to get on the Bluesky waitlist"

View File

@ -100,7 +100,7 @@ export function Component({
accessibilityHint="Sets image aspect ratio to wide"> accessibilityHint="Sets image aspect ratio to wide">
<RectWideIcon <RectWideIcon
size={24} size={24}
style={as === AspectRatio.Wide ? s.blue3 : undefined} style={as === AspectRatio.Wide ? s.blue3 : pal.text}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@ -110,7 +110,7 @@ export function Component({
accessibilityHint="Sets image aspect ratio to tall"> accessibilityHint="Sets image aspect ratio to tall">
<RectTallIcon <RectTallIcon
size={24} size={24}
style={as === AspectRatio.Tall ? s.blue3 : undefined} style={as === AspectRatio.Tall ? s.blue3 : pal.text}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@ -120,7 +120,7 @@ export function Component({
accessibilityHint="Sets image aspect ratio to square"> accessibilityHint="Sets image aspect ratio to square">
<SquareIcon <SquareIcon
size={24} size={24}
style={as === AspectRatio.Square ? s.blue3 : undefined} style={as === AspectRatio.Square ? s.blue3 : pal.text}
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -132,20 +132,19 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
}, [store, view]) }, [store, view])
const onPressToggleFollow = React.useCallback(() => { const onPressToggleFollow = React.useCallback(() => {
track(
view.viewer.following
? 'ProfileHeader:FollowButtonClicked'
: 'ProfileHeader:UnfollowButtonClicked',
)
view?.toggleFollowing().then( view?.toggleFollowing().then(
() => { () => {
setShowSuggestedFollows(Boolean(view.viewer.following)) setShowSuggestedFollows(Boolean(view.viewer.following))
Toast.show( Toast.show(
`${ `${
view.viewer.following ? 'Following' : 'No longer following' view.viewer.following ? 'Following' : 'No longer following'
} ${sanitizeDisplayName(view.displayName || view.handle)}`, } ${sanitizeDisplayName(view.displayName || view.handle)}`,
) )
track(
view.viewer.following
? 'ProfileHeader:FollowButtonClicked'
: 'ProfileHeader:UnfollowButtonClicked',
)
}, },
err => store.log.error('Failed to toggle follow', err), err => store.log.error('Failed to toggle follow', err),
) )

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {View, StyleSheet, ScrollView, Pressable} from 'react-native' import {View, StyleSheet, Pressable, ScrollView} from 'react-native'
import Animated, { import Animated, {
useSharedValue, useSharedValue,
withTiming, withTiming,
@ -26,6 +26,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {Link} from 'view/com/util/Link' import {Link} from 'view/com/util/Link'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {isWeb} from 'platform/detection'
const OUTER_PADDING = 10 const OUTER_PADDING = 10
const INNER_PADDING = 14 const INNER_PADDING = 14
@ -100,7 +101,6 @@ export function ProfileHeaderSuggestedFollows({
backgroundColor: pal.viewLight.backgroundColor, backgroundColor: pal.viewLight.backgroundColor,
height: '100%', height: '100%',
paddingTop: INNER_PADDING / 2, paddingTop: INNER_PADDING / 2,
paddingBottom: INNER_PADDING,
}}> }}>
<View <View
style={{ style={{
@ -130,11 +130,15 @@ export function ProfileHeaderSuggestedFollows({
</View> </View>
<ScrollView <ScrollView
horizontal horizontal={true}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={isWeb}
persistentScrollbar={true}
scrollIndicatorInsets={{bottom: 0}}
scrollEnabled={true}
contentContainerStyle={{ contentContainerStyle={{
alignItems: 'flex-start', alignItems: 'flex-start',
paddingLeft: INNER_PADDING / 2, paddingLeft: INNER_PADDING / 2,
paddingBottom: INNER_PADDING,
}}> }}>
{isLoading ? ( {isLoading ? (
<> <>

View File

@ -1,5 +1,4 @@
import React, {ComponentProps, useMemo} from 'react' import React, {ComponentProps, memo, useMemo} from 'react'
import {observer} from 'mobx-react-lite'
import { import {
Linking, Linking,
GestureResponderEvent, GestureResponderEvent,
@ -50,7 +49,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
anchorNoUnderline?: boolean anchorNoUnderline?: boolean
} }
export const Link = observer(function Link({ export const Link = memo(function Link({
testID, testID,
style, style,
href, href,
@ -136,7 +135,7 @@ export const Link = observer(function Link({
) )
}) })
export const TextLink = observer(function TextLink({ export const TextLink = memo(function TextLink({
testID, testID,
type = 'md', type = 'md',
style, style,
@ -236,7 +235,7 @@ interface DesktopWebTextLinkProps extends TextProps {
accessibilityHint?: string accessibilityHint?: string
title?: string title?: string
} }
export const DesktopWebTextLink = observer(function DesktopWebTextLink({ export const DesktopWebTextLink = memo(function DesktopWebTextLink({
testID, testID,
type = 'md', type = 'md',
style, style,

View File

@ -23,14 +23,18 @@ interface BaseUserAvatarProps {
type?: Type type?: Type
size: number size: number
avatar?: string | null avatar?: string | null
moderation?: ModerationUI
} }
interface UserAvatarProps extends BaseUserAvatarProps { interface UserAvatarProps extends BaseUserAvatarProps {
onSelectNewAvatar?: (img: RNImage | null) => void moderation?: ModerationUI
}
interface EditableUserAvatarProps extends BaseUserAvatarProps {
onSelectNewAvatar: (img: RNImage | null) => void
} }
interface PreviewableUserAvatarProps extends BaseUserAvatarProps { interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
moderation?: ModerationUI
did: string did: string
handle: string handle: string
} }
@ -106,8 +110,65 @@ export function UserAvatar({
size, size,
avatar, avatar,
moderation, moderation,
onSelectNewAvatar,
}: UserAvatarProps) { }: UserAvatarProps) {
const pal = usePalette('default')
const aviStyle = useMemo(() => {
if (type === 'algo' || type === 'list') {
return {
width: size,
height: size,
borderRadius: size > 32 ? 8 : 3,
}
}
return {
width: size,
height: size,
borderRadius: Math.floor(size / 2),
}
}, [type, size])
const alert = useMemo(() => {
if (!moderation?.alert) {
return null
}
return (
<View style={[styles.alertIconContainer, pal.view]}>
<FontAwesomeIcon
icon="exclamation-circle"
style={styles.alertIcon}
size={Math.floor(size / 3)}
/>
</View>
)
}, [moderation?.alert, size, pal])
return avatar &&
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<View style={{width: size, height: size}}>
<HighPriorityImage
testID="userAvatarImage"
style={aviStyle}
contentFit="cover"
source={{uri: avatar}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
{alert}
</View>
) : (
<View style={{width: size, height: size}}>
<DefaultAvatar type={type} size={size} />
{alert}
</View>
)
}
export function EditableUserAvatar({
type = 'user',
size,
avatar,
onSelectNewAvatar,
}: EditableUserAvatarProps) {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestCameraAccessIfNeeded} = useCameraPermission()
@ -146,7 +207,7 @@ export function UserAvatar({
return return
} }
onSelectNewAvatar?.( onSelectNewAvatar(
await openCamera(store, { await openCamera(store, {
width: 1000, width: 1000,
height: 1000, height: 1000,
@ -186,7 +247,7 @@ export function UserAvatar({
path: item.path, path: item.path,
}) })
onSelectNewAvatar?.(croppedImage) onSelectNewAvatar(croppedImage)
}, },
}, },
!!avatar && { !!avatar && {
@ -203,7 +264,7 @@ export function UserAvatar({
web: 'trash', web: 'trash',
}, },
onPress: async () => { onPress: async () => {
onSelectNewAvatar?.(null) onSelectNewAvatar(null)
}, },
}, },
].filter(Boolean) as DropdownItem[], ].filter(Boolean) as DropdownItem[],
@ -216,23 +277,7 @@ export function UserAvatar({
], ],
) )
const alert = useMemo(() => {
if (!moderation?.alert) {
return null
}
return ( return (
<View style={[styles.alertIconContainer, pal.view]}>
<FontAwesomeIcon
icon="exclamation-circle"
style={styles.alertIcon}
size={Math.floor(size / 3)}
/>
</View>
)
}, [moderation?.alert, size, pal])
// onSelectNewAvatar is only passed as prop on the EditProfile component
return onSelectNewAvatar ? (
<NativeDropdown <NativeDropdown
testID="changeAvatarBtn" testID="changeAvatarBtn"
items={dropdownItems} items={dropdownItems}
@ -256,23 +301,6 @@ export function UserAvatar({
/> />
</View> </View>
</NativeDropdown> </NativeDropdown>
) : avatar &&
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<View style={{width: size, height: size}}>
<HighPriorityImage
testID="userAvatarImage"
style={aviStyle}
contentFit="cover"
source={{uri: avatar}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
{alert}
</View>
) : (
<View style={{width: size, height: size}}>
<DefaultAvatar type={type} size={size} />
{alert}
</View>
) )
} }

View File

@ -63,8 +63,8 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
case 4: case 4:
return ( return (
<>
<View style={styles.flexRow}> <View style={styles.flexRow}>
<View style={{flex: 1}}>
<View style={styles.smallItem}> <View style={styles.smallItem}>
<GalleryItem {...props} index={0} imageStyle={styles.image} /> <GalleryItem {...props} index={0} imageStyle={styles.image} />
</View> </View>
@ -72,7 +72,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
<GalleryItem {...props} index={2} imageStyle={styles.image} /> <GalleryItem {...props} index={2} imageStyle={styles.image} />
</View> </View>
</View> </View>
<View style={{flex: 1}}> <View style={styles.flexRow}>
<View style={styles.smallItem}> <View style={styles.smallItem}>
<GalleryItem {...props} index={1} imageStyle={styles.image} /> <GalleryItem {...props} index={1} imageStyle={styles.image} />
</View> </View>
@ -80,7 +80,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
<GalleryItem {...props} index={3} imageStyle={styles.image} /> <GalleryItem {...props} index={3} imageStyle={styles.image} />
</View> </View>
</View> </View>
</View> </>
) )
default: default:

View File

@ -9,6 +9,7 @@ import {TextLink} from 'view/com/util/Link'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {HELP_DESK_URL} from 'lib/constants'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Support'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Support'>
export const SupportScreen = (_props: Props) => { export const SupportScreen = (_props: Props) => {
@ -29,14 +30,13 @@ export const SupportScreen = (_props: Props) => {
Support Support
</Text> </Text>
<Text style={[pal.text, s.p20]}> <Text style={[pal.text, s.p20]}>
If you need help, email us at{' '} The support form has been moved. If you need help, please
<TextLink <TextLink
href="mailto:support@bsky.app" href={HELP_DESK_URL}
text="support@bsky.app" text=" click here"
style={pal.link} style={pal.link}
/>{' '} />{' '}
with a description of your issue and information about how we can help or visit {HELP_DESK_URL} to get in touch with us.
you.
</Text> </Text>
</CenteredView> </CenteredView>
</View> </View>