Remove React Virtualized from <Timeline>
This commit is contained in:
parent
1eafe79905
commit
0c31ad25ef
40 changed files with 798 additions and 2512 deletions
|
@ -1,408 +0,0 @@
|
||||||
diff --git a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
|
||||||
index d9716a0..e7a9f9f 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
|
||||||
@@ -166,13 +166,19 @@ var CellMeasurer = function (_React$PureComponent) {
|
|
||||||
height = _getCellMeasurements2.height,
|
|
||||||
width = _getCellMeasurements2.width;
|
|
||||||
|
|
||||||
+
|
|
||||||
cache.set(rowIndex, columnIndex, width, height);
|
|
||||||
|
|
||||||
// If size has changed, let Grid know to re-render.
|
|
||||||
if (parent && typeof parent.invalidateCellSizeAfterRender === 'function') {
|
|
||||||
+ const heightChange = height - cache.defaultHeight;
|
|
||||||
+ const widthChange = width - cache.defaultWidth;
|
|
||||||
+
|
|
||||||
parent.invalidateCellSizeAfterRender({
|
|
||||||
columnIndex: columnIndex,
|
|
||||||
- rowIndex: rowIndex
|
|
||||||
+ rowIndex: rowIndex,
|
|
||||||
+ heightChange: heightChange,
|
|
||||||
+ widthChange: widthChange,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
|
||||||
index e1b959a..09c16c5 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
|
||||||
@@ -132,6 +132,9 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
_this._renderedRowStopIndex = 0;
|
|
||||||
_this._styleCache = {};
|
|
||||||
_this._cellCache = {};
|
|
||||||
+ _this._cellUpdates = [];
|
|
||||||
+ this._hasScrolledToColumnTarget = false;
|
|
||||||
+ this._hasScrolledToRowTarget = false;
|
|
||||||
|
|
||||||
_this._debounceScrollEndedCallback = function () {
|
|
||||||
_this._disablePointerEventsTimeoutId = null;
|
|
||||||
@@ -345,7 +348,11 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
scrollLeft: scrollLeft,
|
|
||||||
scrollTop: scrollTop,
|
|
||||||
totalColumnsWidth: totalColumnsWidth,
|
|
||||||
- totalRowsHeight: totalRowsHeight
|
|
||||||
+ totalRowsHeight: totalRowsHeight,
|
|
||||||
+ scrollToColumn: this.props.scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow: this.props.scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -363,6 +370,13 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
var columnIndex = _ref3.columnIndex,
|
|
||||||
rowIndex = _ref3.rowIndex;
|
|
||||||
|
|
||||||
+ if (columnIndex < this._lastColumnStartIndex) {
|
|
||||||
+ this._cellUpdates.push({ columnIndex, widthChange: _ref3.widthChange });
|
|
||||||
+ }
|
|
||||||
+ if (rowIndex < this._lastRowStartIndex) {
|
|
||||||
+ this._cellUpdates.push({ rowIndex, heightChange: _ref3.heightChange });
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === 'number' ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) : columnIndex;
|
|
||||||
this._deferredInvalidateRowIndex = typeof this._deferredInvalidateRowIndex === 'number' ? Math.min(this._deferredInvalidateRowIndex, rowIndex) : rowIndex;
|
|
||||||
}
|
|
||||||
@@ -381,8 +395,12 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
rowCount = _props2.rowCount;
|
|
||||||
var instanceProps = this.state.instanceProps;
|
|
||||||
|
|
||||||
- instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1);
|
|
||||||
- instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
|
|
||||||
+ if (columnCount > 0) {
|
|
||||||
+ instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1);
|
|
||||||
+ }
|
|
||||||
+ if (rowCount > 0) {
|
|
||||||
+ instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
@@ -415,6 +433,16 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
this._recomputeScrollLeftFlag = scrollToColumn >= 0 && (this.state.scrollDirectionHorizontal === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? columnIndex <= scrollToColumn : columnIndex >= scrollToColumn);
|
|
||||||
this._recomputeScrollTopFlag = scrollToRow >= 0 && (this.state.scrollDirectionVertical === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? rowIndex <= scrollToRow : rowIndex >= scrollToRow);
|
|
||||||
|
|
||||||
+ // Important to ensure that when we, say, change the width of the viewport,
|
|
||||||
+ // we don't re-render, capture deltas, and move the scroll location around.
|
|
||||||
+ if (rowIndex === 0 && columnIndex === 0) {
|
|
||||||
+ this._disableCellUpdates = true;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ // Global notification that we should retry our scroll to props-requested indices
|
|
||||||
+ this._hasScrolledToColumnTarget = false;
|
|
||||||
+ this._hasScrolledToRowTarget = false;
|
|
||||||
+
|
|
||||||
// Clear cell cache in case we are scrolling;
|
|
||||||
// Invalid row heights likely mean invalid cached content as well.
|
|
||||||
this._styleCache = {};
|
|
||||||
@@ -526,7 +554,11 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
scrollLeft: scrollLeft || 0,
|
|
||||||
scrollTop: scrollTop || 0,
|
|
||||||
totalColumnsWidth: instanceProps.columnSizeAndPositionManager.getTotalSize(),
|
|
||||||
- totalRowsHeight: instanceProps.rowSizeAndPositionManager.getTotalSize()
|
|
||||||
+ totalRowsHeight: instanceProps.rowSizeAndPositionManager.getTotalSize(),
|
|
||||||
+ scrollToColumn: scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow: scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._maybeCallOnScrollbarPresenceChange();
|
|
||||||
@@ -584,6 +616,65 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ var totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize();
|
|
||||||
+ var totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize();
|
|
||||||
+
|
|
||||||
+ // We reset our hasScrolled flags if our target has changed, or if target is not longer set
|
|
||||||
+ if (scrollToColumn !== prevProps.scrollToColumn || scrollToColumn == null || scrollToColumn < 0) {
|
|
||||||
+ this._hasScrolledToColumnTarget = false;
|
|
||||||
+ }
|
|
||||||
+ if (scrollToRow !== prevProps.scrollToRow || scrollToRow == null || scrollToRow < 0) {
|
|
||||||
+ this._hasScrolledToRowTarget = false;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ // We deactivate our forced scrolling if the user scrolls
|
|
||||||
+ if (scrollLeft !== prevState.scrollLeft && scrollToColumn >= 0 && scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.OBSERVED) {
|
|
||||||
+ this._hasScrolledToColumnTarget = true;
|
|
||||||
+ }
|
|
||||||
+ if (scrollTop !== prevState.scrollTop && scrollToRow >= 0 && scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.OBSERVED) {
|
|
||||||
+ this._hasScrolledToRowTarget = true;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ if (scrollToColumn >= 0 && !this._hasScrolledToColumnTarget && scrollLeft + width <= totalColumnsWidth) {
|
|
||||||
+ const scrollRight = scrollLeft + width;
|
|
||||||
+ const targetColumn = instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(scrollToColumn);
|
|
||||||
+
|
|
||||||
+ let isVisible = false;
|
|
||||||
+ if (targetColumn.size <= width) {
|
|
||||||
+ const targetColumnRight = targetColumn.offset + targetColumn.size;
|
|
||||||
+ isVisible = (targetColumn.offset >= scrollLeft && targetColumnRight <= scrollRight);
|
|
||||||
+ } else {
|
|
||||||
+ isVisible = (targetColumn.offset >= scrollLeft && targetColumn.offset <= scrollRight);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ if (isVisible) {
|
|
||||||
+ const maxScroll = totalColumnsWidth - width;
|
|
||||||
+ this._hasScrolledToColumnTarget = (scrollLeft >= maxScroll || targetColumn.offset === scrollLeft);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ if (scrollToRow >= 0 && !this._hasScrolledToRowTarget && scrollTop + height <= totalRowsHeight) {
|
|
||||||
+ const scrollBottom = scrollTop + height;
|
|
||||||
+ const targetRow = instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(scrollToRow);
|
|
||||||
+ const maxScroll = totalRowsHeight - height;
|
|
||||||
+
|
|
||||||
+ // When scrolling to bottom row, we want to go all the way to the bottom
|
|
||||||
+ if (scrollToRow === rowCount - 1) {
|
|
||||||
+ this._hasScrolledToRowTarget = scrollTop >= maxScroll;
|
|
||||||
+ } else {
|
|
||||||
+ let isVisible = false;
|
|
||||||
+ if (targetRow.size <= height) {
|
|
||||||
+ const targetRowBottom = targetRow.offset + targetRow.size;
|
|
||||||
+ isVisible = (targetRow.offset >= scrollTop && targetRowBottom <= scrollBottom);
|
|
||||||
+ } else {
|
|
||||||
+ isVisible = (targetRow.offset >= scrollTop && targetRow.offset <= scrollBottom);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ if (isVisible) {
|
|
||||||
+ this._hasScrolledToRowTarget = (scrollTop >= maxScroll || targetRow.offset === scrollTop);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
// Special case where the previous size was 0:
|
|
||||||
// In this case we don't show any windowed cells at all.
|
|
||||||
// So we should always recalculate offset afterwards.
|
|
||||||
@@ -594,6 +685,8 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
if (this._recomputeScrollLeftFlag) {
|
|
||||||
this._recomputeScrollLeftFlag = false;
|
|
||||||
this._updateScrollLeftForScrollToColumn(this.props);
|
|
||||||
+ } else if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) {
|
|
||||||
+ this._updateScrollLeftForScrollToColumn(this.props);
|
|
||||||
} else {
|
|
||||||
(0, _updateScrollIndexHelper2.default)({
|
|
||||||
cellSizeAndPositionManager: instanceProps.columnSizeAndPositionManager,
|
|
||||||
@@ -616,6 +709,8 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
if (this._recomputeScrollTopFlag) {
|
|
||||||
this._recomputeScrollTopFlag = false;
|
|
||||||
this._updateScrollTopForScrollToRow(this.props);
|
|
||||||
+ } else if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) {
|
|
||||||
+ this._updateScrollTopForScrollToRow(this.props);
|
|
||||||
} else {
|
|
||||||
(0, _updateScrollIndexHelper2.default)({
|
|
||||||
cellSizeAndPositionManager: instanceProps.rowSizeAndPositionManager,
|
|
||||||
@@ -635,19 +730,50 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
+
|
|
||||||
+ if (this._disableCellUpdates) {
|
|
||||||
+ this._cellUpdates = [];
|
|
||||||
+ }
|
|
||||||
+ this._disableCellUpdates = false;
|
|
||||||
+ if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) {
|
|
||||||
+ this._cellUpdates = [];
|
|
||||||
+ }
|
|
||||||
+ if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) {
|
|
||||||
+ this._cellUpdates = [];
|
|
||||||
+ }
|
|
||||||
+ if (this._cellUpdates.length && scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.OBSERVED) {
|
|
||||||
+ let item;
|
|
||||||
+ let verticalDelta = 0;
|
|
||||||
+ let horizontalDelta = 0;
|
|
||||||
+
|
|
||||||
+ while (item = this._cellUpdates.shift()) {
|
|
||||||
+ verticalDelta += item.heightChange || 0;
|
|
||||||
+ horizontalDelta += item.widthChange || 0;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ if (verticalDelta !== 0 || horizontalDelta !== 0) {
|
|
||||||
+ this.setState(Grid._getScrollToPositionStateUpdate({
|
|
||||||
+ prevState: this.state,
|
|
||||||
+ scrollTop: scrollTop + verticalDelta,
|
|
||||||
+ scrollLeft: scrollLeft + horizontalDelta,
|
|
||||||
+ }));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
// Update onRowsRendered callback if start/stop indices have changed
|
|
||||||
this._invokeOnGridRenderedHelper();
|
|
||||||
|
|
||||||
// Changes to :scrollLeft or :scrollTop should also notify :onScroll listeners
|
|
||||||
if (scrollLeft !== prevState.scrollLeft || scrollTop !== prevState.scrollTop) {
|
|
||||||
- var totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize();
|
|
||||||
- var totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize();
|
|
||||||
-
|
|
||||||
this._invokeOnScrollMemoizer({
|
|
||||||
scrollLeft: scrollLeft,
|
|
||||||
scrollTop: scrollTop,
|
|
||||||
totalColumnsWidth: totalColumnsWidth,
|
|
||||||
- totalRowsHeight: totalRowsHeight
|
|
||||||
+ totalRowsHeight: totalRowsHeight,
|
|
||||||
+ scrollToColumn: scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow: scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -750,6 +876,7 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
}, containerProps, {
|
|
||||||
'aria-label': this.props['aria-label'],
|
|
||||||
'aria-readonly': this.props['aria-readonly'],
|
|
||||||
+ 'aria-rowcount': this.props['rowCount'],
|
|
||||||
className: (0, _classnames2.default)('ReactVirtualized__Grid', className),
|
|
||||||
id: id,
|
|
||||||
onScroll: this._onScroll,
|
|
||||||
@@ -909,6 +1036,11 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
visibleRowIndices: visibleRowIndices
|
|
||||||
});
|
|
||||||
|
|
||||||
+ this._lastColumnStartIndex = this._columnStartIndex;
|
|
||||||
+ this._lastColumnStopIndex = this._columnStopIndex;
|
|
||||||
+ this._lastRowStartIndex = this._rowStartIndex;
|
|
||||||
+ this._lastRowStopIndex = this._rowStopIndex;
|
|
||||||
+
|
|
||||||
// update the indices
|
|
||||||
this._columnStartIndex = columnStartIndex;
|
|
||||||
this._columnStopIndex = columnStopIndex;
|
|
||||||
@@ -962,7 +1094,11 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
var scrollLeft = _ref6.scrollLeft,
|
|
||||||
scrollTop = _ref6.scrollTop,
|
|
||||||
totalColumnsWidth = _ref6.totalColumnsWidth,
|
|
||||||
- totalRowsHeight = _ref6.totalRowsHeight;
|
|
||||||
+ totalRowsHeight = _ref6.totalRowsHeight,
|
|
||||||
+ scrollToColumn = _ref6.scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget = _ref6._hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow = _ref6.scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget = _ref6._hasScrolledToRowTarget;
|
|
||||||
|
|
||||||
this._onScrollMemoizer({
|
|
||||||
callback: function callback(_ref7) {
|
|
||||||
@@ -973,19 +1109,26 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
onScroll = _props7.onScroll,
|
|
||||||
width = _props7.width;
|
|
||||||
|
|
||||||
-
|
|
||||||
onScroll({
|
|
||||||
clientHeight: height,
|
|
||||||
clientWidth: width,
|
|
||||||
scrollHeight: totalRowsHeight,
|
|
||||||
scrollLeft: scrollLeft,
|
|
||||||
scrollTop: scrollTop,
|
|
||||||
- scrollWidth: totalColumnsWidth
|
|
||||||
+ scrollWidth: totalColumnsWidth,
|
|
||||||
+ scrollToColumn: scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget: _hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow: scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget: _hasScrolledToRowTarget,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
indices: {
|
|
||||||
scrollLeft: scrollLeft,
|
|
||||||
- scrollTop: scrollTop
|
|
||||||
+ scrollTop: scrollTop,
|
|
||||||
+ scrollToColumn: scrollToColumn,
|
|
||||||
+ _hasScrolledToColumnTarget: _hasScrolledToColumnTarget,
|
|
||||||
+ scrollToRow: scrollToRow,
|
|
||||||
+ _hasScrolledToRowTarget: _hasScrolledToRowTarget,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1325,6 +1468,15 @@ var Grid = function (_React$PureComponent) {
|
|
||||||
var totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize();
|
|
||||||
var scrollBarSize = instanceProps.scrollbarSizeMeasured && totalColumnsWidth > width ? instanceProps.scrollbarSize : 0;
|
|
||||||
|
|
||||||
+ // If we're scrolling to the last row, then we scroll as far as we can,
|
|
||||||
+ // even if we can't see the entire row. We need to be at the bottom.
|
|
||||||
+ if (targetIndex === finalRow) {
|
|
||||||
+ const totalHeight = instanceProps.rowSizeAndPositionManager.getTotalSize();
|
|
||||||
+ const maxScroll = totalHeight - height;
|
|
||||||
+
|
|
||||||
+ return maxScroll;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
return instanceProps.rowSizeAndPositionManager.getUpdatedOffsetForIndex({
|
|
||||||
align: scrollToAlignment,
|
|
||||||
containerSize: height - scrollBarSize,
|
|
||||||
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js b/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
|
||||||
index 70b0abe..8e12ffc 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
|
||||||
@@ -32,15 +32,8 @@ function defaultOverscanIndicesGetter(_ref) {
|
|
||||||
// For more info see issues #625
|
|
||||||
overscanCellsCount = Math.max(1, overscanCellsCount);
|
|
||||||
|
|
||||||
- if (scrollDirection === SCROLL_DIRECTION_FORWARD) {
|
|
||||||
- return {
|
|
||||||
- overscanStartIndex: Math.max(0, startIndex - 1),
|
|
||||||
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
|
|
||||||
- };
|
|
||||||
- } else {
|
|
||||||
- return {
|
|
||||||
- overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
|
||||||
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + 1)
|
|
||||||
- };
|
|
||||||
- }
|
|
||||||
+ return {
|
|
||||||
+ overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
|
||||||
+ overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
|
|
||||||
+ };
|
|
||||||
}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js b/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
|
||||||
index d5f6d04..c4b3d84 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
|
||||||
@@ -27,15 +27,8 @@ function defaultOverscanIndicesGetter(_ref) {
|
|
||||||
startIndex = _ref.startIndex,
|
|
||||||
stopIndex = _ref.stopIndex;
|
|
||||||
|
|
||||||
- if (scrollDirection === SCROLL_DIRECTION_FORWARD) {
|
|
||||||
- return {
|
|
||||||
- overscanStartIndex: Math.max(0, startIndex),
|
|
||||||
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
|
|
||||||
- };
|
|
||||||
- } else {
|
|
||||||
- return {
|
|
||||||
- overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
|
||||||
- overscanStopIndex: Math.min(cellCount - 1, stopIndex)
|
|
||||||
- };
|
|
||||||
- }
|
|
||||||
+ return {
|
|
||||||
+ overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
|
||||||
+ overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
|
|
||||||
+ };
|
|
||||||
}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/node_modules/react-virtualized/dist/commonjs/List/List.js b/node_modules/react-virtualized/dist/commonjs/List/List.js
|
|
||||||
index b5ad0eb..efb2cd7 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/commonjs/List/List.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/commonjs/List/List.js
|
|
||||||
@@ -112,13 +112,8 @@ var List = function (_React$PureComponent) {
|
|
||||||
}, _this._setRef = function (ref) {
|
|
||||||
_this.Grid = ref;
|
|
||||||
}, _this._onScroll = function (_ref3) {
|
|
||||||
- var clientHeight = _ref3.clientHeight,
|
|
||||||
- scrollHeight = _ref3.scrollHeight,
|
|
||||||
- scrollTop = _ref3.scrollTop;
|
|
||||||
var onScroll = _this.props.onScroll;
|
|
||||||
-
|
|
||||||
-
|
|
||||||
- onScroll({ clientHeight: clientHeight, scrollHeight: scrollHeight, scrollTop: scrollTop });
|
|
||||||
+ onScroll(_ref3);
|
|
||||||
}, _this._onSectionRendered = function (_ref4) {
|
|
||||||
var rowOverscanStartIndex = _ref4.rowOverscanStartIndex,
|
|
||||||
rowOverscanStopIndex = _ref4.rowOverscanStopIndex,
|
|
||||||
diff --git a/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js b/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
|
|
||||||
index 6418a78..afbc3c3 100644
|
|
||||||
--- a/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
|
|
||||||
+++ b/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
|
|
||||||
@@ -72,4 +72,3 @@ export function unregisterScrollListener(component, element) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-import { bpfrpt_proptype_WindowScroller } from '../WindowScroller.js';
|
|
||||||
\ No newline at end of file
|
|
|
@ -73,12 +73,12 @@
|
||||||
|
|
||||||
.module-message {
|
.module-message {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
outline: none;
|
outline: none;
|
||||||
margin-left: 16px;
|
padding-left: 16px;
|
||||||
margin-right: 16px;
|
padding-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message--expired {
|
.module-message--expired {
|
||||||
|
@ -104,8 +104,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message--outgoing {
|
.module-message--outgoing {
|
||||||
float: right;
|
flex-direction: row-reverse;
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__buttons {
|
.module-message__buttons {
|
||||||
|
@ -316,7 +315,6 @@
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 306px;
|
max-width: 306px;
|
||||||
|
|
||||||
|
@ -336,8 +334,9 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// These should match the margins in .module-message__attachment-container.
|
// These should match the margins in .module-message__attachment-container.
|
||||||
|
@ -5495,21 +5494,26 @@ button.module-image__border-overlay:focus {
|
||||||
// Module: Timeline
|
// Module: Timeline
|
||||||
|
|
||||||
.module-timeline {
|
.module-timeline {
|
||||||
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.ReactVirtualized__List {
|
|
||||||
@include scrollbar;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-timeline--disabled {
|
.module-timeline--disabled {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-timeline__message-container {
|
.module-timeline__messages__container {
|
||||||
padding-top: 4px;
|
flex: 1 1;
|
||||||
padding-bottom: 4px;
|
overflow-x: hidden;
|
||||||
|
overflow-y: overlay;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-timeline__messages {
|
||||||
|
flex: 1 1;
|
||||||
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactVirtualized__List {
|
.ReactVirtualized__List {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.module-TimelineWarnings {
|
.module-TimelineWarnings {
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: $z-index-base;
|
z-index: $z-index-above-above-base;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FunctionComponent, ReactNode } from 'react';
|
import type { FunctionComponent, ReactNode } from 'react';
|
||||||
import React, { useRef, useEffect, Children } from 'react';
|
import React, { useRef, useEffect, Children } from 'react';
|
||||||
|
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { scrollToBottom } from '../util/scrollToBottom';
|
import { scrollToBottom } from '../util/scrollUtil';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|
|
@ -21,7 +21,7 @@ const SampleMessage = ({
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestampDeltaFromNow,
|
||||||
status,
|
status,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
|
@ -29,7 +29,7 @@ const SampleMessage = ({
|
||||||
direction: 'incoming' | 'outgoing';
|
direction: 'incoming' | 'outgoing';
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
text: string;
|
text: string;
|
||||||
timestamp: number;
|
timestampDeltaFromNow: number;
|
||||||
status: 'delivered' | 'read' | 'sent';
|
status: 'delivered' | 'read' | 'sent';
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
|
@ -51,7 +51,7 @@ const SampleMessage = ({
|
||||||
<span
|
<span
|
||||||
className={`module-message__metadata__date module-message__metadata__date--${direction}`}
|
className={`module-message__metadata__date module-message__metadata__date--${direction}`}
|
||||||
>
|
>
|
||||||
{formatTime(i18n, timestamp)}
|
{formatTime(i18n, Date.now() - timestampDeltaFromNow, Date.now())}
|
||||||
</span>
|
</span>
|
||||||
{direction === 'outgoing' && (
|
{direction === 'outgoing' && (
|
||||||
<div
|
<div
|
||||||
|
@ -78,7 +78,7 @@ export const SampleMessageBubbles = ({
|
||||||
direction={includeAnotherBubble ? 'outgoing' : 'incoming'}
|
direction={includeAnotherBubble ? 'outgoing' : 'incoming'}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
text={i18n('ChatColorPicker__sampleBubble1')}
|
text={i18n('ChatColorPicker__sampleBubble1')}
|
||||||
timestamp={Date.now() - A_FEW_DAYS_AGO}
|
timestampDeltaFromNow={A_FEW_DAYS_AGO}
|
||||||
status="read"
|
status="read"
|
||||||
style={firstBubbleStyle}
|
style={firstBubbleStyle}
|
||||||
/>
|
/>
|
||||||
|
@ -91,7 +91,7 @@ export const SampleMessageBubbles = ({
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
text={i18n('ChatColorPicker__sampleBubble2')}
|
text={i18n('ChatColorPicker__sampleBubble2')}
|
||||||
timestamp={Date.now() - A_FEW_DAYS_AGO / 2}
|
timestampDeltaFromNow={A_FEW_DAYS_AGO / 2}
|
||||||
status="read"
|
status="read"
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
@ -103,7 +103,7 @@ export const SampleMessageBubbles = ({
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
text={i18n('ChatColorPicker__sampleBubble3')}
|
text={i18n('ChatColorPicker__sampleBubble3')}
|
||||||
timestamp={Date.now()}
|
timestampDeltaFromNow={0}
|
||||||
status="delivered"
|
status="delivered"
|
||||||
style={backgroundStyle}
|
style={backgroundStyle}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,8 +19,8 @@ const getCommonProps = () => ({
|
||||||
conversationId: 'fake-conversation-id',
|
conversationId: 'fake-conversation-id',
|
||||||
i18n,
|
i18n,
|
||||||
messageId: 'fake-message-id',
|
messageId: 'fake-message-id',
|
||||||
messageSizeChanged: action('messageSizeChanged'),
|
|
||||||
nextItem: undefined,
|
nextItem: undefined,
|
||||||
|
now: Date.now(),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import { SystemMessage } from './SystemMessage';
|
import { SystemMessage } from './SystemMessage';
|
||||||
|
@ -16,14 +15,12 @@ import {
|
||||||
getCallingIcon,
|
getCallingIcon,
|
||||||
getCallingNotificationText,
|
getCallingNotificationText,
|
||||||
} from '../../util/callingNotification';
|
} from '../../util/callingNotification';
|
||||||
import { usePrevious } from '../../hooks/usePrevious';
|
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||||
import type { TimelineItemType } from './TimelineItem';
|
import type { TimelineItemType } from './TimelineItem';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
messageSizeChanged: (messageId: string, conversationId: string) => void;
|
|
||||||
returnToActiveCall: () => void;
|
returnToActiveCall: () => void;
|
||||||
startCallingLobby: (_: {
|
startCallingLobby: (_: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -34,27 +31,14 @@ export type PropsActionsType = {
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messageId: string;
|
|
||||||
nextItem: undefined | TimelineItemType;
|
nextItem: undefined | TimelineItemType;
|
||||||
|
now: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
||||||
|
|
||||||
export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
||||||
const { conversationId, i18n, messageId, messageSizeChanged } = props;
|
const { i18n, now } = props;
|
||||||
|
|
||||||
const [height, setHeight] = useState<null | number>(null);
|
|
||||||
const previousHeight = usePrevious<null | number>(null, height);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (height === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousHeight !== null && height !== previousHeight) {
|
|
||||||
messageSizeChanged(messageId, conversationId);
|
|
||||||
}
|
|
||||||
}, [height, previousHeight, conversationId, messageId, messageSizeChanged]);
|
|
||||||
|
|
||||||
let timestamp: number;
|
let timestamp: number;
|
||||||
let wasMissed = false;
|
let wasMissed = false;
|
||||||
|
@ -75,38 +59,25 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
||||||
const icon = getCallingIcon(props);
|
const icon = getCallingIcon(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Measure
|
<SystemMessage
|
||||||
bounds
|
button={renderCallingNotificationButton(props)}
|
||||||
onResize={({ bounds }) => {
|
contents={
|
||||||
if (!bounds) {
|
<>
|
||||||
log.error('We should be measuring the bounds');
|
{getCallingNotificationText(props, i18n)} ·{' '}
|
||||||
return;
|
<MessageTimestamp
|
||||||
}
|
direction="outgoing"
|
||||||
setHeight(bounds.height);
|
i18n={i18n}
|
||||||
}}
|
now={now}
|
||||||
>
|
timestamp={timestamp}
|
||||||
{({ measureRef }) => (
|
withImageNoCaption={false}
|
||||||
<SystemMessage
|
withSticker={false}
|
||||||
button={renderCallingNotificationButton(props)}
|
withTapToViewExpired={false}
|
||||||
contents={
|
/>
|
||||||
<>
|
</>
|
||||||
{getCallingNotificationText(props, i18n)} ·{' '}
|
}
|
||||||
<MessageTimestamp
|
icon={icon}
|
||||||
direction="outgoing"
|
isError={wasMissed}
|
||||||
i18n={i18n}
|
/>
|
||||||
timestamp={timestamp}
|
|
||||||
withImageNoCaption={false}
|
|
||||||
withSticker={false}
|
|
||||||
withTapToViewExpired={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
icon={icon}
|
|
||||||
isError={wasMissed}
|
|
||||||
ref={measureRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Measure>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -19,6 +19,7 @@ const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
story.add('Default', () => (
|
story.add('Default', () => (
|
||||||
<ChangeNumberNotification
|
<ChangeNumberNotification
|
||||||
|
now={Date.now()}
|
||||||
sender={getDefaultConversation()}
|
sender={getDefaultConversation()}
|
||||||
timestamp={1618894800000}
|
timestamp={1618894800000}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -27,6 +28,7 @@ story.add('Default', () => (
|
||||||
|
|
||||||
story.add('Long name', () => (
|
story.add('Long name', () => (
|
||||||
<ChangeNumberNotification
|
<ChangeNumberNotification
|
||||||
|
now={Date.now()}
|
||||||
sender={getDefaultConversation({
|
sender={getDefaultConversation({
|
||||||
firstName: '💅😇🖋'.repeat(50),
|
firstName: '💅😇🖋'.repeat(50),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -18,12 +18,13 @@ export type PropsData = {
|
||||||
|
|
||||||
export type PropsHousekeeping = {
|
export type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
now: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export const ChangeNumberNotification: React.FC<Props> = props => {
|
export const ChangeNumberNotification: React.FC<Props> = props => {
|
||||||
const { i18n, sender, timestamp } = props;
|
const { i18n, now, sender, timestamp } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SystemMessage
|
<SystemMessage
|
||||||
|
@ -37,7 +38,7 @@ export const ChangeNumberNotification: React.FC<Props> = props => {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
·
|
·
|
||||||
<MessageTimestamp i18n={i18n} timestamp={timestamp} />
|
<MessageTimestamp i18n={i18n} now={now} timestamp={timestamp} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
icon="phone"
|
icon="phone"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -55,7 +55,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
'Fifth',
|
'Fifth',
|
||||||
]}
|
]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -83,7 +82,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
'Fourth',
|
'Fourth',
|
||||||
]}
|
]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -106,7 +104,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -129,7 +126,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -152,7 +148,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={['NYC Rock Climbers']}
|
sharedGroupNames={['NYC Rock Climbers']}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -175,7 +170,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -198,7 +192,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -221,7 +214,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -243,7 +235,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -265,7 +256,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -285,7 +275,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -305,7 +294,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -326,7 +314,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -347,7 +334,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -367,7 +353,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -386,7 +371,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
onHeightChange={action('onHeightChange')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { Props as AvatarProps } from '../Avatar';
|
import type { Props as AvatarProps } from '../Avatar';
|
||||||
import { Avatar, AvatarBlur } from '../Avatar';
|
import { Avatar, AvatarBlur } from '../Avatar';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
|
@ -12,7 +12,6 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||||
import * as log from '../../logging/log';
|
|
||||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
@ -22,7 +21,6 @@ export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
membersCount?: number;
|
membersCount?: number;
|
||||||
onHeightChange: () => unknown;
|
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
sharedGroupNames?: Array<string>;
|
sharedGroupNames?: Array<string>;
|
||||||
unblurAvatar: () => void;
|
unblurAvatar: () => void;
|
||||||
|
@ -111,13 +109,10 @@ export const ConversationHero = ({
|
||||||
profileName,
|
profileName,
|
||||||
theme,
|
theme,
|
||||||
title,
|
title,
|
||||||
onHeightChange,
|
|
||||||
unblurAvatar,
|
unblurAvatar,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
updateSharedGroups,
|
updateSharedGroups,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const firstRenderRef = useRef(true);
|
|
||||||
|
|
||||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const closeMessageRequestWarning = () => {
|
const closeMessageRequestWarning = () => {
|
||||||
|
@ -129,30 +124,6 @@ export const ConversationHero = ({
|
||||||
updateSharedGroups();
|
updateSharedGroups();
|
||||||
}, [updateSharedGroups]);
|
}, [updateSharedGroups]);
|
||||||
|
|
||||||
const sharedGroupNamesStringified = JSON.stringify(sharedGroupNames);
|
|
||||||
useEffect(() => {
|
|
||||||
const isFirstRender = firstRenderRef.current;
|
|
||||||
if (isFirstRender) {
|
|
||||||
firstRenderRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('ConversationHero: calling onHeightChange');
|
|
||||||
onHeightChange();
|
|
||||||
}, [
|
|
||||||
about,
|
|
||||||
conversationType,
|
|
||||||
groupDescription,
|
|
||||||
isMe,
|
|
||||||
membersCount,
|
|
||||||
name,
|
|
||||||
onHeightChange,
|
|
||||||
phoneNumber,
|
|
||||||
profileName,
|
|
||||||
title,
|
|
||||||
sharedGroupNamesStringified,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let avatarBlur: AvatarBlur;
|
let avatarBlur: AvatarBlur;
|
||||||
let avatarOnClick: undefined | (() => void);
|
let avatarOnClick: undefined | (() => void);
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -83,6 +83,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => {
|
||||||
audio={audio}
|
audio={audio}
|
||||||
computePeaks={computePeaks}
|
computePeaks={computePeaks}
|
||||||
setActiveAudioID={(id, context) => setActive({ id, context })}
|
setActiveAudioID={(id, context) => setActive({ id, context })}
|
||||||
|
now={Date.now()}
|
||||||
onFirstPlayed={action('onFirstPlayed')}
|
onFirstPlayed={action('onFirstPlayed')}
|
||||||
activeAudioID={active.id}
|
activeAudioID={active.id}
|
||||||
activeAudioContext={active.context}
|
activeAudioContext={active.context}
|
||||||
|
@ -131,6 +132,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
||||||
i18n,
|
i18n,
|
||||||
id: text('id', overrideProps.id || ''),
|
id: text('id', overrideProps.id || ''),
|
||||||
|
now: Date.now(),
|
||||||
renderingContext: 'storybook',
|
renderingContext: 'storybook',
|
||||||
interactionMode: overrideProps.interactionMode || 'keyboard',
|
interactionMode: overrideProps.interactionMode || 'keyboard',
|
||||||
isSticker: isBoolean(overrideProps.isSticker)
|
isSticker: isBoolean(overrideProps.isSticker)
|
||||||
|
@ -149,7 +151,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
markViewed: action('markViewed'),
|
markViewed: action('markViewed'),
|
||||||
messageExpanded: action('messageExpanded'),
|
messageExpanded: action('messageExpanded'),
|
||||||
onHeightChange: action('onHeightChange'),
|
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2018-2022 Signal Messenger, LLC
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { RefObject } from 'react';
|
import type { ReactNode, RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM, { createPortal } from 'react-dom';
|
import ReactDOM, { createPortal } from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -118,6 +118,7 @@ export type AudioAttachmentProps = {
|
||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
expirationTimestamp?: number;
|
expirationTimestamp?: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
now: number;
|
||||||
played: boolean;
|
played: boolean;
|
||||||
showMessageDetail: (id: string) => void;
|
showMessageDetail: (id: string) => void;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
|
@ -210,6 +211,7 @@ export type PropsHousekeeping = {
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
now: number;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
disableMenu?: boolean;
|
disableMenu?: boolean;
|
||||||
|
@ -224,7 +226,6 @@ export type PropsHousekeeping = {
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
clearSelectedMessage: () => unknown;
|
clearSelectedMessage: () => unknown;
|
||||||
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
|
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
|
||||||
onHeightChange: () => unknown;
|
|
||||||
messageExpanded: (id: string, displayLimit: number) => unknown;
|
messageExpanded: (id: string, displayLimit: number) => unknown;
|
||||||
checkForAccount: (identifier: string) => unknown;
|
checkForAccount: (identifier: string) => unknown;
|
||||||
|
|
||||||
|
@ -471,7 +472,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkExpired();
|
this.checkExpired();
|
||||||
this.checkForHeightChange(prevProps);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.status === 'sending' &&
|
prevProps.status === 'sending' &&
|
||||||
|
@ -491,24 +491,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkForHeightChange(prevProps: Props): void {
|
|
||||||
const { contact, onHeightChange } = this.props;
|
|
||||||
const willRenderSendMessageButton = Boolean(
|
|
||||||
contact && contact.firstNumber && contact.isNumberOnSignal
|
|
||||||
);
|
|
||||||
|
|
||||||
const { contact: previousContact } = prevProps;
|
|
||||||
const previouslyRenderedSendMessageButton = Boolean(
|
|
||||||
previousContact &&
|
|
||||||
previousContact.firstNumber &&
|
|
||||||
previousContact.isNumberOnSignal
|
|
||||||
);
|
|
||||||
|
|
||||||
if (willRenderSendMessageButton !== previouslyRenderedSendMessageButton) {
|
|
||||||
onHeightChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public startSelectedTimer(): void {
|
public startSelectedTimer(): void {
|
||||||
const { clearSelectedMessage, interactionMode } = this.props;
|
const { clearSelectedMessage, interactionMode } = this.props;
|
||||||
const { isSelected } = this.state;
|
const { isSelected } = this.state;
|
||||||
|
@ -609,6 +591,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
status,
|
status,
|
||||||
i18n,
|
i18n,
|
||||||
|
now,
|
||||||
text,
|
text,
|
||||||
textPending,
|
textPending,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -640,6 +623,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isShowingImage={this.isShowingImage()}
|
isShowingImage={this.isShowingImage()}
|
||||||
isSticker={isStickerLike}
|
isSticker={isStickerLike}
|
||||||
isTapToViewExpired={isTapToViewExpired}
|
isTapToViewExpired={isTapToViewExpired}
|
||||||
|
now={now}
|
||||||
showMessageDetail={showMessageDetail}
|
showMessageDetail={showMessageDetail}
|
||||||
status={status}
|
status={status}
|
||||||
textPending={textPending}
|
textPending={textPending}
|
||||||
|
@ -705,6 +689,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
markViewed,
|
||||||
|
now,
|
||||||
quote,
|
quote,
|
||||||
readStatus,
|
readStatus,
|
||||||
reducedMotion,
|
reducedMotion,
|
||||||
|
@ -834,6 +819,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
id,
|
id,
|
||||||
|
now,
|
||||||
played,
|
played,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
status,
|
status,
|
||||||
|
@ -1238,7 +1224,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
onHeightChange,
|
|
||||||
openConversation,
|
openConversation,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
|
@ -1276,7 +1261,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
id={id}
|
id={id}
|
||||||
messageExpanded={messageExpanded}
|
messageExpanded={messageExpanded}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
onHeightChange={onHeightChange}
|
|
||||||
text={contents || ''}
|
text={contents || ''}
|
||||||
textPending={textPending}
|
textPending={textPending}
|
||||||
/>
|
/>
|
||||||
|
@ -1284,13 +1268,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderError(isCorrectSide: boolean): JSX.Element | null {
|
private renderError(): ReactNode {
|
||||||
const { status, direction } = this.props;
|
const { status, direction } = this.props;
|
||||||
|
|
||||||
if (!isCorrectSide) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status !== 'paused' &&
|
status !== 'paused' &&
|
||||||
status !== 'error' &&
|
status !== 'error' &&
|
||||||
|
@ -1312,10 +1292,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMenu(
|
private renderMenu(triggerId: string): ReactNode {
|
||||||
isCorrectSide: boolean,
|
|
||||||
triggerId: string
|
|
||||||
): JSX.Element | null {
|
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
@ -1334,7 +1311,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
selectedReaction,
|
selectedReaction,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!isCorrectSide || disableMenu) {
|
if (disableMenu) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2462,12 +2439,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
ref={this.focusRef}
|
ref={this.focusRef}
|
||||||
>
|
>
|
||||||
{this.renderError(direction === 'incoming')}
|
{this.renderError()}
|
||||||
{this.renderMenu(direction === 'outgoing', triggerId)}
|
|
||||||
{this.renderAvatar()}
|
{this.renderAvatar()}
|
||||||
{this.renderContainer()}
|
{this.renderContainer()}
|
||||||
{this.renderError(direction === 'outgoing')}
|
{this.renderMenu(triggerId)}
|
||||||
{this.renderMenu(direction === 'incoming', triggerId)}
|
|
||||||
{this.renderContextMenu(triggerId)}
|
{this.renderContextMenu(triggerId)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
@ -27,6 +27,7 @@ export type Props = {
|
||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
expirationTimestamp?: number;
|
expirationTimestamp?: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
now: number;
|
||||||
played: boolean;
|
played: boolean;
|
||||||
showMessageDetail: (id: string) => void;
|
showMessageDetail: (id: string) => void;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
|
@ -157,6 +158,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
id,
|
id,
|
||||||
|
now,
|
||||||
played,
|
played,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
status,
|
status,
|
||||||
|
@ -539,6 +541,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
isShowingImage={false}
|
isShowingImage={false}
|
||||||
isSticker={false}
|
isSticker={false}
|
||||||
isTapToViewExpired={false}
|
isTapToViewExpired={false}
|
||||||
|
now={now}
|
||||||
showMessageDetail={showMessageDetail}
|
showMessageDetail={showMessageDetail}
|
||||||
status={status}
|
status={status}
|
||||||
textPending={textPending}
|
textPending={textPending}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
@ -23,7 +23,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
i18n,
|
i18n,
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
messageExpanded: action('messageExpanded'),
|
messageExpanded: action('messageExpanded'),
|
||||||
onHeightChange: action('onHeightChange'),
|
|
||||||
text: text('text', overrideProps.text || ''),
|
text: text('text', overrideProps.text || ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { Props as MessageBodyPropsType } from './MessageBody';
|
import type { Props as MessageBodyPropsType } from './MessageBody';
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBody } from './MessageBody';
|
||||||
import { usePrevious } from '../../hooks/usePrevious';
|
|
||||||
|
|
||||||
export type Props = Pick<
|
export type Props = Pick<
|
||||||
MessageBodyPropsType,
|
MessageBodyPropsType,
|
||||||
|
@ -20,7 +19,6 @@ export type Props = Pick<
|
||||||
id: string;
|
id: string;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
messageExpanded: (id: string, displayLimit: number) => unknown;
|
messageExpanded: (id: string, displayLimit: number) => unknown;
|
||||||
onHeightChange: () => unknown;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_LENGTH = 800;
|
const INITIAL_LENGTH = 800;
|
||||||
|
@ -70,19 +68,11 @@ export function MessageBodyReadMore({
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
onHeightChange,
|
|
||||||
openConversation,
|
openConversation,
|
||||||
text,
|
text,
|
||||||
textPending,
|
textPending,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const maxLength = displayLimit || INITIAL_LENGTH;
|
const maxLength = displayLimit || INITIAL_LENGTH;
|
||||||
const previousMaxLength = usePrevious(maxLength, maxLength);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (previousMaxLength !== maxLength) {
|
|
||||||
onHeightChange();
|
|
||||||
}
|
|
||||||
}, [maxLength, previousMaxLength, onHeightChange]);
|
|
||||||
|
|
||||||
const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength);
|
const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength);
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { SendStatus } from '../../messages/MessageSendState';
|
||||||
import { WidthBreakpoint } from '../_util';
|
import { WidthBreakpoint } from '../_util';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { formatDateTimeLong } from '../../util/timestamp';
|
import { formatDateTimeLong } from '../../util/timestamp';
|
||||||
|
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
|
||||||
|
import { MINUTE } from '../../util/durations';
|
||||||
|
|
||||||
export type Contact = Pick<
|
export type Contact = Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -98,16 +100,20 @@ export type PropsReduxActions = Pick<
|
||||||
export type ExternalProps = PropsData & PropsBackboneActions;
|
export type ExternalProps = PropsData & PropsBackboneActions;
|
||||||
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
|
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
|
||||||
|
|
||||||
|
type State = { nowThatUpdatesEveryMinute: number };
|
||||||
|
|
||||||
const contactSortCollator = new Intl.Collator();
|
const contactSortCollator = new Intl.Collator();
|
||||||
|
|
||||||
const _keyForError = (error: Error): string => {
|
const _keyForError = (error: Error): string => {
|
||||||
return `${error.name}-${error.message}`;
|
return `${error.name}-${error.message}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MessageDetail extends React.Component<Props> {
|
export class MessageDetail extends React.Component<Props, State> {
|
||||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
override state = { nowThatUpdatesEveryMinute: Date.now() };
|
||||||
|
|
||||||
|
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||||
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
|
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
private nowThatUpdatesEveryMinuteInterval?: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
public override componentDidMount(): void {
|
public override componentDidMount(): void {
|
||||||
// When this component is created, it's initially not part of the DOM, and then it's
|
// When this component is created, it's initially not part of the DOM, and then it's
|
||||||
|
@ -117,6 +123,14 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
this.focusRef.current.focus();
|
this.focusRef.current.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.nowThatUpdatesEveryMinuteInterval = setInterval(() => {
|
||||||
|
this.setState({ nowThatUpdatesEveryMinute: Date.now() });
|
||||||
|
}, MINUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override componentWillUnmount(): void {
|
||||||
|
clearTimeoutIfNecessary(this.nowThatUpdatesEveryMinuteInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAvatar(contact: Contact): JSX.Element {
|
public renderAvatar(contact: Contact): JSX.Element {
|
||||||
|
@ -298,6 +312,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
theme,
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { nowThatUpdatesEveryMinute } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
|
@ -335,7 +350,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||||
markViewed={markViewed}
|
markViewed={markViewed}
|
||||||
messageExpanded={noop}
|
messageExpanded={noop}
|
||||||
onHeightChange={noop}
|
now={nowThatUpdatesEveryMinute}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
openLink={openLink}
|
openLink={openLink}
|
||||||
reactToMessage={reactToMessage}
|
reactToMessage={reactToMessage}
|
||||||
|
|
|
@ -22,6 +22,7 @@ type PropsType = {
|
||||||
isShowingImage: boolean;
|
isShowingImage: boolean;
|
||||||
isSticker?: boolean;
|
isSticker?: boolean;
|
||||||
isTapToViewExpired?: boolean;
|
isTapToViewExpired?: boolean;
|
||||||
|
now: number;
|
||||||
showMessageDetail: (id: string) => void;
|
showMessageDetail: (id: string) => void;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
textPending?: boolean;
|
textPending?: boolean;
|
||||||
|
@ -40,6 +41,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||||
isShowingImage,
|
isShowingImage,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
|
now,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
status,
|
status,
|
||||||
textPending,
|
textPending,
|
||||||
|
@ -97,6 +99,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||||
<MessageTimestamp
|
<MessageTimestamp
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
|
now={now}
|
||||||
direction={metadataDirection}
|
direction={metadataDirection}
|
||||||
withImageNoCaption={withImageNoCaption}
|
withImageNoCaption={withImageNoCaption}
|
||||||
withSticker={isSticker}
|
withSticker={isSticker}
|
||||||
|
|
|
@ -42,6 +42,7 @@ const times = (): Array<[string, number]> => [
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
i18n,
|
i18n,
|
||||||
timestamp: overrideProps.timestamp,
|
timestamp: overrideProps.timestamp,
|
||||||
|
now: Date.now(),
|
||||||
module: text('module', ''),
|
module: text('module', ''),
|
||||||
withImageNoCaption: boolean('withImageNoCaption', false),
|
withImageNoCaption: boolean('withImageNoCaption', false),
|
||||||
withSticker: boolean('withSticker', false),
|
withSticker: boolean('withSticker', false),
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
// Copyright 2018-2022 Signal Messenger, LLC
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
import { formatTime } from '../../util/timestamp';
|
import { formatTime } from '../../util/timestamp';
|
||||||
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
|
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { Time } from '../Time';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
now: number;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
module?: string;
|
module?: string;
|
||||||
withImageNoCaption?: boolean;
|
withImageNoCaption?: boolean;
|
||||||
|
@ -20,63 +21,36 @@ export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UPDATE_FREQUENCY = 60 * 1000;
|
export function MessageTimestamp({
|
||||||
|
direction,
|
||||||
|
i18n,
|
||||||
|
module,
|
||||||
|
now,
|
||||||
|
timestamp,
|
||||||
|
withImageNoCaption,
|
||||||
|
withSticker,
|
||||||
|
withTapToViewExpired,
|
||||||
|
}: Readonly<Props>): null | ReactElement {
|
||||||
|
const moduleName = module || 'module-timestamp';
|
||||||
|
|
||||||
export class MessageTimestamp extends React.Component<Props> {
|
if (timestamp === null || timestamp === undefined) {
|
||||||
private interval: NodeJS.Timeout | null;
|
return null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.interval = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override componentDidMount(): void {
|
return (
|
||||||
const update = () => {
|
<Time
|
||||||
this.setState({
|
className={classNames(
|
||||||
// Used to trigger renders
|
moduleName,
|
||||||
// eslint-disable-next-line react/no-unused-state
|
direction ? `${moduleName}--${direction}` : null,
|
||||||
lastUpdated: Date.now(),
|
withTapToViewExpired && direction
|
||||||
});
|
? `${moduleName}--${direction}-with-tap-to-view-expired`
|
||||||
};
|
: null,
|
||||||
this.interval = setInterval(update, UPDATE_FREQUENCY);
|
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
|
||||||
}
|
withSticker ? `${moduleName}--with-sticker` : null
|
||||||
|
)}
|
||||||
public override componentWillUnmount(): void {
|
timestamp={timestamp}
|
||||||
clearTimeoutIfNecessary(this.interval);
|
>
|
||||||
}
|
{formatTime(i18n, timestamp, now)}
|
||||||
|
</Time>
|
||||||
public override render(): JSX.Element | null {
|
);
|
||||||
const {
|
|
||||||
direction,
|
|
||||||
i18n,
|
|
||||||
module,
|
|
||||||
timestamp,
|
|
||||||
withImageNoCaption,
|
|
||||||
withSticker,
|
|
||||||
withTapToViewExpired,
|
|
||||||
} = this.props;
|
|
||||||
const moduleName = module || 'module-timestamp';
|
|
||||||
|
|
||||||
if (timestamp === null || timestamp === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
moduleName,
|
|
||||||
direction ? `${moduleName}--${direction}` : null,
|
|
||||||
withTapToViewExpired && direction
|
|
||||||
? `${moduleName}--${direction}-with-tap-to-view-expired`
|
|
||||||
: null,
|
|
||||||
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
|
|
||||||
withSticker ? `${moduleName}--with-sticker` : null
|
|
||||||
)}
|
|
||||||
title={moment(timestamp).format('llll')}
|
|
||||||
>
|
|
||||||
{formatTime(i18n, timestamp)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -59,6 +59,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
id: 'messageId',
|
id: 'messageId',
|
||||||
|
now: Date.now(),
|
||||||
renderingContext: 'storybook',
|
renderingContext: 'storybook',
|
||||||
interactionMode: 'keyboard',
|
interactionMode: 'keyboard',
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
|
@ -67,7 +68,6 @@ const defaultMessageProps: MessagesProps = {
|
||||||
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||||
markViewed: action('default--markViewed'),
|
markViewed: action('default--markViewed'),
|
||||||
messageExpanded: action('default--message-expanded'),
|
messageExpanded: action('default--message-expanded'),
|
||||||
onHeightChange: action('default--onHeightChange'),
|
|
||||||
openConversation: action('default--openConversation'),
|
openConversation: action('default--openConversation'),
|
||||||
openLink: action('default--openLink'),
|
openLink: action('default--openLink'),
|
||||||
previews: [],
|
previews: [],
|
||||||
|
|
|
@ -324,11 +324,9 @@ const actions = () => ({
|
||||||
'acknowledgeGroupMemberNameCollisions'
|
'acknowledgeGroupMemberNameCollisions'
|
||||||
),
|
),
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearChangedMessages: action('clearChangedMessages'),
|
|
||||||
clearInvitedUuidsForNewlyCreatedGroup: action(
|
clearInvitedUuidsForNewlyCreatedGroup: action(
|
||||||
'clearInvitedUuidsForNewlyCreatedGroup'
|
'clearInvitedUuidsForNewlyCreatedGroup'
|
||||||
),
|
),
|
||||||
setLoadCountdownStart: action('setLoadCountdownStart'),
|
|
||||||
setIsNearBottom: action('setIsNearBottom'),
|
setIsNearBottom: action('setIsNearBottom'),
|
||||||
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
|
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
|
||||||
loadAndScroll: action('loadAndScroll'),
|
loadAndScroll: action('loadAndScroll'),
|
||||||
|
@ -358,7 +356,6 @@ const actions = () => ({
|
||||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
|
|
||||||
onHeightChange: action('onHeightChange'),
|
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
@ -373,7 +370,6 @@ const actions = () => ({
|
||||||
|
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
|
|
||||||
messageSizeChanged: action('messageSizeChanged'),
|
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
|
|
||||||
|
@ -401,11 +397,13 @@ const renderItem = ({
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
isOldestTimelineItem,
|
isOldestTimelineItem,
|
||||||
|
now,
|
||||||
}: {
|
}: {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
containerElementRef: React.RefObject<HTMLElement>;
|
containerElementRef: React.RefObject<HTMLElement>;
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
isOldestTimelineItem: boolean;
|
isOldestTimelineItem: boolean;
|
||||||
|
now: number;
|
||||||
}) => (
|
}) => (
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
getPreferredBadge={() => undefined}
|
getPreferredBadge={() => undefined}
|
||||||
|
@ -417,6 +415,7 @@ const renderItem = ({
|
||||||
item={items[messageId]}
|
item={items[messageId]}
|
||||||
previousItem={undefined}
|
previousItem={undefined}
|
||||||
nextItem={undefined}
|
nextItem={undefined}
|
||||||
|
now={now}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode="keyboard"
|
interactionMode="keyboard"
|
||||||
theme={ThemeType.light}
|
theme={ThemeType.light}
|
||||||
|
@ -460,7 +459,6 @@ const renderHeroRow = () => {
|
||||||
profileName={getProfileName()}
|
profileName={getProfileName()}
|
||||||
phoneNumber={getPhoneNumber()}
|
phoneNumber={getPhoneNumber()}
|
||||||
conversationType="direct"
|
conversationType="direct"
|
||||||
onHeightChange={action('onHeightChange in ConversationHero')}
|
|
||||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
unblurAvatar={action('unblurAvatar')}
|
unblurAvatar={action('unblurAvatar')}
|
||||||
|
@ -486,6 +484,7 @@ const renderTypingBubble = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
discardMessages: action('discardMessages'),
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
theme: React.useContext(StorybookThemeContext),
|
theme: React.useContext(StorybookThemeContext),
|
||||||
|
@ -493,6 +492,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
getTimestampForMessage: Date.now,
|
getTimestampForMessage: Date.now,
|
||||||
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
|
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
|
||||||
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
|
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
|
||||||
|
isConversationSelected: true,
|
||||||
isIncomingMessageRequest: boolean(
|
isIncomingMessageRequest: boolean(
|
||||||
'isIncomingMessageRequest',
|
'isIncomingMessageRequest',
|
||||||
overrideProps.isIncomingMessageRequest === true
|
overrideProps.isIncomingMessageRequest === true
|
||||||
|
@ -502,7 +502,6 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
overrideProps.isLoadingMessages === false
|
overrideProps.isLoadingMessages === false
|
||||||
),
|
),
|
||||||
items: overrideProps.items || Object.keys(items),
|
items: overrideProps.items || Object.keys(items),
|
||||||
resetCounter: 0,
|
|
||||||
scrollToIndex: overrideProps.scrollToIndex,
|
scrollToIndex: overrideProps.scrollToIndex,
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -86,16 +86,15 @@ const getDefaultProps = () => ({
|
||||||
showExpiredOutgoingTapToViewToast: action(
|
showExpiredOutgoingTapToViewToast: action(
|
||||||
'showExpiredIncomingTapToViewToast'
|
'showExpiredIncomingTapToViewToast'
|
||||||
),
|
),
|
||||||
onHeightChange: action('onHeightChange'),
|
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
showIdentity: action('showIdentity'),
|
showIdentity: action('showIdentity'),
|
||||||
messageSizeChanged: action('messageSizeChanged'),
|
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
previousItem: undefined,
|
previousItem: undefined,
|
||||||
nextItem: undefined,
|
nextItem: undefined,
|
||||||
|
now: Date.now(),
|
||||||
|
|
||||||
renderContact,
|
renderContact,
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
|
|
|
@ -54,7 +54,6 @@ import type { SmartContactRendererType } from '../../groupChange';
|
||||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||||
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
||||||
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||||
import * as log from '../../logging/log';
|
|
||||||
import type { FullJSXType } from '../Intl';
|
import type { FullJSXType } from '../Intl';
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
|
@ -156,6 +155,7 @@ type PropsLocalType = {
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
previousItem: undefined | TimelineItemType;
|
previousItem: undefined | TimelineItemType;
|
||||||
nextItem: undefined | TimelineItemType;
|
nextItem: undefined | TimelineItemType;
|
||||||
|
now: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActionsType = MessageActionsType &
|
type PropsActionsType = MessageActionsType &
|
||||||
|
@ -188,8 +188,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
item,
|
item,
|
||||||
i18n,
|
i18n,
|
||||||
theme,
|
theme,
|
||||||
messageSizeChanged,
|
|
||||||
nextItem,
|
nextItem,
|
||||||
|
now,
|
||||||
previousItem,
|
previousItem,
|
||||||
renderContact,
|
renderContact,
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
|
@ -199,8 +199,12 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
log.warn(`TimelineItem: item ${id} provided was falsey`);
|
// This can happen under normal conditions.
|
||||||
|
//
|
||||||
|
// `<Timeline>` and `<TimelineItem>` are connected to Redux separately. If a
|
||||||
|
// timeline item is removed from Redux, `<TimelineItem>` might re-render before
|
||||||
|
// `<Timeline>` does, which means we'll try to render nothing. This should resolve
|
||||||
|
// itself quickly, as soon as `<Timeline>` re-renders.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,9 +233,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<CallingNotification
|
<CallingNotification
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
messageId={id}
|
|
||||||
messageSizeChanged={messageSizeChanged}
|
|
||||||
nextItem={nextItem}
|
nextItem={nextItem}
|
||||||
|
now={now}
|
||||||
returnToActiveCall={returnToActiveCall}
|
returnToActiveCall={returnToActiveCall}
|
||||||
startCallingLobby={startCallingLobby}
|
startCallingLobby={startCallingLobby}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
|
|
|
@ -6,11 +6,9 @@ import type { ThunkAction } from 'redux-thunk';
|
||||||
import {
|
import {
|
||||||
difference,
|
difference,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
intersection,
|
|
||||||
omit,
|
omit,
|
||||||
orderBy,
|
orderBy,
|
||||||
pick,
|
pick,
|
||||||
uniq,
|
|
||||||
values,
|
values,
|
||||||
without,
|
without,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
@ -243,13 +241,10 @@ export type MessageLookupType = {
|
||||||
[key: string]: MessageWithUIFieldsType;
|
[key: string]: MessageWithUIFieldsType;
|
||||||
};
|
};
|
||||||
export type ConversationMessageType = {
|
export type ConversationMessageType = {
|
||||||
heightChangeMessageIds: Array<string>;
|
|
||||||
isLoadingMessages: boolean;
|
isLoadingMessages: boolean;
|
||||||
isNearBottom?: boolean;
|
isNearBottom?: boolean;
|
||||||
loadCountdownStart?: number;
|
|
||||||
messageIds: Array<string>;
|
messageIds: Array<string>;
|
||||||
metrics: MessageMetricsType;
|
metrics: MessageMetricsType;
|
||||||
resetCounter: number;
|
|
||||||
scrollToMessageId?: string;
|
scrollToMessageId?: string;
|
||||||
scrollToMessageCounter: number;
|
scrollToMessageCounter: number;
|
||||||
};
|
};
|
||||||
|
@ -397,6 +392,7 @@ const COMPOSE_REPLACE_AVATAR = 'conversations/compose/REPLACE_AVATAR';
|
||||||
const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED';
|
const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED';
|
||||||
const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION =
|
const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION =
|
||||||
'conversations/CONVERSATION_STOPPED_BY_MISSING_VERIFICATION';
|
'conversations/CONVERSATION_STOPPED_BY_MISSING_VERIFICATION';
|
||||||
|
const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES';
|
||||||
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
|
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
|
||||||
const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE';
|
const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE';
|
||||||
|
|
||||||
|
@ -480,6 +476,10 @@ type CustomColorRemovedActionType = {
|
||||||
colorId: string;
|
colorId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type DiscardMessagesActionType = {
|
||||||
|
type: typeof DISCARD_MESSAGES;
|
||||||
|
payload: { conversationId: string; numberToKeepAtBottom: number };
|
||||||
|
};
|
||||||
type SetPreJoinConversationActionType = {
|
type SetPreJoinConversationActionType = {
|
||||||
type: 'SET_PRE_JOIN_CONVERSATION';
|
type: 'SET_PRE_JOIN_CONVERSATION';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -566,13 +566,6 @@ export type MessageExpandedActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type MessageSizeChangedActionType = {
|
|
||||||
type: 'MESSAGE_SIZE_CHANGED';
|
|
||||||
payload: {
|
|
||||||
id: string;
|
|
||||||
conversationId: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type MessagesAddedActionType = {
|
export type MessagesAddedActionType = {
|
||||||
type: 'MESSAGES_ADDED';
|
type: 'MESSAGES_ADDED';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -615,13 +608,6 @@ export type SetMessagesLoadingActionType = {
|
||||||
isLoadingMessages: boolean;
|
isLoadingMessages: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SetLoadCountdownStartActionType = {
|
|
||||||
type: 'SET_LOAD_COUNTDOWN_START';
|
|
||||||
payload: {
|
|
||||||
conversationId: string;
|
|
||||||
loadCountdownStart?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type SetIsNearBottomActionType = {
|
export type SetIsNearBottomActionType = {
|
||||||
type: 'SET_NEAR_BOTTOM';
|
type: 'SET_NEAR_BOTTOM';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -644,13 +630,6 @@ export type ScrollToMessageActionType = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ClearChangedMessagesActionType = {
|
|
||||||
type: 'CLEAR_CHANGED_MESSAGES';
|
|
||||||
payload: {
|
|
||||||
conversationId: string;
|
|
||||||
baton: unknown;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type ClearSelectedMessageActionType = {
|
export type ClearSelectedMessageActionType = {
|
||||||
type: 'CLEAR_SELECTED_MESSAGE';
|
type: 'CLEAR_SELECTED_MESSAGE';
|
||||||
payload: null;
|
payload: null;
|
||||||
|
@ -759,7 +738,6 @@ export type ConversationActionType =
|
||||||
| CancelVerificationDataByConversationActionType
|
| CancelVerificationDataByConversationActionType
|
||||||
| CantAddContactToGroupActionType
|
| CantAddContactToGroupActionType
|
||||||
| ClearCancelledVerificationActionType
|
| ClearCancelledVerificationActionType
|
||||||
| ClearChangedMessagesActionType
|
|
||||||
| ClearVerificationDataByConversationActionType
|
| ClearVerificationDataByConversationActionType
|
||||||
| ClearGroupCreationErrorActionType
|
| ClearGroupCreationErrorActionType
|
||||||
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
||||||
|
@ -783,11 +761,11 @@ export type ConversationActionType =
|
||||||
| CreateGroupPendingActionType
|
| CreateGroupPendingActionType
|
||||||
| CreateGroupRejectedActionType
|
| CreateGroupRejectedActionType
|
||||||
| CustomColorRemovedActionType
|
| CustomColorRemovedActionType
|
||||||
|
| DiscardMessagesActionType
|
||||||
| MessageChangedActionType
|
| MessageChangedActionType
|
||||||
| MessageDeletedActionType
|
| MessageDeletedActionType
|
||||||
| MessageExpandedActionType
|
| MessageExpandedActionType
|
||||||
| MessageSelectedActionType
|
| MessageSelectedActionType
|
||||||
| MessageSizeChangedActionType
|
|
||||||
| MessagesAddedActionType
|
| MessagesAddedActionType
|
||||||
| MessagesResetActionType
|
| MessagesResetActionType
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
|
@ -805,7 +783,6 @@ export type ConversationActionType =
|
||||||
| SetConversationHeaderTitleActionType
|
| SetConversationHeaderTitleActionType
|
||||||
| SetIsFetchingUsernameActionType
|
| SetIsFetchingUsernameActionType
|
||||||
| SetIsNearBottomActionType
|
| SetIsNearBottomActionType
|
||||||
| SetLoadCountdownStartActionType
|
|
||||||
| SetMessagesLoadingActionType
|
| SetMessagesLoadingActionType
|
||||||
| SetPreJoinConversationActionType
|
| SetPreJoinConversationActionType
|
||||||
| SetRecentMediaItemsActionType
|
| SetRecentMediaItemsActionType
|
||||||
|
@ -826,7 +803,6 @@ export const actions = {
|
||||||
cancelConversationVerification,
|
cancelConversationVerification,
|
||||||
cantAddContactToGroup,
|
cantAddContactToGroup,
|
||||||
clearCancelledConversationVerification,
|
clearCancelledConversationVerification,
|
||||||
clearChangedMessages,
|
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
clearInvitedUuidsForNewlyCreatedGroup,
|
clearInvitedUuidsForNewlyCreatedGroup,
|
||||||
clearSelectedMessage,
|
clearSelectedMessage,
|
||||||
|
@ -847,11 +823,11 @@ export const actions = {
|
||||||
conversationUnloaded,
|
conversationUnloaded,
|
||||||
createGroup,
|
createGroup,
|
||||||
deleteAvatarFromDisk,
|
deleteAvatarFromDisk,
|
||||||
|
discardMessages,
|
||||||
doubleCheckMissingQuoteReference,
|
doubleCheckMissingQuoteReference,
|
||||||
messageChanged,
|
messageChanged,
|
||||||
messageDeleted,
|
messageDeleted,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
messageSizeChanged,
|
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
messagesReset,
|
messagesReset,
|
||||||
myProfileChanged,
|
myProfileChanged,
|
||||||
|
@ -875,7 +851,6 @@ export const actions = {
|
||||||
setComposeGroupName,
|
setComposeGroupName,
|
||||||
setComposeSearchTerm,
|
setComposeSearchTerm,
|
||||||
setIsNearBottom,
|
setIsNearBottom,
|
||||||
setLoadCountdownStart,
|
|
||||||
setMessagesLoading,
|
setMessagesLoading,
|
||||||
setPreJoinConversation,
|
setPreJoinConversation,
|
||||||
setRecentMediaItems,
|
setRecentMediaItems,
|
||||||
|
@ -970,6 +945,12 @@ function deleteAvatarFromDisk(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discardMessages(
|
||||||
|
payload: Readonly<DiscardMessagesActionType['payload']>
|
||||||
|
): DiscardMessagesActionType {
|
||||||
|
return { type: DISCARD_MESSAGES, payload };
|
||||||
|
}
|
||||||
|
|
||||||
function replaceAvatar(
|
function replaceAvatar(
|
||||||
curr: AvatarDataType,
|
curr: AvatarDataType,
|
||||||
prev?: AvatarDataType,
|
prev?: AvatarDataType,
|
||||||
|
@ -1581,18 +1562,6 @@ function messageExpanded(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function messageSizeChanged(
|
|
||||||
id: string,
|
|
||||||
conversationId: string
|
|
||||||
): MessageSizeChangedActionType {
|
|
||||||
return {
|
|
||||||
type: 'MESSAGE_SIZE_CHANGED',
|
|
||||||
payload: {
|
|
||||||
id,
|
|
||||||
conversationId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function messagesAdded({
|
function messagesAdded({
|
||||||
conversationId,
|
conversationId,
|
||||||
isActive,
|
isActive,
|
||||||
|
@ -1694,18 +1663,6 @@ function setMessagesLoading(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function setLoadCountdownStart(
|
|
||||||
conversationId: string,
|
|
||||||
loadCountdownStart?: number
|
|
||||||
): SetLoadCountdownStartActionType {
|
|
||||||
return {
|
|
||||||
type: 'SET_LOAD_COUNTDOWN_START',
|
|
||||||
payload: {
|
|
||||||
conversationId,
|
|
||||||
loadCountdownStart,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function setIsNearBottom(
|
function setIsNearBottom(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
isNearBottom: boolean
|
isNearBottom: boolean
|
||||||
|
@ -1743,18 +1700,6 @@ function setRecentMediaItems(
|
||||||
payload: { id, recentMediaItems },
|
payload: { id, recentMediaItems },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function clearChangedMessages(
|
|
||||||
conversationId: string,
|
|
||||||
baton: unknown
|
|
||||||
): ClearChangedMessagesActionType {
|
|
||||||
return {
|
|
||||||
type: 'CLEAR_CHANGED_MESSAGES',
|
|
||||||
payload: {
|
|
||||||
conversationId,
|
|
||||||
baton,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
|
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
|
||||||
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
|
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
|
||||||
}
|
}
|
||||||
|
@ -2125,71 +2070,6 @@ export function getEmptyState(): ConversationsStateType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMessageHeightChanged(
|
|
||||||
message: MessageAttributesType,
|
|
||||||
previous: MessageAttributesType
|
|
||||||
): boolean {
|
|
||||||
const messageAttachments = message.attachments || [];
|
|
||||||
const previousAttachments = previous.attachments || [];
|
|
||||||
|
|
||||||
const errorStatusChanged =
|
|
||||||
(!message.errors && previous.errors) ||
|
|
||||||
(message.errors && !previous.errors) ||
|
|
||||||
(message.errors &&
|
|
||||||
previous.errors &&
|
|
||||||
message.errors.length !== previous.errors.length);
|
|
||||||
if (errorStatusChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupUpdateChanged = message.group_update !== previous.group_update;
|
|
||||||
if (groupUpdateChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stickerPendingChanged =
|
|
||||||
message.sticker &&
|
|
||||||
message.sticker.data &&
|
|
||||||
previous.sticker &&
|
|
||||||
previous.sticker.data &&
|
|
||||||
!previous.sticker.data.blurHash &&
|
|
||||||
previous.sticker.data.pending !== message.sticker.data.pending;
|
|
||||||
if (stickerPendingChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const longMessageAttachmentLoaded =
|
|
||||||
previous.bodyPending && !message.bodyPending;
|
|
||||||
if (longMessageAttachmentLoaded) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstAttachmentNoLongerPending =
|
|
||||||
previousAttachments[0] &&
|
|
||||||
previousAttachments[0].pending &&
|
|
||||||
messageAttachments[0] &&
|
|
||||||
!messageAttachments[0].pending;
|
|
||||||
if (firstAttachmentNoLongerPending) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentReactions = message.reactions || [];
|
|
||||||
const lastReactions = previous.reactions || [];
|
|
||||||
const reactionsChanged =
|
|
||||||
(currentReactions.length === 0) !== (lastReactions.length === 0);
|
|
||||||
if (reactionsChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDeletedForEveryone = message.deletedForEveryone;
|
|
||||||
const wasDeletedForEveryone = previous.deletedForEveryone;
|
|
||||||
if (isDeletedForEveryone !== wasDeletedForEveryone) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateConversationLookups(
|
export function updateConversationLookups(
|
||||||
added: ConversationType | undefined,
|
added: ConversationType | undefined,
|
||||||
removed: ConversationType | undefined,
|
removed: ConversationType | undefined,
|
||||||
|
@ -2417,6 +2297,38 @@ export function reducer(
|
||||||
return closeComposerModal(state, 'recommendedGroupSizeModalState' as const);
|
return closeComposerModal(state, 'recommendedGroupSizeModalState' as const);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === DISCARD_MESSAGES) {
|
||||||
|
const { conversationId, numberToKeepAtBottom } = action.payload;
|
||||||
|
|
||||||
|
const conversationMessages = getOwn(
|
||||||
|
state.messagesByConversation,
|
||||||
|
conversationId
|
||||||
|
);
|
||||||
|
if (!conversationMessages) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageIds: oldMessageIds } = conversationMessages;
|
||||||
|
if (oldMessageIds.length <= numberToKeepAtBottom) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageIdsToRemove = oldMessageIds.slice(0, -numberToKeepAtBottom);
|
||||||
|
const messageIdsToKeep = oldMessageIds.slice(-numberToKeepAtBottom);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesLookup: omit(state.messagesLookup, messageIdsToRemove),
|
||||||
|
messagesByConversation: {
|
||||||
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...conversationMessages,
|
||||||
|
messageIds: messageIdsToKeep,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === 'SET_PRE_JOIN_CONVERSATION') {
|
if (action.type === 'SET_PRE_JOIN_CONVERSATION') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { data } = payload;
|
const { data } = payload;
|
||||||
|
@ -2645,15 +2557,6 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for changes which could affect height - that's why we need this
|
|
||||||
// heightChangeMessageIds field. It tells Timeline to recalculate all of its heights
|
|
||||||
const hasHeightChanged = hasMessageHeightChanged(data, existingMessage);
|
|
||||||
|
|
||||||
const { heightChangeMessageIds } = existingConversation;
|
|
||||||
const updatedChanges = hasHeightChanged
|
|
||||||
? uniq([...heightChangeMessageIds, id])
|
|
||||||
: heightChangeMessageIds;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messagesLookup: {
|
messagesLookup: {
|
||||||
|
@ -2663,13 +2566,6 @@ export function reducer(
|
||||||
displayLimit: existingMessage.displayLimit,
|
displayLimit: existingMessage.displayLimit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messagesByConversation: {
|
|
||||||
...state.messagesByConversation,
|
|
||||||
[conversationId]: {
|
|
||||||
...existingConversation,
|
|
||||||
heightChangeMessageIds: updatedChanges,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'MESSAGE_EXPANDED') {
|
if (action.type === 'MESSAGE_EXPANDED') {
|
||||||
|
@ -2691,31 +2587,6 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'MESSAGE_SIZE_CHANGED') {
|
|
||||||
const { id, conversationId } = action.payload;
|
|
||||||
|
|
||||||
const existingConversation = getOwn(
|
|
||||||
state.messagesByConversation,
|
|
||||||
conversationId
|
|
||||||
);
|
|
||||||
if (!existingConversation) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
messagesByConversation: {
|
|
||||||
...state.messagesByConversation,
|
|
||||||
[conversationId]: {
|
|
||||||
...existingConversation,
|
|
||||||
heightChangeMessageIds: uniq([
|
|
||||||
...existingConversation.heightChangeMessageIds,
|
|
||||||
id,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === 'MESSAGES_RESET') {
|
if (action.type === 'MESSAGES_RESET') {
|
||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -2727,9 +2598,6 @@ export function reducer(
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
const resetCounter = existingConversation
|
|
||||||
? existingConversation.resetCounter + 1
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const lookup = fromPairs(messages.map(message => [message.id, message]));
|
const lookup = fromPairs(messages.map(message => [message.id, message]));
|
||||||
const sorted = orderBy(
|
const sorted = orderBy(
|
||||||
|
@ -2780,8 +2648,6 @@ export function reducer(
|
||||||
newest,
|
newest,
|
||||||
oldest,
|
oldest,
|
||||||
},
|
},
|
||||||
resetCounter,
|
|
||||||
heightChangeMessageIds: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2803,34 +2669,11 @@ export function reducer(
|
||||||
...messagesByConversation,
|
...messagesByConversation,
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
...existingConversation,
|
...existingConversation,
|
||||||
loadCountdownStart: undefined,
|
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'SET_LOAD_COUNTDOWN_START') {
|
|
||||||
const { payload } = action;
|
|
||||||
const { conversationId, loadCountdownStart } = payload;
|
|
||||||
|
|
||||||
const { messagesByConversation } = state;
|
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
|
||||||
|
|
||||||
if (!existingConversation) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
messagesByConversation: {
|
|
||||||
...messagesByConversation,
|
|
||||||
[conversationId]: {
|
|
||||||
...existingConversation,
|
|
||||||
loadCountdownStart,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === 'SET_NEAR_BOTTOM') {
|
if (action.type === 'SET_NEAR_BOTTOM') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { conversationId, isNearBottom } = payload;
|
const { conversationId, isNearBottom } = payload;
|
||||||
|
@ -2838,7 +2681,10 @@ export function reducer(
|
||||||
const { messagesByConversation } = state;
|
const { messagesByConversation } = state;
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
|
||||||
if (!existingConversation) {
|
if (
|
||||||
|
!existingConversation ||
|
||||||
|
existingConversation.isNearBottom === isNearBottom
|
||||||
|
) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2921,10 +2767,6 @@ export function reducer(
|
||||||
|
|
||||||
// Removing it from our caches
|
// Removing it from our caches
|
||||||
const messageIds = without(existingConversation.messageIds, id);
|
const messageIds = without(existingConversation.messageIds, id);
|
||||||
const heightChangeMessageIds = without(
|
|
||||||
existingConversation.heightChangeMessageIds,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
|
|
||||||
let metrics;
|
let metrics;
|
||||||
if (messageIds.length === 0) {
|
if (messageIds.length === 0) {
|
||||||
|
@ -2946,7 +2788,6 @@ export function reducer(
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
...existingConversation,
|
...existingConversation,
|
||||||
messageIds,
|
messageIds,
|
||||||
heightChangeMessageIds,
|
|
||||||
metrics,
|
metrics,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3135,12 +2976,6 @@ export function reducer(
|
||||||
totalUnread = (totalUnread || 0) + newUnread;
|
totalUnread = (totalUnread || 0) + newUnread;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedIds = intersection(newIds, existingConversation.messageIds);
|
|
||||||
const heightChangeMessageIds = uniq([
|
|
||||||
...changedIds,
|
|
||||||
...existingConversation.heightChangeMessageIds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messagesLookup: {
|
messagesLookup: {
|
||||||
|
@ -3153,7 +2988,6 @@ export function reducer(
|
||||||
...existingConversation,
|
...existingConversation,
|
||||||
isLoadingMessages: false,
|
isLoadingMessages: false,
|
||||||
messageIds,
|
messageIds,
|
||||||
heightChangeMessageIds,
|
|
||||||
scrollToMessageId: isJustSent ? last.id : undefined,
|
scrollToMessageId: isJustSent ? last.id : undefined,
|
||||||
metrics: {
|
metrics: {
|
||||||
...existingConversation.metrics,
|
...existingConversation.metrics,
|
||||||
|
@ -3172,30 +3006,6 @@ export function reducer(
|
||||||
selectedMessage: undefined,
|
selectedMessage: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'CLEAR_CHANGED_MESSAGES') {
|
|
||||||
const { payload } = action;
|
|
||||||
const { conversationId, baton } = payload;
|
|
||||||
const existingConversation = state.messagesByConversation[conversationId];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!existingConversation ||
|
|
||||||
existingConversation.heightChangeMessageIds !== baton
|
|
||||||
) {
|
|
||||||
log.warn('CLEAR_CHANGED_MESSAGES used expired baton');
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
messagesByConversation: {
|
|
||||||
...state.messagesByConversation,
|
|
||||||
[conversationId]: {
|
|
||||||
...existingConversation,
|
|
||||||
heightChangeMessageIds: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === 'CLEAR_UNREAD_METRICS') {
|
if (action.type === 'CLEAR_UNREAD_METRICS') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { conversationId } = payload;
|
const { conversationId } = payload;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import memoizee from 'memoizee';
|
import memoizee from 'memoizee';
|
||||||
import { fromPairs, isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
|
@ -837,13 +837,10 @@ export function _conversationMessagesSelector(
|
||||||
conversation: ConversationMessageType
|
conversation: ConversationMessageType
|
||||||
): TimelinePropsType {
|
): TimelinePropsType {
|
||||||
const {
|
const {
|
||||||
heightChangeMessageIds,
|
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
isNearBottom,
|
isNearBottom,
|
||||||
loadCountdownStart,
|
|
||||||
messageIds,
|
messageIds,
|
||||||
metrics,
|
metrics,
|
||||||
resetCounter,
|
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
scrollToMessageCounter,
|
scrollToMessageCounter,
|
||||||
} = conversation;
|
} = conversation;
|
||||||
|
@ -860,14 +857,6 @@ export function _conversationMessagesSelector(
|
||||||
|
|
||||||
const items = messageIds;
|
const items = messageIds;
|
||||||
|
|
||||||
const messageHeightChangeLookup =
|
|
||||||
heightChangeMessageIds && heightChangeMessageIds.length
|
|
||||||
? fromPairs(heightChangeMessageIds.map(id => [id, true]))
|
|
||||||
: null;
|
|
||||||
const messageHeightChangeIndex = messageHeightChangeLookup
|
|
||||||
? messageIds.findIndex(id => messageHeightChangeLookup[id])
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const oldestUnreadIndex = oldestUnread
|
const oldestUnreadIndex = oldestUnread
|
||||||
? messageIds.findIndex(id => id === oldestUnread.id)
|
? messageIds.findIndex(id => id === oldestUnread.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -880,19 +869,12 @@ export function _conversationMessagesSelector(
|
||||||
haveNewest,
|
haveNewest,
|
||||||
haveOldest,
|
haveOldest,
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
loadCountdownStart,
|
|
||||||
items,
|
|
||||||
isNearBottom,
|
isNearBottom,
|
||||||
messageHeightChangeBaton: heightChangeMessageIds,
|
items,
|
||||||
messageHeightChangeIndex:
|
|
||||||
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
|
||||||
? messageHeightChangeIndex
|
|
||||||
: undefined,
|
|
||||||
oldestUnreadIndex:
|
oldestUnreadIndex:
|
||||||
isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0
|
isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0
|
||||||
? oldestUnreadIndex
|
? oldestUnreadIndex
|
||||||
: undefined,
|
: undefined,
|
||||||
resetCounter,
|
|
||||||
scrollToIndex:
|
scrollToIndex:
|
||||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||||
scrollToIndexCounter: scrollToMessageCounter,
|
scrollToIndexCounter: scrollToMessageCounter,
|
||||||
|
@ -927,8 +909,7 @@ export const getConversationMessagesSelector = createSelector(
|
||||||
return {
|
return {
|
||||||
haveNewest: false,
|
haveNewest: false,
|
||||||
haveOldest: false,
|
haveOldest: false,
|
||||||
isLoadingMessages: false,
|
isLoadingMessages: true,
|
||||||
resetCounter: 0,
|
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: 0,
|
totalUnread: 0,
|
||||||
items: [],
|
items: [],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -28,6 +28,7 @@ export type Props = {
|
||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
expirationTimestamp?: number;
|
expirationTimestamp?: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
now: number;
|
||||||
played: boolean;
|
played: boolean;
|
||||||
showMessageDetail: (id: string) => void;
|
showMessageDetail: (id: string) => void;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { isEmpty, mapValues, pick } from 'lodash';
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import memoizee from 'memoizee';
|
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import type {
|
import type {
|
||||||
|
@ -99,16 +98,6 @@ export type TimelinePropsType = ExternalProps &
|
||||||
| 'updateSharedGroups'
|
| 'updateSharedGroups'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const createBoundOnHeightChange = memoizee(
|
|
||||||
(
|
|
||||||
onHeightChange: (messageId: string) => unknown,
|
|
||||||
messageId: string
|
|
||||||
): (() => unknown) => {
|
|
||||||
return () => onHeightChange(messageId);
|
|
||||||
},
|
|
||||||
{ max: 500 }
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderItem({
|
function renderItem({
|
||||||
actionProps,
|
actionProps,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
|
@ -117,7 +106,7 @@ function renderItem({
|
||||||
isOldestTimelineItem,
|
isOldestTimelineItem,
|
||||||
messageId,
|
messageId,
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
onHeightChange,
|
now,
|
||||||
previousMessageId,
|
previousMessageId,
|
||||||
}: {
|
}: {
|
||||||
actionProps: TimelineActionsType;
|
actionProps: TimelineActionsType;
|
||||||
|
@ -127,7 +116,7 @@ function renderItem({
|
||||||
isOldestTimelineItem: boolean;
|
isOldestTimelineItem: boolean;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
nextMessageId: undefined | string;
|
nextMessageId: undefined | string;
|
||||||
onHeightChange: (messageId: string) => unknown;
|
now: number;
|
||||||
previousMessageId: undefined | string;
|
previousMessageId: undefined | string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@ -140,7 +129,7 @@ function renderItem({
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
previousMessageId={previousMessageId}
|
previousMessageId={previousMessageId}
|
||||||
nextMessageId={nextMessageId}
|
nextMessageId={nextMessageId}
|
||||||
onHeightChange={createBoundOnHeightChange(onHeightChange, messageId)}
|
now={now}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
renderReactionPicker={renderReactionPicker}
|
renderReactionPicker={renderReactionPicker}
|
||||||
renderAudioAttachment={renderAudioAttachment}
|
renderAudioAttachment={renderAudioAttachment}
|
||||||
|
@ -154,14 +143,12 @@ function renderLastSeenIndicator(id: string): JSX.Element {
|
||||||
|
|
||||||
function renderHeroRow(
|
function renderHeroRow(
|
||||||
id: string,
|
id: string,
|
||||||
onHeightChange: () => unknown,
|
|
||||||
unblurAvatar: () => void,
|
unblurAvatar: () => void,
|
||||||
updateSharedGroups: () => unknown
|
updateSharedGroups: () => unknown
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SmartHeroRow
|
<SmartHeroRow
|
||||||
id={id}
|
id={id}
|
||||||
onHeightChange={onHeightChange}
|
|
||||||
unblurAvatar={unblurAvatar}
|
unblurAvatar={unblurAvatar}
|
||||||
updateSharedGroups={updateSharedGroups}
|
updateSharedGroups={updateSharedGroups}
|
||||||
/>
|
/>
|
||||||
|
@ -306,6 +293,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
'typingContactId',
|
'typingContactId',
|
||||||
'isGroupV1AndDisabled',
|
'isGroupV1AndDisabled',
|
||||||
]),
|
]),
|
||||||
|
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||||
isIncomingMessageRequest: Boolean(
|
isIncomingMessageRequest: Boolean(
|
||||||
conversation.messageRequestsEnabled &&
|
conversation.messageRequestsEnabled &&
|
||||||
!conversation.acceptedMessageRequest
|
!conversation.acceptedMessageRequest
|
||||||
|
|
|
@ -27,6 +27,7 @@ type ExternalProps = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
nextMessageId: undefined | string;
|
nextMessageId: undefined | string;
|
||||||
previousMessageId: undefined | string;
|
previousMessageId: undefined | string;
|
||||||
|
now: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderContact(conversationId: string): JSX.Element {
|
function renderContact(conversationId: string): JSX.Element {
|
||||||
|
@ -45,6 +46,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
messageId,
|
messageId,
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
previousMessageId,
|
previousMessageId,
|
||||||
|
now,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const messageSelector = getMessageSelector(state);
|
const messageSelector = getMessageSelector(state);
|
||||||
|
@ -66,6 +68,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
item,
|
item,
|
||||||
previousItem,
|
previousItem,
|
||||||
nextItem,
|
nextItem,
|
||||||
|
now,
|
||||||
id: messageId,
|
id: messageId,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
|
@ -1,299 +0,0 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { times } from 'lodash';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import {
|
|
||||||
fromItemIndexToRow,
|
|
||||||
fromRowToItemIndex,
|
|
||||||
getEphemeralRows,
|
|
||||||
getHeroRow,
|
|
||||||
getLastSeenIndicatorRow,
|
|
||||||
getRowCount,
|
|
||||||
getTypingBubbleRow,
|
|
||||||
} from '../../util/timelineUtil';
|
|
||||||
|
|
||||||
describe('<Timeline> utilities', () => {
|
|
||||||
const getItems = (count: number): Array<string> => times(count, () => uuid());
|
|
||||||
|
|
||||||
describe('fromItemIndexToRow', () => {
|
|
||||||
it('returns the same number under normal conditions', () => {
|
|
||||||
times(5, index => {
|
|
||||||
assert.strictEqual(
|
|
||||||
fromItemIndexToRow(index, { haveOldest: false }),
|
|
||||||
index
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 1 (for the hero row) if you have the oldest messages', () => {
|
|
||||||
times(5, index => {
|
|
||||||
assert.strictEqual(
|
|
||||||
fromItemIndexToRow(index, { haveOldest: true }),
|
|
||||||
index + 1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 1 (for the unread indicator) once crossing the unread indicator index', () => {
|
|
||||||
const props = { haveOldest: false, oldestUnreadIndex: 2 };
|
|
||||||
[0, 1].forEach(index => {
|
|
||||||
assert.strictEqual(fromItemIndexToRow(index, props), index);
|
|
||||||
});
|
|
||||||
[2, 3, 4].forEach(index => {
|
|
||||||
assert.strictEqual(fromItemIndexToRow(index, props), index + 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can include the hero row and the unread indicator', () => {
|
|
||||||
const props = { haveOldest: true, oldestUnreadIndex: 2 };
|
|
||||||
[0, 1].forEach(index => {
|
|
||||||
assert.strictEqual(fromItemIndexToRow(index, props), index + 1);
|
|
||||||
});
|
|
||||||
[2, 3, 4].forEach(index => {
|
|
||||||
assert.strictEqual(fromItemIndexToRow(index, props), index + 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fromRowToItemIndex', () => {
|
|
||||||
it('returns the item index under normal conditions', () => {
|
|
||||||
const props = { haveOldest: false, items: getItems(5) };
|
|
||||||
times(5, row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(5, props));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles the unread indicator', () => {
|
|
||||||
const props = {
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(4),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
[0, 1].forEach(row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(2, props));
|
|
||||||
[3, 4].forEach(row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row - 1);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(5, props));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles the hero row', () => {
|
|
||||||
const props = { haveOldest: true, items: getItems(3) };
|
|
||||||
|
|
||||||
assert.isUndefined(fromRowToItemIndex(0, props));
|
|
||||||
[1, 2, 3].forEach(row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row - 1);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(4, props));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles the whole enchilada', () => {
|
|
||||||
const props = {
|
|
||||||
haveOldest: true,
|
|
||||||
items: getItems(4),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isUndefined(fromRowToItemIndex(0, props));
|
|
||||||
[1, 2].forEach(row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row - 1);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(3, props));
|
|
||||||
[4, 5].forEach(row => {
|
|
||||||
assert.strictEqual(fromRowToItemIndex(row, props), row - 2);
|
|
||||||
});
|
|
||||||
assert.isUndefined(fromRowToItemIndex(6, props));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRowCount', () => {
|
|
||||||
it('returns 1 (for the hero row) if the conversation is empty', () => {
|
|
||||||
assert.strictEqual(getRowCount({ haveOldest: true, items: [] }), 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the number of items under normal conditions', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getRowCount({ haveOldest: false, items: getItems(4) }),
|
|
||||||
4
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 1 (for the hero row) if you have the oldest messages', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getRowCount({ haveOldest: true, items: getItems(4) }),
|
|
||||||
5
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 1 (for the unread indicator) if you have unread messages', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getRowCount({
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(4),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
}),
|
|
||||||
5
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 1 (for the typing contact) if you have unread messages', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getRowCount({
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(4),
|
|
||||||
typingContactId: uuid(),
|
|
||||||
}),
|
|
||||||
5
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can have the whole enchilada', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getRowCount({
|
|
||||||
haveOldest: true,
|
|
||||||
items: getItems(4),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
typingContactId: uuid(),
|
|
||||||
}),
|
|
||||||
7
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getHeroRow', () => {
|
|
||||||
it("returns undefined if there's no hero row", () => {
|
|
||||||
assert.isUndefined(getHeroRow({ haveOldest: false }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 0 if there's a hero row", () => {
|
|
||||||
assert.strictEqual(getHeroRow({ haveOldest: true }), 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLastSeenIndicatorRow', () => {
|
|
||||||
it('returns undefined with no unread messages', () => {
|
|
||||||
assert.isUndefined(getLastSeenIndicatorRow({ haveOldest: false }));
|
|
||||||
assert.isUndefined(getLastSeenIndicatorRow({ haveOldest: true }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the same number if the oldest messages are loaded', () => {
|
|
||||||
[0, 1, 2].forEach(oldestUnreadIndex => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getLastSeenIndicatorRow({ haveOldest: false, oldestUnreadIndex }),
|
|
||||||
oldestUnreadIndex
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("increases the number by 1 if there's a hero row", () => {
|
|
||||||
[0, 1, 2].forEach(oldestUnreadIndex => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getLastSeenIndicatorRow({ haveOldest: true, oldestUnreadIndex }),
|
|
||||||
oldestUnreadIndex + 1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTypingBubbleRow', () => {
|
|
||||||
it('returns undefined if nobody is typing', () => {
|
|
||||||
assert.isUndefined(
|
|
||||||
getTypingBubbleRow({ haveOldest: false, items: getItems(3) })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the last row if people are typing', () => {
|
|
||||||
[
|
|
||||||
{ haveOldest: true, items: [], typingContactId: uuid() },
|
|
||||||
{ haveOldest: false, items: getItems(3), typingContactId: uuid() },
|
|
||||||
{ haveOldest: true, items: getItems(3), typingContactId: uuid() },
|
|
||||||
{
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(3),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
typingContactId: uuid(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
haveOldest: true,
|
|
||||||
items: getItems(3),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
typingContactId: uuid(),
|
|
||||||
},
|
|
||||||
].forEach(props => {
|
|
||||||
assert.strictEqual(getTypingBubbleRow(props), getRowCount(props) - 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEphemeralRows', () => {
|
|
||||||
function iterate<T>(iterator: Iterator<T>): Array<T> {
|
|
||||||
const result: Array<T> = [];
|
|
||||||
let iteration = iterator.next();
|
|
||||||
while (!iteration.done) {
|
|
||||||
result.push(iteration.value);
|
|
||||||
iteration = iterator.next();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
it('yields each row under normal conditions', () => {
|
|
||||||
const result = getEphemeralRows({
|
|
||||||
haveOldest: false,
|
|
||||||
items: ['a', 'b', 'c'],
|
|
||||||
});
|
|
||||||
assert.deepStrictEqual(iterate(result), ['item:a', 'item:b', 'item:c']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('yields a hero row if there is one', () => {
|
|
||||||
const result = getEphemeralRows({ haveOldest: true, items: getItems(3) });
|
|
||||||
const iterated = iterate(result);
|
|
||||||
assert.lengthOf(iterated, 4);
|
|
||||||
assert.strictEqual(iterated[0], 'hero');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('yields an unread indicator if there is one', () => {
|
|
||||||
const result = getEphemeralRows({
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(3),
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
});
|
|
||||||
const iterated = iterate(result);
|
|
||||||
assert.lengthOf(iterated, 4);
|
|
||||||
assert.strictEqual(iterated[2], 'oldest-unread');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('yields a typing row if there is one', () => {
|
|
||||||
const result = getEphemeralRows({
|
|
||||||
haveOldest: false,
|
|
||||||
items: getItems(3),
|
|
||||||
typingContactId: uuid(),
|
|
||||||
});
|
|
||||||
const iterated = iterate(result);
|
|
||||||
assert.lengthOf(iterated, 4);
|
|
||||||
assert.strictEqual(iterated[3], 'typing-contact');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles the whole enchilada', () => {
|
|
||||||
const result = getEphemeralRows({
|
|
||||||
haveOldest: true,
|
|
||||||
items: ['a', 'b', 'c'],
|
|
||||||
oldestUnreadIndex: 2,
|
|
||||||
typingContactId: uuid(),
|
|
||||||
});
|
|
||||||
assert.deepStrictEqual(iterate(result), [
|
|
||||||
'hero',
|
|
||||||
'item:a',
|
|
||||||
'item:b',
|
|
||||||
'oldest-unread',
|
|
||||||
'item:c',
|
|
||||||
'typing-contact',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -187,44 +187,42 @@ describe('timestamp', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('formatTime', () => {
|
describe('formatTime', () => {
|
||||||
useFakeTimers();
|
|
||||||
|
|
||||||
it('returns "Now" for times within the last minute, including unexpected times in the future', () => {
|
it('returns "Now" for times within the last minute, including unexpected times in the future', () => {
|
||||||
[
|
[
|
||||||
Date.now(),
|
FAKE_NOW,
|
||||||
moment().subtract(1, 'second'),
|
moment(FAKE_NOW).subtract(1, 'second'),
|
||||||
moment().subtract(59, 'seconds'),
|
moment(FAKE_NOW).subtract(59, 'seconds'),
|
||||||
moment().add(1, 'minute'),
|
moment(FAKE_NOW).add(1, 'minute'),
|
||||||
moment().add(1, 'year'),
|
moment(FAKE_NOW).add(1, 'year'),
|
||||||
].forEach(timestamp => {
|
].forEach(timestamp => {
|
||||||
assert.strictEqual(formatTime(i18n, timestamp), 'Now');
|
assert.strictEqual(formatTime(i18n, timestamp, FAKE_NOW), 'Now');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns "X minutes ago" for times in the last hour, but older than 1 minute', () => {
|
it('returns "X minutes ago" for times in the last hour, but older than 1 minute', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatTime(i18n, moment().subtract(1, 'minute')),
|
formatTime(i18n, moment(FAKE_NOW).subtract(1, 'minute'), FAKE_NOW),
|
||||||
'1m'
|
'1m'
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatTime(i18n, moment().subtract(30, 'minutes')),
|
formatTime(i18n, moment(FAKE_NOW).subtract(30, 'minutes'), FAKE_NOW),
|
||||||
'30m'
|
'30m'
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatTime(i18n, moment().subtract(59, 'minutes')),
|
formatTime(i18n, moment(FAKE_NOW).subtract(59, 'minutes'), FAKE_NOW),
|
||||||
'59m'
|
'59m'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns hh:mm-like times for times older than 1 hour from now', () => {
|
it('returns hh:mm-like times for times older than 1 hour from now', () => {
|
||||||
const oneHourAgo = new Date('2020-01-23T03:56:00.000');
|
const oneHourAgo = new Date('2020-01-23T03:56:00.000');
|
||||||
assert.deepEqual(formatTime(i18n, oneHourAgo), '3:56 AM');
|
assert.deepEqual(formatTime(i18n, oneHourAgo, FAKE_NOW), '3:56 AM');
|
||||||
|
|
||||||
const oneDayAgo = new Date('2020-01-22T04:56:00.000');
|
const oneDayAgo = new Date('2020-01-22T04:56:00.000');
|
||||||
assert.deepEqual(formatTime(i18n, oneDayAgo), '4:56 AM');
|
assert.deepEqual(formatTime(i18n, oneDayAgo, FAKE_NOW), '4:56 AM');
|
||||||
|
|
||||||
const oneYearAgo = new Date('2019-01-23T04:56:00.000');
|
const oneYearAgo = new Date('2019-01-23T04:56:00.000');
|
||||||
assert.deepEqual(formatTime(i18n, oneYearAgo), '4:56 AM');
|
assert.deepEqual(formatTime(i18n, oneYearAgo, FAKE_NOW), '4:56 AM');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
// Copyright 2021-2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
import { scrollToBottom } from '../util/scrollToBottom';
|
|
||||||
|
|
||||||
describe('scrollToBottom', () => {
|
|
||||||
let sandbox: HTMLDivElement;
|
|
||||||
|
|
||||||
// This test seems to be flaky on Windows CI, sometimes timing out. That doesn't really
|
|
||||||
// make sense because the test is synchronous, but this quick-and-dirty fix is
|
|
||||||
// probably better than a full investigation.
|
|
||||||
before(function thisNeeded() {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
this.skip();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sandbox = document.createElement('div');
|
|
||||||
document.body.appendChild(sandbox);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the element's scrollTop to the element's scrollHeight", () => {
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.innerText = 'a'.repeat(50000);
|
|
||||||
Object.assign(el.style, {
|
|
||||||
height: '50px',
|
|
||||||
overflow: 'scroll',
|
|
||||||
whiteSpace: 'wrap',
|
|
||||||
width: '100px',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
});
|
|
||||||
sandbox.appendChild(el);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
el.scrollTop,
|
|
||||||
0,
|
|
||||||
'Test is not set up correctly. Element is already scrolled'
|
|
||||||
);
|
|
||||||
assert.isAtLeast(
|
|
||||||
el.scrollHeight,
|
|
||||||
50,
|
|
||||||
'Test is not set up correctly. scrollHeight is too low'
|
|
||||||
);
|
|
||||||
|
|
||||||
scrollToBottom(el);
|
|
||||||
|
|
||||||
assert.isAtLeast(el.scrollTop, el.scrollHeight - 50);
|
|
||||||
});
|
|
||||||
});
|
|
86
ts/test-electron/scrollUtil_test.ts
Normal file
86
ts/test-electron/scrollUtil_test.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getScrollBottom,
|
||||||
|
scrollToBottom,
|
||||||
|
setScrollBottom,
|
||||||
|
} from '../util/scrollUtil';
|
||||||
|
|
||||||
|
describe('scroll utilities', () => {
|
||||||
|
let sandbox: HTMLDivElement;
|
||||||
|
let el: HTMLDivElement;
|
||||||
|
|
||||||
|
// These tests to be flaky on Windows CI, sometimes timing out. That doesn't really
|
||||||
|
// make sense because the test is synchronous, but this quick-and-dirty fix is
|
||||||
|
// probably better than a full investigation.
|
||||||
|
before(function thisNeeded() {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = document.createElement('div');
|
||||||
|
document.body.appendChild(sandbox);
|
||||||
|
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.innerText = 'a'.repeat(50000);
|
||||||
|
Object.assign(el.style, {
|
||||||
|
height: '50px',
|
||||||
|
overflow: 'scroll',
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
width: '100px',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
});
|
||||||
|
sandbox.appendChild(el);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
el.scrollTop,
|
||||||
|
0,
|
||||||
|
'Test is not set up correctly. Element is already scrolled'
|
||||||
|
);
|
||||||
|
assert.isAtLeast(
|
||||||
|
el.scrollHeight,
|
||||||
|
50,
|
||||||
|
'Test is not set up correctly. scrollHeight is too low'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getScrollBottom', () => {
|
||||||
|
it('gets the distance from the bottom', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getScrollBottom(el),
|
||||||
|
el.scrollHeight - el.clientHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
el.scrollTop = 999999;
|
||||||
|
|
||||||
|
assert.strictEqual(getScrollBottom(el), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setScrollBottom', () => {
|
||||||
|
it('sets the distance from the bottom', () => {
|
||||||
|
setScrollBottom(el, 12);
|
||||||
|
assert.strictEqual(getScrollBottom(el), 12);
|
||||||
|
|
||||||
|
setScrollBottom(el, 9999999);
|
||||||
|
assert.strictEqual(el.scrollTop, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scrollToBottom', () => {
|
||||||
|
it("sets the element's scrollTop to the element's scrollHeight", () => {
|
||||||
|
scrollToBottom(el);
|
||||||
|
|
||||||
|
assert.isAtLeast(el.scrollTop, el.scrollHeight - 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,7 +5,6 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { set } from 'lodash/fp';
|
|
||||||
import { reducer as rootReducer } from '../../../state/reducer';
|
import { reducer as rootReducer } from '../../../state/reducer';
|
||||||
import { noopAction } from '../../../state/ducks/noop';
|
import { noopAction } from '../../../state/ducks/noop';
|
||||||
import {
|
import {
|
||||||
|
@ -55,9 +54,8 @@ const {
|
||||||
closeContactSpoofingReview,
|
closeContactSpoofingReview,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
createGroup,
|
|
||||||
messageSizeChanged,
|
|
||||||
conversationStoppedByMissingVerification,
|
conversationStoppedByMissingVerification,
|
||||||
|
createGroup,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
repairNewestMessage,
|
repairNewestMessage,
|
||||||
repairOldestMessage,
|
repairOldestMessage,
|
||||||
|
@ -334,13 +332,11 @@ describe('both/state/ducks/conversations', () => {
|
||||||
|
|
||||||
function getDefaultConversationMessage(): ConversationMessageType {
|
function getDefaultConversationMessage(): ConversationMessageType {
|
||||||
return {
|
return {
|
||||||
heightChangeMessageIds: [],
|
|
||||||
isLoadingMessages: false,
|
isLoadingMessages: false,
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnread: 0,
|
||||||
},
|
},
|
||||||
resetCounter: 0,
|
|
||||||
scrollToMessageCounter: 0,
|
scrollToMessageCounter: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -832,76 +828,6 @@ describe('both/state/ducks/conversations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('MESSAGE_SIZE_CHANGED', () => {
|
|
||||||
const stateWithActiveConversation = {
|
|
||||||
...getEmptyState(),
|
|
||||||
messagesByConversation: {
|
|
||||||
[conversationId]: {
|
|
||||||
heightChangeMessageIds: [],
|
|
||||||
isLoadingMessages: false,
|
|
||||||
isNearBottom: true,
|
|
||||||
messageIds: [messageId],
|
|
||||||
metrics: { totalUnread: 0 },
|
|
||||||
resetCounter: 0,
|
|
||||||
scrollToMessageCounter: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messagesLookup: {
|
|
||||||
[messageId]: getDefaultMessage(messageId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it('does nothing if no conversation is active', () => {
|
|
||||||
const state = getEmptyState();
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
reducer(state, messageSizeChanged('messageId', 'convoId')),
|
|
||||||
state
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing if a different conversation is active', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
reducer(
|
|
||||||
stateWithActiveConversation,
|
|
||||||
messageSizeChanged(messageId, 'another-conversation-guid')
|
|
||||||
),
|
|
||||||
stateWithActiveConversation
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds the message ID to the list of messages with changed heights', () => {
|
|
||||||
const result = reducer(
|
|
||||||
stateWithActiveConversation,
|
|
||||||
messageSizeChanged(messageId, conversationId)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.sameMembers(
|
|
||||||
result.messagesByConversation[conversationId]
|
|
||||||
?.heightChangeMessageIds || [],
|
|
||||||
[messageId]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't add duplicates to the list of changed-heights messages", () => {
|
|
||||||
const state = set(
|
|
||||||
['messagesByConversation', conversationId, 'heightChangeMessageIds'],
|
|
||||||
[messageId],
|
|
||||||
stateWithActiveConversation
|
|
||||||
);
|
|
||||||
const result = reducer(
|
|
||||||
state,
|
|
||||||
messageSizeChanged(messageId, conversationId)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.sameMembers(
|
|
||||||
result.messagesByConversation[conversationId]
|
|
||||||
?.heightChangeMessageIds || [],
|
|
||||||
[messageId]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('CONVERSATION_STOPPED_BY_MISSING_VERIFICATION', () => {
|
describe('CONVERSATION_STOPPED_BY_MISSING_VERIFICATION', () => {
|
||||||
it('adds to state, removing duplicates', () => {
|
it('adds to state, removing duplicates', () => {
|
||||||
const first = reducer(
|
const first = reducer(
|
||||||
|
|
|
@ -7662,14 +7662,6 @@
|
||||||
"updated": "2021-01-18T22:24:05.937Z",
|
"updated": "2021-01-18T22:24:05.937Z",
|
||||||
"reasonDetail": "Used to reference popup menu boundaries element"
|
"reasonDetail": "Used to reference popup menu boundaries element"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/conversation/ConversationHero.tsx",
|
|
||||||
"line": " const firstRenderRef = useRef(true);",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-10-26T19:12:24.410Z",
|
|
||||||
"reasonDetail": "Doesn't refer to a DOM element."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/GIF.tsx",
|
"path": "ts/components/conversation/GIF.tsx",
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export function scrollToBottom(el: HTMLElement): void {
|
|
||||||
// We want to mutate the parameter here.
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
}
|
|
23
ts/util/scrollUtil.ts
Normal file
23
ts/util/scrollUtil.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export const getScrollBottom = (
|
||||||
|
el: Readonly<Pick<HTMLElement, 'clientHeight' | 'scrollHeight' | 'scrollTop'>>
|
||||||
|
): number => el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||||
|
|
||||||
|
export function setScrollBottom(
|
||||||
|
el: Pick<HTMLElement, 'clientHeight' | 'scrollHeight' | 'scrollTop'>,
|
||||||
|
newScrollBottom: number
|
||||||
|
): void {
|
||||||
|
// We want to mutate the parameter here.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
el.scrollTop = el.scrollHeight - newScrollBottom - el.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollToBottom(
|
||||||
|
el: Pick<HTMLElement, 'scrollHeight' | 'scrollTop'>
|
||||||
|
): void {
|
||||||
|
// We want to mutate the parameter here.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
|
@ -1,208 +1,8 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { CellMeasurerCacheInterface } from 'react-virtualized/dist/es/CellMeasurer';
|
|
||||||
|
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
import type { PropsType } from '../components/conversation/Timeline';
|
|
||||||
import { WidthBreakpoint } from '../components/_util';
|
import { WidthBreakpoint } from '../components/_util';
|
||||||
|
|
||||||
export class RowHeightCache implements CellMeasurerCacheInterface {
|
|
||||||
private readonly cache = new Map<number, number>();
|
|
||||||
|
|
||||||
private highestRowIndexSeen = 0;
|
|
||||||
|
|
||||||
constructor(private readonly estimatedRowHeight: number) {}
|
|
||||||
|
|
||||||
hasFixedWidth(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidth(): number {
|
|
||||||
// If the cache has a fixed width, we can just return a fixed value. See [the
|
|
||||||
// React Virtualized source code][0] for an example.
|
|
||||||
// [0]: https://github.com/bvaughn/react-virtualized/blob/abe0530a512639c042e74009fbf647abdb52d661/source/CellMeasurer/CellMeasurerCache.js#L6
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFixedHeight(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHeight(rowIndex: number): number {
|
|
||||||
return this.cache.get(rowIndex) ?? this.estimatedRowHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
has(rowIndex: number): boolean {
|
|
||||||
return this.cache.has(rowIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
set(
|
|
||||||
rowIndex: number,
|
|
||||||
_columnIndex: number,
|
|
||||||
_width: number,
|
|
||||||
height: number
|
|
||||||
): void {
|
|
||||||
this.cache.set(rowIndex, height);
|
|
||||||
this.highestRowIndexSeen = Math.max(this.highestRowIndexSeen, rowIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearPlus(rowIndex: number): void {
|
|
||||||
if (rowIndex <= 0) {
|
|
||||||
this.clearAll();
|
|
||||||
} else {
|
|
||||||
for (let i = rowIndex; i <= this.highestRowIndexSeen; i += 1) {
|
|
||||||
this.cache.delete(i);
|
|
||||||
}
|
|
||||||
this.highestRowIndexSeen = Math.min(
|
|
||||||
this.highestRowIndexSeen,
|
|
||||||
rowIndex - 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAll(): void {
|
|
||||||
this.cache.clear();
|
|
||||||
this.highestRowIndexSeen = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromItemIndexToRow(
|
|
||||||
itemIndex: number,
|
|
||||||
{
|
|
||||||
haveOldest,
|
|
||||||
oldestUnreadIndex,
|
|
||||||
}: Readonly<Pick<PropsType, 'haveOldest' | 'oldestUnreadIndex'>>
|
|
||||||
): number {
|
|
||||||
let result = itemIndex;
|
|
||||||
|
|
||||||
// Hero row
|
|
||||||
if (haveOldest) {
|
|
||||||
result += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unread indicator
|
|
||||||
if (isNumber(oldestUnreadIndex) && itemIndex >= oldestUnreadIndex) {
|
|
||||||
result += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromRowToItemIndex(
|
|
||||||
row: number,
|
|
||||||
props: Readonly<Pick<PropsType, 'haveOldest' | 'items' | 'oldestUnreadIndex'>>
|
|
||||||
): undefined | number {
|
|
||||||
const { haveOldest, items, oldestUnreadIndex } = props;
|
|
||||||
|
|
||||||
let result = row;
|
|
||||||
|
|
||||||
// Hero row
|
|
||||||
if (haveOldest) {
|
|
||||||
result -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unread indicator
|
|
||||||
if (isNumber(oldestUnreadIndex)) {
|
|
||||||
if (result === oldestUnreadIndex) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result > oldestUnreadIndex) {
|
|
||||||
result -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result < 0 || result >= items.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRowCount({
|
|
||||||
haveOldest,
|
|
||||||
items,
|
|
||||||
oldestUnreadIndex,
|
|
||||||
typingContactId,
|
|
||||||
}: Readonly<
|
|
||||||
Pick<
|
|
||||||
PropsType,
|
|
||||||
'haveOldest' | 'items' | 'oldestUnreadIndex' | 'typingContactId'
|
|
||||||
>
|
|
||||||
>): number {
|
|
||||||
let result = items?.length || 0;
|
|
||||||
|
|
||||||
// Hero row
|
|
||||||
if (haveOldest) {
|
|
||||||
result += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unread indicator
|
|
||||||
if (isNumber(oldestUnreadIndex)) {
|
|
||||||
result += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typing indicator
|
|
||||||
if (typingContactId) {
|
|
||||||
result += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHeroRow({
|
|
||||||
haveOldest,
|
|
||||||
}: Readonly<Pick<PropsType, 'haveOldest'>>): undefined | number {
|
|
||||||
return haveOldest ? 0 : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLastSeenIndicatorRow(
|
|
||||||
props: Readonly<Pick<PropsType, 'haveOldest' | 'oldestUnreadIndex'>>
|
|
||||||
): undefined | number {
|
|
||||||
const { oldestUnreadIndex } = props;
|
|
||||||
return isNumber(oldestUnreadIndex)
|
|
||||||
? fromItemIndexToRow(oldestUnreadIndex, props) - 1
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTypingBubbleRow(
|
|
||||||
props: Readonly<
|
|
||||||
Pick<
|
|
||||||
PropsType,
|
|
||||||
'haveOldest' | 'items' | 'oldestUnreadIndex' | 'typingContactId'
|
|
||||||
>
|
|
||||||
>
|
|
||||||
): undefined | number {
|
|
||||||
return props.typingContactId ? getRowCount(props) - 1 : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* getEphemeralRows({
|
|
||||||
haveOldest,
|
|
||||||
items,
|
|
||||||
oldestUnreadIndex,
|
|
||||||
typingContactId,
|
|
||||||
}: Readonly<
|
|
||||||
Pick<
|
|
||||||
PropsType,
|
|
||||||
'haveOldest' | 'items' | 'oldestUnreadIndex' | 'typingContactId'
|
|
||||||
>
|
|
||||||
>): Iterator<string> {
|
|
||||||
if (haveOldest) {
|
|
||||||
yield 'hero';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
|
||||||
if (i === oldestUnreadIndex) {
|
|
||||||
yield 'oldest-unread';
|
|
||||||
}
|
|
||||||
yield `item:${items[i]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typingContactId) {
|
|
||||||
yield 'typing-contact';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWidthBreakpoint(width: number): WidthBreakpoint {
|
export function getWidthBreakpoint(width: number): WidthBreakpoint {
|
||||||
if (width > 606) {
|
if (width > 606) {
|
||||||
return WidthBreakpoint.Wide;
|
return WidthBreakpoint.Wide;
|
||||||
|
|
|
@ -67,7 +67,7 @@ export function formatDateTimeShort(
|
||||||
const diff = now - timestamp;
|
const diff = now - timestamp;
|
||||||
|
|
||||||
if (diff < HOUR || isToday(timestamp)) {
|
if (diff < HOUR || isToday(timestamp)) {
|
||||||
return formatTime(i18n, rawTimestamp);
|
return formatTime(i18n, rawTimestamp, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = moment(timestamp);
|
const m = moment(timestamp);
|
||||||
|
@ -102,10 +102,11 @@ export function formatDateTimeLong(
|
||||||
|
|
||||||
export function formatTime(
|
export function formatTime(
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
rawTimestamp: RawTimestamp
|
rawTimestamp: RawTimestamp,
|
||||||
|
now: RawTimestamp
|
||||||
): string {
|
): string {
|
||||||
const timestamp = rawTimestamp.valueOf();
|
const timestamp = rawTimestamp.valueOf();
|
||||||
const diff = Date.now() - timestamp;
|
const diff = now.valueOf() - timestamp;
|
||||||
|
|
||||||
if (diff < MINUTE) {
|
if (diff < MINUTE) {
|
||||||
return i18n('justNow');
|
return i18n('justNow');
|
||||||
|
|
Loading…
Reference in a new issue