@planningcenter/chat-react-native 4.3.0-rc.8 → 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 (81) 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_blank_states.d.ts +7 -0
  39. package/build/screens/search/components/search_blank_states.d.ts.map +1 -0
  40. package/build/screens/search/components/search_blank_states.js +72 -0
  41. package/build/screens/search/components/search_blank_states.js.map +1 -0
  42. package/build/screens/search/components/search_results_list.d.ts +8 -0
  43. package/build/screens/search/components/search_results_list.d.ts.map +1 -0
  44. package/build/screens/search/components/search_results_list.js +314 -0
  45. package/build/screens/search/components/search_results_list.js.map +1 -0
  46. package/build/screens/search/components/snippet.d.ts +8 -0
  47. package/build/screens/search/components/snippet.d.ts.map +1 -0
  48. package/build/screens/search/components/snippet.js +68 -0
  49. package/build/screens/search/components/snippet.js.map +1 -0
  50. package/build/screens/search/search_screen.d.ts.map +1 -1
  51. package/build/screens/search/search_screen.js +37 -12
  52. package/build/screens/search/search_screen.js.map +1 -1
  53. package/build/utils/client/request_helpers.js +1 -1
  54. package/build/utils/client/request_helpers.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/__tests__/utils/client.ts +3 -4
  57. package/src/components/conversation/__tests__/message_form.test.tsx +83 -0
  58. package/src/components/conversation/message_form.tsx +1 -1
  59. package/src/hooks/__tests__/use_debounced_search_term.test.tsx +38 -0
  60. package/src/hooks/__tests__/use_search_results.test.tsx +88 -0
  61. package/src/hooks/use_debounced_search_term.ts +20 -0
  62. package/src/hooks/use_search_results.ts +78 -0
  63. package/src/screens/conversation_select_recipients/components/view_more_link_row.tsx +12 -3
  64. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +3 -8
  65. package/src/screens/search/__tests__/search_screen.test.tsx +64 -0
  66. package/src/screens/search/components/__tests__/conversation_results.test.tsx +55 -0
  67. package/src/screens/search/components/__tests__/message_result_row.test.tsx +65 -0
  68. package/src/screens/search/components/__tests__/result_row.test.tsx +33 -0
  69. package/src/screens/search/components/__tests__/search_blank_states.test.tsx +20 -0
  70. package/src/screens/search/components/__tests__/search_results_list.test.tsx +135 -0
  71. package/src/screens/search/components/conversation_result_row.tsx +36 -0
  72. package/src/screens/search/components/conversation_results.tsx +39 -0
  73. package/src/screens/search/components/message_result_row.tsx +48 -0
  74. package/src/screens/search/components/result_row.tsx +58 -0
  75. package/src/screens/search/components/results_section.tsx +34 -0
  76. package/src/screens/search/components/search_blank_states.tsx +34 -0
  77. package/src/screens/search/components/search_results_list.tsx +147 -0
  78. package/src/screens/search/components/snippet.tsx +37 -0
  79. package/src/screens/search/search_screen.tsx +10 -0
  80. package/src/utils/client/__tests__/request_helpers.test.ts +40 -0
  81. package/src/utils/client/request_helpers.ts +1 -1
@@ -0,0 +1,8 @@
1
+ import type { SnippetSegment } from '../../../types/resources/search_result';
2
+ interface SnippetProps {
3
+ segments: SnippetSegment[];
4
+ }
5
+ export declare function Snippet({ segments }: SnippetProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function plainSnippetText(segments: SnippetSegment[]): string;
7
+ export {};
8
+ //# sourceMappingURL=snippet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.d.ts","sourceRoot":"","sources":["../../../../src/screens/search/components/snippet.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAA;AAE5E,UAAU,YAAY;IACpB,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,YAAY,2CAYjD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,EAAE,UAE1D"}
@@ -0,0 +1,68 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import React from 'react';
3
+ import { StyleSheet } from 'react-native';
4
+ import { Text } from '../../../components/display';
5
+ import { useTheme } from '../../../hooks';
6
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
7
+ export function Snippet(t0) {
8
+ const $ = _c(7);
9
+ const {
10
+ segments
11
+ } = t0;
12
+ const styles = useStyles();
13
+ let t1;
14
+ if ($[0] !== segments || $[1] !== styles) {
15
+ let t2;
16
+ if ($[3] !== styles) {
17
+ t2 = (segment, index) => /*#__PURE__*/_jsx(Text, {
18
+ variant: "tertiary",
19
+ style: segment.match && styles.match,
20
+ children: segment.text
21
+ }, index);
22
+ $[3] = styles;
23
+ $[4] = t2;
24
+ } else {
25
+ t2 = $[4];
26
+ }
27
+ t1 = segments.map(t2);
28
+ $[0] = segments;
29
+ $[1] = styles;
30
+ $[2] = t1;
31
+ } else {
32
+ t1 = $[2];
33
+ }
34
+ let t2;
35
+ if ($[5] !== t1) {
36
+ t2 = /*#__PURE__*/_jsx(_Fragment, {
37
+ children: t1
38
+ });
39
+ $[5] = t1;
40
+ $[6] = t2;
41
+ } else {
42
+ t2 = $[6];
43
+ }
44
+ return t2;
45
+ }
46
+ export function plainSnippetText(segments) {
47
+ return segments.map(segment => segment.text).join('');
48
+ }
49
+ const useStyles = () => {
50
+ const $ = _c(2);
51
+ const {
52
+ colors
53
+ } = useTheme();
54
+ let t0;
55
+ if ($[0] !== colors.interaction) {
56
+ t0 = StyleSheet.create({
57
+ match: {
58
+ color: colors.interaction
59
+ }
60
+ });
61
+ $[0] = colors.interaction;
62
+ $[1] = t0;
63
+ } else {
64
+ t0 = $[1];
65
+ }
66
+ return t0;
67
+ };
68
+ //# sourceMappingURL=snippet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.js","names":["React","StyleSheet","Text","useTheme","jsx","_jsx","Fragment","_Fragment","Snippet","t0","$","_c","segments","styles","useStyles","t1","t2","segment","index","variant","style","match","children","text","map","plainSnippetText","join","colors","interaction","create","color"],"sources":["../../../../src/screens/search/components/snippet.tsx"],"sourcesContent":["import React from 'react'\nimport { StyleSheet } from 'react-native'\nimport { Text } from '../../../components/display'\nimport { useTheme } from '../../../hooks'\nimport type { SnippetSegment } from '../../../types/resources/search_result'\n\ninterface SnippetProps {\n segments: SnippetSegment[]\n}\n\nexport function Snippet({ segments }: SnippetProps) {\n const styles = useStyles()\n\n return (\n <>\n {segments.map((segment, index) => (\n <Text key={index} variant=\"tertiary\" style={segment.match && styles.match}>\n {segment.text}\n </Text>\n ))}\n </>\n )\n}\n\nexport function plainSnippetText(segments: SnippetSegment[]) {\n return segments.map(segment => segment.text).join('')\n}\n\nconst useStyles = () => {\n const { colors } = useTheme()\n\n return StyleSheet.create({\n match: {\n color: colors.interaction,\n },\n })\n}\n"],"mappings":";AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,UAAU,QAAQ,cAAc;AACzC,SAASC,IAAI,QAAQ,6BAA6B;AAClD,SAASC,QAAQ,QAAQ,gBAAgB;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,QAAA,IAAAC,SAAA;AAOzC,OAAO,SAAAC,QAAAC,EAAA;EAAA,MAAAC,CAAA,GAAAC,EAAA;EAAiB;IAAAC;EAAA,IAAAH,EAA0B;EAChD,MAAAI,MAAA,GAAeC,SAAS,CAAC,CAAC;EAAA,IAAAC,EAAA;EAAA,IAAAL,CAAA,QAAAE,QAAA,IAAAF,CAAA,QAAAG,MAAA;IAAA,IAAAG,EAAA;IAAA,IAAAN,CAAA,QAAAG,MAAA;MAIRG,EAAA,GAAAA,CAAAC,OAAA,EAAAC,KAAA,kBACZb,IAAA,CAACH,IAAI;QAAqBiB,OAAU,EAAV,UAAU;QAAQC,KAA6B,EAA7BH,OAAO,CAAAI,KAAsB,IAAZR,MAAM,CAAAQ,KAAM;QAAAC,QAAA,EACtEL,OAAO,CAAAM;MAAK,GADJL,KAEL,CACP;MAAAR,CAAA,MAAAG,MAAA;MAAAH,CAAA,MAAAM,EAAA;IAAA;MAAAA,EAAA,GAAAN,CAAA;IAAA;IAJAK,EAAA,GAAAH,QAAQ,CAAAY,GAAI,CAACR,EAIb,CAAC;IAAAN,CAAA,MAAAE,QAAA;IAAAF,CAAA,MAAAG,MAAA;IAAAH,CAAA,MAAAK,EAAA;EAAA;IAAAA,EAAA,GAAAL,CAAA;EAAA;EAAA,IAAAM,EAAA;EAAA,IAAAN,CAAA,QAAAK,EAAA;IALJC,EAAA,gBAAAX,IAAA,CAAAE,SAAA;MAAAe,QAAA,EACGP;IAIC,CACF,CAAC;IAAAL,CAAA,MAAAK,EAAA;IAAAL,CAAA,MAAAM,EAAA;EAAA;IAAAA,EAAA,GAAAN,CAAA;EAAA;EAAA,OANHM,EAMG;AAAA;AAIP,OAAO,SAASS,gBAAgBA,CAACb,QAA0B,EAAE;EAC3D,OAAOA,QAAQ,CAACY,GAAG,CAACP,OAAO,IAAIA,OAAO,CAACM,IAAI,CAAC,CAACG,IAAI,CAAC,EAAE,CAAC;AACvD;AAEA,MAAMZ,SAAS,GAAGA,CAAA;EAAA,MAAAJ,CAAA,GAAAC,EAAA;EAChB;IAAAgB;EAAA,IAAmBxB,QAAQ,CAAC,CAAC;EAAA,IAAAM,EAAA;EAAA,IAAAC,CAAA,QAAAiB,MAAA,CAAAC,WAAA;IAEtBnB,EAAA,GAAAR,UAAU,CAAA4B,MAAO,CAAC;MAAAR,KAAA,EAChB;QAAAS,KAAA,EACEH,MAAM,CAAAC;MACf;IACF,CAAC,CAAC;IAAAlB,CAAA,MAAAiB,MAAA,CAAAC,WAAA;IAAAlB,CAAA,MAAAD,EAAA;EAAA;IAAAA,EAAA,GAAAC,CAAA;EAAA;EAAA,OAJKD,EAIL;AAAA,CACH","ignoreList":[]}
@@ -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;AAUvC,eAAO,MAAM,mBAAmB,GAAI,iBAEjC,sBAAsB,CAAC,aAAa,CAAC,KAAG,4BAIxC,CAAA;AAEH,wBAAgB,YAAY,4CAU3B"}
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,14 @@
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';
8
+ import { SearchPromptBlankState } from './components/search_blank_states';
6
9
  import { SearchField } from './components/search_field';
7
- import { jsx as _jsx } from "react/jsx-runtime";
10
+ import { SearchResultsList } from './components/search_results_list';
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
12
  export const SearchScreenOptions = ({
9
13
  navigation
10
14
  }) => composeOptions({
@@ -14,26 +18,47 @@ export const SearchScreenOptions = ({
14
18
  onPress: () => navigation.goBack()
15
19
  }));
16
20
  export function SearchScreen() {
17
- const $ = _c(2);
21
+ const $ = _c(7);
18
22
  const [term, setTerm] = useState("");
23
+ const debouncedTerm = useDebouncedSearchTerm(term);
19
24
  let t0;
20
25
  if ($[0] !== term) {
21
- t0 = /*#__PURE__*/_jsx(KeyboardView, {
22
- children: /*#__PURE__*/_jsx(View, {
23
- style: styles.container,
24
- testID: "search-screen",
25
- children: /*#__PURE__*/_jsx(SearchField, {
26
- term: term,
27
- onChange: setTerm
28
- })
29
- })
26
+ t0 = /*#__PURE__*/_jsx(SearchField, {
27
+ term: term,
28
+ onChange: setTerm
30
29
  });
31
30
  $[0] = term;
32
31
  $[1] = t0;
33
32
  } else {
34
33
  t0 = $[1];
35
34
  }
36
- return t0;
35
+ let 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;
43
+ } else {
44
+ t1 = $[3];
45
+ }
46
+ let t2;
47
+ if ($[4] !== t0 || $[5] !== t1) {
48
+ t2 = /*#__PURE__*/_jsx(KeyboardView, {
49
+ children: /*#__PURE__*/_jsxs(View, {
50
+ style: styles.container,
51
+ testID: "search-screen",
52
+ children: [t0, t1]
53
+ })
54
+ });
55
+ $[4] = t0;
56
+ $[5] = t1;
57
+ $[6] = t2;
58
+ } else {
59
+ t2 = $[6];
60
+ }
61
+ return t2;
37
62
  }
38
63
  const styles = StyleSheet.create({
39
64
  container: {
@@ -1 +1 @@
1
- {"version":3,"file":"search_screen.js","names":["React","useState","StyleSheet","View","KeyboardView","composeOptions","withMinimalBack","SearchField","jsx","_jsx","SearchScreenOptions","navigation","title","animation","onPress","goBack","SearchScreen","$","_c","term","setTerm","t0","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 { 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 </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,WAAW,QAAQ,2BAA2B;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAEvD,OAAO,MAAMC,mBAAmB,GAAGA,CAAC;EAClCC;AACqC,CAAC,KACtCN,cAAc,CACZ;EAAEO,KAAK,EAAE,QAAQ;EAAEC,SAAS,EAAE;AAAO,CAAC,EACtCP,eAAe,CAAC;EAAEQ,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,IAAwBnB,QAAQ,CAAC,EAAE,CAAC;EAAA,IAAAoB,EAAA;EAAA,IAAAJ,CAAA,QAAAE,IAAA;IAGlCE,EAAA,gBAAAZ,IAAA,CAACL,YAAY;MAAAkB,QAAA,eACXb,IAAA,CAACN,IAAI;QAAQoB,KAAgB,EAAhBC,MAAM,CAAAC,SAAU;QAASC,MAAe,EAAf,eAAe;QAAAJ,QAAA,eACnDb,IAAA,CAACF,WAAW;UAAOY,IAAI,EAAJA,IAAI;UAAYC,QAAO,EAAPA;QAAO,CAAG;MAAC,CAC1C;IAAC,CACK,CAAC;IAAAH,CAAA,MAAAE,IAAA;IAAAF,CAAA,MAAAI,EAAA;EAAA;IAAAA,EAAA,GAAAJ,CAAA;EAAA;EAAA,OAJfI,EAIe;AAAA;AAInB,MAAMG,MAAM,GAAGtB,UAAU,CAACyB,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":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "4.3.0-rc.8",
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": "9d2cec9da1b024a2b572d2b0cd07efd62a9460be"
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
  )