Skip to content

Commit af8fec7

Browse files
committed
Merge remote-tracking branch 'origin/feat/message-composer' into feat/message-composer
2 parents 880a7af + ffec1a0 commit af8fec7

File tree

5 files changed

+97
-13
lines changed

5 files changed

+97
-13
lines changed

src/channel.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,6 @@ export class Channel {
159159
this.disconnected = false;
160160

161161
this._messageComposer = new MessageComposer({ channel: this });
162-
this._messageComposer.textComposer.use([
163-
createCommandsMiddleware(this),
164-
createMentionsMiddleware(this),
165-
] as TextComposerMiddleware[]);
166162
}
167163

168164
/**

src/client.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,64 @@ import type {
229229
} from './channel_manager';
230230
import { ChannelManager } from './channel_manager';
231231
import { NotificationManager } from './notifications';
232+
import { StateStore } from './store';
233+
import {
234+
createCommandsMiddleware,
235+
createMentionsMiddleware,
236+
MessageComposer,
237+
MessageComposerOptions,
238+
TextComposerMiddleware,
239+
} from './messageComposer';
232240

233241
function isString(x: unknown): x is string {
234242
return typeof x === 'string' || x instanceof String;
235243
}
236244

245+
type MessageComposerDefine = ({
246+
constructorParameters,
247+
}: {
248+
constructorParameters: MessageComposerOptions;
249+
}) => MessageComposer;
250+
251+
type MessageComposerApplyModifications = ({
252+
composer,
253+
}: {
254+
composer: MessageComposer;
255+
}) => void | (() => void);
256+
257+
// TODO: maybe template modifications
258+
// { template1: { applyModifications... }, template2: {applyModifications} }
259+
// new MessageComposer({ channel, modificationTemplate: 'template1' })
260+
type MessageComposerSetupState = {
261+
define: MessageComposerDefine;
262+
/**
263+
* Each `MessageComposer` runs this function each time its signature changes or
264+
* whenever you run `MessageComposer.registerSubscriptions`. Function returned
265+
* from `applyModifications` will be used as a cleanup function - it will be stored
266+
* and ran before new modification is applied. Cleaning up only the
267+
* modified parts is the general way to go but if your setup gets a bit
268+
* complicated, feel free to restore the whole composer with `MessageComposer.restore`.
269+
*/
270+
applyModifications: MessageComposerApplyModifications | null;
271+
};
272+
273+
const INITIAL_MESSAGE_COMPOSER_SETUP_STATE: MessageComposerSetupState = {
274+
define: ({ constructorParameters }) => new MessageComposer(constructorParameters),
275+
applyModifications: ({ composer }) => {
276+
console.log(composer);
277+
278+
composer.textComposer.upsertMiddleware([
279+
createMentionsMiddleware(composer.channel),
280+
createCommandsMiddleware(composer.channel),
281+
// TODO: fix typing
282+
] as TextComposerMiddleware[]);
283+
284+
return () => {
285+
// composer.restore()
286+
};
287+
},
288+
};
289+
237290
export class StreamChat {
238291
private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics
239292

@@ -289,6 +342,8 @@ export class StreamChat {
289342
sdkIdentifier?: SdkIdentifier;
290343
deviceIdentifier?: DeviceIdentifier;
291344
private nextRequestAbortController: AbortController | null = null;
345+
public _messageComposerSetupState: StateStore<MessageComposerSetupState> =
346+
new StateStore(INITIAL_MESSAGE_COMPOSER_SETUP_STATE);
292347

293348
/**
294349
* Initialize a client
@@ -4320,4 +4375,15 @@ export class StreamChat {
43204375
) {
43214376
return await this.post<QueryDraftsResponse>(this.baseURL + '/drafts/query', options);
43224377
}
4378+
4379+
// TODO: this might not be needed
4380+
public createMessageComposer: MessageComposerDefine = (setup) => {
4381+
return this._messageComposerSetupState.getLatestValue().define(setup);
4382+
};
4383+
4384+
public setMessageComposerApplyModifications = (
4385+
applyModifications: MessageComposerSetupState['applyModifications'],
4386+
) => {
4387+
this._messageComposerSetupState.partialNext({ applyModifications });
4388+
};
43234389
}

src/messageComposer/messageComposer.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type MessageComposerOptions = {
4242
composition?: DraftResponse | MessageResponse | LocalMessage;
4343
config?: Partial<MessageComposerConfig>;
4444
threadId?: string;
45+
tag?: string;
4546
};
4647

4748
const isMessageDraft = (composition: unknown): composition is DraftResponse =>
@@ -82,11 +83,14 @@ const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = {
8283
urlPreviewEnabled: false,
8384
};
8485

86+
const noop = () => undefined;
87+
8588
export class MessageComposer {
8689
readonly channel: Channel;
8790
readonly state: StateStore<MessageComposerState>;
8891
readonly editedMessage?: LocalMessage;
8992
readonly threadId: string | null;
93+
readonly tag: string;
9094
config: MessageComposerConfig;
9195
attachmentManager: AttachmentManager;
9296
linkPreviewsManager: LinkPreviewsManager;
@@ -96,10 +100,11 @@ export class MessageComposer {
96100
private unsubscribeFunctions: Set<() => void> = new Set();
97101
private compositionMiddlewareExecutor: MessageComposerMiddlewareExecutor;
98102

99-
constructor({ channel, composition, config, threadId }: MessageComposerOptions) {
103+
constructor({ channel, composition, config, threadId, tag }: MessageComposerOptions) {
100104
this.channel = channel;
101105
this.threadId = threadId ?? null;
102106
this.config = mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {});
107+
this.tag = tag ?? generateUUIDv4();
103108

104109
let message: LocalMessage | DraftMessage | undefined = undefined;
105110
if (isMessageDraft(composition)) {
@@ -167,8 +172,9 @@ export class MessageComposer {
167172
public registerSubscriptions = () => {
168173
if (this.unsubscribeFunctions.size) {
169174
// Already listening for events and changes
170-
return;
175+
return noop;
171176
}
177+
this.unsubscribeFunctions.add(this.subscribeMessageComposerSetupStateChange());
172178
this.unsubscribeFunctions.add(this.subscribeMessageUpdated());
173179
this.unsubscribeFunctions.add(this.subscribeMessageDeleted());
174180
this.unsubscribeFunctions.add(this.subscribeTextChanged());
@@ -177,8 +183,11 @@ export class MessageComposer {
177183
this.unsubscribeFunctions.add(this.subscribeDraftUpdated());
178184
this.unsubscribeFunctions.add(this.subscribeDraftDeleted());
179185
}
186+
187+
return this.unregisterSubscriptions;
180188
};
181189

190+
// TODO: maybe make these private across the SDK
182191
public unregisterSubscriptions = () => {
183192
this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction());
184193
this.unsubscribeFunctions.clear();
@@ -217,6 +226,24 @@ export class MessageComposer {
217226
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
218227
};
219228

229+
private subscribeMessageComposerSetupStateChange = () => {
230+
let cleanupBefore: (() => void) | null = null;
231+
const unsubscribe = this.client._messageComposerSetupState.subscribeWithSelector(
232+
({ applyModifications }) => ({
233+
applyModifications,
234+
}),
235+
({ applyModifications }) => {
236+
cleanupBefore?.();
237+
cleanupBefore = applyModifications?.({ composer: this }) ?? null;
238+
},
239+
);
240+
241+
return () => {
242+
cleanupBefore?.();
243+
unsubscribe();
244+
};
245+
};
246+
220247
private subscribeMessageDeleted = () =>
221248
this.client.on('message.deleted', (event) => {
222249
if (!event.message) return;

src/thread.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,6 @@ export class Thread {
183183
composition: threadData.draft,
184184
threadId: this.id,
185185
});
186-
187-
this._messageComposer.textComposer.use([
188-
createCommandsMiddleware(channel),
189-
createMentionsMiddleware(channel),
190-
] as TextComposerMiddleware[]);
191186
}
192187

193188
get messageComposer() {

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ export type KnownKeys<T> = {
4242
: never;
4343

4444
export type RequireAtLeastOne<T> = {
45-
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>;
45+
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
4646
}[keyof T];
4747

4848
export type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> &
4949
{
50-
[K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
50+
[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>;
5151
}[Keys];
5252

5353
/* Unknown Record */

0 commit comments

Comments
 (0)