@planningcenter/chat-react-native 2.0.0 → 2.1.0-rc.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 (130) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +7 -2
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_reaction.d.ts +1 -1
  5. package/build/components/conversation/message_reaction.d.ts.map +1 -1
  6. package/build/components/conversation/message_reaction.js +1 -1
  7. package/build/components/conversation/message_reaction.js.map +1 -1
  8. package/build/components/conversations.d.ts.map +1 -1
  9. package/build/components/conversations.js +76 -30
  10. package/build/components/conversations.js.map +1 -1
  11. package/build/components/display/badge.d.ts +2 -6
  12. package/build/components/display/badge.d.ts.map +1 -1
  13. package/build/components/display/badge.js +1 -5
  14. package/build/components/display/badge.js.map +1 -1
  15. package/build/components/display/tabs.d.ts +17 -0
  16. package/build/components/display/tabs.d.ts.map +1 -0
  17. package/build/components/display/tabs.js +97 -0
  18. package/build/components/display/tabs.js.map +1 -0
  19. package/build/contexts/api_provider.js +2 -2
  20. package/build/contexts/api_provider.js.map +1 -1
  21. package/build/hooks/use_conversation_jolt_events.d.ts +2 -0
  22. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -0
  23. package/build/hooks/use_conversation_jolt_events.js +47 -0
  24. package/build/hooks/use_conversation_jolt_events.js.map +1 -0
  25. package/build/hooks/use_conversation_messages.d.ts +2 -18
  26. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  27. package/build/hooks/use_conversation_messages.js +2 -2
  28. package/build/hooks/use_conversation_messages.js.map +1 -1
  29. package/build/hooks/use_conversations.d.ts +37 -0
  30. package/build/hooks/use_conversations.d.ts.map +1 -0
  31. package/build/hooks/use_conversations.js +48 -0
  32. package/build/hooks/use_conversations.js.map +1 -0
  33. package/build/hooks/use_jolt.d.ts +9 -0
  34. package/build/hooks/use_jolt.d.ts.map +1 -0
  35. package/build/hooks/use_jolt.js +71 -0
  36. package/build/hooks/use_jolt.js.map +1 -0
  37. package/build/hooks/use_suspense_api.d.ts +7 -2
  38. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  39. package/build/hooks/use_suspense_api.js +7 -2
  40. package/build/hooks/use_suspense_api.js.map +1 -1
  41. package/build/navigation/index.d.ts +5 -0
  42. package/build/navigation/index.d.ts.map +1 -1
  43. package/build/navigation/index.js +7 -2
  44. package/build/navigation/index.js.map +1 -1
  45. package/build/screens/message_actions_screen.d.ts +1 -1
  46. package/build/screens/message_actions_screen.d.ts.map +1 -1
  47. package/build/screens/message_actions_screen.js +1 -1
  48. package/build/screens/message_actions_screen.js.map +1 -1
  49. package/build/screens/reactions_screen.d.ts +11 -0
  50. package/build/screens/reactions_screen.d.ts.map +1 -0
  51. package/build/screens/reactions_screen.js +83 -0
  52. package/build/screens/reactions_screen.js.map +1 -0
  53. package/build/types/resources/app_name.d.ts +2 -0
  54. package/build/types/resources/app_name.d.ts.map +1 -0
  55. package/build/types/resources/app_name.js +2 -0
  56. package/build/types/resources/app_name.js.map +1 -0
  57. package/build/types/resources/conversation.d.ts +18 -10
  58. package/build/types/resources/conversation.d.ts.map +1 -1
  59. package/build/types/resources/conversation.js.map +1 -1
  60. package/build/types/resources/conversation_badge.d.ts +12 -0
  61. package/build/types/resources/conversation_badge.d.ts.map +1 -0
  62. package/build/types/resources/conversation_badge.js +2 -0
  63. package/build/types/resources/conversation_badge.js.map +1 -0
  64. package/build/types/resources/group_resource.d.ts +12 -0
  65. package/build/types/resources/group_resource.d.ts.map +1 -0
  66. package/build/types/resources/group_resource.js +2 -0
  67. package/build/types/resources/group_resource.js.map +1 -0
  68. package/build/types/resources/index.d.ts +2 -1
  69. package/build/types/resources/index.d.ts.map +1 -1
  70. package/build/types/resources/index.js +2 -1
  71. package/build/types/resources/index.js.map +1 -1
  72. package/build/types/resources/member.d.ts +23 -0
  73. package/build/types/resources/member.d.ts.map +1 -0
  74. package/build/types/resources/member.js +2 -0
  75. package/build/types/resources/member.js.map +1 -0
  76. package/build/types/resources/member_ability.d.ts +6 -0
  77. package/build/types/resources/member_ability.d.ts.map +1 -0
  78. package/build/types/resources/member_ability.js +2 -0
  79. package/build/types/resources/member_ability.js.map +1 -0
  80. package/build/types/resources/reaction.d.ts +1 -1
  81. package/build/types/resources/reaction.js.map +1 -1
  82. package/build/utils/cache/page_mutations.d.ts +19 -2
  83. package/build/utils/cache/page_mutations.d.ts.map +1 -1
  84. package/build/utils/cache/page_mutations.js +21 -7
  85. package/build/utils/cache/page_mutations.js.map +1 -1
  86. package/build/utils/client/client.d.ts +1 -1
  87. package/build/utils/client/client.d.ts.map +1 -1
  88. package/build/utils/client/client.js +1 -1
  89. package/build/utils/client/client.js.map +1 -1
  90. package/build/utils/date.d.ts +4 -0
  91. package/build/utils/date.d.ts.map +1 -0
  92. package/build/utils/date.js +23 -0
  93. package/build/utils/date.js.map +1 -0
  94. package/build/utils/session.d.ts +0 -6
  95. package/build/utils/session.d.ts.map +1 -1
  96. package/build/utils/session.js +0 -6
  97. package/build/utils/session.js.map +1 -1
  98. package/build/utils/uri.d.ts +1 -1
  99. package/build/utils/uri.d.ts.map +1 -1
  100. package/build/utils/uri.js +1 -1
  101. package/build/utils/uri.js.map +1 -1
  102. package/package.json +7 -3
  103. package/src/__tests__/utils/cache/page_mutations.ts +7 -46
  104. package/src/components/conversation/message.tsx +8 -3
  105. package/src/components/conversation/message_reaction.tsx +6 -2
  106. package/src/components/conversations.tsx +95 -32
  107. package/src/components/display/badge.tsx +3 -8
  108. package/src/components/display/tabs.tsx +142 -0
  109. package/src/contexts/api_provider.tsx +3 -3
  110. package/src/hooks/use_conversation_jolt_events.ts +67 -0
  111. package/src/hooks/use_conversation_messages.ts +6 -2
  112. package/src/hooks/use_conversations.ts +53 -0
  113. package/src/hooks/use_jolt.ts +101 -0
  114. package/src/hooks/use_suspense_api.ts +10 -3
  115. package/src/navigation/index.tsx +10 -2
  116. package/src/screens/message_actions_screen.tsx +1 -1
  117. package/src/screens/reactions_screen.tsx +131 -0
  118. package/src/types/resources/app_name.ts +1 -0
  119. package/src/types/resources/conversation.ts +18 -10
  120. package/src/types/resources/conversation_badge.ts +10 -0
  121. package/src/types/resources/group_resource.ts +10 -0
  122. package/src/types/resources/index.ts +2 -1
  123. package/src/types/resources/member.ts +24 -0
  124. package/src/types/resources/member_ability.ts +5 -0
  125. package/src/types/resources/reaction.ts +1 -1
  126. package/src/utils/cache/page_mutations.ts +32 -9
  127. package/src/utils/client/client.ts +1 -1
  128. package/src/utils/date.ts +25 -0
  129. package/src/utils/session.ts +0 -7
  130. package/src/utils/uri.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../../src/utils/uri.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,MAAM,CAAC,OAAO,OAAO,GAAG;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAA;gBAEA,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;IAKhE,IAAI,MAAM,qBAMT;IAED,IAAI,IAAI,6FAWP;IAED,IAAI,GAAG,4BAEN;IAED,IAAI,OAAO,WAEV;IAED,IAAI,SAAS,WAEZ;IAED,IAAI,OAAO;;;MAKV;IAED,MAAM,qBAIL;IAED,GAAG,qBAIF;CACF"}
1
+ {"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../../src/utils/uri.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,qBAAa,GAAG;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAA;gBAEA,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;IAKhE,IAAI,MAAM,qBAMT;IAED,IAAI,IAAI,6FAWP;IAED,IAAI,GAAG,4BAEN;IAED,IAAI,OAAO,WAEV;IAED,IAAI,SAAS,WAEZ;IAED,IAAI,OAAO;;;MAKV;IAED,MAAM,qBAIL;IAED,GAAG,qBAIF;CACF"}
@@ -5,7 +5,7 @@ const systemName = DeviceInfo.getSystemName();
5
5
  const systemVersion = DeviceInfo.getSystemVersion();
6
6
  const readableVersion = DeviceInfo.getReadableVersion();
7
7
  const appName = DeviceInfo.getApplicationName();
8
- export default class Uri {
8
+ export class Uri {
9
9
  session;
10
10
  app;
11
11
  constructor({ session, app }) {
@@ -1 +1 @@
1
- {"version":3,"file":"uri.js","sourceRoot":"","sources":["../../src/utils/uri.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,0BAA0B,CAAA;AAEjD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,UAAU,GAAG,UAAU,CAAC,aAAa,EAAE,CAAA;AAC7C,MAAM,aAAa,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;AACnD,MAAM,eAAe,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AACvD,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AAE/C,MAAM,CAAC,OAAO,OAAO,GAAG;IACtB,OAAO,CAAS;IAChB,GAAG,CAAS;IAEZ,YAAY,EAAE,OAAO,EAAE,GAAG,EAAsC;QAC9D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;IAChB,CAAC;IAED,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,GAAG,KAAK,aAAa,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAA;QACf,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC;YACjB,KAAK,YAAY;gBACf,OAAO,8BAA8B,CAAA;YACvC,KAAK,SAAS;gBACZ,OAAO,sCAAsC,CAAA;YAC/C,KAAK,aAAa;gBAChB,OAAO,cAAc,CAAA;YACvB;gBACE,OAAO,8BAA8B,CAAA;QACzC,CAAC;IACH,CAAC;IAED,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,YAAY,CAAA;IAC1C,CAAC;IAED,IAAI,OAAO;QACT,OAAO,GAAG,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;IACxC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1C,CAAC;IAED,IAAI,OAAO;QACT,OAAO;YACL,YAAY,EAAE,GAAG,OAAO,IAAI,eAAe,KAAK,KAAK,KAAK,KAAK,KAAK,UAAU,KAAK,aAAa,GAAG;YACnG,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE;SAC5D,CAAA;IACH,CAAC;IAED,MAAM,GAAG,IAAI,CAAC,EAAE;QACd,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAErD,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzD,CAAC,CAAA;IAED,GAAG,GAAG,IAAI,CAAC,EAAE;QACX,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAErD,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACxC,CAAC,CAAA;CACF","sourcesContent":["import DeviceInfo from 'react-native-device-info'\nimport { Session } from './session'\nconst brand = DeviceInfo.getBrand()\nconst model = DeviceInfo.getModel()\nconst systemName = DeviceInfo.getSystemName()\nconst systemVersion = DeviceInfo.getSystemVersion()\nconst readableVersion = DeviceInfo.getReadableVersion()\nconst appName = DeviceInfo.getApplicationName()\n\nexport default class Uri {\n session: Session\n app?: string\n\n constructor({ session, app }: { session: Session; app?: string }) {\n this.session = session\n this.app = app\n }\n\n get schema() {\n if (this.env === 'development') {\n return 'http'\n } else {\n return 'https'\n }\n }\n\n get host() {\n switch (this.env) {\n case 'production':\n return 'api.planningcenteronline.com'\n case 'staging':\n return 'api-staging.planningcenteronline.com'\n case 'development':\n return 'api.pco.test'\n default:\n return 'api.planningcenteronline.com'\n }\n }\n\n get env() {\n return this.session?.env || 'production'\n }\n\n get baseUrl() {\n return `${this.schema}://${this.host}`\n }\n\n get directory() {\n return this.app ? `/${this.app}/v2` : ''\n }\n\n get headers() {\n return {\n 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,\n Authorization: `Bearer ${this.session.token?.access_token}`,\n }\n }\n\n appUrl = path => {\n if (path.startsWith(`${this.schema}://`)) return path\n\n return `${this.baseUrl}${this.directory}${path || '/'}`\n }\n\n api = path => {\n if (path.startsWith(`${this.schema}://`)) return path\n\n return `${this.baseUrl}${path || '/'}`\n }\n}\n"]}
1
+ {"version":3,"file":"uri.js","sourceRoot":"","sources":["../../src/utils/uri.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,0BAA0B,CAAA;AAEjD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,UAAU,GAAG,UAAU,CAAC,aAAa,EAAE,CAAA;AAC7C,MAAM,aAAa,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;AACnD,MAAM,eAAe,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AACvD,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AAE/C,MAAM,OAAO,GAAG;IACd,OAAO,CAAS;IAChB,GAAG,CAAS;IAEZ,YAAY,EAAE,OAAO,EAAE,GAAG,EAAsC;QAC9D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;IAChB,CAAC;IAED,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,GAAG,KAAK,aAAa,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAA;QACf,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC;YACjB,KAAK,YAAY;gBACf,OAAO,8BAA8B,CAAA;YACvC,KAAK,SAAS;gBACZ,OAAO,sCAAsC,CAAA;YAC/C,KAAK,aAAa;gBAChB,OAAO,cAAc,CAAA;YACvB;gBACE,OAAO,8BAA8B,CAAA;QACzC,CAAC;IACH,CAAC;IAED,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,YAAY,CAAA;IAC1C,CAAC;IAED,IAAI,OAAO;QACT,OAAO,GAAG,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;IACxC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1C,CAAC;IAED,IAAI,OAAO;QACT,OAAO;YACL,YAAY,EAAE,GAAG,OAAO,IAAI,eAAe,KAAK,KAAK,KAAK,KAAK,KAAK,UAAU,KAAK,aAAa,GAAG;YACnG,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE;SAC5D,CAAA;IACH,CAAC;IAED,MAAM,GAAG,IAAI,CAAC,EAAE;QACd,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAErD,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzD,CAAC,CAAA;IAED,GAAG,GAAG,IAAI,CAAC,EAAE;QACX,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAErD,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACxC,CAAC,CAAA;CACF","sourcesContent":["import DeviceInfo from 'react-native-device-info'\nimport { Session } from './session'\nconst brand = DeviceInfo.getBrand()\nconst model = DeviceInfo.getModel()\nconst systemName = DeviceInfo.getSystemName()\nconst systemVersion = DeviceInfo.getSystemVersion()\nconst readableVersion = DeviceInfo.getReadableVersion()\nconst appName = DeviceInfo.getApplicationName()\n\nexport class Uri {\n session: Session\n app?: string\n\n constructor({ session, app }: { session: Session; app?: string }) {\n this.session = session\n this.app = app\n }\n\n get schema() {\n if (this.env === 'development') {\n return 'http'\n } else {\n return 'https'\n }\n }\n\n get host() {\n switch (this.env) {\n case 'production':\n return 'api.planningcenteronline.com'\n case 'staging':\n return 'api-staging.planningcenteronline.com'\n case 'development':\n return 'api.pco.test'\n default:\n return 'api.planningcenteronline.com'\n }\n }\n\n get env() {\n return this.session?.env || 'production'\n }\n\n get baseUrl() {\n return `${this.schema}://${this.host}`\n }\n\n get directory() {\n return this.app ? `/${this.app}/v2` : ''\n }\n\n get headers() {\n return {\n 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,\n Authorization: `Bearer ${this.session.token?.access_token}`,\n }\n }\n\n appUrl = path => {\n if (path.startsWith(`${this.schema}://`)) return path\n\n return `${this.baseUrl}${this.directory}${path || '/'}`\n }\n\n api = path => {\n if (path.startsWith(`${this.schema}://`)) return path\n\n return `${this.baseUrl}${path || '/'}`\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "2.0.0",
3
+ "version": "2.1.0-rc.0",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -24,13 +24,17 @@
24
24
  "react-native-url-polyfill": "^2.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
- "@planningcenter/icons": "^15.17.0",
27
+ "@planningcenter/datetime-fmt": ">=1.0.0",
28
+ "@planningcenter/icons": ">=15.0.0",
29
+ "@planningcenter/jolt-client": ">=2.0.0",
28
30
  "@react-navigation/elements": "*",
29
31
  "@react-navigation/native": ">=7.0.0",
30
32
  "@react-navigation/native-stack": ">=7.0.0",
31
33
  "@tanstack/react-query": "^5.0.0",
32
34
  "color": "^3.1.2",
33
35
  "lodash": "*",
36
+ "moment": ">=2.0.0",
37
+ "moment-timezone": ">=2.0.0",
34
38
  "react": "*",
35
39
  "react-native": "*",
36
40
  "react-native-device-info": "*",
@@ -47,5 +51,5 @@
47
51
  "prettier": "^3.4.2",
48
52
  "typescript": "<5.6.0"
49
53
  },
50
- "gitHead": "cdb7edc65761b084f41cee5fa84d10f8f341b5d6"
54
+ "gitHead": "9ada391b04ffc168aa7ffb4baaa746429ee7e093"
51
55
  }
@@ -1,4 +1,4 @@
1
- import { addRecordInPagesData, updateRecordInPagesData } from '../../../utils/'
1
+ import { deleteRecordInPagesData, updateRecordInPagesData } from '../../../utils/'
2
2
 
3
3
  const data = {
4
4
  pageParams: {},
@@ -105,6 +105,7 @@ describe('updateRecordInPagesData', () => {
105
105
  pages: [
106
106
  {
107
107
  data: [
108
+ { id, type: 'Message', text: `updated example ${id}` },
108
109
  { id: '1', type: 'Message', text: 'message 1' },
109
110
  { id: '2', type: 'Message', text: 'message 2' },
110
111
  ],
@@ -116,7 +117,6 @@ describe('updateRecordInPagesData', () => {
116
117
  data: [
117
118
  { id: '3', type: 'Message', text: 'message 3' },
118
119
  { id: '4', type: 'Message', text: 'message 4' },
119
- { id, type: 'Message', text: `updated example ${id}` },
120
120
  ],
121
121
  included: [],
122
122
  links: {},
@@ -127,49 +127,14 @@ describe('updateRecordInPagesData', () => {
127
127
  })
128
128
  })
129
129
 
130
- describe('addRecordInPagesData', () => {
131
- it('should add records that do not yet exist', () => {
132
- const id = '7'
130
+ describe('deleteRecordInPagesData', () => {
131
+ it('should delete records from page', () => {
132
+ const id = '4'
133
133
  const record = createRecord({ id })
134
134
 
135
- const result = addRecordInPagesData<typeof record>({
136
- data,
137
- record,
138
- })
139
-
140
- expect(result).toEqual({
141
- pageParams: {},
142
- pages: [
143
- {
144
- data: [
145
- { id: '1', type: 'Message', text: 'message 1' },
146
- { id: '2', type: 'Message', text: 'message 2' },
147
- ],
148
- included: [],
149
- links: {},
150
- meta: { count: 2, totalCount: 2 },
151
- },
152
- {
153
- data: [
154
- { id: '3', type: 'Message', text: 'message 3' },
155
- { id: '4', type: 'Message', text: 'message 4' },
156
- record,
157
- ],
158
- included: [],
159
- links: {},
160
- meta: { count: 2, totalCount: 2 },
161
- },
162
- ],
163
- })
164
- })
165
-
166
- it('should add the record in the pages data and update with a custom processRecord function', () => {
167
- const id = '8'
168
- const record = createRecord({ id })
169
- const result = addRecordInPagesData<typeof record>({
135
+ const result = deleteRecordInPagesData<typeof record>({
170
136
  data,
171
137
  record,
172
- processRecord: r => ({ ...r, text: 'updated ' + r.text }),
173
138
  })
174
139
 
175
140
  expect(result).toEqual({
@@ -185,11 +150,7 @@ describe('addRecordInPagesData', () => {
185
150
  meta: { count: 2, totalCount: 2 },
186
151
  },
187
152
  {
188
- data: [
189
- { id: '3', type: 'Message', text: 'message 3' },
190
- { id: '4', type: 'Message', text: 'message 4' },
191
- { ...record, text: `updated example ${id}` },
192
- ],
153
+ data: [{ id: '3', type: 'Message', text: 'message 3' }],
193
154
  included: [],
194
155
  links: {},
195
156
  meta: { count: 2, totalCount: 2 },
@@ -7,6 +7,7 @@ import { MessageReaction } from '../../components/conversation/message_reaction'
7
7
  import { Avatar, Text } from '../../components/display'
8
8
  import { useTheme } from '../../hooks'
9
9
  import { MessageResource } from '../../types'
10
+ import { ReactionCountResource } from '../../types/resources/reaction'
10
11
 
11
12
  /** Message
12
13
  * Component for display of a message within a conversation list
@@ -21,9 +22,13 @@ export function Message(props: MessageResource & { conversation_id: string }) {
21
22
  conversation_id,
22
23
  })
23
24
  }
24
- // TODO: open the reaction screen to show who reacted
25
- const handleReactionPress = handleMessagePress
26
-
25
+ const handleReactionPress = (reaction: ReactionCountResource) => {
26
+ navigation.navigate('Reactions', {
27
+ message_id: props.id,
28
+ conversation_id,
29
+ reaction_value: reaction.value,
30
+ })
31
+ }
27
32
  if (!text) return null
28
33
 
29
34
  return (
@@ -19,12 +19,16 @@ export function MessageReaction({
19
19
  onPress,
20
20
  }: {
21
21
  reaction: ReactionCountResource
22
- onPress: () => void
22
+ onPress: (_reaction: ReactionCountResource) => void
23
23
  }) {
24
24
  const styles = useReactionStyles(reaction)
25
25
 
26
26
  return (
27
- <PlatformPressable key={reaction.value} style={styles.reaction} onPress={onPress}>
27
+ <PlatformPressable
28
+ key={reaction.value}
29
+ style={styles.reaction}
30
+ onPress={() => onPress(reaction)}
31
+ >
28
32
  <Text style={styles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
29
33
  <Text style={styles.reactionText}>{reaction.count}</Text>
30
34
  </PlatformPressable>
@@ -1,45 +1,42 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
- import { FlatList, Pressable, StyleSheet } from 'react-native'
3
+ import { FlatList, Pressable, StyleSheet, View } from 'react-native'
4
4
  import { useTheme } from '../hooks'
5
- import { useSuspensePaginator } from '../hooks/use_suspense_api'
6
- import { ConversationResource } from '../types'
7
- import { GetRequest } from '../utils/client/types'
8
- import { Heading, Text } from './display'
5
+ import { useConversationsJoltEvents } from '../hooks/use_conversation_jolt_events'
6
+ import { useConversations } from '../hooks/use_conversations'
7
+ import { formatDatePreview } from '../utils/date'
8
+ import { AvatarGroup, Badge, Heading, Text, TextButton } from './display'
9
9
 
10
10
  export const Conversations = () => {
11
11
  const styles = useStyles()
12
- const request: GetRequest = {
13
- url: '/me/conversations',
14
- data: {
15
- perPage: 20,
16
- order: '-last_message',
17
- fields: {
18
- Conversation: [
19
- 'title',
20
- 'last_message_created_at',
21
- 'last_message_author_name',
22
- 'last_message_text_preview',
23
- 'unread_count',
24
- ],
25
- },
26
- },
27
- }
28
- const { data: conversations, fetchNextPage } = useSuspensePaginator<ConversationResource>(request)
12
+
13
+ const { conversations, fetchNextPage, refetch, isRefetching } = useConversations()
29
14
 
30
15
  // TODO: Filter using the API
31
16
  const nonEmptyConversations = conversations.filter(c => c.lastMessageTextPreview) || []
32
-
33
17
  const navigation = useNavigation()
34
18
 
19
+ useConversationsJoltEvents()
20
+
35
21
  return (
36
22
  <FlatList
37
23
  data={nonEmptyConversations}
38
24
  contentContainerStyle={styles.container}
39
25
  style={styles.scrollView}
26
+ onRefresh={refetch}
27
+ refreshing={isRefetching}
28
+ ListHeaderComponent={
29
+ <View style={styles.header}>
30
+ <Heading numberOfLines={1} variant="h2">
31
+ Conversations
32
+ </Heading>
33
+ <TextButton>Mark all read</TextButton>
34
+ </View>
35
+ }
40
36
  ListEmptyComponent={<Text>No conversations found</Text>}
41
37
  renderItem={({ item }) => (
42
38
  <Pressable
39
+ style={styles.conversation}
43
40
  onPress={() =>
44
41
  navigation.navigate('Conversation', {
45
42
  conversation_id: item.id,
@@ -47,12 +44,30 @@ export const Conversations = () => {
47
44
  })
48
45
  }
49
46
  >
50
- <Heading numberOfLines={1} variant="h3">
51
- {item.title}
52
- </Heading>
53
- <Text style={styles.listItem}>
54
- {item.lastMessageAuthorName}: {item.lastMessageTextPreview}
55
- </Text>
47
+ <AvatarGroup size="lg" sourceUris={item.previewAvatarUrls || []} />
48
+ <View style={styles.conversationBody}>
49
+ <Heading numberOfLines={1} variant="h3" style={styles.conversationTitle}>
50
+ {item.title}
51
+ </Heading>
52
+ <Text style={styles.listItem} numberOfLines={2}>
53
+ {item.lastMessageAuthorName}: {item.lastMessageTextPreview}
54
+ </Text>
55
+ <View style={styles.conversationBadges}>
56
+ {item.badges?.map(badge => (
57
+ <Badge
58
+ key={badge.text}
59
+ variant="meta"
60
+ productLogoName={badge.appName}
61
+ label={badge.pcoResourceType}
62
+ metaLabel={badge.text || ''}
63
+ />
64
+ ))}
65
+ </View>
66
+ </View>
67
+ <View style={styles.conversationExtra}>
68
+ <Text variant="secondary">{formatDatePreview(item.lastMessageCreatedAt)}</Text>
69
+ <UnreadCountBadge count={item.unreadCount} />
70
+ </View>
56
71
  </Pressable>
57
72
  )}
58
73
  onEndReached={() => fetchNextPage()}
@@ -60,13 +75,61 @@ export const Conversations = () => {
60
75
  )
61
76
  }
62
77
 
78
+ const UnreadCountBadge = ({ count }: { count: number }) => {
79
+ const styles = useStyles()
80
+ const displayCount = count > 99 ? '99+' : count
81
+
82
+ if (count === 0) return null
83
+
84
+ return (
85
+ <Text variant="tertiary" style={styles.unreadCountBadge}>
86
+ {displayCount}
87
+ </Text>
88
+ )
89
+ }
90
+
63
91
  const useStyles = () => {
64
92
  const { colors } = useTheme()
65
93
 
66
94
  return StyleSheet.create({
67
- scrollView: { flex: 1, backgroundColor: colors.fillColorNeutral090 },
68
- container: { gap: 8, padding: 16 },
69
- foo: { fontSize: 24, color: colors.testColor },
95
+ header: {
96
+ flexDirection: 'row',
97
+ justifyContent: 'space-between',
98
+ paddingTop: 8,
99
+ paddingBottom: 8,
100
+ paddingHorizontal: 16,
101
+ },
102
+ scrollView: { flex: 1 },
103
+ container: { gap: 8, paddingVertical: 16 },
70
104
  listItem: { color: colors.fillColorNeutral020 },
105
+ conversation: {
106
+ flexDirection: 'row',
107
+ gap: 8,
108
+ borderBottomWidth: 1,
109
+ borderBottomColor: colors.fillColorNeutral060,
110
+ paddingTop: 4,
111
+ paddingBottom: 12,
112
+ paddingHorizontal: 16,
113
+ },
114
+ conversationTitle: {},
115
+ conversationBody: {
116
+ flex: 1,
117
+ rowGap: 2,
118
+ },
119
+ conversationExtra: {
120
+ rowGap: 2,
121
+ },
122
+ conversationBadges: {
123
+ marginTop: 4,
124
+ alignItems: 'flex-start',
125
+ },
126
+ unreadCountBadge: {
127
+ alignSelf: 'flex-end',
128
+ backgroundColor: colors.interaction,
129
+ paddingVertical: 0,
130
+ paddingHorizontal: 10,
131
+ borderRadius: 24,
132
+ color: 'white',
133
+ },
71
134
  })
72
135
  }
@@ -54,12 +54,7 @@ type VariantStyles = Record<
54
54
  }
55
55
  >
56
56
 
57
- const LOGO_NAMES = {
58
- groups: 'groups',
59
- services: 'services',
60
- } as const
61
-
62
- type PoductLogoNameUnion = (typeof LOGO_NAMES)[keyof typeof LOGO_NAMES]
57
+ type ProductLogoName = 'Groups' | 'Services' | 'groups' | 'services'
63
58
 
64
59
  const PRODUCT_LOGO_COMPONENT_MAP = {
65
60
  groups: GroupsLogo,
@@ -90,7 +85,7 @@ interface BadgeProps {
90
85
  /**
91
86
  * Adds a product logo to the left of the text.
92
87
  */
93
- productLogoName?: PoductLogoNameUnion
88
+ productLogoName?: ProductLogoName
94
89
  /**
95
90
  * Shows an icon of the user choice to the left of the text.
96
91
  */
@@ -123,7 +118,7 @@ export function Badge({
123
118
  const hasMetaLabel = Boolean(metaLabel)
124
119
 
125
120
  const showLogo = showBadgeLogo && productLogoName && isMeta
126
- const ProductLogoSvg = showLogo && PRODUCT_LOGO_COMPONENT_MAP[productLogoName]
121
+ const ProductLogoSvg = showLogo && PRODUCT_LOGO_COMPONENT_MAP[productLogoName?.toLowerCase()]
127
122
  const badgeLabel = isMetaSubtle && hasMetaLabel ? `${label}:` : label
128
123
 
129
124
  return (
@@ -0,0 +1,142 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import React, { useEffect, useRef, useState } from 'react'
3
+ import { Animated, Easing, StyleSheet, View, ViewStyle } from 'react-native'
4
+ import { useTheme } from '../../hooks'
5
+
6
+ // =================================
7
+ // ====== Component ================
8
+ // =================================
9
+
10
+ interface TabsProps<ItemT> {
11
+ data: ArrayLike<ItemT> | null | undefined
12
+ activeTab?: ItemT
13
+ keyExtractor?: (_item: ItemT, _index?: number) => string
14
+ onTabPress?: (_item: ItemT) => void
15
+ renderItem: (_: { item: ItemT; index: number }) => React.ReactNode
16
+ style?: ViewStyle
17
+ contentContainerStyle?: ViewStyle
18
+ }
19
+
20
+ const defaultKeyExtractor = (item: any, index?: number) => {
21
+ if (typeof item === 'string') return item
22
+
23
+ return item.id || index?.toString() || item.toString()
24
+ }
25
+
26
+ export function Tabs<ItemT>({
27
+ activeTab = { id: '' } as ItemT,
28
+ contentContainerStyle,
29
+ data,
30
+ keyExtractor = defaultKeyExtractor,
31
+ onTabPress,
32
+ renderItem,
33
+ style,
34
+ }: TabsProps<ItemT>) {
35
+ const [tabDimSet, setTabDimensions] = useState<Set<{ index: number; width: number }>>(new Set())
36
+ const tabDimensions = Array.from(tabDimSet)
37
+ const [tabHeight, setTabHeight] = useState(0)
38
+ const styles = useStyles()
39
+ const opacity = useRef(new Animated.Value(0)).current
40
+ const tabCursorPosition = useRef(new Animated.Value(0)).current
41
+ const dataArray = Array.from(data || [])
42
+ const activeTabIndex = dataArray.findIndex(
43
+ (item, index) => keyExtractor(item, index) === keyExtractor(activeTab)
44
+ )
45
+ const gap = 8
46
+ const tabCursorSpacing = tabDimensions
47
+ .slice(0, activeTabIndex)
48
+ .reduce((acc, { width }) => acc + width + gap, 0)
49
+
50
+ Animated.timing(tabCursorPosition, {
51
+ toValue: tabCursorSpacing,
52
+ easing: Easing.inOut(Easing.ease),
53
+ duration: 100,
54
+ useNativeDriver: true,
55
+ }).start()
56
+
57
+ useEffect(() => {
58
+ if (activeTabIndex === -1) return
59
+
60
+ Animated.timing(opacity, {
61
+ toValue: 1,
62
+ easing: Easing.inOut(Easing.ease),
63
+ duration: 500,
64
+ useNativeDriver: true,
65
+ }).start()
66
+ }, [opacity, activeTabIndex])
67
+
68
+ return (
69
+ <View style={[styles.container, style]}>
70
+ <Animated.View style={[styles.contentContainer, contentContainerStyle]}>
71
+ <View
72
+ style={styles.tabsContainer}
73
+ onLayout={event => {
74
+ const { height } = event.nativeEvent.layout
75
+ setTabHeight(height)
76
+ }}
77
+ >
78
+ {dataArray.map((item, index) => {
79
+ return (
80
+ <PlatformPressable
81
+ key={index}
82
+ style={styles.tab}
83
+ onPress={() => {
84
+ onTabPress?.(item)
85
+ }}
86
+ >
87
+ <View
88
+ onLayout={event => {
89
+ const { width } = event.nativeEvent.layout
90
+ setTabDimensions(dimensions => dimensions.add({ index, width }))
91
+ }}
92
+ >
93
+ {renderItem({ item, index })}
94
+ </View>
95
+ </PlatformPressable>
96
+ )
97
+ })}
98
+ </View>
99
+ <Animated.View
100
+ style={[
101
+ styles.cursor,
102
+ {
103
+ opacity,
104
+ top: tabHeight - 5,
105
+ width: tabDimensions[activeTabIndex]?.width || 0,
106
+ transform: [{ translateX: tabCursorPosition }],
107
+ },
108
+ ]}
109
+ />
110
+ </Animated.View>
111
+ </View>
112
+ )
113
+ }
114
+
115
+ // =================================
116
+ // ====== Styles ===================
117
+ // =================================
118
+
119
+ const useStyles = () => {
120
+ const theme = useTheme()
121
+ return StyleSheet.create({
122
+ container: {
123
+ flexDirection: 'row',
124
+ justifyContent: 'center',
125
+ },
126
+ contentContainer: {},
127
+ cursor: {
128
+ borderBottomWidth: 3,
129
+ borderBottomColor: theme.colors.interaction,
130
+ height: 5,
131
+ flex: 1,
132
+ position: 'absolute',
133
+ zIndex: 5,
134
+ },
135
+ tab: {},
136
+ tabsContainer: {
137
+ flex: 1,
138
+ flexDirection: 'row',
139
+ gap: 8,
140
+ },
141
+ })
142
+ }
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-quer
2
2
  import React, { useContext, useEffect, useRef } from 'react'
3
3
  import { ViewProps } from 'react-native'
4
4
  import { Client } from '../utils'
5
- import { GetRequest } from '../utils/client/types'
6
5
  import { ChatContext, ChatContextValue } from './chat_context'
6
+ import { RequestQueryKey } from '../hooks'
7
7
 
8
8
  let apiClient: Client | undefined
9
9
 
@@ -12,9 +12,9 @@ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
12
12
  throw new Error('No token present')
13
13
  }
14
14
 
15
- const data = queryKey[0] as GetRequest
15
+ const [url, data, headers] = queryKey as RequestQueryKey
16
16
 
17
- return apiClient.get(data)
17
+ return apiClient.get({ url, data, headers })
18
18
  }
19
19
 
20
20
  export const queryClient = new QueryClient({
@@ -0,0 +1,67 @@
1
+ import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
2
+ import { InfiniteData, useQueryClient } from '@tanstack/react-query'
3
+ import { useContext } from 'react'
4
+ import { ChatContext } from '../contexts'
5
+ import { ApiCollection, ApiResource, ConversationResource } from '../types'
6
+ import { deleteRecordInPagesData, updateRecordInPagesData } from '../utils'
7
+ import { getConversationsRequestArgs } from './use_conversations'
8
+ import { useCurrentPerson } from './use_current_person'
9
+ import { useJoltChannel, useJoltEvent } from './use_jolt'
10
+ import { getRequestQueryKey } from './use_suspense_api'
11
+
12
+ type QueryData = InfiniteData<ApiCollection<ConversationResource>>
13
+ interface JoltConversationsEvent extends CustomMessage {
14
+ data: {
15
+ data: ConversationResource
16
+ }
17
+ }
18
+
19
+ export function useConversationsJoltEvents() {
20
+ const { client } = useContext(ChatContext)
21
+ const queryClient = useQueryClient()
22
+ const currentPerson = useCurrentPerson()
23
+ const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
24
+
25
+ const conversationsRequestArgs = getConversationsRequestArgs()
26
+ const conversationQueryKey = getRequestQueryKey(conversationsRequestArgs)
27
+
28
+ const fetchConversation = async ({ id }: ConversationResource) => {
29
+ const { data: argsData } = conversationsRequestArgs
30
+ const { data } = await client.get<ApiResource<ConversationResource>>({
31
+ url: `/me/conversations/${id}`,
32
+ data: {
33
+ fields: argsData.fields,
34
+ include: argsData.include,
35
+ },
36
+ })
37
+
38
+ return data
39
+ }
40
+
41
+ const handleConversationUpdateOrCreate = async (e: JoltConversationsEvent) => {
42
+ const conversation = await fetchConversation(e.data.data).catch(c => c)
43
+
44
+ queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
45
+ updateRecordInPagesData({
46
+ data: prev,
47
+ record: conversation,
48
+ processRecord: (record, current) => {
49
+ return { ...current, ...record }
50
+ },
51
+ })
52
+ )
53
+ }
54
+
55
+ const handleConversationDestroy = (e: JoltConversationsEvent) => {
56
+ queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
57
+ deleteRecordInPagesData({
58
+ data: prev,
59
+ record: e.data.data,
60
+ })
61
+ )
62
+ }
63
+
64
+ useJoltEvent(joltChannel, 'conversation.updated', handleConversationUpdateOrCreate)
65
+ useJoltEvent(joltChannel, 'conversation.created', handleConversationUpdateOrCreate)
66
+ useJoltEvent(joltChannel, 'conversation.destroyed', handleConversationDestroy)
67
+ }
@@ -1,5 +1,9 @@
1
1
  import { MessageResource } from '../types'
2
- import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
2
+ import {
3
+ getRequestQueryKey,
4
+ SuspensePaginatorOptions,
5
+ useSuspensePaginator,
6
+ } from './use_suspense_api'
3
7
 
4
8
  export const useConversationMessages = (
5
9
  { conversation_id }: { conversation_id: string },
@@ -39,5 +43,5 @@ export const getMessagesRequestArgs = ({ conversation_id }: { conversation_id: s
39
43
 
40
44
  export const getMessagesQueryKey = ({ conversation_id }: { conversation_id: string }) => {
41
45
  const requestArgs = getMessagesRequestArgs({ conversation_id })
42
- return [requestArgs.url, requestArgs.data]
46
+ return getRequestQueryKey(requestArgs)
43
47
  }