signalbird 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ değil, kasıtlı bir duvardır — anahtar bir kez istemciye indiğinde herkesi
94
94
  | Go | `go get github.com/Pariette-Inc/signalbird.sdk` |
95
95
  | .NET, ASP.NET Core | `dotnet add package Signalbird.Sdk` |
96
96
  | Swift (iOS, macOS) | SPM: `https://github.com/Pariette-Inc/signalbird.sdk` |
97
- | Kotlin (Android) | `implementation("io.signalbird:signalbird-sdk:1.7.0")` |
97
+ | Kotlin (Android) | `implementation("io.signalbird:signalbird-sdk:1.8.1")` |
98
98
  | Canlı sohbet widget'ı (herhangi bir site) | `<script async src="https://signalbird.io/sdk/v1/signalbird.js" data-app-key="sbw_pub_…"></script>` |
99
99
 
100
100
  > Hepsi **bu repodan** çıkar ve **aynı sürümü** taşır — ayrı SDK reposu ya da
@@ -34,6 +34,12 @@ declare class SignalbirdService {
34
34
  markRead(): Promise<void>;
35
35
  closeConversation(rating?: number, comment?: string): Promise<void>;
36
36
  openSession(input: SessionInput): Promise<unknown>;
37
+ /**
38
+ * Konuşmayı bırakır; sonraki mesaj YENİ bir konuşma açar (29 Ağu 2026).
39
+ * Kapanmış sohbet geri açılmadığı için arayüzün "yeni sohbet" düğmesine
40
+ * bağlanacak bir eyleme ihtiyacı var.
41
+ */
42
+ resetConversation(): void;
37
43
  identify(input: IdentifyInput): Promise<unknown>;
38
44
  registerDevice(input: RegisterDeviceInput): Promise<unknown>;
39
45
  /** Uygulama kapanırken ya da testte: yoklamayı durdurur. */
package/dist/angular.d.ts CHANGED
@@ -34,6 +34,12 @@ declare class SignalbirdService {
34
34
  markRead(): Promise<void>;
35
35
  closeConversation(rating?: number, comment?: string): Promise<void>;
36
36
  openSession(input: SessionInput): Promise<unknown>;
37
+ /**
38
+ * Konuşmayı bırakır; sonraki mesaj YENİ bir konuşma açar (29 Ağu 2026).
39
+ * Kapanmış sohbet geri açılmadığı için arayüzün "yeni sohbet" düğmesine
40
+ * bağlanacak bir eyleme ihtiyacı var.
41
+ */
42
+ resetConversation(): void;
37
43
  identify(input: IdentifyInput): Promise<unknown>;
38
44
  registerDevice(input: RegisterDeviceInput): Promise<unknown>;
39
45
  /** Uygulama kapanırken ya da testte: yoklamayı durdurur. */
package/dist/angular.js CHANGED
@@ -57,6 +57,16 @@ var SignalbirdApp = class {
57
57
  bootstrap() {
58
58
  return this.request("POST", "/v1/sdk/bootstrap", { locale: this.config.locale });
59
59
  }
60
+ /**
61
+ * Canlı bağlantı kanalı için imza.
62
+ *
63
+ * Ziyaretçinin oturumu yoktur; hangi kanalı dinleyebileceğine SUNUCU karar
64
+ * verir ve yalnız kendi `visitor.<id>` kanalını imzalar. Soket servisi
65
+ * kimseyi tanımaz, yalnız imzayı doğrular.
66
+ */
67
+ socketAuth(socketId, channel) {
68
+ return this.request("POST", "/v1/sdk/chat/socket/auth", { socket_id: socketId, channel });
69
+ }
60
70
  /**
61
71
  * Ziyaretçi oturumu açar ya da mevcut olanı günceller.
62
72
  *
@@ -298,9 +308,163 @@ function buildQuery(query) {
298
308
  return encoded ? `?${encoded}` : "";
299
309
  }
300
310
 
311
+ // src/shared/socket.ts
312
+ var BACKOFF = [1e3, 2e3, 5e3, 1e4, 3e4];
313
+ var Socket = class {
314
+ constructor(config, auth, onEvent, onState, log = () => {
315
+ }) {
316
+ this.config = config;
317
+ this.auth = auth;
318
+ this.onEvent = onEvent;
319
+ this.onState = onState;
320
+ this.log = log;
321
+ this.ws = null;
322
+ this.sid = null;
323
+ this.attempt = 0;
324
+ this.closed = false;
325
+ this.timer = null;
326
+ this.pending = /* @__PURE__ */ new Set();
327
+ this.joined = /* @__PURE__ */ new Set();
328
+ }
329
+ get connected() {
330
+ return this.ws?.readyState === WebSocket.OPEN && this.sid !== null;
331
+ }
332
+ connect() {
333
+ if (!this.config.enabled || !this.config.url) return;
334
+ if (typeof WebSocket === "undefined") return;
335
+ if (this.ws) return;
336
+ this.closed = false;
337
+ const base = this.config.url.replace(/^http/, "ws").replace(/\/$/, "");
338
+ const url = `${base}/socket.io/?EIO=4&transport=websocket`;
339
+ try {
340
+ this.ws = new WebSocket(url);
341
+ } catch (e) {
342
+ this.log("socket open failed", e);
343
+ this.retry();
344
+ return;
345
+ }
346
+ this.ws.onmessage = (ev) => this.handle(String(ev.data));
347
+ this.ws.onclose = () => this.retry();
348
+ this.ws.onerror = () => {
349
+ this.log("socket error");
350
+ };
351
+ }
352
+ /** Kanala abone ol. Bağlantı yoksa kuyruğa alınır, kurulunca gönderilir. */
353
+ subscribe(channel) {
354
+ if (this.joined.has(channel) || this.pending.has(channel)) return;
355
+ this.pending.add(channel);
356
+ if (this.connected) void this.flush();
357
+ }
358
+ close() {
359
+ this.closed = true;
360
+ if (this.timer) clearTimeout(this.timer);
361
+ this.timer = null;
362
+ this.joined.clear();
363
+ try {
364
+ this.ws?.close();
365
+ } catch {
366
+ }
367
+ this.ws = null;
368
+ this.sid = null;
369
+ }
370
+ // ── Protokol ─────────────────────────────────────────────────────────
371
+ handle(frame) {
372
+ const type = frame[0];
373
+ if (type === "0") {
374
+ this.send("40");
375
+ return;
376
+ }
377
+ if (type === "2") {
378
+ this.send("3");
379
+ return;
380
+ }
381
+ if (type !== "4") return;
382
+ const sub = frame[1];
383
+ const body = frame.slice(2);
384
+ if (sub === "0") {
385
+ try {
386
+ this.sid = String(JSON.parse(body || "{}").sid || "");
387
+ } catch {
388
+ this.sid = "";
389
+ }
390
+ this.attempt = 0;
391
+ this.onState(true);
392
+ void this.flush();
393
+ return;
394
+ }
395
+ if (sub === "2") {
396
+ let parsed;
397
+ try {
398
+ parsed = JSON.parse(body);
399
+ } catch {
400
+ return;
401
+ }
402
+ if (!Array.isArray(parsed)) return;
403
+ const [name, data] = parsed;
404
+ if (typeof name === "string" && name.startsWith("chat.")) {
405
+ this.onEvent({ name, data: data ?? {} });
406
+ }
407
+ }
408
+ }
409
+ /**
410
+ * Bekleyen kanallar için imza al ve `subscribe` yolla.
411
+ *
412
+ * İmza her BAĞLANTIDA yeniden alınır: `socket_id`e bağlı ve zaman damgalı.
413
+ * Saklamak, ikinci bağlantıda sessizce reddedilmek demekti.
414
+ */
415
+ async flush() {
416
+ const sid = this.sid;
417
+ if (!sid) return;
418
+ for (const channel of Array.from(this.pending)) {
419
+ try {
420
+ const signed = await this.auth(sid, channel);
421
+ if (!signed) {
422
+ this.pending.delete(channel);
423
+ continue;
424
+ }
425
+ this.emit("subscribe", { channel, auth: signed.auth, at: signed.at });
426
+ this.pending.delete(channel);
427
+ this.joined.add(channel);
428
+ } catch (e) {
429
+ this.log("subscribe failed", channel, e);
430
+ }
431
+ }
432
+ }
433
+ /**
434
+ * Olay yolla — ACK İSTEMEDEN.
435
+ *
436
+ * `subscribe`in sonucunu beklemek bir tur daha protokol yönetmek demekti;
437
+ * oysa sonucu zaten davranıştan görüyoruz: imza tutmadıysa yayın gelmez ve
438
+ * polling merdiveni işini yapmaya devam eder.
439
+ */
440
+ emit(event, payload) {
441
+ this.send(`42${JSON.stringify([event, payload])}`);
442
+ }
443
+ send(frame) {
444
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
445
+ try {
446
+ this.ws.send(frame);
447
+ } catch {
448
+ }
449
+ }
450
+ retry() {
451
+ this.ws = null;
452
+ this.sid = null;
453
+ this.joined.forEach((c) => this.pending.add(c));
454
+ this.joined.clear();
455
+ this.onState(false);
456
+ if (this.closed) return;
457
+ const delay = BACKOFF[Math.min(this.attempt, BACKOFF.length - 1)];
458
+ this.attempt++;
459
+ if (this.timer) clearTimeout(this.timer);
460
+ this.timer = setTimeout(() => this.connect(), delay);
461
+ }
462
+ };
463
+
301
464
  // src/app/session.ts
302
465
  var IDLE_LADDER = [2e4, 2e4, 2e4, 6e4, 6e4, 18e4];
303
466
  var ACTIVE_INTERVAL = 3e3;
467
+ var ACTIVE_LIVE_INTERVAL = 45e3;
304
468
  var ChatSession = class {
305
469
  constructor(app, options = {}) {
306
470
  this.app = app;
@@ -314,13 +478,24 @@ var ChatSession = class {
314
478
  messages: [],
315
479
  unread: 0,
316
480
  agentTyping: false,
317
- withinHours: true
481
+ withinHours: true,
482
+ settings: null
318
483
  };
319
484
  this.listeners = /* @__PURE__ */ new Set();
320
485
  this.timer = null;
321
486
  this.step = 0;
322
487
  this.stopped = false;
323
488
  this.polling = false;
489
+ this.socket = null;
490
+ this.live = false;
491
+ /**
492
+ * Bir sonraki `refresh()` İMLEÇSİZ olsun mu.
493
+ *
494
+ * "Var olan mesaj değişti" haberi geldiğinde açılır: imleçli çekim
495
+ * (`?after=<son mesaj>`) o mesajı bir daha getirmez, dolayısıyla çeviri ya
496
+ * da düzenleme ekrana hiç yansımaz.
497
+ */
498
+ this.forceFull = false;
324
499
  this.active = options.active ?? false;
325
500
  }
326
501
  // ── Abonelik ──────────────────────────────────────────────────────────
@@ -345,8 +520,10 @@ var ChatSession = class {
345
520
  this.patch({
346
521
  enabled: true,
347
522
  withinHours: app.within_hours ?? true,
348
- topics: boot.data?.topics ?? []
523
+ topics: boot.data?.topics ?? [],
524
+ settings: app.chat ?? null
349
525
  });
526
+ this.openSocket(boot.data?.realtime);
350
527
  if (await this.app.currentVisitor()) {
351
528
  await this.refresh();
352
529
  }
@@ -358,19 +535,42 @@ var ChatSession = class {
358
535
  if (this.active === active) return;
359
536
  this.active = active;
360
537
  this.step = 0;
361
- if (active) void this.refresh();
538
+ if (active) {
539
+ if (this.state.conversation?.status === "closed") this.reset();
540
+ void this.refresh();
541
+ }
362
542
  this.schedule();
363
543
  }
544
+ /**
545
+ * Konuşmayı bırakır; sonraki mesaj YENİ bir konuşma açar.
546
+ *
547
+ * Ekranın "yeni sohbet" düğmesi de bunu çağırır. Sunucuda hiçbir şey
548
+ * silinmez — yalnız bu oturumun neye baktığı değişir.
549
+ */
550
+ reset() {
551
+ this.patch({ conversation: null, messages: [], unread: 0, agentTyping: false, errorCode: void 0 });
552
+ this.step = 0;
553
+ }
364
554
  stop() {
365
555
  this.stopped = true;
366
556
  if (this.timer) clearTimeout(this.timer);
367
557
  this.timer = null;
558
+ this.socket?.close();
559
+ this.socket = null;
560
+ this.live = false;
561
+ }
562
+ /** Canlı bağlantı kurulu mu — arayüz isterse gösterir (zorunlu değil). */
563
+ get isLive() {
564
+ return this.live;
368
565
  }
369
566
  // ── Eylemler ──────────────────────────────────────────────────────────
370
567
  /** Ön-form gönderildiğinde ya da uygulama kullanıcıyı tanıdığında. */
371
568
  async openSession(input) {
372
569
  const result = await this.app.startSession(input);
373
- if (result.ok) await this.refresh();
570
+ if (result.ok) {
571
+ void this.joinVisitorChannel();
572
+ await this.refresh();
573
+ }
374
574
  return result;
375
575
  }
376
576
  /**
@@ -397,6 +597,7 @@ var ChatSession = class {
397
597
  if (!await this.app.currentVisitor()) {
398
598
  const session = await this.app.startSession(this.options.visitor ?? {});
399
599
  if (!session.ok) return this.markFailed(cid, session);
600
+ void this.joinVisitorChannel();
400
601
  }
401
602
  const conversation = this.state.conversation;
402
603
  const result = conversation ? await this.app.sendMessage(conversation.id, { body: trimmed, client_id: cid, attachments }) : await this.app.startConversation({
@@ -447,7 +648,7 @@ var ChatSession = class {
447
648
  const current = this.state.conversation;
448
649
  if (!current) {
449
650
  const list = await this.app.listConversations();
450
- const first = list.data?.data?.[0];
651
+ const first = (list.data?.data ?? []).find((c) => c.status !== "closed");
451
652
  if (!first) {
452
653
  this.patch({ errorCode: list.ok ? void 0 : list.code });
453
654
  return;
@@ -456,7 +657,8 @@ var ChatSession = class {
456
657
  this.applyConversation(detail2.data?.conversation ?? first, true);
457
658
  return;
458
659
  }
459
- const after = this.lastServerMessageId();
660
+ const after = this.forceFull ? void 0 : this.lastServerMessageId();
661
+ this.forceFull = false;
460
662
  const detail = await this.app.getConversation(current.id, after ? { after } : void 0);
461
663
  if (!detail.ok || !detail.data?.conversation) {
462
664
  this.patch({ errorCode: detail.code });
@@ -497,10 +699,44 @@ var ChatSession = class {
497
699
  });
498
700
  return result;
499
701
  }
702
+ // ── Canlı bağlantı ────────────────────────────────────────────────────
703
+ /**
704
+ * Ziyaretçinin kendi kanalına bağlanır (`visitor.<id>`).
705
+ *
706
+ * Kanal ziyaretçi kimliği kurulduktan SONRA bilinir; ilk mesajla kimlik
707
+ * doğduğunda `refresh()` üzerinden yeniden denenir. Bağlantı kurulamazsa
708
+ * hiçbir şey olmaz: yoklama zaten çalışıyor.
709
+ */
710
+ openSocket(realtime) {
711
+ if (!realtime?.enabled || !realtime.url || this.socket) return;
712
+ this.socket = new Socket(
713
+ realtime,
714
+ async (socketId, channel) => {
715
+ const result = await this.app.socketAuth(socketId, channel);
716
+ return result.ok && result.data ? result.data : null;
717
+ },
718
+ (event) => {
719
+ if (event.data.updated === true) this.forceFull = true;
720
+ this.step = 0;
721
+ void this.refresh();
722
+ },
723
+ (connected) => {
724
+ this.live = connected;
725
+ this.schedule();
726
+ }
727
+ );
728
+ this.socket.connect();
729
+ void this.joinVisitorChannel();
730
+ }
731
+ /** Ziyaretçi kimliği varsa kendi kanalına katılır; yoksa sessizce döner. */
732
+ async joinVisitorChannel() {
733
+ const visitor = await this.app.currentVisitor();
734
+ if (visitor?.id) this.socket?.subscribe(`visitor.${visitor.id}`);
735
+ }
500
736
  schedule() {
501
737
  if (this.timer) clearTimeout(this.timer);
502
738
  if (this.stopped) return;
503
- const delay = this.active ? ACTIVE_INTERVAL : IDLE_LADDER[Math.min(this.step, IDLE_LADDER.length - 1)];
739
+ const delay = this.active ? this.live ? ACTIVE_LIVE_INTERVAL : ACTIVE_INTERVAL : this.live ? IDLE_LADDER[IDLE_LADDER.length - 1] : IDLE_LADDER[Math.min(this.step, IDLE_LADDER.length - 1)];
504
740
  this.timer = setTimeout(() => void this.tick(), delay);
505
741
  }
506
742
  async tick() {
@@ -590,6 +826,14 @@ var SignalbirdService = class {
590
826
  openSession(input) {
591
827
  return this.session.openSession(input);
592
828
  }
829
+ /**
830
+ * Konuşmayı bırakır; sonraki mesaj YENİ bir konuşma açar (29 Ağu 2026).
831
+ * Kapanmış sohbet geri açılmadığı için arayüzün "yeni sohbet" düğmesine
832
+ * bağlanacak bir eyleme ihtiyacı var.
833
+ */
834
+ resetConversation() {
835
+ this.session.reset();
836
+ }
593
837
  identify(input) {
594
838
  return this.client.identify(input);
595
839
  }