From 25f268a1c018066ec3b3a2e755087a1f976a5805 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 16 Jan 2025 14:11:15 +0100 Subject: [PATCH] feat(Button): add `link.bare` variant, enable style override and style combination - Add text.bare variant to IButtonVariant type - Add special case in getButtonViewStyle for text.bare that removes padding and decorations - Keep existing button variants and their styling intact - Use text.bare variant in Profile screen's Edit button This allows for buttons without padding while maintaining the existing button system. --- design-system/Button/Button.props.ts | 1 + design-system/Button/Button.styles.ts | 49 ++++++---- design-system/Button/Button.tsx | 89 +++++++++++-------- .../utils/__tests__/formatUsername.test.ts | 29 ++++++ features/profiles/utils/formatUsername.ts | 26 ++++++ screens/Profile.tsx | 79 +++++++++++----- 6 files changed, 194 insertions(+), 79 deletions(-) create mode 100644 features/profiles/utils/__tests__/formatUsername.test.ts create mode 100644 features/profiles/utils/formatUsername.ts diff --git a/design-system/Button/Button.props.ts b/design-system/Button/Button.props.ts index 1c2db1ded..a22acfb9a 100644 --- a/design-system/Button/Button.props.ts +++ b/design-system/Button/Button.props.ts @@ -14,6 +14,7 @@ export type IButtonVariant = | "outline" | "fill" | "link" + | "link.bare" /** @deprecated */ | "secondary" | "secondary-danger" diff --git a/design-system/Button/Button.styles.ts b/design-system/Button/Button.styles.ts index 2f2ca092e..ce63645b3 100644 --- a/design-system/Button/Button.styles.ts +++ b/design-system/Button/Button.styles.ts @@ -35,16 +35,21 @@ export const getButtonViewStyle = variant, size, action, - pressed = false, + pressed, disabled = false, - }: IButtonStyleProps) => - (theme: Theme): ViewStyle => { - const { spacing, colors, borderRadius } = theme; - - const style: ViewStyle = { + }: { + variant: IButtonVariant; + size: IButtonSize; + action: IButtonAction; + pressed: boolean; + disabled?: boolean; + }) => + ({ spacing, colors, borderRadius }: Theme): ViewStyle => { + const baseStyle: ViewStyle = { flexDirection: "row", - justifyContent: "center", alignItems: "center", + justifyContent: "center", + gap: spacing.xxs, borderRadius: borderRadius.sm, overflow: "hidden", paddingVertical: @@ -53,37 +58,47 @@ export const getButtonViewStyle = size === "md" || size === "sm" ? spacing.xs : spacing.sm, }; + // Special case for bare link text buttons - no padding or other decorations + if (variant === "link.bare") { + return { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: spacing.xxs, + }; + } + if (action === "primary") { switch (variant) { case "fill": - style.backgroundColor = colors.fill.primary; + baseStyle.backgroundColor = colors.fill.primary; if (pressed) { - style.backgroundColor = colors.fill.secondary; + baseStyle.backgroundColor = colors.fill.secondary; } if (disabled) { - style.backgroundColor = colors.fill.tertiary; + baseStyle.backgroundColor = colors.fill.tertiary; } break; case "outline": - style.borderWidth = 1; - style.borderColor = colors.border.secondary; - style.backgroundColor = "transparent"; + baseStyle.borderWidth = 1; + baseStyle.borderColor = colors.border.secondary; + baseStyle.backgroundColor = "transparent"; if (pressed) { - style.backgroundColor = colors.fill.minimal; + baseStyle.backgroundColor = colors.fill.minimal; } break; case "link": case "text": - style.backgroundColor = "transparent"; // Put back when we're done refactoring all the variant="text" button // if (pressed) { // style.backgroundColor = colors.fill.minimal; // } // Temporary opacity change for the variant="text" button + baseStyle.backgroundColor = "transparent"; if (pressed) { - style.opacity = 0.8; + baseStyle.opacity = 0.8; } break; @@ -92,7 +107,7 @@ export const getButtonViewStyle = } } - return style; + return baseStyle; }; export const getButtonTextStyle = diff --git a/design-system/Button/Button.tsx b/design-system/Button/Button.tsx index 8b41dc1f2..ed8ca9728 100644 --- a/design-system/Button/Button.tsx +++ b/design-system/Button/Button.tsx @@ -52,48 +52,54 @@ export function Button(props: IButtonProps) { const variant: IButtonVariant = props.variant ?? "fill"; - const $viewStyle = useCallback( - ({ pressed }: PressableStateCallbackType): StyleProp => { - return [ - themed( - getButtonViewStyle({ variant, size, action: "primary", pressed }) - ), - $viewStyleOverride, - pressed && $pressedViewStyleOverride, - disabled && $disabledViewStyleOverride, - ]; - }, - [ - themed, - variant, - size, + const $viewStyle = useMemo( + () => [ + themed( + getButtonViewStyle({ variant, size, action: "primary", pressed: false }) + ), $viewStyleOverride, + ], + [themed, variant, size, $viewStyleOverride] + ); + + const $pressedViewStyle = useMemo( + () => [ + themed( + getButtonViewStyle({ variant, size, action: "primary", pressed: true }) + ), $pressedViewStyleOverride, - $disabledViewStyleOverride, - disabled, - ] + ], + [themed, variant, size, $pressedViewStyleOverride] ); - const $textStyle = useCallback( - ({ pressed }: PressableStateCallbackType): StyleProp => { - return [ - themed( - getButtonTextStyle({ variant, size, action: "primary", pressed }) - ), - $textStyleOverride, - pressed && $pressedTextStyleOverride, - disabled && $disabledTextStyleOverride, - ]; - }, - [ - themed, - variant, - size, + const $disabledViewStyle = useMemo( + () => [$disabledViewStyleOverride], + [$disabledViewStyleOverride] + ); + + const $combinedTextStyle = useMemo( + () => [ + themed( + getButtonTextStyle({ variant, size, action: "primary", pressed: false }) + ), $textStyleOverride, + ], + [themed, variant, size, $textStyleOverride] + ); + + const $pressedTextStyle = useMemo( + () => [ + themed( + getButtonTextStyle({ variant, size, action: "primary", pressed: true }) + ), $pressedTextStyleOverride, - $disabledTextStyleOverride, - disabled, - ] + ], + [themed, variant, size, $pressedTextStyleOverride] + ); + + const $disabledTextStyle = useMemo( + () => [$disabledTextStyleOverride], + [$disabledTextStyleOverride] ); const handlePress = useCallback( @@ -117,7 +123,10 @@ export function Button(props: IButtonProps) { return ( [ + pressed ? $pressedViewStyle : $viewStyle, + disabled && $disabledViewStyle, + ]} accessibilityRole="button" accessibilityState={{ disabled: !!disabled }} onPress={handlePress} @@ -139,7 +148,6 @@ export function Button(props: IButtonProps) { /> )} - {/* @deprecated stuff */} {!!_icon && ( {children} diff --git a/features/profiles/utils/__tests__/formatUsername.test.ts b/features/profiles/utils/__tests__/formatUsername.test.ts new file mode 100644 index 000000000..68e6beecb --- /dev/null +++ b/features/profiles/utils/__tests__/formatUsername.test.ts @@ -0,0 +1,29 @@ +import { formatUsername } from "../formatUsername"; + +describe("formatUsername", () => { + it("should return undefined when no username provided", () => { + const result = formatUsername(undefined); + expect(result).toBeUndefined(); + }); + + it("should format .conversedev.eth username by extracting first part and adding @ prefix", () => { + const result = formatUsername("louisdev.conversedev.eth"); + expect(result).toBe("@louisdev"); + }); + + it("should format .converse.xyz username by extracting first part and adding @ prefix", () => { + const result = formatUsername("louisdev.converse.xyz"); + expect(result).toBe("@louisdev"); + }); + + it("should return undefined for non-Converse usernames", () => { + expect(formatUsername("louisdev.eth")).toBeUndefined(); + expect(formatUsername("louisdev")).toBeUndefined(); + expect(formatUsername("@louisdev")).toBeUndefined(); + }); + + it("should return undefined for empty string", () => { + const result = formatUsername(""); + expect(result).toBeUndefined(); + }); +}); diff --git a/features/profiles/utils/formatUsername.ts b/features/profiles/utils/formatUsername.ts new file mode 100644 index 000000000..6d66f701d --- /dev/null +++ b/features/profiles/utils/formatUsername.ts @@ -0,0 +1,26 @@ +/** + * Formats usernames from Converse domains by extracting the username part and adding @ prefix + * Returns undefined for non-Converse usernames + * @param username - The username to format + * @returns The formatted username with @ prefix if it's a Converse username, undefined otherwise + */ +export function formatUsername( + username: string | undefined +): string | undefined { + if (!username) return undefined; + + // Check if it's a Converse username (either domain) + if ( + username.endsWith(".conversedev.eth") || + username.endsWith(".converse.xyz") + ) { + // Extract everything before the domain + const cleanUsername = username + .replace(/\.conversedev\.eth$/, "") + .replace(/\.converse\.xyz$/, ""); + return `@${cleanUsername}`; + } + + // Return undefined for non-Converse usernames + return undefined; +} diff --git a/screens/Profile.tsx b/screens/Profile.tsx index 5496f2917..5c76d614a 100644 --- a/screens/Profile.tsx +++ b/screens/Profile.tsx @@ -100,6 +100,8 @@ import Animated, { } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { VStack } from "@/design-system/VStack"; +import { formatUsername } from "@/features/profiles/utils/formatUsername"; +import { Button } from "@/design-system/Button/Button"; export default function ProfileScreen() { return ( @@ -138,20 +140,22 @@ const ExternalWalletPickerWrapper = memo( * Includes name, bio, avatar with interactive animations. */ const ContactCard = memo(function ContactCard({ - name, - bio, + displayName, + userName, avatarUri, + isMyProfile, }: { - name: string; - bio?: string; + displayName: string; + userName?: string; avatarUri?: string; + isMyProfile?: boolean; }) { const { theme } = useAppTheme(); const rotateX = useSharedValue(0); const rotateY = useSharedValue(0); const shadowOffsetX = useSharedValue(0); - const shadowOffsetY = useSharedValue(6); // Positive value pushes shadow down + const shadowOffsetY = useSharedValue(6); const baseStyle = { backgroundColor: theme.colors.fill.primary, @@ -163,6 +167,7 @@ const ContactCard = memo(function ContactCard({ shadowOpacity: 0.25, shadowRadius: 12, elevation: 5, + height: 220, }; const animatedStyle = useAnimatedStyle(() => ({ @@ -180,21 +185,18 @@ const ContactCard = memo(function ContactCard({ const panGesture = Gesture.Pan() .onBegin(() => { - // Reset values when gesture starts rotateX.value = withSpring(0); rotateY.value = withSpring(0); shadowOffsetX.value = withSpring(0); shadowOffsetY.value = withSpring(0); }) .onUpdate((event) => { - // Update tilt based on pan gesture rotateX.value = event.translationY / 10; rotateY.value = event.translationX / 10; shadowOffsetX.value = -event.translationX / 20; shadowOffsetY.value = event.translationY / 20; }) .onEnd(() => { - // Reset to original position when gesture ends rotateX.value = withSpring(0); rotateY.value = withSpring(0); shadowOffsetX.value = withSpring(0); @@ -205,16 +207,37 @@ const ContactCard = memo(function ContactCard({ - - + > + + {isMyProfile && ( +