添加消息通知功能

This commit is contained in:
al 2024-11-26 17:54:48 +08:00
parent 6f60c440fd
commit bc7720f817
8 changed files with 277 additions and 130 deletions

13
App.jsx
View File

@ -67,7 +67,7 @@ import * as Clipboard from "expo-clipboard";
import PrivatyModal from "./components/PrivatyModal";
import * as Sentry from "@sentry/react-native";
import { ImageViewerProvider } from "./context/ImageViewProvider";
import WebSocketComponent from "./components/Websocket";
const RootStack = createNativeStackNavigator();
export const AuthContext = createContext("");
@ -117,6 +117,7 @@ const App = () => {
isSignin: false,
userToken: null,
});
const authContext = useMemo(
() => ({
signIn: async (data, mobilePhone, regionCode) => {
@ -299,12 +300,20 @@ const App = () => {
if (!appIsReady) {
return null;
}
const handleGetWebsocketData = (data) => {
console.log("websocketData", data);
Toast.show({
type: "info",
text1: "🔔 收到一条系统通知",
topOffset: 60,
});
};
return (
<TailwindProvider utilities={utilities}>
<AuthContext.Provider value={authContext}>
<SafeAreaProvider>
<StatusBar style="light" translucent />
<WebSocketComponent getData={handleGetWebsocketData} />
<View style={{ flex: 1, backgroundColor: "#07050A" }}>
<NavigationContainer onReady={onLayoutRootView} theme={MyTheme}>
<ImageViewerProvider>

View File

@ -0,0 +1,93 @@
import React, { useEffect, useState } from "react";
import { View, Text, FlatList } from "react-native";
import baseRequest from "../../utils/baseRequest";
const WebSocketComponent = ({ getData }) => {
const [messages, setMessages] = useState([]);
useEffect(() => {
let socket = null;
async function fn() {
const base = await baseRequest();
// WebSocket
socket = new WebSocket(
`https://wsdebug.tiefen.fun/ws?b_mid=${base.b_mid}&b_did=${
base.b_did
}&b_dt=1&b_token=${base.b_token.replace(/\./g, "_")}`
); // 使wss
//
console.log("WebSocket ccccc.");
socket.onopen = () => {
console.log("WebSocket connected.");
// socket.send('Hello Server!');
socket.send(JSON.stringify({ t: 1 }));
};
//
socket.onmessage = (event) => {
// console.log(
// "-----:",
// Object.prototype.toString.call(event.data),
// JSON.parse(event.data)
// );
if (
Object.prototype.toString.call(event.data) === "[object ArrayBuffer]"
) {
const view = new Uint8Array(event.data);
const str = String.fromCharCode.apply(null, view);
console.log(str);
try {
const data = JSON.parse(str);
console.log("收到消息:", data);
if (data.t === 2) {
socket.send("ping");
setInterval(() => {
// ping
socket.send("ping");
// ping
// socket.on("ping", () => {
// socket.send("pong");
// });
}, data.ping_interval * 1000);
getData(data);
setMessages((prevMessages) => [...prevMessages, data]);
}
} catch (error) {}
}
};
//
socket.onclose = () => {
console.log("WebSocket disconnected.");
};
//
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
// ping
// socket.on("ping", () => {
// socket.send("pong");
// });
}
fn();
// WebSocket
return () => {
socket.close();
};
}, []); // effect
return (
<View>
{/* <Text style={{ fontSize: 24, fontWeight: 'bold' }}>WebSocket Messages</Text>
<FlatList
data={messages}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => <Text>{item}</Text>}
/> */}
</View>
);
};
export default WebSocketComponent;

View File

@ -6,13 +6,10 @@ import {
Animated,
useWindowDimensions,
} from "react-native";
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useRef, useCallback } from "react";
import { useTailwind } from "tailwind-rn";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge } from "@rneui/themed";
import { Icon } from "@rneui/themed";
import baseRequest from "../../../utils/baseRequest";
import { generateSignature } from "../../../utils/crypto";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import MessageList from "../components/MessageList";
export default function NoticeNav({ navigation }) {
@ -20,56 +17,21 @@ export default function NoticeNav({ navigation }) {
const [openNotices, setOpenNotices] = useState(false);
const tailwind = useTailwind();
const insets = useSafeAreaInsets();
const mesListEl = useRef(null);
//tab
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([{ key: "list", title: "消息" }]);
useEffect(() => {
getData();
}, []);
const getData = async (searchValue) => {
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
try {
const base = await baseRequest();
const body = {
mid: base.b_mid,
...base,
};
console.log(base.b_mid);
const signature = await generateSignature(body);
const _response = await fetch(
`${apiUrl}/api/notification/list_by_mid?signature=${signature}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const _data = await _response.json();
if (_data.ret === -1) {
Toast.show({
type: "error",
text1: _data.msg,
topOffset: 60,
});
return;
}
console.log(_data.data.list);
setData(_data.data);
} catch (error) {
console.error(error);
}
};
const renderScene = useCallback(
SceneMap({
list: () => <MessageList navigation={navigation} />,
list: () => <MessageList navigation={navigation} ref={mesListEl} />,
}),
[]
);
const handleReadAll = useCallback(() => {
mesListEl.current.readAllMsg();
}, []);
const renderIndicator = useCallback((props) => {
const { position, navigationState, getTabWidth } = props;
const inputRange = [0, 1];
@ -125,7 +87,7 @@ export default function NoticeNav({ navigation }) {
style={tailwind(
"flex items-center justify-center w-9 h-9 mr-4 bg-[#FFFFFF1A] rounded-full"
)}
onPress={() => navigation.navigate("Search")}
onPress={handleReadAll}
>
<NativeImage
source={require("../../../assets/icon/32DP/remove.png")}

View File

@ -3,17 +3,41 @@ import React, { useState, useEffect } from "react";
import { useTailwind } from "tailwind-rn";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import NoticeItem from "../components/NoticeItem";
import { Icon } from "@rneui/themed";
import baseRequest from "../../../utils/baseRequest";
import { generateSignature } from "../../../utils/crypto";
import { Image } from "expo-image";
import Empty from "../../../components/Empty";
export default function SystemNotice({ navigation, route }) {
const tailwind = useTailwind();
const insets = useSafeAreaInsets();
const [data, setData] = useState([]);
useEffect(() => {
// handleClearCount();
getData();
}, []);
// const handleClearCount = async () => {
// const type = route.params["type"];
// const total = route.params["total"];
// let msgName = "";
// switch (type) {
// case 0:
// msgName = "system_msg";
// break;
// case 1:
// msgName = "pay_msg";
// break;
// case 2:
// msgName = "active_msg";
// break;
// case 3:
// msgName = "exam_msg";
// break;
// default:
// break;
// }
// await save(msgName, total);
// };
const getData = async (searchValue) => {
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
try {
@ -53,6 +77,7 @@ export default function SystemNotice({ navigation, route }) {
});
return;
}
// console.log("_data", _data);
const type = route.params["type"];
setData(_data.data.list.filter((it) => it.n_type == type));
} catch (error) {
@ -79,6 +104,7 @@ export default function SystemNotice({ navigation, route }) {
}
/>
))}
{!data.length && <Empty type="nodata" />}
</ScrollView>
);
}

View File

@ -3,18 +3,20 @@ import {
TouchableOpacity,
Image as NativeImage,
Text,
Animated,
useWindowDimensions,
Easing,
} from "react-native";
import React, { useState, useEffect, useMemo } from "react";
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { useTailwind } from "tailwind-rn";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import baseRequest from "../../../../utils/baseRequest";
import { generateSignature } from "../../../../utils/crypto";
import { formatDate } from "../../../../utils/tools";
import ScrollNotice from "../ScrollNotice";
export default function MessageList({ navigation }) {
const MessageList = forwardRef(({ navigation }, ref) => {
const [data, setData] = useState([]);
const [scollNotice, setScollNotice] = useState("");
const tailwind = useTailwind();
@ -25,6 +27,7 @@ export default function MessageList({ navigation }) {
time: "",
subtitle: "展示相关活动推送消息",
count: 0,
type: "system_msg",
icon: require(`../../../../assets/icon/others/m_system.png`),
},
{
@ -32,6 +35,7 @@ export default function MessageList({ navigation }) {
time: "",
subtitle: "展示相关活动推送消息",
count: 0,
type: "pay_msg",
icon: require(`../../../../assets/icon/others/m_pay.png`),
},
{
@ -39,6 +43,7 @@ export default function MessageList({ navigation }) {
time: "",
subtitle: "展示相关活动推送消息",
count: 0,
type: "active_msg",
icon: require(`../../../../assets/icon/others/m_active.png`),
},
{
@ -46,15 +51,21 @@ export default function MessageList({ navigation }) {
time: "",
subtitle: "展示相关活动推送消息",
count: 0,
type: "exam_msg",
icon: require(`../../../../assets/icon/others/m_exam.png`),
},
]);
useImperativeHandle(ref, () => ({
readAllMsg: async () => {
console.log("清除未读数!");
},
}));
useEffect(() => {
getData();
getActiveNotice();
navigation.addListener("focus", () => {
getData();
getActiveNotice();
});
}, []);
const getData = async (searchValue) => {
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
try {
@ -161,7 +172,9 @@ export default function MessageList({ navigation }) {
return (
<TouchableOpacity
key={index}
onPress={() => navigation?.navigate("SystemNotice", { type: index })}
onPress={() =>
navigation?.navigate("SystemNotice", { type: index, total: count })
}
style={{
...tailwind("flex flex-row items-center py-4 mb-2 rounded-xl"),
backgroundColor: "#ffffff1a",
@ -319,4 +332,6 @@ export default function MessageList({ navigation }) {
</View>
</View>
);
}
});
export default MessageList;

View File

@ -30,6 +30,10 @@ import MyDivider from "../../components/MyDivider/index";
import MySlider from "../../components/MySlider";
import Picker from "../../components/Picker";
import { get } from "../../utils/storeInfo";
import { debounce } from "../../utils/tools";
const newDebounce = debounce(function (fn) {
fn && fn();
}, 500);
const filterComprehensiveItems = {
age: { lower_bound: 18, upper_bound: 60 },
fans: { lower_bound: 1, upper_bound: 1000 },
@ -224,11 +228,11 @@ export default function Search({ navigation, route }) {
...filterComprehensiveItems,
...filterPriceItems,
});
const updateSearch = (search) => {
setSearch(search);
if (!search) return;
setIsloading(true);
};
// const updateSearch = (search) => {
// setSearch(search);
// if (!search) return;
// setIsloading(true);
// };
//focus
useEffect(() => {
@ -269,81 +273,63 @@ export default function Search({ navigation, route }) {
getIsMember();
}, [])
);
//
useEffect(() => {
if (!search) {
setStreamers([]);
setZones([]);
return;
const handleSearch = async (searchValue) => {
if (filtersValue.comprehensiveUsed.used || filtersValue.priceUsed.used) {
handleResetFiltersValue();
handleResetFiltersSearchValue();
}
const isNumeric = (str) => {
return /^\d+$/.test(str);
};
const getResult = async () => {
if (filtersValue.comprehensiveUsed.used || filtersValue.priceUsed.used) {
handleResetFiltersValue();
handleResetFiltersSearchValue();
}
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const isSearchInt = isNumeric(search);
let api;
let querryParams;
if (isSearchInt) {
api = "/api/streamer/list_ext_fuzzily_by_user_id";
querryParams = { user_id: parseInt(search, 10) };
} else {
api = "/api/streamer/list_ext_fuzzily_by_name";
querryParams = { name: search };
}
try {
const base = await baseRequest();
const signature = await generateSignature({
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const isSearchInt = isNumeric(searchValue);
let api;
let querryParams;
if (isSearchInt) {
api = "/api/streamer/list_ext_fuzzily_by_user_id";
querryParams = { user_id: parseInt(searchValue, 10) };
} else {
api = "/api/streamer/list_ext_fuzzily_by_name";
querryParams = { name: searchValue };
}
try {
const base = await baseRequest();
const signature = await generateSignature({
...base,
...querryParams,
offset: 0,
limit: 20,
});
const response = await fetch(`${apiUrl}${api}?signature=${signature}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...base,
...querryParams,
offset: 0,
limit: 20,
}),
});
const data = await response.json();
if (data.ret === -1) {
Toast.show({
type: "error",
text1: data.msg,
topOffset: 60,
});
const response = await fetch(`${apiUrl}${api}?signature=${signature}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...base,
...querryParams,
offset: 0,
limit: 20,
}),
});
const data = await response.json();
if (data.ret === -1) {
Toast.show({
type: "error",
text1: data.msg,
topOffset: 60,
});
return;
}
if (!ignore) {
const zonesData = data.data.list.filter(
(item) => item.zones.length > 0
);
setStreamers(data.data.list);
setZones(zonesData);
setRecommList([]);
}
setIsloading(false);
} catch (error) {
console.error(error);
return;
}
};
let ignore = false;
getResult();
return () => {
ignore = true;
};
}, [search]);
const zonesData = data.data.list.filter((item) => item.zones.length > 0);
setStreamers(data.data.list);
setZones(zonesData);
setRecommList([]);
setIsloading(false);
} catch (error) {
console.error(error);
}
};
const getFiltersResult = async (obj) => {
if (search != "") {
setSearch("");
@ -733,7 +719,20 @@ export default function Search({ navigation, route }) {
clearIcon={() => <></>}
searchIcon={() => <></>}
showLoading={isloading}
onChangeText={updateSearch}
onChangeText={(val) => {
setSearch((old) => {
let test = (e) => {
if (val == "") {
setStreamers([]);
setZones([]);
return;
}
handleSearch(val);
};
newDebounce(test);
return val;
});
}}
value={search}
/>
</View>

View File

@ -187,6 +187,30 @@ export default function Wallet({ navigation, route }) {
color="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() =>
navigation.navigate("WebWithHeader", {
title: "收支明细",
uri:
process.env.EXPO_PUBLIC_WEB_URL +
"/bill/income/income_querry",
})
}
style={tailwind("flex-row justify-between items-center py-4")}
>
<View style={tailwind("flex-row items-center")}>
<Icon type="ionicon" name="print" size={32} color="#60a5fa" />
<Text style={tailwind("text-base text-white font-medium ml-2")}>
近一周收益
</Text>
</View>
<Icon
type="ionicon"
name="chevron-forward-outline"
size={28}
color="white"
/>
</TouchableOpacity>
</View>
</View>
</View>

View File

@ -34,3 +34,22 @@ export function formatDate(timestamp) {
return `${hours}:${minutes > 9 ? minutes : "0" + minutes}`;
}
}
// 防抖函数
export function debounce(fn, delay) {
let timer = null;
return (fnn) => {
//清除上一次的延时器
if (timer) {
clearTimeout(timer);
// return;
// console.log(timer);
}
//重新设置新的延时器
timer = setTimeout(() => {
//修改this指向问题
// fn.apply(this,value)
fn(fnn);
}, delay);
};
}