feat(profile): add social media icons and links to user header banner

- Introduced social media icons for platforms like GitHub, Twitter, YouTube, Discord, Instagram, and Facebook in the UserHeaderBanner component.
- Enhanced the UserHeaderBanner to display user bio, website, and social links dynamically.
- Updated database schema to include new fields for user profiles: bio, website, and social_links.
- Adjusted layout and styles for better presentation of user information.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-11 19:11:31 +08:00
parent 95616e2d6c
commit 8c0a5be15c
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
18 changed files with 1095 additions and 51 deletions

View File

@ -2,7 +2,7 @@ import { composeEventHandlers } from "@follow/utils"
import { cn } from "@follow/utils/utils"
import { useEffect, useImperativeHandle, useRef, useState } from "react"
import type { StyleProp, TextInputProps, ViewStyle } from "react-native"
import { StyleSheet, Text, TextInput, TouchableWithoutFeedback, View } from "react-native"
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native"
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
import { gentleSpringPreset } from "@/src/constants/spring"
@ -144,14 +144,14 @@ export const PlainTextField = ({
}}
/>
<Animated.View style={animatedStyle}>
<TouchableWithoutFeedback
<TouchableOpacity
onPress={() => {
textInputRef.current?.clear()
props.onChangeText?.("")
}}
>
<CloseCircleFillIcon height={16} width={16} color={secondaryLabelColor} />
</TouchableWithoutFeedback>
</TouchableOpacity>
</Animated.View>
</View>
)

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface FacebookCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const FacebookCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: FacebookCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M11.28 2.024c-.099.009-.387.044-.64.078-3.737.51-6.847 3.065-8.095 6.649-.664 1.908-.71 4.177-.122 6.115a10.022 10.022 0 0 0 7.907 6.992l.15.026V15.007l-.67-.014c-.636-.012-.682-.019-.914-.126a1.698 1.698 0 0 1-.764-.767c-.097-.207-.112-.29-.112-.6 0-.32.014-.389.124-.62.157-.33.406-.579.736-.736.253-.121.277-.124.926-.137l.665-.014.019-1.246c.017-1.102.029-1.283.103-1.556.191-.709.446-1.153.945-1.653.501-.501.962-.764 1.645-.939.405-.103 1.16-.144 1.519-.082.53.093.951.41 1.171.883.088.187.103.276.103.6 0 .324-.015.413-.103.6a1.565 1.565 0 0 1-1.13.877 4.257 4.257 0 0 1-.594.043c-.353 0-.359.002-.493.136l-.136.136v2.201l.67.014c.636.012.682.019.914.126.309.143.617.452.764.767.097.207.112.29.112.6s-.015.393-.112.6a1.698 1.698 0 0 1-.764.767c-.232.107-.278.114-.914.126l-.67.014v3.436c0 1.891.011 3.437.024 3.437.013 0 .206-.037.43-.082a10.022 10.022 0 0 0 6.881-5.161c1.565-2.967 1.508-6.645-.149-9.546-1.547-2.709-4.127-4.488-7.213-4.972-.495-.078-1.782-.133-2.213-.095"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface FacebookCuteReIconProps {
width?: number
height?: number
color?: string
}
export const FacebookCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: FacebookCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M11.08 2.045c-1.874.165-3.723.904-5.28 2.109-.437.339-1.307 1.209-1.646 1.646-1.8 2.326-2.505 5.195-1.976 8.046.29 1.566.959 3.04 1.976 4.354.339.437 1.209 1.307 1.646 1.646 2.441 1.889 5.453 2.566 8.44 1.895 2.487-.559 4.752-2.144 6.145-4.301.806-1.247 1.283-2.527 1.521-4.08.098-.641.098-2.079 0-2.72-.285-1.858-.936-3.388-2.06-4.84-.339-.437-1.209-1.307-1.646-1.646-2.067-1.599-4.554-2.336-7.12-2.109m1.752 1.997a8.182 8.182 0 0 1 4.208 1.747c.354.286 1.027.972 1.286 1.311A8.123 8.123 0 0 1 20 12a8.1 8.1 0 0 1-1.789 5.04c-.286.354-.972 1.027-1.311 1.286a8.467 8.467 0 0 1-2.4 1.269c-.479.156-1.203.325-1.398.325H13V14h.553c.696 0 .893-.052 1.144-.303.183-.183.303-.46.303-.697 0-.237-.12-.514-.303-.697-.251-.251-.448-.303-1.144-.303H13l.002-1.09c.002-1.172.015-1.267.215-1.529a1.18 1.18 0 0 1 .291-.248c.17-.099.234-.111.729-.134.615-.028.772-.082 1.002-.344.18-.205.241-.37.241-.655 0-.285-.061-.45-.241-.655-.254-.29-.375-.325-1.099-.324-.541.002-.69.016-.962.093-.958.27-1.756 1.057-2.054 2.025-.094.306-.099.379-.115 1.591L10.993 12h-.549c-.691 0-.89.053-1.138.3a.96.96 0 0 0 0 1.4c.248.248.447.3 1.141.3H11v5.92h-.102c-.426 0-1.649-.371-2.369-.72a7.375 7.375 0 0 1-2.083-1.459 7.632 7.632 0 0 1-1.645-2.267c-1.321-2.735-.987-5.939.873-8.374.259-.339.932-1.025 1.286-1.311a8.254 8.254 0 0 1 4.16-1.745 10.09 10.09 0 0 1 1.712-.002"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface FingerPressCuteReIconProps {
width?: number
height?: number
color?: string
}
export const FingerPressCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: FingerPressCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M9.814 2.041c-1.483.156-2.819.8-3.896 1.877-1.911 1.911-2.434 4.653-1.368 7.18.134.319.23.484.35.604a.984.984 0 0 0 1.405-.005c.198-.198.316-.527.285-.793a3.004 3.004 0 0 0-.187-.555 4.555 4.555 0 0 1-.392-1.652c-.132-2.844 2.43-5.113 5.232-4.632A4.506 4.506 0 0 1 14.88 9.52c-.13.571-.064.89.246 1.188.184.176.388.252.674.252.285 0 .45-.061.655-.241.185-.163.275-.338.358-.699a6.456 6.456 0 0 0-.156-3.574c-.543-1.636-1.782-3.051-3.317-3.787a6.61 6.61 0 0 0-3.526-.618m.146 4.021c-.837.179-1.557.843-1.855 1.711-.083.241-.085.319-.105 3.458l-.02 3.21-.28-.206c-.325-.239-.895-.521-1.24-.613a3.552 3.552 0 0 0-1.367-.047c-.876.153-1.361.507-1.617 1.181-.107.282-.143.82-.077 1.14.06.289 2.688 5.578 2.88 5.797a.984.984 0 0 0 1.201.203c.347-.189.52-.486.52-.894 0-.235-.026-.293-1.3-2.841-.715-1.431-1.3-2.611-1.3-2.621 0-.047.454-.014.628.045.455.154 1.085.711 1.881 1.663.336.402.503.57.63.631.411.2.86.128 1.164-.184.316-.325.297-.014.297-4.924 0-4.83-.016-4.529.252-4.693a.451.451 0 0 1 .496 0c.261.159.252.066.252 2.753 0 1.576.015 2.518.043 2.672.122.667.64 1.27 1.288 1.497.141.05 1.098.193 2.654.398 1.729.228 2.505.346 2.675.407a1.971 1.971 0 0 1 1.279 1.413c.079.309.079 1.076.001 1.602a5.958 5.958 0 0 1-.412 1.5c-.236.578-.25.822-.068 1.154a.99.99 0 0 0 1.43.344c.201-.133.321-.326.535-.858.396-.985.547-1.807.552-3 .003-.776-.006-.895-.09-1.22a4.822 4.822 0 0 0-.784-1.57 4.095 4.095 0 0 0-2.343-1.407 145.83 145.83 0 0 0-2.38-.323 191.702 191.702 0 0 1-2.27-.304l-.11-.022-.001-2.427c0-2.739-.002-2.759-.288-3.326-.502-.997-1.642-1.536-2.751-1.299"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface InstagramCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const InstagramCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: InstagramCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M9.16 2.545c-1.548.124-2.453.417-3.56 1.153-1.366.909-2.449 2.41-2.838 3.936-.208.813-.222 1.088-.222 4.326 0 3.166.011 3.412.186 4.24.146.69.563 1.612 1.047 2.314.931 1.35 2.513 2.434 4.029 2.759.816.176 1.071.187 4.198.187 3.127 0 3.382-.011 4.198-.187 1.336-.287 2.795-1.2 3.693-2.311.695-.861 1.217-1.917 1.406-2.846.152-.75.163-1.029.163-4.116 0-3.087-.011-3.366-.163-4.116-.199-.975-.793-2.139-1.523-2.982-.882-1.02-2.281-1.885-3.494-2.16-.813-.185-.996-.194-4.04-.203a183.89 183.89 0 0 0-3.08.006m7.788 4.063c.336.158.541.495.543.892.003.566-.417.987-.992.993-.716.007-1.221-.783-.901-1.413.119-.235.226-.349.422-.45.323-.167.605-.174.928-.022m-3.91 1.53c2.036.562 3.296 2.59 2.879 4.636-.375 1.844-1.893 3.136-3.777 3.214-1.858.077-3.501-1.14-4.008-2.971-.098-.354-.11-.464-.11-1.017 0-.551.012-.663.108-1.011.435-1.568 1.634-2.651 3.267-2.951a5.21 5.21 0 0 1 .723-.019c.425.012.621.037.918.119m-1.458 1.909c-.543.104-1.128.563-1.373 1.078-.578 1.214.167 2.637 1.488 2.842a1.99 1.99 0 0 0 2.272-1.662 1.937 1.937 0 0 0-.563-1.709c-.491-.49-1.127-.682-1.824-.549"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface TelegramCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const TelegramCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: TelegramCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M18.221 4.525c-.587.085-1.448.357-2.636.832-1.051.421-8.787 3.649-9.561 3.99-2.222.981-3.27 1.694-3.691 2.513-.222.431-.267.665-.244 1.244.023.569.119.869.403 1.267.218.305.463.532.84.778.365.238 1.191.633 1.488.711.347.092.806.085 1.112-.017.719-.24 2.292-1.28 5.105-3.375 2.768-2.063 3.636-2.637 4.074-2.696.136-.018.168-.008.207.064.069.13-.057.368-.406.764-.311.353-.588.616-2.232 2.122-1.764 1.616-2.971 2.873-3.201 3.333-.22.44-.143.918.214 1.329.139.159.433.369 2.767 1.976 1.994 1.373 2.881 1.888 3.58 2.079.981.269 1.83.06 2.502-.615.251-.252.365-.406.512-.693.385-.753.571-1.506 1.023-4.151.653-3.824 1.085-6.441 1.161-7.027.223-1.735.12-2.69-.369-3.422-.541-.808-1.502-1.173-2.648-1.006"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface TelegramCuteReIconProps {
width?: number
height?: number
color?: string
}
export const TelegramCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: TelegramCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M18.34 4.467c-.595.075-1.434.325-2.518.751-.735.289-7.532 3.116-9.11 3.789-1.28.546-2.64 1.218-3.152 1.558-.441.293-1.055.886-1.215 1.175-.228.408-.271.571-.293 1.1-.035.868.127 1.3.706 1.884.784.789 1.926 1.262 5.742 2.377.506.148.99.304 1.075.347.085.043.886.584 1.78 1.203 3.665 2.535 4.219 2.839 5.345 2.93.999.081 1.908-.483 2.419-1.501.411-.819.536-1.362 1.177-5.14.945-5.561 1.043-6.204 1.089-7.131.045-.881-.092-1.635-.389-2.156-.5-.876-1.516-1.329-2.656-1.186m.86 2.12c.219.245.254.802.119 1.895-.115.931-.178 1.323-.762 4.778-.74 4.372-.878 5.074-1.12 5.702-.156.403-.366.638-.57.638-.355 0-.846-.231-1.814-.854a154.94 154.94 0 0 1-3.367-2.297l-.135-.1 2.141-2.144c1.177-1.18 2.173-2.208 2.213-2.285.106-.204.1-.649-.013-.86a1.113 1.113 0 0 0-.501-.478c-.206-.086-.612-.079-.811.015-.108.051-.919.835-2.5 2.419-1.287 1.289-2.361 2.344-2.387 2.344-.085 0-2.935-.868-3.553-1.082-1.712-.593-2.296-1.005-2.098-1.483.151-.364 1.206-.98 3.04-1.775.876-.379 8.136-3.404 8.967-3.735 1.645-.657 2.285-.851 2.745-.834.273.01.303.02.406.136"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface WeiboCuteReIconProps {
width?: number
height?: number
color?: string
}
export const WeiboCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: WeiboCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
d="M16.643 3.069C16.291 3.193 16 3.614 16 4c0 .396.291.808.66.934.107.036.31.066.451.066.341 0 .98.133 1.358.283a4.013 4.013 0 0 1 2.248 2.248c.15.378.283 1.017.283 1.358 0 .348.096.609.299.812.64.64 1.701.172 1.701-.75 0-.107-.019-.369-.042-.583a5.986 5.986 0 0 0-5.326-5.326c-.516-.056-.775-.049-.989.027m-6.176 1.972c-.819.129-1.58.451-2.64 1.115-1.573.985-3.025 2.34-4.058 3.789-1.444 2.024-2.048 4.188-1.629 5.84.382 1.503 1.606 2.89 3.381 3.83 3.475 1.839 8.475 1.838 11.959-.001 1.705-.901 2.924-2.241 3.338-3.671a4.603 4.603 0 0 0 .167-1.458c-.096-1.255-.765-2.226-2.067-2.998l-.383-.227.057-.18c.031-.099.065-.423.076-.72.023-.624-.03-.928-.233-1.33-.665-1.318-2.113-1.794-4.041-1.327a12.27 12.27 0 0 1-.417.097c-.009 0-.017-.064-.017-.142 0-.255-.124-.711-.284-1.046a2.79 2.79 0 0 0-1.815-1.501c-.327-.088-1.051-.124-1.394-.07m.921 2.019c.56.167.747.84.455 1.64-.072.198-.15.441-.173.541-.137.59.33 1.171.946 1.175.211.002.303-.026.684-.202 1.055-.488 1.697-.669 2.38-.672.391-.002.488.011.649.085.466.216.504.813.092 1.473-.23.369-.288.597-.224.88.055.247.229.508.414.621.071.044.255.127.409.185.687.26 1.274.618 1.581.963.463.52.519 1.204.163 1.988-.724 1.597-2.985 2.831-5.844 3.19-.552.07-2.288.07-2.84 0-3.2-.402-5.618-1.896-6.025-3.722-.121-.545-.009-1.388.291-2.184.265-.708.812-1.638 1.398-2.381.425-.539 1.61-1.693 2.173-2.115 1.634-1.227 2.723-1.687 3.471-1.465"
fill={color}
fillRule="evenodd"
/>
</Svg>
)
}

View File

@ -2,19 +2,40 @@ import { useImageColors, usePrefetchImageColors } from "@follow/store/image/hook
import { useUserById } from "@follow/store/user/hooks"
import { cn, getLuminance } from "@follow/utils"
import { LinearGradient } from "expo-linear-gradient"
import type { FC } from "react"
import { useMemo } from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
import { Linking, StyleSheet, Text, TouchableOpacity, View } from "react-native"
import type { SharedValue } from "react-native-reanimated"
import ReAnimated, { FadeIn, FadeOut, interpolate, useAnimatedStyle } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useColor } from "react-native-uikit-colors"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { DiscordCuteFiIcon } from "@/src/icons/discord_cute_fi"
import { FacebookCuteFiIcon } from "@/src/icons/facebook_cute_fi"
import { GithubCuteFiIcon } from "@/src/icons/github_cute_fi"
import { InstagramCuteFiIcon } from "@/src/icons/instagram_cute_fi"
import { LinkCuteReIcon } from "@/src/icons/link_cute_re"
import { TwitterCuteFiIcon } from "@/src/icons/twitter_cute_fi"
import { WebCuteReIcon } from "@/src/icons/web_cute_re"
import { YoutubeCuteFiIcon } from "@/src/icons/youtube_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { LoginScreen } from "@/src/screens/(modal)/LoginScreen"
const defaultGradientColors = ["#000", "#100", "#200"]
const PlatformInfoMap: Record<
string,
{ component: FC<any>; color: { light: string; dark: string } }
> = {
github: { component: GithubCuteFiIcon, color: { light: "#181717", dark: "#FFFFFF" } },
twitter: { component: TwitterCuteFiIcon, color: { light: "#1DA1F2", dark: "#1DA1F2" } },
youtube: { component: YoutubeCuteFiIcon, color: { light: "#FF0000", dark: "#FF0000" } },
discord: { component: DiscordCuteFiIcon, color: { light: "#5865F2", dark: "#5865F2" } },
instagram: { component: InstagramCuteFiIcon, color: { light: "#C13584", dark: "#C13584" } },
facebook: { component: FacebookCuteFiIcon, color: { light: "#1877F2", dark: "#1877F2" } },
}
export const UserHeaderBanner = ({
scrollY,
userId,
@ -46,6 +67,15 @@ export const UserHeaderBanner = ({
return [imageColors.primary, imageColors.secondary, imageColors.background]
}, [bgColor, imageColors, user])
const socialLinks = useMemo(() => {
if (!user?.socialLinks) {
return []
}
return Object.entries(user.socialLinks)
.filter(([, value]) => !!value)
.map(([platform, link]) => ({ platform, link: link! }))
}, [user?.socialLinks])
const gradientLight = useMemo(() => {
if (!imageColors) return false
if (imageColors.platform === "web") return false
@ -61,10 +91,8 @@ export const UserHeaderBanner = ({
extrapolateRight: "clamp",
})
if (!gradientColors) return {}
return {
transform: [{ scale: scaleValue }],
height: 250,
}
})
@ -83,7 +111,6 @@ export const UserHeaderBanner = ({
})
return {
marginTop: insets.top,
transform: [{ scale: avatarScale }, { translateY: avatarTranslateY }],
}
})
@ -92,10 +119,10 @@ export const UserHeaderBanner = ({
return (
<View
className="relative h-[200px] items-center justify-center"
style={{ marginTop: -insets.top }}
className="relative items-center justify-center pt-[22px]"
style={{ marginTop: -insets.top - 22 }}
>
<ReAnimated.View entering={FadeIn} className="absolute inset-x-0 bottom-0" style={styles}>
<ReAnimated.View entering={FadeIn} className="absolute inset-0" style={styles}>
<LinearGradient
colors={defaultGradientColors as [string, string, ...string[]]}
start={{ x: 0, y: 0 }}
@ -117,46 +144,95 @@ export const UserHeaderBanner = ({
</ReAnimated.View>
)}
</ReAnimated.View>
<ReAnimated.View
style={avatarStyles}
className="bg-system-background overflow-hidden rounded-full"
>
<UserAvatar
image={user?.image}
name={user?.name}
size={60}
className={!user?.name ? "bg-system-grouped-background" : ""}
color={avatarIconColor}
/>
</ReAnimated.View>
<View className="items-center px-4 pb-[24px]" style={{ paddingTop: insets.top }}>
<ReAnimated.View
style={avatarStyles}
className="bg-system-background overflow-hidden rounded-full"
>
<UserAvatar
image={user?.image}
name={user?.name}
size={60}
className={!user?.name ? "bg-system-grouped-background" : ""}
color={avatarIconColor}
/>
</ReAnimated.View>
<View className="mt-2 items-center">
{user?.name ? (
<View className="mt-2 items-center">
{user?.name ? (
<Text
numberOfLines={1}
className={cn(
"px-8 text-2xl font-bold",
gradientLight ? "text-black" : "text-white/95",
)}
>
{user.name}
</Text>
) : (
<Text className="text-text text-2xl font-bold">Folo Account</Text>
)}
{user?.handle ? (
<Text className={cn(gradientLight ? "text-black/70" : "text-white/70")}>
@{user.handle}
</Text>
) : !user ? (
<TouchableOpacity
className="mx-auto"
onPress={() => navigation.presentControllerView(LoginScreen)}
>
<Text className="text-accent m-[6] text-[16px]">Sign in to your account</Text>
</TouchableOpacity>
) : null}
</View>
{user?.bio ? (
<Text
numberOfLines={1}
numberOfLines={3}
className={cn(
"px-8 text-2xl font-bold",
gradientLight ? "text-black" : "text-white/95",
"mt-2 px-8 text-center",
gradientLight ? "text-black/80" : "text-white/80",
)}
>
{user.name}
{user.bio}
</Text>
) : (
<Text className="text-text text-2xl font-bold">Folo Account</Text>
)}
{user?.handle ? (
<Text className={cn(gradientLight ? "text-black/70" : "text-white/70")}>
@{user.handle}
</Text>
) : !user ? (
<TouchableOpacity
className="mx-auto"
onPress={() => navigation.presentControllerView(LoginScreen)}
>
<Text className="text-accent m-[6] text-[16px]">Sign in to your account</Text>
</TouchableOpacity>
) : null}
<View className="mt-4 flex-row flex-wrap items-center justify-center gap-x-6 gap-y-2 px-8">
{user?.website && (
<TouchableOpacity
className="flex-row items-center gap-1"
onPress={() => user.website && Linking.openURL(user.website)}
>
<WebCuteReIcon
height={16}
width={16}
color={gradientLight ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.7)"}
/>
<Text
className={cn("font-semibold", gradientLight ? "text-black/70" : "text-white/70")}
>
{user.website.replace(/^(https?:\/\/)?(www\.)?/, "")}
</Text>
</TouchableOpacity>
)}
{socialLinks.map(({ platform, link }) => {
const platformInfo = PlatformInfoMap[platform as keyof typeof PlatformInfoMap]
const IconComponent = platformInfo ? platformInfo.component : LinkCuteReIcon
const color = platformInfo
? gradientLight
? platformInfo.color.light
: platformInfo.color.dark
: gradientLight
? "rgba(0,0,0,0.8)"
: "rgba(255,255,255,0.8)"
return (
<TouchableOpacity key={platform} onPress={() => Linking.openURL(link)}>
<IconComponent height={22} width={22} color={color} />
</TouchableOpacity>
)
})}
</View>
</View>
</View>
)

View File

@ -6,7 +6,7 @@ import { useMutation } from "@tanstack/react-query"
import type { FC } from "react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { Text, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native"
import { Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native"
import { KeyboardController } from "react-native-keyboard-controller"
import { HeaderSubmitTextButton } from "@/src/components/layouts/header/HeaderElements"
@ -206,9 +206,9 @@ const ProfileForm: FC<{
<GroupedInsetListSectionHeader label={t("profile.bio.label", "Bio")} />
<GroupedInsetListCard>
<View className="flex-1 px-4 py-3">
<PlainTextField
className="text-text h-[100px] w-full flex-1"
<View className="flex-1">
<TextInput
className="text-label h-[100px] w-full flex-1 px-4 py-3"
value={dirtyFields.bio ?? whoami?.bio ?? ""}
hitSlop={10}
multiline

View File

@ -141,7 +141,11 @@ function ProfileScreenImpl(props: { userId: string }) {
<ReAnimatedScrollView
nestedScrollEnabled
onScroll={scrollHandler}
contentContainerStyle={{ paddingBottom: insets.bottom + 24, paddingTop: headerHeight }}
contentContainerStyle={{
paddingBottom: insets.bottom + 24,
paddingTop: headerHeight + 22,
marginTop: -22,
}}
>
<UserHeaderBanner scrollY={scrollY} userId={props.userId} />

View File

@ -22,11 +22,8 @@ import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner"
export function Settings() {
const insets = useSafeAreaInsets()
const screenContext = use(ScreenItemContext)
const whoami = useWhoami()
const scrollViewRef = useRegisterNavigationScrollView<ScrollView>()
return (

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.28 2.024c-.099.009-.387.044-.64.078-3.737.51-6.847 3.065-8.095 6.649-.664 1.908-.71 4.177-.122 6.115a10.022 10.022 0 0 0 7.907 6.992l.15.026V15.007l-.67-.014c-.636-.012-.682-.019-.914-.126a1.698 1.698 0 0 1-.764-.767c-.097-.207-.112-.29-.112-.6 0-.32.014-.389.124-.62.157-.33.406-.579.736-.736.253-.121.277-.124.926-.137l.665-.014.019-1.246c.017-1.102.029-1.283.103-1.556.191-.709.446-1.153.945-1.653.501-.501.962-.764 1.645-.939.405-.103 1.16-.144 1.519-.082.53.093.951.41 1.171.883.088.187.103.276.103.6 0 .324-.015.413-.103.6a1.565 1.565 0 0 1-1.13.877 4.257 4.257 0 0 1-.594.043c-.353 0-.359.002-.493.136l-.136.136v2.201l.67.014c.636.012.682.019.914.126.309.143.617.452.764.767.097.207.112.29.112.6s-.015.393-.112.6a1.698 1.698 0 0 1-.764.767c-.232.107-.278.114-.914.126l-.67.014v3.436c0 1.891.011 3.437.024 3.437.013 0 .206-.037.43-.082a10.022 10.022 0 0 0 6.881-5.161c1.565-2.967 1.508-6.645-.149-9.546-1.547-2.709-4.127-4.488-7.213-4.972-.495-.078-1.782-.133-2.213-.095" fill="#10161F" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M9.16 2.545c-1.548.124-2.453.417-3.56 1.153-1.366.909-2.449 2.41-2.838 3.936-.208.813-.222 1.088-.222 4.326 0 3.166.011 3.412.186 4.24.146.69.563 1.612 1.047 2.314.931 1.35 2.513 2.434 4.029 2.759.816.176 1.071.187 4.198.187 3.127 0 3.382-.011 4.198-.187 1.336-.287 2.795-1.2 3.693-2.311.695-.861 1.217-1.917 1.406-2.846.152-.75.163-1.029.163-4.116 0-3.087-.011-3.366-.163-4.116-.199-.975-.793-2.139-1.523-2.982-.882-1.02-2.281-1.885-3.494-2.16-.813-.185-.996-.194-4.04-.203a183.89 183.89 0 0 0-3.08.006m7.788 4.063c.336.158.541.495.543.892.003.566-.417.987-.992.993-.716.007-1.221-.783-.901-1.413.119-.235.226-.349.422-.45.323-.167.605-.174.928-.022m-3.91 1.53c2.036.562 3.296 2.59 2.879 4.636-.375 1.844-1.893 3.136-3.777 3.214-1.858.077-3.501-1.14-4.008-2.971-.098-.354-.11-.464-.11-1.017 0-.551.012-.663.108-1.011.435-1.568 1.634-2.651 3.267-2.951a5.21 5.21 0 0 1 .723-.019c.425.012.621.037.918.119m-1.458 1.909c-.543.104-1.128.563-1.373 1.078-.578 1.214.167 2.637 1.488 2.842a1.99 1.99 0 0 0 2.272-1.662 1.937 1.937 0 0 0-.563-1.709c-.491-.49-1.127-.682-1.824-.549" fill="#10161F" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
ALTER TABLE `users` ADD `bio` text;--> statement-breakpoint
ALTER TABLE `users` ADD `website` text;--> statement-breakpoint
ALTER TABLE `users` ADD `social_links` text;

View File

@ -0,0 +1,785 @@
{
"version": "6",
"dialect": "sqlite",
"id": "3788abcf-7e98-4d02-ba93-7d3407c4eea5",
"prevId": "f4599e18-03d8-442b-80b4-eb73fb18b9d3",
"tables": {
"collections": {
"name": "collections",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"entry_id": {
"name": "entry_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"entries": {
"name": "entries",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"source_content": {
"name": "source_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"guid": {
"name": "guid",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"author_url": {
"name": "author_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"author_avatar": {
"name": "author_avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inserted_at": {
"name": "inserted_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"published_at": {
"name": "published_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"media": {
"name": "media",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"categories": {
"name": "categories",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"attachments": {
"name": "attachments",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"extra": {
"name": "extra",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_handle": {
"name": "inbox_handle",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"read": {
"name": "read",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sources": {
"name": "sources",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"settings": {
"name": "settings",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"subscription_count": {
"name": "subscription_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updates_per_week": {
"name": "updates_per_week",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"latest_entry_published_at": {
"name": "latest_entry_published_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tip_users": {
"name": "tip_users",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_at": {
"name": "published_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"images": {
"name": "images",
"columns": {
"url": {
"name": "url",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"colors": {
"name": "colors",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"subscription_count": {
"name": "subscription_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"purchase_amount": {
"name": "purchase_amount",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"summaries": {
"name": "summaries",
"columns": {
"entry_id": {
"name": "entry_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"readability_summary": {
"name": "readability_summary",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"unq": {
"name": "unq",
"columns": ["entry_id", "language"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"translations": {
"name": "translations",
"columns": {
"entry_id": {
"name": "entry_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"readability_content": {
"name": "readability_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"translation-unique-index": {
"name": "translation-unique-index",
"columns": ["entry_id", "language"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"unread": {
"name": "unread",
"columns": {
"subscription_id": {
"name": "subscription_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"handle": {
"name": "handle",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_me": {
"name": "is_me",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"website": {
"name": "website",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"social_links": {
"name": "social_links",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -183,6 +183,13 @@
"when": 1749633611180,
"tag": "0025_colorful_valkyrie",
"breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1749640123453,
"tag": "0026_numerous_slyde",
"breakpoints": true
}
]
}

View File

@ -26,6 +26,7 @@ import m0022 from "./0022_tiny_northstar.sql"
import m0023 from "./0023_pink_namor.sql"
import m0024 from "./0024_spooky_alex_power.sql"
import m0025 from "./0025_colorful_valkyrie.sql"
import m0026 from "./0026_numerous_slyde.sql"
import journal from "./meta/_journal.json"
export default {
@ -57,5 +58,6 @@ export default {
m0023,
m0024,
m0025,
m0026,
},
}