feat(群推任务): 添加获取群推任务详情的API接口
feat(客服聊天): 实现搜索功能并优化联系人筛选逻辑 refactor(WebSocket): 重构连接逻辑,增加状态检查和调试信息 fix(WebSocket): 修复页面刷新后重连逻辑,避免重复连接
This commit is contained in:
@@ -3,6 +3,10 @@ import request from "@/api/request";
|
||||
export function createGroupPushTask(data) {
|
||||
return request("/v1/workbench/create", { ...data, type: 3 }, "POST");
|
||||
}
|
||||
// 获取自动点赞任务详情
|
||||
export function fetchGroupPushTaskDetail(id: string) {
|
||||
return request("/v1/workbench/detail", { id }, "GET");
|
||||
}
|
||||
|
||||
export function updateGroupPushTask(data) {
|
||||
return request("/v1/workbench/update", { ...data, type: 3 }, "POST");
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Button } from "antd";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { createGroupPushTask } from "./index.api";
|
||||
import { createGroupPushTask, fetchGroupPushTaskDetail } from "./index.api";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
|
||||
@@ -35,6 +35,7 @@ const NewGroupPush: React.FC = () => {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
startTime: "06:00", // 允许推送的开始时间
|
||||
dailyPushCount: 0, // 每日已推送次数
|
||||
endTime: "23:59", // 允许推送的结束时间
|
||||
maxPerDay: 20,
|
||||
pushOrder: 2, // 2: 按最新
|
||||
|
||||
@@ -22,6 +22,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
);
|
||||
const kfSelected = useCkChatStore(state => state.kfSelected);
|
||||
const countLables = useCkChatStore(state => state.countLables);
|
||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
||||
|
||||
// 使用useEffect来处理异步的getNewContractList调用
|
||||
useEffect(() => {
|
||||
@@ -36,7 +37,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
};
|
||||
|
||||
fetchNewContractList();
|
||||
}, [getNewContractListFn, kfSelected, countLables]);
|
||||
}, [getNewContractListFn, kfSelected, countLables, searchKeyword]);
|
||||
|
||||
const [activeKey, setActiveKey] = useState<string[]>([]); // 默认展开第一个分组
|
||||
|
||||
|
||||
@@ -26,29 +26,21 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
loading = false,
|
||||
}) => {
|
||||
const chatSessions = useCkChatStore(state => state.getChatSessions());
|
||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
||||
const setSearchKeyword = useCkChatStore(state => state.setSearchKeyword);
|
||||
const clearSearchKeyword = useCkChatStore(state => state.clearSearchKeyword);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("chats");
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
setSearchKeyword(value);
|
||||
};
|
||||
|
||||
const getFilteredContacts = () => {
|
||||
if (!searchText) return contracts;
|
||||
return contracts.filter(
|
||||
contract =>
|
||||
contract.nickname.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
contract.phone.includes(searchText),
|
||||
);
|
||||
const handleClearSearch = () => {
|
||||
clearSearchKeyword();
|
||||
};
|
||||
|
||||
const getFilteredSessions = () => {
|
||||
if (!searchText) return chatSessions;
|
||||
return chatSessions.filter(session =>
|
||||
session.nickname.toLowerCase().includes(searchText.toLowerCase()),
|
||||
);
|
||||
};
|
||||
// 过滤逻辑已移至store中,这里直接使用store返回的已过滤数据
|
||||
|
||||
// 渲染骨架屏
|
||||
const renderSkeleton = () => (
|
||||
@@ -103,8 +95,9 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
<Input
|
||||
placeholder="搜索联系人、群组"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
value={searchKeyword}
|
||||
onChange={e => handleSearch(e.target.value)}
|
||||
onClear={handleClearSearch}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
@@ -142,7 +135,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
case "chats":
|
||||
return (
|
||||
<MessageList
|
||||
chatSessions={getFilteredSessions()}
|
||||
chatSessions={chatSessions}
|
||||
onContactClick={onContactClick}
|
||||
currentChat={currentChat}
|
||||
/>
|
||||
@@ -150,7 +143,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
case "contracts":
|
||||
return (
|
||||
<WechatFriends
|
||||
contracts={getFilteredContacts() as ContractData[]}
|
||||
contracts={contracts as ContractData[]}
|
||||
onContactClick={onContactClick}
|
||||
selectedContactId={currentChat}
|
||||
/>
|
||||
|
||||
@@ -9,23 +9,27 @@ import PageSkeleton from "./components/Skeleton";
|
||||
import styles from "./index.module.scss";
|
||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
||||
const { Header, Content, Sider } = Layout;
|
||||
import { chatInitAPIdata } from "./main";
|
||||
import { chatInitAPIdata, initSocket } from "./main";
|
||||
import { clearUnreadCount } from "@/pages/pc/ckbox/api";
|
||||
import {
|
||||
KfUserListData,
|
||||
weChatGroup,
|
||||
ContractData,
|
||||
} from "@/pages/pc/ckbox/data";
|
||||
|
||||
import { useWebSocketStore } from "@/store/module/websocket";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
const CkboxPage: React.FC = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [contracts, setContacts] = useState<any[]>([]);
|
||||
const [currentChat, setCurrentChat] = useState<ContractData | weChatGroup>(
|
||||
null,
|
||||
);
|
||||
|
||||
const status = useWebSocketStore(state => state.status);
|
||||
const messages = useWebSocketStore(state => state.messages);
|
||||
// 不要在组件初始化时获取sendCommand,而是在需要时动态获取
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProfile, setShowProfile] = useState(true);
|
||||
const kfUserList = useCkChatStore(state => state.kfUserList);
|
||||
|
||||
useEffect(() => {
|
||||
// 方法一:使用 Promise 链式调用处理异步函数
|
||||
@@ -49,6 +53,9 @@ const CkboxPage: React.FC = () => {
|
||||
});
|
||||
|
||||
setContacts(isChatList);
|
||||
|
||||
// 数据加载完成后初始化WebSocket连接
|
||||
initSocket();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取联系人列表失败:", error);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
asyncChatSessions,
|
||||
asyncWeChatGroup,
|
||||
asyncCountLables,
|
||||
useCkChatStore,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
import { useWebSocketStore } from "@/store/module/websocket";
|
||||
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
getContactList,
|
||||
getGroupList,
|
||||
} from "./api";
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
|
||||
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||
@@ -92,6 +93,35 @@ export const chatInitAPIdata = async () => {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
//发起soket连接
|
||||
export const initSocket = () => {
|
||||
// 检查WebSocket是否已经连接
|
||||
const { status } = useWebSocketStore.getState();
|
||||
|
||||
// 如果已经连接或正在连接,则不重复连接
|
||||
if (status === "connected" || status === "connecting") {
|
||||
console.log("WebSocket已连接或正在连接,跳过重复连接", { status });
|
||||
return;
|
||||
}
|
||||
|
||||
// 从store获取token和accountId
|
||||
const { token2 } = useUserStore.getState();
|
||||
const { getAccountId } = useCkChatStore.getState();
|
||||
const Token = token2;
|
||||
const accountId = getAccountId();
|
||||
// 使用WebSocket store初始化连接
|
||||
const { connect } = useWebSocketStore.getState();
|
||||
console.log("发起链接", Token, accountId);
|
||||
|
||||
// 连接WebSocket
|
||||
connect({
|
||||
accessToken: Token,
|
||||
accountId: Number(accountId),
|
||||
client: "kefu-client",
|
||||
cmdType: "CmdSignIn",
|
||||
seq: +new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getCountLables = async () => {
|
||||
const LablesRes = await Promise.all(
|
||||
@@ -240,14 +270,6 @@ export const getAllGroupList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getChatInfo = () => {
|
||||
//获取UserId
|
||||
sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: ["300745", "4880930", "32686452"],
|
||||
seq: +new Date(),
|
||||
});
|
||||
console.log("发送链接信息");
|
||||
};
|
||||
//获取token
|
||||
const getToken = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
countLables: [], //标签列表
|
||||
newContractList: [], //联系人分组
|
||||
kfSelected: 0, //选中的客服
|
||||
searchKeyword: "", //搜索关键词
|
||||
//客服列表
|
||||
asyncKfUserList: async data => {
|
||||
set({ kfUserList: data });
|
||||
@@ -42,6 +43,14 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
await state.getNewContractList();
|
||||
}
|
||||
},
|
||||
// 设置搜索关键词
|
||||
setSearchKeyword: (keyword: string) => {
|
||||
set({ searchKeyword: keyword });
|
||||
},
|
||||
// 清除搜索关键词
|
||||
clearSearchKeyword: () => {
|
||||
set({ searchKeyword: "" });
|
||||
},
|
||||
asyncKfSelected: async (data: number) => {
|
||||
set({ kfSelected: data });
|
||||
// 清除getChatSessions、getContractList和getNewContractList缓存
|
||||
@@ -74,6 +83,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
let cachedResult: any = null;
|
||||
let lastKfSelected: number | null = null;
|
||||
let lastCountLablesLength: number = 0;
|
||||
let lastSearchKeyword: string = "";
|
||||
|
||||
return async () => {
|
||||
const state = useCkChatStore.getState();
|
||||
@@ -82,16 +92,38 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
const shouldRecalculate =
|
||||
cachedResult === null ||
|
||||
lastKfSelected !== state.kfSelected ||
|
||||
lastCountLablesLength !== (state.countLables?.length || 0);
|
||||
lastCountLablesLength !== (state.countLables?.length || 0) ||
|
||||
lastSearchKeyword !== state.searchKeyword;
|
||||
|
||||
if (shouldRecalculate) {
|
||||
// 使用createContractList构建联系人分组数据
|
||||
cachedResult = await createContractList(
|
||||
let contractList = await createContractList(
|
||||
state.kfSelected,
|
||||
state.countLables,
|
||||
);
|
||||
|
||||
// 根据搜索关键词筛选联系人分组
|
||||
if (state.searchKeyword.trim()) {
|
||||
const keyword = state.searchKeyword.toLowerCase();
|
||||
contractList = contractList
|
||||
.map(group => ({
|
||||
...group,
|
||||
contracts:
|
||||
group.contracts?.filter(item => {
|
||||
const nickname = (item.nickname || "").toLowerCase();
|
||||
const conRemark = (item.conRemark || "").toLowerCase();
|
||||
return (
|
||||
nickname.includes(keyword) || conRemark.includes(keyword)
|
||||
);
|
||||
}) || [],
|
||||
}))
|
||||
.filter(group => group.contracts.length > 0);
|
||||
}
|
||||
|
||||
cachedResult = contractList;
|
||||
lastKfSelected = state.kfSelected;
|
||||
lastCountLablesLength = state.countLables?.length || 0;
|
||||
lastSearchKeyword = state.searchKeyword;
|
||||
}
|
||||
|
||||
return cachedResult;
|
||||
@@ -142,6 +174,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
let cachedResult: any = null;
|
||||
let lastKfSelected: number | null = null;
|
||||
let lastContractListLength: number = 0;
|
||||
let lastSearchKeyword: string = "";
|
||||
|
||||
return () => {
|
||||
const state = useCkChatStore.getState();
|
||||
@@ -150,17 +183,33 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
const shouldRecalculate =
|
||||
cachedResult === null ||
|
||||
lastKfSelected !== state.kfSelected ||
|
||||
lastContractListLength !== state.contractList.length;
|
||||
lastContractListLength !== state.contractList.length ||
|
||||
lastSearchKeyword !== state.searchKeyword;
|
||||
|
||||
if (shouldRecalculate) {
|
||||
const filteredContracts = state.contractList.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
let filteredContracts = state.contractList;
|
||||
|
||||
cachedResult =
|
||||
state.kfSelected !== 0 ? filteredContracts : state.contractList;
|
||||
// 根据客服筛选
|
||||
if (state.kfSelected !== 0) {
|
||||
filteredContracts = filteredContracts.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// 根据搜索关键词筛选
|
||||
if (state.searchKeyword.trim()) {
|
||||
const keyword = state.searchKeyword.toLowerCase();
|
||||
filteredContracts = filteredContracts.filter(item => {
|
||||
const nickname = (item.nickname || "").toLowerCase();
|
||||
const conRemark = (item.conRemark || "").toLowerCase();
|
||||
return nickname.includes(keyword) || conRemark.includes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
cachedResult = filteredContracts;
|
||||
lastKfSelected = state.kfSelected;
|
||||
lastContractListLength = state.contractList.length;
|
||||
lastSearchKeyword = state.searchKeyword;
|
||||
}
|
||||
|
||||
return cachedResult;
|
||||
@@ -203,6 +252,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
let cachedResult: any = null;
|
||||
let lastKfSelected: number | null = null;
|
||||
let lastChatSessionsLength: number = 0;
|
||||
let lastSearchKeyword: string = "";
|
||||
|
||||
return () => {
|
||||
const state = useCkChatStore.getState();
|
||||
@@ -211,17 +261,33 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
const shouldRecalculate =
|
||||
cachedResult === null ||
|
||||
lastKfSelected !== state.kfSelected ||
|
||||
lastChatSessionsLength !== state.chatSessions.length;
|
||||
lastChatSessionsLength !== state.chatSessions.length ||
|
||||
lastSearchKeyword !== state.searchKeyword;
|
||||
|
||||
if (shouldRecalculate) {
|
||||
const filteredSessions = state.chatSessions.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
let filteredSessions = state.chatSessions;
|
||||
|
||||
cachedResult =
|
||||
state.kfSelected !== 0 ? filteredSessions : state.chatSessions;
|
||||
// 根据客服筛选
|
||||
if (state.kfSelected !== 0) {
|
||||
filteredSessions = filteredSessions.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// 根据搜索关键词筛选
|
||||
if (state.searchKeyword.trim()) {
|
||||
const keyword = state.searchKeyword.toLowerCase();
|
||||
filteredSessions = filteredSessions.filter(item => {
|
||||
const nickname = (item.nickname || "").toLowerCase();
|
||||
const conRemark = (item.conRemark || "").toLowerCase();
|
||||
return nickname.includes(keyword) || conRemark.includes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
cachedResult = filteredSessions;
|
||||
lastKfSelected = state.kfSelected;
|
||||
lastChatSessionsLength = state.chatSessions.length;
|
||||
lastSearchKeyword = state.searchKeyword;
|
||||
}
|
||||
|
||||
return cachedResult;
|
||||
@@ -348,6 +414,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
partialize: state => ({
|
||||
userInfo: state.userInfo,
|
||||
isLoggedIn: state.isLoggedIn,
|
||||
kfUserList: state.kfUserList,
|
||||
}),
|
||||
onRehydrateStorage: () => state => {
|
||||
// console.log("CkChat store hydrated:", state);
|
||||
@@ -399,4 +466,8 @@ export const asyncCountLables = (data: ContactGroupByLabel[]) =>
|
||||
export const asyncNewContractList = (data: any[]) =>
|
||||
useCkChatStore.getState().asyncNewContractList(data);
|
||||
export const getCountLables = () => useCkChatStore.getState().countLables;
|
||||
export const setSearchKeyword = (keyword: string) =>
|
||||
useCkChatStore.getState().setSearchKeyword(keyword);
|
||||
export const clearSearchKeyword = () =>
|
||||
useCkChatStore.getState().clearSearchKeyword();
|
||||
useCkChatStore.getState().getKfSelectedUser();
|
||||
|
||||
@@ -72,7 +72,7 @@ interface WebSocketState {
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: WebSocketConfig = {
|
||||
url: (import.meta as any).env?.VITE_API_WS_URL,
|
||||
url: "wss://kf.quwanzhi.com:9993",
|
||||
client: "kefu-client",
|
||||
accountId: 0,
|
||||
accessToken: "",
|
||||
@@ -97,8 +97,21 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
connect: (config: Partial<WebSocketConfig>) => {
|
||||
const currentState = get();
|
||||
|
||||
// 如果已经连接,先断开
|
||||
// 检查当前连接状态,避免重复连接
|
||||
if (
|
||||
currentState.status === WebSocketStatus.CONNECTED ||
|
||||
currentState.status === WebSocketStatus.CONNECTING
|
||||
) {
|
||||
console.log("WebSocket已连接或正在连接,跳过重复连接", {
|
||||
currentStatus: currentState.status,
|
||||
hasWebSocket: !!currentState.ws,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经有WebSocket实例,先断开
|
||||
if (currentState.ws) {
|
||||
console.log("断开现有WebSocket连接");
|
||||
currentState.disconnect();
|
||||
}
|
||||
|
||||
@@ -124,8 +137,18 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
t: Date.now().toString(),
|
||||
});
|
||||
|
||||
// 检查URL是否为localhost,如果是则不连接
|
||||
// 调试信息:输出配置和环境变量
|
||||
console.log("WebSocket配置信息:", {
|
||||
configUrl: fullConfig.url,
|
||||
envUrl: (import.meta as any).env?.VITE_API_WS_URL,
|
||||
fullConfig,
|
||||
params: params.toString(),
|
||||
});
|
||||
|
||||
const wsUrl = fullConfig.url + "?" + params;
|
||||
console.log("最终WebSocket URL:", wsUrl);
|
||||
|
||||
// 检查URL是否为localhost,如果是则不连接
|
||||
if (wsUrl.includes("localhost") || wsUrl.includes("127.0.0.1")) {
|
||||
console.error("WebSocket连接被拦截:不允许连接到本地地址", wsUrl);
|
||||
Toast.show({
|
||||
@@ -273,11 +296,6 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
client: currentState.config?.client || "kefu-client",
|
||||
seq: +new Date(),
|
||||
});
|
||||
//获取UserId
|
||||
currentState.sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: ["300745", "4880930", "32686452"],
|
||||
seq: +new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
Toast.show({ content: "WebSocket连接成功", position: "top" });
|
||||
@@ -416,9 +434,56 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
onRehydrateStorage: () => state => {
|
||||
// 页面刷新后,如果之前是连接状态,尝试重新连接
|
||||
if (state && state.status === WebSocketStatus.CONNECTED && state.config) {
|
||||
console.log("页面刷新后恢复WebSocket连接", {
|
||||
persistedConfig: state.config,
|
||||
currentDefaultConfig: DEFAULT_CONFIG,
|
||||
});
|
||||
|
||||
// 使用最新的默认配置,而不是持久化的配置
|
||||
const freshConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
client: state.config.client,
|
||||
accountId: state.config.accountId,
|
||||
accessToken: state.config.accessToken,
|
||||
autoReconnect: state.config.autoReconnect,
|
||||
};
|
||||
|
||||
console.log("使用刷新后的配置重连:", freshConfig);
|
||||
|
||||
// 延迟一下再重连,确保页面完全加载
|
||||
// 同时检查当前状态,避免重复连接
|
||||
setTimeout(() => {
|
||||
state.connect(state.config);
|
||||
// 重新获取最新的状态,而不是使用闭包中的state
|
||||
const currentState = useWebSocketStore.getState();
|
||||
console.log("页面刷新后检查状态", {
|
||||
status: currentState.status,
|
||||
hasWs: !!currentState.ws,
|
||||
});
|
||||
|
||||
// 强制重置状态为disconnected,因为页面刷新后WebSocket实例已失效
|
||||
if (
|
||||
currentState.status === WebSocketStatus.CONNECTED &&
|
||||
!currentState.ws
|
||||
) {
|
||||
console.log("检测到状态不一致,重置为disconnected");
|
||||
useWebSocketStore.setState({
|
||||
status: WebSocketStatus.DISCONNECTED,
|
||||
});
|
||||
}
|
||||
|
||||
// 重新获取状态进行连接
|
||||
const latestState = useWebSocketStore.getState();
|
||||
if (
|
||||
latestState.status === WebSocketStatus.DISCONNECTED ||
|
||||
latestState.status === WebSocketStatus.ERROR
|
||||
) {
|
||||
console.log("页面刷新后开始重连");
|
||||
latestState.connect(freshConfig);
|
||||
} else {
|
||||
console.log("WebSocket已连接或正在连接,跳过页面刷新重连", {
|
||||
status: latestState.status,
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user