diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index b3516b8918..f379ea40f2 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -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", diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 94a4caff76..45e644e8b4 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -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", diff --git a/pkg/interface/src/logic/api/chat.ts b/pkg/interface/src/logic/api/chat.ts index bd16529cfc..ad6e5029d0 100644 --- a/pkg/interface/src/logic/api/chat.ts +++ b/pkg/interface/src/logic/api/chat.ts @@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi { * 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({ diff --git a/pkg/interface/src/logic/reducers/chat-update.ts b/pkg/interface/src/logic/reducers/chat-update.ts index 2921750381..531d97453a 100644 --- a/pkg/interface/src/logic/reducers/chat-update.ts +++ b/pkg/interface/src/logic/reducers/chat-update.ts @@ -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; @@ -49,8 +50,11 @@ export default class ChatReducer { 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 + ); } } diff --git a/pkg/interface/src/types/chat-update.ts b/pkg/interface/src/types/chat-update.ts index 63b23eee11..6d03048f4e 100644 --- a/pkg/interface/src/types/chat-update.ts +++ b/pkg/interface/src/types/chat-update.ts @@ -73,6 +73,10 @@ export interface Envelope { letter: Letter; } +export type IMessage = Envelope & { + pending?: boolean +}; + interface LetterText { text: string; } diff --git a/pkg/interface/src/views/apps/chat/app.tsx b/pkg/interface/src/views/apps/chat/app.tsx index 9ce2a580c7..f7a91ca306 100644 --- a/pkg/interface/src/views/apps/chat/app.tsx +++ b/pkg/interface/src/views/apps/chat/app.tsx @@ -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 { 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 ( - + - - - ); - }} + chatInitialized={chatInitialized} + hideAvatars={hideAvatars} + hideNicknames={hideNicknames} + remoteContentPolicy={remoteContentPolicy} + {...props} + /> + + ); + }} /> { !(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 (
- + + {...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 ( {messageElem} @@ -75,12 +76,12 @@ export const ChatMessage = (props) => { } else if (dayBreak) { return ( - {messageElem}

{moment(_.get(msg, when)).calendar()}

+ {messageElem}
); } else { diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js b/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js deleted file mode 100644 index 74580f9954..0000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js +++ /dev/null @@ -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 ( -
-
-
- {this.props.children} -
-
- ); - } - - normalScrollContainer() { - return ( -
-
- {this.props.children} -
- ); - } - - 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 - ); - } - } - -} diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx b/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx index ebb9952837..137a4f9c4c 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx +++ b/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx @@ -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 }) => ( +
+
+ +
+
+
+

+ +

+

+

+
+ +
+
+); -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 { + private unreadReference: React.RefObject; + private virtualList: React.RefObject; + 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 ( - - - - { messages.map((msg, i) => ( - 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} - /> - )) - } - + unreadCount={unreadCount} + unreadMsg={unreadMsg} + dismissUnread={this.dismissUnread} + onClick={this.scrollToUnread} + /> + + + {messages.length ? Math.abs(velocity) > 2000, + exit: velocity => Math.abs(velocity) < 200, + change: (_velocity, _range) => {}, + placeholder: this.state.initialized ? Placeholder : () =>
+ }} + 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 ; + } + return + }} + /> : null}
); } diff --git a/pkg/interface/src/views/apps/chat/components/lib/message.js b/pkg/interface/src/views/apps/chat/components/lib/message.js index f4753bdb57..6174afd0bb 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/message.js +++ b/pkg/interface/src/views/apps/chat/components/lib/message.js @@ -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) ) : (

{timestamp}

- +
) @@ -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 ( -
- -
-
-

- { - writeText(props.msg.author); - }} - title={`~${props.msg.author}`} - > - {name} - -

-

{timestamp}

-

- {datestamp} -

-
- -
-
- ); + 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 ( +
+ +
+
+

+ { + writeText(props.msg.author); + }} + title={`~${props.msg.author}`} + > + {name} + +

+

{timestamp}

+

+ {datestamp} +

+
+ +
+
+ ); +} + diff --git a/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js b/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js index 2724d23c9b..954ed33578 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js +++ b/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js @@ -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" }> -

+

{unreadCount} new messages since{' '} {datestamp && ( <> diff --git a/pkg/interface/src/views/apps/groups/components/lib/contact-sidebar.tsx b/pkg/interface/src/views/apps/groups/components/lib/contact-sidebar.tsx index f337efee5d..60ab9a0035 100644 --- a/pkg/interface/src/views/apps/groups/components/lib/contact-sidebar.tsx +++ b/pkg/interface/src/views/apps/groups/components/lib/contact-sidebar.tsx @@ -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 ComponentChannels {shareSheet}

Members

- - {({ index, 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 - }
)} -
+ } + />
diff --git a/pkg/interface/src/views/components/Group.tsx b/pkg/interface/src/views/components/Group.tsx index e364c0aeb4..1e635cb8f1 100644 --- a/pkg/interface/src/views/components/Group.tsx +++ b/pkg/interface/src/views/components/Group.tsx @@ -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)}
Members
- - {({ index, style }) =>
{memberElements[index]}
} -
+
{memberElements[index]}
} + />
); } else if (isOembed && remoteContentPolicy.oembedShown) { - this.loadOembed(); + if (!this.state.embed) { + this.loadOembed(); + } + return ( {renderUrl ? this.wrapInLink(this.state.embed && this.state.embed.title ? this.state.embed.title : url) : null}