chat: sigil overlay exclusive of mini-sig (WIP)

Fixes urbit/landscape#303
This commit is contained in:
James Acklin 2021-02-06 17:19:34 -05:00
parent 1a6c3a1767
commit cf0189019f
3 changed files with 436 additions and 230 deletions

View File

@ -1,32 +1,58 @@
import React, { useState, useEffect, Component, PureComponent } from "react";
import moment from "moment";
import _ from "lodash";
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
import OverlaySigil from '~/views/components/OverlaySigil';
import { uxToHex, cite, writeText, useShowNickname, useHovering } from '~/logic/lib/util';
import { Group, Association, Contacts, Post } from "~/types";
/* eslint-disable max-lines-per-function */
import React, {
useState,
useEffect,
useRef,
Component,
PureComponent
} from 'react';
import moment from 'moment';
import _ from 'lodash';
import { Box, Row, Text, Rule } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil';
import { OverlayBox } from '~/views/components/OverlaySigil';
import {
uxToHex,
cite,
writeText,
useShowNickname,
useHovering
} from '~/logic/lib/util';
import { Group, Association, Contacts, Post } from '~/types';
import TextContent from './content/text';
import CodeContent from './content/code';
import RemoteContent from '~/views/components/RemoteContent';
import { Mention } from "~/views/components/MentionText";
import styled from "styled-components";
import useLocalState from "~/logic/state/local";
import { Mention } from '~/views/components/MentionText';
import styled from 'styled-components';
import useLocalState from '~/logic/state/local';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
<Row flexShrink={0} ref={ref} color='blue' alignItems='center' fontSize='0' position='absolute' width='100%' py='2'>
<Row
flexShrink={0}
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' />
<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 }) => (
<Row pb='3' alignItems="center" justifyContent="center" width='100%'>
<Text gray>{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}</Text>
<Row pb='3' alignItems='center' justifyContent='center' width='100%'>
<Text gray>
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
</Text>
</Row>
);
@ -86,14 +112,21 @@ export default class ChatMessage extends Component<ChatMessageProps> {
fontSize
} = this.props;
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
const dayBreak = nextMsg && new Date(msg['time-sent']).getDate() !== new Date(nextMsg['time-sent']).getDate();
const renderSigil = Boolean(
(nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1
);
const dayBreak =
nextMsg &&
new Date(msg['time-sent']).getDate() !==
new Date(nextMsg['time-sent']).getDate();
const containerClass = `${renderSigil
? `cf pl2 lh-copy`
: `items-top cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
const containerClass = `${
renderSigil ? 'cf pl2 lh-copy' : 'items-top cf hide-child'
} ${isPending ? 'o-40' : ''} ${className}`;
const timestamp = moment.unix(msg['time-sent'] / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
const timestamp = moment
.unix(msg['time-sent'] / 1000)
.format(renderSigil ? 'hh:mm a' : 'hh:mm');
const reboundMeasure = (event) => {
return measure(this.divRef.current);
@ -117,7 +150,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
};
const unreadContainerStyle = {
height: isLastRead ? '2rem' : '0',
height: isLastRead ? '2rem' : '0'
};
return (
@ -134,15 +167,30 @@ export default class ChatMessage extends Component<ChatMessageProps> {
className={containerClass}
style={style}
mb={1}
position="relative"
position='relative'
>
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
{renderSigil
? <MessageWithSigil {...messageProps} />
: <MessageWithoutSigil {...messageProps} />}
<Box flexShrink={0} fontSize={0} position='relative' width='100%' overflow='visible' style={unreadContainerStyle}>{isLastRead
? <UnreadMarker dayBreak={dayBreak} when={msg['time-sent']} ref={unreadMarkerRef} />
: null}</Box>
{renderSigil ? (
<MessageWithSigil {...messageProps} />
) : (
<MessageWithoutSigil {...messageProps} />
)}
<Box
flexShrink={0}
fontSize={0}
position='relative'
width='100%'
overflow='visible'
style={unreadContainerStyle}
>
{isLastRead ? (
<UnreadMarker
dayBreak={dayBreak}
when={msg['time-sent']}
ref={unreadMarkerRef}
/>
) : null}
</Box>
</Box>
);
}
@ -159,7 +207,7 @@ interface MessageProps {
style: any;
measure(element): void;
scrollWindow: HTMLDivElement;
};
}
export const MessageWithSigil = (props) => {
const {
@ -175,17 +223,27 @@ export const MessageWithSigil = (props) => {
fontSize
} = props;
const dark = useLocalState(state => state.dark);
const dark = useLocalState((state) => state.dark);
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
const datestamp = moment
.unix(msg['time-sent'] / 1000)
.format(DATESTAMP_FORMAT);
const contact = msg.author in contacts ? contacts[msg.author] : false;
const showNickname = useShowNickname(contact);
const shipName = showNickname ? contact.nickname : cite(msg.author);
const copyNotice = 'Copied';
const color = contact ? `#${uxToHex(contact.color)}` : dark ? '#000000' :'#FFFFFF'
const sigilClass = contact ? '' : dark ? 'mix-blend-diff' : 'mix-blend-darken';
const color = contact
? `#${uxToHex(contact.color)}`
: dark
? '#000000'
: '#FFFFFF';
const sigilClass = contact
? ''
: dark
? 'mix-blend-diff'
: 'mix-blend-darken';
const [displayName, setDisplayName] = useState(shipName);
const [nameMono, setNameMono] = useState((showNickname ? false : true));
const [nameMono, setNameMono] = useState(showNickname ? false : true);
const { hovering, bind } = useHovering();
const showCopyNotice = () => {
@ -196,7 +254,7 @@ export const MessageWithSigil = (props) => {
useEffect(() => {
const resetDisplay = () => {
setDisplayName(shipName);
setNameMono((showNickname ? false : true));
setNameMono(showNickname ? false : true);
};
const timer = setTimeout(() => resetDisplay(), 800);
return () => clearTimeout(timer);
@ -204,24 +262,20 @@ export const MessageWithSigil = (props) => {
return (
<>
<OverlaySigil
ship={msg.author}
contact={contact}
color={color}
sigilClass={sigilClass}
group={group}
scrollWindow={scrollWindow}
history={history}
api={api}
bg="white"
className="fl v-top pt1"
pr={3}
pl={2}
/>
<Box flexGrow={1} display='block' className="clamp-message" {...bind}>
<Box mr={12} ml={12} pt={1} pb={1} height={16}>
<Sigil
ship={ship}
size={16}
color={color}
classes={sigilClass}
icon
padded
/>
</Box>
<Box flexGrow={1} display='block' className='clamp-message' {...bind}>
<Box
flexShrink={0}
className="hide-child"
className='hide-child'
pt={1}
pb={1}
display='flex'
@ -233,7 +287,7 @@ export const MessageWithSigil = (props) => {
flexShrink={0}
mono={nameMono}
fontWeight={nameMono ? '400' : '500'}
className={`mw5 db truncate pointer`}
className={'mw5 db truncate pointer'}
onClick={() => {
writeText(`~${msg.author}`);
showCopyNotice();
@ -242,7 +296,9 @@ export const MessageWithSigil = (props) => {
>
{displayName}
</Text>
<Text flexShrink={0} fontSize={0} gray mono>{timestamp}</Text>
<Text flexShrink={0} fontSize={0} gray mono>
{timestamp}
</Text>
<Text
flexShrink={0}
fontSize={0}
@ -250,32 +306,42 @@ export const MessageWithSigil = (props) => {
mono
ml={2}
display={['none', hovering ? 'block' : 'none']}
>{datestamp}</Text>
>
{datestamp}
</Text>
</Box>
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map(c =>
<MessageContent
contacts={contacts}
content={c}
measure={measure}
fontSize={fontSize}
group={group}
/>)}
{msg.contents.map((c, i) => (
<MessageContent
key={i}
contacts={contacts}
content={c}
measure={measure}
scrollWindow={scrollWindow}
fontSize={fontSize}
group={group}
/>
))}
</ContentBox>
</Box>
</>
);
}
};
const ContentBox = styled(Box)`
& > :first-child {
margin-left: 0px;
}
`;
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
export const MessageWithoutSigil = ({
timestamp,
contacts,
msg,
measure,
group,
scrollWindow
}) => {
const { hovering, bind } = useHovering();
return (
<>
@ -283,17 +349,19 @@ export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }
flexShrink={0}
mono
gray
display={hovering ? 'block': 'none'}
display={hovering ? 'block' : 'none'}
pt='2px'
lineHeight='tall'
fontSize={0}
position="absolute"
position='absolute'
left={1}
>{timestamp}</Text>
>
{timestamp}
</Text>
<ContentBox
flexShrink={0}
fontSize='14px'
className="clamp-message"
className='clamp-message'
style={{ flexGrow: 1 }}
{...bind}
pl={6}
@ -304,52 +372,87 @@ export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }
contacts={contacts}
content={c}
group={group}
measure={measure}/>))}
measure={measure}
scrollWindow={scrollWindow}
/>
))}
</ContentBox>
</>
)
);
};
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
export const MessageContent = ({
content,
contacts,
measure,
scrollWindow,
fontSize,
group
}) => {
if ('code' in content) {
return <CodeContent content={content} />;
} else if ('url' in content) {
return (
<Box mx="2px" flexShrink={0} fontSize={fontSize ? fontSize : '14px'} lineHeight="tall" color='black'>
<Box
mx='2px'
flexShrink={0}
fontSize={fontSize ? fontSize : '14px'}
lineHeight='tall'
color='black'
>
<RemoteContent
url={content.url}
onLoad={measure}
imageProps={{style: {
maxWidth: 'min(100%,18rem)',
display: 'block'
}}}
videoProps={{style: {
maxWidth: '18rem',
display: 'block'
}
imageProps={{
style: {
maxWidth: 'min(100%,18rem)',
display: 'block'
}
}}
videoProps={{
style: {
maxWidth: '18rem',
display: 'block'
}
}}
textProps={{
style: {
fontSize: 'inherit',
borderBottom: '1px solid',
textDecoration: 'none'
}
}}
textProps={{style: {
fontSize: 'inherit',
borderBottom: '1px solid',
textDecoration: 'none'
}}}
/>
</Box>
);
} else if ('text' in content) {
return <TextContent fontSize={fontSize} content={content} />;
} else if ('mention' in content) {
return <Mention group={group} ship={content.mention} contact={contacts?.[content.mention]} />
return (
<Mention
group={group}
scrollWindow={scrollWindow}
ship={content.mention}
contact={contacts?.[content.mention]}
/>
);
} else {
return null;
}
};
export const MessagePlaceholder = ({ height, index, className = '', style = {}, ...props }) => (
export const MessagePlaceholder = ({
height,
index,
className = '',
style = {},
...props
}) => (
<Box
width='100%'
fontSize='2'
pl='3' pt='4'
pl='3'
pt='4'
pr='3'
display='flex'
lineHeight='tall'
@ -357,78 +460,86 @@ export const MessagePlaceholder = ({ height, index, className = '', style = {},
style={{ height, ...style }}
{...props}
>
<Box pr='3' verticalAlign='top' backgroundColor='white' style={{ float: 'left' }}>
<Text
display='block'
background='gray'
width='24px'
height='24px'
borderRadius='50%'
style={{
visibility: (index % 5 == 0) ? "initial" : "hidden",
}}
></Text>
</Box>
<Box
pr='3'
verticalAlign='top'
backgroundColor='white'
style={{ float: 'left' }}
>
<Text
display='block'
background='gray'
width='24px'
height='24px'
borderRadius='50%'
style={{
visibility: index % 5 == 0 ? 'initial' : 'hidden'
}}
></Text>
</Box>
<Box
style={{ float: 'right', flexGrow: 1 }}
color='black'
className='clamp-message'
>
<Box
style={{ float: 'right', flexGrow: 1 }}
color='black'
className="clamp-message"
className='hide-child'
paddingTop='4'
style={{ visibility: index % 5 == 0 ? 'initial' : 'hidden' }}
>
<Box
className="hide-child"
paddingTop='4'
style={{visibility: (index % 5 == 0) ? "initial" : "hidden" }}
<Text
display='inline-block'
verticalAlign='middle'
fontSize='0'
gray
cursor='default'
>
<Text
display='inline-block'
verticalAlign='middle'
fontSize='0'
gray
cursor='default'
>
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='gray'
display='block'
width='100%'
height='100%'></Text>
</Text>
</Text>
<Text
display='inline-block'
mono
verticalAlign='middle'
fontSize='0'
gray
>
<Text
background='gray'
display='block'
height='1em'
style={{ width: `${(index % 3 + 1) * 3}em` }}
></Text>
</Text>
<Text
mono
verticalAlign='middle'
fontSize='0'
ml='2'
gray
display={['none', 'inline-block']}
className="child">
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='gray'
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Box>
></Text>
</Text>
</Text>
<Text
display='block'
backgroundColor='gray'
height='1em'
style={{ width: `${(index % 5) * 20}%` }}></Text>
display='inline-block'
mono
verticalAlign='middle'
fontSize='0'
gray
>
<Text
background='gray'
display='block'
height='1em'
style={{ width: `${((index % 3) + 1) * 3}em` }}
></Text>
</Text>
<Text
mono
verticalAlign='middle'
fontSize='0'
ml='2'
gray
display={['none', 'inline-block']}
className='child'
>
<Text
backgroundColor='gray'
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Box>
<Text
display='block'
backgroundColor='gray'
height='1em'
style={{ width: `${(index % 5) * 20}%` }}
></Text>
</Box>
</Box>
);

View File

@ -1,16 +1,13 @@
import React, { useState, useCallback } from "react";
import _ from "lodash";
import { Text, Box } from "@tlon/indigo-react";
import {
Contact,
Contacts,
Content,
Group,
} from "~/types";
import RichText from "~/views/components/RichText";
import { cite, useShowNickname, uxToHex } from "~/logic/lib/util";
import ProfileOverlay from "./ProfileOverlay";
import { useHistory } from "react-router-dom";
/* eslint-disable max-lines-per-function */
import React, { useState, useCallback } from 'react';
import _ from 'lodash';
import { Text, Box } from '@tlon/indigo-react';
import { Contact, Contacts, Content, Group } from '~/types';
import RichText from '~/views/components/RichText';
import { cite, useShowNickname, uxToHex } from '~/logic/lib/util';
// import ProfileOverlay from './ProfileOverlay';
import OverlaySigil, { OverlayBox } from '~/views/components/OverlaySigil';
import { useHistory } from 'react-router-dom';
interface MentionTextProps {
contact?: Contact;
@ -24,9 +21,9 @@ export function MentionText(props: MentionTextProps) {
return (
<RichText contacts={contacts} contact={contact} group={group} {...rest}>
{content.reduce((accum, c) => {
if ("text" in c) {
if ('text' in c) {
return accum + c.text;
} else if ("mention" in c) {
} else if ('mention' in c) {
return accum + `[~${c.mention}]`;
}
return accum;
@ -36,12 +33,13 @@ export function MentionText(props: MentionTextProps) {
}
export function Mention(props: {
ship: string;
contact: Contact;
contacts?: Contacts;
group: Group;
scrollWindow?: HTMLElement;
ship: string;
}) {
const { contacts, ship } = props;
const { contacts, ship, scrollWindow } = props;
let { contact } = props;
contact = (contact?.color) ? contact : contacts?.[ship];
@ -50,43 +48,38 @@ export function Mention(props: {
const name = showNickname ? contact?.nickname : cite(ship);
const [showOverlay, setShowOverlay] = useState(false);
const onDismiss = useCallback(() => {
setShowOverlay(false);
}, [setShowOverlay]);
const onClick = useCallback(() => {
setShowOverlay(true);
}, [setShowOverlay]);
const group = props.group ?? { hidden: true };
const toggleOverlay = () => {
setShowOverlay((value) => !value);
};
const history = useHistory();
return (
<Box
position="relative"
display="inline-block"
cursor="pointer"
>
{showOverlay && (
<ProfileOverlay
ship={ship}
contact={contact}
color={`#${uxToHex(contact?.color ?? '0x0')}`}
group={group}
onDismiss={onDismiss}
history={history}
/>
)}
<Box position='relative' display='inline-block' cursor='pointer'>
<Text
onClick={onClick}
mx="2px"
px="2px"
bg="washedBlue"
color="blue"
onClick={() => toggleOverlay()}
mx='2px'
px='2px'
bg='washedBlue'
color='blue'
mono={!showNickname}
>
{name}
</Text>
{showOverlay && (
<OverlayBox
ship={ship}
contact={contact}
color={`#${uxToHex(contact?.color ?? '0x0')}`}
group={group}
onDismiss={() => toggleOverlay()}
history={history}
className='relative'
scrollWindow={scrollWindow}
/>
)}
</Box>
);
}

View File

@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
/* eslint-disable max-lines-per-function */
import React, { useState, useRef, useEffect, PureComponent } from 'react';
import { Sigil } from '~/logic/lib/sigil';
import { Contact, Group } from '~/types';
@ -9,17 +10,17 @@ import { Box, BaseImage, ColProps } from '@tlon/indigo-react';
import { withLocalState } from '~/logic/state/local';
type OverlaySigilProps = ColProps & {
ship: string;
contact?: Contact;
color: string;
sigilClass: string;
group?: Group;
scrollWindow?: HTMLElement;
history: any;
api: any;
className: string;
color: string;
contact?: Contact;
group?: Group;
hideAvatars: boolean;
}
history: any;
scrollWindow?: HTMLElement;
ship: string;
sigilClass: string;
};
interface OverlaySigilState {
clicked: boolean;
@ -27,6 +28,80 @@ interface OverlaySigilState {
bottomSpace: number | 'auto';
}
export const OverlayBox = (props) => {
const {
api,
className,
color,
contact,
group,
hideAvatars,
history,
onDismiss,
scrollWindow,
ship,
sigilClass,
...rest
} = {
...props
};
const containerRef = useRef(null);
const [space, setSpace] = useState({ top: 'auto', bottom: 'auto' });
const updateContainerOffset = () => {
if (containerRef && containerRef.current) {
const container = containerRef.current;
const bottomSpace = scrollWindow
? scrollWindow.clientHeight -
(container.getBoundingClientRect().top +
OVERLAY_HEIGHT -
scrollWindow.getBoundingClientRect().top)
: 'auto';
const topSpace = scrollWindow
? container.getBoundingClientRect().top -
scrollWindow.getBoundingClientRect().top
: 0;
setSpace({
top: topSpace,
bottom: bottomSpace
});
}
};
useEffect(() => {
updateContainerOffset();
scrollWindow.addEventListener('scroll', updateContainerOffset);
return () => {
scrollWindow.removeEventListener('scroll', updateContainerOffset, true);
};
}, []);
return (
<Box
cursor='pointer'
position='relative'
className={className}
pr={0}
pl={0}
ref={containerRef}
>
<ProfileOverlay
api={api}
bottomSpace={space.bottom}
color={color}
contact={contact}
group={group}
history={history}
onDismiss={() => onDismiss()}
ship={ship}
topSpace={space.top}
{...rest}
/>
</Box>
);
};
class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
public containerRef: React.Ref<HTMLDivElement>;
@ -48,20 +123,35 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
profileShow() {
this.updateContainerOffset();
this.setState({ clicked: true });
this.props.scrollWindow?.addEventListener('scroll', this.updateContainerOffset);
this.props.scrollWindow?.addEventListener(
'scroll',
this.updateContainerOffset
);
}
profileHide() {
this.setState({ clicked: false });
this.props.scrollWindow?.removeEventListener('scroll', this.updateContainerOffset, true);
this.props.scrollWindow?.removeEventListener(
'scroll',
this.updateContainerOffset,
true
);
}
updateContainerOffset() {
if (this.containerRef && this.containerRef.current) {
const container = this.containerRef.current;
const scrollWindow = this.props.scrollWindow;
const bottomSpace = scrollWindow ? scrollWindow.clientHeight - ((container.getBoundingClientRect().top + OVERLAY_HEIGHT) - scrollWindow.getBoundingClientRect().top) : 'auto';
const topSpace = scrollWindow ? container.getBoundingClientRect().top - scrollWindow.getBoundingClientRect().top : 0;
const bottomSpace = scrollWindow
? scrollWindow.clientHeight -
(container.getBoundingClientRect().top +
OVERLAY_HEIGHT -
scrollWindow.getBoundingClientRect().top)
: 'auto';
const topSpace = scrollWindow
? container.getBoundingClientRect().top -
scrollWindow.getBoundingClientRect().top
: 0;
this.setState({
topSpace,
bottomSpace
@ -70,7 +160,11 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
}
componentWillUnmount() {
this.props.scrollWindow?.removeEventListener('scroll', this.updateContainerOffset, true);
this.props.scrollWindow?.removeEventListener(
'scroll',
this.updateContainerOffset,
true
);
}
render() {
@ -91,26 +185,34 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
const { state } = this;
const img = (contact && (contact.avatar !== null) && !hideAvatars)
? <BaseImage display='inline-block' src={contact.avatar} height={16} width={16} />
: <Sigil
ship={ship}
size={16}
color={color}
classes={sigilClass}
icon
padded
/>;
const img =
contact && contact.avatar !== null && !hideAvatars ? (
<BaseImage
display='inline-block'
src={contact.avatar}
height={16}
width={16}
/>
) : (
<Sigil
ship={ship}
size={16}
color={color}
classes={sigilClass}
icon
padded
/>
);
return (
return (
<Box
cursor='pointer'
position='relative'
onClick={this.profileShow}
ref={this.containerRef}
className={className}
pr={pr}
pl={pl}
ref={this.containerRef}
className={className}
pr={pr}
pl={pl}
>
{state.clicked && (
<ProfileOverlay
@ -122,8 +224,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
group={group}
onDismiss={this.profileHide}
history={history}
api={api}
{...rest}
api={api}
{...rest}
/>
)}
{img}