chat-fe: MVP graphification

This commit is contained in:
Liam Fitzgerald 2020-11-23 15:57:43 +10:00
parent a1cf88faba
commit 403ef0a275
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
11 changed files with 142 additions and 160 deletions

View File

@ -67,6 +67,9 @@ function moduleToMark(mod: string): string | undefined {
if(mod === 'publish') { if(mod === 'publish') {
return 'graph-validator-publish'; return 'graph-validator-publish';
} }
if(mod === 'chat') {
return 'graph-validator-chat';
}
return undefined; return undefined;
} }

View File

@ -1,3 +1,5 @@
import urbitOb from 'urbit-ob';
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
const isUrl = (string) => { const isUrl = (string) => {
@ -45,11 +47,20 @@ const tokenizeMessage = (text) => {
if (isUrl(str) && !isInCodeBlock) { if (isUrl(str) && !isInCodeBlock) {
if (message.length > 0) { if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset // If we're in the middle of a message, add it to the stack and reset
messages.push(message); messages.push({ text: message.join('') });
message = []; message = [];
} }
messages.push([str]); messages.push({ url: str });
message = []; message = [];
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join('') });
message = [];
}
messages.push({ mention: str });
message = [];
} else { } else {
message.push(str); message.push(str);
} }
@ -59,7 +70,7 @@ const tokenizeMessage = (text) => {
if (message.length) { if (message.length) {
// Add any remaining message // Add any remaining message
messages.push(message); messages.push({ text: message.join('') });
} }
return messages; return messages;
}; };

View File

@ -8,9 +8,12 @@ export interface UrlContent {
url: string; url: string;
} }
export interface CodeContent { export interface CodeContent {
expresssion: string; code: {
output: string; expresssion: string;
output: string | undefined;
}
} }
export interface ReferenceContent { export interface ReferenceContent {
uid: string; uid: string;
} }

View File

@ -11,6 +11,7 @@ import ChatInput from './components/ChatInput';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { SubmitDragger } from '~/views/components/s3-upload'; import { SubmitDragger } from '~/views/components/s3-upload';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import {Loading} from '~/views/components/Loading';
type ChatResourceProps = StoreState & { type ChatResourceProps = StoreState & {
association: Association; association: Association;
@ -31,6 +32,8 @@ export function ChatResource(props: ChatResourceProps) {
const group = props.groups[groupPath]; const group = props.groups[groupPath];
const contacts = props.contacts[groupPath] || {}; const contacts = props.contacts[groupPath] || {};
const graph = props.graphs[station.slice(7)];
const pendingMessages = (props.pendingMessages.get(station) || []).map( const pendingMessages = (props.pendingMessages.get(station) || []).map(
value => ({ value => ({
...value, ...value,
@ -38,12 +41,7 @@ export function ChatResource(props: ChatResourceProps) {
}) })
); );
const isChatMissing = const isChatMissing = !props.graphKeys.has(station.slice(7));
(props.chatInitialized &&
!(station in props.inbox) &&
props.chatSynced &&
!(station in props.chatSynced)) ||
false;
const isChatLoading = const isChatLoading =
(props.chatInitialized && (props.chatInitialized &&
@ -61,12 +59,16 @@ export function ChatResource(props: ChatResourceProps) {
const unreadCount = length - read; const unreadCount = length - read;
const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1]; const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1];
const [, owner, name] = station.split('/'); const [,, owner, name] = station.split('/');
const ourContact = contacts?.[window.ship]; const ourContact = contacts?.[window.ship];
const lastMsgNum = envelopes.length || 0; const lastMsgNum = envelopes.length || 0;
const chatInput = useRef<ChatInput>(); const chatInput = useRef<ChatInput>();
useEffect(() => {
props.api.graph.getGraph(owner,name);
}, [station]);
const onFileDrag = useCallback( const onFileDrag = useCallback(
(files: FileList) => { (files: FileList) => {
if (!chatInput.current) { if (!chatInput.current) {
@ -102,21 +104,26 @@ export function ChatResource(props: ChatResourceProps) {
return clear; return clear;
}, [station]); }, [station]);
if(!graph) {
return <Loading />;
}
return ( return (
<Col {...bind} height="100%" overflow="hidden" position="relative"> <Col {...bind} height="100%" overflow="hidden" position="relative">
{dragging && <SubmitDragger />} {dragging && <SubmitDragger />}
<ChatWindow <ChatWindow
remoteContentPolicy={props.remoteContentPolicy} remoteContentPolicy={props.remoteContentPolicy}
mailboxSize={length} mailboxSize={5}
match={props.match as any} match={props.match as any}
stationPendingMessages={pendingMessages} stationPendingMessages={[]}
history={props.history} history={props.history}
isChatMissing={isChatMissing} isChatMissing={isChatMissing}
isChatLoading={isChatLoading} isChatLoading={isChatLoading}
isChatUnsynced={isChatUnsynced} isChatUnsynced={isChatUnsynced}
unreadCount={unreadCount} graph={graph}
unreadMsg={unreadMsg} unreadCount={0}
envelopes={envelopes || []} unreadMsg={false}
envelopes={[]}
contacts={contacts} contacts={contacts}
association={props.association} association={props.association}
group={group} group={group}

View File

@ -3,10 +3,11 @@ import ChatEditor from './chat-editor';
import { S3Upload } from '~/views/components/s3-upload' ; import { S3Upload } from '~/views/components/s3-upload' ;
import { uxToHex } from '~/logic/lib/util'; import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import { createPost } from '~/logic/api/graph';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { Envelope } from '~/types/chat-update'; import { Envelope } from '~/types/chat-update';
import { Contacts } from '~/types'; import { Contacts, Content } from '~/types';
import { Row, BaseImage, Box, Icon } from '@tlon/indigo-react'; import { Row, BaseImage, Box, Icon } from '@tlon/indigo-react';
interface ChatInputProps { interface ChatInputProps {
@ -82,39 +83,23 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
submit(text) { submit(text) {
const { props, state } = this; const { props, state } = this;
const [,,ship,name] = props.station.split('/');
if (state.inCodeMode) { if (state.inCodeMode) {
this.setState({ this.setState({
inCodeMode: false inCodeMode: false
}, () => { }, () => {
props.api.chat.message( const contents: Content[] = [{ code: { expression: text, output: undefined }}];
props.station, const post = createPost(contents);
`~${window.ship}`, props.api.graph.addPost(ship, name, post);
Date.now(), {
code: {
expression: text,
output: undefined
}
}
);
}); });
return; return;
} }
const messages = tokenizeMessage(text); const post = createPost(tokenizeMessage((text)))
props.deleteMessage(); props.deleteMessage();
messages.forEach((message) => { props.api.graph.addPost(ship,name, post);
if (message.length > 0) {
message = this.getLetterType(message.join(' '));
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
message
);
}
});
} }
uploadSuccess(url) { uploadSuccess(url) {
@ -123,12 +108,8 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
this.chatEditor.current.editor.setValue(url); this.chatEditor.current.editor.setValue(url);
this.setState({ uploadingPaste: false }); this.setState({ uploadingPaste: false });
} else { } else {
props.api.chat.message( const [,,ship,name] = props.station.split('/');
props.station, props.api.graph.addPost(ship,name, createPost([{ url }]));
`~${window.ship}`,
Date.now(),
{ url }
);
} }
} }

View File

@ -6,10 +6,11 @@ import { Box, Row, Text, Rule } from "@tlon/indigo-react";
import { OverlaySigil } from './overlay-sigil'; import { OverlaySigil } from './overlay-sigil';
import { uxToHex, cite, writeText } from '~/logic/lib/util'; import { uxToHex, cite, writeText } from '~/logic/lib/util';
import { Envelope, IMessage } from "~/types/chat-update"; import { Envelope, IMessage } from "~/types/chat-update";
import { Group, Association, Contacts, LocalUpdateRemoteContentPolicy } from "~/types"; import { Group, Association, Contacts, LocalUpdateRemoteContentPolicy, Post } 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';
import { Mention } from "~/views/components/MentionText";
export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -30,9 +31,9 @@ export const DayBreak = ({ when }) => (
interface ChatMessageProps { interface ChatMessageProps {
measure(element): void; measure(element): void;
msg: Envelope | IMessage; msg: Post;
previousMsg?: Envelope | IMessage; previousMsg?: Post;
nextMsg?: Envelope | IMessage; nextMsg?: Post;
isLastRead: boolean; isLastRead: boolean;
group: Group; group: Group;
association: Association; association: Association;
@ -91,13 +92,13 @@ export default class ChatMessage extends Component<ChatMessageProps> {
} = this.props; } = this.props;
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1); const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
const dayBreak = nextMsg && new Date(msg.when).getDate() !== new Date(nextMsg.when).getDate(); const dayBreak = nextMsg && new Date(msg['time-sent']).getDate() !== new Date(nextMsg['time-sent']).getDate();
const containerClass = `${renderSigil const containerClass = `${renderSigil
? `cf pl2 lh-copy` ? `cf pl2 lh-copy`
: `items-top cf hide-child`} ${isPending ? 'o-40' : ''} ${className}` : `items-top cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
const timestamp = moment.unix(msg.when / 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) => { const reboundMeasure = (event) => {
return measure(this.divRef.current); return measure(this.divRef.current);
@ -140,7 +141,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
ref={this.divRef} ref={this.divRef}
className={containerClass} className={containerClass}
style={style} style={style}
data-number={msg.number}
mb={1} mb={1}
> >
{dayBreak && !isLastRead ? <DayBreak when={msg.when} /> : null} {dayBreak && !isLastRead ? <DayBreak when={msg.when} /> : null}
@ -156,7 +156,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
} }
interface MessageProps { interface MessageProps {
msg: Envelope | IMessage; msg: Post;
timestamp: string; timestamp: string;
group: Group; group: Group;
association: Association; association: Association;
@ -191,10 +191,10 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
fontSize fontSize
} = this.props; } = this.props;
const datestamp = moment.unix(msg.when / 1000).format(DATESTAMP_FORMAT); const datestamp = moment.unix(msg['time-sent']).format(DATESTAMP_FORMAT);
const contact = msg.author in contacts ? contacts[msg.author] : false; const contact = msg.author in contacts ? contacts[msg.author] : false;
const showNickname = !hideNicknames && contact && contact.nickname; const showNickname = !hideNicknames && contact && contact.nickname;
const name = showNickname ? contact.nickname : cite(msg.author); const name = showNickname ? contact!.nickname : cite(msg.author);
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF' const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken'; const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
@ -251,23 +251,32 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text> <Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text> <Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
</Box> </Box>
<Box flexShrink={0} fontSize={fontSize ? fontSize : '14px'}><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} fontSize={fontSize} /></Box> <Box flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map(c =>
<MessageContent
contacts={contacts}
content={c}
remoteContentPolicy={remoteContentPolicy}
measure={measure}
fontSize={fontSize}
/>)}
</Box>
</Box> </Box>
</> </>
); );
} }
} }
export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => ( export const MessageWithoutSigil = ({ timestamp, contacts, msg, remoteContentPolicy, measure }) => (
<> <>
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text> <Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
<Box flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}> <Box flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure}/> {msg.contents.map(c => (<MessageContent contacts={contacts} content={c} remoteContentPolicy={remoteContentPolicy} measure={measure}/>))}
</Box> </Box>
</> </>
); );
export const MessageContent = ({ content, remoteContentPolicy, measure, fontSize }) => { export const MessageContent = ({ content, contacts, remoteContentPolicy, measure, fontSize }) => {
if ('code' in content) { if ('code' in content) {
return <CodeContent content={content} />; return <CodeContent content={content} />;
} else if ('url' in content) { } else if ('url' in content) {
@ -286,15 +295,10 @@ export const MessageContent = ({ content, remoteContentPolicy, measure, fontSize
/> />
</Text> </Text>
); );
} else if ('me' in content) { } else if ('text' in content) {
return (
<Text flexShrink={0} fontStyle='italic' fontSize={fontSize ? fontSize : '14px'} lineHeight='tall' color='black'>
{content.me}
</Text>
);
}
else if ('text' in content) {
return <TextContent fontSize={fontSize} content={content} />; return <TextContent fontSize={fontSize} content={content} />;
} else if ('mention' in content) {
return <Mention ship={content.mention} contacts={contacts} />
} else { } else {
return null; return null;
} }

View File

@ -9,7 +9,7 @@ import { Contacts } from "~/types/contact-update";
import { Association } from "~/types/metadata-update"; import { Association } from "~/types/metadata-update";
import { Group } from "~/types/group-update"; import { Group } from "~/types/group-update";
import { Envelope, IMessage } from "~/types/chat-update"; import { Envelope, IMessage } from "~/types/chat-update";
import { LocalUpdateRemoteContentPolicy } from "~/types"; import { LocalUpdateRemoteContentPolicy, Graph } from "~/types";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import VirtualScroller from "~/views/components/VirtualScroller"; import VirtualScroller from "~/views/components/VirtualScroller";
@ -29,13 +29,12 @@ type ChatWindowProps = RouteComponentProps<{
station: string; station: string;
}> & { }> & {
unreadCount: number; unreadCount: number;
envelopes: Envelope[];
isChatMissing: boolean; isChatMissing: boolean;
isChatLoading: boolean; isChatLoading: boolean;
isChatUnsynced: boolean; isChatUnsynced: boolean;
unreadMsg: Envelope | false; unreadMsg: Envelope | false;
stationPendingMessages: IMessage[]; stationPendingMessages: IMessage[];
mailboxSize: number; graph: Graph;
contacts: Contacts; contacts: Contacts;
association: Association; association: Association;
group: Group; group: Group;
@ -58,6 +57,7 @@ interface ChatWindowState {
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;
INITIALIZATION_MAX_TIME = 1500; INITIALIZATION_MAX_TIME = 1500;
@ -112,6 +112,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
} }
initialFetch() { initialFetch() {
/*
const { envelopes, mailboxSize, unreadCount } = this.props; const { envelopes, mailboxSize, unreadCount } = this.props;
if (envelopes.length > 0) { if (envelopes.length > 0) {
const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE); const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE);
@ -127,28 +128,37 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.initialFetch(); this.initialFetch();
}, 2000); }, 2000);
} }
*/
} }
componentDidUpdate(prevProps: ChatWindowProps, prevState) { componentDidUpdate(prevProps: ChatWindowProps, prevState) {
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount, station } = this.props; const { isChatMissing, history, graph, unreadCount, station } = this.props;
if (isChatMissing) { if (isChatMissing) {
history.push("/~404"); history.push("/~404");
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) { } else if (graph.size !== prevProps.graph.size && this.state.fetchPending) {
this.setState({ fetchPending: false }); this.setState({ fetchPending: false });
} }
if ((mailboxSize !== prevProps.mailboxSize) || (envelopes.length !== prevProps.envelopes.length)) { /*if ((mailboxSize !== prevProps.mailboxSize) || (envelopes.length !== prevProps.envelopes.length)) {
this.virtualList?.calculateVisibleItems(); this.virtualList?.calculateVisibleItems();
this.stayLockedIfActive(); this.stayLockedIfActive();
} }*/
if (unreadCount > prevProps.unreadCount && this.state.idle) { /*if (unreadCount > prevProps.unreadCount && this.state.idle) {
this.setState({ this.setState({
lastRead: unreadCount ? mailboxSize - unreadCount : -1, lastRead: unreadCount ? mailboxSize - unreadCount : -1,
}); });
} }*/
console.log(graph.size);
console.log(prevProps.graph.size);
if(this.prevSize !== graph.size) {
this.prevSize = graph.size;
this.virtualList?.calculateVisibleItems();
}
/*
if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) { if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) {
this.virtualList?.calculateVisibleItems(); this.virtualList?.calculateVisibleItems();
} }
@ -164,6 +174,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
lastRead: unreadCount ? mailboxSize - unreadCount : -1, lastRead: unreadCount ? mailboxSize - unreadCount : -1,
}); });
} }
*/
} }
stayLockedIfActive() { stayLockedIfActive() {
@ -174,16 +185,18 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
} }
scrollToUnread() { scrollToUnread() {
/*
const { mailboxSize, unreadCount, scrollTo } = this.props; const { mailboxSize, unreadCount, scrollTo } = this.props;
const target = scrollTo || (mailboxSize - unreadCount); const target = scrollTo || (mailboxSize - unreadCount);
this.virtualList?.scrollToData(target); this.virtualList?.scrollToData(target);
*/
} }
dismissUnread() { dismissUnread() {
if (this.state.fetchPending) return; if (this.state.fetchPending) return;
if (this.props.unreadCount === 0) return; if (this.props.unreadCount === 0) return;
this.props.api.chat.read(this.props.station); //this.props.api.chat.read(this.props.station);
this.props.api.hark.readIndex({ chat: { chat: this.props.station, mention: false }}); //this.props.api.hark.readIndex({ chat: { chat: this.props.station, mention: false }});
} }
fetchMessages(start, end, force = false): Promise<void> { fetchMessages(start, end, force = false): Promise<void> {
@ -235,7 +248,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
render() { render() {
const { const {
envelopes,
stationPendingMessages, stationPendingMessages,
unreadCount, unreadCount,
unreadMsg, unreadMsg,
@ -248,6 +260,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
group, group,
contacts, contacts,
mailboxSize, mailboxSize,
graph,
hideAvatars, hideAvatars,
hideNicknames, hideNicknames,
remoteContentPolicy, remoteContentPolicy,
@ -256,27 +269,12 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const unreadMarkerRef = this.unreadMarkerRef; const unreadMarkerRef = this.unreadMarkerRef;
const messages = new BigIntOrderedMap();
let lastMessage = 0; let lastMessage = 0;
[...envelopes]
.sort((a, b) => a.number - b.number)
.forEach(message => {
const num = bigInt(message.number);
messages.set(num, message);
lastMessage = message.number;
});
stationPendingMessages
.sort((a, b) => a.when - b.when)
.forEach((message, index) => {
const idx = bigInt(index + 1); // To 1-index it
messages.set(bigInt(mailboxSize).add(idx), message);
lastMessage = mailboxSize + index;
});
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, history, api }; const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, history, api };
const keys = graph.keys().reverse();
return ( return (
<> <>
<UnreadNotice <UnreadNotice
@ -296,10 +294,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.dismissUnread(); this.dismissUnread();
}} }}
onScroll={this.onScroll.bind(this)} onScroll={this.onScroll.bind(this)}
data={messages} data={graph}
size={mailboxSize + stationPendingMessages.length} size={graph.size}
renderer={({ index, measure, scrollWindow }) => { renderer={({ index, measure, scrollWindow }) => {
const msg: Envelope | IMessage = messages.get(index); const msg = graph.get(index)!.post;
if (!msg) return null; if (!msg) 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} />;
@ -309,11 +307,14 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const isLastRead: boolean = Boolean(!isLastMessage && index.eq(bigInt(this.state.lastRead))); const isLastRead: boolean = Boolean(!isLastMessage && index.eq(bigInt(this.state.lastRead)));
const highlighted = bigInt(this.props.scrollTo || -1).eq(index); const highlighted = bigInt(this.props.scrollTo || -1).eq(index);
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps }; const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
const graphIdx = keys.findIndex(idx => idx.eq(index));
const prevIdx = keys[graphIdx+1];
const nextIdx = keys[graphIdx-1];
return ( return (
<ChatMessage <ChatMessage
key={index.toString()} key={index.toString()}
previousMsg={messages.get(index.add(bigInt.one))} previousMsg={prevIdx && graph.get(prevIdx)?.post}
nextMsg={messages.get(index.subtract(bigInt.one))} nextMsg={nextIdx && graph.get(nextIdx)?.post}
{...props} {...props}
/> />
); );

View File

@ -37,7 +37,7 @@ export function MentionText(props: MentionTextProps) {
); );
} }
function Mention(props: { ship: string; contacts: Contacts }) { export function Mention(props: { ship: string; contacts: Contacts }) {
const { contacts, ship } = props; const { contacts, ship } = props;
const contact = contacts[ship]; const contact = contacts[ship];
const showNickname = !!contact?.nickname; const showNickname = !!contact?.nickname;

View File

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { PureComponent, Component } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import normalizeWheel from 'normalize-wheel'; import normalizeWheel from 'normalize-wheel';
@ -34,10 +34,10 @@ interface VirtualScrollerState {
scrollTop: number; scrollTop: number;
} }
export default class VirtualScroller extends PureComponent<VirtualScrollerProps, VirtualScrollerState> { export default class VirtualScroller extends Component<VirtualScrollerProps, VirtualScrollerState> {
private scrollContainer: React.RefObject<HTMLDivElement>; private scrollContainer: React.RefObject<HTMLDivElement>;
public window: HTMLDivElement | null; public window: HTMLDivElement | null;
private cache: BigIntOrderedMap<BigInteger, any>; private cache: BigIntOrderedMap<any>;
private pendingLoad: { private pendingLoad: {
start: BigInteger; start: BigInteger;
end: BigInteger end: BigInteger
@ -144,8 +144,8 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
//console.log([...items].map(([index]) => this.heightOf(index))); //console.log([...items].map(([index]) => this.heightOf(index)));
const list = [...data]; //const list = [...data];
console.log(list[0][0].toString()); //console.log(list[0][0].toString());
// console.log(list[list.length - 1][0].toString()); // console.log(list[list.length - 1][0].toString());
[...data].forEach(([index, datum]) => { [...data].forEach(([index, datum]) => {
const height = this.heightOf(index); const height = this.heightOf(index);

View File

@ -95,14 +95,11 @@ export function GroupsPane(props: GroupsPaneProps) {
string, string,
string string
>; >;
const appName = app as AppName;
const isGraph = appIsGraph(app);
const resource = `${isGraph ? "/ship" : ""}/${host}/${name}`; const appName = app as AppName;
const association =
isGraph const resource = `/ship/${host}/${name}`;
? associations.graph[resource] const association = associations.graph[resource]
: associations[appName][resource];
const resourceUrl = `${baseUrl}/resource/${app}${resource}`; const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
if (!association) { if (!association) {
@ -133,10 +130,8 @@ export function GroupsPane(props: GroupsPaneProps) {
path={relativePath("/join/:app/(ship)?/:host/:name")} path={relativePath("/join/:app/(ship)?/:host/:name")}
render={(routeProps) => { render={(routeProps) => {
const { app, host, name } = routeProps.match.params; const { app, host, name } = routeProps.match.params;
const appName = app as AppName; const appPath = `/ship/${host}/${name}`;
const isGraph = appIsGraph(app); const association = associations.graph[appPath];
const appPath = `${isGraph ? '/ship/' : '/'}${host}/${name}`;
const association = isGraph ? associations.graph[appPath] : associations[appName][appPath];
const resourceUrl = `${baseUrl}/join/${app}${appPath}`; const resourceUrl = `${baseUrl}/join/${app}${appPath}`;
if (!association) { if (!association) {

View File

@ -52,44 +52,22 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
const resId: string = stringToSymbol(values.name); const resId: string = stringToSymbol(values.name);
try { try {
const { name, description, moduleType, ships } = values; const { name, description, moduleType, ships } = values;
switch (moduleType) { if (group) {
case 'chat': await api.graph.createManagedGraph(
const appPath = `/~${window.ship}/${resId}`; resId,
const groupPath = group || `/ship${appPath}`; name,
description,
await api.chat.create( group,
name, moduleType
description, );
appPath, } else {
groupPath, await api.graph.createUnmanagedGraph(
{ invite: { pending: ships.map(s => `~${s}`) } }, resId,
ships.map(s => `~${s}`), name,
true, description,
false { invite: { pending: ships.map((s) => `~${s}`) } },
); moduleType
break; );
case "publish":
case "link":
if (group) {
await api.graph.createManagedGraph(
resId,
name,
description,
group,
moduleType
);
} else {
await api.graph.createUnmanagedGraph(
resId,
name,
description,
{ invite: { pending: ships.map((s) => `~${s}`) } },
moduleType
);
}
break;
default:
console.log('fallthrough');
} }
if (!group) { if (!group) {
@ -98,8 +76,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
actions.setStatus({ success: null }); actions.setStatus({ success: null });
const resourceUrl = parentPath(location.pathname); const resourceUrl = parentPath(location.pathname);
history.push( history.push(
`${resourceUrl}/resource/${moduleType}` + `${resourceUrl}/resource/${moduleType}/ship/~${window.ship}/${resId}`
`${moduleType !== 'chat' ? '/ship' : ''}/~${window.ship}/${resId}`
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);