mirror of
https://github.com/urbit/shrub.git
synced 2024-12-20 17:32:11 +03:00
Merge pull request #3357 from tylershuster/virtualized-lists
interface: changed virtual scroll implementation and added to chat
This commit is contained in:
commit
2ac1f9bd8f
22
pkg/interface/package-lock.json
generated
22
pkg/interface/package-lock.json
generated
@ -6383,11 +6383,6 @@
|
||||
"p-is-promise": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
@ -7994,13 +7989,13 @@
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
||||
"integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg=="
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==",
|
||||
"react-virtuoso": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-0.20.0.tgz",
|
||||
"integrity": "sha512-h+U6t/+m91AzfUe6bBfaacdLLJl1y8v7CfcXwPgQ/Dic+vNlgQmi6cIKTq18zuF+kI8Q7QN0ojIeqPHWbU8TZA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
@ -8281,6 +8276,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||
|
@ -32,7 +32,7 @@
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-oembed-container": "^1.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-window": "^1.8.5",
|
||||
"react-virtuoso": "^0.20.0",
|
||||
"remark-disable-tokenizers": "^1.0.24",
|
||||
"style-loader": "^1.2.1",
|
||||
"styled-components": "^5.1.0",
|
||||
|
@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi<StoreState> {
|
||||
* Fetch backlog
|
||||
*/
|
||||
fetchMessages(start: number, end: number, path: Path) {
|
||||
fetch(`/chat-view/paginate/${start}/${end}${path}`)
|
||||
return fetch(`/chat-view/paginate/${start}/${end}${path}`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.store.handleEvent({
|
||||
|
@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../../store/type';
|
||||
import { StoreState } from '~/logic/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { ChatUpdate } from '~/types/chat-update';
|
||||
import { ChatHookUpdate } from '~/types/chat-hook-update';
|
||||
import { Envelope } from "~/types/chat-update";
|
||||
|
||||
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
|
||||
|
||||
@ -49,8 +50,11 @@ export default class ChatReducer<S extends ChatState> {
|
||||
messages(json: ChatUpdate, state: S) {
|
||||
const data = _.get(json, 'messages', false);
|
||||
if (data) {
|
||||
state.inbox[data.path].envelopes =
|
||||
state.inbox[data.path].envelopes.concat(data.envelopes);
|
||||
state.inbox[data.path].envelopes = _.unionBy(
|
||||
state.inbox[data.path].envelopes,
|
||||
data.envelopes,
|
||||
(envelope: Envelope) => envelope.uid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,10 @@ export interface Envelope {
|
||||
letter: Letter;
|
||||
}
|
||||
|
||||
export type IMessage = Envelope & {
|
||||
pending?: boolean
|
||||
};
|
||||
|
||||
interface LetterText {
|
||||
text: string;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import './css/custom.css';
|
||||
import { Skeleton } from './components/skeleton';
|
||||
import { Sidebar } from './components/sidebar';
|
||||
import { ChatScreen } from './components/chat';
|
||||
import { MemberScreen } from './components/member';
|
||||
import { SettingsScreen } from './components/settings';
|
||||
import { NewScreen } from './components/new';
|
||||
import { JoinScreen } from './components/join';
|
||||
@ -227,57 +226,57 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
envelopes: []
|
||||
};
|
||||
|
||||
let roomContacts = {};
|
||||
const associatedGroup =
|
||||
station in associations['chat'] &&
|
||||
'group-path' in associations.chat[station]
|
||||
? associations.chat[station]['group-path']
|
||||
: '';
|
||||
let roomContacts = {};
|
||||
const associatedGroup =
|
||||
station in associations['chat'] &&
|
||||
'group-path' in associations.chat[station]
|
||||
? associations.chat[station]['group-path']
|
||||
: '';
|
||||
|
||||
if (associations.chat[station] && associatedGroup in contacts) {
|
||||
roomContacts = contacts[associatedGroup];
|
||||
}
|
||||
if (associations.chat[station] && associatedGroup in contacts) {
|
||||
roomContacts = contacts[associatedGroup];
|
||||
}
|
||||
|
||||
const association =
|
||||
station in associations['chat'] ? associations.chat[station] : {};
|
||||
const association =
|
||||
station in associations['chat'] ? associations.chat[station] : {};
|
||||
|
||||
const group = groups[association['group-path']] || groupBunts.group();
|
||||
const group = groups[association['group-path']] || groupBunts.group();
|
||||
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
sidebarHideOnMobile={true}
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
sidebarHideOnMobile={true}
|
||||
popout={popout}
|
||||
sidebarShown={sidebarShown}
|
||||
sidebar={renderChannelSidebar(props, station)}
|
||||
>
|
||||
<ChatScreen
|
||||
chatSynced={chatSynced || {}}
|
||||
station={station}
|
||||
association={association}
|
||||
api={api}
|
||||
read={mailbox.config.read}
|
||||
mailboxSize={mailbox.config.length}
|
||||
envelopes={mailbox.envelopes}
|
||||
inbox={inbox}
|
||||
contacts={roomContacts}
|
||||
group={group}
|
||||
pendingMessages={pendingMessages}
|
||||
s3={s3}
|
||||
popout={popout}
|
||||
sidebarShown={sidebarShown}
|
||||
sidebar={renderChannelSidebar(props, station)}
|
||||
>
|
||||
<ChatScreen
|
||||
chatSynced={chatSynced || {}}
|
||||
station={station}
|
||||
association={association}
|
||||
api={api}
|
||||
read={mailbox.config.read}
|
||||
length={mailbox.config.length}
|
||||
envelopes={mailbox.envelopes}
|
||||
inbox={inbox}
|
||||
contacts={roomContacts}
|
||||
group={group}
|
||||
pendingMessages={pendingMessages}
|
||||
s3={s3}
|
||||
popout={popout}
|
||||
sidebarShown={sidebarShown}
|
||||
chatInitialized={chatInitialized}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
chatInitialized={chatInitialized}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
|
@ -8,7 +8,6 @@ import { ChatHeader } from './lib/chat-header';
|
||||
import { ChatInput } from "./lib/chat-input";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
import { ChatHookUpdate } from "~/types/chat-hook-update";
|
||||
import ChatApi from "~/logic/api/chat";
|
||||
import { Inbox, Envelope } from "~/types/chat-update";
|
||||
import { Contacts } from "~/types/contact-update";
|
||||
import { Path, Patp } from "~/types/noun";
|
||||
@ -27,7 +26,7 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
association: Association;
|
||||
api: GlobalApi;
|
||||
read: number;
|
||||
length: number;
|
||||
mailboxSize: number;
|
||||
inbox: Inbox;
|
||||
contacts: Contacts;
|
||||
group: Group;
|
||||
@ -99,42 +98,23 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
!(props.station in props.chatSynced) &&
|
||||
props.envelopes.length > 0;
|
||||
|
||||
const unreadCount = props.length - props.read;
|
||||
const unreadCount = props.mailboxSize - props.read;
|
||||
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={props.station}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
||||
<ChatHeader
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
api={props.api}
|
||||
group={props.group}
|
||||
association={props.association}
|
||||
station={props.station}
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout} />
|
||||
<ChatHeader {...props} />
|
||||
<ChatWindow
|
||||
history={props.history}
|
||||
isChatMissing={isChatMissing}
|
||||
isChatLoading={isChatLoading}
|
||||
isChatUnsynced={isChatUnsynced}
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={unreadMsg}
|
||||
pendingMessages={pendingMessages}
|
||||
messages={props.envelopes}
|
||||
length={props.length}
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
stationPendingMessages={pendingMessages}
|
||||
ship={props.match.params.ship}
|
||||
station={props.station}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
{...props} />
|
||||
<ChatInput
|
||||
api={props.api}
|
||||
numMsgs={lastMsgNum}
|
||||
|
@ -2,23 +2,23 @@ import React, { PureComponent, Fragment } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
import { Message } from "./message";
|
||||
|
||||
type IMessage = Envelope & { pending?: boolean };
|
||||
|
||||
import { Envelope } from "~/types/chat-update";
|
||||
import _ from "lodash";
|
||||
|
||||
export const ChatMessage = (props) => {
|
||||
const {
|
||||
msg,
|
||||
previousMsg,
|
||||
nextMsg,
|
||||
isLastUnread,
|
||||
isFirstUnread,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy
|
||||
remoteContentPolicy,
|
||||
className = ''
|
||||
} = props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
@ -48,10 +48,11 @@ export const ChatMessage = (props) => {
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.isLastUnread) {
|
||||
if (isFirstUnread) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
@ -75,12 +76,12 @@ export const ChatMessage = (props) => {
|
||||
} else if (dayBreak) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div
|
||||
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
|
||||
>
|
||||
<p>{moment(_.get(msg, when)).calendar()}</p>
|
||||
</div>
|
||||
{messageElem}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
|
@ -1,143 +0,0 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import { scrollIsAtTop, scrollIsAtBottom } from "~/logic/lib/util";
|
||||
|
||||
// Restore chat position on FF when new messages come in
|
||||
const recalculateScrollTop = (lastScrollHeight, scrollContainer) => {
|
||||
if (!scrollContainer || !lastScrollHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
if (scrollContainer.scrollTop !== 0 ||
|
||||
scrollContainer.scrollTop === newScrollTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
};
|
||||
|
||||
|
||||
export class ChatScrollContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// only for FF
|
||||
this.state = {
|
||||
lastScrollHeight: null
|
||||
};
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
|
||||
this.containerDidScroll = this.containerDidScroll.bind(this);
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
this.scrollRef = React.createRef();
|
||||
}
|
||||
|
||||
containerDidScroll(e) {
|
||||
const { props } = this;
|
||||
if (scrollIsAtTop(e.target)) {
|
||||
// Save scroll position for FF
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
this.setState({
|
||||
lastScrollHeight: e.target.scrollHeight,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isAtTop) {
|
||||
props.scrollIsAtTop();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = true;
|
||||
} else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) {
|
||||
if (!this.isAtBottom) {
|
||||
props.scrollIsAtBottom();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
} else {
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = false;
|
||||
this.isTriggeredScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Replace with just the "not Firefox" implementation
|
||||
// when Firefox #1042151 is patched.
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
return this.firefoxScrollContainer();
|
||||
} else {
|
||||
return this.normalScrollContainer();
|
||||
}
|
||||
}
|
||||
|
||||
firefoxScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-y-scroll h-100"
|
||||
onScroll={this.containerDidScroll}
|
||||
ref={this.containerRef}>
|
||||
<div
|
||||
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
||||
style={{ resize: "vertical" }}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
normalScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex " +
|
||||
"flex-column-reverse relative"
|
||||
}
|
||||
style={{ height: "100%", resize: "vertical" }}
|
||||
onScroll={this.containerDidScroll}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current) {
|
||||
this.scrollRef.current.scrollIntoView(false);
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToReference(ref) {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current && ref.current) {
|
||||
ref.current.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,54 +1,140 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Virtuoso as VirtualList, VirtuosoMethods } from 'react-virtuoso';
|
||||
|
||||
import { ChatMessage } from './chat-message';
|
||||
import { ChatScrollContainer } from "./chat-scroll-container";
|
||||
import { UnreadNotice } from "./unread-notice";
|
||||
import { ResubscribeElement } from "./resubscribe-element";
|
||||
import { BacklogElement } from "./backlog-element";
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { Patp, Path } from "~/types/noun";
|
||||
import { Contacts } from "~/types/contact-update";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import { Group } from "~/types/group-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import _ from "lodash";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
import { ListRange } from "react-virtuoso/dist/engines/scrollSeekEngine";
|
||||
|
||||
|
||||
const MAX_BACKLOG_SIZE = 1000;
|
||||
const DEFAULT_BACKLOG_SIZE = 200;
|
||||
const PAGE_SIZE = 50;
|
||||
const INITIAL_LOAD = 20;
|
||||
const DEFAULT_BACKLOG_SIZE = 200;
|
||||
const IDLE_THRESHOLD = 3;
|
||||
|
||||
const Placeholder = ({ height, index, className = '', style = {}, ...props }) => (
|
||||
<div className={`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${className}`} style={{ height, ...style }} {...props}>
|
||||
<div className="fl pr3 v-top bg-white bg-gray0-d">
|
||||
<span
|
||||
className="db bg-gray2 bg-white-d"
|
||||
style={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "50%",
|
||||
visibility: (index % 5 == 0) ? "initial" : "hidden",
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
<div className="hide-child" style={{paddingTop: "6px", visibility: (index % 5 == 0) ? "initial" : "hidden" }}>
|
||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
||||
<span className="mw5 db"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></span>
|
||||
</p>
|
||||
<p className="v-mid mono f9 gray2 dib"><span className="bg-gray5 bg-gray1-d db w-100 h-100" style={{height: "1em", width: `${(index % 3 + 1) * 3}em`}}></span></p>
|
||||
<p className="v-mid mono f9 ml2 gray2 dib child dn-s"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></p>
|
||||
</div>
|
||||
<span className="bg-gray5 bg-gray1-d db w-100 h-100 db" style={{height: `1em`, width: `${(index % 5) * 20}%`}}></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
export class ChatWindow extends Component {
|
||||
type ChatWindowProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
station: string;
|
||||
}> & {
|
||||
unreadCount: number;
|
||||
envelopes: Envelope[];
|
||||
isChatMissing: boolean;
|
||||
isChatLoading: boolean;
|
||||
isChatUnsynced: boolean;
|
||||
unreadMsg: Envelope | false;
|
||||
stationPendingMessages: IMessage[];
|
||||
mailboxSize: number;
|
||||
contacts: Contacts;
|
||||
association: Association;
|
||||
group: Group;
|
||||
ship: Patp;
|
||||
station: any;
|
||||
api: GlobalApi;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
}
|
||||
|
||||
interface ChatWindowState {
|
||||
fetchPending: boolean;
|
||||
idle: boolean;
|
||||
range: ListRange;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
|
||||
private unreadReference: React.RefObject<Component>;
|
||||
private virtualList: React.RefObject<VirtuosoMethods>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
numPages: 1,
|
||||
};
|
||||
|
||||
this.hasAskedForMessages = false;
|
||||
this.state = {
|
||||
fetchPending: false,
|
||||
idle: (this.initialIndex() < props.mailboxSize - IDLE_THRESHOLD) ? true : false,
|
||||
range: { startIndex: 0, endIndex: 0},
|
||||
initialized: false
|
||||
};
|
||||
|
||||
this.dismissUnread = this.dismissUnread.bind(this);
|
||||
this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this);
|
||||
this.scrollIsAtTop = this.scrollIsAtTop.bind(this);
|
||||
this.initialIndex = this.initialIndex.bind(this);
|
||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||
|
||||
this.scrollReference = React.createRef();
|
||||
this.unreadReference = React.createRef();
|
||||
this.virtualList = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialFetch();
|
||||
}
|
||||
|
||||
if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
initialIndex() {
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD
|
||||
? 0
|
||||
: unreadCount // otherwise if there are unread messages
|
||||
? mailboxSize - unreadCount - 1 // put the one right before at the top
|
||||
: mailboxSize - 1,
|
||||
0), mailboxSize);
|
||||
}
|
||||
|
||||
initialFetch() {
|
||||
const { props } = this;
|
||||
if (props.messages.length > 0) {
|
||||
const unreadUnloaded = props.unreadCount - props.messages.length;
|
||||
|
||||
if (unreadUnloaded <= MAX_BACKLOG_SIZE &&
|
||||
unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) {
|
||||
this.fetchBacklog(unreadUnloaded + INITIAL_LOAD);
|
||||
} else {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
const { envelopes, mailboxSize, unreadCount } = this.props;
|
||||
if (envelopes.length > 0) {
|
||||
const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE);
|
||||
this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true);
|
||||
const initialIndex = this.initialIndex();
|
||||
if (initialIndex < mailboxSize - IDLE_THRESHOLD) {
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
if (unreadCount !== mailboxSize) {
|
||||
this.virtualList.current?.scrollToIndex({
|
||||
index: initialIndex,
|
||||
align: initialIndex <= 1 ? 'end' : 'start'
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({ initialized: true });
|
||||
}, 500);
|
||||
} else {
|
||||
this.setState({ initialized: true });
|
||||
}
|
||||
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.initialFetch();
|
||||
@ -57,141 +143,156 @@ export class ChatWindow extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
const { isChatMissing, history, envelopes, mailboxSize, unreadCount } = this.props;
|
||||
let { idle } = this.state;
|
||||
|
||||
if (props.isChatMissing) {
|
||||
props.history.push("/~chat");
|
||||
} else if (props.messages.length >= prevProps.messages.length + 10) {
|
||||
this.hasAskedForMessages = false;
|
||||
let numPages = props.unreadCount > 0 ?
|
||||
Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages;
|
||||
if (isChatMissing) {
|
||||
history.push("/~chat");
|
||||
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) {
|
||||
this.setState({ fetchPending: false });
|
||||
}
|
||||
|
||||
if (this.state.numPages === numPages) {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
if (this.state.range.endIndex !== prevState.range.endIndex) {
|
||||
if (this.state.range.endIndex < mailboxSize - IDLE_THRESHOLD) {
|
||||
if (!idle) {
|
||||
idle = true;
|
||||
}
|
||||
} else {
|
||||
this.setState({ numPages }, () => {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
}
|
||||
});
|
||||
} else if (idle) {
|
||||
idle = false;
|
||||
}
|
||||
} else if (
|
||||
state.numPages === 1 &&
|
||||
this.props.unreadCount < INITIAL_LOAD &&
|
||||
this.props.unreadCount > 0
|
||||
) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
this.setState({ idle });
|
||||
}
|
||||
}
|
||||
|
||||
scrollIsAtTop() {
|
||||
const { props, state } = this;
|
||||
this.setState({ numPages: state.numPages + 1 }, () => {
|
||||
if (state.numPages * PAGE_SIZE < props.length) {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollIsAtBottom() {
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
this.dismissUnread();
|
||||
if (!idle && idle !== prevState.idle) {
|
||||
setTimeout(() => {
|
||||
this.virtualList.current?.scrollToIndex(mailboxSize);
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (this.scrollReference.current) {
|
||||
this.scrollReference.current.scrollToBottom();
|
||||
}
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
if (!idle && prevProps.unreadCount !== unreadCount) {
|
||||
this.virtualList.current?.scrollToIndex(mailboxSize);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToUnread() {
|
||||
if (this.scrollReference.current && this.unreadReference.current) {
|
||||
this.scrollReference.current.scrollToReference(this.unreadReference);
|
||||
}
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
this.virtualList.current?.scrollToIndex({
|
||||
index: mailboxSize - unreadCount,
|
||||
align: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
fetchBacklog(size) {
|
||||
const { props } = this;
|
||||
fetchMessages(start, end, force = false) {
|
||||
start = Math.max(start, 0);
|
||||
end = Math.max(end, 0);
|
||||
const { api, mailboxSize, station } = this.props;
|
||||
|
||||
if (
|
||||
props.messages.length >= props.length ||
|
||||
this.hasAskedForMessages ||
|
||||
props.length <= 0
|
||||
(this.state.fetchPending ||
|
||||
mailboxSize <= 0)
|
||||
&& !force
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.chat
|
||||
.fetchMessages(Math.max(mailboxSize - end, 0), Math.min(mailboxSize - start, mailboxSize), station)
|
||||
.finally(() => {
|
||||
this.setState({ fetchPending: false });
|
||||
});
|
||||
|
||||
const start =
|
||||
props.length - props.messages[props.messages.length - 1].number;
|
||||
if (start > 0) {
|
||||
const end = start + size < props.length ? start + size : props.length;
|
||||
props.api.chat.fetchMessages(start + 1, end, props.station);
|
||||
this.hasAskedForMessages = true;
|
||||
}
|
||||
this.setState({ fetchPending: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const sliceLength = Math.min(
|
||||
state.numPages * PAGE_SIZE,
|
||||
props.messages.length + props.pendingMessages.length
|
||||
);
|
||||
const messages =
|
||||
props.pendingMessages
|
||||
.concat(props.messages)
|
||||
.slice(0, sliceLength);
|
||||
const {
|
||||
envelopes,
|
||||
stationPendingMessages,
|
||||
unreadCount,
|
||||
unreadMsg,
|
||||
isChatLoading,
|
||||
isChatUnsynced,
|
||||
api,
|
||||
ship,
|
||||
station,
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
mailboxSize,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
} = this.props;
|
||||
|
||||
const messages: Envelope[] = [];
|
||||
const debouncedFetch = _.debounce(this.fetchMessages, 500).bind(this);
|
||||
|
||||
envelopes
|
||||
.concat(stationPendingMessages)
|
||||
.forEach((message) => {
|
||||
messages[message.number] = message;
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<UnreadNotice
|
||||
unreadCount={props.unreadCount}
|
||||
unreadMsg={props.unreadMsg}
|
||||
dismissUnread={this.dismissUnread} />
|
||||
<ChatScrollContainer
|
||||
ref={this.scrollReference}
|
||||
scrollIsAtBottom={this.scrollIsAtBottom}
|
||||
scrollIsAtTop={this.scrollIsAtTop}>
|
||||
<BacklogElement isChatLoading={props.isChatLoading} />
|
||||
<ResubscribeElement
|
||||
api={props.api}
|
||||
host={props.ship}
|
||||
station={props.station}
|
||||
isChatUnsynced={props.isChatUnsynced}
|
||||
/>
|
||||
{ messages.map((msg, i) => (
|
||||
<ChatMessage
|
||||
key={msg.uid}
|
||||
unreadRef={this.unreadReference}
|
||||
isLastUnread={
|
||||
props.unreadCount > 0 &&
|
||||
i === props.unreadCount - 1 &&
|
||||
state.numPages !== 1
|
||||
}
|
||||
msg={msg}
|
||||
previousMsg={messages[i - 1]}
|
||||
nextMsg={messages[i + 1]}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
contacts={props.contacts}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ChatScrollContainer>
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={unreadMsg}
|
||||
dismissUnread={this.dismissUnread}
|
||||
onClick={this.scrollToUnread}
|
||||
/>
|
||||
<BacklogElement isChatLoading={isChatLoading} />
|
||||
<ResubscribeElement {...{ api, host: ship, station, isChatUnsynced}} />
|
||||
{messages.length ? <VirtualList
|
||||
ref={this.virtualList}
|
||||
style={{height: '100%', width: '100%', visibility: this.state.initialized ? 'initial' : 'hidden'}}
|
||||
totalCount={mailboxSize}
|
||||
followOutput={!this.state.idle}
|
||||
endReached={this.dismissUnread}
|
||||
scrollSeek={{
|
||||
enter: velocity => Math.abs(velocity) > 2000,
|
||||
exit: velocity => Math.abs(velocity) < 200,
|
||||
change: (_velocity, _range) => {},
|
||||
placeholder: this.state.initialized ? Placeholder : () => <div></div>
|
||||
}}
|
||||
startReached={() => debouncedFetch(0, DEFAULT_BACKLOG_SIZE)}
|
||||
overscan={DEFAULT_BACKLOG_SIZE}
|
||||
rangeChanged={(range) => {
|
||||
this.setState({ range });
|
||||
debouncedFetch(range.startIndex - (DEFAULT_BACKLOG_SIZE / 2), range.endIndex + (DEFAULT_BACKLOG_SIZE / 2));
|
||||
}}
|
||||
item={(i) => {
|
||||
const number = i + 1;
|
||||
const msg = messages[number];
|
||||
|
||||
if (!msg) {
|
||||
debouncedFetch(number - DEFAULT_BACKLOG_SIZE, number + DEFAULT_BACKLOG_SIZE);
|
||||
return <Placeholder index={number} height="0px" style={{overflow: 'hidden'}} />;
|
||||
}
|
||||
return <ChatMessage
|
||||
key={number}
|
||||
unreadRef={this.unreadReference}
|
||||
isFirstUnread={
|
||||
unreadCount
|
||||
&& mailboxSize - unreadCount === number
|
||||
}
|
||||
msg={msg}
|
||||
previousMsg={messages[number + 1]}
|
||||
nextMsg={messages[number - 1]}
|
||||
association={association}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
className={number === mailboxSize ? 'pb3' : ''}
|
||||
/>
|
||||
}}
|
||||
/> : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -6,15 +6,21 @@ import moment from 'moment';
|
||||
|
||||
|
||||
export const Message = (props) => {
|
||||
const pending = props.msg.pending ? ' o-40' : '';
|
||||
const {
|
||||
msg,
|
||||
renderSigil,
|
||||
remoteContentPolicy,
|
||||
className = ''
|
||||
} = props;
|
||||
const pending = msg.pending ? ' o-40' : '';
|
||||
const containerClass =
|
||||
props.renderSigil ?
|
||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
||||
'w-100 pr3 cf hide-child flex' + pending;
|
||||
renderSigil
|
||||
? `w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${pending} ${className}`
|
||||
: `w-100 pr3 cf hide-child flex ${pending} ${className}`;
|
||||
|
||||
const timestamp =
|
||||
moment.unix(props.msg.when / 1000).format(
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
moment.unix(msg.when / 1000).format(
|
||||
renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
|
||||
|
||||
@ -24,14 +30,14 @@ export const Message = (props) => {
|
||||
minHeight: 'min-content'
|
||||
}}>
|
||||
{
|
||||
props.renderSigil ? (
|
||||
renderSigil ? (
|
||||
renderWithSigil(props, timestamp)
|
||||
) : (
|
||||
<div className="flex w-100">
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
||||
style={{ flexGrow: 1 }}>
|
||||
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy}/>
|
||||
<MessageContent letter={msg.letter} remoteContentPolicy={remoteContentPolicy}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -41,66 +47,67 @@ export const Message = (props) => {
|
||||
};
|
||||
|
||||
const renderWithSigil = (props, timestamp) => {
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = showNickname
|
||||
? contact.nickname
|
||||
: `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-100">
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
<div className="hide-child" style={paddingTop}>
|
||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
||||
<span
|
||||
className={
|
||||
'mw5 db truncate pointer ' +
|
||||
(showNickname ? '' : 'mono')
|
||||
}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
}}
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</p>
|
||||
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
|
||||
<p className={`v-mid mono f9 ml2 gray2 dib child dn-s`}>
|
||||
{datestamp}
|
||||
</p>
|
||||
</div>
|
||||
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = showNickname
|
||||
? contact.nickname
|
||||
: `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-100">
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
<div className="hide-child" style={paddingTop}>
|
||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
||||
<span
|
||||
className={
|
||||
'mw5 db truncate pointer ' +
|
||||
(showNickname ? '' : 'mono')
|
||||
}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
}}
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</p>
|
||||
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
|
||||
<p className={`v-mid mono f9 ml2 gray2 dib child dn-s`}>
|
||||
{datestamp}
|
||||
</p>
|
||||
</div>
|
||||
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
export const UnreadNotice = (props) => {
|
||||
const { unreadCount, unreadMsg, dismissUnread } = props;
|
||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||
|
||||
if (!unreadMsg || (unreadCount === 0)) {
|
||||
return null;
|
||||
@ -22,7 +22,7 @@ export const UnreadNotice = (props) => {
|
||||
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
|
||||
"pa2 f9 justify-between br1"
|
||||
}>
|
||||
<p className="lh-copy db">
|
||||
<p className="lh-copy db pointer" onClick={onClick}>
|
||||
{unreadCount} new messages since{' '}
|
||||
{datestamp && (
|
||||
<>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { Virtuoso as VirtualList } from 'react-virtuoso';
|
||||
|
||||
import { ContactItem } from './contact-item';
|
||||
import { ShareSheet } from './share-sheet';
|
||||
@ -180,19 +180,17 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
|
||||
>Channels</Link>
|
||||
{shareSheet}
|
||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
|
||||
<List
|
||||
height={this.state.memberboxHeight}
|
||||
<VirtualList
|
||||
style={{ height: this.state.memberboxHeight, width: '100%' }}
|
||||
className="flex-auto"
|
||||
itemCount={contactItems.length + groupItems.length}
|
||||
itemSize={44}
|
||||
width="100%"
|
||||
>
|
||||
{({ index, style }) => (<div style={style}>{
|
||||
index <= (contactItems.length - 1) // If the index is within the length of contact items,
|
||||
totalCount={contactItems.length + groupItems.length}
|
||||
itemHeight={44} // We happen to know this
|
||||
item={
|
||||
(index) => index <= (contactItems.length - 1) // If the index is within the length of contact items,
|
||||
? contactItems[index] // show a contact item
|
||||
: groupItems[index - contactItems.length] // Otherwise show a group item
|
||||
}</div>)}
|
||||
</List>
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import _, { capitalize } from 'lodash';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { Virtuoso as VirtualList } from 'react-virtuoso';
|
||||
|
||||
import { cite, deSig } from '~/logic/lib/util';
|
||||
import { roleForShip, resourceFromPath } from '~/logic/lib/group';
|
||||
@ -334,14 +334,11 @@ export class GroupView extends Component<
|
||||
{'open' in group.policy && this.renderBanned(group.policy)}
|
||||
<div className='flex flex-column'>
|
||||
<div className='f9 gray2 mt6 mb3'>Members</div>
|
||||
<List
|
||||
height={500}
|
||||
itemCount={memberElements.length}
|
||||
itemSize={44}
|
||||
width="100%"
|
||||
>
|
||||
{({ index, style }) => <div key={index} style={style} className='flex flex-column pv3'>{memberElements[index]}</div>}
|
||||
</List>
|
||||
<VirtualList
|
||||
style={{ height: '500px', width: '100%' }}
|
||||
totalCount={memberElements.length}
|
||||
item={(index) => <div key={index} className='flex flex-column pv3'>{memberElements[index]}</div>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spinner
|
||||
|
@ -116,7 +116,10 @@ export default class RemoteContent extends Component<RemoteContentProps, RemoteC
|
||||
</>
|
||||
);
|
||||
} else if (isOembed && remoteContentPolicy.oembedShown) {
|
||||
this.loadOembed();
|
||||
if (!this.state.embed) {
|
||||
this.loadOembed();
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderUrl ? this.wrapInLink(this.state.embed && this.state.embed.title ? this.state.embed.title : url) : null}
|
||||
|
Loading…
Reference in New Issue
Block a user