diff --git a/assets/icon.png b/assets/icon.png
index dc1f853..39838b7 100644
Binary files a/assets/icon.png and b/assets/icon.png differ
diff --git a/components/ImageViewer/index.jsx b/components/ImageViewer/index.jsx
new file mode 100644
index 0000000..0abc1d3
--- /dev/null
+++ b/components/ImageViewer/index.jsx
@@ -0,0 +1,191 @@
+import {
+ View,
+ Text,
+ Modal,
+ TouchableOpacity,
+ ActivityIndicator,
+} from "react-native";
+import React, { useState, useCallback, useEffect } from "react";
+import { ImageViewer as OriginImageViewer } from "react-native-image-zoom-viewer";
+import { Icon } from "@rneui/themed";
+import { useTailwind } from "tailwind-rn";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import saveImage from "../../utils/saveImage";
+import Toast from "react-native-toast-message";
+import { get } from "../../utils/storeInfo";
+import { useNavigation } from "@react-navigation/native";
+import MyModal from "../MyModal";
+
+//isVisible boolean
+//setIsVisible function
+//imageUrls [{ url: string }]
+//index int
+
+export default function ImageViewer({
+ isVisible,
+ setIsVisible,
+ imageUrls,
+ index,
+}) {
+ const tailwind = useTailwind();
+ const insets = useSafeAreaInsets();
+
+ const navigation = useNavigation();
+
+ const [isSaved, setIsSaved] = useState(false);
+ const [isVip, setIsVip] = useState(false);
+
+ useEffect(() => {
+ const checkRole = async () => {
+ const account = await get("account");
+ const role = account.role;
+ const isVip = account.is_a_member;
+ if (role !== 0 || isVip === 1) {
+ setIsVip(true);
+ }
+ };
+ checkRole();
+ }, []);
+
+ const MenusComponent = useCallback(
+ ({ cancel, saveToLocal }) => (
+
+
+
+
+ {
+ setIsVisible(false);
+ navigation.navigate("WebWithoutHeader", {
+ uri: process.env.EXPO_PUBLIC_WEB_URL + "/vip",
+ });
+ }
+ }
+ style={tailwind("flex flex-col items-center px-4")}
+ >
+
+
+ {isSaved ? "已保存" : "保存(会员特权)"}
+
+
+
+
+
+ 取消
+
+
+
+
+
+
+ ),
+ [isSaved, isVip]
+ );
+
+ const [isVipModalVisible, setIsVipModalVisible] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const hanldSaveImage = async (index) => {
+ if (!isVip) {
+ setIsVipModalVisible(true);
+ return;
+ }
+ if (isSaving) return;
+ setIsSaving(true);
+ const isSuccess = await saveImage(imageUrls[index].url);
+ setIsSaved(isSuccess);
+ setIsSaving(false);
+ };
+
+ return (
+
+ setIsVisible(false)}
+ onSwipeDown={() => setIsVisible(false)}
+ enableSwipeDown
+ backgroundColor="#07050A"
+ renderFooter={(index) => (
+ hanldSaveImage(index)}
+ style={{
+ marginLeft: 20,
+ marginBottom: insets.bottom,
+ ...tailwind(
+ "flex justify-center items-center w-12 h-12 bg-[#FFFFFF1A] rounded-full"
+ ),
+ }}
+ >
+ {isSaving && }
+ {!isSaving && (
+
+ )}
+
+ )}
+ onSave={async (url) => {
+ const isSuccess = await saveImage(url);
+ setIsSaved(isSuccess);
+ }}
+ saveToLocalByLongPress={true}
+ menus={MenusComponent}
+ loadingRender={() => }
+ />
+
+ {
+ setIsVipModalVisible(false);
+ }}
+ confirm={() => {
+ setIsVipModalVisible(false);
+ setIsVisible(false);
+ navigation.navigate("WebWithoutHeader", {
+ uri: process.env.EXPO_PUBLIC_WEB_URL + "/vip",
+ });
+ }}
+ />
+
+ );
+}
diff --git a/components/Post/index.jsx b/components/Post/index.jsx
index 58752d2..300172a 100644
--- a/components/Post/index.jsx
+++ b/components/Post/index.jsx
@@ -4,14 +4,13 @@ import {
TouchableOpacity,
Modal,
TouchableWithoutFeedback,
- ActivityIndicator,
Platform,
Image as NativeImage,
} from "react-native";
import React, { useEffect, useState } from "react";
import { useTailwind } from "tailwind-rn";
import { Image } from "expo-image";
-import ImageViewer from "react-native-image-zoom-viewer";
+import ImageViewer from "../ImageViewer";
import VideoModal from "../VideoModal";
import { useNavigation } from "@react-navigation/native";
import formatTimestamp from "../../utils/formatTimestamp";
@@ -327,18 +326,12 @@ function ImageDisplay({ blur, media }) {
/>
)}
-
- setIsModalVisible(false)}>
- setIsModalVisible(false)}
- index={imageIndex}
- loadingRender={() => }
- />
-
-
+
);
}
diff --git a/components/VideoModal/index.jsx b/components/VideoModal/index.jsx
index 25aac70..86e62e6 100644
--- a/components/VideoModal/index.jsx
+++ b/components/VideoModal/index.jsx
@@ -10,11 +10,14 @@ import React, { useState, useRef } from "react";
import { useTailwind } from "tailwind-rn";
import { Icon } from "@rneui/themed";
import { Video, ResizeMode } from "expo-av";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
const screenWidth = Dimensions.get("window").width;
export default function VideoModal({ visible, setVisible, url }) {
const tailwind = useTailwind();
+ const insets = useSafeAreaInsets();
+
const [isReady, setIsReady] = useState(false);
const videoRef = useRef(null);
const [videoSize, setVideoSize] = useState({ width: 720, height: 1280 });
@@ -32,13 +35,16 @@ export default function VideoModal({ visible, setVisible, url }) {
...tailwind("flex-1 justify-center items-center"),
}}
>
-
-
+
+
{!isReady && }
diff --git a/screeens/Login/SetPassword/index.jsx b/screeens/Login/SetPassword/index.jsx
index 66ed809..5bb3651 100644
--- a/screeens/Login/SetPassword/index.jsx
+++ b/screeens/Login/SetPassword/index.jsx
@@ -98,7 +98,6 @@ export default function SetPassword({ navigation, route }) {
}
);
const newData = await response.json();
- console.log("base", base);
if (newData.ret === -1) {
Toast.show({
type: "error",
diff --git a/screeens/Posts/FeedPosts/index.jsx b/screeens/Posts/FeedPosts/index.jsx
index eadfa9b..9ed2c7c 100644
--- a/screeens/Posts/FeedPosts/index.jsx
+++ b/screeens/Posts/FeedPosts/index.jsx
@@ -5,6 +5,7 @@ import {
Image as NativeImage,
Text,
TouchableOpacity,
+ FlatList,
} from "react-native";
import React, { useState, useEffect } from "react";
import Empty from "../../../components/Empty";
@@ -12,7 +13,6 @@ import { useTailwind } from "tailwind-rn";
import baseRequest from "../../../utils/baseRequest";
import Toast from "react-native-toast-message";
import { generateSignature } from "../../../utils/crypto";
-import { FlashList } from "@shopify/flash-list";
import Post from "../../../components/Post";
import { useNavigation } from "@react-navigation/native";
import { get } from "../../../utils/storeInfo";
@@ -136,45 +136,30 @@ export default function FeedPosts({ blur }) {
}
};
- //获取当前时间
- const [currentTime, setCurrentTime] = useState();
- const getCurrentTime = async () => {
- setCurrentTime(Math.floor(new Date().getTime() / 1000));
- };
-
- //获取截止当前时间的动态数据
+ //获取新数据
const [data, setData] = useState([]);
- const [offset, setOffset] = useState(0);
- const [more, setMore] = useState(1);
const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
- const getData = async (type) => {
- if (!currentTime) return;
- if (data.length === 0 && type === "bottom") return;
- if (!more && type === "bottom") {
- setIsActivityIndicatorShow(false);
- return;
- }
- setIsActivityIndicatorShow(true);
+ const [isFetching, setIsFetching] = useState(false);
+ const getData = async (type = 0) => {
+ //type 0-上滑 1-下拉 2-初始化
+ if (isFetching) return;
+ const apiUrl = process.env.EXPO_PUBLIC_API_URL;
+ setIsFetching(true);
try {
- const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const base = await baseRequest();
const signature = await generateSignature({
- ct_upper_bound: currentTime,
- offset: offset,
- limit: 4,
+ op_type: type,
...base,
});
const _response = await fetch(
- `${apiUrl}/api/moment/list?signature=${signature}`,
+ `${apiUrl}/api/moment/recomm_list?signature=${signature}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
- ct_upper_bound: currentTime,
- offset: offset,
- limit: 4,
+ op_type: type,
...base,
}),
}
@@ -188,48 +173,44 @@ export default function FeedPosts({ blur }) {
});
return;
}
- if (type === "top") {
+ if (type === 1 || type === 2) {
const topPosts = await getTopPostsData();
- setData((prev) => [...topPosts, ..._data.data.list]);
+ setData((prev) => [...topPosts, ..._data.data.recomm_list]);
+ setIsActivityIndicatorShow(true);
} else {
- setData((prev) => [...prev, ..._data.data.list]);
+ setData((prev) => [...prev, ..._data.data.recomm_list]);
+ if (_data.data.recomm_list.length === 0)
+ setIsActivityIndicatorShow(false);
}
- setOffset(_data.data.offset);
- setMore(_data.data.more);
} catch (error) {
console.error(error);
+ } finally {
+ setIsFetching(false);
}
};
- //获取当前时间
+ //初始化加载
useEffect(() => {
getVipPrice();
- getCurrentTime();
+ getData(2);
}, []);
- //当时间改变,获取新数据
- useEffect(() => {
- getData("top");
- }, [currentTime]);
-
const tailwind = useTailwind();
const renderItem = ({ item }) => ;
const [refreshing, setRefreshing] = useState(false);
//下拉刷新
const handleRefresh = async () => {
setRefreshing(true);
- setOffset(0);
- setMore(1);
- await getCurrentTime();
+ await getData(1);
setRefreshing(false);
};
return (
- handleRefresh()}
/>
}
- onEndReached={() => getData("bottom")}
+ onEndReached={() => getData(0)}
ListEmptyComponent={}
ListFooterComponent={
diff --git a/screeens/Posts/FollowPosts/index.jsx b/screeens/Posts/FollowPosts/index.jsx
index 15f6b26..0329772 100644
--- a/screeens/Posts/FollowPosts/index.jsx
+++ b/screeens/Posts/FollowPosts/index.jsx
@@ -4,6 +4,7 @@ import {
Image as NativeImage,
Text,
TouchableOpacity,
+ FlatList,
} from "react-native";
import React, { useState, useEffect } from "react";
import Empty from "../../../components/Empty";
@@ -12,7 +13,6 @@ import baseRequest from "../../../utils/baseRequest";
import Toast from "react-native-toast-message";
import { get } from "../../../utils/storeInfo";
import { generateSignature } from "../../../utils/crypto";
-import { FlashList } from "@shopify/flash-list";
import Post from "../../../components/Post";
import { useNavigation } from "@react-navigation/native";
@@ -190,10 +190,9 @@ export default function FollowPosts({ blur }) {
return (
- {
- if (!isEnd) return;
+ //获取新数据
+ const [data, setData] = useState([]);
+ const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
+ const [isFetching, setIsFetching] = useState(false);
+ const getData = async (type = 0) => {
+ //type 0-上滑 1-下拉 2-初始化
+ if (isFetching) return;
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
+ setIsFetching(true);
try {
const base = await baseRequest();
const signature = await generateSignature({
+ op_type: type,
...base,
});
- const response = await fetch(
+ const _response = await fetch(
`${apiUrl}/api/streamer/recomm_list?signature=${signature}`,
{
method: "POST",
@@ -35,110 +40,39 @@ export default function FeedStream() {
"Content-Type": "application/json",
},
body: JSON.stringify({
+ op_type: type,
...base,
}),
}
);
- const recommData = await response.json();
- if (recommData.ret === -1) {
+ const _data = await _response.json();
+ if (_data.ret === -1) {
Toast.show({
type: "error",
- text1: recommData.msg,
+ text1: _data.msg,
topOffset: 60,
});
return;
}
- setIsEnd(false);
- setRecommMids(recommData.data.recomm_list);
- } catch (error) {
- console.error(error);
- }
- };
-
- //查询的起始索引
- const [startIndex, setStartIndex] = useState(0);
- // 定义每次取多少个用户ID
- const batchSize = 4;
- //获取新数据
- const [data, setData] = useState([]);
- const [isEnd, setIsEnd] = useState(true);
- const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
- const [isFetching, setIsFetching] = useState(false);
- const getData = async (type) => {
- if (isFetching) return;
- if (isEnd) return;
- if (recommMids.length === 0) return;
- const apiUrl = process.env.EXPO_PUBLIC_API_URL;
- const batchStreamerMids = recommMids.slice(
- startIndex,
- startIndex + batchSize
- );
- if (batchStreamerMids.length === 0) {
- if (type !== "top") {
- setIsActivityIndicatorShow(false);
- return;
- }
- setIsActivityIndicatorShow(true);
- setIsEnd(true);
- setStartIndex(0);
- return;
- }
- setIsFetching((prev) => true);
- try {
- const base = await baseRequest();
- const signature = await generateSignature({
- mids: batchStreamerMids,
- ...base,
- });
- const detailResponse = await fetch(
- `${apiUrl}/api/streamer/list_ext_by_mids?signature=${signature}`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- mids: batchStreamerMids,
- ...base,
- }),
- }
- );
- const detailData = await detailResponse.json();
- if (detailData.ret === -1) {
- Toast.show({
- type: "error",
- text1: detailData.msg,
- topOffset: 60,
- });
- return;
- }
- const sortedData = detailData.data.list.sort((a, b) => b.fans - a.fans);
- if (type === "top") {
- setData((prev) => {
- if (startIndex === 0) {
- return sortedData;
- } else return [...sortedData, ...prev];
- });
+ if (type === 1) {
+ setData((prev) => _data.data.recomm_list);
+ setIsActivityIndicatorShow(true);
} else {
- setData((prev) => [...prev, ...sortedData]);
+ setData((prev) => [...prev, ..._data.data.recomm_list]);
+ if (_data.data.recomm_list.length === 0)
+ setIsActivityIndicatorShow(false);
}
- // 更新下一次查询的起始索引
- setStartIndex(startIndex + batchSize);
- setIsFetching((prev) => false);
} catch (error) {
console.error(error);
+ } finally {
+ setIsFetching(false);
}
};
- //获取推荐mid
+ //初始化加载
useEffect(() => {
- getRecommMids();
- }, [isEnd]);
-
- //当推荐的mid改变,获取新数据
- useEffect(() => {
- getData("top");
- }, [recommMids]);
+ getData(2);
+ }, []);
const tailwind = useTailwind();
const renderItem = ({ item }) => ;
@@ -166,7 +100,7 @@ export default function FeedStream() {
startRotation();
flatListRef.current.scrollToOffset({ animated: false, offset: 0 });
setRefreshing(true);
- await getData("top");
+ await getData(1);
setRefreshing(false);
};
@@ -175,6 +109,7 @@ export default function FeedStream() {
item.mid}
renderItem={renderItem}
estimatedItemSize={287}
initialNumToRender={4}
@@ -186,7 +121,7 @@ export default function FeedStream() {
onRefresh={() => handleRefresh()}
/>
}
- onEndReached={() => getData("bottom")}
+ onEndReached={() => getData(0)}
ListEmptyComponent={}
ListFooterComponent={
@@ -195,7 +130,6 @@ export default function FeedStream() {
)}
}
- keyExtractor={(item) => item.mid}
/>
{/* 展示图片的modal */}
-
- setImagesVisible(false)}>
- setImagesVisible(false)}
- index={imageIndex}
- loadingRender={() => }
- />
-
-
+
>
);
};
diff --git a/tailwind.css b/tailwind.css
index 6c4e2fa..b4a0569 100644
--- a/tailwind.css
+++ b/tailwind.css
@@ -14,6 +14,10 @@
top: -6rem
}
+.bottom-0 {
+ bottom: 0px
+}
+
.bottom-12 {
bottom: 3rem
}
@@ -277,6 +281,10 @@
height: 2.25rem
}
+.h-\[1px\] {
+ height: 1px
+}
+
.h-\[3px\] {
height: 3px
}
@@ -293,8 +301,8 @@
height: 100%
}
-.h-\[1px\] {
- height: 1px
+.h-48 {
+ height: 12rem
}
.w-1\/2 {
@@ -475,6 +483,16 @@
border-bottom-right-radius: 0.25rem
}
+.rounded-t-xl {
+ border-top-left-radius: 0.75rem;
+ border-top-right-radius: 0.75rem
+}
+
+.rounded-t-2xl {
+ border-top-left-radius: 1rem;
+ border-top-right-radius: 1rem
+}
+
.border {
border-width: 1px
}
@@ -618,6 +636,10 @@
background-color: rgb(250 204 21 / var(--tw-bg-opacity))
}
+.bg-\[\#00000080\] {
+ background-color: #00000080
+}
+
.p-0 {
padding: 0px
}
diff --git a/tailwind.json b/tailwind.json
index 39d80ab..565d7ff 100644
--- a/tailwind.json
+++ b/tailwind.json
@@ -19,6 +19,11 @@
"top": -96
}
},
+ "bottom-0": {
+ "style": {
+ "bottom": 0
+ }
+ },
"bottom-12": {
"style": {
"bottom": 48
@@ -352,6 +357,11 @@
"height": 36
}
},
+ "h-[1px]": {
+ "style": {
+ "height": 1
+ }
+ },
"h-[3px]": {
"style": {
"height": 3
@@ -372,9 +382,9 @@
"height": "100%"
}
},
- "h-[1px]": {
+ "h-48": {
"style": {
- "height": 1
+ "height": 192
}
},
"w-1/2": {
@@ -621,6 +631,18 @@
"borderBottomRightRadius": 4
}
},
+ "rounded-t-xl": {
+ "style": {
+ "borderTopLeftRadius": 12,
+ "borderTopRightRadius": 12
+ }
+ },
+ "rounded-t-2xl": {
+ "style": {
+ "borderTopLeftRadius": 16,
+ "borderTopRightRadius": 16
+ }
+ },
"border": {
"style": {
"borderTopWidth": 1,
@@ -816,6 +838,11 @@
"backgroundColor": "rgb(250 204 21 / var(--tw-bg-opacity))"
}
},
+ "bg-[#00000080]": {
+ "style": {
+ "backgroundColor": "#00000080"
+ }
+ },
"p-0": {
"style": {
"paddingTop": 0,
diff --git a/utils/saveImage.js b/utils/saveImage.js
new file mode 100644
index 0000000..95850fc
--- /dev/null
+++ b/utils/saveImage.js
@@ -0,0 +1,31 @@
+import * as MediaLibrary from "expo-media-library";
+import * as FileSystem from "expo-file-system";
+import Toast from "react-native-toast-message";
+
+export default async function saveImage(uri) {
+ const permission = await MediaLibrary.requestPermissionsAsync();
+ if (permission.granted) {
+ const timestamp = new Date().getTime();
+ const fileUri = FileSystem.cacheDirectory + `${timestamp}.png`;
+ try {
+ const res = await FileSystem.downloadAsync(uri, fileUri);
+ await MediaLibrary.saveToLibraryAsync(res.uri);
+ Toast.show({
+ type: "success",
+ text1: "已保存到相册",
+ topOffset: 60,
+ });
+ return true;
+ } catch (err) {
+ console.error("FS Err: ", err);
+ return false;
+ }
+ } else {
+ Toast.show({
+ type: "error",
+ text1: "保存失败,请检查APP媒体权限",
+ topOffset: 60,
+ });
+ return false;
+ }
+}
diff --git a/utils/upload.js b/utils/upload.js
index 8a24ce9..0b3b592 100644
--- a/utils/upload.js
+++ b/utils/upload.js
@@ -274,16 +274,18 @@ export async function uploadVideo(asset) {
//上传多个图片或者视频
export async function multiUpload(assets) {
let ids = { image_ids: [], video_ids: [] };
- await Promise.all(
- assets.map(async (asset) => {
- if (asset.duration === 0) {
- const id = await uploadImage(asset);
- ids.image_ids.push(id);
- } else {
- const id = await uploadVideo(asset);
- ids.video_ids.push(id);
- }
- })
- );
- return ids;
+ const tasks = assets.map(async (asset, index) => {
+ if (asset.duration === 0) {
+ const id = await uploadImage(asset);
+ ids.image_ids[index] = id;
+ } else {
+ const id = await uploadVideo(asset);
+ ids.video_ids[index] = id;
+ }
+ });
+ await Promise.all(tasks);
+ return {
+ image_ids: ids.image_ids.filter(Boolean),
+ video_ids: ids.video_ids.filter(Boolean),
+ };
}