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 {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
outline: none;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.module-message--expired {
|
||||
|
@ -104,8 +104,7 @@
|
|||
}
|
||||
|
||||
.module-message--outgoing {
|
||||
float: right;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.module-message__buttons {
|
||||
|
@ -316,7 +315,6 @@
|
|||
line-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 306px;
|
||||
|
||||
|
@ -336,8 +334,9 @@
|
|||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
// These should match the margins in .module-message__attachment-container.
|
||||
|
@ -5495,21 +5494,26 @@ button.module-image__border-overlay:focus {
|
|||
// Module: Timeline
|
||||
|
||||
.module-timeline {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.ReactVirtualized__List {
|
||||
@include scrollbar;
|
||||
}
|
||||
}
|
||||
|
||||
.module-timeline--disabled {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.module-timeline__message-container {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
.module-timeline__messages__container {
|
||||
flex: 1 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.module-timeline__messages {
|
||||
flex: 1 1;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.ReactVirtualized__List {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-TimelineWarnings {
|
||||
|
@ -6,7 +6,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $z-index-base;
|
||||
z-index: $z-index-above-above-base;
|
||||
|
||||
display: flex;
|
||||
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
|
||||
|
||||
import type { FunctionComponent, ReactNode } from 'react';
|
||||
import React, { useRef, useEffect, Children } from 'react';
|
||||
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { scrollToBottom } from '../util/scrollToBottom';
|
||||
import { scrollToBottom } from '../util/scrollUtil';
|
||||
|
||||
type PropsType = {
|
||||
children?: ReactNode;
|
||||
|
|
|
@ -21,7 +21,7 @@ const SampleMessage = ({
|
|||
direction,
|
||||
i18n,
|
||||
text,
|
||||
timestamp,
|
||||
timestampDeltaFromNow,
|
||||
status,
|
||||
style,
|
||||
}: {
|
||||
|
@ -29,7 +29,7 @@ const SampleMessage = ({
|
|||
direction: 'incoming' | 'outgoing';
|
||||
i18n: LocalizerType;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
timestampDeltaFromNow: number;
|
||||
status: 'delivered' | 'read' | 'sent';
|
||||
style?: CSSProperties;
|
||||
}): JSX.Element => (
|
||||
|
@ -51,7 +51,7 @@ const SampleMessage = ({
|
|||
<span
|
||||
className={`module-message__metadata__date module-message__metadata__date--${direction}`}
|
||||
>
|
||||
{formatTime(i18n, timestamp)}
|
||||
{formatTime(i18n, Date.now() - timestampDeltaFromNow, Date.now())}
|
||||
</span>
|
||||
{direction === 'outgoing' && (
|
||||
<div
|
||||
|
@ -78,7 +78,7 @@ export const SampleMessageBubbles = ({
|
|||
direction={includeAnotherBubble ? 'outgoing' : 'incoming'}
|
||||
i18n={i18n}
|
||||
text={i18n('ChatColorPicker__sampleBubble1')}
|
||||
timestamp={Date.now() - A_FEW_DAYS_AGO}
|
||||
timestampDeltaFromNow={A_FEW_DAYS_AGO}
|
||||
status="read"
|
||||
style={firstBubbleStyle}
|
||||
/>
|
||||
|
@ -91,7 +91,7 @@ export const SampleMessageBubbles = ({
|
|||
direction="incoming"
|
||||
i18n={i18n}
|
||||
text={i18n('ChatColorPicker__sampleBubble2')}
|
||||
timestamp={Date.now() - A_FEW_DAYS_AGO / 2}
|
||||
timestampDeltaFromNow={A_FEW_DAYS_AGO / 2}
|
||||
status="read"
|
||||
/>
|
||||
<br />
|
||||
|
@ -103,7 +103,7 @@ export const SampleMessageBubbles = ({
|
|||
direction="outgoing"
|
||||
i18n={i18n}
|
||||
text={i18n('ChatColorPicker__sampleBubble3')}
|
||||
timestamp={Date.now()}
|
||||
timestampDeltaFromNow={0}
|
||||
status="delivered"
|
||||
style={backgroundStyle}
|
||||
/>
|
||||
|
|
|
@ -19,8 +19,8 @@ const getCommonProps = () => ({
|
|||
conversationId: 'fake-conversation-id',
|
||||
i18n,
|
||||
messageId: 'fake-message-id',
|
||||
messageSizeChanged: action('messageSizeChanged'),
|
||||
nextItem: undefined,
|
||||
now: Date.now(),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
startCallingLobby: action('startCallingLobby'),
|
||||
});
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
|
@ -16,14 +15,12 @@ import {
|
|||
getCallingIcon,
|
||||
getCallingNotificationText,
|
||||
} from '../../util/callingNotification';
|
||||
import { usePrevious } from '../../hooks/usePrevious';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
export type PropsActionsType = {
|
||||
messageSizeChanged: (messageId: string, conversationId: string) => void;
|
||||
returnToActiveCall: () => void;
|
||||
startCallingLobby: (_: {
|
||||
conversationId: string;
|
||||
|
@ -34,27 +31,14 @@ export type PropsActionsType = {
|
|||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
nextItem: undefined | TimelineItemType;
|
||||
now: number;
|
||||
};
|
||||
|
||||
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
||||
|
||||
export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
||||
const { conversationId, i18n, messageId, messageSizeChanged } = 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]);
|
||||
const { i18n, now } = props;
|
||||
|
||||
let timestamp: number;
|
||||
let wasMissed = false;
|
||||
|
@ -75,38 +59,25 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
|||
const icon = getCallingIcon(props);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
bounds
|
||||
onResize={({ bounds }) => {
|
||||
if (!bounds) {
|
||||
log.error('We should be measuring the bounds');
|
||||
return;
|
||||
}
|
||||
setHeight(bounds.height);
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<SystemMessage
|
||||
button={renderCallingNotificationButton(props)}
|
||||
contents={
|
||||
<>
|
||||
{getCallingNotificationText(props, i18n)} ·{' '}
|
||||
<MessageTimestamp
|
||||
direction="outgoing"
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={false}
|
||||
withSticker={false}
|
||||
withTapToViewExpired={false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
icon={icon}
|
||||
isError={wasMissed}
|
||||
ref={measureRef}
|
||||
/>
|
||||
)}
|
||||
</Measure>
|
||||
<SystemMessage
|
||||
button={renderCallingNotificationButton(props)}
|
||||
contents={
|
||||
<>
|
||||
{getCallingNotificationText(props, i18n)} ·{' '}
|
||||
<MessageTimestamp
|
||||
direction="outgoing"
|
||||
i18n={i18n}
|
||||
now={now}
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={false}
|
||||
withSticker={false}
|
||||
withTapToViewExpired={false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
icon={icon}
|
||||
isError={wasMissed}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -19,6 +19,7 @@ const i18n = setupI18n('en', enMessages);
|
|||
|
||||
story.add('Default', () => (
|
||||
<ChangeNumberNotification
|
||||
now={Date.now()}
|
||||
sender={getDefaultConversation()}
|
||||
timestamp={1618894800000}
|
||||
i18n={i18n}
|
||||
|
@ -27,6 +28,7 @@ story.add('Default', () => (
|
|||
|
||||
story.add('Long name', () => (
|
||||
<ChangeNumberNotification
|
||||
now={Date.now()}
|
||||
sender={getDefaultConversation({
|
||||
firstName: '💅😇🖋'.repeat(50),
|
||||
})}
|
||||
|
|
|
@ -18,12 +18,13 @@ export type PropsData = {
|
|||
|
||||
export type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
now: number;
|
||||
};
|
||||
|
||||
export type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export const ChangeNumberNotification: React.FC<Props> = props => {
|
||||
const { i18n, sender, timestamp } = props;
|
||||
const { i18n, now, sender, timestamp } = props;
|
||||
|
||||
return (
|
||||
<SystemMessage
|
||||
|
@ -37,7 +38,7 @@ export const ChangeNumberNotification: React.FC<Props> = props => {
|
|||
i18n={i18n}
|
||||
/>
|
||||
·
|
||||
<MessageTimestamp i18n={i18n} timestamp={timestamp} />
|
||||
<MessageTimestamp i18n={i18n} now={now} timestamp={timestamp} />
|
||||
</>
|
||||
}
|
||||
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
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -55,7 +55,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
'Fifth',
|
||||
]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -83,7 +82,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
'Fourth',
|
||||
]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -106,7 +104,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -129,7 +126,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -152,7 +148,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={['NYC Rock Climbers']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -175,7 +170,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -198,7 +192,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -221,7 +214,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
updateSharedGroups={updateSharedGroups}
|
||||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -243,7 +235,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -265,7 +256,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -285,7 +275,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -305,7 +294,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -326,7 +314,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -347,7 +334,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -367,7 +353,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -386,7 +371,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
sharedGroupNames={[]}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
onHeightChange={action('onHeightChange')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// 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 { Avatar, AvatarBlur } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
|
@ -12,7 +12,6 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
|
|||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import * as log from '../../logging/log';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
|
||||
export type Props = {
|
||||
|
@ -22,7 +21,6 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
isMe: boolean;
|
||||
membersCount?: number;
|
||||
onHeightChange: () => unknown;
|
||||
phoneNumber?: string;
|
||||
sharedGroupNames?: Array<string>;
|
||||
unblurAvatar: () => void;
|
||||
|
@ -111,13 +109,10 @@ export const ConversationHero = ({
|
|||
profileName,
|
||||
theme,
|
||||
title,
|
||||
onHeightChange,
|
||||
unblurAvatar,
|
||||
unblurredAvatarPath,
|
||||
updateSharedGroups,
|
||||
}: Props): JSX.Element => {
|
||||
const firstRenderRef = useRef(true);
|
||||
|
||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||
useState(false);
|
||||
const closeMessageRequestWarning = () => {
|
||||
|
@ -129,30 +124,6 @@ export const ConversationHero = ({
|
|||
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 avatarOnClick: undefined | (() => void);
|
||||
if (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -83,6 +83,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => {
|
|||
audio={audio}
|
||||
computePeaks={computePeaks}
|
||||
setActiveAudioID={(id, context) => setActive({ id, context })}
|
||||
now={Date.now()}
|
||||
onFirstPlayed={action('onFirstPlayed')}
|
||||
activeAudioID={active.id}
|
||||
activeAudioContext={active.context}
|
||||
|
@ -131,6 +132,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
||||
i18n,
|
||||
id: text('id', overrideProps.id || ''),
|
||||
now: Date.now(),
|
||||
renderingContext: 'storybook',
|
||||
interactionMode: overrideProps.interactionMode || 'keyboard',
|
||||
isSticker: isBoolean(overrideProps.isSticker)
|
||||
|
@ -149,7 +151,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
markViewed: action('markViewed'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openConversation: action('openConversation'),
|
||||
openLink: action('openLink'),
|
||||
previews: overrideProps.previews || [],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2018-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { RefObject } from 'react';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM, { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
@ -118,6 +118,7 @@ export type AudioAttachmentProps = {
|
|||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
now: number;
|
||||
played: boolean;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
|
@ -210,6 +211,7 @@ export type PropsHousekeeping = {
|
|||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
now: number;
|
||||
interactionMode: InteractionModeType;
|
||||
theme: ThemeType;
|
||||
disableMenu?: boolean;
|
||||
|
@ -224,7 +226,6 @@ export type PropsHousekeeping = {
|
|||
export type PropsActions = {
|
||||
clearSelectedMessage: () => unknown;
|
||||
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
|
||||
onHeightChange: () => unknown;
|
||||
messageExpanded: (id: string, displayLimit: number) => unknown;
|
||||
checkForAccount: (identifier: string) => unknown;
|
||||
|
||||
|
@ -471,7 +472,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
this.checkExpired();
|
||||
this.checkForHeightChange(prevProps);
|
||||
|
||||
if (
|
||||
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 {
|
||||
const { clearSelectedMessage, interactionMode } = this.props;
|
||||
const { isSelected } = this.state;
|
||||
|
@ -609,6 +591,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
isTapToViewExpired,
|
||||
status,
|
||||
i18n,
|
||||
now,
|
||||
text,
|
||||
textPending,
|
||||
timestamp,
|
||||
|
@ -640,6 +623,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
isShowingImage={this.isShowingImage()}
|
||||
isSticker={isStickerLike}
|
||||
isTapToViewExpired={isTapToViewExpired}
|
||||
now={now}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
|
@ -705,6 +689,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
markViewed,
|
||||
now,
|
||||
quote,
|
||||
readStatus,
|
||||
reducedMotion,
|
||||
|
@ -834,6 +819,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
expirationLength,
|
||||
expirationTimestamp,
|
||||
id,
|
||||
now,
|
||||
played,
|
||||
showMessageDetail,
|
||||
status,
|
||||
|
@ -1238,7 +1224,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
i18n,
|
||||
id,
|
||||
messageExpanded,
|
||||
onHeightChange,
|
||||
openConversation,
|
||||
status,
|
||||
text,
|
||||
|
@ -1276,7 +1261,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
id={id}
|
||||
messageExpanded={messageExpanded}
|
||||
openConversation={openConversation}
|
||||
onHeightChange={onHeightChange}
|
||||
text={contents || ''}
|
||||
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;
|
||||
|
||||
if (!isCorrectSide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
status !== 'paused' &&
|
||||
status !== 'error' &&
|
||||
|
@ -1312,10 +1292,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderMenu(
|
||||
isCorrectSide: boolean,
|
||||
triggerId: string
|
||||
): JSX.Element | null {
|
||||
private renderMenu(triggerId: string): ReactNode {
|
||||
const {
|
||||
attachments,
|
||||
canDownload,
|
||||
|
@ -1334,7 +1311,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
selectedReaction,
|
||||
} = this.props;
|
||||
|
||||
if (!isCorrectSide || disableMenu) {
|
||||
if (disableMenu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -2462,12 +2439,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
onFocus={this.handleFocus}
|
||||
ref={this.focusRef}
|
||||
>
|
||||
{this.renderError(direction === 'incoming')}
|
||||
{this.renderMenu(direction === 'outgoing', triggerId)}
|
||||
{this.renderError()}
|
||||
{this.renderAvatar()}
|
||||
{this.renderContainer()}
|
||||
{this.renderError(direction === 'outgoing')}
|
||||
{this.renderMenu(direction === 'incoming', triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
{this.renderContextMenu(triggerId)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
|
@ -27,6 +27,7 @@ export type Props = {
|
|||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
now: number;
|
||||
played: boolean;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
|
@ -157,6 +158,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
expirationLength,
|
||||
expirationTimestamp,
|
||||
id,
|
||||
now,
|
||||
played,
|
||||
showMessageDetail,
|
||||
status,
|
||||
|
@ -539,6 +541,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
isShowingImage={false}
|
||||
isSticker={false}
|
||||
isTapToViewExpired={false}
|
||||
now={now}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
@ -23,7 +23,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
i18n,
|
||||
id: 'some-id',
|
||||
messageExpanded: action('messageExpanded'),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
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
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Props as MessageBodyPropsType } from './MessageBody';
|
||||
import { MessageBody } from './MessageBody';
|
||||
import { usePrevious } from '../../hooks/usePrevious';
|
||||
|
||||
export type Props = Pick<
|
||||
MessageBodyPropsType,
|
||||
|
@ -20,7 +19,6 @@ export type Props = Pick<
|
|||
id: string;
|
||||
displayLimit?: number;
|
||||
messageExpanded: (id: string, displayLimit: number) => unknown;
|
||||
onHeightChange: () => unknown;
|
||||
};
|
||||
|
||||
const INITIAL_LENGTH = 800;
|
||||
|
@ -70,19 +68,11 @@ export function MessageBodyReadMore({
|
|||
i18n,
|
||||
id,
|
||||
messageExpanded,
|
||||
onHeightChange,
|
||||
openConversation,
|
||||
text,
|
||||
textPending,
|
||||
}: Props): JSX.Element {
|
||||
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);
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import { SendStatus } from '../../messages/MessageSendState';
|
|||
import { WidthBreakpoint } from '../_util';
|
||||
import * as log from '../../logging/log';
|
||||
import { formatDateTimeLong } from '../../util/timestamp';
|
||||
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
export type Contact = Pick<
|
||||
ConversationType,
|
||||
|
@ -98,16 +100,20 @@ export type PropsReduxActions = Pick<
|
|||
export type ExternalProps = PropsData & PropsBackboneActions;
|
||||
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
|
||||
|
||||
type State = { nowThatUpdatesEveryMinute: number };
|
||||
|
||||
const contactSortCollator = new Intl.Collator();
|
||||
|
||||
const _keyForError = (error: Error): string => {
|
||||
return `${error.name}-${error.message}`;
|
||||
};
|
||||
|
||||
export class MessageDetail extends React.Component<Props> {
|
||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||
export class MessageDetail extends React.Component<Props, State> {
|
||||
override state = { nowThatUpdatesEveryMinute: Date.now() };
|
||||
|
||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
|
||||
private nowThatUpdatesEveryMinuteInterval?: ReturnType<typeof setInterval>;
|
||||
|
||||
public override componentDidMount(): void {
|
||||
// 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.nowThatUpdatesEveryMinuteInterval = setInterval(() => {
|
||||
this.setState({ nowThatUpdatesEveryMinute: Date.now() });
|
||||
}, MINUTE);
|
||||
}
|
||||
|
||||
public override componentWillUnmount(): void {
|
||||
clearTimeoutIfNecessary(this.nowThatUpdatesEveryMinuteInterval);
|
||||
}
|
||||
|
||||
public renderAvatar(contact: Contact): JSX.Element {
|
||||
|
@ -298,6 +312,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
showVisualAttachment,
|
||||
theme,
|
||||
} = this.props;
|
||||
const { nowThatUpdatesEveryMinute } = this.state;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
|
@ -335,7 +350,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
markViewed={markViewed}
|
||||
messageExpanded={noop}
|
||||
onHeightChange={noop}
|
||||
now={nowThatUpdatesEveryMinute}
|
||||
openConversation={openConversation}
|
||||
openLink={openLink}
|
||||
reactToMessage={reactToMessage}
|
||||
|
|
|
@ -22,6 +22,7 @@ type PropsType = {
|
|||
isShowingImage: boolean;
|
||||
isSticker?: boolean;
|
||||
isTapToViewExpired?: boolean;
|
||||
now: number;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
|
@ -40,6 +41,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
|||
isShowingImage,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
now,
|
||||
showMessageDetail,
|
||||
status,
|
||||
textPending,
|
||||
|
@ -97,6 +99,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
|||
<MessageTimestamp
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
now={now}
|
||||
direction={metadataDirection}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
|
|
|
@ -42,6 +42,7 @@ const times = (): Array<[string, number]> => [
|
|||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
timestamp: overrideProps.timestamp,
|
||||
now: Date.now(),
|
||||
module: text('module', ''),
|
||||
withImageNoCaption: boolean('withImageNoCaption', false),
|
||||
withSticker: boolean('withSticker', false),
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
// Copyright 2018-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
import { formatTime } from '../../util/timestamp';
|
||||
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { Time } from '../Time';
|
||||
|
||||
export type Props = {
|
||||
now: number;
|
||||
timestamp?: number;
|
||||
module?: string;
|
||||
withImageNoCaption?: boolean;
|
||||
|
@ -20,63 +21,36 @@ export type Props = {
|
|||
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> {
|
||||
private interval: NodeJS.Timeout | null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.interval = null;
|
||||
if (timestamp === null || timestamp === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public override componentDidMount(): void {
|
||||
const update = () => {
|
||||
this.setState({
|
||||
// Used to trigger renders
|
||||
// eslint-disable-next-line react/no-unused-state
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
};
|
||||
this.interval = setInterval(update, UPDATE_FREQUENCY);
|
||||
}
|
||||
|
||||
public override componentWillUnmount(): void {
|
||||
clearTimeoutIfNecessary(this.interval);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Time
|
||||
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
|
||||
)}
|
||||
timestamp={timestamp}
|
||||
>
|
||||
{formatTime(i18n, timestamp, now)}
|
||||
</Time>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -59,6 +59,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
id: 'messageId',
|
||||
now: Date.now(),
|
||||
renderingContext: 'storybook',
|
||||
interactionMode: 'keyboard',
|
||||
isBlocked: false,
|
||||
|
@ -67,7 +68,6 @@ const defaultMessageProps: MessagesProps = {
|
|||
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||
markViewed: action('default--markViewed'),
|
||||
messageExpanded: action('default--message-expanded'),
|
||||
onHeightChange: action('default--onHeightChange'),
|
||||
openConversation: action('default--openConversation'),
|
||||
openLink: action('default--openLink'),
|
||||
previews: [],
|
||||
|
|
|
@ -324,11 +324,9 @@ const actions = () => ({
|
|||
'acknowledgeGroupMemberNameCollisions'
|
||||
),
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearChangedMessages: action('clearChangedMessages'),
|
||||
clearInvitedUuidsForNewlyCreatedGroup: action(
|
||||
'clearInvitedUuidsForNewlyCreatedGroup'
|
||||
),
|
||||
setLoadCountdownStart: action('setLoadCountdownStart'),
|
||||
setIsNearBottom: action('setIsNearBottom'),
|
||||
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
|
||||
loadAndScroll: action('loadAndScroll'),
|
||||
|
@ -358,7 +356,6 @@ const actions = () => ({
|
|||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openLink: action('openLink'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
|
@ -373,7 +370,6 @@ const actions = () => ({
|
|||
|
||||
downloadNewVersion: action('downloadNewVersion'),
|
||||
|
||||
messageSizeChanged: action('messageSizeChanged'),
|
||||
startCallingLobby: action('startCallingLobby'),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
|
||||
|
@ -401,11 +397,13 @@ const renderItem = ({
|
|||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
isOldestTimelineItem,
|
||||
now,
|
||||
}: {
|
||||
messageId: string;
|
||||
containerElementRef: React.RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
isOldestTimelineItem: boolean;
|
||||
now: number;
|
||||
}) => (
|
||||
<TimelineItem
|
||||
getPreferredBadge={() => undefined}
|
||||
|
@ -417,6 +415,7 @@ const renderItem = ({
|
|||
item={items[messageId]}
|
||||
previousItem={undefined}
|
||||
nextItem={undefined}
|
||||
now={now}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
theme={ThemeType.light}
|
||||
|
@ -460,7 +459,6 @@ const renderHeroRow = () => {
|
|||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
onHeightChange={action('onHeightChange in ConversationHero')}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
theme={theme}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
|
@ -486,6 +484,7 @@ const renderTypingBubble = () => (
|
|||
);
|
||||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
discardMessages: action('discardMessages'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
|
@ -493,6 +492,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
getTimestampForMessage: Date.now,
|
||||
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
|
||||
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
|
||||
isConversationSelected: true,
|
||||
isIncomingMessageRequest: boolean(
|
||||
'isIncomingMessageRequest',
|
||||
overrideProps.isIncomingMessageRequest === true
|
||||
|
@ -502,7 +502,6 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
overrideProps.isLoadingMessages === false
|
||||
),
|
||||
items: overrideProps.items || Object.keys(items),
|
||||
resetCounter: 0,
|
||||
scrollToIndex: overrideProps.scrollToIndex,
|
||||
scrollToIndexCounter: 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(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openLink: action('openLink'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
downloadNewVersion: action('downloadNewVersion'),
|
||||
showIdentity: action('showIdentity'),
|
||||
messageSizeChanged: action('messageSizeChanged'),
|
||||
startCallingLobby: action('startCallingLobby'),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
previousItem: undefined,
|
||||
nextItem: undefined,
|
||||
now: Date.now(),
|
||||
|
||||
renderContact,
|
||||
renderUniversalTimerNotification,
|
||||
|
|
|
@ -54,7 +54,6 @@ import type { SmartContactRendererType } from '../../groupChange';
|
|||
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
||||
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||
import * as log from '../../logging/log';
|
||||
import type { FullJSXType } from '../Intl';
|
||||
|
||||
type CallHistoryType = {
|
||||
|
@ -156,6 +155,7 @@ type PropsLocalType = {
|
|||
theme: ThemeType;
|
||||
previousItem: undefined | TimelineItemType;
|
||||
nextItem: undefined | TimelineItemType;
|
||||
now: number;
|
||||
};
|
||||
|
||||
type PropsActionsType = MessageActionsType &
|
||||
|
@ -188,8 +188,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
item,
|
||||
i18n,
|
||||
theme,
|
||||
messageSizeChanged,
|
||||
nextItem,
|
||||
now,
|
||||
previousItem,
|
||||
renderContact,
|
||||
renderUniversalTimerNotification,
|
||||
|
@ -199,8 +199,12 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
} = this.props;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -229,9 +233,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
<CallingNotification
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
messageId={id}
|
||||
messageSizeChanged={messageSizeChanged}
|
||||
nextItem={nextItem}
|
||||
now={now}
|
||||
returnToActiveCall={returnToActiveCall}
|
||||
startCallingLobby={startCallingLobby}
|
||||
{...item.data}
|
||||
|
|
|
@ -6,11 +6,9 @@ import type { ThunkAction } from 'redux-thunk';
|
|||
import {
|
||||
difference,
|
||||
fromPairs,
|
||||
intersection,
|
||||
omit,
|
||||
orderBy,
|
||||
pick,
|
||||
uniq,
|
||||
values,
|
||||
without,
|
||||
} from 'lodash';
|
||||
|
@ -243,13 +241,10 @@ export type MessageLookupType = {
|
|||
[key: string]: MessageWithUIFieldsType;
|
||||
};
|
||||
export type ConversationMessageType = {
|
||||
heightChangeMessageIds: Array<string>;
|
||||
isLoadingMessages: boolean;
|
||||
isNearBottom?: boolean;
|
||||
loadCountdownStart?: number;
|
||||
messageIds: Array<string>;
|
||||
metrics: MessageMetricsType;
|
||||
resetCounter: number;
|
||||
scrollToMessageId?: string;
|
||||
scrollToMessageCounter: number;
|
||||
};
|
||||
|
@ -397,6 +392,7 @@ const COMPOSE_REPLACE_AVATAR = 'conversations/compose/REPLACE_AVATAR';
|
|||
const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED';
|
||||
const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION =
|
||||
'conversations/CONVERSATION_STOPPED_BY_MISSING_VERIFICATION';
|
||||
const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES';
|
||||
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
|
||||
const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE';
|
||||
|
||||
|
@ -480,6 +476,10 @@ type CustomColorRemovedActionType = {
|
|||
colorId: string;
|
||||
};
|
||||
};
|
||||
type DiscardMessagesActionType = {
|
||||
type: typeof DISCARD_MESSAGES;
|
||||
payload: { conversationId: string; numberToKeepAtBottom: number };
|
||||
};
|
||||
type SetPreJoinConversationActionType = {
|
||||
type: 'SET_PRE_JOIN_CONVERSATION';
|
||||
payload: {
|
||||
|
@ -566,13 +566,6 @@ export type MessageExpandedActionType = {
|
|||
};
|
||||
};
|
||||
|
||||
type MessageSizeChangedActionType = {
|
||||
type: 'MESSAGE_SIZE_CHANGED';
|
||||
payload: {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
export type MessagesAddedActionType = {
|
||||
type: 'MESSAGES_ADDED';
|
||||
payload: {
|
||||
|
@ -615,13 +608,6 @@ export type SetMessagesLoadingActionType = {
|
|||
isLoadingMessages: boolean;
|
||||
};
|
||||
};
|
||||
export type SetLoadCountdownStartActionType = {
|
||||
type: 'SET_LOAD_COUNTDOWN_START';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
loadCountdownStart?: number;
|
||||
};
|
||||
};
|
||||
export type SetIsNearBottomActionType = {
|
||||
type: 'SET_NEAR_BOTTOM';
|
||||
payload: {
|
||||
|
@ -644,13 +630,6 @@ export type ScrollToMessageActionType = {
|
|||
messageId: string;
|
||||
};
|
||||
};
|
||||
export type ClearChangedMessagesActionType = {
|
||||
type: 'CLEAR_CHANGED_MESSAGES';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
baton: unknown;
|
||||
};
|
||||
};
|
||||
export type ClearSelectedMessageActionType = {
|
||||
type: 'CLEAR_SELECTED_MESSAGE';
|
||||
payload: null;
|
||||
|
@ -759,7 +738,6 @@ export type ConversationActionType =
|
|||
| CancelVerificationDataByConversationActionType
|
||||
| CantAddContactToGroupActionType
|
||||
| ClearCancelledVerificationActionType
|
||||
| ClearChangedMessagesActionType
|
||||
| ClearVerificationDataByConversationActionType
|
||||
| ClearGroupCreationErrorActionType
|
||||
| ClearInvitedUuidsForNewlyCreatedGroupActionType
|
||||
|
@ -783,11 +761,11 @@ export type ConversationActionType =
|
|||
| CreateGroupPendingActionType
|
||||
| CreateGroupRejectedActionType
|
||||
| CustomColorRemovedActionType
|
||||
| DiscardMessagesActionType
|
||||
| MessageChangedActionType
|
||||
| MessageDeletedActionType
|
||||
| MessageExpandedActionType
|
||||
| MessageSelectedActionType
|
||||
| MessageSizeChangedActionType
|
||||
| MessagesAddedActionType
|
||||
| MessagesResetActionType
|
||||
| RemoveAllConversationsActionType
|
||||
|
@ -805,7 +783,6 @@ export type ConversationActionType =
|
|||
| SetConversationHeaderTitleActionType
|
||||
| SetIsFetchingUsernameActionType
|
||||
| SetIsNearBottomActionType
|
||||
| SetLoadCountdownStartActionType
|
||||
| SetMessagesLoadingActionType
|
||||
| SetPreJoinConversationActionType
|
||||
| SetRecentMediaItemsActionType
|
||||
|
@ -826,7 +803,6 @@ export const actions = {
|
|||
cancelConversationVerification,
|
||||
cantAddContactToGroup,
|
||||
clearCancelledConversationVerification,
|
||||
clearChangedMessages,
|
||||
clearGroupCreationError,
|
||||
clearInvitedUuidsForNewlyCreatedGroup,
|
||||
clearSelectedMessage,
|
||||
|
@ -847,11 +823,11 @@ export const actions = {
|
|||
conversationUnloaded,
|
||||
createGroup,
|
||||
deleteAvatarFromDisk,
|
||||
discardMessages,
|
||||
doubleCheckMissingQuoteReference,
|
||||
messageChanged,
|
||||
messageDeleted,
|
||||
messageExpanded,
|
||||
messageSizeChanged,
|
||||
messagesAdded,
|
||||
messagesReset,
|
||||
myProfileChanged,
|
||||
|
@ -875,7 +851,6 @@ export const actions = {
|
|||
setComposeGroupName,
|
||||
setComposeSearchTerm,
|
||||
setIsNearBottom,
|
||||
setLoadCountdownStart,
|
||||
setMessagesLoading,
|
||||
setPreJoinConversation,
|
||||
setRecentMediaItems,
|
||||
|
@ -970,6 +945,12 @@ function deleteAvatarFromDisk(
|
|||
};
|
||||
}
|
||||
|
||||
function discardMessages(
|
||||
payload: Readonly<DiscardMessagesActionType['payload']>
|
||||
): DiscardMessagesActionType {
|
||||
return { type: DISCARD_MESSAGES, payload };
|
||||
}
|
||||
|
||||
function replaceAvatar(
|
||||
curr: 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({
|
||||
conversationId,
|
||||
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(
|
||||
conversationId: string,
|
||||
isNearBottom: boolean
|
||||
|
@ -1743,18 +1700,6 @@ function setRecentMediaItems(
|
|||
payload: { id, recentMediaItems },
|
||||
};
|
||||
}
|
||||
function clearChangedMessages(
|
||||
conversationId: string,
|
||||
baton: unknown
|
||||
): ClearChangedMessagesActionType {
|
||||
return {
|
||||
type: 'CLEAR_CHANGED_MESSAGES',
|
||||
payload: {
|
||||
conversationId,
|
||||
baton,
|
||||
},
|
||||
};
|
||||
}
|
||||
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
|
||||
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(
|
||||
added: ConversationType | undefined,
|
||||
removed: ConversationType | undefined,
|
||||
|
@ -2417,6 +2297,38 @@ export function reducer(
|
|||
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') {
|
||||
const { payload } = action;
|
||||
const { data } = payload;
|
||||
|
@ -2645,15 +2557,6 @@ export function reducer(
|
|||
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 {
|
||||
...state,
|
||||
messagesLookup: {
|
||||
|
@ -2663,13 +2566,6 @@ export function reducer(
|
|||
displayLimit: existingMessage.displayLimit,
|
||||
},
|
||||
},
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: {
|
||||
...existingConversation,
|
||||
heightChangeMessageIds: updatedChanges,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
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') {
|
||||
const {
|
||||
conversationId,
|
||||
|
@ -2727,9 +2598,6 @@ export function reducer(
|
|||
const { messagesByConversation, messagesLookup } = state;
|
||||
|
||||
const existingConversation = messagesByConversation[conversationId];
|
||||
const resetCounter = existingConversation
|
||||
? existingConversation.resetCounter + 1
|
||||
: 0;
|
||||
|
||||
const lookup = fromPairs(messages.map(message => [message.id, message]));
|
||||
const sorted = orderBy(
|
||||
|
@ -2780,8 +2648,6 @@ export function reducer(
|
|||
newest,
|
||||
oldest,
|
||||
},
|
||||
resetCounter,
|
||||
heightChangeMessageIds: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -2803,34 +2669,11 @@ export function reducer(
|
|||
...messagesByConversation,
|
||||
[conversationId]: {
|
||||
...existingConversation,
|
||||
loadCountdownStart: undefined,
|
||||
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') {
|
||||
const { payload } = action;
|
||||
const { conversationId, isNearBottom } = payload;
|
||||
|
@ -2838,7 +2681,10 @@ export function reducer(
|
|||
const { messagesByConversation } = state;
|
||||
const existingConversation = messagesByConversation[conversationId];
|
||||
|
||||
if (!existingConversation) {
|
||||
if (
|
||||
!existingConversation ||
|
||||
existingConversation.isNearBottom === isNearBottom
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
@ -2921,10 +2767,6 @@ export function reducer(
|
|||
|
||||
// Removing it from our caches
|
||||
const messageIds = without(existingConversation.messageIds, id);
|
||||
const heightChangeMessageIds = without(
|
||||
existingConversation.heightChangeMessageIds,
|
||||
id
|
||||
);
|
||||
|
||||
let metrics;
|
||||
if (messageIds.length === 0) {
|
||||
|
@ -2946,7 +2788,6 @@ export function reducer(
|
|||
[conversationId]: {
|
||||
...existingConversation,
|
||||
messageIds,
|
||||
heightChangeMessageIds,
|
||||
metrics,
|
||||
},
|
||||
},
|
||||
|
@ -3135,12 +2976,6 @@ export function reducer(
|
|||
totalUnread = (totalUnread || 0) + newUnread;
|
||||
}
|
||||
|
||||
const changedIds = intersection(newIds, existingConversation.messageIds);
|
||||
const heightChangeMessageIds = uniq([
|
||||
...changedIds,
|
||||
...existingConversation.heightChangeMessageIds,
|
||||
]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
messagesLookup: {
|
||||
|
@ -3153,7 +2988,6 @@ export function reducer(
|
|||
...existingConversation,
|
||||
isLoadingMessages: false,
|
||||
messageIds,
|
||||
heightChangeMessageIds,
|
||||
scrollToMessageId: isJustSent ? last.id : undefined,
|
||||
metrics: {
|
||||
...existingConversation.metrics,
|
||||
|
@ -3172,30 +3006,6 @@ export function reducer(
|
|||
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') {
|
||||
const { payload } = action;
|
||||
const { conversationId } = payload;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import memoizee from 'memoizee';
|
||||
import { fromPairs, isNumber } from 'lodash';
|
||||
import { isNumber } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
|
@ -837,13 +837,10 @@ export function _conversationMessagesSelector(
|
|||
conversation: ConversationMessageType
|
||||
): TimelinePropsType {
|
||||
const {
|
||||
heightChangeMessageIds,
|
||||
isLoadingMessages,
|
||||
isNearBottom,
|
||||
loadCountdownStart,
|
||||
messageIds,
|
||||
metrics,
|
||||
resetCounter,
|
||||
scrollToMessageId,
|
||||
scrollToMessageCounter,
|
||||
} = conversation;
|
||||
|
@ -860,14 +857,6 @@ export function _conversationMessagesSelector(
|
|||
|
||||
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
|
||||
? messageIds.findIndex(id => id === oldestUnread.id)
|
||||
: undefined;
|
||||
|
@ -880,19 +869,12 @@ export function _conversationMessagesSelector(
|
|||
haveNewest,
|
||||
haveOldest,
|
||||
isLoadingMessages,
|
||||
loadCountdownStart,
|
||||
items,
|
||||
isNearBottom,
|
||||
messageHeightChangeBaton: heightChangeMessageIds,
|
||||
messageHeightChangeIndex:
|
||||
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
||||
? messageHeightChangeIndex
|
||||
: undefined,
|
||||
items,
|
||||
oldestUnreadIndex:
|
||||
isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0
|
||||
? oldestUnreadIndex
|
||||
: undefined,
|
||||
resetCounter,
|
||||
scrollToIndex:
|
||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||
scrollToIndexCounter: scrollToMessageCounter,
|
||||
|
@ -927,8 +909,7 @@ export const getConversationMessagesSelector = createSelector(
|
|||
return {
|
||||
haveNewest: false,
|
||||
haveOldest: false,
|
||||
isLoadingMessages: false,
|
||||
resetCounter: 0,
|
||||
isLoadingMessages: true,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
items: [],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -28,6 +28,7 @@ export type Props = {
|
|||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
now: number;
|
||||
played: boolean;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
|
|
|
@ -5,7 +5,6 @@ import { isEmpty, mapValues, pick } from 'lodash';
|
|||
import type { RefObject } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import memoizee from 'memoizee';
|
||||
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type {
|
||||
|
@ -99,16 +98,6 @@ export type TimelinePropsType = ExternalProps &
|
|||
| 'updateSharedGroups'
|
||||
>;
|
||||
|
||||
const createBoundOnHeightChange = memoizee(
|
||||
(
|
||||
onHeightChange: (messageId: string) => unknown,
|
||||
messageId: string
|
||||
): (() => unknown) => {
|
||||
return () => onHeightChange(messageId);
|
||||
},
|
||||
{ max: 500 }
|
||||
);
|
||||
|
||||
function renderItem({
|
||||
actionProps,
|
||||
containerElementRef,
|
||||
|
@ -117,7 +106,7 @@ function renderItem({
|
|||
isOldestTimelineItem,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
onHeightChange,
|
||||
now,
|
||||
previousMessageId,
|
||||
}: {
|
||||
actionProps: TimelineActionsType;
|
||||
|
@ -127,7 +116,7 @@ function renderItem({
|
|||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
onHeightChange: (messageId: string) => unknown;
|
||||
now: number;
|
||||
previousMessageId: undefined | string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
|
@ -140,7 +129,7 @@ function renderItem({
|
|||
messageId={messageId}
|
||||
previousMessageId={previousMessageId}
|
||||
nextMessageId={nextMessageId}
|
||||
onHeightChange={createBoundOnHeightChange(onHeightChange, messageId)}
|
||||
now={now}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
|
@ -154,14 +143,12 @@ function renderLastSeenIndicator(id: string): JSX.Element {
|
|||
|
||||
function renderHeroRow(
|
||||
id: string,
|
||||
onHeightChange: () => unknown,
|
||||
unblurAvatar: () => void,
|
||||
updateSharedGroups: () => unknown
|
||||
): JSX.Element {
|
||||
return (
|
||||
<SmartHeroRow
|
||||
id={id}
|
||||
onHeightChange={onHeightChange}
|
||||
unblurAvatar={unblurAvatar}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
/>
|
||||
|
@ -306,6 +293,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
'typingContactId',
|
||||
'isGroupV1AndDisabled',
|
||||
]),
|
||||
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||
isIncomingMessageRequest: Boolean(
|
||||
conversation.messageRequestsEnabled &&
|
||||
!conversation.acceptedMessageRequest
|
||||
|
|
|
@ -27,6 +27,7 @@ type ExternalProps = {
|
|||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
previousMessageId: undefined | string;
|
||||
now: number;
|
||||
};
|
||||
|
||||
function renderContact(conversationId: string): JSX.Element {
|
||||
|
@ -45,6 +46,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
messageId,
|
||||
nextMessageId,
|
||||
previousMessageId,
|
||||
now,
|
||||
} = props;
|
||||
|
||||
const messageSelector = getMessageSelector(state);
|
||||
|
@ -66,6 +68,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
item,
|
||||
previousItem,
|
||||
nextItem,
|
||||
now,
|
||||
id: messageId,
|
||||
containerElementRef,
|
||||
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', () => {
|
||||
useFakeTimers();
|
||||
|
||||
it('returns "Now" for times within the last minute, including unexpected times in the future', () => {
|
||||
[
|
||||
Date.now(),
|
||||
moment().subtract(1, 'second'),
|
||||
moment().subtract(59, 'seconds'),
|
||||
moment().add(1, 'minute'),
|
||||
moment().add(1, 'year'),
|
||||
FAKE_NOW,
|
||||
moment(FAKE_NOW).subtract(1, 'second'),
|
||||
moment(FAKE_NOW).subtract(59, 'seconds'),
|
||||
moment(FAKE_NOW).add(1, 'minute'),
|
||||
moment(FAKE_NOW).add(1, 'year'),
|
||||
].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', () => {
|
||||
assert.strictEqual(
|
||||
formatTime(i18n, moment().subtract(1, 'minute')),
|
||||
formatTime(i18n, moment(FAKE_NOW).subtract(1, 'minute'), FAKE_NOW),
|
||||
'1m'
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatTime(i18n, moment().subtract(30, 'minutes')),
|
||||
formatTime(i18n, moment(FAKE_NOW).subtract(30, 'minutes'), FAKE_NOW),
|
||||
'30m'
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatTime(i18n, moment().subtract(59, 'minutes')),
|
||||
formatTime(i18n, moment(FAKE_NOW).subtract(59, 'minutes'), FAKE_NOW),
|
||||
'59m'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns hh:mm-like times for times older than 1 hour from now', () => {
|
||||
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');
|
||||
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');
|
||||
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 { v4 as uuid } from 'uuid';
|
||||
import { times } from 'lodash';
|
||||
import { set } from 'lodash/fp';
|
||||
import { reducer as rootReducer } from '../../../state/reducer';
|
||||
import { noopAction } from '../../../state/ducks/noop';
|
||||
import {
|
||||
|
@ -55,9 +54,8 @@ const {
|
|||
closeContactSpoofingReview,
|
||||
closeMaximumGroupSizeModal,
|
||||
closeRecommendedGroupSizeModal,
|
||||
createGroup,
|
||||
messageSizeChanged,
|
||||
conversationStoppedByMissingVerification,
|
||||
createGroup,
|
||||
openConversationInternal,
|
||||
repairNewestMessage,
|
||||
repairOldestMessage,
|
||||
|
@ -334,13 +332,11 @@ describe('both/state/ducks/conversations', () => {
|
|||
|
||||
function getDefaultConversationMessage(): ConversationMessageType {
|
||||
return {
|
||||
heightChangeMessageIds: [],
|
||||
isLoadingMessages: false,
|
||||
messageIds: [],
|
||||
metrics: {
|
||||
totalUnread: 0,
|
||||
},
|
||||
resetCounter: 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', () => {
|
||||
it('adds to state, removing duplicates', () => {
|
||||
const first = reducer(
|
||||
|
|
|
@ -7662,14 +7662,6 @@
|
|||
"updated": "2021-01-18T22:24:05.937Z",
|
||||
"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",
|
||||
"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
|
||||
// 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';
|
||||
|
||||
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 {
|
||||
if (width > 606) {
|
||||
return WidthBreakpoint.Wide;
|
||||
|
|
|
@ -67,7 +67,7 @@ export function formatDateTimeShort(
|
|||
const diff = now - timestamp;
|
||||
|
||||
if (diff < HOUR || isToday(timestamp)) {
|
||||
return formatTime(i18n, rawTimestamp);
|
||||
return formatTime(i18n, rawTimestamp, now);
|
||||
}
|
||||
|
||||
const m = moment(timestamp);
|
||||
|
@ -102,10 +102,11 @@ export function formatDateTimeLong(
|
|||
|
||||
export function formatTime(
|
||||
i18n: LocalizerType,
|
||||
rawTimestamp: RawTimestamp
|
||||
rawTimestamp: RawTimestamp,
|
||||
now: RawTimestamp
|
||||
): string {
|
||||
const timestamp = rawTimestamp.valueOf();
|
||||
const diff = Date.now() - timestamp;
|
||||
const diff = now.valueOf() - timestamp;
|
||||
|
||||
if (diff < MINUTE) {
|
||||
return i18n('justNow');
|
||||
|
|
Loading…
Reference in a new issue