Rebrand the entire application from "胡萝卜社区" (Carrot BBS) to "威友" (WithYou), including:
- 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:
@@ -1,4 +1,4 @@
|
||||
name: Frontend CI
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -20,7 +20,7 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: code.littlelan.cn
|
||||
IMAGE_NAME: carrot_bbs/frontend-web
|
||||
IMAGE_NAME: with_you/frontend-web
|
||||
OTA_PLATFORM: android
|
||||
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
||||
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
||||
@@ -150,6 +150,33 @@ jobs:
|
||||
- name: Generate Android native project
|
||||
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
|
||||
run: |
|
||||
cd android
|
||||
@@ -198,7 +225,7 @@ jobs:
|
||||
}
|
||||
expoAutolinking.useExpoModules()
|
||||
|
||||
rootProject.name = '萝卜社区'
|
||||
rootProject.name = '威友'
|
||||
|
||||
expoAutolinking.useExpoVersionCatalog()
|
||||
|
||||
@@ -260,6 +287,16 @@ jobs:
|
||||
expo.useLegacyPackaging=false
|
||||
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)
|
||||
run: |
|
||||
cd android
|
||||
@@ -269,7 +306,7 @@ jobs:
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: carrot-bbs-android-release-apk
|
||||
name: withyou-android-release-apk
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
- name: Resolve runtime version
|
||||
@@ -350,8 +387,8 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
cache-from: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache
|
||||
cache-to: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache,mode=max
|
||||
cache-from: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache
|
||||
cache-to: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache,mode=max
|
||||
|
||||
- name: Show image tags
|
||||
run: |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const appJson = require('./app.json');
|
||||
|
||||
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 devApiBaseUrl = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.31.238:8080/api/v1';
|
||||
|
||||
|
||||
28
app.json
28
app.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
{
|
||||
"expo": {
|
||||
"name": "萝卜社区",
|
||||
"name": "威友",
|
||||
"slug": "qojo",
|
||||
"version": "1.0.13",
|
||||
"version": "0.1.sha",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"scheme": "carrotbbs",
|
||||
"scheme": "withyou",
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
@@ -15,7 +15,7 @@
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"infoPlist": {
|
||||
"NSMicrophoneUsageDescription": "允许萝卜社区访问您的麦克风以进行语音通话",
|
||||
"NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话",
|
||||
"UIBackgroundModes": [
|
||||
"fetch",
|
||||
"remote-notification",
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
},
|
||||
"bundleIdentifier": "skin.carrot.bbs"
|
||||
"bundleIdentifier": "cn.qczlit.withyou"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||
},
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"package": "skin.carrot.bbs",
|
||||
"package": "cn.qczlit.withyou",
|
||||
"versionCode": 7,
|
||||
"permissions": [
|
||||
"VIBRATE",
|
||||
@@ -66,10 +66,10 @@
|
||||
[
|
||||
"./plugins/withForegroundService",
|
||||
{
|
||||
"notificationTitle": "萝卜社区",
|
||||
"notificationTitle": "威友",
|
||||
"notificationBody": "正在后台同步消息",
|
||||
"notificationIcon": "ic_notification",
|
||||
"channelId": "carrot_sync_foreground",
|
||||
"channelId": "withyou_sync_foreground",
|
||||
"channelName": "消息同步"
|
||||
}
|
||||
],
|
||||
@@ -86,7 +86,7 @@
|
||||
[
|
||||
"expo-camera",
|
||||
{
|
||||
"cameraPermission": "允许萝卜社区访问您的相机以扫描二维码"
|
||||
"cameraPermission": "允许威友访问您的相机以扫描二维码"
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -104,16 +104,16 @@
|
||||
[
|
||||
"expo-media-library",
|
||||
{
|
||||
"photosPermission": "允许萝卜社区访问您的照片以发布内容",
|
||||
"savePhotosPermission": "允许萝卜社区保存照片到您的相册",
|
||||
"photosPermission": "允许威友访问您的照片以发布内容",
|
||||
"savePhotosPermission": "允许威友保存照片到您的相册",
|
||||
"isAccessMediaLocationEnabled": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
"photosPermission": "允许萝卜社区访问您的照片以选择图片",
|
||||
"cameraPermission": "允许萝卜社区访问您的相机以拍摄图片",
|
||||
"photosPermission": "允许威友访问您的照片以选择图片",
|
||||
"cameraPermission": "允许威友访问您的相机以拍摄图片",
|
||||
"colors": {
|
||||
"cropToolbarColor": "#111827",
|
||||
"cropToolbarIconColor": "#ffffff",
|
||||
|
||||
3
eas.json
3
eas.json
@@ -27,6 +27,9 @@
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true,
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
},
|
||||
"ios": {
|
||||
"resourceClass": "m-medium"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ config.resolver.alias = {
|
||||
'@': 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,
|
||||
// but there's currently no official alternative for custom dev server headers.
|
||||
// For production, configure COEP/COOP headers on your hosting platform.
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "carrot_bbs",
|
||||
"version": "1.0.3",
|
||||
"name": "with_you",
|
||||
"version": "0.1.sha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "carrot_bbs",
|
||||
"version": "1.0.3",
|
||||
"name": "with_you",
|
||||
"version": "0.1.sha",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.1.1",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "carrot_bbs",
|
||||
"version": "1.0.3",
|
||||
"name": "with_you",
|
||||
"version": "0.1.sha",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins');
|
||||
const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -18,10 +18,10 @@ const FOREGROUND_SERVICE_DATA_SYNC = 'android.permission.FOREGROUND_SERVICE_DATA
|
||||
*/
|
||||
const withForegroundService = (config, options = {}) => {
|
||||
const {
|
||||
notificationTitle = '萝卜社区',
|
||||
notificationTitle = '威友',
|
||||
notificationBody = '正在后台同步消息',
|
||||
notificationIcon = 'ic_notification',
|
||||
channelId = 'carrot_sync_foreground',
|
||||
channelId = 'withyou_sync_foreground',
|
||||
channelName = '消息同步',
|
||||
} = options;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -187,7 +187,7 @@ export function AppDesktopShell() {
|
||||
>
|
||||
<View style={styles.sidebarHeader}>
|
||||
<MaterialCommunityIcons name="carrot" size={32} color={colors.primary.main} />
|
||||
{!isCollapsed && <Text style={styles.logoText}>胡萝卜BBS</Text>}
|
||||
{!isCollapsed && <Text style={styles.logoText}>威友</Text>}
|
||||
</View>
|
||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
|
||||
@@ -221,7 +221,7 @@ const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose })
|
||||
setScanned(true);
|
||||
onClose();
|
||||
|
||||
if (data.startsWith('carrotbbs://qrcode/login')) {
|
||||
if (data.startsWith('withyou://qrcode/login')) {
|
||||
const sessionId = extractSessionId(data);
|
||||
if (sessionId) {
|
||||
router.push(hrefs.hrefQrLoginConfirm(sessionId));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Share as RNShare, Platform } from 'react-native';
|
||||
|
||||
export interface ShareSheetProps {
|
||||
@@ -18,7 +18,7 @@ const ShareSheet: React.FC<ShareSheetProps> = ({
|
||||
}) => {
|
||||
|
||||
const shareText = useMemo(() => {
|
||||
const title = postTitle || '胡萝卜BBS帖子';
|
||||
const title = postTitle || '威友帖子';
|
||||
return `${title} ${postUrl || ''}`;
|
||||
}, [postTitle, postUrl]);
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
const allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
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);
|
||||
|
||||
// File.downloadFileAsync 是新版 expo-file-system/next 的静态方法
|
||||
|
||||
@@ -7,11 +7,18 @@ type PagerViewProps = {
|
||||
onPageSelected?: (e: { nativeEvent: { position: number } }) => void;
|
||||
overdrag?: boolean;
|
||||
children: React.ReactNode;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
type PagerViewRef = {
|
||||
setPage: (page: number) => void;
|
||||
setPageWithoutAnimation: (page: number) => void;
|
||||
setScrollEnabled: (scrollEnabled: boolean) => void;
|
||||
};
|
||||
|
||||
function PagerViewInternal(
|
||||
{ style, children }: PagerViewProps,
|
||||
_ref: ForwardedRef<{ setPage: (page: number) => void }>
|
||||
_ref: ForwardedRef<PagerViewRef>
|
||||
) {
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DEFAULT_DB_NAME = 'carrot_bbs.db';
|
||||
const DB_NAME_PREFIX = 'carrot_bbs_';
|
||||
const DEFAULT_DB_NAME = 'with_you.db';
|
||||
const DB_NAME_PREFIX = 'with_you_';
|
||||
|
||||
export function getDbName(userId: string | null): string {
|
||||
return userId ? `${DB_NAME_PREFIX}${userId}.db` : DEFAULT_DB_NAME;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 忘记密码页 ForgotPasswordScreen(简洁表单样式)
|
||||
* 胡萝卜BBS - 找回密码
|
||||
* 威友 - 找回密码
|
||||
*
|
||||
* 设计风格:
|
||||
* - 纯白背景,扁平化设计
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 登录页 LoginScreen(简洁表单样式)
|
||||
* 胡萝卜BBS - 用户登录
|
||||
* 威友 - 用户登录
|
||||
*
|
||||
* 设计风格:
|
||||
* - 纯白背景,扁平化设计
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
/**
|
||||
* 注册页面(分步向导版)
|
||||
* 胡萝卜BBS - 用户注册流程
|
||||
* 威友 - 用户注册流程
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 输入邮箱
|
||||
* 2. 填写验证码
|
||||
* 3. 设置用户信息
|
||||
*
|
||||
* 使用 registerStore 持久化存储步骤数据
|
||||
* 使用 registerStore 纯内存状态管理
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
@@ -42,7 +42,7 @@ import { RegisterStep1Email } from './RegisterStep1Email';
|
||||
import { RegisterStep2Verification } from './RegisterStep2Verification';
|
||||
import { RegisterStep3Profile } from './RegisterStep3Profile';
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
// 威友橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
@@ -87,16 +87,9 @@ export const RegisterScreen: React.FC = () => {
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const slideAnim = useRef(new Animated.Value(20)).current;
|
||||
|
||||
// 恢复持久化的数据
|
||||
const [isHydrated, setIsHydrated] = React.useState(false);
|
||||
|
||||
// 页面挂载时重置倒计时,避免上次残留的倒计时导致无法重新获取验证码
|
||||
useEffect(() => {
|
||||
const hydrateData = async () => {
|
||||
const { hydrate } = useRegisterStore.getState();
|
||||
await hydrate();
|
||||
setIsHydrated(true);
|
||||
};
|
||||
hydrateData();
|
||||
setCountdown(0);
|
||||
}, []);
|
||||
|
||||
// 检查是否已登录
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
/**
|
||||
* 欢迎页 WelcomeScreen
|
||||
* 胡萝卜BBS - 用户欢迎页
|
||||
* 威友 - 用户欢迎页
|
||||
*
|
||||
* 亮色模式:白色主色调 + 胡萝卜橙辅色
|
||||
* 暗色模式:深色背景 + 胡萝卜橙辅色
|
||||
* 亮色模式:白色主色调 + 威友橙辅色
|
||||
* 暗色模式:深色背景 + 威友橙辅色
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useMemo } from 'react';
|
||||
@@ -89,7 +89,7 @@ export const WelcomeScreen: React.FC = () => {
|
||||
{/* 品牌区域 */}
|
||||
<View style={styles.brandSection}>
|
||||
<Text style={styles.welcomeEn}>Welcome To</Text>
|
||||
<Text style={styles.brandName}>萝卜社区</Text>
|
||||
<Text style={styles.brandName}>威友</Text>
|
||||
<View style={styles.brandUnderline} />
|
||||
|
||||
<Text style={styles.subtitle}>
|
||||
@@ -139,7 +139,7 @@ function createWelcomeStyles(colors: AppColors) {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
|
||||
// 装饰性背景元素 - 使用胡萝卜橙的淡色
|
||||
// 装饰性背景元素 - 使用威友橙的淡色
|
||||
decorCircle1: {
|
||||
position: 'absolute',
|
||||
top: -100,
|
||||
@@ -147,7 +147,7 @@ function createWelcomeStyles(colors: AppColors) {
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: 200,
|
||||
backgroundColor: colors.primary.main + '08', // 非常淡的胡萝卜橙
|
||||
backgroundColor: colors.primary.main + '08', // 非常淡的威友橙
|
||||
},
|
||||
decorCircle2: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 认证屏幕导出
|
||||
* 胡萝卜BBS - 登录注册模块
|
||||
* 威友 - 登录注册模块
|
||||
*/
|
||||
|
||||
export { LoginScreen } from './LoginScreen';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 发帖页 CreatePostScreen(响应式适配)
|
||||
* 胡萝卜BBS - 发布新帖子
|
||||
* 威友 - 发布新帖子
|
||||
* 参考微博发帖界面设计
|
||||
* 表单在宽屏下居中显示
|
||||
* 图片选择器在宽屏下显示更大的预览
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 首页 HomeScreen
|
||||
* 胡萝卜BBS - 首页展示
|
||||
* 威友 - 首页展示
|
||||
* 支持列表和多列网格模式(响应式布局)
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 帖子详情页 PostDetailScreen - QQ频道风格(响应式版本)
|
||||
* 胡萝卜BBS - 显示完整帖子内容和评论
|
||||
* 威友 - 显示完整帖子内容和评论
|
||||
* 桌面端使用双栏布局,移动端使用单栏布局
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 搜索页 SearchScreen(响应式版本)
|
||||
* 胡萝卜BBS - 搜索帖子、用户
|
||||
* 威友 - 搜索帖子、用户
|
||||
* 支持响应式布局,宽屏下显示更大的搜索结果区域
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 聊天页 ChatScreen
|
||||
* 胡萝卜BBS - 私信/群聊聊天界面
|
||||
* 威友 - 私信/群聊聊天界面
|
||||
* 高级现代化设计
|
||||
* 支持群聊功能:显示发送者头像和昵称、@提及功能
|
||||
* 支持响应式布局(桌面端宽屏优化)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 消息列表页 MessageListScreen
|
||||
* 胡萝卜BBS - 消息列表
|
||||
* 威友 - 消息列表
|
||||
*
|
||||
* 【重构后】使用MessageManager架构
|
||||
* - 纯渲染组件,不直接管理数据
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 通知页 NotificationsScreen(扁平化风格)
|
||||
* 胡萝卜BBS - 系统消息列表
|
||||
* 威友 - 系统消息列表
|
||||
* 【游标分页】使用 messageService.getSystemMessagesCursor
|
||||
* 支持响应式布局
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* PrivateChatInfoScreen 私聊聊天管理页面
|
||||
* 胡萝卜BBS - 私聊设置界面
|
||||
* 威友 - 私聊设置界面
|
||||
* 参考QQ和微信的实现,提供聊天管理功能
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 关于我们页面 AboutScreen
|
||||
* 胡萝卜BBS - 应用信息、版本检查与更新
|
||||
* 威友 - 应用信息、版本检查与更新
|
||||
* 扁平化设计风格,参考登录、注册、设置页面
|
||||
*/
|
||||
|
||||
@@ -39,7 +39,7 @@ const THEME_COLORS = {
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
const APP_NAME = '萝卜社区';
|
||||
const APP_NAME = '威友';
|
||||
const APP_SLOGAN = '连接校园,分享生活';
|
||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
||||
const BUILD_NUMBER = Constants.expoConfig?.android?.versionCode || Constants.expoConfig?.ios?.buildNumber || '1';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 聊天设置页 ChatSettingsScreen
|
||||
* 胡萝卜BBS - 聊天个性化设置
|
||||
* 威友 - 聊天个性化设置
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useRef } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 数据与存储设置页 DataStorageScreen(响应式适配)
|
||||
* 胡萝卜BBS - 缓存管理、存储统计
|
||||
* 威友 - 缓存管理、存储统计
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 编辑资料页 EditProfileScreen(响应式适配)
|
||||
* 胡萝卜BBS - 编辑用户资料
|
||||
* 威友 - 编辑用户资料
|
||||
* 与用户资料页样式完全一致
|
||||
* 表单在宽屏下居中显示
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 通知设置页 NotificationSettingsScreen(响应式适配)
|
||||
* 胡萝卜BBS - 通知相关设置
|
||||
* 威友 - 通知相关设置
|
||||
* 在宽屏下居中显示
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 隐私政策页面 PrivacyPolicyScreen
|
||||
* 胡萝卜BBS - 隐私政策
|
||||
* 威友 - 隐私政策
|
||||
* 扁平化设计风格
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 400;
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
// 威友橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const LAST_UPDATED = '2026年4月4日';
|
||||
const PRIVACY_SECTIONS = [
|
||||
{
|
||||
title: '引言',
|
||||
content: `萝卜社区(以下简称"我们"或"本应用")非常重视用户的隐私和个人信息保护。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。
|
||||
content: `威友(以下简称"我们"或"本应用")非常重视用户的隐私和个人信息保护。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。
|
||||
|
||||
本隐私政策旨在帮助您了解我们如何收集、使用、存储、共享和保护您的个人信息,以及您享有的相关权利。请您在使用我们的服务前,仔细阅读并理解本隐私政策的全部内容。
|
||||
|
||||
@@ -293,7 +293,7 @@ export const PrivacyPolicyScreen: React.FC = () => {
|
||||
<View style={styles.highlightBox}>
|
||||
<Text style={styles.highlightTitle}>我们承诺保护您的隐私</Text>
|
||||
<Text style={styles.highlightText}>
|
||||
萝卜社区致力于保护用户的个人信息安全。我们仅收集提供服务所必需的信息,并采取严格的安全措施保护您的数据。
|
||||
威友致力于保护用户的个人信息安全。我们仅收集提供服务所必需的信息,并采取严格的安全措施保护您的数据。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 个人主页 ProfileScreen
|
||||
* 胡萝卜BBS - 当前用户个人主页
|
||||
* 威友 - 当前用户个人主页
|
||||
* 使用统一的 UserProfileScreen 组件,mode='self'
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 设置页 SettingsScreen(响应式适配)
|
||||
* 胡萝卜BBS - 应用设置
|
||||
* 威友 - 应用设置
|
||||
* 在宽屏下居中显示,最大宽度限制
|
||||
*/
|
||||
|
||||
@@ -363,7 +363,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text variant="caption" color={colors.text.hint}>
|
||||
萝卜社区 v{APP_VERSION}
|
||||
威友 v{APP_VERSION}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.copyright}>
|
||||
© 2026 青春之旅电子信息科技(威海)有限公司
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 用户协议页面 TermsOfServiceScreen
|
||||
* 胡萝卜BBS - 用户服务协议
|
||||
* 威友 - 用户服务协议
|
||||
* 扁平化设计风格
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 400;
|
||||
|
||||
// 胡萝卜橙主题色
|
||||
// 威友橙主题色
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const LAST_UPDATED = '2026年4月4日';
|
||||
const TERMS_SECTIONS = [
|
||||
{
|
||||
title: '一、协议的范围',
|
||||
content: `1.1 本协议是您与萝卜社区(以下简称"我们"或"本应用")之间关于您使用本应用服务所订立的协议。
|
||||
content: `1.1 本协议是您与威友(以下简称"我们"或"本应用")之间关于您使用本应用服务所订立的协议。
|
||||
|
||||
1.2 本协议内容包括协议正文及所有我们已经发布或将来可能发布的各类规则、公告或通知。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 统一的用户主页组件
|
||||
* 胡萝卜BBS - 支持两种模式:self(当前用户)和 other(其他用户)
|
||||
* 威友 - 支持两种模式:self(当前用户)和 other(其他用户)
|
||||
* 采用现代卡片式设计,优化视觉层次和交互体验
|
||||
* 支持桌面端双栏布局
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* 用户主页 UserScreen
|
||||
* 胡萝卜BBS - 查看其他用户资料
|
||||
* 威友 - 查看其他用户资料
|
||||
* 使用统一的 UserProfileScreen 组件,mode='other'
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 后台同步管理器
|
||||
*
|
||||
* 整合前台服务、后台同步、消息状态管理
|
||||
@@ -86,7 +86,7 @@ class BackgroundSyncManager {
|
||||
if (mode === BackgroundSyncMode.REALTIME) {
|
||||
// 切换到实时模式:立即启动前台服务(无论前台还是后台)
|
||||
const started = await ForegroundServiceModule.start({
|
||||
title: '萝卜社区',
|
||||
title: '威友',
|
||||
body: '正在后台同步消息',
|
||||
});
|
||||
if (started) {
|
||||
@@ -134,7 +134,7 @@ class BackgroundSyncManager {
|
||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
||||
// 确保前台服务正在运行
|
||||
await ForegroundServiceModule.start({
|
||||
title: '萝卜社区',
|
||||
title: '威友',
|
||||
body: '正在后台同步消息',
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ class BackgroundSyncManager {
|
||||
|
||||
// 实时模式下:前台服务保持运行,只更新通知文字
|
||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
||||
await ForegroundServiceModule.update('萝卜社区', '正在后台同步消息');
|
||||
await ForegroundServiceModule.update('威友', '正在后台同步消息');
|
||||
}
|
||||
|
||||
// 立即同步一次(获取离线消息)
|
||||
@@ -235,12 +235,12 @@ class BackgroundSyncManager {
|
||||
|
||||
if (totalUnread > 0) {
|
||||
await ForegroundServiceModule.update(
|
||||
'萝卜社区',
|
||||
'威友',
|
||||
`您有 ${totalUnread} 条未读消息`
|
||||
);
|
||||
} else {
|
||||
await ForegroundServiceModule.update(
|
||||
'萝卜社区',
|
||||
'威友',
|
||||
'正在后台同步消息'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 前台服务模块
|
||||
*
|
||||
* 提供 React Native 层对 Android 前台服务的控制接口
|
||||
@@ -40,7 +40,7 @@ export const ForegroundServiceModule = {
|
||||
|
||||
try {
|
||||
const result = await ForegroundService.start(
|
||||
options.title || '萝卜社区',
|
||||
options.title || '威友',
|
||||
options.body || '正在后台同步消息'
|
||||
);
|
||||
console.log('[ForegroundService] 服务已启动');
|
||||
|
||||
@@ -11,13 +11,13 @@ import { Platform } from 'react-native';
|
||||
|
||||
import { eventBus } from '@/core/events/EventBus';
|
||||
|
||||
// 生产地址 https://bbs.littlelan.cn
|
||||
// 生产地址 https://withyou.littlelan.cn
|
||||
const getBaseUrl = () => {
|
||||
const configuredBaseUrl = Constants.expoConfig?.extra?.apiBaseUrl;
|
||||
if (typeof configuredBaseUrl === 'string' && configuredBaseUrl.trim().length > 0) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
return 'https://bbs.littlelan.cn/api/v1';
|
||||
return 'https://withyou.littlelan.cn/api/v1';
|
||||
};
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
|
||||
@@ -144,7 +144,7 @@ async function downloadAndInstallAPK(downloadUrl: string, version: string): Prom
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadPath = `${FileSystem.documentDirectory}carrot-bbs-${version}.apk`;
|
||||
const downloadPath = `${FileSystem.documentDirectory}with-you-${version}.apk`;
|
||||
|
||||
// 显示下载进度
|
||||
showConfirm({
|
||||
|
||||
@@ -96,22 +96,24 @@ class WebRTCManager {
|
||||
console.log('[WebRTC] ontrack: no track in event');
|
||||
return;
|
||||
}
|
||||
// Official react-native-webrtc approach: use event.streams[0] if available
|
||||
if (event.streams && event.streams[0]) {
|
||||
this.remoteStream = event.streams[0];
|
||||
} else {
|
||||
// Fallback: manually construct stream
|
||||
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);
|
||||
}
|
||||
|
||||
// Always manually construct/append to remoteStream to ensure all tracks are collected
|
||||
// react-native-webrtc may fire ontrack per-track and event.streams[0] might not contain all tracks
|
||||
if (!this.remoteStream) {
|
||||
this.remoteStream = new MediaStream();
|
||||
}
|
||||
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
|
||||
@@ -655,7 +657,8 @@ class WebRTCManager {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
/**
|
||||
* 注册流程状态管理
|
||||
* 使用 Zustand + AsyncStorage 实现步骤数据的持久化
|
||||
* 使用 Zustand 纯内存状态,不持久化到 AsyncStorage
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
@@ -42,9 +39,6 @@ interface RegisterState {
|
||||
resetRegisterData: () => void;
|
||||
goToNextStep: () => void;
|
||||
goToPrevStep: () => void;
|
||||
// 持久化
|
||||
hydrate: () => Promise<void>;
|
||||
persistData: () => Promise<void>;
|
||||
}
|
||||
|
||||
const initialFormData: RegisterFormData = {
|
||||
@@ -63,31 +57,18 @@ const initialCompletedSteps: Record<RegisterStep, boolean> = {
|
||||
2: false,
|
||||
};
|
||||
|
||||
export const useRegisterStore = create<RegisterState>((set, get) => ({
|
||||
export const useRegisterStore = create<RegisterState>((set) => ({
|
||||
currentStep: 0,
|
||||
formData: initialFormData,
|
||||
countdown: 0,
|
||||
completedSteps: initialCompletedSteps,
|
||||
|
||||
setCurrentStep: (step) => {
|
||||
set({ currentStep: step });
|
||||
get().persistData();
|
||||
},
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
|
||||
updateFormData: (data) =>
|
||||
set((state) => {
|
||||
const newFormData = { ...state.formData, ...data };
|
||||
// 异步保存到 AsyncStorage
|
||||
AsyncStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
currentStep: state.currentStep,
|
||||
formData: newFormData,
|
||||
completedSteps: state.completedSteps,
|
||||
})
|
||||
).catch(() => {});
|
||||
return { formData: newFormData };
|
||||
}),
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, ...data },
|
||||
})),
|
||||
|
||||
setCountdown: (seconds) => set({ countdown: seconds }),
|
||||
|
||||
@@ -97,81 +78,33 @@ export const useRegisterStore = create<RegisterState>((set, get) => ({
|
||||
})),
|
||||
|
||||
markStepCompleted: (step) =>
|
||||
set((state) => {
|
||||
const newCompletedSteps = { ...state.completedSteps, [step]: true };
|
||||
// 异步保存到 AsyncStorage
|
||||
AsyncStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
currentStep: state.currentStep,
|
||||
formData: state.formData,
|
||||
completedSteps: newCompletedSteps,
|
||||
})
|
||||
).catch(() => {});
|
||||
return { completedSteps: newCompletedSteps };
|
||||
}),
|
||||
set((state) => ({
|
||||
completedSteps: { ...state.completedSteps, [step]: true },
|
||||
})),
|
||||
|
||||
resetRegisterData: () => {
|
||||
resetRegisterData: () =>
|
||||
set({
|
||||
currentStep: 0,
|
||||
formData: initialFormData,
|
||||
countdown: 0,
|
||||
completedSteps: initialCompletedSteps,
|
||||
});
|
||||
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
|
||||
},
|
||||
}),
|
||||
|
||||
goToNextStep: () => {
|
||||
const { currentStep } = get();
|
||||
if (currentStep < 2) {
|
||||
const newStep = (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 },
|
||||
});
|
||||
goToNextStep: () =>
|
||||
set((state) => {
|
||||
if (state.currentStep < 2) {
|
||||
return { currentStep: (state.currentStep + 1) as RegisterStep };
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
},
|
||||
return state;
|
||||
}),
|
||||
|
||||
// 保存数据到 AsyncStorage
|
||||
persistData: async () => {
|
||||
const { currentStep, formData, completedSteps } = get();
|
||||
try {
|
||||
await AsyncStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
currentStep,
|
||||
formData,
|
||||
completedSteps,
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
},
|
||||
goToPrevStep: () =>
|
||||
set((state) => {
|
||||
if (state.currentStep > 0) {
|
||||
return { currentStep: (state.currentStep - 1) as RegisterStep };
|
||||
}
|
||||
return state;
|
||||
}),
|
||||
}));
|
||||
|
||||
// 导出便捷 hooks
|
||||
|
||||
@@ -146,13 +146,20 @@ function setupWebRTCEvents(callId: string, myUserId: string): void {
|
||||
* Handle remote stream with video track detection
|
||||
*/
|
||||
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
|
||||
const videoTracks = stream.getVideoTracks();
|
||||
const videoTracks = newStream.getVideoTracks();
|
||||
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) => ({
|
||||
currentCall: s.currentCall
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 胡萝卜BBS 主题:明暗色板、Paper 主题、设计 token
|
||||
/**
|
||||
* 威友 主题:明暗色板、Paper 主题、设计 token
|
||||
* 新代码请优先使用 useAppColors();静态 colors 为浅色快照,与深色模式不同步。
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================
|
||||
// Carrot BBS - Core Type Definitions
|
||||
// ============================================
|
||||
// WithYou - Core Type Definitions
|
||||
// 使用后端DTO类型,保持snake_case命名规范
|
||||
// ============================================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user