feat: enhance call and point registration features with sensor data integration
- Updated the CallWindow component to include connection quality states and reconnection attempts, improving user experience during calls. - Enhanced the ChatWindow to allow starting audio and video calls in a new window, providing users with more flexibility. - Integrated accelerometer and gyroscope data collection in the RegistroPonto component, enabling validation of point registration authenticity. - Improved error handling and user feedback for sensor permissions and data validation, ensuring a smoother registration process. - Updated backend logic to validate sensor data and adjust confidence scores for point registration, enhancing security against spoofing.
This commit is contained in:
200
apps/web/src/lib/utils/callWindowManager.ts
Normal file
200
apps/web/src/lib/utils/callWindowManager.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Utilitário para gerenciar abertura de CallWindow em nova janela
|
||||
*/
|
||||
|
||||
export interface CallWindowOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
features?: string;
|
||||
}
|
||||
|
||||
export interface CallWindowData {
|
||||
chamadaId: string;
|
||||
conversaId: string;
|
||||
tipo: 'audio' | 'video';
|
||||
roomName: string;
|
||||
ehAnfitriao: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<CallWindowOptions> = {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
left: undefined,
|
||||
top: undefined,
|
||||
features: 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes'
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcular posição centralizada da janela
|
||||
*/
|
||||
function calcularPosicaoCentralizada(width: number, height: number): { left: number; top: number } {
|
||||
const left = window.screenX + (window.outerWidth - width) / 2;
|
||||
const top = window.screenY + (window.outerHeight - height) / 2;
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
/**
|
||||
* Abrir CallWindow em nova janela do navegador
|
||||
*/
|
||||
export function abrirCallWindowEmPopup(
|
||||
data: CallWindowData,
|
||||
options: CallWindowOptions = {}
|
||||
): Window | null {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
|
||||
// Calcular posição se não fornecida
|
||||
let left = opts.left;
|
||||
let top = opts.top;
|
||||
|
||||
if (left === undefined || top === undefined) {
|
||||
const posicao = calcularPosicaoCentralizada(opts.width, opts.height);
|
||||
left = left ?? posicao.left;
|
||||
top = top ?? posicao.top;
|
||||
}
|
||||
|
||||
// Construir features da janela
|
||||
const features = [
|
||||
`width=${opts.width}`,
|
||||
`height=${opts.height}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
opts.features
|
||||
].join(',');
|
||||
|
||||
// Criar URL com dados da chamada
|
||||
const url = new URL('/call', window.location.origin);
|
||||
url.searchParams.set('chamadaId', data.chamadaId);
|
||||
url.searchParams.set('conversaId', data.conversaId);
|
||||
url.searchParams.set('tipo', data.tipo);
|
||||
url.searchParams.set('roomName', data.roomName);
|
||||
url.searchParams.set('ehAnfitriao', String(data.ehAnfitriao));
|
||||
|
||||
// Abrir janela
|
||||
const popup = window.open(url.toString(), `call-${data.chamadaId}`, features);
|
||||
|
||||
if (!popup) {
|
||||
console.error('Falha ao abrir popup. Verifique se o bloqueador de popups está desabilitado.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Focar na nova janela
|
||||
popup.focus();
|
||||
|
||||
// Configurar comunicação via postMessage
|
||||
configurarComunicacaoPopup(popup, data);
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurar comunicação entre janelas usando postMessage
|
||||
*/
|
||||
function configurarComunicacaoPopup(popup: Window, data: CallWindowData): void {
|
||||
// Listener para mensagens da janela popup
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
// Verificar origem
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar se a mensagem é da janela popup
|
||||
if (event.data?.source === 'call-window-popup') {
|
||||
switch (event.data.type) {
|
||||
case 'ready':
|
||||
console.log('CallWindow popup está pronto');
|
||||
break;
|
||||
case 'closed':
|
||||
console.log('CallWindow popup foi fechado');
|
||||
window.removeEventListener('message', messageHandler);
|
||||
break;
|
||||
case 'error':
|
||||
console.error('Erro na CallWindow popup:', event.data.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Detectar quando a janela é fechada
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
console.log('CallWindow popup foi fechado pelo usuário');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se popups estão habilitados
|
||||
*/
|
||||
export function verificarSuportePopup(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tentar abrir um popup de teste
|
||||
const testPopup = window.open('about:blank', '_blank', 'width=1,height=1');
|
||||
|
||||
if (!testPopup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fechar popup de teste
|
||||
testPopup.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter dados da chamada da URL (para uso na página de popup)
|
||||
*/
|
||||
export function obterDadosChamadaDaUrl(): CallWindowData | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const chamadaId = params.get('chamadaId');
|
||||
const conversaId = params.get('conversaId');
|
||||
const tipo = params.get('tipo');
|
||||
const roomName = params.get('roomName');
|
||||
const ehAnfitriao = params.get('ehAnfitriao');
|
||||
|
||||
if (!chamadaId || !conversaId || !tipo || !roomName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tipo !== 'audio' && tipo !== 'video') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
chamadaId,
|
||||
conversaId,
|
||||
tipo,
|
||||
roomName,
|
||||
ehAnfitriao: ehAnfitriao === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notificar janela pai sobre eventos
|
||||
*/
|
||||
export function notificarJanelaPai(type: string, data?: unknown): void {
|
||||
if (typeof window === 'undefined' || !window.opener) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.opener.postMessage(
|
||||
{
|
||||
source: 'call-window-popup',
|
||||
type,
|
||||
data
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { getLocalIP } from './browserInfo';
|
||||
|
||||
export interface DadosAcelerometro {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
movimentoDetectado: boolean;
|
||||
magnitude: number;
|
||||
variacao: number; // Variância entre leituras
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface DadosGiroscopio {
|
||||
alpha: number;
|
||||
beta: number;
|
||||
gamma: number;
|
||||
}
|
||||
|
||||
export interface InformacoesDispositivo {
|
||||
ipAddress?: string;
|
||||
ipPublico?: string;
|
||||
@@ -37,6 +53,10 @@ export interface InformacoesDispositivo {
|
||||
isDesktop?: boolean;
|
||||
connectionType?: string;
|
||||
memoryInfo?: string;
|
||||
acelerometro?: DadosAcelerometro;
|
||||
giroscopio?: DadosGiroscopio;
|
||||
sensorDisponivel?: boolean; // Indica se o sensor está disponível no dispositivo
|
||||
permissaoNegada?: boolean; // Indica se a permissão foi negada pelo usuário
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -637,6 +657,145 @@ async function obterIPPublico(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solicita permissão para acesso aos sensores de movimento (iOS 13+)
|
||||
*/
|
||||
async function solicitarPermissaoSensor(): Promise<PermissionState> {
|
||||
if (typeof DeviceMotionEvent === 'undefined' || typeof (DeviceMotionEvent as { requestPermission?: () => Promise<PermissionState> }).requestPermission !== 'function') {
|
||||
// Permissão não necessária ou já concedida (navegadores modernos)
|
||||
return 'granted';
|
||||
}
|
||||
|
||||
try {
|
||||
const requestPermission = (DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }).requestPermission;
|
||||
const resultado = await requestPermission();
|
||||
return resultado;
|
||||
} catch (error) {
|
||||
console.warn('Erro ao solicitar permissão de sensor:', error);
|
||||
return 'denied';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém dados de acelerômetro e giroscópio durante um período
|
||||
* @param duracaoMs Duração da coleta em milissegundos (padrão: 5000ms = 5 segundos)
|
||||
*/
|
||||
async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
|
||||
acelerometro?: DadosAcelerometro;
|
||||
giroscopio?: DadosGiroscopio;
|
||||
sensorDisponivel: boolean;
|
||||
permissaoNegada: boolean;
|
||||
}> {
|
||||
// Verificar se DeviceMotionEvent está disponível
|
||||
if (typeof DeviceMotionEvent === 'undefined' || typeof DeviceOrientationEvent === 'undefined') {
|
||||
return {
|
||||
sensorDisponivel: false,
|
||||
permissaoNegada: false
|
||||
};
|
||||
}
|
||||
|
||||
// Solicitar permissão (especialmente necessário no iOS 13+)
|
||||
const permissao = await solicitarPermissaoSensor();
|
||||
|
||||
if (permissao === 'denied') {
|
||||
return {
|
||||
sensorDisponivel: true,
|
||||
permissaoNegada: true
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const leiturasAcelerometro: Array<{ x: number; y: number; z: number; timestamp: number }> = [];
|
||||
const leiturasGiroscopio: Array<{ alpha: number; beta: number; gamma: number; timestamp: number }> = [];
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.removeEventListener('devicemotion', handleDeviceMotion);
|
||||
window.removeEventListener('deviceorientation', handleDeviceOrientation);
|
||||
|
||||
// Processar dados de acelerômetro
|
||||
let acelerometro: DadosAcelerometro | undefined;
|
||||
if (leiturasAcelerometro.length > 0) {
|
||||
const ultimaLeitura = leiturasAcelerometro[leiturasAcelerometro.length - 1]!;
|
||||
|
||||
// Calcular magnitude média
|
||||
const magnitudes = leiturasAcelerometro.map(l =>
|
||||
Math.sqrt(l.x * l.x + l.y * l.y + l.z * l.z)
|
||||
);
|
||||
const magnitude = magnitudes.reduce((sum, m) => sum + m, 0) / magnitudes.length;
|
||||
|
||||
// Calcular variância para detectar movimento
|
||||
const mediaX = leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
|
||||
const mediaY = leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
|
||||
const mediaZ = leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
|
||||
|
||||
const variacoes = leiturasAcelerometro.map(l =>
|
||||
Math.pow(l.x - mediaX, 2) + Math.pow(l.y - mediaY, 2) + Math.pow(l.z - mediaZ, 2)
|
||||
);
|
||||
const variacao = variacoes.reduce((sum, v) => sum + v, 0) / variacoes.length;
|
||||
|
||||
// Detectar movimento: se variância > 0.01, há movimento
|
||||
const movimentoDetectado = variacao > 0.01;
|
||||
|
||||
acelerometro = {
|
||||
x: ultimaLeitura.x,
|
||||
y: ultimaLeitura.y,
|
||||
z: ultimaLeitura.z,
|
||||
movimentoDetectado,
|
||||
magnitude,
|
||||
variacao,
|
||||
timestamp: ultimaLeitura.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Processar dados de giroscópio
|
||||
let giroscopio: DadosGiroscopio | undefined;
|
||||
if (leiturasGiroscopio.length > 0) {
|
||||
const ultimaLeitura = leiturasGiroscopio[leiturasGiroscopio.length - 1]!;
|
||||
giroscopio = {
|
||||
alpha: ultimaLeitura.alpha || 0,
|
||||
beta: ultimaLeitura.beta || 0,
|
||||
gamma: ultimaLeitura.gamma || 0
|
||||
};
|
||||
}
|
||||
|
||||
resolve({
|
||||
acelerometro,
|
||||
giroscopio,
|
||||
sensorDisponivel: true,
|
||||
permissaoNegada: false
|
||||
});
|
||||
}, duracaoMs);
|
||||
|
||||
function handleDeviceMotion(event: DeviceMotionEvent) {
|
||||
if (event.accelerationIncludingGravity) {
|
||||
const acc = event.accelerationIncludingGravity;
|
||||
if (acc.x !== null && acc.y !== null && acc.z !== null) {
|
||||
leiturasAcelerometro.push({
|
||||
x: acc.x,
|
||||
y: acc.y,
|
||||
z: acc.z,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeviceOrientation(event: DeviceOrientationEvent) {
|
||||
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
|
||||
leiturasGiroscopio.push({
|
||||
alpha: event.alpha,
|
||||
beta: event.beta,
|
||||
gamma: event.gamma,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('devicemotion', handleDeviceMotion);
|
||||
window.addEventListener('deviceorientation', handleDeviceOrientation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém todas as informações do dispositivo
|
||||
*/
|
||||
@@ -675,13 +834,14 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
|
||||
informacoes.screenResolution = tela.screenResolution;
|
||||
informacoes.coresTela = tela.coresTela;
|
||||
|
||||
// Informações de conexão e memória (assíncronas)
|
||||
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao] = await Promise.all([
|
||||
// Informações de conexão, memória e localização (assíncronas)
|
||||
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] = await Promise.all([
|
||||
obterInformacoesConexao(),
|
||||
Promise.resolve(obterInformacoesMemoria()),
|
||||
obterIPPublico(),
|
||||
getLocalIP(),
|
||||
obterLocalizacao(),
|
||||
obterDadosAcelerometro(5000), // Coletar dados por 5 segundos
|
||||
]);
|
||||
|
||||
informacoes.connectionType = connectionType;
|
||||
@@ -703,6 +863,12 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
|
||||
informacoes.estado = localizacao.estado;
|
||||
informacoes.pais = localizacao.pais;
|
||||
|
||||
// Dados de sensores
|
||||
informacoes.acelerometro = dadosSensores.acelerometro;
|
||||
informacoes.giroscopio = dadosSensores.giroscopio;
|
||||
informacoes.sensorDisponivel = dadosSensores.sensorDisponivel;
|
||||
informacoes.permissaoNegada = dadosSensores.permissaoNegada;
|
||||
|
||||
// IP address (usar público se disponível, senão local)
|
||||
informacoes.ipAddress = ipPublico || ipLocal;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user