Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Into The Wild #6004

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"lodash.isequal": "^4.5.0",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"mix-css-color": "^0.2.0",
"multiformats": "^13.1.0",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
Expand Down Expand Up @@ -200,6 +201,7 @@
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"tldts": "^6.1.46",
"wcag-contrast": "^3.0.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
Expand Down Expand Up @@ -227,6 +229,7 @@
"@types/react-dom": "^18.2.18",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
"@types/wcag-contrast": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^29.7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function InnerApp() {
}, [_])

return (
<Alf theme={theme}>
<Alf themeName={theme}>
<ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
Expand Down
2 changes: 1 addition & 1 deletion src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function InnerApp() {
if (!isReady || !hasCheckedReferrer) return null

return (
<Alf theme={theme}>
<Alf themeName={theme}>
<ThemeProvider theme={theme}>
<RootSiblingParent>
<VideoVolumeProvider>
Expand Down
15 changes: 12 additions & 3 deletions src/alf/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ export const Context = React.createContext<Alf>({

export function ThemeProvider({
children,
theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) {
themeName,
theme,
}: React.PropsWithChildren<{
themeName: ThemeName
theme?: ReturnType<typeof createThemes>
}>) {
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
getFontScale(),
)
Expand All @@ -74,6 +78,7 @@ export function ThemeProvider({
const setFontScaleAndPersist = React.useCallback<
Alf['fonts']['setFontScale']
>(
// eslint-disable-next-line @typescript-eslint/no-shadow
fontScale => {
setFontScale(fontScale)
persistFontScale(fontScale)
Expand All @@ -87,21 +92,25 @@ export function ThemeProvider({
const setFontFamilyAndPersist = React.useCallback<
Alf['fonts']['setFontFamily']
>(
// eslint-disable-next-line @typescript-eslint/no-shadow
fontFamily => {
setFontFamily(fontFamily)
persistFontFamily(fontFamily)
},
[setFontFamily],
)
const themes = React.useMemo(() => {
if (theme) {
return theme
}
return createThemes({
hues: {
primary: BLUE_HUE,
negative: RED_HUE,
positive: GREEN_HUE,
},
})
}, [])
}, [theme])

const value = React.useMemo<Alf>(
() => ({
Expand Down
28 changes: 14 additions & 14 deletions src/components/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {atoms as a, flatten, native, TextStyleProp, web} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText, LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
import {useDynamicColor} from './hooks/useDynamicColor'

const WORD_WRAP = {wordWrap: 1}

Expand All @@ -29,6 +30,7 @@ export type RichTextProps = TextStyleProp &
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
dynamicColor?: boolean
}

export function RichText({
Expand All @@ -43,17 +45,21 @@ export function RichText({
onLinkPress,
interactiveStyle,
emojiMultiplier = 1.85,
dynamicColor = false,
}: RichTextProps) {
const richText = React.useMemo(
() =>
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
[value],
)

const interactiveColor = useDynamicColor({enabled: dynamicColor})

const flattenedStyle = flatten(style)
const plainStyles = [a.leading_snug, flattenedStyle]
const interactiveStyles = [
a.leading_snug,
interactiveColor,
flatten(interactiveStyle),
flattenedStyle,
]
Expand Down Expand Up @@ -182,7 +188,6 @@ function RichTextTag({
selectable?: boolean
authorHandle?: string
} & TextStyleProp) {
const t = useTheme()
const {_} = useLingui()
const control = useTagMenuControl()
const {
Expand Down Expand Up @@ -224,22 +229,17 @@ function RichTextTag({
{...web({
onMouseEnter: onHoverIn,
onMouseLeave: onHoverOut,
onFocus: onFocus,
onBlur: onBlur,
})}
// @ts-ignore
onFocus={onFocus}
onBlur={onBlur}
style={[
web({
cursor: 'pointer',
}),
{color: t.palette.primary_500},
(hovered || focused) && {
...web({
web({cursor: 'pointer'}),
web(
(hovered || focused) && {
outline: 0,
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
},
},
),
style,
]}>
{text}
Expand Down
2 changes: 1 addition & 1 deletion src/components/dms/MessageProfileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function MessageProfileButton({
accessibilityRole="button"
testID="dmBtn"
size="small"
color="secondary"
color="secondary_inverted"
variant="solid"
shape="round"
label={_(msg`Message ${profile.handle}`)}
Expand Down
61 changes: 61 additions & 0 deletions src/components/hooks/useDynamicColor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {useMemo} from 'react'
import {StyleProp, TextStyle} from 'react-native'
import mixColors from 'mix-css-color'
import {rgb as compareContrast} from 'wcag-contrast'

import {useTheme} from '#/alf'

export function useDynamicColor({enabled} = {enabled: false}) {
const t = useTheme()

const color = enabled
? getDynamicColor(t.name !== 'light', t.palette.primary_500)
: t.palette.primary_500

return useMemo(
() =>
({
color,
textDecorationColor: color,
} satisfies StyleProp<TextStyle>),
[color],
)
}

// the goal is to return a blue color that should be an acceptable constrast with
// the background color. the default blue is rgb(16, 131, 254)
// we want to color mix it with the background color
// and flip to whiteish if it's going to be unacceptable low contrast
const BASE_COLOR_LIGHT = 'rgb(16, 131, 254)'
const BASE_COLOR_DARK = 'rgb(32, 139, 254)'
function getDynamicColor(dark: boolean, themeColor: string) {
const baseColor = dark ? BASE_COLOR_DARK : BASE_COLOR_LIGHT
const blendedColor = mixColors(baseColor, themeColor, 75)
debugColorContrast(
blendedColor,
// for debug, we simulate the linear gradient on the profile
mixColors(themeColor, '#ffffff', 30),
)
let [h, s, l] = blendedColor.hsla
return `hsl(${h}, ${s}%, ${l * 1.1}%)`
}

function debugColorContrast(
foreground: ReturnType<typeof mixColors>,
background: ReturnType<typeof mixColors>,
) {
const score = getContrastScore(foreground, background)
console.log(
`%c ${score > 4.5 ? 'PASS' : 'FAIL'}: ${Math.trunc(score * 10) / 10} `,
`color: ${foreground.hex}; background: ${background.hex}`,
)
}

function getContrastScore(
foreground: ReturnType<typeof mixColors>,
background: ReturnType<typeof mixColors>,
) {
const [r1, g1, b1] = foreground.rgba
const [r2, g2, b2] = background.rgba
return compareContrast([r1, g1, b1], [r2, g2, b2])
}
6 changes: 5 additions & 1 deletion src/screens/Profile/Header/ProfileHeaderLabeler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface Props {
moderationOpts: ModerationOpts
hideBackButton?: boolean
isPlaceholderProfile?: boolean
backgroundColor: string
}

let ProfileHeaderLabeler = ({
Expand All @@ -58,6 +59,7 @@ let ProfileHeaderLabeler = ({
moderationOpts,
hideBackButton = false,
isPlaceholderProfile,
backgroundColor,
}: Props): React.ReactNode => {
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
Expand Down Expand Up @@ -168,7 +170,8 @@ let ProfileHeaderLabeler = ({
profile={profile}
moderation={moderation}
hideBackButton={hideBackButton}
isPlaceholderProfile={isPlaceholderProfile}>
isPlaceholderProfile={isPlaceholderProfile}
backgroundColor={backgroundColor}>
<View
style={[a.px_lg, a.pt_md, a.pb_sm]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
Expand Down Expand Up @@ -261,6 +264,7 @@ let ProfileHeaderLabeler = ({
value={descriptionRT}
enableTags
authorHandle={profile.handle}
dynamicColor
/>
</View>
) : undefined}
Expand Down
14 changes: 9 additions & 5 deletions src/screens/Profile/Header/ProfileHeaderStandard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface Props {
moderationOpts: ModerationOpts
hideBackButton?: boolean
isPlaceholderProfile?: boolean
backgroundColor: string
}

let ProfileHeaderStandard = ({
Expand All @@ -54,6 +55,7 @@ let ProfileHeaderStandard = ({
moderationOpts,
hideBackButton = false,
isPlaceholderProfile,
backgroundColor,
}: Props): React.ReactNode => {
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
Expand Down Expand Up @@ -153,7 +155,8 @@ let ProfileHeaderStandard = ({
profile={profile}
moderation={moderation}
hideBackButton={hideBackButton}
isPlaceholderProfile={isPlaceholderProfile}>
isPlaceholderProfile={isPlaceholderProfile}
backgroundColor={backgroundColor}>
<View
style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
Expand All @@ -173,11 +176,10 @@ let ProfileHeaderStandard = ({
<Button
testID="profileHeaderEditProfileButton"
size="small"
color="secondary"
color="secondary_inverted"
variant="solid"
onPress={onPressEditProfile}
label={_(msg`Edit profile`)}
style={[a.rounded_full]}>
label={_(msg`Edit profile`)}>
<ButtonText>
<Trans>Edit Profile</Trans>
</ButtonText>
Expand Down Expand Up @@ -210,7 +212,9 @@ let ProfileHeaderStandard = ({
<Button
testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
size="small"
color={profile.viewer?.following ? 'secondary' : 'primary'}
color={
profile.viewer?.following ? 'secondary_inverted' : 'primary'
}
variant="solid"
label={
profile.viewer?.following
Expand Down
18 changes: 16 additions & 2 deletions src/screens/Profile/Header/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {memo} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
Expand Down Expand Up @@ -29,6 +30,7 @@ interface Props {
moderation: ModerationDecision
hideBackButton?: boolean
isPlaceholderProfile?: boolean
backgroundColor: string
}

let ProfileHeaderShell = ({
Expand All @@ -37,6 +39,7 @@ let ProfileHeaderShell = ({
moderation,
hideBackButton = false,
isPlaceholderProfile,
backgroundColor,
}: React.PropsWithChildren<Props>): React.ReactNode => {
const t = useTheme()
const {currentAccount} = useSession()
Expand Down Expand Up @@ -96,7 +99,18 @@ let ProfileHeaderShell = ({
)

return (
<View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
<View pointerEvents={isIOS ? 'auto' : 'box-none'}>
<LinearGradient
key={t.name}
colors={[t.palette.primary_25, t.palette.primary_25, backgroundColor]}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
<View
pointerEvents={isIOS ? 'auto' : 'box-none'}
style={[a.relative, {height: 150}]}>
Expand Down Expand Up @@ -162,7 +176,7 @@ let ProfileHeaderShell = ({
<View
style={[
t.atoms.bg,
{borderColor: t.atoms.bg.backgroundColor},
{borderColor: t.palette.primary_25},
styles.avi,
profile.associated?.labeler && styles.aviLabeler,
]}>
Expand Down
1 change: 1 addition & 0 deletions src/screens/Profile/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Props {
moderationOpts: ModerationOpts
hideBackButton?: boolean
isPlaceholderProfile?: boolean
backgroundColor: string
}

let ProfileHeader = (props: Props): React.ReactNode => {
Expand Down
Loading
Loading