Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: remove nin and ne operator usage in the SDK and the sample app #2672

Merged
merged 9 commits into from
Sep 18, 2024
25 changes: 7 additions & 18 deletions examples/SampleApp/src/hooks/usePaginatedUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ export const usePaginatedUsers = (): PaginatedUsers => {
try {
queryInProgress.current = true;
const filter: UserFilters = {
id: {
$nin: [chatClient?.userID],
},
role: 'user',
};

Expand All @@ -143,7 +140,7 @@ export const usePaginatedUsers = (): PaginatedUsers => {
return;
}

const res = await chatClient?.queryUsers(
const { users } = await chatClient?.queryUsers(
filter,
{ name: 1 },
{
Expand All @@ -153,33 +150,25 @@ export const usePaginatedUsers = (): PaginatedUsers => {
},
);

if (!res?.users) {
queryInProgress.current = false;
return;
}

// Dumb check to avoid duplicates
if (query === searchText && results.findIndex((r) => res?.users[0].id === r.id) > -1) {
queryInProgress.current = false;
return;
}
const usersWithoutClientUserId = users.filter((user) => user.id !== chatClient.userID);

setResults((r) => {
if (query !== searchText) {
return res?.users;
return usersWithoutClientUserId;
}
return r.concat(res?.users || []);
return r.concat(usersWithoutClientUserId);
});

if (res?.users.length < 10 && (offset.current === 0 || query === searchText)) {
if (usersWithoutClientUserId.length < 10 && (offset.current === 0 || query === searchText)) {
hasMoreResults.current = false;
}

if (!query && offset.current === 0) {
setInitialResults(res?.users || []);
setInitialResults(usersWithoutClientUserId);
}
} catch (e) {
// do nothing;
console.log('Error fetching users', e);
}
queryInProgress.current = false;
setLoading(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ import {
} from '../../contexts/translationContext/TranslationContext';
import type { Emoji } from '../../emoji-data';
import type { DefaultStreamChatGenerics } from '../../types/types';
import { isCommandTrigger, isEmojiTrigger, isMentionTrigger } from '../../utils/utils';

import type { Trigger } from '../../utils/utils';
import {
isCommandTrigger,
isEmojiTrigger,
isMentionTrigger,
Trigger,
} from '../../utils/ACITriggerSettings';

const styles = StyleSheet.create({
inputBox: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useMockedApis } from '../../../mock-builders/api/useMockedApis';
import { generateChannelResponse } from '../../../mock-builders/generator/channel';
import { generateUser } from '../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../mock-builders/mock';
import { ACITriggerSettings } from '../../../utils/utils';
import { ACITriggerSettings } from '../../../utils/ACITriggerSettings';
import { Chat } from '../../Chat/Chat';
import { AutoCompleteInput } from '../AutoCompleteInput';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,19 @@ import {
ImageUpload,
UnknownType,
} from '../../types/types';
import { compressedImageURI } from '../../utils/compressImage';
import { removeReservedFields } from '../../utils/removeReservedFields';
import {
ACITriggerSettings,
ACITriggerSettingsParams,
TriggerSettings,
} from '../../utils/ACITriggerSettings';
import { compressedImageURI } from '../../utils/compressImage';
import { removeReservedFields } from '../../utils/removeReservedFields';
import {
FileState,
FileStateValue,
generateRandomId,
getFileNameFromPath,
isBouncedMessage,
TriggerSettings,
} from '../../utils/utils';
import { useAttachmentPickerContext } from '../attachmentPickerContext/AttachmentPickerContext';
import { ChannelContextValue, useChannelContext } from '../channelContext/ChannelContext';
Expand Down
3 changes: 3 additions & 0 deletions package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ export * from './icons';

export * from './types/types';

export * from './utils/ACITriggerSettings';
export * from './utils/patchMessageTextCommand';
export * from './utils/i18n/Streami18n';
export * from './utils/queryMembers';
export * from './utils/queryUsers';
export * from './utils/utils';
export * from './utils/StreamChatRN';

Expand Down
269 changes: 269 additions & 0 deletions package/src/utils/ACITriggerSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import type { DebouncedFunc } from 'lodash';
import type { Channel, CommandResponse, StreamChat } from 'stream-chat';

import { defaultAutoCompleteSuggestionsLimit, defaultMentionAllAppUsersQuery } from './constants';
import { getMembersAndWatchers, queryMembersDebounced, QueryMembersFunction } from './queryMembers';
import { queryUsersDebounced, QueryUsersFunction } from './queryUsers';

import type {
EmojiSearchIndex,
MentionAllAppUsersQuery,
} from '../contexts/messageInputContext/MessageInputContext';
import type {
SuggestionCommand,
SuggestionComponentType,
SuggestionUser,
} from '../contexts/suggestionsContext/SuggestionsContext';
import { Emoji } from '../emoji-data';
import type { DefaultStreamChatGenerics } from '../types/types';

const getCommands = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
channel: Channel<StreamChatGenerics>,
) => channel.getConfig()?.commands || [];

export type TriggerSettings<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = {
'/'?: {
dataProvider: (
query: CommandResponse<StreamChatGenerics>['name'],
text: string,
onReady?: (
data: CommandResponse<StreamChatGenerics>[],
q: CommandResponse<StreamChatGenerics>['name'],
) => void,
options?: {
limit?: number;
},
) => SuggestionCommand<StreamChatGenerics>[];
output: (entity: CommandResponse<StreamChatGenerics>) => {
caretPosition: string;
key: string;
text: string;
};
type: SuggestionComponentType;
};
':'?: {
dataProvider: (
query: Emoji['name'],
_: string,
onReady?: (data: Emoji[], q: Emoji['name']) => void,
) => Emoji[] | Promise<Emoji[]>;
output: (entity: Emoji) => {
caretPosition: string;
key: string;
text: string;
};
type: SuggestionComponentType;
};
'@'?: {
callback: (item: SuggestionUser<StreamChatGenerics>) => void;
dataProvider: (
query: SuggestionUser<StreamChatGenerics>['name'],
_: string,
onReady?: (
data: SuggestionUser<StreamChatGenerics>[],
q: SuggestionUser<StreamChatGenerics>['name'],
) => void,
options?: {
limit?: number;
mentionAllAppUsersEnabled?: boolean;
mentionAllAppUsersQuery?: MentionAllAppUsersQuery<StreamChatGenerics>;
},
) => SuggestionUser<StreamChatGenerics>[] | Promise<void> | void;
output: (entity: SuggestionUser<StreamChatGenerics>) => {
caretPosition: string;
key: string;
text: string;
};
type: SuggestionComponentType;
};
};

export type ACITriggerSettingsParams<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = {
channel: Channel<StreamChatGenerics>;
client: StreamChat<StreamChatGenerics>;
onMentionSelectItem: (item: SuggestionUser<StreamChatGenerics>) => void;
emojiSearchIndex?: EmojiSearchIndex;
};

export const isCommandTrigger = (trigger: Trigger): trigger is '/' => trigger === '/';

export const isEmojiTrigger = (trigger: Trigger): trigger is ':' => trigger === ':';

export const isMentionTrigger = (trigger: Trigger): trigger is '@' => trigger === '@';

export type Trigger = '/' | '@' | ':';

/**
* ACI = AutoCompleteInput
*
* DataProvider accepts `onReady` function, which will execute once the data is ready.
* Another approach would have been to simply return the data from dataProvider and let the
* component await for it and then execute the required logic. We are going for callback instead
* of async-await since we have debounce function in dataProvider. Which will delay the execution
* of api call on trailing end of debounce (lets call it a1) but will return with result of
* previous call without waiting for a1. So in this case, we want to execute onReady, when trailing
* end of debounce executes.
*/
export const ACITriggerSettings = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>({
channel,
client,
emojiSearchIndex,
onMentionSelectItem,
}: ACITriggerSettingsParams<StreamChatGenerics>): TriggerSettings<StreamChatGenerics> => ({
'/': {
dataProvider: (query, text, onReady, options = {}) => {
if (text.indexOf('/') !== 0) return [];

const { limit = defaultAutoCompleteSuggestionsLimit } = options;
const selectedCommands = !query
? getCommands(channel)
: getCommands(channel).filter((command) => query && command.name?.indexOf(query) !== -1);

// sort alphabetically unless the you're matching the first char
selectedCommands.sort((a, b) => {
let nameA = a.name?.toLowerCase() || '';
let nameB = b.name?.toLowerCase() || '';
if (query && nameA.indexOf(query) === 0) {
nameA = `0${nameA}`;
}
if (query && nameB.indexOf(query) === 0) {
nameB = `0${nameB}`;
}
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;

return 0;
});

const result = selectedCommands.slice(0, limit);

if (onReady) {
onReady(result, query);
}

return result;
},
output: (entity) => ({
caretPosition: 'next',
key: `${entity.name}`,
text: `/${entity.name}`,
}),
type: 'command',
},
':': {
dataProvider: async (query, _, onReady) => {
if (!query) return [];

const emojis = (await emojiSearchIndex?.search(query)) ?? [];

if (onReady) {
onReady(emojis, query);
}

return emojis;
},
output: (entity) => ({
caretPosition: 'next',
key: entity.name,
text: entity.unicode,
}),
type: 'emoji',
},
'@': {
callback: (item) => {
onMentionSelectItem(item);
},
dataProvider: (
query,
_,
onReady,
options = {
limit: defaultAutoCompleteSuggestionsLimit,
mentionAllAppUsersEnabled: false,
mentionAllAppUsersQuery: defaultMentionAllAppUsersQuery,
},
) => {
try {
if (!query) return [];
if (options?.mentionAllAppUsersEnabled) {
return (queryUsersDebounced as DebouncedFunc<QueryUsersFunction<StreamChatGenerics>>)(
client,
query,
(data) => {
if (onReady) {
onReady(data, query);
}
},
{
limit: options.limit,
mentionAllAppUsersQuery: options.mentionAllAppUsersQuery,
},
);
}
/**
* By default, we return maximum 100 members via queryChannels api call.
* Thus it is safe to assume, that if number of members in channel.state is < 100,
* then all the members are already available on client side and we don't need to
* make any api call to queryMembers endpoint.
*/
if (Object.values(channel.state.members).length < 100) {
const users = getMembersAndWatchers(channel);

const matchingUsers = users.filter((user) => {
if (!query) return true;
// Don't show current authenticated user in the list
if (user.id === client.userID) {
return false;
}
if (user.name?.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
return true;
}
if (user.id.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
return true;
}
return false;
});

const data = matchingUsers.slice(0, options?.limit);

if (onReady) {
onReady(data, query);
}

return data;
}

return (queryMembersDebounced as DebouncedFunc<QueryMembersFunction<StreamChatGenerics>>)(
client,
channel,
query,
(data) => {
if (onReady) {
onReady(data, query);
}
},
{
limit: options.limit,
},
);
} catch (error) {
console.warn("Error querying users/members while using '@':", error);
throw error;
}
},
output: (entity) => ({
caretPosition: 'next',
key: entity.id,
text: `@${entity.name || entity.id}`,
}),
type: 'mention',
},
});
6 changes: 6 additions & 0 deletions package/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const defaultAutoCompleteSuggestionsLimit = 10;
export const defaultMentionAllAppUsersQuery = {
filters: {},
options: {},
sort: {},
};
Loading
Loading