SHA256
Add WS push events, PWA/FCM scaffolding, and direct messaging MVP
This commit is contained in:
@@ -235,6 +235,47 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
|
||||
onEvent(op, handler) {
|
||||
return this.ws.onEvent(op, handler);
|
||||
}
|
||||
|
||||
async upsertPushToken({ tokenId, token, provider = 'fcm', platform = 'web', userAgent = navigator.userAgent || '' }) {
|
||||
const response = await this.ws.request('UpsertPushToken', { tokenId, token, provider, platform, userAgent });
|
||||
if (response.status !== 200) throw opError('UpsertPushToken', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async sendDirectMessage(toLogin, text) {
|
||||
const response = await this.ws.request('SendDirectMessage', { toLogin, text });
|
||||
if (response.status !== 200) throw opError('SendDirectMessage', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async ackIncomingMessage(eventId, messageId) {
|
||||
const response = await this.ws.request('AckIncomingMessage', { eventId, messageId });
|
||||
if (response.status !== 200) throw opError('AckIncomingMessage', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async listContacts() {
|
||||
const response = await this.ws.request('ListContacts', {});
|
||||
if (response.status !== 200) throw opError('ListContacts', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async getUserConnectionsGraph(login) {
|
||||
const response = await this.ws.request('GetUserConnectionsGraph', { login });
|
||||
if (response.status !== 200) throw opError('GetUserConnectionsGraph', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async searchUsers(prefix) {
|
||||
const response = await this.ws.request('SearchUsers', { prefix });
|
||||
if (response.status !== 200) throw opError('SearchUsers', response);
|
||||
return response.payload?.logins || [];
|
||||
}
|
||||
|
||||
async reportClientError(details) {
|
||||
try {
|
||||
const response = await this.ws.request('ClientErrorLog', details || {}, 3000);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const LS_KEY = 'shine-ui-fcm-token-v1';
|
||||
|
||||
export async function initPwaPush({ authService }) {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
try {
|
||||
await navigator.serviceWorker.register('./firebase-messaging-sw.js');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.firebase || !window.firebase.messaging) return;
|
||||
|
||||
try {
|
||||
const config = window.__SHINE_FIREBASE_CONFIG__ || null;
|
||||
if (!config) return;
|
||||
if (!window.firebase.apps.length) {
|
||||
window.firebase.initializeApp(config);
|
||||
}
|
||||
const messaging = window.firebase.messaging();
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return;
|
||||
|
||||
const vapidKey = window.__SHINE_FIREBASE_VAPID_KEY__ || '';
|
||||
const token = await messaging.getToken({ vapidKey });
|
||||
if (!token) return;
|
||||
|
||||
const prev = localStorage.getItem(LS_KEY);
|
||||
if (prev === token) return;
|
||||
|
||||
localStorage.setItem(LS_KEY, token);
|
||||
const tokenId = `tok-${new Date().toISOString().replace(/[-:.TZ]/g, '')}-${Math.random().toString(36).slice(2, 12)}`;
|
||||
await authService.upsertPushToken({
|
||||
tokenId,
|
||||
token,
|
||||
provider: 'fcm',
|
||||
platform: 'web',
|
||||
userAgent: navigator.userAgent || '',
|
||||
});
|
||||
|
||||
messaging.onMessage((payload) => {
|
||||
const title = payload?.notification?.title || 'Новое сообщение';
|
||||
const body = payload?.notification?.body || '';
|
||||
try {
|
||||
new Notification(title, { body });
|
||||
} catch {}
|
||||
});
|
||||
} catch {
|
||||
// silent for MVP
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export class WsJsonClient {
|
||||
this.ws = null;
|
||||
this.pending = new Map();
|
||||
this.openPromise = null;
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
async open() {
|
||||
@@ -53,6 +54,7 @@ export class WsJsonClient {
|
||||
});
|
||||
}).finally(() => {
|
||||
this.openPromise = null;
|
||||
this.eventListeners = new Map();
|
||||
});
|
||||
|
||||
return this.openPromise;
|
||||
@@ -116,14 +118,40 @@ export class WsJsonClient {
|
||||
}
|
||||
|
||||
const requestId = data?.requestId;
|
||||
if (!requestId) return;
|
||||
const isEvent = data?.event === true || (requestId && !this.pending.has(requestId));
|
||||
if (isEvent) {
|
||||
this.emitEvent(data?.op || '', data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!requestId) return;
|
||||
const slot = this.pending.get(requestId);
|
||||
if (!slot) return;
|
||||
this.pending.delete(requestId);
|
||||
slot.resolve(data);
|
||||
}
|
||||
|
||||
onEvent(op, callback) {
|
||||
if (!op || typeof callback !== 'function') return () => {};
|
||||
if (!this.eventListeners.has(op)) {
|
||||
this.eventListeners.set(op, new Set());
|
||||
}
|
||||
const set = this.eventListeners.get(op);
|
||||
set.add(callback);
|
||||
return () => {
|
||||
set.delete(callback);
|
||||
if (!set.size) this.eventListeners.delete(op);
|
||||
};
|
||||
}
|
||||
|
||||
emitEvent(op, data) {
|
||||
const listeners = this.eventListeners.get(op);
|
||||
if (!listeners) return;
|
||||
listeners.forEach((cb) => {
|
||||
try { cb(data); } catch {}
|
||||
});
|
||||
}
|
||||
|
||||
failPending(message) {
|
||||
const pendingOps = [...this.pending.values()]
|
||||
.map((slot) => slot.op)
|
||||
|
||||
Reference in New Issue
Block a user