stream-chat 9.42.3 → 9.43.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.
Files changed (39) hide show
  1. package/dist/cjs/index.browser.js +1451 -1169
  2. package/dist/cjs/index.browser.js.map +3 -3
  3. package/dist/cjs/index.node.js +1452 -1171
  4. package/dist/cjs/index.node.js.map +3 -3
  5. package/dist/esm/index.mjs +1451 -1169
  6. package/dist/esm/index.mjs.map +3 -3
  7. package/dist/types/messageComposer/CustomDataManager.d.ts +3 -0
  8. package/dist/types/messageComposer/LocationComposer.d.ts +3 -0
  9. package/dist/types/messageComposer/MessageComposerEffectHandlers.d.ts +17 -0
  10. package/dist/types/messageComposer/attachmentManager.d.ts +7 -0
  11. package/dist/types/messageComposer/linkPreviewsManager.d.ts +4 -1
  12. package/dist/types/messageComposer/messageComposer.d.ts +39 -1
  13. package/dist/types/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.d.ts +2 -1
  14. package/dist/types/messageComposer/middleware/textComposer/commandEffects.d.ts +5 -0
  15. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +7 -0
  16. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -0
  17. package/dist/types/messageComposer/middleware/textComposer/index.d.ts +1 -0
  18. package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +0 -34
  19. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +13 -0
  20. package/dist/types/messageComposer/pollComposer.d.ts +3 -0
  21. package/dist/types/messageComposer/textComposer.d.ts +5 -1
  22. package/package.json +1 -1
  23. package/src/messageComposer/CustomDataManager.ts +8 -0
  24. package/src/messageComposer/LocationComposer.ts +8 -0
  25. package/src/messageComposer/MessageComposerEffectHandlers.ts +87 -0
  26. package/src/messageComposer/attachmentManager.ts +55 -0
  27. package/src/messageComposer/linkPreviewsManager.ts +12 -3
  28. package/src/messageComposer/messageComposer.ts +107 -0
  29. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +58 -18
  30. package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +7 -1
  31. package/src/messageComposer/middleware/textComposer/commandEffects.ts +51 -0
  32. package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +1 -4
  33. package/src/messageComposer/middleware/textComposer/commandUtils.ts +48 -0
  34. package/src/messageComposer/middleware/textComposer/commands.ts +15 -7
  35. package/src/messageComposer/middleware/textComposer/index.ts +1 -0
  36. package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +3 -46
  37. package/src/messageComposer/middleware/textComposer/types.ts +20 -0
  38. package/src/messageComposer/pollComposer.ts +8 -0
  39. package/src/messageComposer/textComposer.ts +54 -6
@@ -6,6 +6,7 @@ export type CustomDataManagerState = {
6
6
  message: CustomMessageData;
7
7
  custom: CustomMessageComposerData;
8
8
  };
9
+ export type CustomDataManagerSnapshot = CustomDataManagerState;
9
10
  export type CustomDataManagerOptions = {
10
11
  composer: MessageComposer;
11
12
  message?: DraftMessage | LocalMessage;
@@ -20,6 +21,8 @@ export declare class CustomDataManager {
20
21
  initState: ({ message }?: {
21
22
  message?: DraftMessage | LocalMessage;
22
23
  }) => void;
24
+ getSnapshot: () => CustomDataManagerSnapshot;
25
+ restoreSnapshot: (snapshot: CustomDataManagerSnapshot) => void;
23
26
  setMessageData(data: DeepPartial<CustomMessageData>): void;
24
27
  setCustomData(data: DeepPartial<CustomMessageComposerData>): void;
25
28
  }
@@ -16,6 +16,7 @@ export type LiveLocationPreview = Omit<LiveLocationPayload, 'end_at'> & {
16
16
  export type LocationComposerState = {
17
17
  location: StaticLocationPreview | LiveLocationPreview | null;
18
18
  };
19
+ export type LocationComposerSnapshot = LocationComposerState;
19
20
  export declare class LocationComposer {
20
21
  readonly state: StateStore<LocationComposerState>;
21
22
  readonly composer: MessageComposer;
@@ -28,6 +29,8 @@ export declare class LocationComposer {
28
29
  initState: ({ message }?: {
29
30
  message?: DraftMessage | LocalMessage;
30
31
  }) => void;
32
+ getSnapshot: () => LocationComposerSnapshot;
33
+ restoreSnapshot: (snapshot: LocationComposerSnapshot) => void;
31
34
  setData: (data: {
32
35
  durationMs?: number;
33
36
  } & Coords) => void;
@@ -0,0 +1,17 @@
1
+ import type { MessageComposer, MessageComposerEffectHandler } from './messageComposer';
2
+ export type MessageComposerEffectHandlersOptions = {
3
+ composer: MessageComposer;
4
+ };
5
+ export declare class MessageComposerEffectHandlers {
6
+ private options;
7
+ private handlers;
8
+ constructor(options: MessageComposerEffectHandlersOptions);
9
+ private registerDefaultHandlers;
10
+ registerEffectHandler: <T extends {
11
+ type: string;
12
+ }>(type: T["type"], handler: MessageComposerEffectHandler<T>) => void;
13
+ applyEffects: <T extends {
14
+ type: string;
15
+ }>(effects?: T[]) => void;
16
+ private applyEffect;
17
+ }
@@ -8,6 +8,7 @@ export type FileUploadFilter = (file: Partial<LocalUploadAttachment>) => boolean
8
8
  export type AttachmentManagerState = {
9
9
  attachments: LocalAttachment[];
10
10
  };
11
+ export type AttachmentManagerSnapshot = AttachmentManagerState;
11
12
  export type AttachmentManagerOptions = {
12
13
  composer: MessageComposer;
13
14
  message?: DraftMessage | LocalMessage;
@@ -41,9 +42,15 @@ export declare class AttachmentManager {
41
42
  get pendingUploadsCount(): number;
42
43
  get availableUploadSlots(): number;
43
44
  getUploadsByState(state: AttachmentLoadingState): LocalAttachment[];
45
+ private cancelAttachmentUploads;
46
+ private normalizeSnapshotAttachment;
44
47
  initState: ({ message }?: {
45
48
  message?: DraftMessage | LocalMessage;
46
49
  }) => void;
50
+ getSnapshot: () => AttachmentManagerSnapshot;
51
+ restoreSnapshot: (snapshot: AttachmentManagerSnapshot) => void;
52
+ setAttachments: (attachments: LocalAttachment[]) => void;
53
+ clearAttachments: () => void;
47
54
  getAttachmentIndex: (localId: string) => number;
48
55
  private prepareAttachmentUpdate;
49
56
  updateAttachment: (attachmentToUpdate: LocalAttachment) => void;
@@ -7,7 +7,7 @@ export type LinkPreview = OGAttachment & {
7
7
  status: LinkPreviewStatus;
8
8
  };
9
9
  export interface ILinkPreviewsManager {
10
- /** Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. */
10
+ /** Function cancels all scheduled or in-progress URL enrichment queries. */
11
11
  cancelURLEnrichment: () => void;
12
12
  /** Function that triggers the search for URLs and their enrichment. */
13
13
  findAndEnrichUrls?: DebouncedFunc<(text: string) => void>;
@@ -29,6 +29,7 @@ export type LinkPreviewMap = Map<LinkURL, LinkPreview>;
29
29
  export type LinkPreviewsManagerState = {
30
30
  previews: LinkPreviewMap;
31
31
  };
32
+ export type LinkPreviewsManagerSnapshot = LinkPreviewsManagerState;
32
33
  export type LinkPreviewsManagerOptions = {
33
34
  composer: MessageComposer;
34
35
  message?: DraftMessage | LocalMessage;
@@ -59,6 +60,8 @@ export declare class LinkPreviewsManager implements ILinkPreviewsManager {
59
60
  initState: ({ message }?: {
60
61
  message?: DraftMessage | LocalMessage;
61
62
  }) => void;
63
+ getSnapshot: () => LinkPreviewsManagerSnapshot;
64
+ restoreSnapshot: (snapshot: LinkPreviewsManagerSnapshot) => void;
62
65
  private _findAndEnrichUrls;
63
66
  cancelURLEnrichment: () => void;
64
67
  /**
@@ -11,10 +11,17 @@ import { StateStore } from '../store';
11
11
  import { generateUUIDv4 } from '../utils';
12
12
  import { Channel } from '../channel';
13
13
  import { Thread } from '../thread';
14
- import type { ChannelAPIResponse, DraftResponse, LocalMessage, LocalMessageBase, MessageResponse } from '../types';
14
+ import type { ChannelAPIResponse, CommandResponse, DraftResponse, LocalMessage, LocalMessageBase, MessageResponse } from '../types';
15
15
  import { WithSubscriptions } from '../utils/WithSubscriptions';
16
16
  import type { StreamChat } from '../client';
17
17
  import type { MessageComposerConfig } from './configuration/types';
18
+ import type { CommandSuggestionDisabledReason, TextComposerCommandActivationEffect, TextComposerCommandClearEffect } from './middleware/textComposer/types';
19
+ import type { AttachmentManagerSnapshot } from './attachmentManager';
20
+ import type { CustomDataManagerSnapshot } from './CustomDataManager';
21
+ import type { LinkPreviewsManagerSnapshot } from './linkPreviewsManager';
22
+ import type { LocationComposerSnapshot } from './LocationComposer';
23
+ import type { PollComposerSnapshot } from './pollComposer';
24
+ import type { TextComposerSnapshot } from './textComposer';
18
25
  import type { DeepPartial } from '../types.utility';
19
26
  type UnregisterSubscriptions = Unsubscribe;
20
27
  export type LastComposerChange = {
@@ -24,6 +31,22 @@ export type LastComposerChange = {
24
31
  export type EditingAuditState = {
25
32
  lastChange: LastComposerChange;
26
33
  };
34
+ export type BuiltInMessageComposerEffect = TextComposerCommandActivationEffect | TextComposerCommandClearEffect;
35
+ export type CustomMessageComposerEffect = {
36
+ type: string & {};
37
+ } & Record<string, unknown>;
38
+ export type MessageComposerEffect = BuiltInMessageComposerEffect | CustomMessageComposerEffect;
39
+ export type MessageComposerEffectHandler<T extends {
40
+ type: string;
41
+ } = MessageComposerEffect> = (effect: T, composer: MessageComposer) => void;
42
+ export type MessageComposerSnapshot = {
43
+ attachmentManager: AttachmentManagerSnapshot;
44
+ customDataManager: CustomDataManagerSnapshot;
45
+ linkPreviewsManager: LinkPreviewsManagerSnapshot;
46
+ locationComposer: LocationComposerSnapshot;
47
+ pollComposer: PollComposerSnapshot;
48
+ textComposer: TextComposerSnapshot;
49
+ };
27
50
  export type LocalMessageWithLegacyThreadId = LocalMessage & {
28
51
  legacyThreadId?: string;
29
52
  };
@@ -60,6 +83,8 @@ export declare class MessageComposer extends WithSubscriptions {
60
83
  pollComposer: PollComposer;
61
84
  locationComposer: LocationComposer;
62
85
  customDataManager: CustomDataManager;
86
+ private snapshots;
87
+ private effectHandlers;
63
88
  constructor({ composition, config, compositionContext, client, }: MessageComposerOptions);
64
89
  static evaluateContextType(compositionContext: CompositionContext): "message" | "channel" | "thread" | "legacy_thread";
65
90
  static constructTag(compositionContext: CompositionContext): `${ReturnType<typeof MessageComposer.evaluateContextType>}_${string}`;
@@ -76,6 +101,8 @@ export declare class MessageComposer extends WithSubscriptions {
76
101
  get draftId(): string | null;
77
102
  get lastChange(): LastComposerChange;
78
103
  get quotedMessage(): LocalMessageBase | null;
104
+ getCommandDisabledReason: (command: CommandResponse) => CommandSuggestionDisabledReason | undefined;
105
+ isCommandDisabled: (command: CommandResponse) => boolean;
79
106
  get pollId(): string | null;
80
107
  get showReplyInChannel(): boolean;
81
108
  get hasSendableData(): boolean;
@@ -89,6 +116,17 @@ export declare class MessageComposer extends WithSubscriptions {
89
116
  }) => void;
90
117
  initStateFromChannelResponse: (channelApiResponse: ChannelAPIResponse) => void;
91
118
  initEditingAuditState: (composition?: DraftResponse | MessageResponse | LocalMessage) => EditingAuditState;
119
+ clearSnapshots: () => void;
120
+ getSnapshot: () => MessageComposerSnapshot;
121
+ restoreSnapshot: (snapshot: MessageComposerSnapshot) => void;
122
+ captureSnapshot: (snapshot?: MessageComposerSnapshot) => void;
123
+ popSnapshot: () => MessageComposerSnapshot | undefined;
124
+ registerEffectHandler: <T extends {
125
+ type: string;
126
+ }>(type: T["type"], handler: MessageComposerEffectHandler<T>) => void;
127
+ applyEffects: <T extends {
128
+ type: string;
129
+ }>(effects?: T[]) => void;
92
130
  private logStateUpdateTimestamp;
93
131
  private logDraftUpdateTimestamp;
94
132
  registerDraftEventSubscriptions: () => () => void;
@@ -1,10 +1,11 @@
1
1
  import { MiddlewareExecutor } from '../../../middleware';
2
2
  import type { ExecuteParams, MiddlewareExecutionResult, MiddlewareHandler } from '../../../middleware';
3
- import type { Suggestion, TextComposerMiddlewareExecutorOptions, TextComposerState } from './types';
3
+ import type { Suggestion, TextComposerEffect, TextComposerMiddlewareExecutorOptions, TextComposerState } from './types';
4
4
  export type TextComposerMiddlewareExecutorState<T extends Suggestion = Suggestion> = TextComposerState<T> & {
5
5
  change?: {
6
6
  selectedSuggestion?: T;
7
7
  };
8
+ effects?: TextComposerEffect[];
8
9
  };
9
10
  export type TextComposerHandlerNames = 'onChange' | 'onSuggestionItemSelect';
10
11
  export type TextComposerMiddleware<T extends Suggestion = Suggestion> = {
@@ -0,0 +1,5 @@
1
+ import type { Middleware } from '../../../middleware';
2
+ import type { CommandSuggestion } from './types';
3
+ import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
4
+ export type CommandEffectsMiddleware = Middleware<TextComposerMiddlewareExecutorState<CommandSuggestion>, 'onChange' | 'onSuggestionItemSelect'>;
5
+ export declare const createCommandEffectsMiddleware: () => CommandEffectsMiddleware;
@@ -0,0 +1,7 @@
1
+ import type { MessageComposer } from '../../messageComposer';
2
+ import type { CommandResponse } from '../../../types';
3
+ export declare function escapeCommandRegExp(text: string): string;
4
+ export declare const getRawCommandName: (text?: string) => string | undefined;
5
+ export declare const getCompleteCommandInString: (text: string) => string | null;
6
+ export declare const stripCommandFromText: (text: string, commandName: string) => string;
7
+ export declare const notifyCommandDisabled: (composer: MessageComposer, command: CommandResponse) => true | undefined;
@@ -2,6 +2,7 @@ import type { Channel } from '../../../channel';
2
2
  import type { Middleware } from '../../../middleware';
3
3
  import type { SearchSourceOptions } from '../../../search';
4
4
  import { BaseSearchSourceSync } from '../../../search';
5
+ import type { MessageComposer } from '../../messageComposer';
5
6
  import type { CommandSuggestion, TextComposerMiddlewareOptions } from './types';
6
7
  import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
7
8
  export declare class CommandSearchSource extends BaseSearchSourceSync<CommandSuggestion> {
@@ -35,5 +36,6 @@ export declare class CommandSearchSource extends BaseSearchSourceSync<CommandSug
35
36
  }
36
37
  export type CommandsMiddleware = Middleware<TextComposerMiddlewareExecutorState<CommandSuggestion>, 'onChange' | 'onSuggestionItemSelect'>;
37
38
  export declare const createCommandsMiddleware: (channel: Channel, options?: Partial<TextComposerMiddlewareOptions> & {
39
+ composer?: MessageComposer;
38
40
  searchSource?: CommandSearchSource;
39
41
  }) => CommandsMiddleware;
@@ -1,5 +1,6 @@
1
1
  export * from './activeCommandGuard';
2
2
  export * from './commands';
3
+ export * from './commandEffects';
3
4
  export * from './commandStringExtraction';
4
5
  export * from './mentions';
5
6
  export * from './validation';
@@ -10,7 +10,6 @@ export declare const getTriggerCharWithToken: ({ trigger, text, isCommand, accep
10
10
  isCommand?: boolean;
11
11
  acceptTrailingSpaces?: boolean;
12
12
  }) => string | null;
13
- export declare const getCompleteCommandInString: (text: string) => string | null;
14
13
  export declare const insertItemWithTrigger: ({ insertText, selection, text, trigger, }: {
15
14
  insertText: string;
16
15
  selection: TextSelection;
@@ -28,39 +27,6 @@ export declare const replaceWordWithEntity: ({ caretPosition, getEntityString, t
28
27
  getEntityString: (word: string) => Promise<string | null> | string | null;
29
28
  text: string;
30
29
  }) => Promise<string>;
31
- /**
32
- * Escapes a string for use in a regular expression
33
- * @param text - The string to escape
34
- * @returns The escaped string
35
- * What does this regex do?
36
-
37
- The regex escapes special regex characters by adding a backslash before them. Here's what it matches:
38
- - dash
39
- [ ] square brackets
40
- { } curly braces
41
- ( ) parentheses
42
- * asterisk
43
- + plus
44
- ? question mark
45
- . period
46
- , comma
47
- / forward slash
48
- \ backslash
49
- ^ caret
50
- $ dollar sign
51
- | pipe
52
- # hash
53
-
54
- The \\$& replacement adds a backslash before any matched character.
55
- This is needed when you want to use these characters literally
56
- in a regex pattern instead of their special regex meanings.
57
- For example:
58
- escapeRegExp("hello.world") // Returns: "hello\.world"
59
- escapeRegExp("[test]") // Returns: "\[test\]"
60
-
61
- This is commonly used when building dynamic regex patterns from user input to prevent special characters from being interpreted as regex syntax.
62
- */
63
- export declare function escapeRegExp(text: string): string;
64
30
  export type TokenizationPayload = {
65
31
  tokenizedDisplayName: {
66
32
  token: string;
@@ -1,4 +1,5 @@
1
1
  import type { MessageComposer } from '../../messageComposer';
2
+ import type { MessageComposerEffect } from '../../messageComposer';
2
3
  import type { CommandResponse, UserResponse } from '../../../types';
3
4
  import type { TokenizationPayload } from './textMiddlewareUtils';
4
5
  import type { SearchSource, SearchSourceSync } from '../../../search';
@@ -9,10 +10,22 @@ export type TextComposerSuggestion<T = unknown> = T & {
9
10
  export type BaseSuggestion = {
10
11
  id: string;
11
12
  };
13
+ export type CommandSuggestionDisabledReason = 'editing' | 'quoted_message';
12
14
  export type CommandSuggestion = BaseSuggestion & CommandResponse;
13
15
  export type UserSuggestion = BaseSuggestion & UserResponse & TokenizationPayload;
14
16
  export type CustomValidSuggestion = BaseSuggestion & CustomTextComposerSuggestion;
15
17
  export type Suggestion = CommandSuggestion | UserSuggestion | CustomValidSuggestion;
18
+ export type TextComposerStateSnapshot = TextComposerState;
19
+ export type TextComposerCommandActivationStateToRestore = Partial<TextComposerStateSnapshot>;
20
+ export type TextComposerCommandActivationEffect = {
21
+ command: CommandResponse;
22
+ stateToRestore?: TextComposerCommandActivationStateToRestore;
23
+ type: 'command.activate';
24
+ };
25
+ export type TextComposerCommandClearEffect = {
26
+ type: 'command.clear';
27
+ };
28
+ export type TextComposerEffect = MessageComposerEffect;
16
29
  export type TextComposerMiddlewareOptions = {
17
30
  minChars: number;
18
31
  trigger: string;
@@ -3,6 +3,7 @@ import { StateStore } from '../store';
3
3
  import { VotingVisibility } from '../types';
4
4
  import type { MessageComposer } from './messageComposer';
5
5
  import type { PollComposerFieldErrors, PollComposerState, UpdateFieldsData } from './middleware/pollComposer';
6
+ export type PollComposerSnapshot = PollComposerState;
6
7
  export type PollComposerOptions = {
7
8
  composer: MessageComposer;
8
9
  };
@@ -25,6 +26,8 @@ export declare class PollComposer {
25
26
  get voting_visibility(): VotingVisibility | undefined;
26
27
  get canCreatePoll(): boolean;
27
28
  initState: () => void;
29
+ getSnapshot: () => PollComposerSnapshot;
30
+ restoreSnapshot: (snapshot: PollComposerSnapshot) => void;
28
31
  /**
29
32
  * Updates specified fields and generates relevant errors
30
33
  * @param data
@@ -2,7 +2,7 @@ import { TextComposerMiddlewareExecutor } from './middleware';
2
2
  import { StateStore } from '../store';
3
3
  import type { TextComposerSuggestion } from './middleware/textComposer/types';
4
4
  import type { TextSelection } from './middleware/textComposer/types';
5
- import type { TextComposerState } from './middleware/textComposer/types';
5
+ import type { TextComposerState, TextComposerStateSnapshot } from './middleware/textComposer/types';
6
6
  import type { Suggestions } from './middleware/textComposer/types';
7
7
  import type { MessageComposer } from './messageComposer';
8
8
  import type { CommandResponse, DraftMessage, LocalMessage, UserResponse } from '../types';
@@ -10,6 +10,7 @@ export type TextComposerOptions = {
10
10
  composer: MessageComposer;
11
11
  message?: DraftMessage | LocalMessage;
12
12
  };
13
+ export type TextComposerSnapshot = TextComposerStateSnapshot;
13
14
  export declare const textIsEmpty: (text: string) => boolean;
14
15
  export declare class TextComposer {
15
16
  readonly composer: MessageComposer;
@@ -37,6 +38,8 @@ export declare class TextComposer {
37
38
  initState: ({ message }?: {
38
39
  message?: DraftMessage | LocalMessage;
39
40
  }) => void;
41
+ getSnapshot: (state?: TextComposerState) => TextComposerSnapshot;
42
+ restoreSnapshot: (snapshot: TextComposerSnapshot) => void;
40
43
  setMentionedUsers(users: UserResponse[]): void;
41
44
  clearCommand(): void;
42
45
  upsertMentionedUser: (user: UserResponse) => void;
@@ -56,6 +59,7 @@ export declare class TextComposer {
56
59
  }) => void;
57
60
  setSuggestions: (suggestions: Suggestions) => void;
58
61
  closeSuggestions: () => void;
62
+ private commitState;
59
63
  handleChange: ({ text, selection, }: {
60
64
  selection: TextSelection;
61
65
  text: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.42.3",
3
+ "version": "9.43.0",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
@@ -13,6 +13,8 @@ export type CustomDataManagerState = {
13
13
  custom: CustomMessageComposerData;
14
14
  };
15
15
 
16
+ export type CustomDataManagerSnapshot = CustomDataManagerState;
17
+
16
18
  export type CustomDataManagerOptions = {
17
19
  composer: MessageComposer;
18
20
  message?: DraftMessage | LocalMessage;
@@ -50,6 +52,12 @@ export class CustomDataManager {
50
52
  this.state.next(initState({ composer: this.composer, message }));
51
53
  };
52
54
 
55
+ getSnapshot = (): CustomDataManagerSnapshot => this.state.getLatestValue();
56
+
57
+ restoreSnapshot = (snapshot: CustomDataManagerSnapshot) => {
58
+ this.state.next(snapshot);
59
+ };
60
+
53
61
  setMessageData(data: DeepPartial<CustomMessageData>) {
54
62
  this.state.partialNext({
55
63
  message: {
@@ -24,6 +24,8 @@ export type LocationComposerState = {
24
24
  location: StaticLocationPreview | LiveLocationPreview | null;
25
25
  };
26
26
 
27
+ export type LocationComposerSnapshot = LocationComposerState;
28
+
27
29
  const MIN_LIVE_LOCATION_SHARE_DURATION = 60 * 1000; // 1 minute;
28
30
 
29
31
  const initState = ({
@@ -79,6 +81,12 @@ export class LocationComposer {
79
81
  this.state.next(initState({ message }));
80
82
  };
81
83
 
84
+ getSnapshot = (): LocationComposerSnapshot => this.state.getLatestValue();
85
+
86
+ restoreSnapshot = (snapshot: LocationComposerSnapshot) => {
87
+ this.state.next(snapshot);
88
+ };
89
+
82
90
  setData = (data: { durationMs?: number } & Coords) => {
83
91
  if (!this.config.enabled) return;
84
92
  if (!data.latitude || !data.longitude) return;
@@ -0,0 +1,87 @@
1
+ import type { MessageComposer, MessageComposerEffectHandler } from './messageComposer';
2
+ import type {
3
+ TextComposerCommandActivationEffect,
4
+ TextComposerCommandClearEffect,
5
+ } from './middleware/textComposer/types';
6
+
7
+ type RegisteredMessageComposerEffectHandler = (
8
+ effect: { type: string },
9
+ composer: MessageComposer,
10
+ ) => void;
11
+
12
+ export type MessageComposerEffectHandlersOptions = {
13
+ composer: MessageComposer;
14
+ };
15
+
16
+ const applyCommandActivationEffect: MessageComposerEffectHandler<
17
+ TextComposerCommandActivationEffect
18
+ > = (effect, composer) => {
19
+ const snapshot = composer.getSnapshot();
20
+ if (effect.stateToRestore) {
21
+ snapshot.textComposer = {
22
+ ...snapshot.textComposer,
23
+ ...effect.stateToRestore,
24
+ command: null,
25
+ };
26
+ }
27
+ composer.captureSnapshot(snapshot);
28
+
29
+ // we manually clear because we want the command to still persist
30
+ composer.textComposer.state.next({
31
+ command: effect.command,
32
+ mentionedUsers: [],
33
+ suggestions: undefined,
34
+ selection: { start: 0, end: 0 },
35
+ text: '',
36
+ });
37
+ composer.attachmentManager.initState();
38
+ composer.linkPreviewsManager.initState();
39
+ composer.locationComposer.initState();
40
+ composer.pollComposer.initState();
41
+ composer.customDataManager.initState();
42
+ };
43
+
44
+ const applyCommandClearEffect: MessageComposerEffectHandler<
45
+ TextComposerCommandClearEffect
46
+ > = (_, composer) => {
47
+ const snapshot = composer.popSnapshot();
48
+
49
+ if (!snapshot) return;
50
+
51
+ composer.restoreSnapshot(snapshot);
52
+ };
53
+
54
+ export class MessageComposerEffectHandlers {
55
+ private handlers = new Map<string, RegisteredMessageComposerEffectHandler>();
56
+
57
+ constructor(private options: MessageComposerEffectHandlersOptions) {
58
+ this.registerDefaultHandlers();
59
+ }
60
+
61
+ private registerDefaultHandlers = () => {
62
+ this.registerEffectHandler<TextComposerCommandActivationEffect>(
63
+ 'command.activate',
64
+ applyCommandActivationEffect,
65
+ );
66
+ this.registerEffectHandler<TextComposerCommandClearEffect>(
67
+ 'command.clear',
68
+ applyCommandClearEffect,
69
+ );
70
+ };
71
+
72
+ registerEffectHandler = <T extends { type: string }>(
73
+ type: T['type'],
74
+ handler: MessageComposerEffectHandler<T>,
75
+ ): void => {
76
+ this.handlers.set(type, handler as RegisteredMessageComposerEffectHandler);
77
+ };
78
+
79
+ applyEffects = <T extends { type: string }>(effects: T[] = []) => {
80
+ effects.forEach((effect) => this.applyEffect(effect));
81
+ };
82
+
83
+ private applyEffect = (effect: { type: string }) => {
84
+ const handler = this.handlers.get(effect.type);
85
+ handler?.(effect, this.options.composer);
86
+ };
87
+ }
@@ -41,6 +41,8 @@ export type AttachmentManagerState = {
41
41
  attachments: LocalAttachment[];
42
42
  };
43
43
 
44
+ export type AttachmentManagerSnapshot = AttachmentManagerState;
45
+
44
46
  export type AttachmentManagerOptions = {
45
47
  composer: MessageComposer;
46
48
  message?: DraftMessage | LocalMessage;
@@ -208,10 +210,63 @@ export class AttachmentManager {
208
210
  );
209
211
  }
210
212
 
213
+ private cancelAttachmentUploads = (attachments: LocalAttachment[]) => {
214
+ for (const { localMetadata } of attachments) {
215
+ this.client.uploadManager.deleteUploadRecord(localMetadata.id);
216
+ }
217
+ };
218
+
219
+ private normalizeSnapshotAttachment = (attachment: LocalAttachment) => {
220
+ if (attachment.localMetadata.uploadState !== 'uploading') return attachment;
221
+
222
+ this.client.uploadManager.deleteUploadRecord(attachment.localMetadata.id);
223
+
224
+ return {
225
+ ...attachment,
226
+ localMetadata: {
227
+ ...attachment.localMetadata,
228
+ uploadProgress: undefined,
229
+ uploadState: 'failed',
230
+ },
231
+ } as LocalAttachment;
232
+ };
233
+
211
234
  initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
235
+ this.cancelAttachmentUploads(this.attachments);
212
236
  this.state.next(initState({ message }));
213
237
  };
214
238
 
239
+ getSnapshot = (): AttachmentManagerSnapshot => {
240
+ const state = this.state.getLatestValue();
241
+ let hasUpdates = false;
242
+ const attachments = state.attachments.map(this.normalizeSnapshotAttachment);
243
+
244
+ for (let i = 0; i < attachments.length; i++) {
245
+ if (attachments[i] !== state.attachments[i]) {
246
+ hasUpdates = true;
247
+ break;
248
+ }
249
+ }
250
+
251
+ return hasUpdates ? { ...state, attachments } : state;
252
+ };
253
+
254
+ restoreSnapshot = (snapshot: AttachmentManagerSnapshot) => {
255
+ this.cancelAttachmentUploads(this.attachments);
256
+ this.state.next(snapshot);
257
+ };
258
+
259
+ setAttachments = (attachments: LocalAttachment[]) => {
260
+ this.state.partialNext({ attachments });
261
+ };
262
+
263
+ clearAttachments = () => {
264
+ if (!this.attachments.length) return;
265
+ this.removeAttachments(
266
+ this.attachments.map((attachment) => attachment.localMetadata.id),
267
+ );
268
+ };
269
+
215
270
  getAttachmentIndex = (localId: string) => {
216
271
  const attachmentsById = this.attachmentsById;
217
272
 
@@ -11,7 +11,7 @@ export type LinkPreview = OGAttachment & {
11
11
  };
12
12
 
13
13
  export interface ILinkPreviewsManager {
14
- /** Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. */
14
+ /** Function cancels all scheduled or in-progress URL enrichment queries. */
15
15
  cancelURLEnrichment: () => void;
16
16
  /** Function that triggers the search for URLs and their enrichment. */
17
17
  findAndEnrichUrls?: DebouncedFunc<(text: string) => void>;
@@ -38,6 +38,8 @@ export type LinkPreviewsManagerState = {
38
38
  previews: LinkPreviewMap;
39
39
  };
40
40
 
41
+ export type LinkPreviewsManagerSnapshot = LinkPreviewsManagerState;
42
+
41
43
  export type LinkPreviewsManagerOptions = {
42
44
  composer: MessageComposer;
43
45
  message?: DraftMessage | LocalMessage;
@@ -186,9 +188,16 @@ export class LinkPreviewsManager implements ILinkPreviewsManager {
186
188
  }
187
189
 
188
190
  initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
191
+ this.cancelURLEnrichment();
189
192
  this.state.next(initState({ message: this.enabled ? message : undefined }));
190
193
  };
191
194
 
195
+ getSnapshot = (): LinkPreviewsManagerSnapshot => this.state.getLatestValue();
196
+
197
+ restoreSnapshot = (snapshot: LinkPreviewsManagerSnapshot) => {
198
+ this.state.next(snapshot);
199
+ };
200
+
192
201
  private _findAndEnrichUrls = async (text: string) => {
193
202
  if (!this.enabled) return;
194
203
  const urls = this.config.findURLFn(text);
@@ -254,8 +263,8 @@ export class LinkPreviewsManager implements ILinkPreviewsManager {
254
263
  };
255
264
 
256
265
  cancelURLEnrichment = () => {
257
- this.findAndEnrichUrls.cancel();
258
- this.findAndEnrichUrls.flush();
266
+ this.findAndEnrichUrls.cancel?.();
267
+ this.findAndEnrichUrls.flush?.();
259
268
  };
260
269
 
261
270
  /**