Merge pull request #4441 from urbit/james/chatmessage

chat: ChatMessage spacing refactor
This commit is contained in:
matildepark 2021-02-19 14:50:06 -05:00 committed by GitHub
commit e9a9863b22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 449 additions and 390 deletions

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines-per-function */
import React, { import React, {
useState, useState,
useEffect, useEffect,
@ -17,7 +18,14 @@ import {
useShowNickname, useShowNickname,
useHovering useHovering
} from '~/logic/lib/util'; } from '~/logic/lib/util';
import { Group, Association, Contacts, Post, Groups, Associations } from '@urbit/api'; import {
Group,
Association,
Contacts,
Post,
Groups,
Associations
} from '~/types';
import TextContent from './content/text'; import TextContent from './content/text';
import CodeContent from './content/code'; import CodeContent from './content/code';
import RemoteContent from '~/views/components/RemoteContent'; import RemoteContent from '~/views/components/RemoteContent';
@ -28,34 +36,47 @@ import Timestamp from '~/views/components/Timestamp';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => ( interface DayBreakProps {
<Row when: string;
flexShrink={0} shimTop?: boolean;
ref={ref} }
color='blue'
alignItems='center'
fontSize='0'
position='absolute'
width='100%'
py='2'
>
<Rule borderColor='blue' display={['none', 'block']} m='0' width='2rem' />
<Text flexShrink='0' display='block' zIndex='2' mx='4' color='blue'>
New messages below
</Text>
<Rule borderColor='blue' flexGrow='1' m='0' />
<Rule style={{ width: 'calc(50% - 48px)' }} borderColor='blue' m='0' />
</Row>
));
export const DayBreak = ({ when }) => ( export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
<Row pb='3' alignItems='center' justifyContent='center' width='100%'> <Row
<Text gray> px={2}
height={5}
mb={2}
justifyContent='center'
alignItems='center'
mt={shimTop ? '-8px' : '0'}
>
<Rule borderColor='lightGray' />
<Text gray flexShrink='0' fontSize={0} px={2}>
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })} {moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
</Text> </Text>
<Rule borderColor='lightGray' />
</Row> </Row>
); );
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
<Row
position='absolute'
ref={ref}
px={2}
mt={2}
height={5}
justifyContent='center'
alignItems='center'
width='100%'
>
<Rule borderColor='lightBlue' />
<Text color='blue' fontSize={0} flexShrink='0' px={2}>
New messages below
</Text>
<Rule borderColor='lightBlue' />
</Row>
));
interface ChatMessageProps { interface ChatMessageProps {
measure(element): void; measure(element): void;
msg: Post; msg: Post;
@ -74,6 +95,7 @@ interface ChatMessageProps {
history: unknown; history: unknown;
api: GlobalApi; api: GlobalApi;
highlighted?: boolean; highlighted?: boolean;
renderSigil?: boolean;
} }
export default class ChatMessage extends Component<ChatMessageProps> { export default class ChatMessage extends Component<ChatMessageProps> {
@ -114,19 +136,26 @@ export default class ChatMessage extends Component<ChatMessageProps> {
associations associations
} = this.props; } = this.props;
const renderSigil = Boolean( let { renderSigil } = this.props;
(nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1
); if (renderSigil === undefined) {
renderSigil = Boolean(
(nextMsg && msg.author !== nextMsg.author) ||
!nextMsg ||
msg.number === 1
);
}
const dayBreak = const dayBreak =
nextMsg && nextMsg &&
new Date(msg['time-sent']).getDate() !== new Date(msg['time-sent']).getDate() !==
new Date(nextMsg['time-sent']).getDate(); new Date(nextMsg['time-sent']).getDate();
const containerClass = `${ const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
renderSigil ? 'cf pl2 lh-copy' : 'items-top cf hide-child'
} ${isPending ? 'o-40' : ''} ${className}`;
const timestamp = moment.unix(msg['time-sent'] / 1000); const timestamp = moment
.unix(msg['time-sent'] / 1000)
.format(renderSigil ? 'h:mm A' : 'h:mm');
const reboundMeasure = (event) => { const reboundMeasure = (event) => {
return measure(this.divRef.current); return measure(this.divRef.current);
@ -157,34 +186,24 @@ export default class ChatMessage extends Component<ChatMessageProps> {
return ( return (
<Box <Box
bg={highlighted ? 'washedBlue' : 'white'}
flexShrink={0}
width='100%'
display='flex'
flexWrap='wrap'
pt={this.props.pt ? this.props.pt : renderSigil ? 3 : 0}
pr={3}
pb={isLastMessage ? 3 : 0}
ref={this.divRef} ref={this.divRef}
pt={renderSigil ? 2 : 0}
pb={2}
className={containerClass} className={containerClass}
style={style} style={style}
mb={1}
position='relative'
> >
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null} {dayBreak && !isLastRead ? (
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
) : null}
{renderSigil ? ( {renderSigil ? (
<MessageWithSigil {...messageProps} /> <>
<MessageAuthor pb={'2px'} {...messageProps} />
<Message pl={5} pr={3} {...messageProps} />
</>
) : ( ) : (
<MessageWithoutSigil {...messageProps} /> <Message pl={5} pr={3} timestampHover {...messageProps} />
)} )}
<Box <Box style={unreadContainerStyle}>
flexShrink={0}
fontSize={0}
position='relative'
width='100%'
overflow='visible'
style={unreadContainerStyle}
>
{isLastRead ? ( {isLastRead ? (
<UnreadMarker <UnreadMarker
dayBreak={dayBreak} dayBreak={dayBreak}
@ -198,40 +217,25 @@ export default class ChatMessage extends Component<ChatMessageProps> {
} }
} }
interface MessageProps { export const MessageAuthor = ({
msg: Post; timestamp,
timestamp: string; contacts,
group: Group; msg,
association: Association; measure,
contacts: Contacts; group,
containerClass: string; api,
isPending: boolean; associations,
style: any; groups,
measure(element): void; scrollWindow,
scrollWindow: HTMLDivElement; ...rest
associations: Associations; }) => {
groups: Groups; const dark = useLocalState((state) => state.dark);
}
export const MessageWithSigil = (props) => { const datestamp = moment
const { .unix(msg['time-sent'] / 1000)
msg, .format(DATESTAMP_FORMAT);
contacts, const contact =
association, `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
associations,
groups,
group,
measure,
api,
history,
scrollWindow,
fontSize
} = props;
const dark = useLocalState(state => state.dark);
const stamp = moment.unix(msg['time-sent'] / 1000);
const contact = `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);
const { hideAvatars } = const { hideAvatars } =
useLocalState(({ hideAvatars }) => useLocalState(({ hideAvatars }) =>
@ -255,7 +259,7 @@ export const MessageWithSigil = (props) => {
const [showOverlay, setShowOverlay] = useState(false); const [showOverlay, setShowOverlay] = useState(false);
const toggleOverlay = () => { const toggleOverlay = () => {
setShowOverlay(value => !value); setShowOverlay((value) => !value);
}; };
const showCopyNotice = () => { const showCopyNotice = () => {
@ -289,17 +293,16 @@ export const MessageWithSigil = (props) => {
padding={2} padding={2}
/> />
); );
return ( return (
<> <Box display='flex' alignItems='center' {...rest}>
<Box <Box
onClick={() => { onClick={() => {
setShowOverlay(true); setShowOverlay(true);
}} }}
className='fl v-top pt1'
height={16} height={16}
pr={3} pr={2}
pl={2} pl={2}
cursor='pointer'
position='relative' position='relative'
> >
{showOverlay && ( {showOverlay && (
@ -328,11 +331,11 @@ export const MessageWithSigil = (props) => {
> >
<Text <Text
fontSize={0} fontSize={0}
mr={3} mr={2}
flexShrink={0} flexShrink={0}
mono={nameMono} mono={nameMono}
fontWeight={nameMono ? '400' : '500'} fontWeight={nameMono ? '400' : '500'}
className={'mw5 db truncate pointer'} className={'pointer'}
onClick={() => { onClick={() => {
writeText(`~${msg.author}`); writeText(`~${msg.author}`);
showCopyNotice(); showCopyNotice();
@ -341,36 +344,25 @@ export const MessageWithSigil = (props) => {
> >
{displayName} {displayName}
</Text> </Text>
<Timestamp stamp={stamp} fontSize={0}/> <Text flexShrink={0} fontSize={0} gray>
{timestamp}
</Text>
<Text
flexShrink={0}
fontSize={0}
gray
ml={2}
display={['none', hovering ? 'block' : 'none']}
>
{datestamp}
</Text>
</Box> </Box>
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map((c, i) => (
<MessageContent
key={i}
contacts={contacts}
content={c}
measure={measure}
scrollWindow={scrollWindow}
fontSize={fontSize}
group={group}
api={api}
associations={associations}
groups={groups}
/>
))}
</ContentBox>
</Box> </Box>
</> </Box>
); );
}; };
const ContentBox = styled(Box)` export const Message = ({
& > :first-child {
margin-left: 0px;
}
`;
export const MessageWithoutSigil = ({
timestamp, timestamp,
contacts, contacts,
msg, msg,
@ -379,115 +371,95 @@ export const MessageWithoutSigil = ({
api, api,
associations, associations,
groups, groups,
scrollWindow scrollWindow,
timestampHover,
...rest
}) => { }) => {
const { hovering, bind } = useHovering(); const { hovering, bind } = useHovering();
return ( return (
<> <Box position='relative' {...rest}>
<Timestamp {timestampHover ? (
stamp={timestamp} <Text
date={false} display={hovering ? 'block' : 'none'}
display={hovering ? 'block' : 'none'} position='absolute'
pt='2px' left='0'
lineHeight='tall' top='3px'
position='absolute' fontSize={0}
left={1} gray
/> >
<ContentBox {timestamp}
flexShrink={0} </Text>
fontSize='14px' ) : (
className='clamp-message' <></>
style={{ flexGrow: 1 }} )}
{...bind} <Box {...bind}>
pl={6} {msg.contents.map((content, i) => {
> switch (Object.keys(content)[0]) {
{msg.contents.map((c, i) => ( case 'text':
<MessageContent return (
key={i} <TextContent
contacts={contacts} associations={associations}
content={c} groups={groups}
group={group} measure={measure}
measure={measure} api={api}
scrollWindow={scrollWindow} fontSize={1}
groups={groups} lineHeight={'20px'}
associations={associations} content={content}
api={api} />
/> );
))} case 'code':
</ContentBox> return <CodeContent content={content} />;
</> case 'url':
); return (
}; <Box
flexShrink={0}
export const MessageContent = ({ fontSize={1}
content, lineHeight='20px'
contacts, color='black'
api, >
associations, <RemoteContent
groups, url={content.url}
measure, onLoad={measure}
scrollWindow, imageProps={{
fontSize, style: {
group maxWidth: 'min(100%,18rem)',
}) => { display: 'inline-block',
if ('code' in content) { marginTop: '0.5rem'
return <CodeContent content={content} />; }
} else if ('url' in content) { }}
return ( videoProps={{
<Box style: {
mx='2px' maxWidth: '18rem',
flexShrink={0} display: 'block',
fontSize={fontSize ? fontSize : '14px'} marginTop: '0.5rem'
lineHeight='tall' }
color='black' }}
> textProps={{
<RemoteContent style: {
url={content.url} fontSize: 'inherit',
onLoad={measure} borderBottom: '1px solid',
imageProps={{ textDecoration: 'none'
style: { }
maxWidth: 'min(100%,18rem)', }}
display: 'block' />
} </Box>
}} );
videoProps={{ case 'mention':
style: { return (
maxWidth: '18rem', <Mention
display: 'block' group={group}
} scrollWindow={scrollWindow}
}} ship={content.mention}
textProps={{ contact={contacts?.[`~${content.mention}`]}
style: { />
fontSize: 'inherit', );
borderBottom: '1px solid', default:
textDecoration: 'none' return null;
} }
}} })}
/>
</Box> </Box>
); </Box>
} else if ('text' in content) { );
return (
<TextContent
associations={associations}
groups={groups}
measure={measure}
api={api}
fontSize={fontSize}
content={content}
/>);
} else if ('mention' in content) {
return (
<Mention
group={group}
scrollWindow={scrollWindow}
ship={content.mention}
contact={contacts?.[`~${content.mention}`]}
/>
);
} else {
return null;
}
}; };
export const MessagePlaceholder = ({ export const MessagePlaceholder = ({

View File

@ -4,7 +4,15 @@ import _ from 'lodash';
import bigInt, { BigInteger } from 'big-integer'; import bigInt, { BigInteger } from 'big-integer';
import { Col } from '@tlon/indigo-react'; import { Col } from '@tlon/indigo-react';
import { Patp, Contacts, Association, Associations, Group, Groups, Graph } from '@urbit/api'; import {
Patp,
Contacts,
Association,
Associations,
Group,
Groups,
Graph
} from '@urbit/api';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
@ -33,7 +41,7 @@ type ChatWindowProps = RouteComponentProps<{
scrollTo?: number; scrollTo?: number;
associations: Associations; associations: Associations;
groups: Groups; groups: Groups;
} };
interface ChatWindowState { interface ChatWindowState {
fetchPending: boolean; fetchPending: boolean;
@ -42,7 +50,10 @@ interface ChatWindowState {
unreadIndex: BigInteger; unreadIndex: BigInteger;
} }
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> { export default class ChatWindow extends Component<
ChatWindowProps,
ChatWindowState
> {
private virtualList: VirtualScroller | null; private virtualList: VirtualScroller | null;
private unreadMarkerRef: React.RefObject<HTMLDivElement>; private unreadMarkerRef: React.RefObject<HTMLDivElement>;
private prevSize = 0; private prevSize = 0;
@ -79,7 +90,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
window.addEventListener('blur', this.handleWindowBlur); window.addEventListener('blur', this.handleWindowBlur);
window.addEventListener('focus', this.handleWindowFocus); window.addEventListener('focus', this.handleWindowFocus);
setTimeout(() => { setTimeout(() => {
if(this.props.scrollTo) { if (this.props.scrollTo) {
this.scrollToUnread(); this.scrollToUnread();
} }
@ -95,7 +106,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
calculateUnreadIndex() { calculateUnreadIndex() {
const { graph, unreadCount } = this.props; const { graph, unreadCount } = this.props;
const unreadIndex = graph.keys()[unreadCount]; const unreadIndex = graph.keys()[unreadCount];
if(!unreadIndex || unreadCount === 0) { if (!unreadIndex || unreadCount === 0) {
this.setState({ this.setState({
unreadIndex: bigInt.zero unreadIndex: bigInt.zero
}); });
@ -128,8 +139,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.calculateUnreadIndex(); this.calculateUnreadIndex();
} }
if(this.prevSize !== graph.size) { if (this.prevSize !== graph.size) {
if(this.state.unreadIndex.eq(bigInt.zero)) { if (this.state.unreadIndex.eq(bigInt.zero)) {
this.calculateUnreadIndex(); this.calculateUnreadIndex();
this.scrollToUnread(); this.scrollToUnread();
} }
@ -153,7 +164,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
scrollToUnread() { scrollToUnread() {
const { unreadIndex } = this.state; const { unreadIndex } = this.state;
if(unreadIndex.eq(bigInt.zero)) { if (unreadIndex.eq(bigInt.zero)) {
return; return;
} }
@ -162,10 +173,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
dismissUnread() { dismissUnread() {
const { association } = this.props; const { association } = this.props;
if (this.state.fetchPending) if (this.state.fetchPending) return;
return; if (this.props.unreadCount === 0) return;
if (this.props.unreadCount === 0)
return;
this.props.api.hark.markCountAsRead(association, '/', 'message'); this.props.api.hark.markCountAsRead(association, '/', 'message');
this.props.api.hark.markCountAsRead(association, '/', 'mention'); this.props.api.hark.markCountAsRead(association, '/', 'mention');
} }
@ -173,26 +182,31 @@ return;
async fetchMessages(newer: boolean, force = false): Promise<void> { async fetchMessages(newer: boolean, force = false): Promise<void> {
const { api, station, graph } = this.props; const { api, station, graph } = this.props;
if ( this.state.fetchPending && !force) { if (this.state.fetchPending && !force) {
return new Promise((resolve, reject) => {}); return new Promise((resolve, reject) => {});
} }
this.setState({ fetchPending: true }); this.setState({ fetchPending: true });
const [,, ship, name] = station.split('/'); const [, , ship, name] = station.split('/');
const currSize = graph.size; const currSize = graph.size;
if(newer && !this.loadedNewest) { if (newer && !this.loadedNewest) {
const [index] = graph.peekLargest()!; const [index] = graph.peekLargest()!;
await api.graph.getYoungerSiblings(ship,name, 20, `/${index.toString()}`); await api.graph.getYoungerSiblings(
if(currSize === graph.size) { ship,
name,
20,
`/${index.toString()}`
);
if (currSize === graph.size) {
console.log('loaded all newest'); console.log('loaded all newest');
this.loadedNewest = true; this.loadedNewest = true;
} }
} else if(!newer && !this.loadedOldest) { } else if (!newer && !this.loadedOldest) {
const [index] = graph.peekSmallest()!; const [index] = graph.peekSmallest()!;
await api.graph.getOlderSiblings(ship,name, 20, `/${index.toString()}`); await api.graph.getOlderSiblings(ship, name, 20, `/${index.toString()}`);
this.calculateUnreadIndex(); this.calculateUnreadIndex();
if(currSize === graph.size) { if (currSize === graph.size) {
console.log('loaded all oldest'); console.log('loaded all oldest');
this.loadedOldest = true; this.loadedOldest = true;
} }
@ -209,17 +223,14 @@ return;
} }
dismissIfLineVisible() { dismissIfLineVisible() {
if (this.props.unreadCount === 0) if (this.props.unreadCount === 0) return;
return; if (!this.unreadMarkerRef.current || !this.virtualList?.window) return;
if (!this.unreadMarkerRef.current || !this.virtualList?.window)
return;
const parent = this.unreadMarkerRef.current.parentElement?.parentElement; const parent = this.unreadMarkerRef.current.parentElement?.parentElement;
if (!parent) if (!parent) return;
return;
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window; const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
if ( if (
(scrollHeight - parent.offsetTop > scrollTop) scrollHeight - parent.offsetTop > scrollTop &&
&& (scrollHeight - parent.offsetTop < scrollTop + offsetHeight) scrollHeight - parent.offsetTop < scrollTop + offsetHeight
) { ) {
this.dismissUnread(); this.dismissUnread();
} }
@ -241,26 +252,39 @@ return;
} = this.props; } = this.props;
const unreadMarkerRef = this.unreadMarkerRef; const unreadMarkerRef = this.unreadMarkerRef;
const messageProps = {
const messageProps = { association, group, contacts, unreadMarkerRef, history, api, groups, associations }; association,
group,
contacts,
unreadMarkerRef,
history,
api,
groups,
associations
};
const keys = graph.keys().reverse(); const keys = graph.keys().reverse();
const unreadIndex = graph.keys()[this.props.unreadCount]; const unreadIndex = graph.keys()[this.props.unreadCount];
const unreadMsg = unreadIndex && graph.get(unreadIndex); const unreadMsg = unreadIndex && graph.get(unreadIndex);
return ( return (
<Col height='100%' overflow='hidden' position="relative"> <Col height='100%' overflow='hidden' position='relative'>
<UnreadNotice <UnreadNotice
unreadCount={unreadCount} unreadCount={unreadCount}
unreadMsg={unreadCount === 1 && unreadMsg && unreadMsg?.post.author === window.ship ? false : unreadMsg} unreadMsg={
unreadCount === 1 &&
unreadMsg &&
unreadMsg?.post.author === window.ship
? false
: unreadMsg
}
dismissUnread={this.dismissUnread} dismissUnread={this.dismissUnread}
onClick={this.scrollToUnread} onClick={this.scrollToUnread}
/> />
<VirtualScroller <VirtualScroller
ref={(list) => { ref={(list) => {
this.virtualList = list; this.virtualList = list;
}} }}
origin="bottom" origin='bottom'
style={{ height: '100%' }} style={{ height: '100%' }}
onStartReached={() => { onStartReached={() => {
this.setState({ idle: false }); this.setState({ idle: false });
@ -271,20 +295,35 @@ return;
size={graph.size} size={graph.size}
renderer={({ index, measure, scrollWindow }) => { renderer={({ index, measure, scrollWindow }) => {
const msg = graph.get(index)?.post; const msg = graph.get(index)?.post;
if (!msg) if (!msg) return null;
return null;
if (!this.state.initialized) { if (!this.state.initialized) {
return <MessagePlaceholder key={index.toString()} height="64px" index={index} />; return (
<MessagePlaceholder
key={index.toString()}
height='64px'
index={index}
/>
);
} }
const isPending: boolean = 'pending' in msg && Boolean(msg.pending); const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
const isLastMessage = index.eq(graph.peekLargest()?.[0] ?? bigInt.zero); const isLastMessage = index.eq(
graph.peekLargest()?.[0] ?? bigInt.zero
);
const highlighted = bigInt(this.props.scrollTo || -1).eq(index); const highlighted = bigInt(this.props.scrollTo || -1).eq(index);
const graphIdx = keys.findIndex(idx => idx.eq(index)); const graphIdx = keys.findIndex((idx) => idx.eq(index));
const prevIdx = keys[graphIdx+1]; const prevIdx = keys[graphIdx + 1];
const nextIdx = keys[graphIdx-1]; const nextIdx = keys[graphIdx - 1];
const isLastRead: boolean = this.state.unreadIndex.eq(index); const isLastRead: boolean = this.state.unreadIndex.eq(index);
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps }; const props = {
measure,
highlighted,
scrollWindow,
isPending,
isLastRead,
isLastMessage,
msg,
...messageProps
};
return ( return (
<ChatMessage <ChatMessage
key={index.toString()} key={index.toString()}
@ -302,4 +341,3 @@ return null;
); );
} }
} }

View File

@ -4,7 +4,7 @@ import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers'; import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import urbitOb from 'urbit-ob'; import urbitOb from 'urbit-ob';
import { Text } from '@tlon/indigo-react'; import { Text } from '@tlon/indigo-react';
import { GroupLink } from "~/views/components/GroupLink"; import { GroupLink } from '~/views/components/GroupLink';
const DISABLED_BLOCK_TOKENS = [ const DISABLED_BLOCK_TOKENS = [
'indentedCode', 'indentedCode',
@ -26,44 +26,60 @@ const DISABLED_INLINE_TOKENS = [
]; ];
const renderers = { const renderers = {
inlineCode: ({language, value}) => { inlineCode: ({ language, value }) => {
return <Text mono p='1' backgroundColor='washedGray' fontSize='0' style={{ whiteSpace: 'preWrap'}}>{value}</Text> return (
<Text
mono
p='1'
backgroundColor='washedGray'
fontSize='0'
style={{ whiteSpace: 'preWrap' }}
>
{value}
</Text>
);
}, },
paragraph: ({ children }) => { paragraph: ({ children }) => {
return (<Text fontSize="1">{children}</Text>); return (
<Text fontSize='1' lineHeight={'20px'}>
{children}
</Text>
);
}, },
code: ({language, value}) => { code: ({ language, value }) => {
return <Text return (
p='1' <Text
className='clamp-message' p='1'
display='block' className='clamp-message'
borderRadius='1' display='block'
mono borderRadius='1'
fontSize='0' mono
backgroundColor='washedGray' fontSize='0'
overflowX='auto' backgroundColor='washedGray'
style={{ whiteSpace: 'pre'}}> overflowX='auto'
{value} style={{ whiteSpace: 'pre' }}
</Text> >
{value}
</Text>
);
} }
}; };
const MessageMarkdown = React.memo(props => { const MessageMarkdown = React.memo((props) => {
const { source, ...rest } = props; const { source, ...rest } = props;
const blockCode = source.split('```'); const blockCode = source.split('```');
const codeLines = blockCode.map(codes => codes.split("\n")); const codeLines = blockCode.map((codes) => codes.split('\n'));
const lines = codeLines.reduce((acc, val, i) => { const lines = codeLines.reduce((acc, val, i) => {
if((i % 2) === 1) { if (i % 2 === 1) {
return [...acc, `\`\`\`${val.join('\n')}\`\`\``]; return [...acc, `\`\`\`${val.join('\n')}\`\`\``];
} else { } else {
return [...acc, ...val]; return [...acc, ...val];
} }
}, []); }, []);
return lines.map((line, i) => ( return lines.map((line, i) => (
<> <>
{ i !== 0 && <br />} {i !== 0 && <br />}
<ReactMarkdown <ReactMarkdown
{...rest} {...rest}
source={line} source={line}
@ -71,25 +87,31 @@ const MessageMarkdown = React.memo(props => {
renderers={renderers} renderers={renderers}
allowNode={(node, index, parent) => { allowNode={(node, index, parent) => {
if ( if (
node.type === 'blockquote' node.type === 'blockquote' &&
&& parent.type === 'root' parent.type === 'root' &&
&& node.children.length node.children.length &&
&& node.children[0].type === 'paragraph' node.children[0].type === 'paragraph' &&
&& node.children[0].position.start.offset < 2 node.children[0].position.start.offset < 2
) { ) {
node.children[0].children[0].value = '>' + node.children[0].children[0].value; node.children[0].children[0].value =
'>' + node.children[0].children[0].value;
return false; return false;
} }
return true; return true;
}} }}
plugins={[[RemarkDisableTokenizers, { plugins={[
block: DISABLED_BLOCK_TOKENS, [
inline: DISABLED_INLINE_TOKENS RemarkDisableTokenizers,
}]]} {
/> block: DISABLED_BLOCK_TOKENS,
inline: DISABLED_INLINE_TOKENS
}
]
]}
/>
</> </>
)) ));
}); });
export default function TextContent(props) { export default function TextContent(props) {
@ -98,12 +120,13 @@ export default function TextContent(props) {
const group = content.text.match( const group = content.text.match(
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/ /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/
); );
const isGroupLink = ((group !== null) // matched possible chatroom const isGroupLink =
&& (group[2].length > 2) // possible ship? group !== null && // matched possible chatroom
&& (urbitOb.isValidPatp(group[2]) // valid patp? group[2].length > 2 && // possible ship?
&& (group[0] === content.text))) // entire message is room name? urbitOb.isValidPatp(group[2]) && // valid patp?
group[0] === content.text; // entire message is room name?
if(isGroupLink) { if (isGroupLink) {
const resource = `/ship/${content.text}`; const resource = `/ship/${content.text}`;
return ( return (
<GroupLink <GroupLink
@ -120,7 +143,13 @@ export default function TextContent(props) {
); );
} else { } else {
return ( return (
<Text mx="2px" flexShrink={0} color='black' fontSize={props.fontSize ? props.fontSize : '14px'} lineHeight="tall" style={{ overflowWrap: 'break-word' }}> <Text
flexShrink={0}
color='black'
fontSize={props.fontSize ? props.fontSize : '14px'}
lineHeight={props.lineHeight ? props.lineHeight : '20px'}
style={{ overflowWrap: 'break-word' }}
>
<MessageMarkdown source={content.text} /> <MessageMarkdown source={content.text} />
</Text> </Text>
); );

View File

@ -1,28 +1,27 @@
import React, { ReactElement, useCallback } from 'react'; import React, { ReactNode, useCallback } from 'react';
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react'; import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react';
import { Link, useHistory } from 'react-router-dom';
import _ from 'lodash';
import { import {
GraphNotifIndex, GraphNotifIndex,
GraphNotificationContents, GraphNotificationContents,
Associations, Associations,
Rolodex, Rolodex,
Groups Groups
} from '@urbit/api'; } from '~/types';
import { Header } from './header'; import { Header } from './header';
import { cite, deSig, pluralize } from '~/logic/lib/util'; import { cite, deSig, pluralize } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import RichText from '~/views/components/RichText';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import ReactMarkdown from 'react-markdown';
import { getSnippet } from '~/logic/lib/publish'; import { getSnippet } from '~/logic/lib/publish';
import styled from 'styled-components';
import { MentionText } from '~/views/components/MentionText'; import { MentionText } from '~/views/components/MentionText';
import { MessageWithoutSigil } from '../chat/components/ChatMessage'; import ChatMessage from '../chat/components/ChatMessage';
import Timestamp from '~/views/components/Timestamp';
function getGraphModuleIcon(module: string): string { function getGraphModuleIcon(module: string) {
if (module === 'link') { if (module === 'link') {
return 'Collection'; return 'Collection';
} }
@ -58,16 +57,24 @@ function describeNotification(description: string, plural: boolean): string {
} }
} }
const GraphUrl = ({ url, title }): ReactElement => ( const GraphUrl = ({ url, title }) => (
<Box borderRadius="2" p="2" bg="scales.black05"> <Box borderRadius='2' p='2' bg='scales.black05'>
<Anchor underline={false} target="_blank" color="black" href={url}> <Anchor underline={false} target='_blank' color='black' href={url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" /> <Icon verticalAlign='bottom' mr='2' icon='ArrowExternal' />
{title} {title}
</Anchor> </Anchor>
</Box> </Box>
); );
const GraphNodeContent = ({ group, post, contacts, mod, index }): ReactElement => { const GraphNodeContent = ({
group,
post,
contacts,
mod,
description,
index,
remoteContentPolicy
}) => {
const { contents } = post; const { contents } = post;
const idx = index.slice(1).split('/'); const idx = index.slice(1).split('/');
if (mod === 'link') { if (mod === 'link') {
@ -75,39 +82,39 @@ const GraphNodeContent = ({ group, post, contacts, mod, index }): ReactElement =
const [{ text }, { url }] = contents; const [{ text }, { url }] = contents;
return <GraphUrl title={text} url={url} />; return <GraphUrl title={text} url={url} />;
} else if (idx.length === 3) { } else if (idx.length === 3) {
return <MentionText return (
content={contents} <MentionText content={contents} contacts={contacts} group={group} />
contacts={contacts} );
group={group}
/>;
} }
return null; return null;
} }
if (mod === 'publish') { if (mod === 'publish') {
if (idx[1] === '2') { if (idx[1] === '2') {
return <MentionText return (
content={contents} <MentionText
group={group} content={contents}
contacts={contacts} group={group}
fontSize='14px' contacts={contacts}
lineHeight="tall" fontSize='14px'
/>; lineHeight='tall'
/>
);
} else if (idx[1] === '1') { } else if (idx[1] === '1') {
const [{ text: header }, { text: body }] = contents; const [{ text: header }, { text: body }] = contents;
const snippet = getSnippet(body); const snippet = getSnippet(body);
return ( return (
<Col> <Col>
<Box mb="2" fontWeight="500"> <Box mb='2' fontWeight='500'>
<Text>{header}</Text> <Text>{header}</Text>
</Box> </Box>
<Box overflow="hidden" maxHeight="400px" position="relative"> <Box overflow='hidden' maxHeight='400px' position='relative'>
<Text lineHeight="tall">{snippet}</Text> <Text lineHeight='tall'>{snippet}</Text>
<FilterBox <FilterBox
width="100%" width='100%'
zIndex="1" zIndex='1'
height="calc(100% - 2em)" height='calc(100% - 2em)'
bottom="-4px" bottom='-4px'
position="absolute" position='absolute'
/> />
</Box> </Box>
</Col> </Col>
@ -115,31 +122,40 @@ const GraphNodeContent = ({ group, post, contacts, mod, index }): ReactElement =
} }
} }
if(mod === 'chat') { if (mod === 'chat') {
return ( return (
<Row <Row
width="100%" width='100%'
flexShrink={0} flexShrink={0}
flexGrow={1} flexGrow={1}
flexWrap="wrap" flexWrap='wrap'
marginLeft='-32px'
> >
<MessageWithoutSigil <ChatMessage
containerClass="items-top cf hide-child" renderSigil={false}
measure={() => {}} containerClass='items-top cf hide-child'
group={group} measure={() => {}}
contacts={contacts} group={group}
groups={{}} contacts={contacts}
associations={{ graph: {}, groups: {} }} groups={{}}
msg={post} associations={{ graph: {}, groups: {} }}
fontSize='0' msg={post}
pt='2' fontSize='0'
/> pt='2'
</Row>); />
</Row>
);
} }
return null; return null;
}; };
function getNodeUrl(mod: string, hidden: boolean, groupPath: string, graph: string, index: string): string { function getNodeUrl(
mod: string,
hidden: boolean,
groupPath: string,
graph: string,
index: string
) {
if (hidden && mod === 'chat') { if (hidden && mod === 'chat') {
groupPath = '/messages'; groupPath = '/messages';
} else if (hidden) { } else if (hidden) {
@ -181,40 +197,46 @@ const GraphNode = ({
ship={`~${author}`} ship={`~${author}`}
size={16} size={16}
icon icon
color={'#000000'} color={`#000000`}
classes="mix-blend-diff" classes='mix-blend-diff'
padding={2} padding={2}
/> />
) : <Box style={{ width: '16px' }}></Box>; ) : (
<Box style={{ width: '16px' }}></Box>
);
const groupContacts = contacts[groupPath] ?? {}; const groupContacts = contacts[groupPath] ?? {};
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index); const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
const onClick = useCallback(() => { const onClick = useCallback(() => {
if(!read) { if (!read) {
onRead(); onRead();
} }
history.push(nodeUrl); history.push(nodeUrl);
}, [read, onRead]); }, [read, onRead]);
return ( return (
<Row onClick={onClick} gapX="2" pt={showContact ? 2 : 0}> <Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
<Col>{img}</Col> <Col>{img}</Col>
<Col flexGrow={1} alignItems="flex-start"> <Col flexGrow={1} alignItems='flex-start'>
{showContact && <Row {showContact && (
mb="2" <Row
height="16px" mb='2'
alignItems="center" height='16px'
p="1" alignItems='center'
backgroundColor="white" p='1'
> backgroundColor='white'
<Text mono title={author}> >
{cite(author)} <Text mono title={author}>
</Text> {cite(author)}
<Timestamp stamp={moment(time)} date={false} ml={2} /> </Text>
</Row>} <Text ml='2' gray>
<Row width="100%" p="1" flexDirection="column"> {moment(time).format('HH:mm')}
</Text>
</Row>
)}
<Row width='100%' p='1' flexDirection='column'>
<GraphNodeContent <GraphNodeContent
contacts={groupContacts} contacts={groupContacts}
post={post} post={post}
@ -256,7 +278,7 @@ export function GraphNotification(props: {
return api.hark['read'](timebox, { graph: index }); return api.hark['read'](timebox, { graph: index });
}, [api, timebox, index, read]); }, [api, timebox, index, read]);
return ( return (
<> <>
<Header <Header
onClick={onClick} onClick={onClick}
@ -271,7 +293,7 @@ return (
description={desc} description={desc}
associations={props.associations} associations={props.associations}
/> />
<Box flexGrow={1} width="100%" pl={5} gridArea="main"> <Box flexGrow={1} width='100%' pl={5} gridArea='main'>
{_.map(contents, (content, idx) => ( {_.map(contents, (content, idx) => (
<GraphNode <GraphNode
post={content} post={content}

View File

@ -48,21 +48,19 @@ export function Mention(props: {
const group = props.group ?? { hidden: true }; const group = props.group ?? { hidden: true };
const [showOverlay, setShowOverlay] = useState(false); const [showOverlay, setShowOverlay] = useState(false);
const toggleOverlay = useCallback( const toggleOverlay = useCallback(() => {
() => { setShowOverlay((value) => !value);
setShowOverlay(value => !value); }, [showOverlay]);
},
[showOverlay]
);
return ( return (
<Box position='relative' display='inline-block' cursor='pointer'> <Box position='relative' display='inline-block' cursor='pointer'>
<Text <Text
onClick={() => toggleOverlay()} onClick={() => toggleOverlay()}
mx='2px' mx={1}
px='2px' px={1}
bg='washedBlue' bg='washedBlue'
color='blue' color='blue'
fontSize={showNickname ? 1 : 0}
mono={!showNickname} mono={!showNickname}
> >
{name} {name}