stream-chat 9.5.1 → 9.6.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.
@@ -10,6 +10,7 @@ export * from './insights';
10
10
  export * from './messageComposer';
11
11
  export * from './middleware';
12
12
  export * from './moderation';
13
+ export * from './notifications';
13
14
  export * from './pagination';
14
15
  export * from './permissions';
15
16
  export * from './poll';
@@ -29,9 +29,10 @@ export type LocalMessageWithLegacyThreadId = LocalMessage & {
29
29
  export type CompositionContext = Channel | Thread | LocalMessageWithLegacyThreadId;
30
30
  export type MessageComposerState = {
31
31
  id: string;
32
- quotedMessage: LocalMessageBase | null;
33
- pollId: string | null;
34
32
  draftId: string | null;
33
+ pollId: string | null;
34
+ quotedMessage: LocalMessageBase | null;
35
+ showReplyInChannel: boolean;
35
36
  };
36
37
  export type MessageComposerOptions = {
37
38
  client: StreamChat;
@@ -54,12 +55,12 @@ export declare class MessageComposer extends WithSubscriptions {
54
55
  pollComposer: PollComposer;
55
56
  customDataManager: CustomDataManager;
56
57
  constructor({ composition, config, compositionContext, client, }: MessageComposerOptions);
57
- static evaluateContextType(compositionContext: CompositionContext): "channel" | "thread" | "legacy_thread" | "message";
58
+ static evaluateContextType(compositionContext: CompositionContext): "message" | "channel" | "thread" | "legacy_thread";
58
59
  static constructTag(compositionContext: CompositionContext): `${ReturnType<typeof MessageComposer.evaluateContextType>}_${string}`;
59
60
  get config(): MessageComposerConfig;
60
61
  updateConfig(config: DeepPartial<MessageComposerConfig>): void;
61
- get contextType(): "channel" | "thread" | "legacy_thread" | "message";
62
- get tag(): `channel_${string}` | `thread_${string}` | `legacy_thread_${string}` | `message_${string}`;
62
+ get contextType(): "message" | "channel" | "thread" | "legacy_thread";
63
+ get tag(): `message_${string}` | `channel_${string}` | `thread_${string}` | `legacy_thread_${string}`;
63
64
  get threadId(): string | null;
64
65
  get client(): StreamChat;
65
66
  get id(): string;
@@ -67,6 +68,7 @@ export declare class MessageComposer extends WithSubscriptions {
67
68
  get lastChange(): LastComposerChange;
68
69
  get quotedMessage(): LocalMessageBase | null;
69
70
  get pollId(): string | null;
71
+ get showReplyInChannel(): boolean;
70
72
  get hasSendableData(): boolean;
71
73
  get compositionIsEmpty(): boolean;
72
74
  get lastChangeOriginIsLocal(): boolean;
@@ -91,6 +93,7 @@ export declare class MessageComposer extends WithSubscriptions {
91
93
  private subscribeMessageComposerStateChanged;
92
94
  private subscribeMessageComposerConfigStateChanged;
93
95
  setQuotedMessage: (quotedMessage: LocalMessage | null) => void;
96
+ toggleShowReplyInChannel: () => void;
94
97
  clear: () => void;
95
98
  restore: () => void;
96
99
  compose: () => Promise<MessageComposerMiddlewareValue["state"] | undefined>;
@@ -0,0 +1,3 @@
1
+ import type { MessageComposer } from '../../messageComposer';
2
+ import type { MessageCompositionMiddleware } from './types';
3
+ export declare const createUserDataInjectionMiddleware: (composer: MessageComposer) => MessageCompositionMiddleware;
@@ -0,0 +1,2 @@
1
+ import type { NotificationManagerConfig } from './types';
2
+ export declare const DEFAULT_NOTIFICATION_MANAGER_CONFIG: NotificationManagerConfig;
@@ -28,38 +28,77 @@ export type Notification = {
28
28
  * The identifier then can be recognized by notification consumers to act upon specific origin values.
29
29
  */
30
30
  origin: NotificationOrigin;
31
- /** Optional timestamp when notification should expire */
32
- expiresAt?: number;
33
- /** Whether notification should automatically close after duration. Defaults to true */
34
- autoClose?: boolean;
35
31
  /** Array of action buttons for the notification */
36
32
  actions?: NotificationAction[];
33
+ /**
34
+ * Optional code that can be used to group the notifications of the same type, e.g. attachment-upload-blocked.
35
+ * Format: domain:entity:operation:result
36
+ * domain: where the error occurred (api, validation, permission, etc)
37
+ * entity: what was being operated on (poll, attachment, message, etc)
38
+ * operation: what was being attempted (create, upload, validate, etc)
39
+ * result: what happened (failed, blocked, invalid, etc)
40
+ *
41
+ * Poll related errors
42
+ * 'api:poll:create:failed' // API call to create poll failed
43
+ * 'validation:poll:create:invalid' // Poll creation validation failed
44
+ *
45
+ * Attachment related errors
46
+ * 'validation:attachment:file:missing' // Required file is missing
47
+ * 'permission:attachment:upload:blocked' // Upload blocked due to permissions
48
+ * 'api:attachment:upload:failed' // API upload call failed
49
+ * 'validation:attachment:type:unsupported' // Unsupported file type
50
+ * 'validation:attachment:size:exceeded' // File size too large
51
+ * 'validation:attachment:count:exceeded' // Too many attachments
52
+ *
53
+ * Message related errors
54
+ * 'api:message:send:failed' // Message send failed
55
+ * 'validation:message:content:empty' // Message content validation failed
56
+ *
57
+ * Channel related errors
58
+ * 'api:channel:join:failed' // Channel join failed
59
+ * 'permission:channel:access:denied' // Channel access denied
60
+ *
61
+ * Authentication related errors
62
+ * 'auth:token:expired' // Auth token expired
63
+ * 'auth:token:invalid' // Invalid auth token
64
+ *
65
+ * Network related errors
66
+ * 'network:request:timeout' // Request timed out
67
+ * 'network:request:failed' // Network request failed
68
+ *
69
+ * Rate limiting
70
+ * 'rate:limit:exceeded' // Rate limit exceeded
71
+ *
72
+ * System errors
73
+ * 'system:internal:error' // Internal system error
74
+ * 'system:resource:unavailable'; // System resource unavailable
75
+ */
76
+ type?: string;
77
+ /** Optional timestamp when notification should expire */
78
+ expiresAt?: number;
37
79
  /** Optional metadata to attach to the notification */
38
80
  metadata?: Record<string, unknown>;
81
+ /** In case of error notification the instance of the originally thrown error */
82
+ originalError?: Error;
39
83
  };
40
84
  /** Configuration options when creating a notification */
41
- export type NotificationOptions = {
42
- /** The severity level. Defaults to 'info' */
43
- severity?: NotificationSeverity;
44
- /** How long notification should display in milliseconds */
85
+ export type NotificationOptions = Partial<Pick<Notification, 'type' | 'severity' | 'actions' | 'metadata' | 'originalError'>> & {
86
+ /** How long a notification should be displayed in milliseconds */
45
87
  duration?: number;
46
- /** Whether notification should auto-close after duration. Defaults to true */
47
- autoClose?: boolean;
48
- /** Array of action buttons for the notification */
49
- actions?: NotificationAction[];
50
- /** Optional metadata to attach to the notification */
51
- metadata?: Record<string, unknown>;
52
88
  };
53
- /** State shape for the notification store */
89
+ /**
90
+ * State shape for the notification store
91
+ * @deprcated use NotificationManagerState
92
+ */
54
93
  export type NotificationState = {
55
94
  /** Array of current notification objects */
56
95
  notifications: Notification[];
57
96
  };
97
+ /** State shape for the notification store */
98
+ export type NotificationManagerState = NotificationState;
58
99
  export type NotificationManagerConfig = {
59
100
  durations: Record<NotificationSeverity, number>;
60
101
  };
61
- export type AddNotificationPayload = {
62
- message: string;
63
- origin: NotificationOrigin;
102
+ export type AddNotificationPayload = Pick<Notification, 'message' | 'origin'> & {
64
103
  options?: NotificationOptions;
65
104
  };
@@ -11,7 +11,7 @@ export declare class PollManager extends WithSubscriptions {
11
11
  get data(): Map<string, Poll>;
12
12
  fromState: (id: string) => Poll | undefined;
13
13
  registerSubscriptions: () => void;
14
- createPoll: (poll: CreatePollData) => Promise<Poll>;
14
+ createPoll: (poll: CreatePollData) => Promise<Poll | undefined>;
15
15
  getPoll: (id: string) => Promise<Poll | undefined>;
16
16
  queryPolls: (filter: QueryPollsFilters, sort?: PollSort, options?: QueryPollsOptions) => Promise<{
17
17
  polls: (Poll | undefined)[];
@@ -10,14 +10,19 @@ export declare abstract class WithSubscriptions {
10
10
  * overriding `unregisterSubscriptions` call the base method and return
11
11
  * its unique symbol value.
12
12
  */
13
- private static symbol;
13
+ protected static symbol: symbol;
14
+ private refCount;
14
15
  abstract registerSubscriptions(): void;
15
16
  /**
16
17
  * Returns a boolean, provides information of whether `registerSubscriptions`
17
18
  * method has already been called for this instance.
18
19
  */
19
20
  get hasSubscriptions(): boolean;
20
- addUnsubscribeFunction(unsubscribeFunction: Unsubscribe): void;
21
+ protected addUnsubscribeFunction(unsubscribeFunction: Unsubscribe): void;
22
+ /**
23
+ * Increments `refCount` by one and returns new value.
24
+ */
25
+ protected incrementRefCount(): number;
21
26
  /**
22
27
  * If you re-declare `unregisterSubscriptions` method within your class
23
28
  * make sure to run the original too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.5.1",
3
+ "version": "9.6.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './insights';
10
10
  export * from './messageComposer';
11
11
  export * from './middleware';
12
12
  export * from './moderation';
13
+ export * from './notifications';
13
14
  export * from './pagination';
14
15
  export * from './permissions';
15
16
  export * from './poll';
@@ -386,12 +386,11 @@ export class AttachmentManager {
386
386
  this.client.notifications.addError({
387
387
  message: 'File is required for upload attachment',
388
388
  origin: { emitter: 'AttachmentManager', context: { attachment } },
389
+ options: { type: 'validation:attachment:file:missing' },
389
390
  });
390
391
  return;
391
392
  }
392
393
 
393
- // todo: document this
394
- // the following is substitute for: if (noFiles && !isImage) return att
395
394
  if (!this.fileUploadFilter(attachment)) return;
396
395
 
397
396
  const newAttachment = await this.fileToLocalUploadAttachment(
@@ -453,8 +452,17 @@ export class AttachmentManager {
453
452
  if (localAttachment.localMetadata.uploadState === 'blocked') {
454
453
  this.upsertAttachments([localAttachment]);
455
454
  this.client.notifications.addError({
456
- message: 'Error uploading attachment',
457
- origin: { emitter: 'AttachmentManager', context: { attachment } },
455
+ message: `The attachment upload was blocked`,
456
+ origin: {
457
+ emitter: 'AttachmentManager',
458
+ context: { attachment, blockedAttachment: localAttachment },
459
+ },
460
+ options: {
461
+ type: 'validation:attachment:upload:blocked',
462
+ metadata: {
463
+ reason: localAttachment.localMetadata.uploadPermissionCheck?.reason,
464
+ },
465
+ },
458
466
  });
459
467
  return localAttachment;
460
468
  }
@@ -473,21 +481,7 @@ export class AttachmentManager {
473
481
  try {
474
482
  response = await this.doUploadRequest(localAttachment.localMetadata.file);
475
483
  } catch (error) {
476
- let finalError: Error = {
477
- message: 'Error uploading attachment',
478
- name: 'Error',
479
- };
480
- if (typeof (error as Error).message === 'string') {
481
- finalError = error as Error;
482
- } else if (typeof error === 'object') {
483
- finalError = Object.assign(finalError, error);
484
- }
485
-
486
- this.client.notifications.addError({
487
- message: finalError.message,
488
- origin: { emitter: 'AttachmentManager', context: { attachment } },
489
- });
490
-
484
+ const reason = error instanceof Error ? error.message : 'unknown error';
491
485
  const failedAttachment: LocalUploadAttachment = {
492
486
  ...attachment,
493
487
  localMetadata: {
@@ -496,6 +490,19 @@ export class AttachmentManager {
496
490
  },
497
491
  };
498
492
 
493
+ this.client.notifications.addError({
494
+ message: 'Error uploading attachment',
495
+ origin: {
496
+ emitter: 'AttachmentManager',
497
+ context: { attachment, failedAttachment },
498
+ },
499
+ options: {
500
+ type: 'api:attachment:upload:failed',
501
+ metadata: { reason },
502
+ originalError: error instanceof Error ? error : undefined,
503
+ },
504
+ });
505
+
499
506
  this.updateAttachment(failedAttachment);
500
507
  return failedAttachment;
501
508
  }
@@ -42,9 +42,10 @@ export type CompositionContext = Channel | Thread | LocalMessageWithLegacyThread
42
42
 
43
43
  export type MessageComposerState = {
44
44
  id: string;
45
- quotedMessage: LocalMessageBase | null;
46
- pollId: string | null;
47
45
  draftId: string | null;
46
+ pollId: string | null;
47
+ quotedMessage: LocalMessageBase | null;
48
+ showReplyInChannel: boolean;
48
49
  };
49
50
 
50
51
  export type MessageComposerOptions = {
@@ -82,10 +83,11 @@ const initState = (
82
83
  ): MessageComposerState => {
83
84
  if (!composition) {
84
85
  return {
86
+ draftId: null,
85
87
  id: MessageComposer.generateId(),
86
- quotedMessage: null,
87
88
  pollId: null,
88
- draftId: null,
89
+ quotedMessage: null,
90
+ showReplyInChannel: false,
89
91
  };
90
92
  }
91
93
 
@@ -104,15 +106,14 @@ const initState = (
104
106
  return {
105
107
  draftId,
106
108
  id,
109
+ pollId: message.poll_id ?? null,
107
110
  quotedMessage: quotedMessage
108
111
  ? formatMessage(quotedMessage as MessageResponseBase)
109
112
  : null,
110
- pollId: message.poll_id ?? null,
113
+ showReplyInChannel: false,
111
114
  };
112
115
  };
113
116
 
114
- const noop = () => undefined;
115
-
116
117
  export class MessageComposer extends WithSubscriptions {
117
118
  readonly channel: Channel;
118
119
  readonly state: StateStore<MessageComposerState>;
@@ -274,6 +275,10 @@ export class MessageComposer extends WithSubscriptions {
274
275
  return this.state.getLatestValue().pollId;
275
276
  }
276
277
 
278
+ get showReplyInChannel() {
279
+ return this.state.getLatestValue().showReplyInChannel;
280
+ }
281
+
277
282
  get hasSendableData() {
278
283
  return !!(
279
284
  (!this.attachmentManager.uploadsInProgressCount &&
@@ -356,23 +361,23 @@ export class MessageComposer extends WithSubscriptions {
356
361
  }
357
362
 
358
363
  public registerSubscriptions = (): UnregisterSubscriptions => {
359
- if (this.hasSubscriptions) {
360
- // Already listening for events and changes
361
- return noop;
364
+ if (!this.hasSubscriptions) {
365
+ this.addUnsubscribeFunction(this.subscribeMessageComposerSetupStateChange());
366
+ this.addUnsubscribeFunction(this.subscribeMessageUpdated());
367
+ this.addUnsubscribeFunction(this.subscribeMessageDeleted());
368
+
369
+ this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
370
+ this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
371
+ this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
372
+ this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
373
+ this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
374
+ this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
375
+ this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
362
376
  }
363
- this.addUnsubscribeFunction(this.subscribeMessageComposerSetupStateChange());
364
- this.addUnsubscribeFunction(this.subscribeMessageUpdated());
365
- this.addUnsubscribeFunction(this.subscribeMessageDeleted());
366
-
367
- this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
368
- this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
369
- this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
370
- this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
371
- this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
372
- this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
373
- this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
374
-
375
- return this.unregisterSubscriptions.bind(this);
377
+
378
+ this.incrementRefCount();
379
+
380
+ return () => this.unregisterSubscriptions();
376
381
  };
377
382
 
378
383
  private subscribeMessageUpdated = () => {
@@ -580,6 +585,10 @@ export class MessageComposer extends WithSubscriptions {
580
585
  this.state.partialNext({ quotedMessage });
581
586
  };
582
587
 
588
+ toggleShowReplyInChannel = () => {
589
+ this.state.partialNext({ showReplyInChannel: !this.showReplyInChannel });
590
+ };
591
+
583
592
  clear = () => {
584
593
  this.initState();
585
594
  };
@@ -664,16 +673,22 @@ export class MessageComposer extends WithSubscriptions {
664
673
  const composition = await this.pollComposer.compose();
665
674
  if (!composition || !composition.data.id) return;
666
675
  try {
667
- const { poll } = await this.client.createPoll(composition.data);
668
- this.state.partialNext({ pollId: poll.id });
669
- this.pollComposer.initState();
676
+ const poll = await this.client.polls.createPoll(composition.data);
677
+ this.state.partialNext({ pollId: poll?.id });
670
678
  } catch (error) {
671
- this.client.notifications.add({
679
+ this.client.notifications.addError({
672
680
  message: 'Failed to create the poll',
673
681
  origin: {
674
682
  emitter: 'MessageComposer',
675
683
  context: { composer: this },
676
684
  },
685
+ options: {
686
+ type: 'api:poll:create:failed',
687
+ metadata: {
688
+ reason: (error as Error).message,
689
+ },
690
+ originalError: error instanceof Error ? error : undefined,
691
+ },
677
692
  });
678
693
  throw error;
679
694
  }
@@ -30,6 +30,7 @@ import {
30
30
  createCustomDataCompositionMiddleware,
31
31
  createDraftCustomDataCompositionMiddleware,
32
32
  } from './customData';
33
+ import { createUserDataInjectionMiddleware } from './userDataInjection';
33
34
  import { createPollOnlyCompositionMiddleware } from './pollOnly';
34
35
 
35
36
  export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
@@ -41,6 +42,7 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
41
42
  // todo: document how to add custom data to a composed message using middleware
42
43
  // or adding custom composer components (apart from AttachmentsManager, TextComposer etc.)
43
44
  this.use([
45
+ createUserDataInjectionMiddleware(composer),
44
46
  createPollOnlyCompositionMiddleware(composer),
45
47
  createTextComposerCompositionMiddleware(composer),
46
48
  createAttachmentsCompositionMiddleware(composer),
@@ -29,7 +29,6 @@ export const createCompositionDataCleanupMiddleware = (
29
29
  ...composer.editedMessage,
30
30
  ...state.localMessage,
31
31
  ...common,
32
- user: composer.client.user,
33
32
  }),
34
33
  message: {
35
34
  ...editedMessagePayloadToBeSent,
@@ -17,7 +17,10 @@ export const createMessageComposerStateCompositionMiddleware = (
17
17
  state,
18
18
  next,
19
19
  }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
20
- const payload: Pick<LocalMessage, 'poll_id' | 'quoted_message_id'> = {};
20
+ const payload: Pick<
21
+ LocalMessage,
22
+ 'poll_id' | 'quoted_message_id' | 'show_in_channel'
23
+ > = {};
21
24
  if (composer.quotedMessage) {
22
25
  payload.quoted_message_id = composer.quotedMessage.id;
23
26
  }
@@ -25,6 +28,10 @@ export const createMessageComposerStateCompositionMiddleware = (
25
28
  payload.poll_id = composer.pollId;
26
29
  }
27
30
 
31
+ if (composer.showReplyInChannel) {
32
+ payload.show_in_channel = true;
33
+ }
34
+
28
35
  return next({
29
36
  ...state,
30
37
  localMessage: {
@@ -50,7 +57,10 @@ export const createDraftMessageComposerStateCompositionMiddleware = (
50
57
  state,
51
58
  next,
52
59
  }: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
53
- const payload: Pick<LocalMessage, 'poll_id' | 'quoted_message_id'> = {};
60
+ const payload: Pick<
61
+ LocalMessage,
62
+ 'poll_id' | 'quoted_message_id' | 'show_in_channel'
63
+ > = {};
54
64
  if (composer.quotedMessage) {
55
65
  payload.quoted_message_id = composer.quotedMessage.id;
56
66
  }
@@ -58,6 +68,10 @@ export const createDraftMessageComposerStateCompositionMiddleware = (
58
68
  payload.poll_id = composer.pollId;
59
69
  }
60
70
 
71
+ if (composer.showReplyInChannel) {
72
+ payload.show_in_channel = true;
73
+ }
74
+
61
75
  return next({
62
76
  ...state,
63
77
  draft: {
@@ -0,0 +1,43 @@
1
+ import type { MessageComposer } from '../../messageComposer';
2
+ import type {
3
+ MessageComposerMiddlewareState,
4
+ MessageCompositionMiddleware,
5
+ } from './types';
6
+ import type { MiddlewareHandlerParams } from '../../../middleware';
7
+ import type { OwnUserResponse } from '../../../types';
8
+
9
+ export const createUserDataInjectionMiddleware = (
10
+ composer: MessageComposer,
11
+ ): MessageCompositionMiddleware => ({
12
+ id: 'stream-io/message-composer-middleware/user-data-injection',
13
+ handlers: {
14
+ compose: ({
15
+ state,
16
+ next,
17
+ forward,
18
+ }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
19
+ if (!composer.client.user) {
20
+ return forward();
21
+ }
22
+ // Exclude the following properties from client.user as they can be large objects
23
+ // that provide no value for localMessage (and will never exist within message.user).
24
+ // This way we make sure that our localMessage is enriched with data as close as
25
+ // possible to the actual user.
26
+ // The reason why we need to explicitly cast is because OwnUserResponse only takes
27
+ // precedence after we connectUser the first time and we get the connection health
28
+ // check event. Due to how liberal the type of client.user is, we have to do it this
29
+ // way to maintain type safety.
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ const { channel_mutes, devices, mutes, ...messageUser } = composer.client
32
+ .user as OwnUserResponse;
33
+ return next({
34
+ ...state,
35
+ localMessage: {
36
+ ...state.localMessage,
37
+ user: messageUser,
38
+ user_id: messageUser.id,
39
+ },
40
+ });
41
+ },
42
+ },
43
+ });
@@ -6,13 +6,8 @@ import type {
6
6
  NotificationManagerConfig,
7
7
  NotificationState,
8
8
  } from './types';
9
-
10
- const DURATIONS: NotificationManagerConfig['durations'] = {
11
- error: 10000,
12
- warning: 5000,
13
- info: 3000,
14
- success: 3000,
15
- } as const;
9
+ import { mergeWith } from '../utils/mergeWith';
10
+ import { DEFAULT_NOTIFICATION_MANAGER_CONFIG } from './configuration';
16
11
 
17
12
  export class NotificationManager {
18
13
  store: StateStore<NotificationState>;
@@ -21,10 +16,7 @@ export class NotificationManager {
21
16
 
22
17
  constructor(config: Partial<NotificationManagerConfig> = {}) {
23
18
  this.store = new StateStore<NotificationState>({ notifications: [] });
24
- this.config = {
25
- ...config,
26
- durations: config.durations || DURATIONS,
27
- };
19
+ this.config = mergeWith(DEFAULT_NOTIFICATION_MANAGER_CONFIG, config);
28
20
  }
29
21
 
30
22
  get notifications() {
@@ -50,24 +42,27 @@ export class NotificationManager {
50
42
  add({ message, origin, options = {} }: AddNotificationPayload): string {
51
43
  const id = generateUUIDv4();
52
44
  const now = Date.now();
45
+ const severity = options.severity || 'info';
46
+ const duration = options.duration ?? this.config.durations[severity];
53
47
 
54
48
  const notification: Notification = {
55
49
  id,
56
50
  message,
57
51
  origin,
58
- severity: options.severity || 'info',
52
+ type: options?.type,
53
+ severity,
59
54
  createdAt: now,
60
- expiresAt: options.duration ? now + options.duration : undefined,
61
- autoClose: options.autoClose ?? true,
55
+ expiresAt: now + duration,
62
56
  actions: options.actions,
63
57
  metadata: options.metadata,
58
+ originalError: options.originalError,
64
59
  };
65
60
 
66
61
  this.store.partialNext({
67
62
  notifications: [...this.store.getLatestValue().notifications, notification],
68
63
  });
69
64
 
70
- if (notification.autoClose && notification.expiresAt) {
65
+ if (notification.expiresAt) {
71
66
  const timeout = setTimeout(() => {
72
67
  this.remove(id);
73
68
  }, options.duration || this.config.durations[notification.severity]);
@@ -0,0 +1,12 @@
1
+ import type { NotificationManagerConfig } from './types';
2
+
3
+ const DURATION_MS = 3000 as const;
4
+
5
+ export const DEFAULT_NOTIFICATION_MANAGER_CONFIG: NotificationManagerConfig = {
6
+ durations: {
7
+ error: DURATION_MS,
8
+ info: DURATION_MS,
9
+ success: DURATION_MS,
10
+ warning: DURATION_MS,
11
+ },
12
+ };