Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/messageComposer/attachmentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ export class AttachmentManager {
this.composer.updateConfig({ attachments: { fileUploadFilter } });
}

get maxNumberOfFilesPerMessage() {
return this.config.maxNumberOfFilesPerMessage;
}

set maxNumberOfFilesPerMessage(
maxNumberOfFilesPerMessage: AttachmentManagerConfig['maxNumberOfFilesPerMessage'],
) {
if (maxNumberOfFilesPerMessage === this.maxNumberOfFilesPerMessage) return;
this.composer.updateConfig({ attachments: { maxNumberOfFilesPerMessage } });
}

Expand Down
1 change: 1 addition & 0 deletions src/messageComposer/linkPreviewsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export class LinkPreviewsManager implements ILinkPreviewsManager {
}

set enabled(enabled: LinkPreviewsManagerConfig['enabled']) {
if (enabled === this.enabled) return;
this.composer.updateConfig({ linkPreviews: { enabled } });
}

Expand Down
180 changes: 130 additions & 50 deletions src/messageComposer/middleware/pollComposer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
PollComposerFieldErrors,
PollComposerState,
PollComposerStateMiddlewareValueState,
TargetedPollOptionTextUpdate,
} from './types';
import { generateUUIDv4 } from '../../../utils';
import type { Middleware } from '../../../middleware';
Expand All @@ -10,20 +11,22 @@ export const VALID_MAX_VOTES_VALUE_REGEX = /^([2-9]|10)$/;

export const MAX_POLL_OPTIONS = 100 as const;

type ValidationOutput = Partial<
export type PollStateValidationOutput = Partial<
Omit<Record<keyof PollComposerState['data'], string>, 'options'> & {
options?: Record<string, string>;
}
>;

type Validator = (params: {
export type PollStateChangeValidator = (params: {
data: PollComposerState['data'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
currentError?: PollComposerFieldErrors[keyof PollComposerFieldErrors];
}) => ValidationOutput;
}) => PollStateValidationOutput;

const validators: Partial<Record<keyof PollComposerState['data'], Validator>> = {
export const pollStateChangeValidators: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
> = {
enforce_unique_vote: () => ({ max_votes_allowed: undefined }),
max_votes_allowed: ({ data, value }) => {
if (data.enforce_unique_vote && value)
Expand All @@ -49,14 +52,18 @@ const validators: Partial<Record<keyof PollComposerState['data'], Validator>> =
},
};

const changeValidators: Partial<Record<keyof PollComposerState['data'], Validator>> = {
export const defaultPollFieldChangeEventValidators: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
> = {
name: ({ currentError, value }) =>
value && currentError
? { name: undefined }
: { name: typeof currentError === 'string' ? currentError : undefined },
};

const blurValidators: Partial<Record<keyof PollComposerState['data'], Validator>> = {
export const defaultPollFieldBlurEventValidators: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
> = {
max_votes_allowed: ({ value }) => {
if (value && !value.match(VALID_MAX_VOTES_VALUE_REGEX))
return { max_votes_allowed: 'Type a number from 2 to 10' };
Expand All @@ -68,15 +75,24 @@ const blurValidators: Partial<Record<keyof PollComposerState['data'], Validator>
},
};

type ProcessorOutput = Partial<PollComposerState['data']>;
export type PollCompositionStateProcessorOutput = Partial<PollComposerState['data']>;

type Processor = (params: {
export type PollCompositionStateProcessor = (params: {
data: PollComposerState['data'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
}) => ProcessorOutput;
}) => PollCompositionStateProcessorOutput;

const processors: Partial<Record<keyof PollComposerState['data'], Processor>> = {
export const isTargetedOptionTextUpdate = (
value: unknown,
): value is TargetedPollOptionTextUpdate =>
!Array.isArray(value) &&
typeof (value as TargetedPollOptionTextUpdate)?.index === 'number' &&
typeof (value as TargetedPollOptionTextUpdate)?.text === 'string';

export const pollCompositionStateProcessors: Partial<
Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
> = {
enforce_unique_vote: ({ value }) => ({
enforce_unique_vote: value,
max_votes_allowed: '',
Expand Down Expand Up @@ -118,20 +134,53 @@ const processors: Partial<Record<keyof PollComposerState['data'], Processor>> =
},
};

export const createPollComposerStateMiddleware =
(): Middleware<PollComposerStateMiddlewareValueState> => ({
id: 'stream-io/poll-composer-state-processing',
handleFieldChange: ({
input,
nextHandler,
}: MiddlewareHandlerParams<PollComposerStateMiddlewareValueState>) => {
if (!input.state.targetFields) return nextHandler(input);
const {
state: { previousState, targetFields },
} = input;
const finalValidators = { ...validators, ...changeValidators };
export type PollComposerStateMiddlewareFactoryOptions = {
processors?: {
handleFieldChange?: Partial<
Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
>;
handleFieldBlur?: Partial<
Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
>;
};
validators?: {
handleFieldChange?: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
>;
handleFieldBlur?: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
>;
};
};

export const createPollComposerStateMiddleware = ({
processors: customProcessors,
validators: customValidators,
}: PollComposerStateMiddlewareFactoryOptions = {}): Middleware<PollComposerStateMiddlewareValueState> => {
const universalHandler = (
state: PollComposerStateMiddlewareValueState,
validators: Partial<
Record<keyof PollComposerState['data'], PollStateChangeValidator>
>,
processors?: Partial<
Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
>,
) => {
const { previousState, targetFields } = state;

const newData = Object.entries(targetFields).reduce(
let newData: Partial<PollComposerState['data']>;
if (!processors && isTargetedOptionTextUpdate(targetFields.options)) {
const options = [...previousState.data.options];
const targetOption = previousState.data.options[targetFields.options.index];
if (targetOption) {
targetOption.text = targetFields.options.text;
options.splice(targetFields.options.index, 1, targetOption);
}
newData = { ...targetFields, options };
} else if (!processors) {
newData = targetFields as PollComposerState['data'];
} else {
newData = Object.entries(targetFields).reduce(
(acc, [key, value]) => {
const processor = processors[key as keyof PollComposerState['data']];
acc = {
Expand All @@ -144,19 +193,49 @@ export const createPollComposerStateMiddleware =
},
{} as PollComposerState['data'],
);
}

const newErrors = Object.keys(targetFields).reduce((acc, key) => {
const validator = validators[key as keyof PollComposerState['data']];
if (validator) {
const error = validator({
data: previousState.data,
value: newData[key as keyof PollComposerState['data']],
currentError: previousState.errors[key as keyof PollComposerState['data']],
});
acc = { ...acc, ...error };
}
return acc;
}, {} as PollComposerFieldErrors);

return { newData, newErrors };
};

return {
id: 'stream-io/poll-composer-state-processing',
handleFieldChange: ({
input,
nextHandler,
}: MiddlewareHandlerParams<PollComposerStateMiddlewareValueState>) => {
if (!input.state.targetFields) return nextHandler(input);
const {
state: { previousState },
} = input;
const finalValidators = {
...pollStateChangeValidators,
...defaultPollFieldChangeEventValidators,
...customValidators?.handleFieldChange,
};
const finalProcessors = {
...pollCompositionStateProcessors,
...customProcessors?.handleFieldChange,
};

const newErrors = Object.keys(targetFields).reduce((acc, key) => {
const validator = finalValidators[key as keyof PollComposerState['data']];
if (validator) {
const error = validator({
data: previousState.data,
value: newData[key as keyof PollComposerState['data']],
currentError: previousState.errors[key as keyof PollComposerState['data']],
});
acc = { ...acc, ...error };
}
return acc;
}, {} as PollComposerFieldErrors);
const { newData, newErrors } = universalHandler(
input.state,
finalValidators,
finalProcessors,
);

return nextHandler({
...input,
Expand All @@ -174,32 +253,33 @@ export const createPollComposerStateMiddleware =
input,
nextHandler,
}: MiddlewareHandlerParams<PollComposerStateMiddlewareValueState>) => {
if (!input.state.targetFields) return nextHandler(input);

const {
state: { previousState, targetFields },
state: { previousState },
} = input;
const finalValidators = { ...validators, ...blurValidators };
const newErrors = Object.entries(targetFields).reduce((acc, [key, value]) => {
const validator = finalValidators[key as keyof PollComposerState['data']];
if (validator) {
const error = validator({
data: previousState.data,
value,
currentError: previousState.errors[key as keyof PollComposerState['data']],
});
acc = { ...acc, ...error };
}
return acc;
}, {} as PollComposerFieldErrors);
const finalValidators = {
...pollStateChangeValidators,
...defaultPollFieldBlurEventValidators,
...customValidators?.handleFieldBlur,
};
const { newData, newErrors } = universalHandler(
input.state,
finalValidators,
customProcessors?.handleFieldBlur,
);

return nextHandler({
...input,
state: {
...input.state,
nextState: {
...previousState,
data: { ...previousState.data, ...newData },
errors: { ...previousState.errors, ...newErrors },
},
},
});
},
});
};
};
10 changes: 6 additions & 4 deletions src/messageComposer/middleware/pollComposer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ export type PollComposerOption = {
text: string;
};

export type TargetedPollOptionTextUpdate = {
index: number;
text: string;
};

export type PollComposerOptionUpdate =
| PollComposerOption[]
| {
index: number;
text: string;
};
| TargetedPollOptionTextUpdate;

export type UpdateFieldsData = Partial<Omit<PollComposerState['data'], 'options'>> & {
options?: PollComposerOptionUpdate;
Expand Down
2 changes: 1 addition & 1 deletion src/messageComposer/middleware/textComposer/mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export const createMentionsMiddleware = (
trigger: finalOptions.trigger,
},
},
stop: true, // Stop other middleware from processing '@' character
status: 'complete', // Stop other middleware from processing '@' character
});
},
onSuggestionItemSelect: ({
Expand Down
38 changes: 29 additions & 9 deletions src/messageComposer/textComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,43 @@ export class TextComposer {
}

set enabled(enabled: boolean) {
if (enabled === this.enabled) return;
this.composer.updateConfig({ text: { enabled } });
}

set defaultValue(defaultValue: string) {
get defaultValue() {
return this.composer.config.text.defaultValue;
}

set defaultValue(defaultValue: string | undefined) {
if (defaultValue === this.defaultValue) return;
this.composer.updateConfig({ text: { defaultValue } });
}

set maxLengthOnEdit(maxLengthOnEdit: number) {
get maxLengthOnEdit() {
return this.composer.config.text.maxLengthOnEdit;
}

set maxLengthOnEdit(maxLengthOnEdit: number | undefined) {
if (maxLengthOnEdit === this.maxLengthOnEdit) return;
this.composer.updateConfig({ text: { maxLengthOnEdit } });
}

set maxLengthOnSend(maxLengthOnSend: number) {
get maxLengthOnSend() {
return this.composer.config.text.maxLengthOnSend;
}

set maxLengthOnSend(maxLengthOnSend: number | undefined) {
if (maxLengthOnSend === this.maxLengthOnSend) return;
this.composer.updateConfig({ text: { maxLengthOnSend } });
}

get publishTypingEvents() {
return this.composer.config.text.publishTypingEvents;
}

set publishTypingEvents(publishTypingEvents: boolean) {
if (publishTypingEvents === this.publishTypingEvents) return;
this.composer.updateConfig({ text: { publishTypingEvents } });
}

Expand Down Expand Up @@ -151,22 +172,21 @@ export class TextComposer {
};

setText = (text: string) => {
if (!this.enabled) return;
if (!this.enabled || text === this.text) return;
this.state.partialNext({ text });
};

setSelection = (selection: TextSelection) => {
if (!this.enabled) return;
const selectionChanged =
selection.start !== this.selection.start || selection.end !== this.selection.end;
if (!this.enabled || !selectionChanged) return;
this.state.partialNext({ selection });
};

insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => {
if (!this.enabled) return;

const finalSelection: TextSelection = selection ?? {
start: this.text.length,
end: this.text.length,
};
const finalSelection: TextSelection = selection ?? this.selection;
const { maxLengthOnEdit } = this.composer.config.text ?? {};
const currentText = this.text;
const textBeforeTrim = [
Expand Down
Loading
Loading