mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Virtualize Messages List - only render what's visible
This commit is contained in:
@@ -23,7 +23,7 @@ const contact = {
|
||||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -31,8 +31,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -41,8 +41,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -51,8 +51,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -62,7 +62,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -89,7 +89,7 @@ const contact = {
|
||||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -97,8 +97,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -107,7 +107,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -131,15 +131,15 @@ const contact = {
|
||||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
i18n={util.i18n}
|
||||
timestamp={Date.now()}
|
||||
contact={contact}/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -147,7 +147,7 @@ const contact = {
|
||||
i18n={util.i18n}
|
||||
timestamp={Date.now()}
|
||||
contact={contact}/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -171,8 +171,8 @@ const contact = {
|
||||
},
|
||||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} type="group" ios={util.ios}>
|
||||
<li>
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
conversationType="group"
|
||||
@@ -183,8 +183,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -195,8 +195,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -207,7 +207,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -231,7 +231,7 @@ const contact = {
|
||||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -239,8 +239,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -249,8 +249,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -259,8 +259,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -270,7 +270,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -292,7 +292,7 @@ const contact = {
|
||||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -300,8 +300,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -310,8 +310,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -320,8 +320,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -331,7 +331,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -356,7 +356,7 @@ const contact = {
|
||||
signalAccount: '+12025551000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -364,8 +364,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -374,8 +374,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -384,8 +384,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -395,7 +395,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -414,7 +414,7 @@ const contact = {
|
||||
],
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -422,8 +422,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -432,8 +432,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -442,8 +442,8 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -453,7 +453,7 @@ const contact = {
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -462,7 +462,7 @@ const contact = {
|
||||
```jsx
|
||||
const contact = {};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -470,8 +470,8 @@ const contact = {};
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -480,8 +480,8 @@ const contact = {};
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
@@ -490,8 +490,8 @@ const contact = {};
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
@@ -501,7 +501,7 @@ const contact = {};
|
||||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -542,7 +542,7 @@ const contactWithoutAccount = {
|
||||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -551,8 +551,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -562,8 +562,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -572,8 +572,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -583,8 +583,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -594,8 +594,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -606,8 +606,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -617,8 +617,8 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
@@ -629,6 +629,6 @@ const contactWithoutAccount = {
|
||||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -13,8 +13,8 @@
|
||||
expirationLength={10 * 1000}
|
||||
expirationTimestamp={Date.now() + 10 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="outgoing"
|
||||
status="delivered"
|
||||
@@ -25,8 +25,8 @@
|
||||
expirationLength={30 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -37,8 +37,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -49,7 +49,7 @@
|
||||
expirationLength={5 * 60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -67,8 +67,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -79,8 +79,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -90,8 +90,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -102,8 +102,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -113,8 +113,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -125,8 +125,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -136,8 +136,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -148,8 +148,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -159,8 +159,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now()}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -171,8 +171,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now()}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -182,8 +182,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 120 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -194,8 +194,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 120 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
@@ -205,8 +205,8 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() - 20 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
@@ -217,6 +217,6 @@
|
||||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() - 20 * 1000}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
|
||||
overlayText?: string;
|
||||
|
||||
isSelected?: boolean;
|
||||
noBorder?: boolean;
|
||||
noBackground?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
@@ -51,6 +52,7 @@ export class Image extends React.Component<Props> {
|
||||
darkOverlay,
|
||||
height,
|
||||
i18n,
|
||||
isSelected,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
@@ -118,7 +120,7 @@ export class Image extends React.Component<Props> {
|
||||
alt={i18n('imageCaptionIconAlt')}
|
||||
/>
|
||||
) : null}
|
||||
{!noBorder ? (
|
||||
{!noBorder || isSelected ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__border-overlay',
|
||||
@@ -128,7 +130,8 @@ export class Image extends React.Component<Props> {
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
|
||||
softCorners ? 'module-image--soft-corners' : null,
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null,
|
||||
isSelected ? 'module-image__border-overlay--selected' : null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -21,6 +21,7 @@ interface Props {
|
||||
withContentBelow?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
isSticker?: boolean;
|
||||
isSelected?: boolean;
|
||||
stickerSize?: number;
|
||||
|
||||
i18n: LocalizerType;
|
||||
@@ -37,6 +38,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
bottomOverlay,
|
||||
i18n,
|
||||
isSticker,
|
||||
isSelected,
|
||||
stickerSize,
|
||||
onError,
|
||||
onClick,
|
||||
@@ -83,6 +85,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
curveBottomRight={curveBottomRight}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
isSelected={isSelected}
|
||||
height={finalHeight}
|
||||
width={finalWidth}
|
||||
url={getUrl(attachments[0])}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ interface Trigger {
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
const STICKER_SIZE = 128;
|
||||
const SELECTED_TIMEOUT = 1000;
|
||||
|
||||
interface LinkPreviewType {
|
||||
title: string;
|
||||
@@ -54,6 +55,8 @@ export type PropsData = {
|
||||
text?: string;
|
||||
textPending?: boolean;
|
||||
isSticker: boolean;
|
||||
isSelected: boolean;
|
||||
isSelectedCounter: number;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
timestamp: number;
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||
@@ -97,6 +100,8 @@ type PropsHousekeeping = {
|
||||
};
|
||||
|
||||
export type PropsActions = {
|
||||
clearSelectedMessage: () => unknown;
|
||||
|
||||
replyToMessage: (id: string) => void;
|
||||
retrySend: (id: string) => void;
|
||||
deleteMessage: (id: string) => void;
|
||||
@@ -120,11 +125,10 @@ export type PropsActions = {
|
||||
displayTapToViewMessage: (messageId: string) => unknown;
|
||||
|
||||
openLink: (url: string) => void;
|
||||
scrollToMessage: (
|
||||
scrollToQuotedMessage: (
|
||||
options: {
|
||||
author: string;
|
||||
sentAt: number;
|
||||
referencedMessageNotFound: boolean;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
@@ -135,6 +139,9 @@ interface State {
|
||||
expiring: boolean;
|
||||
expired: boolean;
|
||||
imageBroken: boolean;
|
||||
|
||||
isSelected: boolean;
|
||||
prevSelectedCounter: number;
|
||||
}
|
||||
|
||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||
@@ -148,6 +155,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
public menuTriggerRef: Trigger | undefined;
|
||||
public expirationCheckInterval: any;
|
||||
public expiredTimeout: any;
|
||||
public selectedTimeout: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -160,10 +168,30 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
expiring: false,
|
||||
expired: false,
|
||||
imageBroken: false,
|
||||
|
||||
isSelected: props.isSelected,
|
||||
prevSelectedCounter: props.isSelectedCounter,
|
||||
};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||
if (
|
||||
props.isSelected &&
|
||||
props.isSelectedCounter !== state.prevSelectedCounter
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isSelected: props.isSelected,
|
||||
prevSelectedCounter: props.isSelectedCounter,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.startSelectedTimer();
|
||||
|
||||
const { expirationLength } = this.props;
|
||||
if (!expirationLength) {
|
||||
return;
|
||||
@@ -180,6 +208,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.selectedTimeout) {
|
||||
clearInterval(this.selectedTimeout);
|
||||
}
|
||||
if (this.expirationCheckInterval) {
|
||||
clearInterval(this.expirationCheckInterval);
|
||||
}
|
||||
@@ -189,9 +220,26 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.startSelectedTimer();
|
||||
|
||||
this.checkExpired();
|
||||
}
|
||||
|
||||
public startSelectedTimer() {
|
||||
const { isSelected } = this.state;
|
||||
if (!isSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedTimeout) {
|
||||
this.selectedTimeout = setTimeout(() => {
|
||||
this.selectedTimeout = undefined;
|
||||
this.setState({ isSelected: false });
|
||||
this.props.clearSelectedMessage();
|
||||
}, SELECTED_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
public checkExpired() {
|
||||
const now = Date.now();
|
||||
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
||||
@@ -379,7 +427,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
isSticker,
|
||||
text,
|
||||
} = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
const { imageBroken, isSelected } = this.state;
|
||||
|
||||
if (!attachments || !attachments[0]) {
|
||||
return null;
|
||||
@@ -422,6 +470,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
withContentAbove={isSticker || withContentAbove}
|
||||
withContentBelow={isSticker || withContentBelow}
|
||||
isSticker={isSticker}
|
||||
isSelected={isSticker && isSelected}
|
||||
stickerSize={STICKER_SIZE}
|
||||
bottomOverlay={bottomOverlay}
|
||||
i18n={i18n}
|
||||
@@ -622,7 +671,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
disableScroll,
|
||||
i18n,
|
||||
quote,
|
||||
scrollToMessage,
|
||||
scrollToQuotedMessage,
|
||||
} = this.props;
|
||||
|
||||
if (!quote) {
|
||||
@@ -633,15 +682,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
conversationType === 'group' && direction === 'incoming';
|
||||
const quoteColor =
|
||||
direction === 'incoming' ? authorColor : quote.authorColor;
|
||||
|
||||
const { referencedMessageNotFound } = quote;
|
||||
|
||||
const clickHandler = disableScroll
|
||||
? undefined
|
||||
: () => {
|
||||
scrollToMessage({
|
||||
scrollToQuotedMessage({
|
||||
author: quote.authorId,
|
||||
sentAt: quote.sentAt,
|
||||
referencedMessageNotFound,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1195,12 +1243,24 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public renderSelectionHighlight() {
|
||||
const { isSticker } = this.props;
|
||||
const { isSelected } = this.state;
|
||||
|
||||
if (!isSelected || isSticker) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <div className="module-message__container__selection" />;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line cyclomatic-complexity
|
||||
public render() {
|
||||
const {
|
||||
authorPhoneNumber,
|
||||
authorColor,
|
||||
attachments,
|
||||
conversationType,
|
||||
direction,
|
||||
displayTapToViewMessage,
|
||||
id,
|
||||
@@ -1211,6 +1271,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
timestamp,
|
||||
} = this.props;
|
||||
const { expired, expiring, imageBroken } = this.state;
|
||||
|
||||
const isAttachmentPending = this.isAttachmentPending();
|
||||
const isButton = isTapToView && !isTapToViewExpired && !isAttachmentPending;
|
||||
|
||||
@@ -1236,7 +1297,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
className={classNames(
|
||||
'module-message',
|
||||
`module-message--${direction}`,
|
||||
expiring ? 'module-message--expired' : null
|
||||
expiring ? 'module-message--expired' : null,
|
||||
conversationType === 'group' ? 'module-message--group' : null
|
||||
)}
|
||||
>
|
||||
{this.renderError(direction === 'incoming')}
|
||||
@@ -1271,6 +1333,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
>
|
||||
{this.renderAuthor()}
|
||||
{this.renderContents()}
|
||||
{this.renderSelectionHighlight()}
|
||||
{this.renderAvatar()}
|
||||
</div>
|
||||
{this.renderError(direction === 'outgoing')}
|
||||
|
||||
+173
-173
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
// import classNames from 'classnames';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
### None
|
||||
### No new messages
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={0}
|
||||
withNewMessages={false}
|
||||
conversationId="id-1"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
@@ -11,28 +11,15 @@
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### One
|
||||
### With new messages
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={1}
|
||||
withNewMessages={true}
|
||||
conversationId="id-2"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### More than one
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={2}
|
||||
conversationId="id-3"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
withNewMessages: boolean;
|
||||
conversationId: string;
|
||||
|
||||
scrollDown: (conversationId: string) => void;
|
||||
@@ -14,21 +14,17 @@ type Props = {
|
||||
|
||||
export class ScrollDownButton extends React.Component<Props> {
|
||||
public render() {
|
||||
const { conversationId, count, i18n, scrollDown } = this.props;
|
||||
|
||||
let altText = i18n('scrollDown');
|
||||
if (count > 1) {
|
||||
altText = i18n('messagesBelow');
|
||||
} else if (count === 1) {
|
||||
altText = i18n('messageBelow');
|
||||
}
|
||||
const { conversationId, withNewMessages, i18n, scrollDown } = this.props;
|
||||
const altText = withNewMessages
|
||||
? i18n('messagesBelow')
|
||||
: i18n('scrollDown');
|
||||
|
||||
return (
|
||||
<div className="module-scroll-down">
|
||||
<button
|
||||
className={classNames(
|
||||
'module-scroll-down__button',
|
||||
count > 0 ? 'module-scroll-down__button--new-messages' : null
|
||||
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
||||
)}
|
||||
onClick={() => {
|
||||
scrollDown(conversationId);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
```javascript
|
||||
const itemLookup = {
|
||||
## With oldest and newest
|
||||
|
||||
```jsx
|
||||
window.itemLookup = {
|
||||
'id-1': {
|
||||
type: 'message',
|
||||
data: {
|
||||
@@ -15,12 +17,24 @@ const itemLookup = {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-2',
|
||||
conversationType: 'group',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorColor: 'green',
|
||||
text: 'Hello there from the new world! http://somewhere.com',
|
||||
},
|
||||
},
|
||||
'id-2.5': {
|
||||
type: 'unsupportedMessage',
|
||||
data: {
|
||||
id: 'id-2.5',
|
||||
canProcessNow: false,
|
||||
contact: {
|
||||
phoneNumber: '(202) 555-1000',
|
||||
profileName: 'Mr. Pig',
|
||||
},
|
||||
},
|
||||
},
|
||||
'id-3': {
|
||||
type: 'message',
|
||||
data: {
|
||||
@@ -155,25 +169,186 @@ const itemLookup = {
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
window.actions = {
|
||||
// For messages
|
||||
downloadAttachment: options => console.log('onDownload', options),
|
||||
replyToitem: id => console.log('onReply', id),
|
||||
showMessageDetail: id => console.log('onShowDetail', id),
|
||||
deleteMessage: id => console.log('onDelete', id),
|
||||
downloadNewVersion: () => console.log('downloadNewVersion'),
|
||||
|
||||
// For Timeline
|
||||
clearChangedMessages: (...args) => console.log('clearChangedMessages', args),
|
||||
setLoadCountdownStart: (...args) =>
|
||||
console.log('setLoadCountdownStart', args),
|
||||
|
||||
loadAndScroll: (...args) => console.log('loadAndScroll', args),
|
||||
loadOlderMessages: (...args) => console.log('loadOlderMessages', args),
|
||||
loadNewerMessages: (...args) => console.log('loadNewerMessages', args),
|
||||
loadNewestMessages: (...args) => console.log('loadNewestMessages', args),
|
||||
markMessageRead: (...args) => console.log('markMessageRead', args),
|
||||
};
|
||||
|
||||
const items = util._.keys(itemLookup);
|
||||
const renderItem = id => {
|
||||
const item = itemLookup[id];
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: true,
|
||||
haveOldest: true,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: null,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: null,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
|
||||
// Because we can't use ...item syntax
|
||||
return React.createElement(
|
||||
TimelineItem,
|
||||
util._.merge({ item, i18n: util.i18n }, actions)
|
||||
);
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline items={items} renderItem={renderItem} i18n={util.i18n} />
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
## With last seen indicator
|
||||
|
||||
```
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: true,
|
||||
haveOldest: true,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: 2,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: null,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 2,
|
||||
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
renderLastSeenIndicator: () => (
|
||||
<LastSeenIndicator count={2} i18n={util.i18n} />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
## With target index = 0
|
||||
|
||||
```
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: true,
|
||||
haveOldest: true,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: null,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: 0,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
## With typing indicator
|
||||
|
||||
```
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: true,
|
||||
haveOldest: true,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: null,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: null,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
|
||||
typingContact: true,
|
||||
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
renderTypingBubble: () => (
|
||||
<TypingBubble color="red" conversationType="direct" phoneNumber="+18005552222" i18n={util.i18n} />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
## Without newest message
|
||||
|
||||
```
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: false,
|
||||
haveOldest: true,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: null,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: 3,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
## Without oldest message
|
||||
|
||||
```
|
||||
const props = {
|
||||
id: 'conversationId-1',
|
||||
haveNewest: true,
|
||||
haveOldest: false,
|
||||
isLoadingMessages: false,
|
||||
items: util._.keys(window.itemLookup),
|
||||
messagesHaveChanged: false,
|
||||
oldestUnreadIndex: null,
|
||||
resetCounter: 0,
|
||||
scrollToIndex: null,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
renderLoadingRow: () => (
|
||||
<TimelineLoadingRow state="idle" />
|
||||
),
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { debounce, isNumber } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
AutoSizer,
|
||||
@@ -6,24 +7,64 @@ import {
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import { ScrollDownButton } from './ScrollDownButton';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { PropsActions as MessageActionsType } from './Message';
|
||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||
|
||||
type PropsData = {
|
||||
const AT_BOTTOM_THRESHOLD = 1;
|
||||
const NEAR_BOTTOM_THRESHOLD = 15;
|
||||
const AT_TOP_THRESHOLD = 10;
|
||||
const LOAD_MORE_THRESHOLD = 30;
|
||||
const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
|
||||
export const LOAD_COUNTDOWN = 2 * 1000;
|
||||
|
||||
export type PropsDataType = {
|
||||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
isLoadingMessages: boolean;
|
||||
items: Array<string>;
|
||||
|
||||
renderItem: (id: string) => JSX.Element;
|
||||
loadCountdownStart?: number;
|
||||
messageHeightChanges: boolean;
|
||||
oldestUnreadIndex?: number;
|
||||
resetCounter: number;
|
||||
scrollToIndex?: number;
|
||||
scrollToIndexCounter: number;
|
||||
totalUnread: number;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
type PropsHousekeepingType = {
|
||||
id: string;
|
||||
unreadCount?: number;
|
||||
typingContact?: Object;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
renderItem: (id: string, actions: Object) => JSX.Element;
|
||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||
renderLoadingRow: (id: string) => JSX.Element;
|
||||
renderTypingBubble: (id: string) => JSX.Element;
|
||||
};
|
||||
|
||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
||||
type PropsActionsType = {
|
||||
clearChangedMessages: (conversationId: string) => unknown;
|
||||
setLoadCountdownStart: (
|
||||
conversationId: string,
|
||||
loadCountdownStart?: number
|
||||
) => unknown;
|
||||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
loadAndScroll: (messageId: string) => unknown;
|
||||
loadOlderMessages: (messageId: string) => unknown;
|
||||
loadNewerMessages: (messageId: string) => unknown;
|
||||
loadNewestMessages: (messageId: string) => unknown;
|
||||
markMessageRead: (messageId: string) => unknown;
|
||||
} & MessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
type Props = PropsDataType & PropsHousekeepingType & PropsActionsType;
|
||||
|
||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||
type RowRendererParamsType = {
|
||||
@@ -34,37 +75,407 @@ type RowRendererParamsType = {
|
||||
parent: Object;
|
||||
style: Object;
|
||||
};
|
||||
type OnScrollParamsType = {
|
||||
scrollTop: number;
|
||||
clientHeight: number;
|
||||
scrollHeight: number;
|
||||
|
||||
export class Timeline extends React.PureComponent<Props> {
|
||||
clientWidth: number;
|
||||
scrollWidth?: number;
|
||||
scrollLeft?: number;
|
||||
scrollToColumn?: number;
|
||||
_hasScrolledToColumnTarget?: boolean;
|
||||
scrollToRow?: number;
|
||||
_hasScrolledToRowTarget?: boolean;
|
||||
};
|
||||
|
||||
type VisibleRowsType = {
|
||||
newest?: {
|
||||
id: string;
|
||||
offsetTop: number;
|
||||
row: number;
|
||||
};
|
||||
oldest?: {
|
||||
id: string;
|
||||
offsetTop: number;
|
||||
row: number;
|
||||
};
|
||||
};
|
||||
|
||||
type State = {
|
||||
atBottom: boolean;
|
||||
atTop: boolean;
|
||||
oneTimeScrollRow?: number;
|
||||
|
||||
prevPropScrollToIndex?: number;
|
||||
prevPropScrollToIndexCounter?: number;
|
||||
propScrollToIndex?: number;
|
||||
|
||||
shouldShowScrollDownButton: boolean;
|
||||
areUnreadBelowCurrentPosition: boolean;
|
||||
};
|
||||
|
||||
export class Timeline extends React.PureComponent<Props, State> {
|
||||
public cellSizeCache = new CellMeasurerCache({
|
||||
defaultHeight: 85,
|
||||
defaultHeight: 64,
|
||||
fixedWidth: true,
|
||||
});
|
||||
public mostRecentWidth = 0;
|
||||
public mostRecentHeight = 0;
|
||||
public offsetFromBottom: number | undefined = 0;
|
||||
public resizeAllFlag = false;
|
||||
public listRef = React.createRef<any>();
|
||||
public visibleRows: VisibleRowsType | undefined;
|
||||
public loadCountdownTimeout: any;
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (this.resizeAllFlag) {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
this.recomputeRowHeights();
|
||||
} else if (this.props.items !== prevProps.items) {
|
||||
const index = prevProps.items.length;
|
||||
this.cellSizeCache.clear(index, 0);
|
||||
this.recomputeRowHeights(index);
|
||||
}
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { scrollToIndex } = this.props;
|
||||
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||
|
||||
this.state = {
|
||||
atBottom: true,
|
||||
atTop: false,
|
||||
oneTimeScrollRow,
|
||||
propScrollToIndex: scrollToIndex,
|
||||
prevPropScrollToIndex: scrollToIndex,
|
||||
shouldShowScrollDownButton: false,
|
||||
areUnreadBelowCurrentPosition: false,
|
||||
};
|
||||
}
|
||||
|
||||
public resizeAll = () => {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||
if (
|
||||
isNumber(props.scrollToIndex) &&
|
||||
(props.scrollToIndex !== state.prevPropScrollToIndex ||
|
||||
props.scrollToIndexCounter !== state.prevPropScrollToIndexCounter)
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
propScrollToIndex: props.scrollToIndex,
|
||||
prevPropScrollToIndex: props.scrollToIndex,
|
||||
prevPropScrollToIndexCounter: props.scrollToIndexCounter,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public getList = () => {
|
||||
if (!this.listRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { current } = this.listRef;
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
public recomputeRowHeights = (index?: number) => {
|
||||
if (this.listRef && this.listRef) {
|
||||
this.listRef.current.recomputeRowHeights(index);
|
||||
public getGrid = () => {
|
||||
const list = this.getList();
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
return list.Grid;
|
||||
};
|
||||
|
||||
public getScrollContainer = () => {
|
||||
const grid = this.getGrid();
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
return grid._scrollingContainer as HTMLDivElement;
|
||||
};
|
||||
|
||||
public scrollToRow = (row: number) => {
|
||||
const list = this.getList();
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.scrollToRow(row);
|
||||
};
|
||||
|
||||
public recomputeRowHeights = (row?: number) => {
|
||||
const list = this.getList();
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.recomputeRowHeights(row);
|
||||
};
|
||||
|
||||
public onHeightOnlyChange = () => {
|
||||
const grid = this.getGrid();
|
||||
const scrollContainer = this.getScrollContainer();
|
||||
if (!grid || !scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNumber(this.offsetFromBottom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientHeight, scrollHeight, scrollTop } = scrollContainer;
|
||||
const newOffsetFromBottom = Math.max(
|
||||
0,
|
||||
scrollHeight - clientHeight - scrollTop
|
||||
);
|
||||
const delta = newOffsetFromBottom - this.offsetFromBottom;
|
||||
|
||||
grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta });
|
||||
};
|
||||
|
||||
public resizeAll = () => {
|
||||
this.offsetFromBottom = undefined;
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
|
||||
const rowCount = this.getRowCount();
|
||||
this.recomputeRowHeights(rowCount - 1);
|
||||
};
|
||||
|
||||
public onScroll = (data: OnScrollParamsType) => {
|
||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||
// re-measures to get us where we want to go.
|
||||
if (
|
||||
isNumber(data.scrollToRow) &&
|
||||
data.scrollToRow >= 0 &&
|
||||
!data._hasScrolledToRowTarget
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sometimes react-virtualized ends up with some incorrect math - we've scrolled below
|
||||
// what should be possible. In this case, we leave everything the same and ask
|
||||
// react-virtualized to try again. Without this, we'll set atBottom to true and
|
||||
// pop the user back down to the bottom.
|
||||
const { clientHeight, scrollHeight, scrollTop } = data;
|
||||
if (scrollTop + clientHeight > scrollHeight) {
|
||||
this.resizeAll();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateScrollMetrics(data);
|
||||
this.updateWithVisibleRows();
|
||||
};
|
||||
|
||||
// tslint:disable-next-line member-ordering
|
||||
public updateScrollMetrics = debounce(
|
||||
(data: OnScrollParamsType) => {
|
||||
const { clientHeight, clientWidth, scrollHeight, scrollTop } = data;
|
||||
|
||||
if (clientHeight <= 0 || scrollHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
haveNewest,
|
||||
haveOldest,
|
||||
id,
|
||||
setIsNearBottom,
|
||||
setLoadCountdownStart,
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.mostRecentHeight &&
|
||||
clientHeight !== this.mostRecentHeight &&
|
||||
this.mostRecentWidth &&
|
||||
clientWidth === this.mostRecentWidth
|
||||
) {
|
||||
this.onHeightOnlyChange();
|
||||
}
|
||||
|
||||
// If we've scrolled, we want to reset these
|
||||
const oneTimeScrollRow = undefined;
|
||||
const propScrollToIndex = undefined;
|
||||
|
||||
this.offsetFromBottom = Math.max(
|
||||
0,
|
||||
scrollHeight - clientHeight - scrollTop
|
||||
);
|
||||
|
||||
const atBottom =
|
||||
haveNewest && this.offsetFromBottom <= AT_BOTTOM_THRESHOLD;
|
||||
const isNearBottom =
|
||||
haveNewest && this.offsetFromBottom <= NEAR_BOTTOM_THRESHOLD;
|
||||
const atTop = scrollTop <= AT_TOP_THRESHOLD;
|
||||
const loadCountdownStart = atTop && !haveOldest ? Date.now() : undefined;
|
||||
|
||||
if (this.loadCountdownTimeout) {
|
||||
clearTimeout(this.loadCountdownTimeout);
|
||||
this.loadCountdownTimeout = null;
|
||||
}
|
||||
if (isNumber(loadCountdownStart)) {
|
||||
this.loadCountdownTimeout = setTimeout(
|
||||
this.loadOlderMessages,
|
||||
LOAD_COUNTDOWN
|
||||
);
|
||||
}
|
||||
|
||||
if (loadCountdownStart !== this.props.loadCountdownStart) {
|
||||
setLoadCountdownStart(id, loadCountdownStart);
|
||||
}
|
||||
|
||||
setIsNearBottom(id, isNearBottom);
|
||||
|
||||
this.setState({
|
||||
atBottom,
|
||||
atTop,
|
||||
oneTimeScrollRow,
|
||||
propScrollToIndex,
|
||||
});
|
||||
},
|
||||
50,
|
||||
{ maxWait: 50 }
|
||||
);
|
||||
|
||||
public updateVisibleRows = () => {
|
||||
let newest;
|
||||
let oldest;
|
||||
|
||||
const scrollContainer = this.getScrollContainer();
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollContainer.clientHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleTop = scrollContainer.scrollTop;
|
||||
const visibleBottom = visibleTop + scrollContainer.clientHeight;
|
||||
|
||||
const innerScrollContainer = scrollContainer.children[0];
|
||||
if (!innerScrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { children } = innerScrollContainer;
|
||||
|
||||
for (let i = children.length - 1; i >= 0; i -= 1) {
|
||||
const child = children[i] as HTMLDivElement;
|
||||
const { id, offsetTop, offsetHeight } = child;
|
||||
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bottom = offsetTop + offsetHeight;
|
||||
|
||||
if (bottom - AT_BOTTOM_THRESHOLD <= visibleBottom) {
|
||||
const row = parseInt(child.getAttribute('data-row') || '-1', 10);
|
||||
newest = { offsetTop, row, id };
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const max = children.length;
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const child = children[i] as HTMLDivElement;
|
||||
const { offsetTop, id } = child;
|
||||
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offsetTop + AT_TOP_THRESHOLD >= visibleTop) {
|
||||
const row = parseInt(child.getAttribute('data-row') || '-1', 10);
|
||||
oldest = { offsetTop, row, id };
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.visibleRows = { newest, oldest };
|
||||
};
|
||||
|
||||
// tslint:disable-next-line member-ordering cyclomatic-complexity
|
||||
public updateWithVisibleRows = debounce(
|
||||
() => {
|
||||
const {
|
||||
unreadCount,
|
||||
haveNewest,
|
||||
isLoadingMessages,
|
||||
items,
|
||||
loadNewerMessages,
|
||||
markMessageRead,
|
||||
} = this.props;
|
||||
|
||||
if (!items || items.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateVisibleRows();
|
||||
if (!this.visibleRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { newest } = this.visibleRows;
|
||||
if (!newest || !newest.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
markMessageRead(newest.id);
|
||||
|
||||
const rowCount = this.getRowCount();
|
||||
|
||||
const lastId = items[items.length - 1];
|
||||
if (
|
||||
!isLoadingMessages &&
|
||||
!haveNewest &&
|
||||
newest.row > rowCount - LOAD_MORE_THRESHOLD
|
||||
) {
|
||||
loadNewerMessages(lastId);
|
||||
}
|
||||
|
||||
const lastIndex = items.length - 1;
|
||||
const lastItemRow = this.fromItemIndexToRow(lastIndex);
|
||||
const areUnreadBelowCurrentPosition = Boolean(
|
||||
isNumber(unreadCount) &&
|
||||
unreadCount > 0 &&
|
||||
(!haveNewest || newest.row < lastItemRow)
|
||||
);
|
||||
|
||||
const shouldShowScrollDownButton = Boolean(
|
||||
!haveNewest ||
|
||||
areUnreadBelowCurrentPosition ||
|
||||
newest.row < rowCount - SCROLL_DOWN_BUTTON_THRESHOLD
|
||||
);
|
||||
|
||||
this.setState({
|
||||
shouldShowScrollDownButton,
|
||||
areUnreadBelowCurrentPosition,
|
||||
});
|
||||
},
|
||||
500,
|
||||
{ maxWait: 500 }
|
||||
);
|
||||
|
||||
public loadOlderMessages = () => {
|
||||
const {
|
||||
haveOldest,
|
||||
isLoadingMessages,
|
||||
items,
|
||||
loadOlderMessages,
|
||||
} = this.props;
|
||||
|
||||
if (this.loadCountdownTimeout) {
|
||||
clearTimeout(this.loadCountdownTimeout);
|
||||
this.loadCountdownTimeout = null;
|
||||
}
|
||||
|
||||
if (isLoadingMessages || haveOldest || !items || items.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldestId = items[0];
|
||||
loadOlderMessages(oldestId);
|
||||
};
|
||||
|
||||
public rowRenderer = ({
|
||||
@@ -73,8 +484,62 @@ export class Timeline extends React.PureComponent<Props> {
|
||||
parent,
|
||||
style,
|
||||
}: RowRendererParamsType) => {
|
||||
const { items, renderItem } = this.props;
|
||||
const messageId = items[index];
|
||||
const {
|
||||
id,
|
||||
haveOldest,
|
||||
items,
|
||||
renderItem,
|
||||
renderLoadingRow,
|
||||
renderLastSeenIndicator,
|
||||
renderTypingBubble,
|
||||
} = this.props;
|
||||
|
||||
const row = index;
|
||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||
const typingBubbleRow = this.getTypingBubbleRow();
|
||||
let rowContents;
|
||||
|
||||
if (!haveOldest && row === 0) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={style}>
|
||||
{renderLoadingRow(id)}
|
||||
</div>
|
||||
);
|
||||
} else if (oldestUnreadRow === row) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={style}>
|
||||
{renderLastSeenIndicator(id)}
|
||||
</div>
|
||||
);
|
||||
} else if (typingBubbleRow === row) {
|
||||
rowContents = (
|
||||
<div
|
||||
data-row={row}
|
||||
className="module-timeline__message-container"
|
||||
style={style}
|
||||
>
|
||||
{renderTypingBubble(id)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const itemIndex = this.fromRowToItemIndex(row);
|
||||
if (typeof itemIndex !== 'number') {
|
||||
throw new Error(
|
||||
`Attempted to render item with undefined index - row ${row}`
|
||||
);
|
||||
}
|
||||
const messageId = items[itemIndex];
|
||||
rowContents = (
|
||||
<div
|
||||
id={messageId}
|
||||
data-row={row}
|
||||
className="module-timeline__message-container"
|
||||
style={style}
|
||||
>
|
||||
{renderItem(messageId, this.props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
@@ -85,16 +550,277 @@ export class Timeline extends React.PureComponent<Props> {
|
||||
rowIndex={index}
|
||||
width={this.mostRecentWidth}
|
||||
>
|
||||
<div className="module-timeline__message-container" style={style}>
|
||||
{renderItem(messageId)}
|
||||
</div>
|
||||
{rowContents}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
public fromItemIndexToRow(index: number) {
|
||||
const { haveOldest, oldestUnreadIndex } = this.props;
|
||||
|
||||
let addition = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
addition += 1;
|
||||
}
|
||||
|
||||
if (isNumber(oldestUnreadIndex) && index >= oldestUnreadIndex) {
|
||||
addition += 1;
|
||||
}
|
||||
|
||||
return index + addition;
|
||||
}
|
||||
|
||||
public getRowCount() {
|
||||
const { haveOldest, oldestUnreadIndex, typingContact } = this.props;
|
||||
const { items } = this.props;
|
||||
|
||||
if (!items || items.length < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let extraRows = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
extraRows += 1;
|
||||
}
|
||||
|
||||
if (isNumber(oldestUnreadIndex)) {
|
||||
extraRows += 1;
|
||||
}
|
||||
|
||||
if (typingContact) {
|
||||
extraRows += 1;
|
||||
}
|
||||
|
||||
return items.length + extraRows;
|
||||
}
|
||||
|
||||
public fromRowToItemIndex(row: number): number | undefined {
|
||||
const { haveOldest, items } = this.props;
|
||||
|
||||
let subtraction = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
subtraction += 1;
|
||||
}
|
||||
|
||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||
if (isNumber(oldestUnreadRow) && row > oldestUnreadRow) {
|
||||
subtraction += 1;
|
||||
}
|
||||
|
||||
const index = row - subtraction;
|
||||
if (index < 0 || index >= items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
public getLastSeenIndicatorRow() {
|
||||
const { oldestUnreadIndex } = this.props;
|
||||
if (!isNumber(oldestUnreadIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.fromItemIndexToRow(oldestUnreadIndex) - 1;
|
||||
}
|
||||
|
||||
public getTypingBubbleRow() {
|
||||
const { items } = this.props;
|
||||
if (!items || items.length < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const last = items.length - 1;
|
||||
|
||||
return this.fromItemIndexToRow(last) + 1;
|
||||
}
|
||||
|
||||
public onScrollToMessage = (messageId: string) => {
|
||||
const { isLoadingMessages, items, loadAndScroll } = this.props;
|
||||
const index = items.findIndex(item => item === messageId);
|
||||
|
||||
if (index >= 0) {
|
||||
const row = this.fromItemIndexToRow(index);
|
||||
this.setState({
|
||||
oneTimeScrollRow: row,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isLoadingMessages) {
|
||||
loadAndScroll(messageId);
|
||||
}
|
||||
};
|
||||
|
||||
public scrollToBottom = () => {
|
||||
this.setState({
|
||||
propScrollToIndex: undefined,
|
||||
oneTimeScrollRow: undefined,
|
||||
atBottom: true,
|
||||
});
|
||||
};
|
||||
|
||||
public onClickScrollDownButton = () => {
|
||||
const {
|
||||
haveNewest,
|
||||
isLoadingMessages,
|
||||
items,
|
||||
loadNewestMessages,
|
||||
} = this.props;
|
||||
const lastId = items[items.length - 1];
|
||||
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
||||
|
||||
if (!this.visibleRows) {
|
||||
if (haveNewest) {
|
||||
this.scrollToBottom();
|
||||
} else if (!isLoadingMessages) {
|
||||
loadNewestMessages(lastId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { newest } = this.visibleRows;
|
||||
|
||||
if (
|
||||
newest &&
|
||||
isNumber(lastSeenIndicatorRow) &&
|
||||
newest.row < lastSeenIndicatorRow
|
||||
) {
|
||||
this.setState({
|
||||
oneTimeScrollRow: lastSeenIndicatorRow,
|
||||
});
|
||||
} else if (haveNewest) {
|
||||
this.scrollToBottom();
|
||||
} else if (!isLoadingMessages) {
|
||||
loadNewestMessages(lastId);
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
const {
|
||||
id,
|
||||
clearChangedMessages,
|
||||
items,
|
||||
messageHeightChanges,
|
||||
oldestUnreadIndex,
|
||||
resetCounter,
|
||||
scrollToIndex,
|
||||
typingContact,
|
||||
} = this.props;
|
||||
|
||||
// There are a number of situations which can necessitate that we drop our row height
|
||||
// cache and start over. It can cause the scroll position to do weird things, so we
|
||||
// try to minimize those situations. In some cases we could reset a smaller set
|
||||
// of cached row data, but we currently don't have an API for that. We'd need to
|
||||
// create it.
|
||||
if (
|
||||
!prevProps.items ||
|
||||
prevProps.items.length === 0 ||
|
||||
resetCounter !== prevProps.resetCounter
|
||||
) {
|
||||
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||
this.setState({
|
||||
oneTimeScrollRow,
|
||||
atBottom: true,
|
||||
propScrollToIndex: scrollToIndex,
|
||||
prevPropScrollToIndex: scrollToIndex,
|
||||
});
|
||||
|
||||
if (prevProps.items && prevProps.items.length > 0) {
|
||||
this.resizeAll();
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (!typingContact && prevProps.typingContact) {
|
||||
this.resizeAll();
|
||||
} else if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) {
|
||||
this.resizeAll();
|
||||
} else if (
|
||||
items &&
|
||||
items.length > 0 &&
|
||||
prevProps.items &&
|
||||
prevProps.items.length > 0 &&
|
||||
items !== prevProps.items
|
||||
) {
|
||||
if (this.state.atTop) {
|
||||
const oldFirstIndex = 0;
|
||||
const oldFirstId = prevProps.items[oldFirstIndex];
|
||||
|
||||
const newIndex = items.findIndex(item => item === oldFirstId);
|
||||
if (newIndex < 0) {
|
||||
this.resizeAll();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = this.fromItemIndexToRow(newIndex);
|
||||
this.resizeAll();
|
||||
this.setState({ oneTimeScrollRow: newRow });
|
||||
} else {
|
||||
const oldLastIndex = prevProps.items.length - 1;
|
||||
const oldLastId = prevProps.items[oldLastIndex];
|
||||
|
||||
const newIndex = items.findIndex(item => item === oldLastId);
|
||||
if (newIndex < 0) {
|
||||
this.resizeAll();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const indexDelta = newIndex - oldLastIndex;
|
||||
|
||||
// If we've just added to the end of the list, then the index of the last id's
|
||||
// index won't have changed, and we can rely on List's detection that items is
|
||||
// different for the necessary re-render.
|
||||
if (indexDelta !== 0) {
|
||||
this.resizeAll();
|
||||
}
|
||||
}
|
||||
} else if (messageHeightChanges) {
|
||||
this.resizeAll();
|
||||
clearChangedMessages(id);
|
||||
} else if (this.resizeAllFlag) {
|
||||
this.resizeAll();
|
||||
}
|
||||
}
|
||||
|
||||
public getScrollTarget = () => {
|
||||
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
|
||||
|
||||
const rowCount = this.getRowCount();
|
||||
const targetMessage = isNumber(propScrollToIndex)
|
||||
? this.fromItemIndexToRow(propScrollToIndex)
|
||||
: undefined;
|
||||
const scrollToBottom = atBottom ? rowCount - 1 : undefined;
|
||||
|
||||
if (isNumber(targetMessage)) {
|
||||
return targetMessage;
|
||||
}
|
||||
|
||||
if (isNumber(oneTimeScrollRow)) {
|
||||
return oneTimeScrollRow;
|
||||
}
|
||||
|
||||
return scrollToBottom;
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { i18n, id, items } = this.props;
|
||||
const {
|
||||
shouldShowScrollDownButton,
|
||||
areUnreadBelowCurrentPosition,
|
||||
} = this.state;
|
||||
|
||||
if (!items || items.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowCount = this.getRowCount();
|
||||
const scrollToIndex = this.getScrollTarget();
|
||||
|
||||
return (
|
||||
<div className="module-timeline">
|
||||
<AutoSizer>
|
||||
@@ -103,26 +829,41 @@ export class Timeline extends React.PureComponent<Props> {
|
||||
this.resizeAllFlag = true;
|
||||
|
||||
setTimeout(this.resizeAll, 0);
|
||||
} else if (
|
||||
this.mostRecentHeight &&
|
||||
this.mostRecentHeight !== height
|
||||
) {
|
||||
setTimeout(this.onHeightOnlyChange, 0);
|
||||
}
|
||||
|
||||
this.mostRecentWidth = width;
|
||||
this.mostRecentHeight = height;
|
||||
|
||||
return (
|
||||
<List
|
||||
deferredMeasurementCache={this.cellSizeCache}
|
||||
height={height}
|
||||
// This also registers us with parent InfiniteLoader
|
||||
// onRowsRendered={onRowsRendered}
|
||||
overscanRowCount={0}
|
||||
onScroll={this.onScroll as any}
|
||||
overscanRowCount={10}
|
||||
ref={this.listRef}
|
||||
rowCount={items.length}
|
||||
rowCount={rowCount}
|
||||
rowHeight={this.cellSizeCache.rowHeight}
|
||||
rowRenderer={this.rowRenderer}
|
||||
scrollToAlignment="start"
|
||||
scrollToIndex={scrollToIndex}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
{shouldShowScrollDownButton ? (
|
||||
<ScrollDownButton
|
||||
conversationId={id}
|
||||
withNewMessages={areUnreadBelowCurrentPosition}
|
||||
scrollDown={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
### A plain message
|
||||
|
||||
```jsx
|
||||
const item = {} < TimelineItem;
|
||||
const item = {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-1',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorPhoneNumber: '(202) 555-2001',
|
||||
authorColor: 'green',
|
||||
text: '🔥',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### A notification
|
||||
|
||||
```jsx
|
||||
const item = {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### Unknown type
|
||||
|
||||
```jsx
|
||||
const item = {
|
||||
type: 'random',
|
||||
data: {
|
||||
somethin: 'somethin',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### Missing itme
|
||||
|
||||
```jsx
|
||||
<TimelineItem item={null} i18n={util.i18n} />
|
||||
```
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
PropsActions as MessageActionsType,
|
||||
PropsData as MessageProps,
|
||||
} from './Message';
|
||||
import {
|
||||
PropsActions as UnsupportedMessageActionsType,
|
||||
PropsData as UnsupportedMessageProps,
|
||||
UnsupportedMessage,
|
||||
} from './UnsupportedMessage';
|
||||
import {
|
||||
PropsData as TimerNotificationProps,
|
||||
TimerNotification,
|
||||
@@ -29,6 +34,10 @@ type MessageType = {
|
||||
type: 'message';
|
||||
data: MessageProps;
|
||||
};
|
||||
type UnsupportedMessageType = {
|
||||
type: 'unsupportedMessage';
|
||||
data: UnsupportedMessageProps;
|
||||
};
|
||||
type TimerNotificationType = {
|
||||
type: 'timerNotification';
|
||||
data: TimerNotificationProps;
|
||||
@@ -49,22 +58,26 @@ type ResetSessionNotificationType = {
|
||||
type: 'resetSessionNotification';
|
||||
data: null;
|
||||
};
|
||||
export type TimelineItemType =
|
||||
| MessageType
|
||||
| UnsupportedMessageType
|
||||
| TimerNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
| VerificationNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| GroupNotificationType;
|
||||
|
||||
type PropsData = {
|
||||
item:
|
||||
| MessageType
|
||||
| TimerNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
| VerificationNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| GroupNotificationType;
|
||||
item?: TimelineItemType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
||||
type PropsActions = MessageActionsType &
|
||||
UnsupportedMessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
@@ -73,12 +86,18 @@ export class TimelineItem extends React.PureComponent<Props> {
|
||||
const { item, i18n } = this.props;
|
||||
|
||||
if (!item) {
|
||||
throw new Error('TimelineItem: Item was not provided!');
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn('TimelineItem: item provided was falsey');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.type === 'message') {
|
||||
return <Message {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'unsupportedMessage') {
|
||||
return <UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'timerNotification') {
|
||||
return <TimerNotification {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
### Idle
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<TimelineLoadingRow state="idle" />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### Countdown
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<TimelineLoadingRow
|
||||
state="countdown"
|
||||
duration={30000}
|
||||
expiresAt={Date.now() + 20000}
|
||||
onComplete={() => console.log('onComplete')}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<TimelineLoadingRow state="loading" />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import { Countdown } from '../Countdown';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
export type STATE_ENUM = 'idle' | 'countdown' | 'loading';
|
||||
|
||||
type Props = {
|
||||
state: STATE_ENUM;
|
||||
duration?: number;
|
||||
expiresAt?: number;
|
||||
onComplete?: () => unknown;
|
||||
};
|
||||
|
||||
const FAKE_DURATION = 1000;
|
||||
|
||||
export class TimelineLoadingRow extends React.PureComponent<Props> {
|
||||
public renderContents() {
|
||||
const { state, duration, expiresAt, onComplete } = this.props;
|
||||
|
||||
if (state === 'idle') {
|
||||
const fakeExpiresAt = Date.now() - FAKE_DURATION;
|
||||
|
||||
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
|
||||
} else if (
|
||||
state === 'countdown' &&
|
||||
isNumber(duration) &&
|
||||
isNumber(expiresAt)
|
||||
) {
|
||||
return (
|
||||
<Countdown
|
||||
duration={duration}
|
||||
expiresAt={expiresAt}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Spinner size="24" svgSize="small" direction="on-background" />;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="module-timeline-loading-row">{this.renderContents()}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
export type PropsData = {
|
||||
type: 'fromOther' | 'fromMe' | 'fromSync';
|
||||
phoneNumber: string;
|
||||
@@ -63,7 +61,9 @@ export class TimerNotification extends React.Component<Props> {
|
||||
? i18n('disappearingMessagesDisabled')
|
||||
: i18n('timerSetOnSync', [timespan]);
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
console.warn('TimerNotification: unsupported type provided:', type);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ function getDecember1159() {
|
||||
}
|
||||
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -28,8 +28,8 @@ function getDecember1159() {
|
||||
text="500ms ago - all below 1 minute are 'now'"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -38,8 +38,8 @@ function getDecember1159() {
|
||||
text="Five seconds ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -48,8 +48,8 @@ function getDecember1159() {
|
||||
text="30 seconds ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -58,8 +58,8 @@ function getDecember1159() {
|
||||
text="One minute ago - in minutes"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -68,8 +68,8 @@ function getDecember1159() {
|
||||
text="30 minutes ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -78,8 +78,8 @@ function getDecember1159() {
|
||||
text="45 minutes ago (used to round up to 1 hour with moment)"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -88,8 +88,8 @@ function getDecember1159() {
|
||||
text="One hour ago - in hours"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -98,8 +98,8 @@ function getDecember1159() {
|
||||
text="12:01am today"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -108,8 +108,8 @@ function getDecember1159() {
|
||||
text="11:59pm yesterday - adds day name"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -118,8 +118,8 @@ function getDecember1159() {
|
||||
text="24 hours ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -128,8 +128,8 @@ function getDecember1159() {
|
||||
text="Two days ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -138,8 +138,8 @@ function getDecember1159() {
|
||||
text="Seven days ago - adds month"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -148,8 +148,8 @@ function getDecember1159() {
|
||||
text="Thirty days ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -158,8 +158,8 @@ function getDecember1159() {
|
||||
text="January 1st at 12:01am"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -168,8 +168,8 @@ function getDecember1159() {
|
||||
text="December 31st at 11:59pm - adds year"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
@@ -178,6 +178,6 @@ function getDecember1159() {
|
||||
text="One year ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
@@ -15,24 +15,24 @@
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble
|
||||
color="purple"
|
||||
authorName="First Last"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble
|
||||
avatarPath={util.gifObjectUrl}
|
||||
color="blue"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
@@ -9,14 +9,14 @@ import { LocalizerType } from '../../types/Util';
|
||||
interface Props {
|
||||
avatarPath?: string;
|
||||
color: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
phoneNumber: string;
|
||||
profileName: string;
|
||||
conversationType: string;
|
||||
profileName?: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class TypingBubble extends React.Component<Props> {
|
||||
export class TypingBubble extends React.PureComponent<Props> {
|
||||
public renderAvatar() {
|
||||
const {
|
||||
avatarPath,
|
||||
@@ -49,10 +49,17 @@ export class TypingBubble extends React.Component<Props> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
const { i18n, color, conversationType } = this.props;
|
||||
const isGroup = conversationType === 'group';
|
||||
|
||||
return (
|
||||
<div className={classNames('module-message', 'module-message--incoming')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message',
|
||||
'module-message--incoming',
|
||||
isGroup ? 'module-message--group' : null
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
|
||||
@@ -18,14 +18,14 @@ export type PropsData = {
|
||||
contact: ContactType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export type PropsActions = {
|
||||
downloadNewVersion: () => unknown;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
export class UnsupportedMessage extends React.Component<Props> {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function renderAvatar({
|
||||
contact: ContactType;
|
||||
i18n: LocalizerType;
|
||||
size: number;
|
||||
direction?: string;
|
||||
direction?: 'outgoing' | 'incoming';
|
||||
}) {
|
||||
const { avatar } = contact;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user