import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react'; import { StyleSheet, View, Text, Pressable, PanResponder, AppState, Animated as RNAnimated, ImageBackground, Platform, useWindowDimensions, } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useFocusEffect } from 'expo-router'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Animated, { Easing, runOnJS, useAnimatedStyle, useSharedValue, withSequence, withTiming, } from 'react-native-reanimated'; import { MOCK_CONTENT } from '@/src/constants/mockContent'; import { addFavorite, getThemeMode, getUserProfile, setReaction, setThemeMode, clearPendingHomePushMessage, getPendingHomePushMessage, getRecoFeedCache, setRecoFeedCache, getUserProfileScoring, getRecoFeedHistory, recordRecoFeedServed, type ThemeMode, getSuixinThemeState, setSuixinThemeState, type SuixinThemeStateV1, } from '@/src/storage/appStorage'; import { fetchRecoFeed } from '@/src/services/recoApi'; import { subscribeHomePushMessage } from '@/src/services/pushNotificationRoute'; import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale'; import ProfileModal from '@/components/home/ProfileModal'; import ThemeModal from '@/components/home/ThemeModal'; import ThemeIcon from '@/assets/images/home/theme.svg'; import MyIcon from '@/assets/images/home/my.svg'; import LikeFilledIcon from '@/assets/images/home/like_filled.svg'; import LikeIcon from '@/assets/images/icon/like_icon.svg'; import { getBootId } from '@/src/utils/bootSession'; import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme'; import { wrapText } from '@/src/features/textWrap'; import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure'; import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco'; // 预定义风景图列表 const NATURE_IMAGES = [ require('@/assets/theme/nature/1.png'), require('@/assets/theme/nature/2.png'), require('@/assets/theme/nature/3.png'), require('@/assets/theme/nature/4.png'), require('@/assets/theme/nature/5.png'), require('@/assets/theme/nature/6.png'), require('@/assets/theme/nature/7.png'), require('@/assets/theme/nature/8.png'), require('@/assets/theme/nature/9.png'), require('@/assets/theme/nature/10.png'), require('@/assets/theme/nature/11.png'), require('@/assets/theme/nature/12.png'), require('@/assets/theme/nature/13.png'), require('@/assets/theme/nature/14.png'), require('@/assets/theme/nature/15.png'), require('@/assets/theme/nature/17.png'), require('@/assets/theme/nature/18.png'), require('@/assets/theme/nature/19.png'), require('@/assets/theme/nature/20.png'), require('@/assets/theme/nature/22.png'), ]; // 预定义颜色列表 const THEME_COLORS = [ '#F7D9BF', '#CBF2D8', '#F5CDDE', '#F2ECCB', '#E2CBF2', '#CBD9F2', ]; type FeedItem = { content_id: string; text: string }; export default function HomeScreen() { const { t, i18n } = useTranslation(); const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const isEnglish = i18n.language?.startsWith('en'); const isTablet = Platform.OS === 'ios' && Math.min(windowWidth, windowHeight) >= 768; const recoLang: 'en' | 'tc' = toBackendLocaleFromLanguageTag(i18n.language); const insets = useSafeAreaInsets(); const [index, setIndex] = useState(0); const [themeMode, setThemeModeState] = useState('scenery'); const [suixinBgColor, setSuixinBgColor] = useState(NEUTRAL_THEME_COLORS[1]); const [themeOpen, setThemeOpen] = useState(false); const [profileOpen, setProfileOpen] = useState(false); const [profileName, setProfileName] = useState(undefined); const [busy, setBusy] = useState(false); const [likeFilled, setLikeFilled] = useState(false); const [feedItems, setFeedItems] = useState([]); const [pendingPushItem, setPendingPushItem] = useState(null); const [isFetching, setIsFetching] = useState(false); const [cardWidth, setCardWidth] = useState(null); const [wrappedText, setWrappedText] = useState(''); const wrapLogRef = useRef<{ key: string } | null>(null); const busyRef = useRef(false); const indexRef = useRef(0); const currentFeedRef = useRef([]); const likedIdsRef = useRef>(new Set()); const likeInFlightRef = useRef(false); const applyPendingPushItem = useCallback(async (message: { notification_id: string; content_id?: number; text: string }) => { const nextItem: FeedItem = { content_id: message.content_id != null ? String(message.content_id) : `push:${message.notification_id}`, text: message.text, }; setPendingPushItem(nextItem); indexRef.current = 0; setIndex(0); setLikeFilled(likedIdsRef.current.has(String(nextItem.content_id))); await clearPendingHomePushMessage(); }, []); const consumePendingPushItem = useCallback(async () => { const pendingMessage = await getPendingHomePushMessage(); if (!pendingMessage?.text) return; await applyPendingPushItem(pendingMessage); }, [applyPendingPushItem]); useEffect(() => { busyRef.current = busy; }, [busy]); useEffect(() => { indexRef.current = index; }, [index]); // 解决语言切换时重复触发拉取/清空导致“文案不停跳动”的问题: // 用 ref 持有最新状态,避免 useCallback 依赖 feedItems/isFetching 造成函数 identity 变化 → effect 重复执行 const feedItemsRef = useRef([]); const isFetchingRef = useRef(false); const themeModeRef = useRef('scenery'); const suixinStateRef = useRef(null); useEffect(() => { feedItemsRef.current = feedItems; }, [feedItems]); useEffect(() => { isFetchingRef.current = isFetching; }, [isFetching]); useEffect(() => { themeModeRef.current = themeMode; }, [themeMode]); const ensureSuixinReady = useCallback(async () => { const bootId = getBootId(); const stored = await getSuixinThemeState(); if (stored && stored.boot_id === bootId) { suixinStateRef.current = stored; setSuixinBgColor(stored.last_color || NEUTRAL_THEME_COLORS[1]); return stored; } const profile = await getUserProfileScoring(); const next = buildInitialSuixinState({ bootId, profile }); suixinStateRef.current = next; setSuixinBgColor(next.last_color || NEUTRAL_THEME_COLORS[1]); await setSuixinThemeState(next); return next; }, []); const advanceSuixinOnNextContent = useCallback(async () => { if (themeModeRef.current !== 'suixin') return; const bootId = getBootId(); let current = suixinStateRef.current; if (!current) { current = await getSuixinThemeState(); } // 冷启动后首次触发/或状态丢失:先初始化 if (!current || current.boot_id !== bootId) { await ensureSuixinReady(); return; } const next = advanceSuixinState(current); suixinStateRef.current = next; setSuixinBgColor(next.last_color || NEUTRAL_THEME_COLORS[1]); await setSuixinThemeState(next); }, [ensureSuixinReady]); // 动画相关 Shared Values const translateY = useSharedValue(0); const opacity = useSharedValue(1); const likeScale = useSharedValue(1); // 统一文案对象结构 const currentFeed = useMemo(() => { const baseFeed = feedItems.length > 0 ? feedItems : MOCK_CONTENT.map(item => ({ content_id: item.id, text: t(item.textKey) })); if (!pendingPushItem) { return baseFeed; } return [ pendingPushItem, ...baseFeed.filter((entry) => ( String(entry.content_id) !== String(pendingPushItem.content_id) && entry.text !== pendingPushItem.text )), ]; }, [feedItems, pendingPushItem, t]); useEffect(() => { currentFeedRef.current = currentFeed; }, [currentFeed]); const item = useMemo(() => { const data = currentFeed[index % currentFeed.length]; return { id: String(data.content_id), text: data.text }; }, [currentFeed, index]); // Home 文案:使用自主换行算法(Text Wrap 模块) // - 通过 onLayout 获取容器宽度 // - 注入真实测量实现,确保“宽度派”评分与实际渲染一致 useEffect(() => { let cancelled = false; // 未拿到宽度前先用原文(避免闪烁) if (!cardWidth || cardWidth <= 0) { if (__DEV__) { const key = `noWidth|${item.id}|${String(cardWidth)}`; if (wrapLogRef.current?.key !== key) { wrapLogRef.current = { key }; console.log('[TextWrap][Home] cardWidth 未就绪,先回退原文', { itemId: item.id, lang: recoLang, cardWidth, themeMode, }); } } setWrappedText(item.text); return () => { cancelled = true; }; } const paddingHorizontal = themeMode === 'scenery' ? 50 : 30; const availableWidth = Math.max(0, Math.floor(cardWidth - paddingHorizontal * 2)); const lang = recoLang === 'en' ? 'EN' : 'TC'; const fontFamily = lang === 'EN' ? 'STIXTwoText' : Platform.select({ ios: 'System', android: 'sans-serif', default: 'System', }); const fontSpec = { fontSize: 24, fontWeight: lang === 'EN' ? '700' : '800', fontFamily: String(fontFamily ?? 'System'), }; (async () => { try { if (__DEV__) { const key = `start|${item.id}|${lang}|${availableWidth}|${themeMode}`; if (wrapLogRef.current?.key !== key) { wrapLogRef.current = { key }; console.log('[TextWrap][Home] wrapText 开始', { itemId: item.id, lang, themeMode, cardWidth, paddingHorizontal, availableWidth, fontSpec, textPreview: String(item.text ?? '').slice(0, 80), textLength: String(item.text ?? '').length, }); } } const res = await wrapText({ text: item.text, lang, context: 'APP', availableWidth, maxLines: 3, overflowMode: 'CLIP', lineMode: 'AUTO', // Home:采用“更偏好语气停顿/更好看”的排版风格微调(不影响算法默认 v1) configVersion: 'v1-home', debug: __DEV__, fontSpec, contextProfile: `APP|${Platform.OS}|home|${lang}`, measureWidthImpl: defaultMeasureWidthImpl, scoringOverrides: lang === 'TC' ? { // 更偏好在逗号/句号等处断行(即便宽度允许也不一定要塞满) weights: { R_PUNCT_BREAK: 180 }, // 让“理想行宽”更短,避免宽屏下过度延后断行 idealWidthRatio: { APP: 0.82 }, // 更宽容短行(尤其是第一行在标点处停顿) minPreferredRatio: 0.45, shortLastLineRatio: 0.45, } : undefined, }); if (cancelled) return; if (__DEV__) { console.log('[TextWrap][Home] wrapText 成功', { itemId: item.id, wrappedText: res.wrappedText, linesCount: res.lines.length, meta: res.meta, }); } setWrappedText(res.wrappedText); } catch (error) { // 任何异常都回退到原文,避免影响 Home 主流程 if (__DEV__) { console.log('[TextWrap][Home] wrapText 异常,回退原文', { itemId: item.id, lang, availableWidth, fontSpec, errorName: (error as any)?.name, errorMessage: String((error as any)?.message ?? error), errorStack: (error as any)?.stack, }); } if (cancelled) return; setWrappedText(item.text); } })(); return () => { cancelled = true; }; }, [item.text, cardWidth, recoLang, themeMode]); // 异步拉取新文案 const fetchNewFeed = useCallback(async () => { if (isFetchingRef.current) return; isFetchingRef.current = true; setIsFetching(true); try { const scoringProfile = await getUserProfileScoring(); if (!scoringProfile) return; const history = await getRecoFeedHistory(); const { items, meta } = await fetchRecoFeed({ k: 30, user_profile: scoringProfile, already_recommended_ids: history.already_recommended_ids, touched_or_viewed_ids: history.touched_or_viewed_ids, }); if (items.length > 0) { const wasEmpty = feedItemsRef.current.length === 0; const newCache = { saved_at: new Date().toISOString(), lang: recoLang, items: items.map((x) => ({ content_id: x.content_id, text: x.text })), meta: meta as Record, }; await setRecoFeedCache(newCache); await recordRecoFeedServed(items.map((x) => x.content_id)); setFeedItems(newCache.items.map((x) => ({ content_id: String(x.content_id), text: x.text }))); // 如果当前是 mock 数据,切换到新数据的第一条 if (wasEmpty) { setIndex(0); } } } catch (error) { console.error('Failed to fetch new feed:', error); } finally { isFetchingRef.current = false; setIsFetching(false); } }, [recoLang]); // 每次进入页面或页面获得焦点时刷新个人信息和缓存文案 useFocusEffect( useCallback(() => { let cancelled = false; (async () => { const [mode, profile, cache, pendingMessage] = await Promise.all([ getThemeMode(), getUserProfile(), getRecoFeedCache(), getPendingHomePushMessage(), ]); if (cancelled) return; setThemeModeState(mode); setProfileName(profile.name); if (pendingMessage?.text) { await applyPendingPushItem(pendingMessage); if (cancelled) return; } // 随心:若当前主题为随心,进入 Home 时确保状态就绪(仅冷启动会话重算) if (mode === 'suixin') { ensureSuixinReady().catch(() => { // ignore:失败时回退默认中性底色 setSuixinBgColor(NEUTRAL_THEME_COLORS[1]); }); } // 语言切换时:旧语言缓存不复用,触发重新拉取 if (cache && cache.items.length > 0 && (cache.lang ?? 'en') === recoLang) { setFeedItems(cache.items.map((x) => ({ content_id: String(x.content_id), text: x.text }))); } else { // 语言不匹配或没有缓存:先清空回落到本地 mock(会立即随语言切换),再拉取对应语言的推荐文案 setFeedItems([]); setIndex(0); fetchNewFeed(); } // Widget:前台辅助刷新(尽力而为) // - 写入 App Group 的 dailyReco 缓存 // - 生成 wrapped_text_by_family,供 Widget 直接渲染 ensureDailyWidgetRecoUpToDate({ reason: 'home_focus' }).catch(() => {}); })(); return () => { cancelled = true; }; }, [applyPendingPushItem, fetchNewFeed, recoLang, ensureSuixinReady]) ); useEffect(() => { const unsubscribe = subscribeHomePushMessage((message) => { void applyPendingPushItem(message); }); return unsubscribe; }, [applyPendingPushItem]); useEffect(() => { const sub = AppState.addEventListener('change', (state) => { if (state !== 'active') return; void consumePendingPushItem(); }); return () => sub.remove(); }, [consumePendingPushItem]); const backgroundColor = useMemo(() => { if (themeMode === 'suixin') { return suixinBgColor; } if (themeMode === 'color') { const colorIndex = Math.floor(index / 10) % THEME_COLORS.length; return THEME_COLORS[colorIndex]; } return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示) }, [themeMode, suixinBgColor, index]); // 计算当前应该显示的风景图索引(滑动 10 次切换一张) const natureImageIndex = useMemo(() => { return Math.floor(index / 10) % NATURE_IMAGES.length; }, [index]); const currentNatureImage = NATURE_IMAGES[natureImageIndex]; const textAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }], opacity: opacity.value, })); const likeAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: likeScale.value }], })); const setBusySafe = useCallback((next: boolean) => { busyRef.current = next; setBusy(next); }, []); const setLikeInFlight = useCallback((next: boolean) => { likeInFlightRef.current = next; }, []); const syncLikeFilledByIndex = useCallback((nextIndex: number) => { const list = currentFeedRef.current; const len = list.length; if (!len) { setLikeFilled(false); return; } const safe = ((nextIndex % len) + len) % len; const nextId = String(list[safe]?.content_id); setLikeFilled(likedIdsRef.current.has(nextId)); }, []); const applyIndexChange = useCallback((nextIndex: number) => { indexRef.current = nextIndex; setIndex(nextIndex); syncLikeFilledByIndex(nextIndex); }, [syncLikeFilledByIndex]); const maybeFetchNewFeedIfNeeded = useCallback((nextIndex: number) => { const len = currentFeedRef.current.length; if (!len) return; // 当接近当前列表末尾时(例如还剩 5 条)提前拉取 if (nextIndex + 5 >= len && !isFetchingRef.current) { fetchNewFeed(); } }, [fetchNewFeed]); // 切换到下一条文案的统一动画逻辑 const triggerNextContent = useCallback(() => { if (busyRef.current) return; setBusySafe(true); // 注意:不要在 Reanimated worklet 回调里读取 React ref(例如 indexRef/currentFeedRef),会导致值不更新或异常 const nextIndex = indexRef.current + 1; // 1. 当前文案向上移动并消失 translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) }); opacity.value = withTiming(0, { duration: 300 }, (finished) => { if (finished) { // 2. 切换数据索引 runOnJS(applyIndexChange)(nextIndex); runOnJS(advanceSuixinOnNextContent)(); // 检查是否需要拉取新文案(注意:不要把匿名函数塞进 runOnJS,可能导致原生崩溃) runOnJS(maybeFetchNewFeedIfNeeded)(nextIndex); // 3. 准备下一条文案:先瞬移到下方 40pt translateY.value = 40; // 4. 下一条文案向上移动到原位并显现 translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) }); opacity.value = withTiming(1, { duration: 400 }, (finished) => { if (finished) { runOnJS(setBusySafe)(false); } }); } }); }, [applyIndexChange, setBusySafe, translateY, opacity, advanceSuixinOnNextContent, maybeFetchNewFeedIfNeeded]); // 切换到上一条文案的统一动画逻辑(下滑触发) const triggerPrevContent = useCallback(() => { if (busyRef.current) return; setBusySafe(true); // 注意:同上,不要在 worklet 里读取 React ref const len = currentFeedRef.current.length; const raw = indexRef.current - 1; const nextIndex = len ? ((raw % len) + len) % len : Math.max(0, raw); // 1. 当前文案向下移动并消失 translateY.value = withTiming(40, { duration: 300, easing: Easing.out(Easing.quad) }); opacity.value = withTiming(0, { duration: 300 }, (finished) => { if (finished) { // 2. 切换数据索引(循环回退) runOnJS(applyIndexChange)(nextIndex); // 3. 准备上一条文案:先瞬移到上方 40pt translateY.value = -40; // 4. 上一条文案向下移动到原位并显现 translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) }); opacity.value = withTiming(1, { duration: 400 }, (finished) => { if (finished) { runOnJS(setBusySafe)(false); } }); } }); }, [applyIndexChange, setBusySafe, translateY, opacity]); const lastTapRef = useRef(0); // 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function const handlersRef = useRef({ onPressLike, triggerNextContent, triggerPrevContent }); useEffect(() => { handlersRef.current = { onPressLike, triggerNextContent, triggerPrevContent }; }, [onPressLike, triggerNextContent, triggerPrevContent]); // 使用系统自带的 PanResponder 代替第三方手势库 const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, // 允许开始捕获 onMoveShouldSetPanResponder: (_, gestureState) => { // 只有当垂直滑动距离大于 20 时才接管位移手势 return Math.abs(gestureState.dy) > 20; }, onPanResponderRelease: (_, gestureState) => { const now = Date.now(); const DOUBLE_TAP_DELAY = 300; // 1. 双击逻辑判定 if (now - lastTapRef.current < DOUBLE_TAP_DELAY) { // 判定为双击 if (Math.abs(gestureState.dx) < 10 && Math.abs(gestureState.dy) < 10) { runOnJS(handlersRef.current.onPressLike)(); lastTapRef.current = 0; // 重置 return; } } lastTapRef.current = now; // 2. 上滑/下滑逻辑判定 if (gestureState.dy < -50) { // 上滑超过 50pt runOnJS(handlersRef.current.triggerNextContent)(); } else if (gestureState.dy > 50) { // 下滑超过 50pt runOnJS(handlersRef.current.triggerPrevContent)(); } }, }) ).current; async function onPressLike() { if (busyRef.current) return; if (likeInFlightRef.current) return; // 已经喜欢过:不重复写入收藏,直接当作“下一条” if (likeFilled || likedIdsRef.current.has(item.id)) { triggerNextContent(); return; } likeInFlightRef.current = true; const likedItemId = item.id; const likedItemText = item.text; // 先记下“已喜欢”,保证回退时能恢复点亮状态(即便异步保存稍后才完成) likedIdsRef.current.add(likedItemId); setLikeFilled(true); // 1. 获取当前日期 const now = new Date(); const dateStr = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`; // 2. 保存到收藏夹,包含当前背景信息 const favItem = { favId: String(Date.now()), // 生成唯一 ID id: likedItemId, text: likedItemText, date: dateStr, themeMode: themeMode, background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor, }; console.log('Home: Triggering addFavorite', JSON.stringify(favItem)); try { await addFavorite(favItem); } catch (error) { console.error('Home: addFavorite 失败', error); } // 3. 记录到后端 Reaction(喜欢) console.log('Home: Triggering setReaction', item.id); try { await setReaction(item.id, 'like'); } catch (error) { console.error('Home: setReaction 失败', error); } // 4. 爱心缩放动画 likeScale.value = withSequence( withTiming(0.8, { duration: 100 }), withTiming(1.2, { duration: 150 }), withTiming(1, { duration: 100 }, (finished) => { runOnJS(setLikeInFlight)(false); if (finished) { console.log('Home: Like animation finished, triggering next content'); runOnJS(triggerNextContent)(); } }) ); } async function onSelectTheme(next: ThemeMode) { setThemeModeState(next); await setThemeMode(next); setThemeOpen(false); // 切换到随心:不主动重算(除非冷启动会话变化/状态不存在),仅确保可用 if (next === 'suixin') { await ensureSuixinReady().catch(() => { setSuixinBgColor(NEUTRAL_THEME_COLORS[1]); }); } } const actionsBottom = isTablet ? Math.max(insets.bottom + 36, Math.min(windowHeight * 0.12, 140)) : windowHeight * 0.16; return ( {themeMode === 'scenery' && ( )} {/* 自绘顶部按钮:不使用系统 Header,彻底避免 iOS 导航栏的毛玻璃/液玻璃材质 */} setThemeOpen(true)} accessibilityLabel={t('home.theme')} > setProfileOpen(true)} accessibilityLabel={t('home.profile')} > { const w = e.nativeEvent.layout.width; if (Number.isFinite(w) && w > 0) setCardWidth(w); }} > {wrappedText || item.text} {likeFilled ? ( ) : ( )} setThemeOpen(false)} /> setProfileOpen(false)} /> ); } function CircleIconButton({ onPress, accessibilityLabel, children, }: { onPress: () => void; accessibilityLabel: string; children: React.ReactNode; }) { return ( {children} ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: 'center', alignItems: 'center', }, topRight: { position: 'absolute', flexDirection: 'row', gap: 10, zIndex: 30, }, circleBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.75)', alignItems: 'center', justifyContent: 'center', }, card: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 30, zIndex: 5, // 降低层级,防止遮挡底部按钮 }, text: { fontSize: 24, lineHeight: 34, color: '#5E2A28', fontWeight: '800', textAlign: 'center', }, textMeasureBox: { width: '100%', alignItems: 'center', }, textMeasureBoxTablet: { maxWidth: 760, }, textEnglish: { fontFamily: 'STIXTwoText', // 英文字体保持较粗但避免过度发黑 fontWeight: '700', }, sceneryCard: { // 风景模式下稍微收窄文案宽度,增加呼吸感 paddingHorizontal: 50, }, sceneryText: { color: '#FFFFFF', textShadowColor: 'rgba(0, 0, 0, 0.5)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 4, }, actions: { position: 'absolute', left: 0, right: 0, flexDirection: 'row', justifyContent: 'center', zIndex: 20, // 提升层级,确保在最顶层可点击 }, reactionButton: { alignItems: 'center', justifyContent: 'center', }, reactionInner: { alignItems: 'center', justifyContent: 'center', }, });