@planningcenter/chat-react-native 4.3.0-rc.9 → 4.3.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 (75) hide show
  1. package/build/components/conversation/message_form.js +1 -1
  2. package/build/components/conversation/message_form.js.map +1 -1
  3. package/build/hooks/use_debounced_search_term.d.ts +2 -0
  4. package/build/hooks/use_debounced_search_term.d.ts.map +1 -0
  5. package/build/hooks/use_debounced_search_term.js +40 -0
  6. package/build/hooks/use_debounced_search_term.js.map +1 -0
  7. package/build/hooks/use_search_results.d.ts +19 -0
  8. package/build/hooks/use_search_results.d.ts.map +1 -0
  9. package/build/hooks/use_search_results.js +153 -0
  10. package/build/hooks/use_search_results.js.map +1 -0
  11. package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts +2 -1
  12. package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts.map +1 -1
  13. package/build/screens/conversation_select_recipients/components/view_more_link_row.js +39 -24
  14. package/build/screens/conversation_select_recipients/components/view_more_link_row.js.map +1 -1
  15. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
  16. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +142 -158
  17. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
  18. package/build/screens/search/components/conversation_result_row.d.ts +8 -0
  19. package/build/screens/search/components/conversation_result_row.d.ts.map +1 -0
  20. package/build/screens/search/components/conversation_result_row.js +80 -0
  21. package/build/screens/search/components/conversation_result_row.js.map +1 -0
  22. package/build/screens/search/components/conversation_results.d.ts +8 -0
  23. package/build/screens/search/components/conversation_results.d.ts.map +1 -0
  24. package/build/screens/search/components/conversation_results.js +77 -0
  25. package/build/screens/search/components/conversation_results.js.map +1 -0
  26. package/build/screens/search/components/message_result_row.d.ts +8 -0
  27. package/build/screens/search/components/message_result_row.d.ts.map +1 -0
  28. package/build/screens/search/components/message_result_row.js +152 -0
  29. package/build/screens/search/components/message_result_row.js.map +1 -0
  30. package/build/screens/search/components/result_row.d.ts +10 -0
  31. package/build/screens/search/components/result_row.d.ts.map +1 -0
  32. package/build/screens/search/components/result_row.js +102 -0
  33. package/build/screens/search/components/result_row.js.map +1 -0
  34. package/build/screens/search/components/results_section.d.ts +9 -0
  35. package/build/screens/search/components/results_section.d.ts.map +1 -0
  36. package/build/screens/search/components/results_section.js +51 -0
  37. package/build/screens/search/components/results_section.js.map +1 -0
  38. package/build/screens/search/components/search_results_list.d.ts +8 -0
  39. package/build/screens/search/components/search_results_list.d.ts.map +1 -0
  40. package/build/screens/search/components/search_results_list.js +314 -0
  41. package/build/screens/search/components/search_results_list.js.map +1 -0
  42. package/build/screens/search/components/snippet.d.ts +8 -0
  43. package/build/screens/search/components/snippet.d.ts.map +1 -0
  44. package/build/screens/search/components/snippet.js +68 -0
  45. package/build/screens/search/components/snippet.js.map +1 -0
  46. package/build/screens/search/search_screen.d.ts.map +1 -1
  47. package/build/screens/search/search_screen.js +18 -9
  48. package/build/screens/search/search_screen.js.map +1 -1
  49. package/build/utils/client/request_helpers.js +1 -1
  50. package/build/utils/client/request_helpers.js.map +1 -1
  51. package/package.json +3 -3
  52. package/src/__tests__/utils/client.ts +3 -4
  53. package/src/components/conversation/__tests__/message_form.test.tsx +83 -0
  54. package/src/components/conversation/message_form.tsx +1 -1
  55. package/src/hooks/__tests__/use_debounced_search_term.test.tsx +38 -0
  56. package/src/hooks/__tests__/use_search_results.test.tsx +88 -0
  57. package/src/hooks/use_debounced_search_term.ts +20 -0
  58. package/src/hooks/use_search_results.ts +78 -0
  59. package/src/screens/conversation_select_recipients/components/view_more_link_row.tsx +12 -3
  60. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +3 -8
  61. package/src/screens/search/__tests__/search_screen.test.tsx +64 -0
  62. package/src/screens/search/components/__tests__/conversation_results.test.tsx +55 -0
  63. package/src/screens/search/components/__tests__/message_result_row.test.tsx +65 -0
  64. package/src/screens/search/components/__tests__/result_row.test.tsx +33 -0
  65. package/src/screens/search/components/__tests__/search_results_list.test.tsx +135 -0
  66. package/src/screens/search/components/conversation_result_row.tsx +36 -0
  67. package/src/screens/search/components/conversation_results.tsx +39 -0
  68. package/src/screens/search/components/message_result_row.tsx +48 -0
  69. package/src/screens/search/components/result_row.tsx +58 -0
  70. package/src/screens/search/components/results_section.tsx +34 -0
  71. package/src/screens/search/components/search_results_list.tsx +147 -0
  72. package/src/screens/search/components/snippet.tsx +37 -0
  73. package/src/screens/search/search_screen.tsx +9 -1
  74. package/src/utils/client/__tests__/request_helpers.test.ts +40 -0
  75. package/src/utils/client/request_helpers.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search_screen.d.ts","sourceRoot":"","sources":["../../../src/screens/search/search_screen.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACvB,MAAM,gCAAgC,CAAA;AAWvC,eAAO,MAAM,mBAAmB,GAAI,iBAEjC,sBAAsB,CAAC,aAAa,CAAC,KAAG,4BAIxC,CAAA;AAEH,wBAAgB,YAAY,4CAW3B"}
1
+ {"version":3,"file":"search_screen.d.ts","sourceRoot":"","sources":["../../../src/screens/search/search_screen.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACvB,MAAM,gCAAgC,CAAA;AAcvC,eAAO,MAAM,mBAAmB,GAAI,iBAEjC,sBAAsB,CAAC,aAAa,CAAC,KAAG,4BAIxC,CAAA;AAEH,wBAAgB,YAAY,4CAgB3B"}
@@ -1,10 +1,13 @@
1
1
  import { c as _c } from "react/compiler-runtime";
2
+ import { noop } from 'lodash';
2
3
  import React, { useState } from 'react';
3
4
  import { StyleSheet, View } from 'react-native';
4
5
  import { KeyboardView } from '../../components';
5
6
  import { composeOptions, withMinimalBack } from '../../components/display/platform_modal_header_buttons';
7
+ import { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term';
6
8
  import { SearchPromptBlankState } from './components/search_blank_states';
7
9
  import { SearchField } from './components/search_field';
10
+ import { SearchResultsList } from './components/search_results_list';
8
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
12
  export const SearchScreenOptions = ({
10
13
  navigation
@@ -15,8 +18,9 @@ export const SearchScreenOptions = ({
15
18
  onPress: () => navigation.goBack()
16
19
  }));
17
20
  export function SearchScreen() {
18
- const $ = _c(5);
21
+ const $ = _c(7);
19
22
  const [term, setTerm] = useState("");
23
+ const debouncedTerm = useDebouncedSearchTerm(term);
20
24
  let t0;
21
25
  if ($[0] !== term) {
22
26
  t0 = /*#__PURE__*/_jsx(SearchField, {
@@ -29,14 +33,18 @@ export function SearchScreen() {
29
33
  t0 = $[1];
30
34
  }
31
35
  let t1;
32
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
33
- t1 = /*#__PURE__*/_jsx(SearchPromptBlankState, {});
34
- $[2] = t1;
36
+ if ($[2] !== debouncedTerm) {
37
+ t1 = debouncedTerm.length > 0 ? /*#__PURE__*/_jsx(SearchResultsList, {
38
+ term: debouncedTerm,
39
+ onSelect: noop
40
+ }, debouncedTerm) : /*#__PURE__*/_jsx(SearchPromptBlankState, {});
41
+ $[2] = debouncedTerm;
42
+ $[3] = t1;
35
43
  } else {
36
- t1 = $[2];
44
+ t1 = $[3];
37
45
  }
38
46
  let t2;
39
- if ($[3] !== t0) {
47
+ if ($[4] !== t0 || $[5] !== t1) {
40
48
  t2 = /*#__PURE__*/_jsx(KeyboardView, {
41
49
  children: /*#__PURE__*/_jsxs(View, {
42
50
  style: styles.container,
@@ -44,10 +52,11 @@ export function SearchScreen() {
44
52
  children: [t0, t1]
45
53
  })
46
54
  });
47
- $[3] = t0;
48
- $[4] = t2;
55
+ $[4] = t0;
56
+ $[5] = t1;
57
+ $[6] = t2;
49
58
  } else {
50
- t2 = $[4];
59
+ t2 = $[6];
51
60
  }
52
61
  return t2;
53
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search_screen.js","names":["React","useState","StyleSheet","View","KeyboardView","composeOptions","withMinimalBack","SearchPromptBlankState","SearchField","jsx","_jsx","jsxs","_jsxs","SearchScreenOptions","navigation","title","animation","onPress","goBack","SearchScreen","$","_c","term","setTerm","t0","t1","Symbol","for","t2","children","style","styles","container","testID","create","flex"],"sources":["../../../src/screens/search/search_screen.tsx"],"sourcesContent":["import { ParamListBase } from '@react-navigation/native'\nimport {\n NativeStackNavigationOptions,\n NativeStackScreenProps,\n} from '@react-navigation/native-stack'\nimport React, { useState } from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport { KeyboardView } from '../../components'\nimport {\n composeOptions,\n withMinimalBack,\n} from '../../components/display/platform_modal_header_buttons'\nimport { SearchPromptBlankState } from './components/search_blank_states'\nimport { SearchField } from './components/search_field'\n\nexport const SearchScreenOptions = ({\n navigation,\n}: NativeStackScreenProps<ParamListBase>): NativeStackNavigationOptions =>\n composeOptions(\n { title: 'Search', animation: 'fade' },\n withMinimalBack({ onPress: () => navigation.goBack() })\n )\n\nexport function SearchScreen() {\n const [term, setTerm] = useState('')\n\n return (\n <KeyboardView>\n <View style={styles.container} testID=\"search-screen\">\n <SearchField term={term} onChange={setTerm} />\n <SearchPromptBlankState />\n </View>\n </KeyboardView>\n )\n}\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n },\n})\n"],"mappings":";AAKA,OAAOA,KAAK,IAAIC,QAAQ,QAAQ,OAAO;AACvC,SAASC,UAAU,EAAEC,IAAI,QAAQ,cAAc;AAC/C,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SACEC,cAAc,EACdC,eAAe,QACV,wDAAwD;AAC/D,SAASC,sBAAsB,QAAQ,kCAAkC;AACzE,SAASC,WAAW,QAAQ,2BAA2B;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAEvD,OAAO,MAAMC,mBAAmB,GAAGA,CAAC;EAClCC;AACqC,CAAC,KACtCT,cAAc,CACZ;EAAEU,KAAK,EAAE,QAAQ;EAAEC,SAAS,EAAE;AAAO,CAAC,EACtCV,eAAe,CAAC;EAAEW,OAAO,EAAEA,CAAA,KAAMH,UAAU,CAACI,MAAM,CAAC;AAAE,CAAC,CACxD,CAAC;AAEH,OAAO,SAAAC,aAAA;EAAA,MAAAC,CAAA,GAAAC,EAAA;EACL,OAAAC,IAAA,EAAAC,OAAA,IAAwBtB,QAAQ,CAAC,EAAE,CAAC;EAAA,IAAAuB,EAAA;EAAA,IAAAJ,CAAA,QAAAE,IAAA;IAK9BE,EAAA,gBAAAd,IAAA,CAACF,WAAW;MAAOc,IAAI,EAAJA,IAAI;MAAYC,QAAO,EAAPA;IAAO,CAAG,CAAC;IAAAH,CAAA,MAAAE,IAAA;IAAAF,CAAA,MAAAI,EAAA;EAAA;IAAAA,EAAA,GAAAJ,CAAA;EAAA;EAAA,IAAAK,EAAA;EAAA,IAAAL,CAAA,QAAAM,MAAA,CAAAC,GAAA;IAC9CF,EAAA,gBAAAf,IAAA,CAACH,sBAAsB,IAAE,CAAC;IAAAa,CAAA,MAAAK,EAAA;EAAA;IAAAA,EAAA,GAAAL,CAAA;EAAA;EAAA,IAAAQ,EAAA;EAAA,IAAAR,CAAA,QAAAI,EAAA;IAH9BI,EAAA,gBAAAlB,IAAA,CAACN,YAAY;MAAAyB,QAAA,eACXjB,KAAA,CAACT,IAAI;QAAQ2B,KAAgB,EAAhBC,MAAM,CAAAC,SAAU;QAASC,MAAe,EAAf,eAAe;QAAAJ,QAAA,GACnDL,EAA8C,EAC9CC,EAA0B;MAAA,CACtB;IAAC,CACK,CAAC;IAAAL,CAAA,MAAAI,EAAA;IAAAJ,CAAA,MAAAQ,EAAA;EAAA;IAAAA,EAAA,GAAAR,CAAA;EAAA;EAAA,OALfQ,EAKe;AAAA;AAInB,MAAMG,MAAM,GAAG7B,UAAU,CAACgC,MAAM,CAAC;EAC/BF,SAAS,EAAE;IACTG,IAAI,EAAE;EACR;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"search_screen.js","names":["noop","React","useState","StyleSheet","View","KeyboardView","composeOptions","withMinimalBack","useDebouncedSearchTerm","SearchPromptBlankState","SearchField","SearchResultsList","jsx","_jsx","jsxs","_jsxs","SearchScreenOptions","navigation","title","animation","onPress","goBack","SearchScreen","$","_c","term","setTerm","debouncedTerm","t0","t1","length","t2","children","style","styles","container","testID","create","flex"],"sources":["../../../src/screens/search/search_screen.tsx"],"sourcesContent":["import { ParamListBase } from '@react-navigation/native'\nimport {\n NativeStackNavigationOptions,\n NativeStackScreenProps,\n} from '@react-navigation/native-stack'\nimport { noop } from 'lodash'\nimport React, { useState } from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport { KeyboardView } from '../../components'\nimport {\n composeOptions,\n withMinimalBack,\n} from '../../components/display/platform_modal_header_buttons'\nimport { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term'\nimport { SearchPromptBlankState } from './components/search_blank_states'\nimport { SearchField } from './components/search_field'\nimport { SearchResultsList } from './components/search_results_list'\n\nexport const SearchScreenOptions = ({\n navigation,\n}: NativeStackScreenProps<ParamListBase>): NativeStackNavigationOptions =>\n composeOptions(\n { title: 'Search', animation: 'fade' },\n withMinimalBack({ onPress: () => navigation.goBack() })\n )\n\nexport function SearchScreen() {\n const [term, setTerm] = useState('')\n const debouncedTerm = useDebouncedSearchTerm(term)\n\n return (\n <KeyboardView>\n <View style={styles.container} testID=\"search-screen\">\n <SearchField term={term} onChange={setTerm} />\n {debouncedTerm.length > 0 ? (\n <SearchResultsList key={debouncedTerm} term={debouncedTerm} onSelect={noop} />\n ) : (\n <SearchPromptBlankState />\n )}\n </View>\n </KeyboardView>\n )\n}\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n },\n})\n"],"mappings":";AAKA,SAASA,IAAI,QAAQ,QAAQ;AAC7B,OAAOC,KAAK,IAAIC,QAAQ,QAAQ,OAAO;AACvC,SAASC,UAAU,EAAEC,IAAI,QAAQ,cAAc;AAC/C,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SACEC,cAAc,EACdC,eAAe,QACV,wDAAwD;AAC/D,SAASC,sBAAsB,QAAQ,uCAAuC;AAC9E,SAASC,sBAAsB,QAAQ,kCAAkC;AACzE,SAASC,WAAW,QAAQ,2BAA2B;AACvD,SAASC,iBAAiB,QAAQ,kCAAkC;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAEpE,OAAO,MAAMC,mBAAmB,GAAGA,CAAC;EAClCC;AACqC,CAAC,KACtCX,cAAc,CACZ;EAAEY,KAAK,EAAE,QAAQ;EAAEC,SAAS,EAAE;AAAO,CAAC,EACtCZ,eAAe,CAAC;EAAEa,OAAO,EAAEA,CAAA,KAAMH,UAAU,CAACI,MAAM,CAAC;AAAE,CAAC,CACxD,CAAC;AAEH,OAAO,SAAAC,aAAA;EAAA,MAAAC,CAAA,GAAAC,EAAA;EACL,OAAAC,IAAA,EAAAC,OAAA,IAAwBxB,QAAQ,CAAC,EAAE,CAAC;EACpC,MAAAyB,aAAA,GAAsBnB,sBAAsB,CAACiB,IAAI,CAAC;EAAA,IAAAG,EAAA;EAAA,IAAAL,CAAA,QAAAE,IAAA;IAK5CG,EAAA,gBAAAf,IAAA,CAACH,WAAW;MAAOe,IAAI,EAAJA,IAAI;MAAYC,QAAO,EAAPA;IAAO,CAAG,CAAC;IAAAH,CAAA,MAAAE,IAAA;IAAAF,CAAA,MAAAK,EAAA;EAAA;IAAAA,EAAA,GAAAL,CAAA;EAAA;EAAA,IAAAM,EAAA;EAAA,IAAAN,CAAA,QAAAI,aAAA;IAC7CE,EAAA,GAAAF,aAAa,CAAAG,MAAO,GAAG,CAIvB,gBAJAjB,IAAA,CACEF,iBAAiB;MAA2BgB,IAAa,EAAbA,aAAa;MAAY3B,QAAI,EAAJA;IAAI,GAAlD2B,aAG1B,CAAC,gBAJAd,IAAA,CAGEJ,sBAAsB,IACzB,CAAC;IAAAc,CAAA,MAAAI,aAAA;IAAAJ,CAAA,MAAAM,EAAA;EAAA;IAAAA,EAAA,GAAAN,CAAA;EAAA;EAAA,IAAAQ,EAAA;EAAA,IAAAR,CAAA,QAAAK,EAAA,IAAAL,CAAA,QAAAM,EAAA;IAPLE,EAAA,gBAAAlB,IAAA,CAACR,YAAY;MAAA2B,QAAA,eACXjB,KAAA,CAACX,IAAI;QAAQ6B,KAAgB,EAAhBC,MAAM,CAAAC,SAAU;QAASC,MAAe,EAAf,eAAe;QAAAJ,QAAA,GACnDJ,EAA8C,EAC7CC,EAIA;MAAA,CACG;IAAC,CACK,CAAC;IAAAN,CAAA,MAAAK,EAAA;IAAAL,CAAA,MAAAM,EAAA;IAAAN,CAAA,MAAAQ,EAAA;EAAA;IAAAA,EAAA,GAAAR,CAAA;EAAA;EAAA,OATfQ,EASe;AAAA;AAInB,MAAMG,MAAM,GAAG/B,UAAU,CAACkC,MAAM,CAAC;EAC/BF,SAAS,EAAE;IACTG,IAAI,EAAE;EACR;AACF,CAAC,CAAC","ignoreList":[]}
@@ -27,7 +27,7 @@ export const makeRequest = ({
27
27
  uri = uri.query(combinedQuery);
28
28
  }
29
29
  const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined;
30
- return instrumentedFetch(decodeURIComponent(uri.toString()), {
30
+ return instrumentedFetch(uri.toString(), {
31
31
  method: action,
32
32
  headers,
33
33
  body
@@ -1 +1 @@
1
- {"version":3,"file":"request_helpers.js","names":["_","URI","instrumentedFetch","transformRequestData","transformResponse","makeRequest","action","url","data","headers","uri","query","urlQuery","combinedQuery","Object","entries","sort","reduce","obj","key","value","body","includes","JSON","stringify","undefined","decodeURIComponent","toString","method","then","response","ok","text","t","parse","Promise","reject","STANDARD_META_ATTRIBUTES","concatRecords","records","moreRecords","included","meta","pick","throwErrorIfFieldsMissing","walk","args","fields","Error","fieldTypes","keys","dataTypes","castArray","map","includedTypes","responseTypes","uniq","concat","missingFields","difference","filter","length","friendlyErrors","throwErrorIfQueryParams","noQueryParamsFriendlyErrors","request","passthrough","resolve","ensureFieldsInDev","__DEV__","ensureNoQueryParamsInDev","search","transformGetToPost","rest","include","fieldsArray","Array","isArray","join","fromEntries"],"sources":["../../../src/utils/client/request_helpers.ts"],"sourcesContent":["import _ from 'lodash'\nimport URI from 'urijs'\nimport { instrumentedFetch } from './instrumented_fetch'\nimport { transformRequestData } from './transform_request_data'\nimport transformResponse from './transform_response'\nimport { Accumulator, GetRequest, PostRequest, RequestData, WalkRequest } from './types'\n\nexport type MakeRequestArgs = {\n action: 'GET' | 'POST' | 'PATCH' | 'DELETE'\n url: string\n data?: Partial<RequestData>\n headers: Record<string, string>\n}\n\nexport const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }: MakeRequestArgs) => {\n // break apart url so we can reconstruct the url with the query params\n let uri = new URI(url)\n const query = transformRequestData(data)\n const urlQuery = uri.query(true)\n\n // This likely doesn't matter but will enforce consistent ordering of query params\n const combinedQuery = Object.entries({ ...query, ...urlQuery })\n .sort()\n .reduce((obj, [key, value]) => {\n // @ts-ignore\n obj[key] = value\n return obj\n }, {})\n\n if (action === 'GET') {\n uri = uri.query(combinedQuery)\n }\n\n const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined\n\n return instrumentedFetch(decodeURIComponent(uri.toString()), {\n method: action,\n headers,\n body,\n }).then(response => {\n if (response.ok) {\n return response.text().then(t => (t === '' ? '' : JSON.parse(t)))\n } else {\n return Promise.reject(response)\n }\n })\n}\n\nconst STANDARD_META_ATTRIBUTES = [\n 'total_count',\n 'count',\n 'prev',\n 'next',\n 'can_order_by',\n 'can_query_by',\n 'can_include',\n 'can_filter',\n 'parent',\n]\n\nexport const concatRecords = (records: Accumulator, moreRecords: Accumulator) => ({\n data: [...records.data, ...moreRecords.data],\n included: [...records.included, ...moreRecords.included],\n // Because custom meta can be computed on a per-page bases,\n // it is safer to remove all custom meta when concatenating multiple pages of data\n meta: _.pick(moreRecords.meta, STANDARD_META_ATTRIBUTES),\n})\n\ntype Walk = (args: MakeRequestArgs | WalkRequest) => Promise<any>\ntype WalkArgs = MakeRequestArgs & {\n data?: {\n fields?: Record<string, any>\n }\n}\n\nexport const throwErrorIfFieldsMissing = async (walk: Walk, args: WalkArgs) => {\n const fields = args?.data?.fields\n\n if (!fields) {\n throw new Error(`Must pass fields for request: ${args.url}`)\n }\n\n const fieldTypes = Object.keys(fields)\n\n const response = await walk(args)\n\n const dataTypes = _(response.data).castArray().map('type').value()\n const includedTypes = _.map(response.included, 'type')\n const responseTypes = _.uniq(dataTypes.concat(includedTypes))\n const missingFields = _.difference(responseTypes, fieldTypes).filter(t => t)\n\n if (missingFields.length > 0) {\n throw new Error(\n `Must include fields for types: ${JSON.stringify(missingFields)} for request ${args.url}`\n )\n }\n\n return transformResponse(response)\n}\n\nexport const friendlyErrors = (walk: Walk, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => throwErrorIfFieldsMissing(walk, args))\n .then(transformResponse)\n\nexport const noQueryParamsFriendlyErrors = (request: typeof makeRequest, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => request(args))\n .then(transformResponse)\n\nconst passthrough = async <T, R>(walk: (args: T) => Promise<R>, args: T) =>\n Promise.resolve(transformResponse(await walk(args)))\n\nexport const ensureFieldsInDev = __DEV__ ? friendlyErrors : passthrough\n\nexport const ensureNoQueryParamsInDev = __DEV__ ? noQueryParamsFriendlyErrors : passthrough\n\nexport const throwErrorIfQueryParams = (url: string) => {\n if (new URI(url).search().length) {\n throw new Error('Must pass query params as data arg')\n }\n\n return Promise.resolve()\n}\n\nexport const transformGetToPost = (args: GetRequest): PostRequest => {\n const { data, ...rest } = args\n const { fields, include } = data\n const fieldsArray = Object.entries(fields).map(([key, value]) => {\n if (Array.isArray(value)) {\n return [key, value.join(',')]\n }\n\n return [key, value]\n })\n\n return {\n ...rest,\n data: {\n fields: Object.fromEntries(fieldsArray),\n include,\n },\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"request_helpers.js","names":["_","URI","instrumentedFetch","transformRequestData","transformResponse","makeRequest","action","url","data","headers","uri","query","urlQuery","combinedQuery","Object","entries","sort","reduce","obj","key","value","body","includes","JSON","stringify","undefined","toString","method","then","response","ok","text","t","parse","Promise","reject","STANDARD_META_ATTRIBUTES","concatRecords","records","moreRecords","included","meta","pick","throwErrorIfFieldsMissing","walk","args","fields","Error","fieldTypes","keys","dataTypes","castArray","map","includedTypes","responseTypes","uniq","concat","missingFields","difference","filter","length","friendlyErrors","throwErrorIfQueryParams","noQueryParamsFriendlyErrors","request","passthrough","resolve","ensureFieldsInDev","__DEV__","ensureNoQueryParamsInDev","search","transformGetToPost","rest","include","fieldsArray","Array","isArray","join","fromEntries"],"sources":["../../../src/utils/client/request_helpers.ts"],"sourcesContent":["import _ from 'lodash'\nimport URI from 'urijs'\nimport { instrumentedFetch } from './instrumented_fetch'\nimport { transformRequestData } from './transform_request_data'\nimport transformResponse from './transform_response'\nimport { Accumulator, GetRequest, PostRequest, RequestData, WalkRequest } from './types'\n\nexport type MakeRequestArgs = {\n action: 'GET' | 'POST' | 'PATCH' | 'DELETE'\n url: string\n data?: Partial<RequestData>\n headers: Record<string, string>\n}\n\nexport const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }: MakeRequestArgs) => {\n // break apart url so we can reconstruct the url with the query params\n let uri = new URI(url)\n const query = transformRequestData(data)\n const urlQuery = uri.query(true)\n\n // This likely doesn't matter but will enforce consistent ordering of query params\n const combinedQuery = Object.entries({ ...query, ...urlQuery })\n .sort()\n .reduce((obj, [key, value]) => {\n // @ts-ignore\n obj[key] = value\n return obj\n }, {})\n\n if (action === 'GET') {\n uri = uri.query(combinedQuery)\n }\n\n const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined\n\n return instrumentedFetch(uri.toString(), {\n method: action,\n headers,\n body,\n }).then(response => {\n if (response.ok) {\n return response.text().then(t => (t === '' ? '' : JSON.parse(t)))\n } else {\n return Promise.reject(response)\n }\n })\n}\n\nconst STANDARD_META_ATTRIBUTES = [\n 'total_count',\n 'count',\n 'prev',\n 'next',\n 'can_order_by',\n 'can_query_by',\n 'can_include',\n 'can_filter',\n 'parent',\n]\n\nexport const concatRecords = (records: Accumulator, moreRecords: Accumulator) => ({\n data: [...records.data, ...moreRecords.data],\n included: [...records.included, ...moreRecords.included],\n // Because custom meta can be computed on a per-page bases,\n // it is safer to remove all custom meta when concatenating multiple pages of data\n meta: _.pick(moreRecords.meta, STANDARD_META_ATTRIBUTES),\n})\n\ntype Walk = (args: MakeRequestArgs | WalkRequest) => Promise<any>\ntype WalkArgs = MakeRequestArgs & {\n data?: {\n fields?: Record<string, any>\n }\n}\n\nexport const throwErrorIfFieldsMissing = async (walk: Walk, args: WalkArgs) => {\n const fields = args?.data?.fields\n\n if (!fields) {\n throw new Error(`Must pass fields for request: ${args.url}`)\n }\n\n const fieldTypes = Object.keys(fields)\n\n const response = await walk(args)\n\n const dataTypes = _(response.data).castArray().map('type').value()\n const includedTypes = _.map(response.included, 'type')\n const responseTypes = _.uniq(dataTypes.concat(includedTypes))\n const missingFields = _.difference(responseTypes, fieldTypes).filter(t => t)\n\n if (missingFields.length > 0) {\n throw new Error(\n `Must include fields for types: ${JSON.stringify(missingFields)} for request ${args.url}`\n )\n }\n\n return transformResponse(response)\n}\n\nexport const friendlyErrors = (walk: Walk, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => throwErrorIfFieldsMissing(walk, args))\n .then(transformResponse)\n\nexport const noQueryParamsFriendlyErrors = (request: typeof makeRequest, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => request(args))\n .then(transformResponse)\n\nconst passthrough = async <T, R>(walk: (args: T) => Promise<R>, args: T) =>\n Promise.resolve(transformResponse(await walk(args)))\n\nexport const ensureFieldsInDev = __DEV__ ? friendlyErrors : passthrough\n\nexport const ensureNoQueryParamsInDev = __DEV__ ? noQueryParamsFriendlyErrors : passthrough\n\nexport const throwErrorIfQueryParams = (url: string) => {\n if (new URI(url).search().length) {\n throw new Error('Must pass query params as data arg')\n }\n\n return Promise.resolve()\n}\n\nexport const transformGetToPost = (args: GetRequest): PostRequest => {\n const { data, ...rest } = args\n const { fields, include } = data\n const fieldsArray = Object.entries(fields).map(([key, value]) => {\n if (Array.isArray(value)) {\n return [key, value.join(',')]\n }\n\n return [key, value]\n })\n\n return {\n ...rest,\n data: {\n fields: Object.fromEntries(fieldsArray),\n include,\n },\n }\n}\n"],"mappings":"AAAA,OAAOA,CAAC,MAAM,QAAQ;AACtB,OAAOC,GAAG,MAAM,OAAO;AACvB,SAASC,iBAAiB,QAAQ,sBAAsB;AACxD,SAASC,oBAAoB,QAAQ,0BAA0B;AAC/D,OAAOC,iBAAiB,MAAM,sBAAsB;AAUpD,OAAO,MAAMC,WAAW,GAAGA,CAAC;EAAEC,MAAM,GAAG,KAAK;EAAEC,GAAG;EAAEC,IAAI,GAAG,CAAC,CAAC;EAAEC,OAAO,GAAG,CAAC;AAAmB,CAAC,KAAK;EAChG;EACA,IAAIC,GAAG,GAAG,IAAIT,GAAG,CAACM,GAAG,CAAC;EACtB,MAAMI,KAAK,GAAGR,oBAAoB,CAACK,IAAI,CAAC;EACxC,MAAMI,QAAQ,GAAGF,GAAG,CAACC,KAAK,CAAC,IAAI,CAAC;;EAEhC;EACA,MAAME,aAAa,GAAGC,MAAM,CAACC,OAAO,CAAC;IAAE,GAAGJ,KAAK;IAAE,GAAGC;EAAS,CAAC,CAAC,CAC5DI,IAAI,CAAC,CAAC,CACNC,MAAM,CAAC,CAACC,GAAG,EAAE,CAACC,GAAG,EAAEC,KAAK,CAAC,KAAK;IAC7B;IACAF,GAAG,CAACC,GAAG,CAAC,GAAGC,KAAK;IAChB,OAAOF,GAAG;EACZ,CAAC,EAAE,CAAC,CAAC,CAAC;EAER,IAAIZ,MAAM,KAAK,KAAK,EAAE;IACpBI,GAAG,GAAGA,GAAG,CAACC,KAAK,CAACE,aAAa,CAAC;EAChC;EAEA,MAAMQ,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAACC,QAAQ,CAAChB,MAAM,CAAC,GAAGiB,IAAI,CAACC,SAAS,CAAChB,IAAI,CAAC,GAAGiB,SAAS;EAElF,OAAOvB,iBAAiB,CAACQ,GAAG,CAACgB,QAAQ,CAAC,CAAC,EAAE;IACvCC,MAAM,EAAErB,MAAM;IACdG,OAAO;IACPY;EACF,CAAC,CAAC,CAACO,IAAI,CAACC,QAAQ,IAAI;IAClB,IAAIA,QAAQ,CAACC,EAAE,EAAE;MACf,OAAOD,QAAQ,CAACE,IAAI,CAAC,CAAC,CAACH,IAAI,CAACI,CAAC,IAAKA,CAAC,KAAK,EAAE,GAAG,EAAE,GAAGT,IAAI,CAACU,KAAK,CAACD,CAAC,CAAE,CAAC;IACnE,CAAC,MAAM;MACL,OAAOE,OAAO,CAACC,MAAM,CAACN,QAAQ,CAAC;IACjC;EACF,CAAC,CAAC;AACJ,CAAC;AAED,MAAMO,wBAAwB,GAAG,CAC/B,aAAa,EACb,OAAO,EACP,MAAM,EACN,MAAM,EACN,cAAc,EACd,cAAc,EACd,aAAa,EACb,YAAY,EACZ,QAAQ,CACT;AAED,OAAO,MAAMC,aAAa,GAAGA,CAACC,OAAoB,EAAEC,WAAwB,MAAM;EAChF/B,IAAI,EAAE,CAAC,GAAG8B,OAAO,CAAC9B,IAAI,EAAE,GAAG+B,WAAW,CAAC/B,IAAI,CAAC;EAC5CgC,QAAQ,EAAE,CAAC,GAAGF,OAAO,CAACE,QAAQ,EAAE,GAAGD,WAAW,CAACC,QAAQ,CAAC;EACxD;EACA;EACAC,IAAI,EAAEzC,CAAC,CAAC0C,IAAI,CAACH,WAAW,CAACE,IAAI,EAAEL,wBAAwB;AACzD,CAAC,CAAC;AASF,OAAO,MAAMO,yBAAyB,GAAG,MAAAA,CAAOC,IAAU,EAAEC,IAAc,KAAK;EAC7E,MAAMC,MAAM,GAAGD,IAAI,EAAErC,IAAI,EAAEsC,MAAM;EAEjC,IAAI,CAACA,MAAM,EAAE;IACX,MAAM,IAAIC,KAAK,CAAC,iCAAiCF,IAAI,CAACtC,GAAG,EAAE,CAAC;EAC9D;EAEA,MAAMyC,UAAU,GAAGlC,MAAM,CAACmC,IAAI,CAACH,MAAM,CAAC;EAEtC,MAAMjB,QAAQ,GAAG,MAAMe,IAAI,CAACC,IAAI,CAAC;EAEjC,MAAMK,SAAS,GAAGlD,CAAC,CAAC6B,QAAQ,CAACrB,IAAI,CAAC,CAAC2C,SAAS,CAAC,CAAC,CAACC,GAAG,CAAC,MAAM,CAAC,CAAChC,KAAK,CAAC,CAAC;EAClE,MAAMiC,aAAa,GAAGrD,CAAC,CAACoD,GAAG,CAACvB,QAAQ,CAACW,QAAQ,EAAE,MAAM,CAAC;EACtD,MAAMc,aAAa,GAAGtD,CAAC,CAACuD,IAAI,CAACL,SAAS,CAACM,MAAM,CAACH,aAAa,CAAC,CAAC;EAC7D,MAAMI,aAAa,GAAGzD,CAAC,CAAC0D,UAAU,CAACJ,aAAa,EAAEN,UAAU,CAAC,CAACW,MAAM,CAAC3B,CAAC,IAAIA,CAAC,CAAC;EAE5E,IAAIyB,aAAa,CAACG,MAAM,GAAG,CAAC,EAAE;IAC5B,MAAM,IAAIb,KAAK,CACb,kCAAkCxB,IAAI,CAACC,SAAS,CAACiC,aAAa,CAAC,gBAAgBZ,IAAI,CAACtC,GAAG,EACzF,CAAC;EACH;EAEA,OAAOH,iBAAiB,CAACyB,QAAQ,CAAC;AACpC,CAAC;AAED,OAAO,MAAMgC,cAAc,GAAGA,CAACjB,IAAU,EAAEC,IAAc,KACvDiB,uBAAuB,CAACjB,IAAI,CAACtC,GAAG,CAAC,CAC9BqB,IAAI,CAAC,MAAMe,yBAAyB,CAACC,IAAI,EAAEC,IAAI,CAAC,CAAC,CACjDjB,IAAI,CAACxB,iBAAiB,CAAC;AAE5B,OAAO,MAAM2D,2BAA2B,GAAGA,CAACC,OAA2B,EAAEnB,IAAc,KACrFiB,uBAAuB,CAACjB,IAAI,CAACtC,GAAG,CAAC,CAC9BqB,IAAI,CAAC,MAAMoC,OAAO,CAACnB,IAAI,CAAC,CAAC,CACzBjB,IAAI,CAACxB,iBAAiB,CAAC;AAE5B,MAAM6D,WAAW,GAAG,MAAAA,CAAarB,IAA6B,EAAEC,IAAO,KACrEX,OAAO,CAACgC,OAAO,CAAC9D,iBAAiB,CAAC,MAAMwC,IAAI,CAACC,IAAI,CAAC,CAAC,CAAC;AAEtD,OAAO,MAAMsB,iBAAiB,GAAGC,OAAO,GAAGP,cAAc,GAAGI,WAAW;AAEvE,OAAO,MAAMI,wBAAwB,GAAGD,OAAO,GAAGL,2BAA2B,GAAGE,WAAW;AAE3F,OAAO,MAAMH,uBAAuB,GAAIvD,GAAW,IAAK;EACtD,IAAI,IAAIN,GAAG,CAACM,GAAG,CAAC,CAAC+D,MAAM,CAAC,CAAC,CAACV,MAAM,EAAE;IAChC,MAAM,IAAIb,KAAK,CAAC,oCAAoC,CAAC;EACvD;EAEA,OAAOb,OAAO,CAACgC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED,OAAO,MAAMK,kBAAkB,GAAI1B,IAAgB,IAAkB;EACnE,MAAM;IAAErC,IAAI;IAAE,GAAGgE;EAAK,CAAC,GAAG3B,IAAI;EAC9B,MAAM;IAAEC,MAAM;IAAE2B;EAAQ,CAAC,GAAGjE,IAAI;EAChC,MAAMkE,WAAW,GAAG5D,MAAM,CAACC,OAAO,CAAC+B,MAAM,CAAC,CAACM,GAAG,CAAC,CAAC,CAACjC,GAAG,EAAEC,KAAK,CAAC,KAAK;IAC/D,IAAIuD,KAAK,CAACC,OAAO,CAACxD,KAAK,CAAC,EAAE;MACxB,OAAO,CAACD,GAAG,EAAEC,KAAK,CAACyD,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B;IAEA,OAAO,CAAC1D,GAAG,EAAEC,KAAK,CAAC;EACrB,CAAC,CAAC;EAEF,OAAO;IACL,GAAGoD,IAAI;IACPhE,IAAI,EAAE;MACJsC,MAAM,EAAEhC,MAAM,CAACgE,WAAW,CAACJ,WAAW,CAAC;MACvCD;IACF;EACF,CAAC;AACH,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "4.3.0-rc.9",
3
+ "version": "4.3.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
35
35
  "@fortawesome/react-native-fontawesome": "^0.3.2",
36
- "@planningcenter/emoji-keyboard": "4.0.0",
36
+ "@planningcenter/emoji-keyboard": "4.3.0",
37
37
  "lodash-inflection": "^1.5.0"
38
38
  },
39
39
  "peerDependencies": {
@@ -84,5 +84,5 @@
84
84
  "sf-symbols-typescript": "^2.2.0",
85
85
  "typescript": "~6.0.3"
86
86
  },
87
- "gitHead": "4c3d10940f433b0e1722c68aafbbb81534bb21ce"
87
+ "gitHead": "2e1b1dfe9e9c7eeb8d6845f847202c1ce9b32eb9"
88
88
  }
@@ -59,8 +59,7 @@ describe('get', () => {
59
59
  })
60
60
 
61
61
  expect(fetchSpy).toHaveBeenCalledWith(
62
- // Do not url encode the query params or iOS will double encode
63
- APP_BASE_URL + '/records?fields[Record]=id&per_page=100&where[id]=1',
62
+ APP_BASE_URL + '/records?fields%5BRecord%5D=id&per_page=100&where%5Bid%5D=1',
64
63
  expect.any(Object)
65
64
  )
66
65
  })
@@ -75,7 +74,7 @@ describe('get', () => {
75
74
  })
76
75
 
77
76
  it('walks pagination when walk = true', async () => {
78
- const expectUrl = APP_BASE_URL + '/records?fields[Record]=id&per_page=1'
77
+ const expectUrl = APP_BASE_URL + '/records?fields%5BRecord%5D=id&per_page=1'
79
78
  const nextUrl = expectUrl + '&offset=1'
80
79
 
81
80
  MockServer.get(url, { ...DefaultFixtures.records(), links: { next: nextUrl } }, 200, {
@@ -92,7 +91,7 @@ describe('get', () => {
92
91
  })
93
92
 
94
93
  it('does not overwrite params in `url` when walking', async () => {
95
- const expectUrl = APP_BASE_URL + '/records?fields[Record]=id'
94
+ const expectUrl = APP_BASE_URL + '/records?fields%5BRecord%5D=id'
96
95
  const nextUrl = expectUrl + '&offset=1&per_page=1'
97
96
  MockServer.get(url, { ...DefaultFixtures.records(), links: { next: nextUrl } }, 200, {
98
97
  once: true,
@@ -0,0 +1,83 @@
1
+ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
+ import { QueryClientProvider, notifyManager } from '@tanstack/react-query'
3
+ import { act, fireEvent, render, screen } from '@testing-library/react-native'
4
+ import React from 'react'
5
+ import { chatQueryClient } from '../../../contexts/api_provider'
6
+ import type { ConversationResource } from '../../../types'
7
+ import { MessageForm } from '../message_form'
8
+
9
+ jest.mock('../../../contexts/api_provider', () => {
10
+ const { buildTestQueryClient } = require('../../../__utils__/query_client')
11
+ return { chatQueryClient: buildTestQueryClient() }
12
+ })
13
+
14
+ const mockPost = jest.fn()
15
+ jest.mock('../../../hooks/use_api_client', () => ({
16
+ useApiClient: () => ({ chat: { post: mockPost, patch: jest.fn() } }),
17
+ }))
18
+
19
+ jest.mock('../../../hooks/use_current_person', () => ({
20
+ useCurrentPerson: () => ({ type: 'Person', id: 1, name: 'Me', avatar: null }),
21
+ }))
22
+
23
+ jest.mock('../../../hooks/use_message_draft', () => ({
24
+ useMessageDraft: () => ({ draft: null, saveDraft: jest.fn(), clearDraft: jest.fn() }),
25
+ }))
26
+
27
+ jest.mock('../../../hooks/use_attachment_uploader', () => ({
28
+ useAttachmentUploader: () => ({
29
+ attachments: [],
30
+ attachmentIds: [],
31
+ pendingUploads: false,
32
+ flaggedAttachmentCount: 0,
33
+ reset: jest.fn(),
34
+ }),
35
+ }))
36
+
37
+ jest.mock('../../../hooks/use_broadcast_typing_status', () => ({
38
+ useBroadcastTypingStatus: () => jest.fn(),
39
+ }))
40
+
41
+ jest.mock('@react-navigation/native', () => ({
42
+ ...jest.requireActual('@react-navigation/native'),
43
+ useNavigation: () => ({ navigate: jest.fn(), setParams: jest.fn() }),
44
+ useRoute: () => ({ params: { conversation_id: 7 } }),
45
+ }))
46
+
47
+ notifyManager.setNotifyFunction(fn => act(fn))
48
+
49
+ const conversation = { id: 7, disabled: false } as unknown as ConversationResource
50
+
51
+ describe('MessageForm', () => {
52
+ let finishSend: (value: unknown) => void
53
+
54
+ beforeEach(() => {
55
+ chatQueryClient.clear()
56
+ mockPost.mockReset().mockReturnValue(new Promise(resolve => (finishSend = resolve)))
57
+ })
58
+
59
+ afterEach(async () => {
60
+ await act(async () => finishSend({ data: { type: 'Message', id: 'sent', mine: true } }))
61
+ })
62
+
63
+ it('keeps the input editable while a sent message is in flight', async () => {
64
+ render(
65
+ <QueryClientProvider client={chatQueryClient}>
66
+ <ThemeProvider value={DefaultTheme}>
67
+ <MessageForm.Root conversation={conversation}>
68
+ <MessageForm.TextInput />
69
+ <MessageForm.SubmitButton />
70
+ </MessageForm.Root>
71
+ </ThemeProvider>
72
+ </QueryClientProvider>
73
+ )
74
+
75
+ fireEvent.changeText(screen.getByTestId('message-composer-input'), 'hello')
76
+ await act(async () => {
77
+ fireEvent.press(screen.getByLabelText('Send message'))
78
+ })
79
+
80
+ expect(mockPost).toHaveBeenCalled()
81
+ expect(screen.getByTestId('message-composer-input').props.editable).toBe(true)
82
+ })
83
+ })
@@ -276,8 +276,8 @@ function MessageFormRoot({
276
276
  // added earlier.
277
277
  if (!(await catchUpBeforeSend())) return
278
278
 
279
- reset()
280
279
  mutateAsync({ text: messageText, attachments: attachmentsForSubmit })
280
+ reset()
281
281
  }
282
282
  }
283
283
  }
@@ -0,0 +1,38 @@
1
+ import { act, renderHook } from '@testing-library/react-native'
2
+ import { useDebouncedSearchTerm } from '../use_debounced_search_term'
3
+
4
+ beforeEach(() => jest.useFakeTimers())
5
+ afterEach(() => jest.useRealTimers())
6
+
7
+ describe('useDebouncedSearchTerm', () => {
8
+ it('settles on the trimmed term once typing pauses', () => {
9
+ const { result, rerender } = renderHook(
10
+ ({ term }: { term: string }) => useDebouncedSearchTerm(term),
11
+ {
12
+ initialProps: { term: 'bud' },
13
+ }
14
+ )
15
+
16
+ rerender({ term: ' budget ' })
17
+ act(() => jest.advanceTimersByTime(199))
18
+ expect(result.current).toBe('')
19
+
20
+ act(() => jest.advanceTimersByTime(1))
21
+ expect(result.current).toBe('budget')
22
+ })
23
+
24
+ it('clears immediately when the term empties', () => {
25
+ const { result, rerender } = renderHook(
26
+ ({ term }: { term: string }) => useDebouncedSearchTerm(term),
27
+ {
28
+ initialProps: { term: 'budget' },
29
+ }
30
+ )
31
+ act(() => jest.advanceTimersByTime(200))
32
+ expect(result.current).toBe('budget')
33
+
34
+ rerender({ term: ' ' })
35
+
36
+ expect(result.current).toBe('')
37
+ })
38
+ })
@@ -0,0 +1,88 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query'
2
+ import { act, renderHook, waitFor } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
5
+ import type { SearchResultResource } from '../../types/resources/search_result'
6
+ import { NO_FAILED_TYPES, useSearchResults } from '../use_search_results'
7
+
8
+ const mockGet = jest.fn()
9
+ jest.mock('../use_api_client', () => ({ useApiClient: () => ({ chat: { get: mockGet } }) }))
10
+
11
+ const result = (type: SearchResultResource['type'], id: string) =>
12
+ ({ type, id, title: `${type} ${id}`, url: `/${type}s/${id}` }) as SearchResultResource
13
+
14
+ const page = (data: SearchResultResource[], meta: Record<string, unknown> = {}) => ({
15
+ data,
16
+ links: {},
17
+ meta: { count: data.length, totalCount: data.length, ...meta },
18
+ })
19
+
20
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
21
+ <QueryClientProvider client={buildTestQueryClient()}>{children}</QueryClientProvider>
22
+ )
23
+
24
+ beforeEach(() => mockGet.mockReset())
25
+
26
+ describe('useSearchResults', () => {
27
+ it('flattens pages into one list without repeating a result', async () => {
28
+ mockGet
29
+ .mockResolvedValueOnce(
30
+ page([result('conversation', '1'), result('message', '1')], { next: { offset: '20' } })
31
+ )
32
+ .mockResolvedValueOnce(page([result('message', '1'), result('message', '2')]))
33
+
34
+ const { result: hook } = renderHook(() => useSearchResults('potluck'), { wrapper })
35
+ await waitFor(() => expect(hook.current.hasNextPage).toBe(true))
36
+
37
+ act(() => {
38
+ hook.current.loadMore()
39
+ })
40
+ await waitFor(() => expect(hook.current.hasNextPage).toBe(false))
41
+
42
+ expect(hook.current.results.map(({ type, id }) => `${type}:${id}`)).toEqual([
43
+ 'conversation:1',
44
+ 'message:1',
45
+ 'message:2',
46
+ ])
47
+ expect(mockGet).toHaveBeenLastCalledWith({
48
+ url: '/search_results',
49
+ data: expect.objectContaining({ q: 'potluck', offset: 20 }),
50
+ })
51
+ })
52
+
53
+ it('reads failedTypes from the first page', async () => {
54
+ mockGet
55
+ .mockResolvedValueOnce(page([], { failedTypes: ['message'], next: { offset: '20' } }))
56
+ .mockResolvedValueOnce(page([], { failedTypes: ['conversation'] }))
57
+
58
+ const { result: hook } = renderHook(() => useSearchResults('potluck'), { wrapper })
59
+ await waitFor(() => expect(hook.current.hasNextPage).toBe(true))
60
+
61
+ act(() => {
62
+ hook.current.loadMore()
63
+ })
64
+ await waitFor(() => expect(hook.current.hasNextPage).toBe(false))
65
+
66
+ expect(hook.current.failedTypes).toEqual(['message'])
67
+ })
68
+
69
+ it('makes no request for an empty or whitespace-only term', () => {
70
+ const { result: hook } = renderHook(() => useSearchResults(' '), { wrapper })
71
+
72
+ expect(mockGet).not.toHaveBeenCalled()
73
+ expect(hook.current.results).toEqual([])
74
+ expect(hook.current.failedTypes).toBe(NO_FAILED_TYPES)
75
+ })
76
+
77
+ it('searches with the trimmed term', async () => {
78
+ mockGet.mockResolvedValueOnce(page([]))
79
+
80
+ const { result: hook } = renderHook(() => useSearchResults(' potluck '), { wrapper })
81
+ await waitFor(() => expect(hook.current.isPending).toBe(false))
82
+
83
+ expect(mockGet).toHaveBeenCalledWith({
84
+ url: '/search_results',
85
+ data: expect.objectContaining({ q: 'potluck' }),
86
+ })
87
+ })
88
+ })
@@ -0,0 +1,20 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ const DEFAULT_DELAY_MS = 200
4
+
5
+ export function useDebouncedSearchTerm(term: string, delayMs = DEFAULT_DELAY_MS) {
6
+ const [debouncedTerm, setDebouncedTerm] = useState('')
7
+ const trimmedTerm = term.trim()
8
+
9
+ useEffect(() => {
10
+ if (trimmedTerm.length === 0) {
11
+ setDebouncedTerm('')
12
+ return
13
+ }
14
+
15
+ const timer = setTimeout(() => setDebouncedTerm(trimmedTerm), delayMs)
16
+ return () => clearTimeout(timer)
17
+ }, [trimmedTerm, delayMs])
18
+
19
+ return debouncedTerm
20
+ }
@@ -0,0 +1,78 @@
1
+ import { useInfiniteQuery } from '@tanstack/react-query'
2
+ import { useCallback, useMemo } from 'react'
3
+ import type { ApiCollection } from '../types'
4
+ import type { SearchResultResource } from '../types/resources/search_result'
5
+ import {
6
+ getSearchResultsQueryKey,
7
+ getSearchResultsRequestArgs,
8
+ } from '../utils/request/get_search_results'
9
+ import { ONE_MINUTE } from '../utils/time'
10
+ import { useApiClient } from './use_api_client'
11
+
12
+ export type SearchResultsCollection = ApiCollection<SearchResultResource> & {
13
+ meta: { failedTypes?: SearchResultResource['type'][] }
14
+ }
15
+
16
+ type SearchResultsPageParam = { offset?: number }
17
+
18
+ export const NO_FAILED_TYPES: SearchResultResource['type'][] = []
19
+
20
+ export function useSearchResults(term: string) {
21
+ const apiClient = useApiClient()
22
+ const trimmedTerm = term.trim()
23
+
24
+ const {
25
+ data,
26
+ isPending,
27
+ isError,
28
+ isFetchNextPageError,
29
+ fetchNextPage,
30
+ hasNextPage,
31
+ isFetchingNextPage,
32
+ } = useInfiniteQuery({
33
+ queryKey: getSearchResultsQueryKey(trimmedTerm),
34
+ queryFn: ({ pageParam }) => {
35
+ const requestArgs = getSearchResultsRequestArgs(trimmedTerm)
36
+ return apiClient.chat.get<SearchResultsCollection>({
37
+ url: requestArgs.url,
38
+ data: { ...requestArgs.data, ...pageParam },
39
+ })
40
+ },
41
+ initialPageParam: {} as SearchResultsPageParam,
42
+ getNextPageParam: lastPage => nextPageParam(lastPage.meta.next?.offset),
43
+ enabled: trimmedTerm.length >= 1,
44
+ staleTime: ONE_MINUTE,
45
+ refetchOnWindowFocus: false,
46
+ refetchOnMount: false,
47
+ retry: 1,
48
+ })
49
+
50
+ const results = useMemo(() => uniqueResults(data?.pages.flatMap(page => page.data) ?? []), [data])
51
+ const failedTypes = data?.pages[0]?.meta.failedTypes ?? NO_FAILED_TYPES
52
+ const loadMore = useCallback(() => fetchNextPage({ cancelRefetch: false }), [fetchNextPage])
53
+
54
+ return {
55
+ results,
56
+ failedTypes,
57
+ isPending,
58
+ isError: isError && !isFetchNextPageError,
59
+ isFetchNextPageError,
60
+ loadMore,
61
+ hasNextPage,
62
+ isFetchingNextPage,
63
+ }
64
+ }
65
+
66
+ function nextPageParam(offset?: string): SearchResultsPageParam | undefined {
67
+ return offset ? { offset: Number(offset) } : undefined
68
+ }
69
+
70
+ function uniqueResults(results: SearchResultResource[]) {
71
+ const seen = new Set<string>()
72
+ return results.filter(result => {
73
+ const key = `${result.type}:${result.id}`
74
+ if (seen.has(key)) return false
75
+ seen.add(key)
76
+ return true
77
+ })
78
+ }
@@ -4,15 +4,24 @@ import { TextButton } from '../../../components'
4
4
  interface ViewMoreLinkRowProps {
5
5
  onPress: () => void
6
6
  accessibilityHint: string
7
+ expanded?: boolean
7
8
  }
8
9
 
9
- export const ViewMoreLinkRow = ({ accessibilityHint, onPress }: ViewMoreLinkRowProps) => {
10
+ export const ViewMoreLinkRow = ({
11
+ accessibilityHint,
12
+ expanded = false,
13
+ onPress,
14
+ }: ViewMoreLinkRowProps) => {
10
15
  const styles = useRowStyles()
11
16
 
12
17
  return (
13
18
  <View style={[styles.row]}>
14
- <TextButton onPress={onPress} accessibilityHint={accessibilityHint} accessibilityRole="link">
15
- View more
19
+ <TextButton
20
+ onPress={onPress}
21
+ accessibilityHint={accessibilityHint}
22
+ accessibilityState={{ expanded }}
23
+ >
24
+ {expanded ? 'View less' : 'View more'}
16
25
  </TextButton>
17
26
  </View>
18
27
  )
@@ -1,6 +1,6 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StackActions, useNavigation } from '@react-navigation/native'
3
- import { RefObject, useEffect, useRef, useState } from 'react'
3
+ import { RefObject, useRef, useState } from 'react'
4
4
  import { FlatList, Pressable, StyleSheet, TextInput, View } from 'react-native'
5
5
  import { Avatar, Badge, Banner, Icon, KeyboardView, Spinner, Text } from '../../components'
6
6
  import { ActionButton } from '../../components/display/action_button'
@@ -11,6 +11,7 @@ import { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_di
11
11
  import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'
12
12
  import { useDirectMessageConversationValidate } from '../../hooks/direct_messages/use_direct_message_conversation_validate'
13
13
  import { useFindOrCreateDirectMessageConversation } from '../../hooks/direct_messages/use_find_or_create_direct_message_conversation'
14
+ import { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term'
14
15
  import { availableFeatures, useFeatures } from '../../hooks/use_features'
15
16
  import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
16
17
  import { ResultsHeading } from './components/results_heading'
@@ -26,7 +27,7 @@ export function ConversationSelectDirectMessageRecipientsScreen({
26
27
  const navigation = useNavigation()
27
28
  const { featureEnabled } = useFeatures()
28
29
  const [searchTerm, setSearchTerm] = useState('')
29
- const [debouncedTerm, setDebouncedTerm] = useState('')
30
+ const debouncedTerm = useDebouncedSearchTerm(searchTerm)
30
31
  const [selectedPeople, setSelectedPeople] = useState<Map<string, DirectMessageCandidateResource>>(
31
32
  () =>
32
33
  new Map(
@@ -45,16 +46,10 @@ export function ConversationSelectDirectMessageRecipientsScreen({
45
46
  )
46
47
  const searchInputRef = useRef<TextInput>(null)
47
48
 
48
- useEffect(() => {
49
- const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200)
50
- return () => clearTimeout(timer)
51
- }, [searchTerm])
52
-
53
49
  const selectPerson = (person: DirectMessageCandidateResource) => {
54
50
  if (selectedPeople.size >= MAX_RECIPIENTS) return
55
51
  setSelectedPeople(prev => new Map(prev).set(person.id, person))
56
52
  setSearchTerm('')
57
- setDebouncedTerm('')
58
53
  // Controlled TextInput can retain stale native text on iOS when the value
59
54
  // is cleared while it stays focused (keyboardShouldPersistTaps keeps it
60
55
  // focused through the tap) — clear() forces the native view to sync. Gated