stream-chat 9.5.1 → 9.6.0
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/dist/cjs/index.browser.cjs +107 -42
- package/dist/cjs/index.browser.cjs.map +3 -3
- package/dist/cjs/index.node.cjs +108 -42
- package/dist/cjs/index.node.cjs.map +3 -3
- package/dist/esm/index.js +107 -42
- package/dist/esm/index.js.map +3 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messageComposer/messageComposer.d.ts +8 -5
- package/dist/types/messageComposer/middleware/messageComposer/userDataInjection.d.ts +3 -0
- package/dist/types/notifications/configuration.d.ts +2 -0
- package/dist/types/notifications/types.d.ts +57 -18
- package/dist/types/poll_manager.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/messageComposer/attachmentManager.ts +26 -19
- package/src/messageComposer/messageComposer.ts +26 -9
- package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +2 -0
- package/src/messageComposer/middleware/messageComposer/cleanData.ts +0 -1
- package/src/messageComposer/middleware/messageComposer/messageComposerState.ts +16 -2
- package/src/messageComposer/middleware/messageComposer/userDataInjection.ts +43 -0
- package/src/notifications/NotificationManager.ts +10 -15
- package/src/notifications/configuration.ts +12 -0
- package/src/notifications/types.ts +60 -18
- package/src/poll_manager.ts +7 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -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): "
|
|
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(): "
|
|
62
|
-
get tag(): `
|
|
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>;
|
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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)[];
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -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:
|
|
457
|
-
origin: {
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
quotedMessage: null,
|
|
90
|
+
showReplyInChannel: false,
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -104,10 +106,11 @@ 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
|
-
|
|
113
|
+
showReplyInChannel: false,
|
|
111
114
|
};
|
|
112
115
|
};
|
|
113
116
|
|
|
@@ -274,6 +277,10 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
274
277
|
return this.state.getLatestValue().pollId;
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
get showReplyInChannel() {
|
|
281
|
+
return this.state.getLatestValue().showReplyInChannel;
|
|
282
|
+
}
|
|
283
|
+
|
|
277
284
|
get hasSendableData() {
|
|
278
285
|
return !!(
|
|
279
286
|
(!this.attachmentManager.uploadsInProgressCount &&
|
|
@@ -580,6 +587,10 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
580
587
|
this.state.partialNext({ quotedMessage });
|
|
581
588
|
};
|
|
582
589
|
|
|
590
|
+
toggleShowReplyInChannel = () => {
|
|
591
|
+
this.state.partialNext({ showReplyInChannel: !this.showReplyInChannel });
|
|
592
|
+
};
|
|
593
|
+
|
|
583
594
|
clear = () => {
|
|
584
595
|
this.initState();
|
|
585
596
|
};
|
|
@@ -664,16 +675,22 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
664
675
|
const composition = await this.pollComposer.compose();
|
|
665
676
|
if (!composition || !composition.data.id) return;
|
|
666
677
|
try {
|
|
667
|
-
const
|
|
668
|
-
this.state.partialNext({ pollId: poll
|
|
669
|
-
this.pollComposer.initState();
|
|
678
|
+
const poll = await this.client.polls.createPoll(composition.data);
|
|
679
|
+
this.state.partialNext({ pollId: poll?.id });
|
|
670
680
|
} catch (error) {
|
|
671
|
-
this.client.notifications.
|
|
681
|
+
this.client.notifications.addError({
|
|
672
682
|
message: 'Failed to create the poll',
|
|
673
683
|
origin: {
|
|
674
684
|
emitter: 'MessageComposer',
|
|
675
685
|
context: { composer: this },
|
|
676
686
|
},
|
|
687
|
+
options: {
|
|
688
|
+
type: 'api:poll:create:failed',
|
|
689
|
+
metadata: {
|
|
690
|
+
reason: (error as Error).message,
|
|
691
|
+
},
|
|
692
|
+
originalError: error instanceof Error ? error : undefined,
|
|
693
|
+
},
|
|
677
694
|
});
|
|
678
695
|
throw error;
|
|
679
696
|
}
|
|
@@ -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),
|
|
@@ -17,7 +17,10 @@ export const createMessageComposerStateCompositionMiddleware = (
|
|
|
17
17
|
state,
|
|
18
18
|
next,
|
|
19
19
|
}: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
|
|
20
|
-
const payload: Pick<
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
type: options?.type,
|
|
53
|
+
severity,
|
|
59
54
|
createdAt: now,
|
|
60
|
-
expiresAt:
|
|
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.
|
|
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
|
+
};
|
|
@@ -33,42 +33,84 @@ export type Notification = {
|
|
|
33
33
|
* The identifier then can be recognized by notification consumers to act upon specific origin values.
|
|
34
34
|
*/
|
|
35
35
|
origin: NotificationOrigin;
|
|
36
|
-
/** Optional timestamp when notification should expire */
|
|
37
|
-
expiresAt?: number;
|
|
38
|
-
/** Whether notification should automatically close after duration. Defaults to true */
|
|
39
|
-
autoClose?: boolean;
|
|
40
36
|
/** Array of action buttons for the notification */
|
|
41
37
|
actions?: NotificationAction[];
|
|
38
|
+
/**
|
|
39
|
+
* Optional code that can be used to group the notifications of the same type, e.g. attachment-upload-blocked.
|
|
40
|
+
* Format: domain:entity:operation:result
|
|
41
|
+
* domain: where the error occurred (api, validation, permission, etc)
|
|
42
|
+
* entity: what was being operated on (poll, attachment, message, etc)
|
|
43
|
+
* operation: what was being attempted (create, upload, validate, etc)
|
|
44
|
+
* result: what happened (failed, blocked, invalid, etc)
|
|
45
|
+
*
|
|
46
|
+
* Poll related errors
|
|
47
|
+
* 'api:poll:create:failed' // API call to create poll failed
|
|
48
|
+
* 'validation:poll:create:invalid' // Poll creation validation failed
|
|
49
|
+
*
|
|
50
|
+
* Attachment related errors
|
|
51
|
+
* 'validation:attachment:file:missing' // Required file is missing
|
|
52
|
+
* 'permission:attachment:upload:blocked' // Upload blocked due to permissions
|
|
53
|
+
* 'api:attachment:upload:failed' // API upload call failed
|
|
54
|
+
* 'validation:attachment:type:unsupported' // Unsupported file type
|
|
55
|
+
* 'validation:attachment:size:exceeded' // File size too large
|
|
56
|
+
* 'validation:attachment:count:exceeded' // Too many attachments
|
|
57
|
+
*
|
|
58
|
+
* Message related errors
|
|
59
|
+
* 'api:message:send:failed' // Message send failed
|
|
60
|
+
* 'validation:message:content:empty' // Message content validation failed
|
|
61
|
+
*
|
|
62
|
+
* Channel related errors
|
|
63
|
+
* 'api:channel:join:failed' // Channel join failed
|
|
64
|
+
* 'permission:channel:access:denied' // Channel access denied
|
|
65
|
+
*
|
|
66
|
+
* Authentication related errors
|
|
67
|
+
* 'auth:token:expired' // Auth token expired
|
|
68
|
+
* 'auth:token:invalid' // Invalid auth token
|
|
69
|
+
*
|
|
70
|
+
* Network related errors
|
|
71
|
+
* 'network:request:timeout' // Request timed out
|
|
72
|
+
* 'network:request:failed' // Network request failed
|
|
73
|
+
*
|
|
74
|
+
* Rate limiting
|
|
75
|
+
* 'rate:limit:exceeded' // Rate limit exceeded
|
|
76
|
+
*
|
|
77
|
+
* System errors
|
|
78
|
+
* 'system:internal:error' // Internal system error
|
|
79
|
+
* 'system:resource:unavailable'; // System resource unavailable
|
|
80
|
+
*/
|
|
81
|
+
type?: string;
|
|
82
|
+
/** Optional timestamp when notification should expire */
|
|
83
|
+
expiresAt?: number;
|
|
42
84
|
/** Optional metadata to attach to the notification */
|
|
43
85
|
metadata?: Record<string, unknown>;
|
|
86
|
+
/** In case of error notification the instance of the originally thrown error */
|
|
87
|
+
originalError?: Error;
|
|
44
88
|
};
|
|
45
89
|
|
|
46
90
|
/** Configuration options when creating a notification */
|
|
47
|
-
export type NotificationOptions =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/** How long notification should
|
|
91
|
+
export type NotificationOptions = Partial<
|
|
92
|
+
Pick<Notification, 'type' | 'severity' | 'actions' | 'metadata' | 'originalError'>
|
|
93
|
+
> & {
|
|
94
|
+
/** How long a notification should be displayed in milliseconds */
|
|
51
95
|
duration?: number;
|
|
52
|
-
/** Whether notification should auto-close after duration. Defaults to true */
|
|
53
|
-
autoClose?: boolean;
|
|
54
|
-
/** Array of action buttons for the notification */
|
|
55
|
-
actions?: NotificationAction[];
|
|
56
|
-
/** Optional metadata to attach to the notification */
|
|
57
|
-
metadata?: Record<string, unknown>;
|
|
58
96
|
};
|
|
59
97
|
|
|
60
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* State shape for the notification store
|
|
100
|
+
* @deprcated use NotificationManagerState
|
|
101
|
+
*/
|
|
61
102
|
export type NotificationState = {
|
|
62
103
|
/** Array of current notification objects */
|
|
63
104
|
notifications: Notification[];
|
|
64
105
|
};
|
|
65
106
|
|
|
107
|
+
/** State shape for the notification store */
|
|
108
|
+
export type NotificationManagerState = NotificationState;
|
|
109
|
+
|
|
66
110
|
export type NotificationManagerConfig = {
|
|
67
111
|
durations: Record<NotificationSeverity, number>;
|
|
68
112
|
};
|
|
69
113
|
|
|
70
|
-
export type AddNotificationPayload = {
|
|
71
|
-
message: string;
|
|
72
|
-
origin: NotificationOrigin;
|
|
114
|
+
export type AddNotificationPayload = Pick<Notification, 'message' | 'origin'> & {
|
|
73
115
|
options?: NotificationOptions;
|
|
74
116
|
};
|
package/src/poll_manager.ts
CHANGED
|
@@ -49,7 +49,13 @@ export class PollManager extends WithSubscriptions {
|
|
|
49
49
|
public createPoll = async (poll: CreatePollData) => {
|
|
50
50
|
const { poll: createdPoll } = await this.client.createPoll(poll);
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
if (!createdPoll.vote_counts_by_option) {
|
|
53
|
+
createdPoll.vote_counts_by_option = {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.setOrOverwriteInCache(createdPoll);
|
|
57
|
+
|
|
58
|
+
return this.fromState(createdPoll.id);
|
|
53
59
|
};
|
|
54
60
|
|
|
55
61
|
public getPoll = async (id: string) => {
|