Rebrand the entire application from "胡萝卜社区" (Carrot BBS) to "威友" (WithYou), including:
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m51s
Frontend CI / ota-android (push) Successful in 11m16s
Frontend CI / build-android-apk (push) Failing after 12m54s

- Update app name, package name (skin.carrot.bbs → cn.qczlit.withyou), and bundle identifier
- Update API endpoints (bbs.littlelan.cn → withyou.littlelan.cn)
- Update URL scheme (carrotbbs:// → withyou://)
- Rename database from carrot_bbs to with_you
- Update CI/CD pipeline image names and artifact naming
- Add Android signing configuration for release builds
- Update all UI text and comments throughout the codebase
- Refactor registerStore to use in-memory state instead of AsyncStorage persistence
- Improve WebRTC track handling and call stream management
- Add metro platform resolution for native platform support

BREAKING app package, database, and URLs are no longer valid
This commit is contained in:
lafay
2026-04-22 16:54:51 +08:00
parent 1c3797ea7d
commit 437dab3b6e
47 changed files with 221 additions and 235 deletions

View File

@@ -1,4 +1,4 @@
name: Frontend CI name: Frontend CI
on: on:
push: push:
@@ -20,7 +20,7 @@ on:
env: env:
REGISTRY: code.littlelan.cn REGISTRY: code.littlelan.cn
IMAGE_NAME: carrot_bbs/frontend-web IMAGE_NAME: with_you/frontend-web
OTA_PLATFORM: android OTA_PLATFORM: android
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
@@ -150,6 +150,33 @@ jobs:
- name: Generate Android native project - name: Generate Android native project
run: npx expo prebuild --platform android run: npx expo prebuild --platform android
- name: Decode Android signing keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/withyou-release-key.keystore
- name: Patch app/build.gradle with signing config
run: |
cd android/app
python3 -c "
import re
with open('build.gradle', 'r') as f:
content = f.read()
release_signing = '''
signingConfigs {
release {
storeFile file(WITHYOU_UPLOAD_STORE_FILE)
storePassword WITHYOU_UPLOAD_STORE_PASSWORD
keyAlias WITHYOU_UPLOAD_KEY_ALIAS
keyPassword WITHYOU_UPLOAD_KEY_PASSWORD
}
}
'''
content = content.replace('signingConfigs {}', release_signing)
content = content.replace('signingConfig signingConfigs.debug', 'signingConfig signingConfigs.release')
with open('build.gradle', 'w') as f:
f.write(content)
"
- name: Configure Gradle with Aliyun Maven mirror - name: Configure Gradle with Aliyun Maven mirror
run: | run: |
cd android cd android
@@ -198,7 +225,7 @@ jobs:
} }
expoAutolinking.useExpoModules() expoAutolinking.useExpoModules()
rootProject.name = '萝卜社区' rootProject.name = '威友'
expoAutolinking.useExpoVersionCatalog() expoAutolinking.useExpoVersionCatalog()
@@ -260,6 +287,16 @@ jobs:
expo.useLegacyPackaging=false expo.useLegacyPackaging=false
PROPS_EOF PROPS_EOF
- name: Configure Gradle signing properties
run: |
cd android
cat >> gradle.properties << SIGNING_PROPS
WITHYOU_UPLOAD_STORE_FILE=withyou-release-key.keystore
WITHYOU_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
WITHYOU_UPLOAD_KEY_ALIAS=withyou-key
WITHYOU_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}
SIGNING_PROPS
- name: Build Android release APK (arm64 only) - name: Build Android release APK (arm64 only)
run: | run: |
cd android cd android
@@ -269,7 +306,7 @@ jobs:
- name: Upload APK artifact - name: Upload APK artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: carrot-bbs-android-release-apk name: withyou-android-release-apk
path: android/app/build/outputs/apk/release/app-release.apk path: android/app/build/outputs/apk/release/app-release.apk
- name: Resolve runtime version - name: Resolve runtime version
@@ -350,8 +387,8 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64 platforms: linux/amd64
provenance: false provenance: false
cache-from: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache cache-from: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache
cache-to: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache,mode=max cache-to: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache,mode=max
- name: Show image tags - name: Show image tags
run: | run: |

View File

@@ -1,7 +1,7 @@
const appJson = require('./app.json'); const appJson = require('./app.json');
const isDevVariant = process.env.APP_VARIANT === 'dev'; const isDevVariant = process.env.APP_VARIANT === 'dev';
const releaseApiBaseUrl = 'https://bbs.littlelan.cn/api/v1'; const releaseApiBaseUrl = 'https://withyou.littlelan.cn/api/v1';
const releaseUpdatesBaseUrl = 'https://updates.littlelan.cn'; const releaseUpdatesBaseUrl = 'https://updates.littlelan.cn';
const devApiBaseUrl = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.31.238:8080/api/v1'; const devApiBaseUrl = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.31.238:8080/api/v1';

View File

@@ -1,12 +1,12 @@
{ {
"expo": { "expo": {
"name": "萝卜社区", "name": "威友",
"slug": "qojo", "slug": "qojo",
"version": "1.0.13", "version": "0.1.sha",
"orientation": "default", "orientation": "default",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"scheme": "carrotbbs", "scheme": "withyou",
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",
"resizeMode": "contain", "resizeMode": "contain",
@@ -15,7 +15,7 @@
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"infoPlist": { "infoPlist": {
"NSMicrophoneUsageDescription": "允许萝卜社区访问您的麦克风以进行语音通话", "NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话",
"UIBackgroundModes": [ "UIBackgroundModes": [
"fetch", "fetch",
"remote-notification", "remote-notification",
@@ -25,7 +25,7 @@
], ],
"ITSAppUsesNonExemptEncryption": false "ITSAppUsesNonExemptEncryption": false
}, },
"bundleIdentifier": "skin.carrot.bbs" "bundleIdentifier": "cn.qczlit.withyou"
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
@@ -35,7 +35,7 @@
"monochromeImage": "./assets/android-icon-monochrome.png" "monochromeImage": "./assets/android-icon-monochrome.png"
}, },
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "skin.carrot.bbs", "package": "cn.qczlit.withyou",
"versionCode": 7, "versionCode": 7,
"permissions": [ "permissions": [
"VIBRATE", "VIBRATE",
@@ -66,10 +66,10 @@
[ [
"./plugins/withForegroundService", "./plugins/withForegroundService",
{ {
"notificationTitle": "萝卜社区", "notificationTitle": "威友",
"notificationBody": "正在后台同步消息", "notificationBody": "正在后台同步消息",
"notificationIcon": "ic_notification", "notificationIcon": "ic_notification",
"channelId": "carrot_sync_foreground", "channelId": "withyou_sync_foreground",
"channelName": "消息同步" "channelName": "消息同步"
} }
], ],
@@ -86,7 +86,7 @@
[ [
"expo-camera", "expo-camera",
{ {
"cameraPermission": "允许萝卜社区访问您的相机以扫描二维码" "cameraPermission": "允许威友访问您的相机以扫描二维码"
} }
], ],
[ [
@@ -104,16 +104,16 @@
[ [
"expo-media-library", "expo-media-library",
{ {
"photosPermission": "允许萝卜社区访问您的照片以发布内容", "photosPermission": "允许威友访问您的照片以发布内容",
"savePhotosPermission": "允许萝卜社区保存照片到您的相册", "savePhotosPermission": "允许威友保存照片到您的相册",
"isAccessMediaLocationEnabled": true "isAccessMediaLocationEnabled": true
} }
], ],
[ [
"expo-image-picker", "expo-image-picker",
{ {
"photosPermission": "允许萝卜社区访问您的照片以选择图片", "photosPermission": "允许威友访问您的照片以选择图片",
"cameraPermission": "允许萝卜社区访问您的相机以拍摄图片", "cameraPermission": "允许威友访问您的相机以拍摄图片",
"colors": { "colors": {
"cropToolbarColor": "#111827", "cropToolbarColor": "#111827",
"cropToolbarIconColor": "#ffffff", "cropToolbarIconColor": "#ffffff",

View File

@@ -27,6 +27,9 @@
}, },
"production": { "production": {
"autoIncrement": true, "autoIncrement": true,
"android": {
"buildType": "apk"
},
"ios": { "ios": {
"resourceClass": "m-medium" "resourceClass": "m-medium"
} }

View File

@@ -16,6 +16,9 @@ config.resolver.alias = {
'@': path.resolve(__dirname, 'src'), '@': path.resolve(__dirname, 'src'),
}; };
// Ensure platform-specific files are resolved correctly
config.resolver.platforms = ['ios', 'android', 'native', 'web'];
// NOTE: enhanceMiddleware is marked as deprecated in Metro's types, // NOTE: enhanceMiddleware is marked as deprecated in Metro's types,
// but there's currently no official alternative for custom dev server headers. // but there's currently no official alternative for custom dev server headers.
// For production, configure COEP/COOP headers on your hosting platform. // For production, configure COEP/COOP headers on your hosting platform.

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "carrot_bbs", "name": "with_you",
"version": "1.0.3", "version": "0.1.sha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "carrot_bbs", "name": "with_you",
"version": "1.0.3", "version": "0.1.sha",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.1.1", "@expo/vector-icons": "^15.1.1",
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "carrot_bbs", "name": "with_you",
"version": "1.0.3", "version": "0.1.sha",
"main": "expo-router/entry", "main": "expo-router/entry",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",

View File

@@ -1,4 +1,4 @@
const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins'); const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@@ -18,10 +18,10 @@ const FOREGROUND_SERVICE_DATA_SYNC = 'android.permission.FOREGROUND_SERVICE_DATA
*/ */
const withForegroundService = (config, options = {}) => { const withForegroundService = (config, options = {}) => {
const { const {
notificationTitle = '萝卜社区', notificationTitle = '威友',
notificationBody = '正在后台同步消息', notificationBody = '正在后台同步消息',
notificationIcon = 'ic_notification', notificationIcon = 'ic_notification',
channelId = 'carrot_sync_foreground', channelId = 'withyou_sync_foreground',
channelName = '消息同步', channelName = '消息同步',
} = options; } = options;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
View, View,
StyleSheet, StyleSheet,
@@ -187,7 +187,7 @@ export function AppDesktopShell() {
> >
<View style={styles.sidebarHeader}> <View style={styles.sidebarHeader}>
<MaterialCommunityIcons name="carrot" size={32} color={colors.primary.main} /> <MaterialCommunityIcons name="carrot" size={32} color={colors.primary.main} />
{!isCollapsed && <Text style={styles.logoText}>BBS</Text>} {!isCollapsed && <Text style={styles.logoText}></Text>}
</View> </View>
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}> <ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {

View File

@@ -221,7 +221,7 @@ const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose })
setScanned(true); setScanned(true);
onClose(); onClose();
if (data.startsWith('carrotbbs://qrcode/login')) { if (data.startsWith('withyou://qrcode/login')) {
const sessionId = extractSessionId(data); const sessionId = extractSessionId(data);
if (sessionId) { if (sessionId) {
router.push(hrefs.hrefQrLoginConfirm(sessionId)); router.push(hrefs.hrefQrLoginConfirm(sessionId));

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { Share as RNShare, Platform } from 'react-native'; import { Share as RNShare, Platform } from 'react-native';
export interface ShareSheetProps { export interface ShareSheetProps {
@@ -18,7 +18,7 @@ const ShareSheet: React.FC<ShareSheetProps> = ({
}) => { }) => {
const shareText = useMemo(() => { const shareText = useMemo(() => {
const title = postTitle || '胡萝卜BBS帖子'; const title = postTitle || '威友帖子';
return `${title} ${postUrl || ''}`; return `${title} ${postUrl || ''}`;
}, [postTitle, postUrl]); }, [postTitle, postUrl]);

View File

@@ -218,7 +218,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
const allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp']; const allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const fileExt = allowedExts.includes(ext) ? ext : 'jpg'; const fileExt = allowedExts.includes(ext) ? ext : 'jpg';
const fileName = `carrot_${Date.now()}.${fileExt}`; const fileName = `withyou_${Date.now()}.${fileExt}`;
const destination = new File(Paths.cache, fileName); const destination = new File(Paths.cache, fileName);
// File.downloadFileAsync 是新版 expo-file-system/next 的静态方法 // File.downloadFileAsync 是新版 expo-file-system/next 的静态方法

View File

@@ -7,11 +7,18 @@ type PagerViewProps = {
onPageSelected?: (e: { nativeEvent: { position: number } }) => void; onPageSelected?: (e: { nativeEvent: { position: number } }) => void;
overdrag?: boolean; overdrag?: boolean;
children: React.ReactNode; children: React.ReactNode;
testID?: string;
};
type PagerViewRef = {
setPage: (page: number) => void;
setPageWithoutAnimation: (page: number) => void;
setScrollEnabled: (scrollEnabled: boolean) => void;
}; };
function PagerViewInternal( function PagerViewInternal(
{ style, children }: PagerViewProps, { style, children }: PagerViewProps,
_ref: ForwardedRef<{ setPage: (page: number) => void }> _ref: ForwardedRef<PagerViewRef>
) { ) {
const childrenArray = React.Children.toArray(children); const childrenArray = React.Children.toArray(children);
return ( return (

View File

@@ -1,5 +1,5 @@
const DEFAULT_DB_NAME = 'carrot_bbs.db'; const DEFAULT_DB_NAME = 'with_you.db';
const DB_NAME_PREFIX = 'carrot_bbs_'; const DB_NAME_PREFIX = 'with_you_';
export function getDbName(userId: string | null): string { export function getDbName(userId: string | null): string {
return userId ? `${DB_NAME_PREFIX}${userId}.db` : DEFAULT_DB_NAME; return userId ? `${DB_NAME_PREFIX}${userId}.db` : DEFAULT_DB_NAME;

View File

@@ -1,6 +1,6 @@
/** /**
* 忘记密码页 ForgotPasswordScreen简洁表单样式 * 忘记密码页 ForgotPasswordScreen简洁表单样式
* 胡萝卜BBS - 找回密码 * 威友 - 找回密码
* *
* 设计风格: * 设计风格:
* - 纯白背景,扁平化设计 * - 纯白背景,扁平化设计

View File

@@ -1,6 +1,6 @@
/** /**
* 登录页 LoginScreen简洁表单样式 * 登录页 LoginScreen简洁表单样式
* 胡萝卜BBS - 用户登录 * 威友 - 用户登录
* *
* 设计风格: * 设计风格:
* - 纯白背景,扁平化设计 * - 纯白背景,扁平化设计

View File

@@ -1,13 +1,13 @@
/** /**
* 注册页面(分步向导版) * 注册页面(分步向导版)
* 胡萝卜BBS - 用户注册流程 * 威友 - 用户注册流程
* *
* 步骤: * 步骤:
* 1. 输入邮箱 * 1. 输入邮箱
* 2. 填写验证码 * 2. 填写验证码
* 3. 设置用户信息 * 3. 设置用户信息
* *
* 使用 registerStore 持久化存储步骤数据 * 使用 registerStore 纯内存状态管理
*/ */
import React, { useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
@@ -42,7 +42,7 @@ import { RegisterStep1Email } from './RegisterStep1Email';
import { RegisterStep2Verification } from './RegisterStep2Verification'; import { RegisterStep2Verification } from './RegisterStep2Verification';
import { RegisterStep3Profile } from './RegisterStep3Profile'; import { RegisterStep3Profile } from './RegisterStep3Profile';
// 胡萝卜橙主题色 // 威友橙主题色
const THEME_COLORS = { const THEME_COLORS = {
primary: '#FF6B35', primary: '#FF6B35',
primaryLight: '#FF8C5A', primaryLight: '#FF8C5A',
@@ -87,16 +87,9 @@ export const RegisterScreen: React.FC = () => {
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(20)).current; const slideAnim = useRef(new Animated.Value(20)).current;
// 恢复持久化的数据 // 页面挂载时重置倒计时,避免上次残留的倒计时导致无法重新获取验证码
const [isHydrated, setIsHydrated] = React.useState(false);
useEffect(() => { useEffect(() => {
const hydrateData = async () => { setCountdown(0);
const { hydrate } = useRegisterStore.getState();
await hydrate();
setIsHydrated(true);
};
hydrateData();
}, []); }, []);
// 检查是否已登录 // 检查是否已登录

View File

@@ -1,9 +1,9 @@
/** /**
* 欢迎页 WelcomeScreen * 欢迎页 WelcomeScreen
* 胡萝卜BBS - 用户欢迎页 * 威友 - 用户欢迎页
* *
* 亮色模式:白色主色调 + 胡萝卜橙辅色 * 亮色模式:白色主色调 + 威友橙辅色
* 暗色模式:深色背景 + 胡萝卜橙辅色 * 暗色模式:深色背景 + 威友橙辅色
*/ */
import React, { useEffect, useRef, useMemo } from 'react'; import React, { useEffect, useRef, useMemo } from 'react';
@@ -89,7 +89,7 @@ export const WelcomeScreen: React.FC = () => {
{/* 品牌区域 */} {/* 品牌区域 */}
<View style={styles.brandSection}> <View style={styles.brandSection}>
<Text style={styles.welcomeEn}>Welcome To</Text> <Text style={styles.welcomeEn}>Welcome To</Text>
<Text style={styles.brandName}></Text> <Text style={styles.brandName}></Text>
<View style={styles.brandUnderline} /> <View style={styles.brandUnderline} />
<Text style={styles.subtitle}> <Text style={styles.subtitle}>
@@ -139,7 +139,7 @@ function createWelcomeStyles(colors: AppColors) {
paddingBottom: 40, paddingBottom: 40,
}, },
// 装饰性背景元素 - 使用胡萝卜橙的淡色 // 装饰性背景元素 - 使用威友橙的淡色
decorCircle1: { decorCircle1: {
position: 'absolute', position: 'absolute',
top: -100, top: -100,
@@ -147,7 +147,7 @@ function createWelcomeStyles(colors: AppColors) {
width: 400, width: 400,
height: 400, height: 400,
borderRadius: 200, borderRadius: 200,
backgroundColor: colors.primary.main + '08', // 非常淡的胡萝卜 backgroundColor: colors.primary.main + '08', // 非常淡的威友
}, },
decorCircle2: { decorCircle2: {
position: 'absolute', position: 'absolute',

View File

@@ -1,6 +1,6 @@
/** /**
* 认证屏幕导出 * 认证屏幕导出
* 胡萝卜BBS - 登录注册模块 * 威友 - 登录注册模块
*/ */
export { LoginScreen } from './LoginScreen'; export { LoginScreen } from './LoginScreen';

View File

@@ -1,6 +1,6 @@
/** /**
* 发帖页 CreatePostScreen响应式适配 * 发帖页 CreatePostScreen响应式适配
* 胡萝卜BBS - 发布新帖子 * 威友 - 发布新帖子
* 参考微博发帖界面设计 * 参考微博发帖界面设计
* 表单在宽屏下居中显示 * 表单在宽屏下居中显示
* 图片选择器在宽屏下显示更大的预览 * 图片选择器在宽屏下显示更大的预览

View File

@@ -1,6 +1,6 @@
/** /**
* 首页 HomeScreen * 首页 HomeScreen
* 胡萝卜BBS - 首页展示 * 威友 - 首页展示
* 支持列表和多列网格模式(响应式布局) * 支持列表和多列网格模式(响应式布局)
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 帖子详情页 PostDetailScreen - QQ频道风格响应式版本 * 帖子详情页 PostDetailScreen - QQ频道风格响应式版本
* 胡萝卜BBS - 显示完整帖子内容和评论 * 威友 - 显示完整帖子内容和评论
* 桌面端使用双栏布局,移动端使用单栏布局 * 桌面端使用双栏布局,移动端使用单栏布局
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 搜索页 SearchScreen响应式版本 * 搜索页 SearchScreen响应式版本
* 胡萝卜BBS - 搜索帖子、用户 * 威友 - 搜索帖子、用户
* 支持响应式布局,宽屏下显示更大的搜索结果区域 * 支持响应式布局,宽屏下显示更大的搜索结果区域
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 聊天页 ChatScreen * 聊天页 ChatScreen
* 胡萝卜BBS - 私信/群聊聊天界面 * 威友 - 私信/群聊聊天界面
* 高级现代化设计 * 高级现代化设计
* 支持群聊功能:显示发送者头像和昵称、@提及功能 * 支持群聊功能:显示发送者头像和昵称、@提及功能
* 支持响应式布局(桌面端宽屏优化) * 支持响应式布局(桌面端宽屏优化)

View File

@@ -1,6 +1,6 @@
/** /**
* 消息列表页 MessageListScreen * 消息列表页 MessageListScreen
* 胡萝卜BBS - 消息列表 * 威友 - 消息列表
* *
* 【重构后】使用MessageManager架构 * 【重构后】使用MessageManager架构
* - 纯渲染组件,不直接管理数据 * - 纯渲染组件,不直接管理数据

View File

@@ -1,6 +1,6 @@
/** /**
* 通知页 NotificationsScreen扁平化风格 * 通知页 NotificationsScreen扁平化风格
* 胡萝卜BBS - 系统消息列表 * 威友 - 系统消息列表
* 【游标分页】使用 messageService.getSystemMessagesCursor * 【游标分页】使用 messageService.getSystemMessagesCursor
* 支持响应式布局 * 支持响应式布局
* *

View File

@@ -1,6 +1,6 @@
/** /**
* PrivateChatInfoScreen 私聊聊天管理页面 * PrivateChatInfoScreen 私聊聊天管理页面
* 胡萝卜BBS - 私聊设置界面 * 威友 - 私聊设置界面
* 参考QQ和微信的实现提供聊天管理功能 * 参考QQ和微信的实现提供聊天管理功能
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 关于我们页面 AboutScreen * 关于我们页面 AboutScreen
* 胡萝卜BBS - 应用信息、版本检查与更新 * 威友 - 应用信息、版本检查与更新
* 扁平化设计风格,参考登录、注册、设置页面 * 扁平化设计风格,参考登录、注册、设置页面
*/ */
@@ -39,7 +39,7 @@ const THEME_COLORS = {
primaryDark: '#E55A2B', primaryDark: '#E55A2B',
}; };
const APP_NAME = '萝卜社区'; const APP_NAME = '威友';
const APP_SLOGAN = '连接校园,分享生活'; const APP_SLOGAN = '连接校园,分享生活';
const APP_VERSION = Constants.expoConfig?.version || '1.0.0'; const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
const BUILD_NUMBER = Constants.expoConfig?.android?.versionCode || Constants.expoConfig?.ios?.buildNumber || '1'; const BUILD_NUMBER = Constants.expoConfig?.android?.versionCode || Constants.expoConfig?.ios?.buildNumber || '1';

View File

@@ -1,6 +1,6 @@
/** /**
* 聊天设置页 ChatSettingsScreen * 聊天设置页 ChatSettingsScreen
* 胡萝卜BBS - 聊天个性化设置 * 威友 - 聊天个性化设置
*/ */
import React, { useMemo, useCallback, useRef } from 'react'; import React, { useMemo, useCallback, useRef } from 'react';

View File

@@ -1,6 +1,6 @@
/** /**
* 数据与存储设置页 DataStorageScreen响应式适配 * 数据与存储设置页 DataStorageScreen响应式适配
* 胡萝卜BBS - 缓存管理、存储统计 * 威友 - 缓存管理、存储统计
*/ */
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';

View File

@@ -1,6 +1,6 @@
/** /**
* 编辑资料页 EditProfileScreen响应式适配 * 编辑资料页 EditProfileScreen响应式适配
* 胡萝卜BBS - 编辑用户资料 * 威友 - 编辑用户资料
* 与用户资料页样式完全一致 * 与用户资料页样式完全一致
* 表单在宽屏下居中显示 * 表单在宽屏下居中显示
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 通知设置页 NotificationSettingsScreen响应式适配 * 通知设置页 NotificationSettingsScreen响应式适配
* 胡萝卜BBS - 通知相关设置 * 威友 - 通知相关设置
* 在宽屏下居中显示 * 在宽屏下居中显示
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 隐私政策页面 PrivacyPolicyScreen * 隐私政策页面 PrivacyPolicyScreen
* 胡萝卜BBS - 隐私政策 * 威友 - 隐私政策
* 扁平化设计风格 * 扁平化设计风格
*/ */
@@ -22,7 +22,7 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度 // 内容最大宽度
const CONTENT_MAX_WIDTH = 400; const CONTENT_MAX_WIDTH = 400;
// 胡萝卜橙主题色 // 威友橙主题色
const THEME_COLORS = { const THEME_COLORS = {
primary: '#FF6B35', primary: '#FF6B35',
}; };
@@ -34,7 +34,7 @@ const LAST_UPDATED = '2026年4月4日';
const PRIVACY_SECTIONS = [ const PRIVACY_SECTIONS = [
{ {
title: '引言', title: '引言',
content: `萝卜社区(以下简称"我们"或"本应用")非常重视用户的隐私和个人信息保护。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。 content: `威友(以下简称"我们"或"本应用")非常重视用户的隐私和个人信息保护。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。
本隐私政策旨在帮助您了解我们如何收集、使用、存储、共享和保护您的个人信息,以及您享有的相关权利。请您在使用我们的服务前,仔细阅读并理解本隐私政策的全部内容。 本隐私政策旨在帮助您了解我们如何收集、使用、存储、共享和保护您的个人信息,以及您享有的相关权利。请您在使用我们的服务前,仔细阅读并理解本隐私政策的全部内容。
@@ -293,7 +293,7 @@ export const PrivacyPolicyScreen: React.FC = () => {
<View style={styles.highlightBox}> <View style={styles.highlightBox}>
<Text style={styles.highlightTitle}></Text> <Text style={styles.highlightTitle}></Text>
<Text style={styles.highlightText}> <Text style={styles.highlightText}>
</Text> </Text>
</View> </View>

View File

@@ -1,6 +1,6 @@
/** /**
* 个人主页 ProfileScreen * 个人主页 ProfileScreen
* 胡萝卜BBS - 当前用户个人主页 * 威友 - 当前用户个人主页
* 使用统一的 UserProfileScreen 组件mode='self' * 使用统一的 UserProfileScreen 组件mode='self'
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 设置页 SettingsScreen响应式适配 * 设置页 SettingsScreen响应式适配
* 胡萝卜BBS - 应用设置 * 威友 - 应用设置
* 在宽屏下居中显示,最大宽度限制 * 在宽屏下居中显示,最大宽度限制
*/ */
@@ -363,7 +363,7 @@ export const SettingsScreen: React.FC = () => {
<View style={styles.footer}> <View style={styles.footer}>
<Text variant="caption" color={colors.text.hint}> <Text variant="caption" color={colors.text.hint}>
v{APP_VERSION} v{APP_VERSION}
</Text> </Text>
<Text variant="caption" color={colors.text.hint} style={styles.copyright}> <Text variant="caption" color={colors.text.hint} style={styles.copyright}>
© 2026 © 2026

View File

@@ -1,6 +1,6 @@
/** /**
* 用户协议页面 TermsOfServiceScreen * 用户协议页面 TermsOfServiceScreen
* 胡萝卜BBS - 用户服务协议 * 威友 - 用户服务协议
* 扁平化设计风格 * 扁平化设计风格
*/ */
@@ -22,7 +22,7 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度 // 内容最大宽度
const CONTENT_MAX_WIDTH = 400; const CONTENT_MAX_WIDTH = 400;
// 胡萝卜橙主题色 // 威友橙主题色
const THEME_COLORS = { const THEME_COLORS = {
primary: '#FF6B35', primary: '#FF6B35',
}; };
@@ -34,7 +34,7 @@ const LAST_UPDATED = '2026年4月4日';
const TERMS_SECTIONS = [ const TERMS_SECTIONS = [
{ {
title: '一、协议的范围', title: '一、协议的范围',
content: `1.1 本协议是您与萝卜社区(以下简称"我们"或"本应用")之间关于您使用本应用服务所订立的协议。 content: `1.1 本协议是您与威友(以下简称"我们"或"本应用")之间关于您使用本应用服务所订立的协议。
1.2 本协议内容包括协议正文及所有我们已经发布或将来可能发布的各类规则、公告或通知。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。 1.2 本协议内容包括协议正文及所有我们已经发布或将来可能发布的各类规则、公告或通知。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。

View File

@@ -1,6 +1,6 @@
/** /**
* 统一的用户主页组件 * 统一的用户主页组件
* 胡萝卜BBS - 支持两种模式self当前用户和 other其他用户 * 威友 - 支持两种模式self当前用户和 other其他用户
* 采用现代卡片式设计,优化视觉层次和交互体验 * 采用现代卡片式设计,优化视觉层次和交互体验
* 支持桌面端双栏布局 * 支持桌面端双栏布局
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* 用户主页 UserScreen * 用户主页 UserScreen
* 胡萝卜BBS - 查看其他用户资料 * 威友 - 查看其他用户资料
* 使用统一的 UserProfileScreen 组件mode='other' * 使用统一的 UserProfileScreen 组件mode='other'
*/ */

View File

@@ -1,4 +1,4 @@
/** /**
* 后台同步管理器 * 后台同步管理器
* *
* 整合前台服务、后台同步、消息状态管理 * 整合前台服务、后台同步、消息状态管理
@@ -86,7 +86,7 @@ class BackgroundSyncManager {
if (mode === BackgroundSyncMode.REALTIME) { if (mode === BackgroundSyncMode.REALTIME) {
// 切换到实时模式:立即启动前台服务(无论前台还是后台) // 切换到实时模式:立即启动前台服务(无论前台还是后台)
const started = await ForegroundServiceModule.start({ const started = await ForegroundServiceModule.start({
title: '萝卜社区', title: '威友',
body: '正在后台同步消息', body: '正在后台同步消息',
}); });
if (started) { if (started) {
@@ -134,7 +134,7 @@ class BackgroundSyncManager {
if (this.mode === BackgroundSyncMode.REALTIME) { if (this.mode === BackgroundSyncMode.REALTIME) {
// 确保前台服务正在运行 // 确保前台服务正在运行
await ForegroundServiceModule.start({ await ForegroundServiceModule.start({
title: '萝卜社区', title: '威友',
body: '正在后台同步消息', body: '正在后台同步消息',
}); });
@@ -154,7 +154,7 @@ class BackgroundSyncManager {
// 实时模式下:前台服务保持运行,只更新通知文字 // 实时模式下:前台服务保持运行,只更新通知文字
if (this.mode === BackgroundSyncMode.REALTIME) { if (this.mode === BackgroundSyncMode.REALTIME) {
await ForegroundServiceModule.update('萝卜社区', '正在后台同步消息'); await ForegroundServiceModule.update('威友', '正在后台同步消息');
} }
// 立即同步一次(获取离线消息) // 立即同步一次(获取离线消息)
@@ -235,12 +235,12 @@ class BackgroundSyncManager {
if (totalUnread > 0) { if (totalUnread > 0) {
await ForegroundServiceModule.update( await ForegroundServiceModule.update(
'萝卜社区', '威友',
`您有 ${totalUnread} 条未读消息` `您有 ${totalUnread} 条未读消息`
); );
} else { } else {
await ForegroundServiceModule.update( await ForegroundServiceModule.update(
'萝卜社区', '威友',
'正在后台同步消息' '正在后台同步消息'
); );
} }

View File

@@ -1,4 +1,4 @@
/** /**
* 前台服务模块 * 前台服务模块
* *
* 提供 React Native 层对 Android 前台服务的控制接口 * 提供 React Native 层对 Android 前台服务的控制接口
@@ -40,7 +40,7 @@ export const ForegroundServiceModule = {
try { try {
const result = await ForegroundService.start( const result = await ForegroundService.start(
options.title || '萝卜社区', options.title || '威友',
options.body || '正在后台同步消息' options.body || '正在后台同步消息'
); );
console.log('[ForegroundService] 服务已启动'); console.log('[ForegroundService] 服务已启动');

View File

@@ -11,13 +11,13 @@ import { Platform } from 'react-native';
import { eventBus } from '@/core/events/EventBus'; import { eventBus } from '@/core/events/EventBus';
// 生产地址 https://bbs.littlelan.cn // 生产地址 https://withyou.littlelan.cn
const getBaseUrl = () => { const getBaseUrl = () => {
const configuredBaseUrl = Constants.expoConfig?.extra?.apiBaseUrl; const configuredBaseUrl = Constants.expoConfig?.extra?.apiBaseUrl;
if (typeof configuredBaseUrl === 'string' && configuredBaseUrl.trim().length > 0) { if (typeof configuredBaseUrl === 'string' && configuredBaseUrl.trim().length > 0) {
return configuredBaseUrl; return configuredBaseUrl;
} }
return 'https://bbs.littlelan.cn/api/v1'; return 'https://withyou.littlelan.cn/api/v1';
}; };
const BASE_URL = getBaseUrl(); const BASE_URL = getBaseUrl();

View File

@@ -144,7 +144,7 @@ async function downloadAndInstallAPK(downloadUrl: string, version: string): Prom
} }
try { try {
const downloadPath = `${FileSystem.documentDirectory}carrot-bbs-${version}.apk`; const downloadPath = `${FileSystem.documentDirectory}with-you-${version}.apk`;
// 显示下载进度 // 显示下载进度
showConfirm({ showConfirm({

View File

@@ -96,22 +96,24 @@ class WebRTCManager {
console.log('[WebRTC] ontrack: no track in event'); console.log('[WebRTC] ontrack: no track in event');
return; return;
} }
// Official react-native-webrtc approach: use event.streams[0] if available
if (event.streams && event.streams[0]) { // Always manually construct/append to remoteStream to ensure all tracks are collected
this.remoteStream = event.streams[0]; // react-native-webrtc may fire ontrack per-track and event.streams[0] might not contain all tracks
} else { if (!this.remoteStream) {
// Fallback: manually construct stream this.remoteStream = new MediaStream();
if (!this.remoteStream) {
this.remoteStream = new MediaStream();
}
// Check if track already exists to avoid duplicates
const existingTracks = this.remoteStream.getTracks();
const trackExists = existingTracks.some(t => t.id === event.track.id);
if (!trackExists) {
this.remoteStream.addTrack(event.track);
}
} }
this.emit({ type: 'remotestream', stream: this.remoteStream! });
// Check if track already exists to avoid duplicates
const existingTracks = this.remoteStream.getTracks();
const trackExists = existingTracks.some(t => t.id === event.track.id);
if (!trackExists) {
this.remoteStream.addTrack(event.track);
console.log('[WebRTC] Added remote track:', event.track.kind, 'total tracks:', this.remoteStream.getTracks().length);
} else {
console.log('[WebRTC] Remote track already exists:', event.track.kind);
}
this.emit({ type: 'remotestream', stream: this.remoteStream });
}; };
// @ts-ignore // @ts-ignore
@@ -655,7 +657,8 @@ class WebRTCManager {
} }
if (this.remoteStream) { if (this.remoteStream) {
this.remoteStream.getTracks().forEach((track) => track.stop()); // Do not stop remote tracks; they are managed by the peer connection
// and will be ended automatically when the connection closes
this.remoteStream = null; this.remoteStream = null;
} }

View File

@@ -1,12 +1,9 @@
/** /**
* 注册流程状态管理 * 注册流程状态管理
* 使用 Zustand + AsyncStorage 实现步骤数据的持久化 * 使用 Zustand 纯内存状态,不持久化到 AsyncStorage
*/ */
import { create } from 'zustand'; import { create } from 'zustand';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = 'register_form_data';
export type RegisterStep = 0 | 1 | 2; export type RegisterStep = 0 | 1 | 2;
@@ -42,9 +39,6 @@ interface RegisterState {
resetRegisterData: () => void; resetRegisterData: () => void;
goToNextStep: () => void; goToNextStep: () => void;
goToPrevStep: () => void; goToPrevStep: () => void;
// 持久化
hydrate: () => Promise<void>;
persistData: () => Promise<void>;
} }
const initialFormData: RegisterFormData = { const initialFormData: RegisterFormData = {
@@ -63,31 +57,18 @@ const initialCompletedSteps: Record<RegisterStep, boolean> = {
2: false, 2: false,
}; };
export const useRegisterStore = create<RegisterState>((set, get) => ({ export const useRegisterStore = create<RegisterState>((set) => ({
currentStep: 0, currentStep: 0,
formData: initialFormData, formData: initialFormData,
countdown: 0, countdown: 0,
completedSteps: initialCompletedSteps, completedSteps: initialCompletedSteps,
setCurrentStep: (step) => { setCurrentStep: (step) => set({ currentStep: step }),
set({ currentStep: step });
get().persistData();
},
updateFormData: (data) => updateFormData: (data) =>
set((state) => { set((state) => ({
const newFormData = { ...state.formData, ...data }; formData: { ...state.formData, ...data },
// 异步保存到 AsyncStorage })),
AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({
currentStep: state.currentStep,
formData: newFormData,
completedSteps: state.completedSteps,
})
).catch(() => {});
return { formData: newFormData };
}),
setCountdown: (seconds) => set({ countdown: seconds }), setCountdown: (seconds) => set({ countdown: seconds }),
@@ -97,81 +78,33 @@ export const useRegisterStore = create<RegisterState>((set, get) => ({
})), })),
markStepCompleted: (step) => markStepCompleted: (step) =>
set((state) => { set((state) => ({
const newCompletedSteps = { ...state.completedSteps, [step]: true }; completedSteps: { ...state.completedSteps, [step]: true },
// 异步保存到 AsyncStorage })),
AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({
currentStep: state.currentStep,
formData: state.formData,
completedSteps: newCompletedSteps,
})
).catch(() => {});
return { completedSteps: newCompletedSteps };
}),
resetRegisterData: () => { resetRegisterData: () =>
set({ set({
currentStep: 0, currentStep: 0,
formData: initialFormData, formData: initialFormData,
countdown: 0, countdown: 0,
completedSteps: initialCompletedSteps, completedSteps: initialCompletedSteps,
}); }),
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
},
goToNextStep: () => { goToNextStep: () =>
const { currentStep } = get(); set((state) => {
if (currentStep < 2) { if (state.currentStep < 2) {
const newStep = (currentStep + 1) as RegisterStep; return { currentStep: (state.currentStep + 1) as RegisterStep };
set({ currentStep: newStep });
get().persistData();
}
},
goToPrevStep: () => {
const { currentStep } = get();
if (currentStep > 0) {
const newStep = (currentStep - 1) as RegisterStep;
set({ currentStep: newStep });
get().persistData();
}
},
// 从 AsyncStorage 恢复数据
hydrate: async () => {
try {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
if (raw) {
const data = JSON.parse(raw);
set({
currentStep: data.currentStep ?? 0,
formData: { ...initialFormData, ...data.formData },
completedSteps: { ...initialCompletedSteps, ...data.completedSteps },
});
} }
} catch { return state;
// 忽略错误 }),
}
},
// 保存数据到 AsyncStorage goToPrevStep: () =>
persistData: async () => { set((state) => {
const { currentStep, formData, completedSteps } = get(); if (state.currentStep > 0) {
try { return { currentStep: (state.currentStep - 1) as RegisterStep };
await AsyncStorage.setItem( }
STORAGE_KEY, return state;
JSON.stringify({ }),
currentStep,
formData,
completedSteps,
})
);
} catch {
// 忽略错误
}
},
})); }));
// 导出便捷 hooks // 导出便捷 hooks

View File

@@ -146,13 +146,20 @@ function setupWebRTCEvents(callId: string, myUserId: string): void {
* Handle remote stream with video track detection * Handle remote stream with video track detection
*/ */
function handleRemoteStream(stream: MediaStream): void { function handleRemoteStream(stream: MediaStream): void {
callStore.setState({ peerStream: stream }); // Create a new MediaStream instance to force zustand subscribers to update
// because react-native-webrtc may add tracks to the same stream object reference
const newStream = new MediaStream();
stream.getTracks().forEach((track) => {
newStream.addTrack(track);
});
callStore.setState({ peerStream: newStream });
// Detect video tracks in remote stream // Detect video tracks in remote stream
const videoTracks = stream.getVideoTracks(); const videoTracks = newStream.getVideoTracks();
const hasPeerVideo = videoTracks.length > 0 && videoTracks.some((t) => t.enabled); const hasPeerVideo = videoTracks.length > 0 && videoTracks.some((t) => t.enabled);
console.log('[CallStore] Remote stream received, hasVideo:', hasPeerVideo, 'videoTracks:', videoTracks.length); console.log('[CallStore] Remote stream received, hasVideo:', hasPeerVideo, 'videoTracks:', videoTracks.length, 'audioTracks:', newStream.getAudioTracks().length);
callStore.setState((s) => ({ callStore.setState((s) => ({
currentCall: s.currentCall currentCall: s.currentCall

View File

@@ -1,5 +1,5 @@
/** /**
* 胡萝卜BBS 主题明暗色板、Paper 主题、设计 token * 威友 主题明暗色板、Paper 主题、设计 token
* 新代码请优先使用 useAppColors();静态 colors 为浅色快照,与深色模式不同步。 * 新代码请优先使用 useAppColors();静态 colors 为浅色快照,与深色模式不同步。
*/ */

View File

@@ -1,5 +1,5 @@
// ============================================ // ============================================
// Carrot BBS - Core Type Definitions // WithYou - Core Type Definitions
// 使用后端DTO类型保持snake_case命名规范 // 使用后端DTO类型保持snake_case命名规范
// ============================================ // ============================================