@unif/react-native-chat 3.2.4 → 3.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 (114) hide show
  1. package/lib/module/components/Attachments/AttachmentItem.js +3 -2
  2. package/lib/module/components/Attachments/AttachmentItem.js.map +1 -1
  3. package/lib/module/components/Attachments/Attachments.js +1 -0
  4. package/lib/module/components/Attachments/Attachments.js.map +1 -1
  5. package/lib/module/components/Attachments/styles.js +4 -0
  6. package/lib/module/components/Attachments/styles.js.map +1 -1
  7. package/lib/module/components/Composer/Composer.js +13 -9
  8. package/lib/module/components/Composer/Composer.js.map +1 -1
  9. package/lib/module/components/Confirmation/Confirmation.js +29 -13
  10. package/lib/module/components/Confirmation/Confirmation.js.map +1 -1
  11. package/lib/module/components/Confirmation/styles.js +17 -6
  12. package/lib/module/components/Confirmation/styles.js.map +1 -1
  13. package/lib/module/components/Message/MarkdownContent/styles.js +29 -33
  14. package/lib/module/components/Message/MarkdownContent/styles.js.map +1 -1
  15. package/lib/module/components/Message/Message.js +37 -24
  16. package/lib/module/components/Message/Message.js.map +1 -1
  17. package/lib/module/components/Message/MessageWaiting.js +53 -0
  18. package/lib/module/components/Message/MessageWaiting.js.map +1 -0
  19. package/lib/module/components/Message/constants.js +3 -0
  20. package/lib/module/components/Message/constants.js.map +1 -1
  21. package/lib/module/components/Message/styles.js +41 -3
  22. package/lib/module/components/Message/styles.js.map +1 -1
  23. package/lib/module/components/MessageList/MessageList.js +60 -31
  24. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  25. package/lib/module/components/MessageList/MessageListEndButton.js +60 -0
  26. package/lib/module/components/MessageList/MessageListEndButton.js.map +1 -0
  27. package/lib/module/components/MessageList/MessageListRow.js +9 -2
  28. package/lib/module/components/MessageList/MessageListRow.js.map +1 -1
  29. package/lib/module/components/MessageList/styles.js +6 -0
  30. package/lib/module/components/MessageList/styles.js.map +1 -1
  31. package/lib/module/components/MessageList/useMessageListScroll.js +218 -13
  32. package/lib/module/components/MessageList/useMessageListScroll.js.map +1 -1
  33. package/lib/module/components/Process/Process.js +15 -5
  34. package/lib/module/components/Process/Process.js.map +1 -1
  35. package/lib/module/components/Process/ProcessCompactStep.js +21 -12
  36. package/lib/module/components/Process/ProcessCompactStep.js.map +1 -1
  37. package/lib/module/components/Process/ProcessTimelineStep.js +79 -0
  38. package/lib/module/components/Process/ProcessTimelineStep.js.map +1 -0
  39. package/lib/module/components/Process/styles.js +63 -3
  40. package/lib/module/components/Process/styles.js.map +1 -1
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/typescript/src/components/Attachments/AttachmentItem.d.ts +1 -1
  43. package/lib/typescript/src/components/Attachments/AttachmentItem.d.ts.map +1 -1
  44. package/lib/typescript/src/components/Attachments/Attachments.d.ts.map +1 -1
  45. package/lib/typescript/src/components/Attachments/styles.d.ts +4 -0
  46. package/lib/typescript/src/components/Attachments/styles.d.ts.map +1 -1
  47. package/lib/typescript/src/components/Attachments/types.d.ts +2 -1
  48. package/lib/typescript/src/components/Attachments/types.d.ts.map +1 -1
  49. package/lib/typescript/src/components/Composer/Composer.d.ts.map +1 -1
  50. package/lib/typescript/src/components/Confirmation/Confirmation.d.ts.map +1 -1
  51. package/lib/typescript/src/components/Confirmation/styles.d.ts +13 -2
  52. package/lib/typescript/src/components/Confirmation/styles.d.ts.map +1 -1
  53. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts +32 -66
  54. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts.map +1 -1
  55. package/lib/typescript/src/components/Message/Message.d.ts.map +1 -1
  56. package/lib/typescript/src/components/Message/MessageWaiting.d.ts +3 -0
  57. package/lib/typescript/src/components/Message/MessageWaiting.d.ts.map +1 -0
  58. package/lib/typescript/src/components/Message/constants.d.ts +3 -0
  59. package/lib/typescript/src/components/Message/constants.d.ts.map +1 -1
  60. package/lib/typescript/src/components/Message/styles.d.ts +40 -2
  61. package/lib/typescript/src/components/Message/styles.d.ts.map +1 -1
  62. package/lib/typescript/src/components/Message/types.d.ts +5 -0
  63. package/lib/typescript/src/components/Message/types.d.ts.map +1 -1
  64. package/lib/typescript/src/components/MessageList/MessageList.d.ts.map +1 -1
  65. package/lib/typescript/src/components/MessageList/MessageListEndButton.d.ts +3 -0
  66. package/lib/typescript/src/components/MessageList/MessageListEndButton.d.ts.map +1 -0
  67. package/lib/typescript/src/components/MessageList/MessageListRow.d.ts +2 -8
  68. package/lib/typescript/src/components/MessageList/MessageListRow.d.ts.map +1 -1
  69. package/lib/typescript/src/components/MessageList/index.d.ts +1 -1
  70. package/lib/typescript/src/components/MessageList/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/MessageList/styles.d.ts +6 -0
  72. package/lib/typescript/src/components/MessageList/styles.d.ts.map +1 -1
  73. package/lib/typescript/src/components/MessageList/types.d.ts +41 -2
  74. package/lib/typescript/src/components/MessageList/types.d.ts.map +1 -1
  75. package/lib/typescript/src/components/MessageList/useMessageListScroll.d.ts +14 -3
  76. package/lib/typescript/src/components/MessageList/useMessageListScroll.d.ts.map +1 -1
  77. package/lib/typescript/src/components/Process/Process.d.ts.map +1 -1
  78. package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts +1 -1
  79. package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts.map +1 -1
  80. package/lib/typescript/src/components/Process/ProcessTimelineStep.d.ts +5 -0
  81. package/lib/typescript/src/components/Process/ProcessTimelineStep.d.ts.map +1 -0
  82. package/lib/typescript/src/components/Process/styles.d.ts +62 -2
  83. package/lib/typescript/src/components/Process/styles.d.ts.map +1 -1
  84. package/lib/typescript/src/components/Process/types.d.ts +7 -2
  85. package/lib/typescript/src/components/Process/types.d.ts.map +1 -1
  86. package/lib/typescript/src/index.d.ts +1 -1
  87. package/lib/typescript/src/index.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/components/Attachments/AttachmentItem.tsx +11 -3
  90. package/src/components/Attachments/Attachments.tsx +1 -0
  91. package/src/components/Attachments/styles.ts +1 -0
  92. package/src/components/Attachments/types.ts +2 -1
  93. package/src/components/Composer/Composer.tsx +34 -8
  94. package/src/components/Confirmation/Confirmation.tsx +50 -14
  95. package/src/components/Confirmation/styles.ts +13 -6
  96. package/src/components/Message/MarkdownContent/styles.ts +18 -44
  97. package/src/components/Message/Message.tsx +53 -33
  98. package/src/components/Message/MessageWaiting.tsx +68 -0
  99. package/src/components/Message/constants.ts +4 -0
  100. package/src/components/Message/styles.ts +25 -2
  101. package/src/components/Message/types.ts +6 -0
  102. package/src/components/MessageList/MessageList.tsx +107 -35
  103. package/src/components/MessageList/MessageListEndButton.tsx +61 -0
  104. package/src/components/MessageList/MessageListRow.tsx +12 -12
  105. package/src/components/MessageList/index.ts +1 -0
  106. package/src/components/MessageList/styles.ts +6 -0
  107. package/src/components/MessageList/types.ts +52 -2
  108. package/src/components/MessageList/useMessageListScroll.ts +296 -21
  109. package/src/components/Process/Process.tsx +38 -5
  110. package/src/components/Process/ProcessCompactStep.tsx +32 -16
  111. package/src/components/Process/ProcessTimelineStep.tsx +109 -0
  112. package/src/components/Process/styles.ts +48 -3
  113. package/src/components/Process/types.ts +7 -2
  114. package/src/index.tsx +1 -0
@@ -1,14 +1,21 @@
1
1
  import { useCallback, useLayoutEffect, useRef, useState } from 'react';
2
2
  import type { RefObject } from 'react';
3
3
  import type {
4
+ DimensionValue,
4
5
  FlatList,
6
+ FlatListProps,
5
7
  LayoutChangeEvent,
8
+ LayoutRectangle,
6
9
  NativeScrollEvent,
7
10
  NativeSyntheticEvent,
8
11
  } from 'react-native';
9
12
  import { DEFAULT_END_THRESHOLD } from './constants';
10
13
  import type {
11
14
  ListMeasurements,
15
+ MessageListAnchorOptions,
16
+ MessageListAnchorRequest,
17
+ MessageListItemLayout,
18
+ MessageListPendingScroll,
12
19
  MessageListProps,
13
20
  MessageListScrollOptions,
14
21
  } from './types';
@@ -19,10 +26,24 @@ interface MessageListScrollInput<T> extends Pick<
19
26
  > {
20
27
  listRef: RefObject<FlatList<T> | null>;
21
28
  keys: readonly string[];
29
+ contentMinimumHeight?: DimensionValue;
30
+ contentBottomPadding?: DimensionValue;
31
+ hasFooter?: boolean;
22
32
  preservingPrependRef: RefObject<boolean>;
23
33
  cancelPrependPreservation(): void;
24
34
  }
25
35
 
36
+ function layoutValue(value: DimensionValue | undefined, extent: number) {
37
+ if (typeof value === 'number' && Number.isFinite(value))
38
+ return Math.max(0, value);
39
+ if (typeof value === 'string' && value.endsWith('%')) {
40
+ const percentage = Number.parseFloat(value);
41
+ if (Number.isFinite(percentage))
42
+ return Math.max(0, (extent * percentage) / 100);
43
+ }
44
+ return 0;
45
+ }
46
+
26
47
  export function useMessageListScroll<T>({
27
48
  listRef,
28
49
  keys,
@@ -30,10 +51,14 @@ export function useMessageListScroll<T>({
30
51
  followOutput,
31
52
  endThreshold = DEFAULT_END_THRESHOLD,
32
53
  onAtEndChange,
54
+ contentMinimumHeight,
55
+ contentBottomPadding,
56
+ hasFooter,
33
57
  preservingPrependRef,
34
58
  cancelPrependPreservation,
35
59
  }: MessageListScrollInput<T>) {
36
60
  const measurements = useRef<ListMeasurements>({
61
+ width: 0,
37
62
  viewport: 0,
38
63
  content: 0,
39
64
  offset: 0,
@@ -42,29 +67,27 @@ export function useMessageListScroll<T>({
42
67
  const initial = useRef(initialPosition);
43
68
  const initialized = useRef(false);
44
69
  const previousKeys = useRef<readonly string[]>([]);
70
+ const currentKeys = useRef(keys);
71
+ currentKeys.current = keys;
72
+ const contentStyle = useRef({ contentMinimumHeight, contentBottomPadding });
73
+ contentStyle.current = { contentMinimumHeight, contentBottomPadding };
45
74
  const prependPending = useRef(false);
46
- const pendingScroll = useRef<MessageListScrollOptions | undefined>(undefined);
75
+ const pendingScroll = useRef<MessageListPendingScroll | undefined>(undefined);
47
76
  const userScrolling = useRef(false);
48
77
  const atEndRef = useRef<boolean | undefined>(undefined);
49
78
  const [atEnd, setAtEnd] = useState(true);
79
+ const anchor = useRef<MessageListAnchorRequest | undefined>(undefined);
80
+ const layouts = useRef(new Map<string, MessageListItemLayout>());
81
+ const layoutRevision = useRef(0);
82
+ const footerHeight = useRef(0);
83
+ if (!hasFooter) footerHeight.current = 0;
84
+ const layoutChanged = useRef<() => void>(() => {});
85
+ const [anchorMinimumHeight, setAnchorMinimumHeight] = useState<number>();
86
+ const minimumRef = useRef<number | undefined>(undefined);
50
87
  const threshold = Number.isFinite(endThreshold)
51
88
  ? Math.max(0, endThreshold)
52
89
  : DEFAULT_END_THRESHOLD;
53
90
 
54
- useLayoutEffect(() => {
55
- const previous = previousKeys.current;
56
- const added = keys.length - previous.length;
57
- const onlyPrepended =
58
- previous.length > 0 &&
59
- added > 0 &&
60
- previous.every((key, index) => keys[index + added] === key);
61
- const changed =
62
- keys.length !== previous.length ||
63
- keys.some((key, index) => key !== previous[index]);
64
- if (changed) prependPending.current = onlyPrepended;
65
- previousKeys.current = keys;
66
- }, [keys]);
67
-
68
91
  const ready = useCallback(() => {
69
92
  const current = measurements.current;
70
93
  return current.viewport > 0 && current.hasContentSize;
@@ -81,11 +104,192 @@ export function useMessageListScroll<T>({
81
104
  }
82
105
  }, [onAtEndChange, ready, threshold]);
83
106
 
107
+ const cancelAnchor = useCallback(() => {
108
+ anchor.current = undefined;
109
+ minimumRef.current = undefined;
110
+ setAnchorMinimumHeight(undefined);
111
+ }, []);
112
+
113
+ const naturalContentHeight = useCallback(() => {
114
+ const last = currentKeys.current.at(-1);
115
+ const cell =
116
+ last === undefined ? undefined : layouts.current.get(last)?.cell;
117
+ if (!cell) return undefined;
118
+ const { width, viewport } = measurements.current;
119
+ return Math.max(
120
+ cell.y +
121
+ cell.height +
122
+ footerHeight.current +
123
+ layoutValue(contentStyle.current.contentBottomPadding, width),
124
+ layoutValue(contentStyle.current.contentMinimumHeight, viewport)
125
+ );
126
+ }, []);
127
+
128
+ const applyAnchor = useCallback(() => {
129
+ const request = anchor.current;
130
+ if (!request || !ready()) return;
131
+ const index = currentKeys.current.indexOf(request.key);
132
+ if (index < 0) return;
133
+ request.seen = true;
134
+ const position = layouts.current.get(request.key);
135
+ if (!position?.cell || position.itemY === undefined) {
136
+ // An offscreen row first needs to enter FlatList's render window. This
137
+ // estimate only reveals it; success still requires its actual layouts.
138
+ if (request.locatedAt !== layoutRevision.current) {
139
+ request.locatedAt = layoutRevision.current;
140
+ listRef.current?.scrollToIndex({
141
+ index,
142
+ animated: false,
143
+ viewPosition: 0,
144
+ });
145
+ }
146
+ return;
147
+ }
148
+ const { viewport, content } = measurements.current;
149
+ const offset = Math.max(
150
+ 0,
151
+ position.cell.y + position.itemY - request.topOffset
152
+ );
153
+ const minimum = Math.max(
154
+ offset + viewport,
155
+ layoutValue(contentStyle.current.contentMinimumHeight, viewport)
156
+ );
157
+ minimumRef.current = minimum;
158
+ setAnchorMinimumHeight(minimum);
159
+ // Yoga's requested minimum is not yet native scroll space. Wait for the
160
+ // content-size event; otherwise native scrolling can clamp the target.
161
+ if (content + 0.5 < minimum) return;
162
+ if (request.offset === offset && request.viewport === viewport) return;
163
+ request.offset = offset;
164
+ request.viewport = viewport;
165
+ request.settled = false;
166
+ initialized.current = true;
167
+ listRef.current?.scrollToOffset({ offset, animated: request.animated });
168
+ measurements.current.offset = offset;
169
+ publishPosition();
170
+ }, [listRef, publishPosition, ready]);
171
+
172
+ const onCellLayout = useCallback(
173
+ (key: string, index: number, cell: LayoutRectangle) => {
174
+ if (
175
+ currentKeys.current[index] !== key ||
176
+ !Number.isFinite(cell.y) ||
177
+ !Number.isFinite(cell.height)
178
+ )
179
+ return;
180
+ const previous = layouts.current.get(key);
181
+ if (
182
+ previous?.index !== index ||
183
+ previous.cell?.x !== cell.x ||
184
+ previous.cell.y !== cell.y ||
185
+ previous.cell.width !== cell.width ||
186
+ previous.cell.height !== cell.height
187
+ )
188
+ layoutRevision.current++;
189
+ layouts.current.set(key, { ...previous, index, cell });
190
+ layoutChanged.current();
191
+ },
192
+ []
193
+ );
194
+ const onItemLayout = useCallback(
195
+ (key: string, index: number, itemY: number) => {
196
+ if (currentKeys.current[index] !== key || !Number.isFinite(itemY)) return;
197
+ const previous = layouts.current.get(key);
198
+ if (previous?.index !== index || previous.itemY !== itemY)
199
+ layoutRevision.current++;
200
+ layouts.current.set(key, { ...previous, index, itemY });
201
+ layoutChanged.current();
202
+ },
203
+ []
204
+ );
205
+ const onCellUnmount = useCallback((key: string, index: number) => {
206
+ if (layouts.current.get(key)?.index === index) {
207
+ layouts.current.delete(key);
208
+ layoutRevision.current++;
209
+ }
210
+ }, []);
211
+ const onFooterLayout = useCallback((event: LayoutChangeEvent) => {
212
+ footerHeight.current = event.nativeEvent.layout.height;
213
+ layoutChanged.current();
214
+ }, []);
215
+
216
+ const anchorToItem = useCallback(
217
+ (key: string, options: MessageListAnchorOptions = {}) => {
218
+ pendingScroll.current = undefined;
219
+ cancelPrependPreservation();
220
+ userScrolling.current = false;
221
+ anchor.current = {
222
+ key,
223
+ topOffset: Number.isFinite(options.topOffset)
224
+ ? Math.max(0, options.topOffset!)
225
+ : 0,
226
+ animated: options.animated ?? true,
227
+ seen: currentKeys.current.includes(key),
228
+ };
229
+ minimumRef.current = 0;
230
+ setAnchorMinimumHeight(0);
231
+ applyAnchor();
232
+ },
233
+ [applyAnchor, cancelPrependPreservation]
234
+ );
235
+
236
+ useLayoutEffect(() => {
237
+ const previous = previousKeys.current;
238
+ const added = keys.length - previous.length;
239
+ const onlyPrepended =
240
+ previous.length > 0 &&
241
+ added > 0 &&
242
+ previous.every((key, index) => keys[index + added] === key);
243
+ const changed =
244
+ keys.length !== previous.length ||
245
+ keys.some((key, index) => key !== previous[index]);
246
+ if (changed) {
247
+ layoutRevision.current++;
248
+ prependPending.current = onlyPrepended;
249
+ const indices = new Map(keys.map((key, index) => [key, index]));
250
+ for (const [key, layout] of layouts.current) {
251
+ const index = indices.get(key);
252
+ if (index === undefined) layouts.current.delete(key);
253
+ else if (index !== layout.index) {
254
+ // A moved cell gets a new natural y; the row's local separator
255
+ // offset remains valid until an actual layout change replaces it.
256
+ layouts.current.set(key, { index, itemY: layout.itemY });
257
+ }
258
+ }
259
+ }
260
+ previousKeys.current = keys;
261
+ const request = anchor.current;
262
+ if (request) {
263
+ if (keys.includes(request.key)) request.seen = true;
264
+ else if (request.seen) {
265
+ cancelAnchor();
266
+ return;
267
+ }
268
+ // Explicit positioning and native/Web prepend preservation must never
269
+ // compensate the same layout twice.
270
+ cancelPrependPreservation();
271
+ applyAnchor();
272
+ }
273
+ }, [applyAnchor, cancelAnchor, cancelPrependPreservation, keys]);
274
+
84
275
  const scrollToEnd = useCallback(
85
276
  (options: MessageListScrollOptions = {}) => {
86
277
  cancelPrependPreservation();
87
- const request = { animated: options.animated ?? true };
88
- if (!ready()) {
278
+ const request: MessageListPendingScroll = {
279
+ animated: options.animated ?? true,
280
+ };
281
+ const natural = naturalContentHeight();
282
+ const minimum = minimumRef.current;
283
+ if (
284
+ minimum !== undefined &&
285
+ natural !== undefined &&
286
+ measurements.current.content <= minimum + 0.5 &&
287
+ natural + 0.5 < measurements.current.content
288
+ ) {
289
+ request.contentBeforeRelease = measurements.current.content;
290
+ }
291
+ cancelAnchor();
292
+ if (!ready() || request.contentBeforeRelease !== undefined) {
89
293
  pendingScroll.current = request;
90
294
  return;
91
295
  }
@@ -100,14 +304,31 @@ export function useMessageListScroll<T>({
100
304
  if (measurements.current.content <= measurements.current.viewport)
101
305
  publishPosition();
102
306
  },
103
- [cancelPrependPreservation, listRef, publishPosition, ready]
307
+ [
308
+ cancelAnchor,
309
+ cancelPrependPreservation,
310
+ listRef,
311
+ naturalContentHeight,
312
+ publishPosition,
313
+ ready,
314
+ ]
104
315
  );
105
316
 
106
317
  const itemCount = keys.length;
107
318
  const reconcileLayout = useCallback(
108
319
  (contentChanged: boolean) => {
109
320
  if (!ready()) return;
321
+ if (anchor.current) {
322
+ applyAnchor();
323
+ return;
324
+ }
110
325
  const request = pendingScroll.current;
326
+ if (
327
+ request?.contentBeforeRelease !== undefined &&
328
+ measurements.current.content === request.contentBeforeRelease &&
329
+ (naturalContentHeight() ?? 0) < request.contentBeforeRelease
330
+ )
331
+ return;
111
332
  if (itemCount > 0 && !initialized.current) {
112
333
  initialized.current = true;
113
334
  if (request || initial.current === 'end') {
@@ -134,8 +355,10 @@ export function useMessageListScroll<T>({
134
355
  else publishPosition();
135
356
  },
136
357
  [
358
+ applyAnchor,
137
359
  followOutput,
138
360
  itemCount,
361
+ naturalContentHeight,
139
362
  preservingPrependRef,
140
363
  publishPosition,
141
364
  ready,
@@ -143,19 +366,36 @@ export function useMessageListScroll<T>({
143
366
  ]
144
367
  );
145
368
 
369
+ layoutChanged.current = () => {
370
+ if (anchor.current) applyAnchor();
371
+ else if (pendingScroll.current) reconcileLayout(false);
372
+ };
373
+
146
374
  const onScroll = useCallback(
147
375
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
148
376
  const { contentOffset, contentSize, layoutMeasurement } =
149
377
  event.nativeEvent;
150
378
  const movedUp = contentOffset.y < measurements.current.offset;
379
+ const geometryChanged =
380
+ measurements.current.content !== contentSize.height ||
381
+ measurements.current.viewport !== layoutMeasurement.height;
382
+ const request = anchor.current;
383
+ if (request?.offset !== undefined) {
384
+ if (Math.abs(contentOffset.y - request.offset) <= 0.5)
385
+ request.settled = true;
386
+ // RN Web wheel scrolling need not dispatch scrollBeginDrag. Once a
387
+ // requested scroll has settled, a user move at unchanged geometry wins.
388
+ else if (request.settled && !geometryChanged) cancelAnchor();
389
+ }
151
390
  const resizingAtEnd =
391
+ !anchor.current &&
152
392
  atEndRef.current &&
153
393
  !movedUp &&
154
394
  !userScrolling.current &&
155
395
  followOutput === 'whenAtEnd' &&
156
- (measurements.current.content !== contentSize.height ||
157
- measurements.current.viewport !== layoutMeasurement.height);
396
+ geometryChanged;
158
397
  measurements.current = {
398
+ width: layoutMeasurement.width,
159
399
  offset: contentOffset.y,
160
400
  content: contentSize.height,
161
401
  viewport: layoutMeasurement.height,
@@ -163,12 +403,13 @@ export function useMessageListScroll<T>({
163
403
  };
164
404
  if (!resizingAtEnd) publishPosition();
165
405
  },
166
- [followOutput, publishPosition]
406
+ [cancelAnchor, followOutput, publishPosition]
167
407
  );
168
408
 
169
409
  const onLayout = useCallback(
170
410
  ({ nativeEvent: { layout } }: LayoutChangeEvent) => {
171
411
  measurements.current.viewport = layout.height;
412
+ measurements.current.width = layout.width;
172
413
  reconcileLayout(false);
173
414
  },
174
415
  [reconcileLayout]
@@ -183,6 +424,11 @@ export function useMessageListScroll<T>({
183
424
  );
184
425
  const onUserScrollBegin = useCallback(() => {
185
426
  userScrolling.current = true;
427
+ pendingScroll.current = undefined;
428
+ cancelAnchor();
429
+ }, [cancelAnchor]);
430
+ const onMomentumScrollBegin = useCallback(() => {
431
+ userScrolling.current = true;
186
432
  }, []);
187
433
  const onScrollEndDrag = useCallback(
188
434
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -202,14 +448,43 @@ export function useMessageListScroll<T>({
202
448
  },
203
449
  [onScroll, publishPosition]
204
450
  );
451
+ const onScrollToIndexFailed = useCallback(
452
+ (
453
+ failure: Parameters<
454
+ NonNullable<FlatListProps<T>['onScrollToIndexFailed']>
455
+ >[0]
456
+ ) => {
457
+ const request = anchor.current;
458
+ if (
459
+ !request ||
460
+ currentKeys.current.indexOf(request.key) !== failure.index ||
461
+ !Number.isFinite(failure.averageItemLength) ||
462
+ failure.averageItemLength <= 0
463
+ )
464
+ return;
465
+ listRef.current?.scrollToOffset({
466
+ offset: failure.index * failure.averageItemLength,
467
+ animated: false,
468
+ });
469
+ },
470
+ [listRef]
471
+ );
205
472
 
206
473
  return {
207
474
  atEnd,
475
+ anchorMinimumHeight,
476
+ anchorToItem,
477
+ onCellLayout,
478
+ onItemLayout,
479
+ onCellUnmount,
480
+ onFooterLayout,
481
+ onScrollToIndexFailed,
208
482
  scrollToEnd,
209
483
  onLayout,
210
484
  onContentSizeChange,
211
485
  onScroll,
212
486
  onUserScrollBegin,
487
+ onMomentumScrollBegin,
213
488
  onScrollEndDrag,
214
489
  onMomentumScrollEnd,
215
490
  };
@@ -9,6 +9,7 @@ import {
9
9
  import { PROCESS_STATUS_ICONS, PROCESS_STATUS_LABELS } from './constants';
10
10
  import { createStyles } from './styles';
11
11
  import { ProcessCompactStep } from './ProcessCompactStep';
12
+ import { ProcessTimelineStep } from './ProcessTimelineStep';
12
13
  import type { ProcessProps, ProcessStep } from './types';
13
14
 
14
15
  function idsWithDetails(
@@ -45,6 +46,10 @@ function formatElapsedMs(elapsedMs: number | undefined): string | undefined {
45
46
 
46
47
  export function Process(props: ProcessProps): React.JSX.Element {
47
48
  const { steps, title, identity, style, testID, variant = 'card' } = props;
49
+ const inlineIdentity =
50
+ variant === 'compact' &&
51
+ steps.length === 1 &&
52
+ props.identityPlacement === 'inline';
48
53
  const styles = useThemedStyles(createStyles);
49
54
  const colors = useColors();
50
55
  const isControlled = props.expandedIds !== undefined;
@@ -73,25 +78,53 @@ export function Process(props: ProcessProps): React.JSX.Element {
73
78
 
74
79
  return (
75
80
  <View
76
- style={[variant === 'compact' ? styles.compactRoot : styles.root, style]}
81
+ style={[
82
+ variant === 'compact'
83
+ ? styles.compactRoot
84
+ : variant === 'timeline'
85
+ ? styles.timelineRoot
86
+ : styles.root,
87
+ style,
88
+ ]}
77
89
  testID={testID}
78
90
  >
79
- {title || identity ? (
91
+ {title || (identity && !inlineIdentity) ? (
80
92
  <View style={styles.heading}>
81
- {identity}
93
+ {!inlineIdentity ? identity : null}
82
94
  {title ? <Text style={styles.headingTitle}>{title}</Text> : null}
83
95
  </View>
84
96
  ) : null}
85
- <View style={variant === 'compact' ? styles.compactSteps : styles.steps}>
97
+ <View
98
+ style={
99
+ variant === 'compact'
100
+ ? styles.compactSteps
101
+ : variant === 'timeline'
102
+ ? undefined
103
+ : styles.steps
104
+ }
105
+ >
86
106
  {steps.map((step, index) => {
87
- const statusLabel = PROCESS_STATUS_LABELS[step.status];
107
+ const statusLabel =
108
+ step.statusText ?? PROCESS_STATUS_LABELS[step.status];
88
109
  const elapsed = formatElapsedMs(step.elapsedMs);
89
110
  const isExpanded = expandedIds.includes(step.id);
111
+ if (variant === 'timeline')
112
+ return (
113
+ <ProcessTimelineStep
114
+ key={step.id}
115
+ step={step}
116
+ last={index === steps.length - 1}
117
+ expanded={isExpanded}
118
+ elapsed={elapsed}
119
+ onToggle={() => toggleDetails(step.id)}
120
+ />
121
+ );
90
122
  if (variant === 'compact')
91
123
  return (
92
124
  <ProcessCompactStep
93
125
  key={step.id}
94
126
  step={step}
127
+ identity={inlineIdentity ? identity : undefined}
95
128
  expanded={isExpanded}
96
129
  elapsed={elapsed}
97
130
  onToggle={() => toggleDetails(step.id)}
@@ -4,8 +4,8 @@ import {
4
4
  Icon,
5
5
  Pulse,
6
6
  Reveal,
7
+ motion,
7
8
  pressedOpacity,
8
- r,
9
9
  useColors,
10
10
  useThemedStyles,
11
11
  } from '@unif/react-native-design';
@@ -15,6 +15,7 @@ import type { ProcessCompactStepProps } from './types';
15
15
 
16
16
  export function ProcessCompactStep({
17
17
  step,
18
+ identity,
18
19
  expanded,
19
20
  elapsed,
20
21
  onToggle,
@@ -23,6 +24,7 @@ export function ProcessCompactStep({
23
24
  const colors = useColors();
24
25
  const hasDetails = step.details != null;
25
26
  const running = step.status === 'running';
27
+ const statusText = step.statusText ?? PROCESS_STATUS_LABELS[step.status];
26
28
  const color = running
27
29
  ? colors.primary
28
30
  : step.status === 'completed'
@@ -37,7 +39,7 @@ export function ProcessCompactStep({
37
39
  ? 'spark'
38
40
  : PROCESS_STATUS_ICONS[step.status]
39
41
  }
40
- size={r(14)}
42
+ size={14}
41
43
  color={color}
42
44
  />
43
45
  );
@@ -47,31 +49,44 @@ export function ProcessCompactStep({
47
49
  testID={`process-step-${step.id}-toggle`}
48
50
  accessible
49
51
  accessibilityRole={hasDetails ? 'button' : undefined}
50
- accessibilityLabel={`${step.title},${PROCESS_STATUS_LABELS[step.status]}${elapsed ? `,${elapsed}` : ''}`}
52
+ accessibilityLabel={`${step.title},${statusText}${elapsed ? `,${elapsed}` : ''}`}
51
53
  accessibilityState={hasDetails ? { expanded } : undefined}
52
54
  disabled={!hasDetails}
55
+ hitSlop={10}
53
56
  onPress={onToggle}
54
57
  style={({ pressed }) => [
55
58
  styles.compactHeader,
56
59
  pressed && { opacity: pressedOpacity },
57
60
  ]}
58
61
  >
59
- {running ? <Pulse>{icon}</Pulse> : icon}
60
- <Text
61
- style={styles.compactTitle}
62
- testID={`process-step-title-${step.id}`}
63
- numberOfLines={1}
64
- >
65
- {step.title}
66
- </Text>
67
- <Text style={[styles.statusText, { color }]}>
68
- {PROCESS_STATUS_LABELS[step.status]}
69
- </Text>
62
+ {identity ? (
63
+ <>
64
+ {identity}
65
+ <View style={styles.spacer} />
66
+ </>
67
+ ) : null}
68
+ {running ? (
69
+ <Pulse from={0.4} duration={motion.pulse / 2}>
70
+ {icon}
71
+ </Pulse>
72
+ ) : (
73
+ icon
74
+ )}
75
+ {!identity ? (
76
+ <Text
77
+ style={styles.compactTitle}
78
+ testID={`process-step-title-${step.id}`}
79
+ numberOfLines={1}
80
+ >
81
+ {step.title}
82
+ </Text>
83
+ ) : null}
84
+ <Text style={styles.compactStatus}>{statusText}</Text>
70
85
  {elapsed ? <Text style={styles.elapsed}>{elapsed}</Text> : null}
71
86
  {hasDetails ? (
72
87
  <Icon
73
88
  name={expanded ? 'chevron-up' : 'chevron-down'}
74
- size={r(14)}
89
+ size={14}
75
90
  color={colors.foregroundSubtle}
76
91
  />
77
92
  ) : null}
@@ -80,11 +95,12 @@ export function ProcessCompactStep({
80
95
  <Text style={styles.compactDescription}>{step.description}</Text>
81
96
  ) : null}
82
97
  {hasDetails && expanded ? (
83
- <Reveal>
98
+ <Reveal style={styles.compactDetailsContainer}>
84
99
  <ScrollView
85
100
  testID={`process-step-${step.id}-details`}
86
101
  style={styles.compactDetails}
87
102
  nestedScrollEnabled
103
+ showsVerticalScrollIndicator={false}
88
104
  keyboardShouldPersistTaps="handled"
89
105
  >
90
106
  {step.details}