更新
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { UIKitModal } from '../../../components/UIKitModal'
|
||||
|
||||
const MINI_MODAL_ERROR_CODES = [
|
||||
-1001,
|
||||
-1002,
|
||||
101002,
|
||||
];
|
||||
|
||||
const MINI_MODAL_ERROR_MAP = {
|
||||
'-1001': {
|
||||
id: 10001,
|
||||
content: '您的应用还未开通音视频通话能力'
|
||||
},
|
||||
'-1002': {
|
||||
id: 10002,
|
||||
content: '您暂不支持使用该能力,请前往购买页购买开通'
|
||||
},
|
||||
'101002': {
|
||||
id: 10014,
|
||||
content: '发起通话失败,用户 ID 无效,请确认该用户已注册'
|
||||
}
|
||||
}
|
||||
|
||||
export function handleModalError(error) {
|
||||
if (!error || !error?.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleMiniModalError(error);
|
||||
}
|
||||
|
||||
function handleMiniModalError(error) {
|
||||
if (!MINI_MODAL_ERROR_CODES.includes(error.code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorInfo = MINI_MODAL_ERROR_MAP[error.code.toString()];
|
||||
if (errorInfo) {
|
||||
UIKitModal.openModal({
|
||||
id: errorInfo.id,
|
||||
content: errorInfo.content,
|
||||
title: '错误',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { TUIBridge } from '../../../TUIBridge/index';
|
||||
import { LOG_LEVEL } from '@trtc/call-engine-lite-wx';
|
||||
import { COMPONENT, NAME, EVENT } from '../const/index';
|
||||
|
||||
export default class ChatCombine {
|
||||
static instance: ChatCombine;
|
||||
private _callService: any;
|
||||
|
||||
constructor(options) {
|
||||
this._callService = options.callService;
|
||||
|
||||
TUIBridge.registerEvent(EVENT.LOGIN_SUCCESS, this);
|
||||
TUIBridge.registerEvent(EVENT.LOGOUT_SUCCESS, this);
|
||||
TUIBridge.registerEvent(EVENT.ON_CALLS, this);
|
||||
}
|
||||
|
||||
static getInstance(options) {
|
||||
if (!ChatCombine.instance) {
|
||||
ChatCombine.instance = new ChatCombine(options);
|
||||
}
|
||||
return ChatCombine.instance;
|
||||
}
|
||||
/**
|
||||
* tuicore notify event manager
|
||||
* @param {String} eventName event name
|
||||
* @param {String} subKey sub key
|
||||
* @param {Any} options tuicore event parameters
|
||||
*/
|
||||
public async onNotifyEvent(options?: any) {
|
||||
try {
|
||||
if (options?.eventName === EVENT.LOGIN_SUCCESS) {
|
||||
const { chat, userID, userSig, SDKAppID } = options?.params || {};
|
||||
await this._callService?.init({ tim: chat, userID, userSig, sdkAppID: SDKAppID, isFromChat: true, component: COMPONENT.TIM_CALL_KIT });
|
||||
this._callService?.setIsFromChat(true);
|
||||
this._callService?.setLogLevel(LOG_LEVEL.NORMAL);
|
||||
}
|
||||
if (options?.eventName === EVENT.LOGOUT_SUCCESS) {
|
||||
await this._callService?.destroyed();
|
||||
}
|
||||
if (options?.eventName === EVENT.ON_CALLS) {
|
||||
options?.params && this._callService.calls(options.params);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}TUICore onNotifyEvent failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import { ITUIStore } from '../interface/ITUIStore';
|
||||
import { TUICallEvent } from '@trtc/call-engine-lite-wx';
|
||||
import { IUserInfo } from '../interface/ICallService';
|
||||
import { CallMediaType } from '@trtc/call-engine-lite-wx';
|
||||
import { StoreName, CallStatus, NAME, CallRole, ErrorCode, ErrorMessage, NETWORK_QUALITY_THRESHOLD } from '../const/index';
|
||||
import { CallTips, t } from '../locales/index';
|
||||
import { initAndCheckRunEnv } from './miniProgram';
|
||||
import { getRemoteUserProfile, analyzeEventData, deleteRemoteUser } from './utils';
|
||||
import TuiStore from '../TUIStore/tuiStore';
|
||||
const TUIStore: ITUIStore = TuiStore.getInstance();
|
||||
|
||||
|
||||
export default class EngineEventHandler {
|
||||
static instance: EngineEventHandler;
|
||||
private _callService: any;
|
||||
|
||||
constructor(options) {
|
||||
this._callService = options.callService;
|
||||
}
|
||||
|
||||
static getInstance(options) {
|
||||
if (!EngineEventHandler.instance) {
|
||||
EngineEventHandler.instance = new EngineEventHandler(options);
|
||||
}
|
||||
return EngineEventHandler.instance;
|
||||
}
|
||||
public addListenTuiCallEngineEvent() {
|
||||
const callEngine = this._callService?.getTUICallEngineInstance();
|
||||
|
||||
if (!callEngine) {
|
||||
console.warn(`${NAME.PREFIX}add engine event listener failed, engine is empty.`);
|
||||
return;
|
||||
}
|
||||
|
||||
callEngine.on(TUICallEvent.ERROR, this._handleError, this);
|
||||
callEngine.on(TUICallEvent.ON_CALL_RECEIVED, this._handleNewInvitationReceived, this); // 收到邀请事件
|
||||
TUICallEvent?.ON_CALL_BEGIN && callEngine.on(TUICallEvent.ON_CALL_BEGIN, this._handleOnCallBegin, this); // 主叫收到被叫接通事件
|
||||
callEngine.on(TUICallEvent.USER_ENTER, this._handleUserEnter, this); // 有用户进房事件
|
||||
callEngine.on(TUICallEvent.USER_LEAVE, this._handleUserLeave, this); // 有用户离开通话事件
|
||||
callEngine.on(TUICallEvent.REJECT, this._handleInviteeReject, this); // 主叫收到被叫的拒绝通话事件
|
||||
callEngine.on(TUICallEvent.NO_RESP, this._handleNoResponse, this); // 主叫收到被叫的无应答事件
|
||||
callEngine.on(TUICallEvent.LINE_BUSY, this._handleLineBusy, this); // 主叫收到被叫的忙线事件
|
||||
callEngine.on(TUICallEvent.ON_CALL_NOT_CONNECTED, this._handleCallNotConnected, this); // 主被叫在通话未建立时, 收到的取消事件
|
||||
callEngine.on(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this); // 通话存在邀请他人时, 通话里的所有人都会抛出
|
||||
callEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件
|
||||
// @ts-ignore
|
||||
// TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.on(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this); // 用户网络质量
|
||||
callEngine.on(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this);
|
||||
callEngine.on(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, this);
|
||||
callEngine.on(TUICallEvent.CALL_END, this._handleCallingEnd, this); // 主被叫在通话结束时, 收到的通话结束事件
|
||||
}
|
||||
public removeListenTuiCallEngineEvent() {
|
||||
const callEngine = this._callService?.getTUICallEngineInstance();
|
||||
|
||||
callEngine.off(TUICallEvent.ERROR, this._handleError, this);
|
||||
callEngine.off(TUICallEvent.ON_CALL_RECEIVED, this._handleNewInvitationReceived, this);
|
||||
TUICallEvent?.ON_CALL_BEGIN && callEngine.off(TUICallEvent.ON_CALL_BEGIN, this._handleOnCallBegin, this);
|
||||
callEngine.off(TUICallEvent.USER_ENTER, this._handleUserEnter, this);
|
||||
callEngine.off(TUICallEvent.USER_LEAVE, this._handleUserLeave, this);
|
||||
callEngine.off(TUICallEvent.REJECT, this._handleInviteeReject, this);
|
||||
callEngine.off(TUICallEvent.NO_RESP, this._handleNoResponse, this);
|
||||
callEngine.off(TUICallEvent.LINE_BUSY, this._handleLineBusy, this);
|
||||
callEngine.off(TUICallEvent.ON_CALL_NOT_CONNECTED, this._handleCallNotConnected, this);
|
||||
callEngine.off(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this);
|
||||
callEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut, this);
|
||||
// @ts-ignore
|
||||
// TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.off(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this);
|
||||
callEngine.off(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this);
|
||||
callEngine.off(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, this);
|
||||
callEngine.off(TUICallEvent.CALL_END, this._handleCallingEnd, this);
|
||||
}
|
||||
private _callerChangeToConnected() {
|
||||
const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE);
|
||||
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
|
||||
|
||||
if (callStatus === CallStatus.CALLING && callRole === CallRole.CALLER) {
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED);
|
||||
this._callService?.startTimer();
|
||||
}
|
||||
}
|
||||
private _unNormalEventsManager(event: any, eventName: TUICallEvent): void {
|
||||
console.log(`${NAME.PREFIX}${eventName} event data: ${JSON.stringify(event)}.`);
|
||||
const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
|
||||
const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
|
||||
switch (eventName) {
|
||||
case TUICallEvent.REJECT:
|
||||
case TUICallEvent.LINE_BUSY: {
|
||||
const { userID: userId } = analyzeEventData(event);
|
||||
let callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.OTHER_SIDE_REJECT_CALL : CallTips.OTHER_SIDE_LINE_BUSY;
|
||||
let userListNeedToShow = '';
|
||||
if (isGroup) {
|
||||
userListNeedToShow = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId;
|
||||
callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.REJECT_CALL : CallTips.IN_BUSY;
|
||||
}
|
||||
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: callTipsKey, options: { userList: userListNeedToShow } } });
|
||||
userId && deleteRemoteUser([userId]);
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST).length === 0) {
|
||||
this._callService?._resetCallStore();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TUICallEvent.NO_RESP: {
|
||||
const { userIDList = [] } = analyzeEventData(event);
|
||||
const callTipsKey = isGroup ? CallTips.TIMEOUT : CallTips.CALL_TIMEOUT;
|
||||
const userInfoList: string[] = userIDList.map(userId => {
|
||||
const userInfo: IUserInfo = remoteUserInfoList.find(obj => obj.userId === userId) || {};
|
||||
return userInfo.displayUserInfo || userId;
|
||||
});
|
||||
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: callTipsKey, options: { userList: userInfoList.join() } } });
|
||||
userIDList.length > 0 && deleteRemoteUser(userIDList);
|
||||
break;
|
||||
}
|
||||
case TUICallEvent.ON_CALL_NOT_CONNECTED: {
|
||||
this._callService?._resetCallStore();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleError(event: any): void {
|
||||
const { code, message } = event || {};
|
||||
const index = Object.values(ErrorCode).indexOf(code);
|
||||
let callTips = '';
|
||||
|
||||
if (index !== -1) {
|
||||
const key = Object.keys(ErrorCode)[index];
|
||||
callTips = t(ErrorMessage[key]);
|
||||
callTips && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: ErrorMessage[key], type: NAME.ERROR });
|
||||
}
|
||||
console.error(`${NAME.PREFIX}_handleError, errorCode: ${code}; errorMessage: ${callTips || message}.`);
|
||||
}
|
||||
private async _handleNewInvitationReceived(event: any) {
|
||||
console.log(`${NAME.PREFIX}onCallReceived event data: ${JSON.stringify(event)}.`);
|
||||
const { callerId = '', callMediaType, inviteData = {}, calleeIdList = [], chatGroupID: groupID = '', roomID, strRoomID } = analyzeEventData(event);
|
||||
const currentUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
const remoteUserIdList: string[] = [callerId, ...calleeIdList.filter((userId: string) => userId !== currentUserInfo.userId)];
|
||||
const type = callMediaType || inviteData.callType;
|
||||
const callTipsKey = type === CallMediaType.AUDIO ? CallTips.CALLEE_CALLING_AUDIO_MSG : CallTips.CALLEE_CALLING_VIDEO_MSG;
|
||||
let updateStoreParams = {
|
||||
[NAME.CALL_ROLE]: CallRole.CALLEE,
|
||||
[NAME.IS_GROUP]: (!!groupID || calleeIdList.length > 1),
|
||||
[NAME.CALL_STATUS]: CallStatus.CALLING,
|
||||
[NAME.CALL_MEDIA_TYPE]: type,
|
||||
[NAME.CALL_TIPS]: callTipsKey,
|
||||
[NAME.CALLER_USER_INFO]: { userId: callerId },
|
||||
[NAME.GROUP_ID]: groupID,
|
||||
};
|
||||
initAndCheckRunEnv();
|
||||
this._callService.openCamera(type === CallMediaType.VIDEO);
|
||||
this._callService.openMicrophone()
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: type === CallMediaType.VIDEO,
|
||||
};
|
||||
this._callService._preDevicePermission = await this._callService._tuiCallEngine.deviceCheck(deviceMap);
|
||||
TUIStore.updateStore(updateStoreParams, StoreName.CALL);
|
||||
|
||||
const remoteUserInfoList = await getRemoteUserProfile(remoteUserIdList, this._callService?.getTim());
|
||||
const [userInfo] = remoteUserInfoList.filter((userInfo: IUserInfo) => userInfo.userId === callerId);
|
||||
|
||||
remoteUserInfoList.length > 0 && TUIStore.updateStore({
|
||||
[NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList,
|
||||
[NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList,
|
||||
[NAME.CALLER_USER_INFO]: {
|
||||
userId: callerId,
|
||||
nick: userInfo?.nick || '',
|
||||
avatar: userInfo?.avatar || '',
|
||||
displayUserInfo: userInfo?.remark || userInfo?.nick || callerId,
|
||||
},
|
||||
}, StoreName.CALL);
|
||||
}
|
||||
private async _handleOnCallBegin(event: any): Promise<void> {
|
||||
this._callerChangeToConnected();
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, { text: 'answered', duration: 2000 });
|
||||
await this._callService.openMicrophone();
|
||||
console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`);
|
||||
}
|
||||
private async _handleUserEnter(event: any): Promise<void> {
|
||||
const { userID: userId, data } = analyzeEventData(event);
|
||||
|
||||
if (userId && TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE === CallRole.CALLEE)) {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CALLING) {
|
||||
this._callService._handleAcceptResponse({});
|
||||
}
|
||||
}
|
||||
|
||||
this._callerChangeToConnected();
|
||||
await this._addUserToRemoteUserInfoList(userId);
|
||||
let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
remoteUserInfoList = remoteUserInfoList.map((obj: IUserInfo) => {
|
||||
if (obj.userId === userId) obj.isEnter = true;
|
||||
return obj;
|
||||
});
|
||||
|
||||
if (remoteUserInfoList.length > 0) {
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
||||
}
|
||||
console.log(`${NAME.PREFIX}userEnter event data: ${JSON.stringify(event)}.`);
|
||||
}
|
||||
private _handleUserLeave(event: any): void {
|
||||
console.log(`${NAME.PREFIX}userLeave event data: ${JSON.stringify(event)}.`);
|
||||
const { data, userID: userId } = analyzeEventData(event);
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)) {
|
||||
const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
const userListNeedToShow: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId;
|
||||
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: CallTips.END_CALL, options: { userList: userListNeedToShow } } });
|
||||
}
|
||||
userId && deleteRemoteUser([userId]);
|
||||
}
|
||||
private _handleInviteeReject(event: any): void {
|
||||
this._unNormalEventsManager(event, TUICallEvent.REJECT);
|
||||
}
|
||||
private _handleNoResponse(event: any): void {
|
||||
this._unNormalEventsManager(event, TUICallEvent.NO_RESP);
|
||||
}
|
||||
private _handleLineBusy(event: any): void {
|
||||
this._unNormalEventsManager(event, TUICallEvent.LINE_BUSY);
|
||||
}
|
||||
private _handleCallNotConnected(event: any): void {
|
||||
this._unNormalEventsManager(event, TUICallEvent.ON_CALL_NOT_CONNECTED);
|
||||
}
|
||||
private async _handleOnUserInviting(event: any) {
|
||||
const { userID: userId } = analyzeEventData(event);
|
||||
if (!userId) return;
|
||||
|
||||
if (userId !== TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO).userId) {
|
||||
await this._addUserToRemoteUserInfoList(userId);
|
||||
}
|
||||
}
|
||||
private _handleCallingEnd(event: any): void {
|
||||
console.log(`${NAME.PREFIX}callEnd event data: ${JSON.stringify(event)}.`);
|
||||
this._callService?._resetCallStore();
|
||||
}
|
||||
private _handleKickedOut(event: any): void {
|
||||
console.log(`${NAME.PREFIX}kickOut event data: ${JSON.stringify(event)}.`);
|
||||
this._callService?.kickedOut && this._callService?.kickedOut(event);
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.KICK_OUT);
|
||||
this._callService?._resetCallStore();
|
||||
}
|
||||
// private _handleNetworkQuality(event) {
|
||||
// const { networkQualityList = [] } = analyzeEventData(event);
|
||||
// TUIStore.update(StoreName.CALL, NAME.NETWORK_STATUS, networkQualityList);
|
||||
// const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
|
||||
// const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
// const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
|
||||
// if(!isGroup) {
|
||||
// const isRemoteNetworkPoor = networkQualityList.find(user => remoteUserInfoList[0]?.userId === user?.userId && user?.quality >= NETWORK_QUALITY_THRESHOLD);
|
||||
// if(isRemoteNetworkPoor) {
|
||||
// TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.REMOTE_NETWORK_IS_POOR);
|
||||
// return;
|
||||
// };
|
||||
// const isLocalNetworkPoor = networkQualityList.find(user => localUserInfo?.userId === user?.userId && user?.quality >= NETWORK_QUALITY_THRESHOLD);
|
||||
// if(isLocalNetworkPoor) {
|
||||
// TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.LOCAL_NETWORK_IS_POOR);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
private async _startRemoteView(userId: string) {
|
||||
if (!userId) {
|
||||
console.warn(`${NAME.PREFIX}_startRemoteView userID is empty`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const displayMode = TUIStore.getData(StoreName.CALL, NAME.DISPLAY_MODE);
|
||||
await this._callService?.getTUICallEngineInstance().startRemoteView({ userID: userId, videoViewDomID: `${userId}_0`, options: { objectFit: displayMode } });
|
||||
} catch (error: any) {
|
||||
console.error(`${NAME.PREFIX}_startRemoteView error: ${error}.`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
private _setRemoteUserInfoAudioVideoAvailable(isAvailable: boolean, type: string, userId: string) {
|
||||
let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
|
||||
remoteUserInfoList = remoteUserInfoList.map((obj: IUserInfo) => {
|
||||
if (obj.userId === userId) {
|
||||
if (type === NAME.AUDIO) {
|
||||
return { ...obj, isAudioAvailable: isAvailable };
|
||||
}
|
||||
if (type === NAME.VIDEO) {
|
||||
return { ...obj, isVideoAvailable: isAvailable };
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
|
||||
if (remoteUserInfoList.length > 0) {
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
||||
}
|
||||
}
|
||||
private async _handleUserVideoAvailable(event: any): Promise<any> {
|
||||
const { userID: userId, isVideoAvailable } = analyzeEventData(event);
|
||||
console.log(`${NAME.PREFIX}_handleUserVideoAvailable event data: ${JSON.stringify(event)}.`);
|
||||
|
||||
this._setRemoteUserInfoAudioVideoAvailable(isVideoAvailable, NAME.VIDEO, userId);
|
||||
try {
|
||||
isVideoAvailable && await this._startRemoteView(userId);
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}_startRemoteView failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
private _handleUserAudioAvailable(event: any): void {
|
||||
const { userID: userId, isAudioAvailable } = analyzeEventData(event);
|
||||
console.log(`${NAME.PREFIX}_handleUserAudioAvailable event data: ${JSON.stringify(event)}.`);
|
||||
|
||||
this._setRemoteUserInfoAudioVideoAvailable(isAudioAvailable, NAME.AUDIO, userId);
|
||||
}
|
||||
private async _addUserToRemoteUserInfoList(userId: string) {
|
||||
let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
const isInRemoteUserList = remoteUserInfoList.find(item => item?.userId === userId);
|
||||
|
||||
if (!isInRemoteUserList) {
|
||||
remoteUserInfoList.push({ userId });
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
||||
|
||||
const [userInfo] = await getRemoteUserProfile([userId], this._callService?.getTim());
|
||||
remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
remoteUserInfoList.forEach((obj) => {
|
||||
if (obj?.userId === userId) {
|
||||
obj = Object.assign(obj, userInfo);
|
||||
}
|
||||
});
|
||||
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
import { IUserInfo, ISelfInfoParams, IInitParams } from '../interface/ICallService';
|
||||
import { CallMediaType, CameraPosition, AudioPlayBackDevice, LOG_LEVEL, devicePermissions } from '@trtc/call-engine-lite-wx';
|
||||
import { ICallsParam, IInviteUserParam } from '@trtc/call-engine-lite-wx';
|
||||
import { StoreName, CallStatus, NAME, CALL_DATA_KEY, CallRole, COMPONENT, FeatureButton, ButtonState } from '../const/index';
|
||||
import { TUICallEngine } from '@trtc/call-engine-lite-wx';
|
||||
import { beforeCall, handlePackageError } from './miniProgram';
|
||||
import { CallTips, t } from '../locales/index';
|
||||
import { handleModalError } from './UIKitModal';
|
||||
import { handleRepeatedCallError, performanceNow } from '../utils/common-utils';
|
||||
import { getRemoteUserProfile, noDevicePermissionToast, setLocalUserInfoAudioVideoAvailable } from './utils';
|
||||
import { ITUIStore } from '../interface/index';
|
||||
import TuiStore from '../TUIStore/tuiStore';
|
||||
import ChatCombine from './chatCombine';
|
||||
import EngineEventHandler from './engineEventHandler';
|
||||
const TUIStore: ITUIStore = TuiStore.getInstance();
|
||||
const version = '<@VERSION@>';
|
||||
export { TUIStore };
|
||||
const defaultOfflinePushInfo = { title: '', description: t('you have a new call') };
|
||||
|
||||
export default class TUICallService {
|
||||
static instance: TUICallService;
|
||||
public _tuiCallEngine: any;
|
||||
private _tim: any = null;
|
||||
private _timerId: any = -1;
|
||||
private _startTimeStamp: number = performanceNow();
|
||||
private _isFromChat: boolean = false;
|
||||
private _offlinePushInfo = null;
|
||||
private _permissionCheckTimer: any = null;
|
||||
private _chatCombine: any = null;
|
||||
private _engineEventHandler: any = null;
|
||||
|
||||
constructor() {
|
||||
console.log(`${NAME.PREFIX}version: ${version}`);
|
||||
this._watchTUIStore();
|
||||
this._engineEventHandler = EngineEventHandler.getInstance({ callService: this });
|
||||
|
||||
this._chatCombine = ChatCombine.getInstance({ callService: this });
|
||||
}
|
||||
static getInstance() {
|
||||
if (!TUICallService.instance) {
|
||||
TUICallService.instance = new TUICallService();
|
||||
}
|
||||
return TUICallService.instance;
|
||||
}
|
||||
public async init(params: IInitParams) {
|
||||
try {
|
||||
if (this._tuiCallEngine) return;
|
||||
// @ts-ignore
|
||||
let { userID, tim, userSig, sdkAppID, SDKAppID, isFromChat, component = COMPONENT.TUI_CALL_KIT } = params;
|
||||
this._tim = tim;
|
||||
console.log(`${NAME.PREFIX}init sdkAppId: ${sdkAppID || SDKAppID}, userId: ${userID}`);
|
||||
this._tuiCallEngine = TUICallEngine.createInstance({
|
||||
tim,
|
||||
sdkAppID: sdkAppID || SDKAppID, // 兼容传入 SDKAppID 的问题
|
||||
callkitVersion: version,
|
||||
isFromChat: isFromChat || false,
|
||||
component,
|
||||
scene: 'wx-uniapp-vue3-lite',
|
||||
});
|
||||
this._addListenTuiCallEngineEvent();
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { userId: userID });
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { userId: userID });
|
||||
await this._tuiCallEngine.login({ userID, userSig, assetsPath: '' }); // web && mini
|
||||
const uiConfig = TUIStore.getData(StoreName.CALL, NAME.CUSTOM_UI_CONFIG);
|
||||
this._tuiCallEngine?.reportLog?.({
|
||||
name: 'TUICallkit.init',
|
||||
data: {
|
||||
uiConfig,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}init failed, error: ${error}.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// component destroy
|
||||
public async destroyed() {
|
||||
try {
|
||||
const currentCallStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
|
||||
if (currentCallStatus !== CallStatus.IDLE) {
|
||||
throw new Error(`please destroyed when status is idle, current status: ${currentCallStatus}`);
|
||||
}
|
||||
if (this._tuiCallEngine) {
|
||||
this._removeListenTuiCallEngineEvent();
|
||||
await this._tuiCallEngine.destroyInstance();
|
||||
this._tuiCallEngine = null;
|
||||
this._unwatchTUIStore();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}destroyed failed, error: ${error}.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ===============================【通话操作】===============================
|
||||
public async calls(callsParams: ICallsParam) {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; // avoid double click when application stuck
|
||||
try {
|
||||
TUIStore.update(StoreName.CALL, NAME.PUSHER_ID, NAME.NEW_PUSHER);
|
||||
const { userIDList, type, chatGroupID, offlinePushInfo } = callsParams;
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return;
|
||||
const remoteUserInfoList = userIDList.map(userId => ({ userId }));
|
||||
await this._updateCallStoreBeforeCall(type, remoteUserInfoList, chatGroupID);
|
||||
callsParams.offlinePushInfo = { ...defaultOfflinePushInfo, ...offlinePushInfo };
|
||||
const response = await this._tuiCallEngine.calls(callsParams);
|
||||
await this._updateCallStoreAfterCall(userIDList, response);
|
||||
} catch (error: any) {
|
||||
handleModalError(error);
|
||||
this._handleCallError(error, 'calls');
|
||||
}
|
||||
}
|
||||
// public async inviteUser(params: IInviteUserParam) {
|
||||
// if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck
|
||||
// try {
|
||||
// const { userIDList } = params;
|
||||
// let inviteUserInfoList = await getRemoteUserProfile(userIDList, this.getTim());
|
||||
// const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
// const userIDListNotInRemoteUserInfoList = userIDList.filter(userId => {
|
||||
// return !remoteUserInfoList.some(remoteUserInfo => remoteUserInfo.userId === userId);
|
||||
// });
|
||||
// if (userIDListNotInRemoteUserInfoList.length === 0) {
|
||||
// return;
|
||||
// }
|
||||
// TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, [...remoteUserInfoList, ...inviteUserInfoList]);
|
||||
// TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, [...remoteUserInfoList, ...inviteUserInfoList]);
|
||||
// this._tuiCallEngine && await this._tuiCallEngine.inviteUser(params);
|
||||
// } catch (error: any) {
|
||||
// console.error(`${NAME.PREFIX}inviteUser failed, error: ${error}.`);
|
||||
// }
|
||||
// }
|
||||
// public async join(params) {
|
||||
// if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CONNECTED) return; // avoid double click when application stuck
|
||||
// try {
|
||||
// const response = await this._tuiCallEngine.join(params);
|
||||
// TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true);
|
||||
// this.startTimer();
|
||||
|
||||
// const updateStoreParams = {
|
||||
// [NAME.CALL_ROLE]: CallRole.CALLEE,
|
||||
// [NAME.IS_GROUP]: true,
|
||||
// [NAME.CALL_STATUS]: CallStatus.CONNECTED,
|
||||
// [NAME.CALL_MEDIA_TYPE]: TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE) || CallMediaType.AUDIO, // default audio callMediaType
|
||||
// };
|
||||
// TUIStore.updateStore(updateStoreParams, StoreName.CALL);
|
||||
|
||||
// this.setSoundMode(params.type === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
|
||||
// const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
// TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
||||
// TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
||||
|
||||
// await this.openMicrophone();
|
||||
// setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO);
|
||||
// } catch (error) {
|
||||
// this._handleCallError(error, 'join');
|
||||
// }
|
||||
// }
|
||||
// ===============================【其它对外接口】===============================
|
||||
public getTUICallEngineInstance(): any {
|
||||
return this?._tuiCallEngine || null;
|
||||
}
|
||||
public setLogLevel(level: LOG_LEVEL) {
|
||||
this?._tuiCallEngine?.setLogLevel(level);
|
||||
}
|
||||
public enableFloatWindow(enable: boolean) {
|
||||
TUIStore.update(StoreName.CALL, NAME.ENABLE_FLOAT_WINDOW, enable);
|
||||
}
|
||||
public async setSelfInfo(params: ISelfInfoParams) {
|
||||
const { nickName, avatar } = params;
|
||||
try {
|
||||
await this._tuiCallEngine.setSelfInfo(nickName, avatar);
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}setSelfInfo failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public async enableMuteMode(enable: boolean) {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn(`${NAME.PREFIX}enableMuteMode failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
// =============================【实验性接口】=============================
|
||||
public callExperimentalAPI(jsonStr: string) {
|
||||
const jsonObj = JSON.parse(jsonStr);
|
||||
if (jsonObj === jsonStr) return;
|
||||
|
||||
const { api, params } = jsonObj;
|
||||
if (!api || !params) return;
|
||||
|
||||
try {
|
||||
switch(api) {
|
||||
case 'forceUseV2API':
|
||||
const { enable } = params;
|
||||
TUIStore.update(StoreName.CALL, NAME.IS_FORCE_USE_V2_API, !!enable);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this._tuiCallEngine?.reportLog?.({ name: 'TUICallKit.callExperimentalAPI.fail', data: { error } });
|
||||
}
|
||||
}
|
||||
// =============================【内部按钮操作方法】=============================
|
||||
public async accept() {
|
||||
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
|
||||
this._tuiCallEngine?.reportLog?.({
|
||||
name: 'TUICallKit.accept.start',
|
||||
data: { callStatus },
|
||||
});
|
||||
if (callStatus === CallStatus.CONNECTED) return; // avoid double click when application stuck, especially for miniProgram
|
||||
try {
|
||||
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: callMediaType === CallMediaType.VIDEO,
|
||||
};
|
||||
const currentDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap);
|
||||
if (!currentDevicePermission) {
|
||||
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
||||
if (typeof devicePermissions === 'function') {
|
||||
await devicePermissions(callMediaType);
|
||||
}
|
||||
}
|
||||
TUIStore.update(StoreName.CALL, NAME.PUSHER_ID, NAME.NEW_PUSHER);
|
||||
const response = await this._tuiCallEngine.accept();
|
||||
|
||||
await this._handleAcceptResponse(response);
|
||||
} catch (error) {
|
||||
this._tuiCallEngine?.reportLog?.({
|
||||
name: 'TUICallKit.accept.fail',
|
||||
level: 'error',
|
||||
error,
|
||||
});
|
||||
if (handleRepeatedCallError(error)) return;
|
||||
handleModalError(error);
|
||||
noDevicePermissionToast(error, CallMediaType.AUDIO, this._tuiCallEngine);
|
||||
this._resetCallStore();
|
||||
}
|
||||
}
|
||||
private async _handleAcceptResponse(response) {
|
||||
if (response) {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CONNECTED) return;
|
||||
|
||||
// 小程序接通时会进行授权弹框, 状态需要放在 accept 后, 否则先接通后再拉起权限设置
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED);
|
||||
TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true);
|
||||
this.startTimer();
|
||||
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
||||
const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close;
|
||||
(callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO);
|
||||
this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
|
||||
const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
||||
setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini default open audio
|
||||
}
|
||||
}
|
||||
public async hangup() {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck
|
||||
await this._tuiCallEngine.hangup();
|
||||
this._resetCallStore();
|
||||
}
|
||||
public async reject() {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck
|
||||
|
||||
await this._tuiCallEngine.reject();
|
||||
this._resetCallStore();
|
||||
}
|
||||
public async openCamera(videoViewDomID: string) {
|
||||
try {
|
||||
const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION);
|
||||
const isFrontCamera = currentPosition === CameraPosition.FRONT ? true : false;
|
||||
this._tuiCallEngine.openCamera(videoViewDomID, isFrontCamera);
|
||||
|
||||
setLocalUserInfoAudioVideoAvailable(true, NAME.VIDEO);
|
||||
} catch (error: any) {
|
||||
noDevicePermissionToast(error, CallMediaType.VIDEO, this._tuiCallEngine);
|
||||
console.error(`${NAME.PREFIX}openCamera error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public async closeCamera() {
|
||||
try {
|
||||
await this._tuiCallEngine.closeCamera();
|
||||
setLocalUserInfoAudioVideoAvailable(false, NAME.VIDEO);
|
||||
} catch (error: any) {
|
||||
console.error(`${NAME.PREFIX}closeCamera error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public async openMicrophone() {
|
||||
try {
|
||||
await this._tuiCallEngine.openMicrophone();
|
||||
setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO);
|
||||
} catch (error: any) {
|
||||
console.error(`${NAME.PREFIX}openMicrophone failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public async closeMicrophone() {
|
||||
try {
|
||||
await this._tuiCallEngine.closeMicrophone();
|
||||
setLocalUserInfoAudioVideoAvailable(false, NAME.AUDIO);
|
||||
} catch (error: any) {
|
||||
console.error(`${NAME.PREFIX}closeMicrophone failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public switchScreen(userId: string) {
|
||||
if(!userId) return;
|
||||
TUIStore.update(StoreName.CALL, NAME.BIG_SCREEN_USER_ID, userId);
|
||||
}
|
||||
public async switchCamera() {
|
||||
const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION);
|
||||
const targetPosition = currentPosition === CameraPosition.BACK ? CameraPosition.FRONT : CameraPosition.BACK;
|
||||
try {
|
||||
await this._tuiCallEngine.switchCamera(targetPosition);
|
||||
TUIStore.update(StoreName.CALL, NAME.CAMERA_POSITION, targetPosition);
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}_switchCamera failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
public setSoundMode(type?: string): void {
|
||||
try {
|
||||
let isEarPhone = TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE);
|
||||
const soundMode = type || (isEarPhone ? AudioPlayBackDevice.SPEAKER : AudioPlayBackDevice.EAR); // UI 层切换时传参数
|
||||
this._tuiCallEngine?.selectAudioPlaybackDevice(soundMode);
|
||||
if (type) {
|
||||
isEarPhone = type === AudioPlayBackDevice.EAR;
|
||||
} else {
|
||||
isEarPhone = !isEarPhone;
|
||||
}
|
||||
TUIStore.update(StoreName.CALL, NAME.IS_EAR_PHONE, isEarPhone);
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}setSoundMode failed, error: ${error}.`);
|
||||
}
|
||||
}
|
||||
// ==========================【TUICallEngine 事件处理】==========================
|
||||
private _addListenTuiCallEngineEvent() {
|
||||
this._engineEventHandler.addListenTuiCallEngineEvent();
|
||||
}
|
||||
private _removeListenTuiCallEngineEvent() {
|
||||
this._engineEventHandler.removeListenTuiCallEngineEvent();
|
||||
}
|
||||
// 处理用户异常退出的情况, 小程序 ”右滑“、"左上角退出"; web 页面关闭浏览器或关闭 tab 页面
|
||||
public async handleExceptionExit(event?: any) {
|
||||
try {
|
||||
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
|
||||
const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE);
|
||||
this._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.handleExceptionExit', data: { callStatus, callRole } });
|
||||
|
||||
if (callStatus === CallStatus.IDLE) return;
|
||||
// 在呼叫状态下,被叫调用 reject,主叫调用 hangup
|
||||
if (callStatus === CallStatus.CALLING) {
|
||||
if (callRole === CallRole.CALLER) {
|
||||
await this?.hangup();
|
||||
} else {
|
||||
await this?.reject();
|
||||
}
|
||||
}
|
||||
if (callStatus === CallStatus.CONNECTED) {
|
||||
await this?.hangup();
|
||||
}
|
||||
this?._resetCallStore();
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX} handleExceptionExit failed, error: ${error}.`);
|
||||
}
|
||||
if (event) {
|
||||
event.returnValue = '';
|
||||
}
|
||||
}
|
||||
// 通话时长更新
|
||||
public startTimer(): void {
|
||||
if (this._timerId === -1) {
|
||||
this._startTimeStamp = performanceNow();
|
||||
this._timerId = setInterval(() => this._updateCallDuration(), 1000);
|
||||
}
|
||||
}
|
||||
private _stopTimer(): void {
|
||||
if (this._timerId !== -1) {
|
||||
clearInterval(this._timerId);
|
||||
this._timerId = -1;
|
||||
}
|
||||
}
|
||||
// =========================【private methods for service use】=========================
|
||||
// 处理 “呼叫” 抛出的异常
|
||||
private _handleCallError(error: any, methodName?: string) {
|
||||
this._permissionCheckTimer && clearInterval(this._permissionCheckTimer);
|
||||
if (handleRepeatedCallError(error)) return;
|
||||
noDevicePermissionToast(error, CallMediaType.AUDIO, this._tuiCallEngine);
|
||||
console.error(`${NAME.PREFIX}${methodName} failed, error: ${error}.`);
|
||||
this._resetCallStore();
|
||||
throw error;
|
||||
}
|
||||
private async _updateCallStoreBeforeCall(type: number, remoteUserInfoList: IUserInfo[], groupID?: string): Promise<void> {
|
||||
let callTips = CallTips.CALLER_CALLING_MSG;
|
||||
let updateStoreParams: any = {
|
||||
[NAME.CALL_MEDIA_TYPE]: type,
|
||||
[NAME.CALL_ROLE]: CallRole.CALLER,
|
||||
[NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList,
|
||||
[NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList,
|
||||
[NAME.IS_GROUP]: (!!groupID || remoteUserInfoList.length > 1),
|
||||
[NAME.CALL_TIPS]: callTips,
|
||||
[NAME.GROUP_ID]: groupID
|
||||
};
|
||||
const callStatus = await beforeCall(type, this); // 如果没有权限, 此时为 false. 因此需要在 call 后设置为 calling. 和 web 存在差异
|
||||
console.log(`${NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`);
|
||||
TUIStore.updateStore({ ...updateStoreParams, [NAME.CALL_STATUS]: callStatus }, StoreName.CALL);
|
||||
const remoteUserInfoLists = await getRemoteUserProfile(remoteUserInfoList.map(obj => obj.userId), this.getTim());
|
||||
|
||||
if (remoteUserInfoLists.length > 0) {
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoLists);
|
||||
}
|
||||
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: type === CallMediaType.VIDEO,
|
||||
};
|
||||
let hasDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap);
|
||||
if (!hasDevicePermission) {
|
||||
this._permissionCheckTimer && clearInterval(this._permissionCheckTimer);
|
||||
this._permissionCheckTimer = setInterval(async () => {
|
||||
hasDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap);
|
||||
|
||||
if (hasDevicePermission && this._permissionCheckTimer) {
|
||||
clearInterval(this._permissionCheckTimer);
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CALLING);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
private async _updateCallStoreAfterCall(userIdList: string[], response: any) {
|
||||
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
||||
let localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_INFO, { mediaType: callMediaType, inviterId: localUserInfo.userId });
|
||||
if (response) {
|
||||
TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true);
|
||||
this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
|
||||
|
||||
const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close;
|
||||
if((callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose) {
|
||||
await this.openCamera(NAME.LOCAL_VIDEO);
|
||||
}
|
||||
localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
||||
setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini, default open audio
|
||||
} else {
|
||||
this._permissionCheckTimer && clearInterval(this._permissionCheckTimer);
|
||||
this._permissionCheckTimer = null;
|
||||
this._resetCallStore();
|
||||
}
|
||||
}
|
||||
private _getFeatureButtonDefaultState(buttonName: FeatureButton) {
|
||||
const { button: buttonConfig } = TUIStore.getData(StoreName.CALL, NAME.CUSTOM_UI_CONFIG);
|
||||
return buttonConfig?.[buttonName]?.state;
|
||||
}
|
||||
private _updateCallDuration(): void {
|
||||
TUIStore.update(StoreName.CALL, NAME.DURATION, Math.round((performanceNow() - this._startTimeStamp) / 1000));
|
||||
}
|
||||
private _resetCallStore() {
|
||||
this._stopTimer();
|
||||
// localUserInfo, language 在通话结束后不需要清除
|
||||
// callStatus 清除需要通知; isMinimized 也需要通知(basic-vue3 中切小窗关闭后, 再呼叫还是小窗, 因此需要通知到组件侧)
|
||||
// isGroup 也不清除(engine 先抛 cancel 事件, 再抛 reject 事件)
|
||||
// displayMode、videoResolution 也不能清除, 组件不卸载, 这些属性也需保留, 否则采用默认值.
|
||||
// enableFloatWindow 不清除:开启/关闭悬浮窗功能。
|
||||
let notResetOrNotifyKeys = Object.keys(CALL_DATA_KEY).filter((key) => {
|
||||
switch (CALL_DATA_KEY[key]) {
|
||||
case NAME.CALL_STATUS:
|
||||
case NAME.LANGUAGE:
|
||||
case NAME.IS_GROUP:
|
||||
case NAME.ENABLE_FLOAT_WINDOW:
|
||||
case NAME.LOCAL_USER_INFO:
|
||||
case NAME.IS_FORCE_USE_V2_API:
|
||||
case NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
notResetOrNotifyKeys = notResetOrNotifyKeys.map(key => CALL_DATA_KEY[key]);
|
||||
TUIStore.reset(StoreName.CALL, notResetOrNotifyKeys);
|
||||
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
|
||||
callStatus !== CallStatus.IDLE && TUIStore.reset(StoreName.CALL, [NAME.CALL_STATUS], true); // callStatus reset need notify
|
||||
TUIStore.reset(StoreName.CALL, [NAME.IS_MINIMIZED], true); // isMinimized reset need notify
|
||||
TUIStore.reset(StoreName.CALL, [NAME.IS_EAR_PHONE], true); // isEarPhone reset need notify
|
||||
TUIStore.reset(StoreName.CALL, [NAME.PUSHER_ID], true); // pusher unload reset need notify
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, {
|
||||
...TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO),
|
||||
isVideoAvailable: false,
|
||||
isAudioAvailable: false,
|
||||
});
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, {
|
||||
...TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN),
|
||||
isVideoAvailable: false,
|
||||
isAudioAvailable: false,
|
||||
});
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, []);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, []);
|
||||
TUIStore.update(StoreName.CALL, NAME.CAMERA_POSITION, CameraPosition.FRONT);
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_INFO, {});
|
||||
}
|
||||
// =========================【监听 TUIStore 中的状态及处理】=========================
|
||||
private _handleCallStatusChange = async (value: CallStatus) => {
|
||||
try {
|
||||
if (value === CallStatus.CALLING) {
|
||||
} else {
|
||||
// 状态变更通知
|
||||
if (value === CallStatus.CONNECTED) {
|
||||
const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
|
||||
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
||||
const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, '');
|
||||
if (!isGroup && callMediaType === CallMediaType.VIDEO) {
|
||||
this.switchScreen(remoteUserInfoList[0].domId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`${NAME.PREFIX}handleCallStatusChange, ${error}.`);
|
||||
}
|
||||
};
|
||||
private _watchTUIStore() {
|
||||
TUIStore?.watch(StoreName.CALL, {
|
||||
[NAME.CALL_STATUS]: this._handleCallStatusChange,
|
||||
});
|
||||
}
|
||||
private _unwatchTUIStore() {
|
||||
TUIStore?.unwatch(StoreName.CALL, {
|
||||
[NAME.CALL_STATUS]: this._handleCallStatusChange,
|
||||
});
|
||||
}
|
||||
// =========================【set、get methods】=========================
|
||||
public getTim() {
|
||||
if (this._tim) return this._tim;
|
||||
if (!this._tuiCallEngine) {
|
||||
console.warn(`${NAME.PREFIX}getTim warning: _tuiCallEngine Instance is not available.`);
|
||||
return null;
|
||||
}
|
||||
return this._tuiCallEngine?.tim || this._tuiCallEngine?.getTim(); // mini support getTim interface
|
||||
}
|
||||
public setIsFromChat(isFromChat: boolean) {
|
||||
this._isFromChat = isFromChat;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CallMediaType } from '@trtc/call-engine-lite-wx';
|
||||
import { CallStatus } from '../const/index';
|
||||
|
||||
export function initialUI() {
|
||||
// 收起键盘
|
||||
// @ts-ignore
|
||||
wx.hideKeyboard && wx.hideKeyboard({
|
||||
complete: () => {},
|
||||
});
|
||||
};
|
||||
// 检测运行时环境, 当是微信开发者工具时, 提示用户需要手机调试
|
||||
export function checkRunPlatform() {
|
||||
// @ts-ignore
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
if (systemInfo.platform === 'devtools') {
|
||||
// 当前运行在微信开发者工具里
|
||||
// @ts-ignore
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: '运行环境提醒',
|
||||
content: '微信开发者工具不支持原生推拉流组件(即 <live-pusher> 和 <live-player> 标签),请使用真机调试或者扫码预览。',
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
export function initAndCheckRunEnv() {
|
||||
initialUI(); // miniProgram 收起键盘, 隐藏 tabBar
|
||||
checkRunPlatform(); // miniProgram 检测运行时环境
|
||||
}
|
||||
export async function beforeCall(type: CallMediaType, that: any) {
|
||||
try {
|
||||
initAndCheckRunEnv();
|
||||
// 检查设备权限
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: type === CallMediaType.VIDEO,
|
||||
};
|
||||
const hasDevicePermission = await that._tuiCallEngine.deviceCheck(deviceMap); // miniProgram 检查设备权限
|
||||
return hasDevicePermission ? CallStatus.CALLING : CallStatus.IDLE;
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return CallStatus.IDLE;
|
||||
}
|
||||
}
|
||||
// 套餐问题提示, 小程序最低需要群组通话版, 1v1 通话版本使用 TRTC 就会报错
|
||||
export function handlePackageError(error) {
|
||||
if (error?.code === -1002) {
|
||||
// @ts-ignore
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: 'error',
|
||||
content: error?.message || '',
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleNoPusherCapabilityError(){
|
||||
// @ts-ignore
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: '权限提示',
|
||||
content: '当前小程序 appid 不具备 <live-pusher> 和 <live-player> 的使用权限,您将无法正常使用实时通话能力,请使用企业小程序账号申请权限后再进行开发体验',
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { CallMediaType } from '@trtc/call-engine-lite-wx';
|
||||
import { NAME, StoreName } from '../const/index';
|
||||
import { handleNoDevicePermissionError } from '../utils/common-utils';
|
||||
import { IUserInfo } from '../interface/ICallService';
|
||||
import { ITUIStore } from '../interface/ITUIStore';
|
||||
import { CallTips, t } from '../locales/index';
|
||||
import TuiStore from '../TUIStore/tuiStore';
|
||||
// @ts-ignore
|
||||
const TUIStore: ITUIStore = TuiStore.getInstance();
|
||||
|
||||
// 设置默认的 UserInfo 信息
|
||||
export function setDefaultUserInfo(userId: string, domId?: string): IUserInfo {
|
||||
const userInfo: IUserInfo = {
|
||||
userId,
|
||||
nick: '',
|
||||
avatar: '',
|
||||
remark: '',
|
||||
displayUserInfo: '',
|
||||
isAudioAvailable: false,
|
||||
isVideoAvailable: false,
|
||||
isEnter: false,
|
||||
domId: domId || userId,
|
||||
};
|
||||
return domId ? userInfo : { ...userInfo, isEnter: false }; // localUserInfo 没有 isEnter, remoteUserInfoList 有 isEnter
|
||||
}
|
||||
// 获取个人用户信息
|
||||
// export async function getMyProfile(myselfUserId: string, tim: any): Promise<IUserInfo> {
|
||||
// let localUserInfo: IUserInfo = setDefaultUserInfo(myselfUserId, NAME.LOCAL_VIDEO);
|
||||
// try {
|
||||
// if (!tim) return localUserInfo;
|
||||
// const res = await tim.getMyProfile();
|
||||
// const currentLocalUserInfo = TUIStore?.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); // localUserInfo may have been updated
|
||||
// if (res?.code === 0) {
|
||||
// localUserInfo = {
|
||||
// ...localUserInfo,
|
||||
// ...currentLocalUserInfo,
|
||||
// userId: res?.data?.userID,
|
||||
// nick: res?.data?.nick,
|
||||
// avatar: res?.data?.avatar,
|
||||
// displayUserInfo: res?.data?.nick || res?.data?.userID,
|
||||
// };
|
||||
// }
|
||||
// return localUserInfo;
|
||||
// } catch (error) {
|
||||
// console.error(`${NAME.PREFIX}getMyProfile failed, error: ${error}.`);
|
||||
// return localUserInfo;
|
||||
// }
|
||||
// }
|
||||
// 获取远端用户列表信息
|
||||
export async function getRemoteUserProfile(userIdList: Array<string>, tim: any): Promise<any> {
|
||||
let remoteUserInfoList: IUserInfo[] = userIdList.map((userId: string) => setDefaultUserInfo(userId));
|
||||
try {
|
||||
if (!tim) return remoteUserInfoList;
|
||||
|
||||
if (tim?.getFriendProfile) {
|
||||
// const res = await tim.getFriendProfile({ userIDList: userIdList });
|
||||
// if (res.code === 0) {
|
||||
// const { friendList = [], failureUserIDList = [] } = res.data;
|
||||
// let unFriendList: IUserInfo[] = failureUserIDList.map((obj: any) => obj.userID);
|
||||
// if (failureUserIDList.length > 0) {
|
||||
// const res = await tim.getUserProfile({ userIDList: failureUserIDList.map((obj: any) => obj.userID) });
|
||||
// if (res?.code === 0) {
|
||||
// unFriendList = res?.data || [];
|
||||
// }
|
||||
// }
|
||||
// const currentRemoteUserInfoList = TUIStore?.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // remoteUserInfoList may have been updated
|
||||
// const tempFriendIdList: string[] = friendList.map((obj: any) => obj.userID);
|
||||
// const tempUnFriendIdList: string[] = unFriendList.map((obj: any) => obj.userID);
|
||||
// remoteUserInfoList = userIdList.map((userId: string) => {
|
||||
// const defaultUserInfo: IUserInfo = setDefaultUserInfo(userId);
|
||||
// const friendListIndex: number = tempFriendIdList.indexOf(userId);
|
||||
// const unFriendListIndex: number = tempUnFriendIdList.indexOf(userId);
|
||||
// let remark = '';
|
||||
// let nick = '';
|
||||
// let displayUserInfo = '' ;
|
||||
// let avatar = '';
|
||||
// if (friendListIndex !== -1) {
|
||||
// remark = friendList[friendListIndex]?.remark || '';
|
||||
// nick = friendList[friendListIndex]?.profile?.nick || '';
|
||||
// displayUserInfo = remark || nick || defaultUserInfo.userId || '';
|
||||
// avatar = friendList[friendListIndex]?.profile?.avatar || '';
|
||||
// }
|
||||
// if (unFriendListIndex !== -1) {
|
||||
// nick = unFriendList[unFriendListIndex]?.nick || '';
|
||||
// displayUserInfo = nick || defaultUserInfo.userId || '';
|
||||
// avatar = unFriendList[unFriendListIndex]?.avatar || '';
|
||||
// }
|
||||
// const userInfo = currentRemoteUserInfoList.find(subObj => subObj.userId === userId) || {};
|
||||
// return { ...defaultUserInfo, ...userInfo, remark, nick, displayUserInfo, avatar };
|
||||
// });
|
||||
// }
|
||||
return remoteUserInfoList;
|
||||
} else {
|
||||
const res = await tim.getUserProfile({ userIDList: userIdList });
|
||||
const currentRemoteUserInfoList = TUIStore?.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // remoteUserInfoList may have been updated
|
||||
|
||||
remoteUserInfoList = userIdList.map((userId: string) => {
|
||||
const defaultUserInfo: IUserInfo = setDefaultUserInfo(userId);
|
||||
const userInfo = currentRemoteUserInfoList.find(subObj => subObj.userId === userId) || {};
|
||||
const userData = (res.data || []).find(obj => obj.userID === userId) || {};
|
||||
|
||||
return {
|
||||
...defaultUserInfo,
|
||||
...userInfo,
|
||||
nick: userData?.nick || '',
|
||||
displayUserInfo: userData?.nick || userId,
|
||||
avatar: userData?.avatar || '',
|
||||
};
|
||||
});
|
||||
return remoteUserInfoList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${NAME.PREFIX}getRemoteUserProfile failed, error: ${error}.`);
|
||||
return remoteUserInfoList;
|
||||
}
|
||||
}
|
||||
// 获取群组[offset, count + offset]区间成员
|
||||
// export async function getGroupMemberList(groupID: string, tim: any, count, offset) {
|
||||
// let groupMemberList = [];
|
||||
// try {
|
||||
// const res = await tim.getGroupMemberList({ groupID, count, offset });
|
||||
// if (res.code === 0) {
|
||||
// return res.data.memberList || groupMemberList;
|
||||
// }
|
||||
// } catch(error) {
|
||||
// console.error(`${NAME.PREFIX}getGroupMember failed, error: ${error}.`);
|
||||
// return groupMemberList;
|
||||
// }
|
||||
// }
|
||||
// 获取 IM 群信息
|
||||
// export async function getGroupProfile(groupID: string, tim: any): Promise<any> {
|
||||
// let groupProfile = {};
|
||||
// try {
|
||||
// const res = await tim.getGroupProfile({ groupID });
|
||||
// return res.data.group || groupProfile;
|
||||
// } catch(error) {
|
||||
// console.warn(`${NAME.PREFIX}getGroupProfile failed, error: ${error}.`);
|
||||
// return groupProfile;
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* web and miniProgram call engine throw event data structure are different
|
||||
* @param {any} event call engine throw out data
|
||||
* @returns {any} data
|
||||
*/
|
||||
export function analyzeEventData(event: any): any {
|
||||
return event || {};
|
||||
}
|
||||
/**
|
||||
* delete user from remoteUserInfoList
|
||||
* @param {string[]} userIdList to be deleted userIdList
|
||||
* @param {ITUIStore} TUIStore TUIStore instance
|
||||
*/
|
||||
export function deleteRemoteUser(userIdList: string[]): void {
|
||||
if (userIdList.length === 0) return;
|
||||
let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
|
||||
userIdList.forEach((userId) => {
|
||||
remoteUserInfoList = remoteUserInfoList.filter((obj: IUserInfo) => obj.userId !== userId);
|
||||
});
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
|
||||
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
||||
}
|
||||
/**
|
||||
* update the no device permission toast
|
||||
* @param {any} error error
|
||||
* @param {CallMediaType} type call midia type
|
||||
* @param {any} tuiCallEngine TUICallEngine instance
|
||||
*/
|
||||
export function noDevicePermissionToast(error, type: CallMediaType, tuiCallEngine: any) {
|
||||
let toastInfoKey = '';
|
||||
if (handleNoDevicePermissionError(error)) {
|
||||
if (type === CallMediaType.AUDIO) {
|
||||
toastInfoKey = CallTips.NO_MICROPHONE_DEVICE_PERMISSION;
|
||||
}
|
||||
if (type === CallMediaType.VIDEO) {
|
||||
toastInfoKey = CallTips.NO_CAMERA_DEVICE_PERMISSION;
|
||||
}
|
||||
|
||||
toastInfoKey && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: toastInfoKey, type: NAME.ERROR });
|
||||
console.error(`${NAME.PREFIX}call failed, error: ${error.message}.`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* set localUserInfo audio/video available
|
||||
* @param {boolean} isAvailable is available
|
||||
* @param {string} type callMediaType 'audio' | 'video'
|
||||
* @param {ITUIStore} TUIStore TUIStore instance
|
||||
*/
|
||||
export function setLocalUserInfoAudioVideoAvailable(isAvailable: boolean, type: string) {
|
||||
let localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
||||
if (type === NAME.AUDIO) {
|
||||
localUserInfo = { ...localUserInfo, isAudioAvailable: isAvailable };
|
||||
}
|
||||
if (type === NAME.VIDEO) {
|
||||
localUserInfo = { ...localUserInfo, isVideoAvailable: isAvailable };
|
||||
}
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, localUserInfo);
|
||||
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo);
|
||||
}
|
||||
Reference in New Issue
Block a user