Add handoff task document for PWA, chats, and connections

This commit is contained in:
ai5590
2026-04-05 12:13:05 +03:00
parent cf5460c5c7
commit bc8f4a0582
46 changed files with 1671 additions and 106 deletions
+48
View File
@@ -235,6 +235,54 @@ 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 addCloseFriend(toLogin) {
const response = await this.ws.request('AddCloseFriend', { toLogin });
if (response.status !== 200) throw opError('AddCloseFriend', 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);
+51
View File
@@ -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
}
}
+29 -1
View File
@@ -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)