interface: removed unused Chat components

This commit is contained in:
Logan Allen 2020-10-05 16:27:11 -05:00
parent 1b73029bc9
commit ba7055d1e4
25 changed files with 10 additions and 2369 deletions

View File

@ -131,7 +131,7 @@ class App extends React.Component {
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
: null}
</Helmet>
<Root background={background} >
<Root background={background}>
<Router>
<ErrorBoundary>
<StatusBarWithRouter

View File

@ -1,320 +0,0 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Helmet from 'react-helmet';
import './css/custom.css';
import { Skeleton } from './components/skeleton';
import { Sidebar } from './components/sidebar';
import { ChatScreen } from './components/chat';
import { SettingsScreen } from './components/settings';
import { NewScreen } from './components/new';
import { JoinScreen } from './components/join';
import { NewDmScreen } from './components/new-dm';
import { PatpNoSig } from '~/types/noun';
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import GlobalSubscription from '~/logic/subscription/global';
import {groupBunts} from '~/types/group-update';
type ChatAppProps = StoreState & {
ship: PatpNoSig;
api: GlobalApi;
subscription: GlobalSubscription;
};
export default class ChatApp extends React.Component<ChatAppProps, {}> {
constructor(props) {
super(props);
}
componentDidMount() {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.props.subscription.startApp('chat');
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
}
componentWillUnmount() {
this.props.subscription.stopApp('chat');
}
render() {
const { props } = this;
const messagePreviews = {};
const unreads = {};
let totalUnreads = 0;
const associations = props.associations
? props.associations
: { chat: {}, contacts: {} };
Object.keys(props.inbox).forEach((stat) => {
const envelopes = props.inbox[stat].envelopes;
if (envelopes.length === 0) {
messagePreviews[stat] = false;
} else {
messagePreviews[stat] = envelopes[0];
}
const unread = Math.max(
props.inbox[stat].config.length - props.inbox[stat].config.read,
0
);
unreads[stat] = Boolean(unread);
if (
unread &&
stat in associations.chat
) {
totalUnreads += unread;
}
});
const {
invites,
s3,
sidebarShown,
inbox,
contacts,
chatSynced,
api,
chatInitialized,
pendingMessages,
groups,
hideAvatars,
hideNicknames,
remoteContentPolicy
} = props;
const renderChannelSidebar = (props, station?) => (
<Sidebar
inbox={inbox}
messagePreviews={messagePreviews}
associations={associations}
contacts={contacts}
invites={invites['/chat'] || {}}
unreads={unreads}
api={api}
station={station}
{...props}
/>
);
return (
<>
<Helmet defer={false}>
<title>{totalUnreads > 0 ? `(${totalUnreads}) ` : ''}OS1 - Chat</title>
</Helmet>
<Switch>
<Route
exact
path="/~chat"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
chatHideonMobile={true}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props)}
>
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select, create, or join a chat to begin.
</p>
</div>
</div>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new/dm/:ship?"
render={(props) => {
const ship = props.match.params.ship;
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewDmScreen
api={api}
inbox={inbox}
groups={groups || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
autoCreate={ship}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewScreen
api={api}
inbox={inbox || {}}
groups={groups}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/join/:ship?/:station?"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
// ensure we know joined chats
if(!chatInitialized) {
return null;
}
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<JoinScreen
api={api}
inbox={inbox}
station={station}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/room/(~)?/:ship/:station+"
render={(props) => {
const station = `/${props.match.params.ship}/${props.match.params.station}`;
const mailbox = inbox[station] || {
config: {
read: 0,
length: 0
},
envelopes: []
};
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];
}
const association =
station in associations['chat'] ? associations.chat[station] : {};
const group = groups[association['group-path']] || groupBunts.group();
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<ChatScreen
chatSynced={chatSynced || {}}
station={station}
association={association}
api={api}
read={mailbox.config.read}
mailboxSize={mailbox.config.length}
envelopes={mailbox.envelopes}
inbox={inbox}
contacts={roomContacts}
group={group}
pendingMessages={pendingMessages}
s3={s3}
sidebarShown={sidebarShown}
chatInitialized={chatInitialized}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/settings/(~)?/:ship/:station+"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const association =
station in associations['chat'] ? associations.chat[station] : {};
const group = groups[association['group-path']] || groupBunts.group();
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<SettingsScreen
{...props}
station={station}
association={association}
groups={groups || {}}
group={group}
contacts={contacts || {}}
associations={associations.contacts}
api={api}
inbox={inbox}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
</Switch>
</>
);
}
}

View File

@ -1,195 +0,0 @@
import React, { Component } from "react";
import moment from "moment";
import { RouteComponentProps } from "react-router-dom";
import { deSig } from "~/logic/lib/util";
import { ChatHookUpdate } from "~/types/chat-hook-update";
import { Inbox, Envelope } from "~/types/chat-update";
import { Contacts } from "~/types/contact-update";
import { Path, Patp } from "~/types/noun";
import GlobalApi from "~/logic/api/global";
import { Association } from "~/types/metadata-update";
import {Group} from "~/types/group-update";
import { LocalUpdateRemoteContentPolicy } from "~/types";
import { SubmitDragger } from '~/views/components/s3-upload';
import ChatWindow from './lib/ChatWindow';
import ChatHeader from './lib/ChatHeader';
import ChatInput from "./lib/ChatInput";
type ChatScreenProps = RouteComponentProps<{
ship: Patp;
station: string;
}> & {
chatSynced: ChatHookUpdate;
station: any;
association: Association;
api: GlobalApi;
read: number;
mailboxSize: number;
inbox: Inbox;
contacts: Contacts;
group: Group;
pendingMessages: Map<Path, Envelope[]>;
s3: any;
sidebarShown: boolean;
chatInitialized: boolean;
envelopes: Envelope[];
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
};
interface ChatScreenState {
messages: Map<string, string>;
dragover: boolean;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
private chatInput: React.RefObject<ChatInput>;
lastNumPending = 0;
activityTimeout: NodeJS.Timeout | null = null;
constructor(props) {
super(props);
this.state = {
messages: new Map(),
dragover: false,
};
this.chatInput = React.createRef();
moment.updateLocale("en", {
calendar: {
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY",
},
});
}
readyToUpload(): boolean {
return Boolean(this.chatInput.current?.s3Uploader.current?.inputRef.current);
}
onDragEnter(event) {
if (!this.readyToUpload() || (!event.dataTransfer.files.length && !event.dataTransfer.types.includes('Files'))) {
return;
}
this.setState({ dragover: true });
}
onDrop(event: DragEvent) {
this.setState({ dragover: false });
event.preventDefault();
if (!event.dataTransfer || !event.dataTransfer.files.length) {
return;
}
if (event.dataTransfer.items.length && !event.dataTransfer.files.length) {
event.preventDefault();
return;
}
event.preventDefault();
this.chatInput.current?.uploadFiles(event.dataTransfer.files);
}
render() {
const { props, state } = this;
const lastMsgNum = props.envelopes.length > 0 ? props.envelopes.length : 0;
const ownerContact =
window.ship in props.contacts ? props.contacts[window.ship] : false;
const pendingMessages = (props.pendingMessages.get(props.station) || [])
.map((value) => ({
...value,
pending: true
}));
const isChatMissing =
props.chatInitialized &&
!(props.station in props.inbox) &&
props.chatSynced &&
!(props.station in props.chatSynced);
const isChatLoading =
props.chatInitialized &&
!(props.station in props.inbox) &&
props.chatSynced &&
(props.station in props.chatSynced);
const isChatUnsynced =
props.chatSynced &&
!(props.station in props.chatSynced) &&
props.envelopes.length > 0;
const unreadCount = props.mailboxSize - props.read;
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
return (
<div
key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column relative"
onDragEnter={this.onDragEnter.bind(this)}
onDragOver={event => {
event.preventDefault();
if (
!this.state.dragover
&& (
(event.dataTransfer.files.length && event.dataTransfer.files[0].kind === 'file')
|| (event.dataTransfer.items.length && event.dataTransfer.items[0].kind === 'file')
)
) {
this.setState({ dragover: true });
}
}}
onDragLeave={(event) => {
const over = document.elementFromPoint(event.clientX, event.clientY);
if (!over || !event.currentTarget.contains(over)) {
this.setState({ dragover: false });
}}
}
onDrop={this.onDrop.bind(this)}
>
{this.state.dragover ? <SubmitDragger /> : null}
<ChatHeader {...props} />
<div className="h-100 w-100 overflow-hidden flex flex-column relative">
<ChatWindow
isChatMissing={isChatMissing}
isChatLoading={isChatLoading}
isChatUnsynced={isChatUnsynced}
unreadCount={unreadCount}
unreadMsg={unreadMsg}
stationPendingMessages={pendingMessages}
ship={props.match.params.ship}
{...props} />
<ChatInput
ref={this.chatInput}
api={props.api}
numMsgs={lastMsgNum}
station={props.station}
owner={deSig(props.match.params.ship || "")}
ownerContact={ownerContact}
envelopes={props.envelopes}
contacts={props.contacts}
onUnmount={(msg: string) => this.setState({
messages: this.state.messages.set(props.station, msg)
})}
s3={props.s3}
placeholder="Message..."
message={this.state.messages.get(props.station) || ""}
deleteMessage={() => this.setState({
messages: this.state.messages.set(props.station, "")
})}
hideAvatars={props.hideAvatars}
/>
</div>
</div>
);
}
}

View File

@ -1,109 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Spinner } from '../../../components/Spinner';
import urbitOb from 'urbit-ob';
import { Box, Text, ManagedTextInputField as Input, Button } from '@tlon/indigo-react';
import { Formik, Form } from 'formik'
import * as Yup from 'yup';
const schema = Yup.object().shape({
station: Yup.string()
.lowercase()
.trim()
.test('is-station',
'Chat must have a valid name',
(val) =>
val &&
val.split('/').length === 2 &&
urbitOb.isValidPatp(val.split('/')[0])
)
.required('Required')
});
export class JoinScreen extends Component {
constructor(props) {
super(props);
this.state = {
awaiting: false
};
}
componentDidMount() {
if (this.props.station) {
this.onSubmit({ station: this.props.station });
}
}
onSubmit(values) {
const { props } = this;
this.setState({ awaiting: true }, () => {
const station = values.station.trim();
if (`/${station}` in props.chatSynced) {
if (props.station) {
props.history.replace(`/~chat/room${station}`);
} else {
props.history.push(`/~chat/room${station}`);
}
return;
}
const ship = station.substr(1).slice(0,station.substr(1).indexOf('/'));
props.api.chat.join(ship, station, true).then(() => {
if (props.station) {
props.history.replace(`/~chat/room${station}`);
} else {
props.history.push(`/~chat/room${station}`);
}
});
});
}
render() {
const { props, state } = this;
return (
<Formik
enableReinitialize={true}
initialValues={{ station: props.station }}
validationSchema={schema}
onSubmit={this.onSubmit.bind(this)}>
<Form>
<Box width="100%" height="100%" p={3} overflowX="hidden">
<Box
width="100%"
pt={1} pb={5}
display={['', 'none', 'none', 'none']}
fontSize={0}>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</Box>
<Text mb={3} fontSize={0}>Join Existing Chat</Text>
<Box width="100%" maxWidth={350}>
<Box mt={3} mb={3} display="block">
<Text display="inline" fontSize={0}>
Enter a{' '}
</Text>
<Text display="inline" fontSize={0} fontFamily="mono">
~ship/chat-name
</Text>
</Box>
<Input
mt={4}
id="station"
placeholder="~zod/chatroom"
fontFamily="mono"
caption="Chat names use lowercase, hyphens, and slashes." />
<Button>Join Chat</Button>
<Spinner
awaiting={this.state.awaiting}
classes="mt4"
text="Joining chat..." />
</Box>
</Box>
</Form>
</Formik>
);
}
}

View File

@ -1,59 +0,0 @@
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { deSig } from '~/logic/lib/util';
const ChatHeader = (props) => {
const group = Array.from(props.group.members);
let title = props.station.substr(1);
if (props.association &&
'metadata' in props.association &&
props.association.metadata.tile !== '') {
title = props.association.metadata.title;
}
return (
<Fragment>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className={
'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative ' +
'overflow-x-auto overflow-y-hidden flex-shrink-0 '
}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
api={props.api}
/>
<Link
to={'/~chat/' + 'room' + props.station}
className="pt2 white-d"
>
<h2
className={
'dib f9 fw4 lh-solid v-top ' +
(title === props.station.substr(1) ? 'mono' : '')
}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<TabBar
location={props.location}
settings={`/~chat/settings${props.station}`}
/>
</div>
</Fragment>
);
};
export default ChatHeader;

View File

@ -221,7 +221,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
>
<img
className="invert-d"
src="/~chat/img/ImageUpload.png"
src="/~landscape/img/ImageUpload.png"
width="16"
height="16"
/>
@ -238,7 +238,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
width: '14px',
}}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
src="/~landscape/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
</div>
</div>

View File

@ -127,7 +127,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount } = this.props;
if (isChatMissing) {
history.push("/~chat");
// TODO: fix this and push a different route
history.push("/~404");
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) {
this.setState({ fetchPending: false });
}

View File

@ -11,7 +11,7 @@ export const BacklogElement = (props) => {
"white-d flex items-center"
}>
<img className="invert-d spin-active v-mid"
src="/~chat/img/Spinner.png"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
/>

View File

@ -1,37 +0,0 @@
import React, { Component } from 'react';
export class ChannelItem extends Component {
constructor(props) {
super(props);
}
onClick() {
const { props } = this;
props.history.push('/~chat/room' + props.box);
}
render() {
const { props } = this;
const unreadElem = props.unread ? 'fw6 white-d' : '';
const title = props.title;
const selectedCss = props.selected
? 'bg-gray4 bg-gray1-d gray3-d c-default'
: 'bg-white bg-gray0-d gray3-d hover-bg-gray5 hover-bg-gray1-d pointer';
return (
<div
className={'z1 ph5 pv1 ' + selectedCss}
onClick={this.onClick.bind(this)}
>
<div className="w-100 v-mid">
<p className={'dib f9 ' + unreadElem}>
{title}
</p>
</div>
</div>
);
}
}

View File

@ -1,48 +0,0 @@
import React, { memo } from 'react';
import { Button, Text, Box } from '@tlon/indigo-react';
export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api, history }) => {
const deleteChat = () => {
changeLoading(
true,
true,
isOwner ? 'Deleting chat...' : 'Leaving chat...',
() => {
api.chat.delete(station).then(() => {
history.push('/~chat');
});
}
);
};
const groupPath = association['group-path'];
const unmanagedVillage = !contacts[groupPath];
return (
<Box width='100%'>
<Box width='100%' mt='3' opacity={(isOwner) ? '0.3' : '1'}>
<Text fontSize='1' mt='3' display='block' mb='1'>Leave Chat</Text>
<Text fontSize='0' gray display='block' mb='4'>
Remove this chat from your chat list.{' '}
{unmanagedVillage
? 'You will need to request for access again'
: 'You will need to join again from the group page'
}
</Text>
<Button onClick={(!isOwner) ? deleteChat : null}>
Leave this chat
</Button>
</Box>
<Box width='100%' mt='3' opacity={(isOwner) ? '0.3' : '1'}>
<Text display='block' fontSize='1' mt='3' mb='1'>Delete Chat</Text>
<Text display='block' gray fontSize='0' mb='4'>
Permanently delete this chat.{' '}
All current members will no longer see this chat.
</Text>
<Button destructive onClick={(isOwner) ? deleteChat : null}>Delete this chat</Button>
</Box>
</Box>
);
});
DeleteButton.displayName = 'DeleteButton';

View File

@ -1,100 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { ChannelItem } from './channel-item';
import { deSig, cite } from "~/logic/lib/util";
export class GroupItem extends Component {
render() {
const { props } = this;
const association = props.association ? props.association : {};
const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~(?:${window.ship}|${deSig(cite(window.ship))})( <-> )?)`);
let title = association['app-path'] ? association['app-path'] : 'Direct Messages';
if (association.metadata && association.metadata.title) {
title = association.metadata.title !== ''
? association.metadata.title
: title;
}
const channels = props.channels ? props.channels : [];
const first = (props.index === 0) ? 'mt1 ' : 'mt6 ';
const channelItems = channels.sort((a, b) => {
if (props.index === 'dm') {
const aPreview = props.messagePreviews[a];
const bPreview = props.messagePreviews[b];
const aWhen = aPreview ? aPreview.when : 0;
const bWhen = bPreview ? bPreview.when : 0;
return bWhen - aWhen;
} else {
const aAssociation = a in props.chatMetadata ? props.chatMetadata[a] : {};
const bAssociation = b in props.chatMetadata ? props.chatMetadata[b] : {};
let aTitle = a;
let bTitle = b;
if (aAssociation.metadata && aAssociation.metadata.title) {
aTitle = (aAssociation.metadata.title !== '')
? aAssociation.metadata.title : a;
}
if (bAssociation.metadata && bAssociation.metadata.title) {
bTitle =
bAssociation.metadata.title !== '' ? bAssociation.metadata.title : b;
}
return aTitle.toLowerCase().localeCompare(bTitle.toLowerCase());
}
}).map((each, i) => {
const unread = props.unreads[each];
let title = each.substr(1);
if (
each in props.chatMetadata &&
props.chatMetadata[each].metadata
) {
if (props.chatMetadata[each].metadata.title) {
title = props.chatMetadata[each].metadata.title
}
}
if (DEFAULT_TITLE_REGEX.test(title) && props.index === "dm") {
title = title.replace(DEFAULT_TITLE_REGEX, '');
}
const selected = props.station === each;
return (
<ChannelItem
key={i}
unread={unread}
title={title}
selected={selected}
box={each}
{...props}
/>
);
});
if (channelItems.length === 0) {
channelItems.push(<p className="gray2 mt4 f9 tc">No direct messages</p>);
}
let dmLink = <div />;
if (props.index === 'dm') {
dmLink = <Link
key="link"
className="absolute right-0 f9 top-0 mr4 green2 bg-gray5 bg-gray1-d b--transparent br1"
to="/~chat/new/dm"
style={{ padding: '0rem 0.2rem' }}
>
+ DM
</Link>;
}
return (
<div className={first + 'relative'}>
<p className="f9 ph4 gray3" key="p">{title}</p>
{dmLink}
{channelItems}
</div>
);
}
}
export default GroupItem;

View File

@ -1,102 +0,0 @@
import React, { Component } from 'react';
import Toggle from '~/views/components/toggle';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Button, Text, Box } from '@tlon/indigo-react';
export class GroupifyButton extends Component {
constructor(props) {
super(props);
this.state = {
inclusive: false,
targetGroup: null
};
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
renderInclusiveToggle(inclusive) {
return this.state.targetGroup ? (
<Box mt='4'>
<Toggle
boolean={this.state.inclusive}
change={this.changeInclusive.bind(this)}
/>
<Text display='inline-block' fontSize='0' ml='3'>
Add all members to group
</Text>
<Text display='block' fontSize='0' gray pt='1' pl='40px'>
Add chat members to the group if they aren't in it yet
</Text>
</Box>
) : <Box />;
}
render() {
const { inclusive, targetGroup } = this.state;
const {
api,
isOwner,
association,
associations,
contacts,
groups,
station,
changeLoading
} = this.props;
const groupPath = association['group-path'];
const ownedUnmanagedVillage =
isOwner &&
!contacts[groupPath];
if (!ownedUnmanagedVillage) {
return null;
}
return (
<Box width='100%' mt='3' maxWidth='29rem'>
<Text display='block' fontSize='1' mt='3' mb='1'>Convert Chat</Text>
<Text gray display='block' mb='4' fontSize='0'>
Convert this chat into a group with associated chat, or select a
group to add this chat to
</Text>
<InviteSearch
groups={groups}
contacts={contacts}
associations={associations}
groupResults={true}
shipResults={false}
invites={{
groups: targetGroup ? [targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup.bind(this)}
/>
{this.renderInclusiveToggle(inclusive)}
<Button mt='3' onClick={() => {
changeLoading(true, true, 'Converting to group...', () => {
api.chat.groupify(
station, targetGroup, inclusive
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}}
>Convert to group</Button>
</Box>
);
}
}

View File

@ -1,84 +0,0 @@
import React, { Component } from 'react';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Spinner } from '~/views/components/Spinner';
export class InviteElement extends Component {
constructor(props) {
super(props);
this.state = {
members: [],
error: false,
success: false,
awaiting: false
};
this.setInvite = this.setInvite.bind(this);
}
modifyMembers() {
const { props, state } = this;
const aud = state.members.map(mem => `~${mem}`);
if (state.members.length === 0) {
this.setState({
error: true,
success: false
});
return;
}
this.setState({
error: false,
success: true,
members: [],
awaiting: true
}, () => {
props.api.chatView.invite(props.path, aud).then(() => {
this.setState({ awaiting: false });
});
});
}
setInvite(invite) {
this.setState({ members: invite.ships });
}
render() {
const { props, state } = this;
let modifyButtonClasses = 'mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer';
if (state.error) {
modifyButtonClasses = modifyButtonClasses + ' gray3';
}
let buttonText = '';
if (props.permissions.kind === 'black') {
buttonText = 'Ban';
} else if (props.permissions.kind === 'white') {
buttonText = 'Invite';
}
return (
<div>
<InviteSearch
groups={{}}
contacts={props.contacts}
groupResults={false}
shipResults={true}
invites={{
groups: [],
ships: this.state.members
}}
setInvite={this.setInvite}
/>
<button
onClick={this.modifyMembers.bind(this)}
className={modifyButtonClasses}
>
{buttonText}
</button>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Inviting to chat..." />
</div>
);
}
}

View File

@ -1,55 +0,0 @@
import React, { Component } from 'react';
import { Sigil } from '../../../../lib/sigil';
import { uxToHex, cite } from '../../../../lib/util';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
props.api.groups.remove([`~${props.ship}`], props.path);
}
render() {
const { props } = this;
let actionElem;
if (props.ship === props.owner) {
actionElem = (
<p className="w-20 dib list-ship black white-d f8 c-default">
Host
</p>
);
} else if (window.ship !== props.ship && window.ship === props.owner) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-20 dib list-ship black white-d f8 pointer"
>
Ban
</a>
);
} else {
actionElem = (
<span></span>
);
}
const name = props.contact
? `${props.contact.nickname} (${cite(props.ship)})` : `${cite(props.ship)}`;
const color = props.contact ? uxToHex(props.contact.color) : '000000';
const img = (props.contact && (props.contact.avatar !== null))
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
return (
<div className="flex mb2">
{img}
<p className={
'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'
}
>{name}</p>
{actionElem}
</div>
);
}
}

View File

@ -69,6 +69,8 @@ export class ProfileOverlay extends PureComponent {
img = <Link to={`/~groups/view${association['group-path']}/${ship}`}>{img}</Link>;
}
// TODO: create a chat DM
return (
<div
ref={this.popoverRef}
@ -85,7 +87,7 @@ export class ProfileOverlay extends PureComponent {
<div className="mono gray2">{cite(`~${ship}`)}</div>
{!isOwn && (
<Link
to={`/~chat/new/dm/~${ship}`}
to={`/~todo/~${ship}`}
className="b--green0 b--green2-d b--solid ba green2 mt3 tc pa2 pointer db"
>
Send Message

View File

@ -1,371 +0,0 @@
import React, { Component } from 'react';
import _ from 'lodash';
import urbitOb from 'urbit-ob';
import Mousetrap from 'mousetrap';
import cn from 'classnames';
import { Sigil } from '../../../../lib/sigil';
import { hexToRgba, uxToHex, deSig } from '../../../../lib/util';
function ShipSearchItem({ ship, contacts, selected, onSelect }) {
const contact = contacts[ship];
let color = '#000000';
let sigilClass = 'v-mid mix-blend-diff';
let nickname;
const nameStyle = {};
const isSelected = ship === selected;
if (contact) {
const hex = uxToHex(contact.color);
color = `#${hex}`;
nameStyle.color = hexToRgba(hex, 0.7);
nameStyle.textShadow = '0px 0px 0px #000';
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
nameStyle.maxWidth = '200px';
sigilClass = 'v-mid';
nickname = contact.nickname;
}
return (
<div
onClick={() => onSelect(ship)}
className={cn(
'f9 pv1 ph3 pointer hover-bg-gray1-d hover-bg-gray4 relative flex items-center',
{
'white-d bg-gray0-d bg-white': !isSelected,
'black-d bg-gray1-d bg-gray4': isSelected
}
)}
key={ship}
>
<Sigil ship={'~' + ship} size={24} color={color} classes={sigilClass} />
{nickname && (
<p style={nameStyle} className="dib ml4 b truncate">
{nickname}
</p>
)}
<div className="mono gray2 gray4-d ml4">{'~' + ship}</div>
<p className="nowrap ml4">{status}</p>
</div>
);
}
export class ShipSearch extends Component {
constructor() {
super();
this.state = {
selected: null,
suggestions: [],
bound: false
};
this.keymap = {
Tab: cm =>
this.nextAutocompleteSuggestion(),
'Shift-Tab': cm =>
this.nextAutocompleteSuggestion(true),
'Up': cm =>
this.nextAutocompleteSuggestion(true),
'Escape': cm =>
this.props.onClear(),
'Down': cm =>
this.nextAutocompleteSuggestion(),
'Enter': (cm) => {
if(this.props.searchTerm !== null) {
this.props.onSelect(this.state.selected);
}
},
'Shift-3': cm =>
this.toggleCode()
};
}
componentDidMount() {
if(this.props.searchTerm !== null) {
this.updateSuggestions(true);
}
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if(!state.bound && props.inputRef) {
this.bindShortcuts();
}
if(props.searchTerm === null) {
if(state.suggestions.length > 0) {
this.setState({ suggestions: [] });
}
this.unbindShortcuts();
return;
}
if (
props.searchTerm === null &&
props.searchTerm !== prevProps.searchTerm &&
props.searchTerm.startsWith(prevProps.searchTerm)
) {
this.updateSuggestions();
} else if (prevProps.searchTerm !== props.searchTerm) {
this.updateSuggestions(true);
}
}
updateSuggestions(isStale = false) {
const needle = this.props.searchTerm;
const matchString = (hay) => {
hay = hay.toLowerCase();
return (
hay.startsWith(needle) ||
_.some(_.words(hay), s => s.startsWith(needle))
);
};
let candidates = this.state.suggestions;
if (isStale || this.state.suggestions.length === 0) {
const contacts = _.chain(this.props.contacts)
.defaultTo({})
.map((details, ship) => ({ ...details, ship }))
.filter(
({ nickname, ship }) => matchString(nickname) || matchString(ship)
)
.map('ship')
.value();
const exactMatch = urbitOb.isValidPatp(`~${needle}`) ? [needle] : [];
candidates = _.chain(this.props.candidates)
.defaultTo([])
.union(contacts)
.union(exactMatch)
.value();
}
const suggestions = _.chain(candidates)
.filter(matchString)
.filter(s => s.length < 28) // exclude comets
.value();
this.bindShortcuts();
this.setState({ suggestions, selected: suggestions[0] });
}
bindCmShortcuts() {
if(!this.props.cm) {
return;
}
this.props.cm.addKeyMap(this.keymap);
}
unbindCmShortcuts() {
if(!this.props.cm) {
return;
}
this.props.cm.removeKeyMap(this.keymap);
}
bindShortcuts() {
if (this.state.bound) {
return;
}
if (!this.props.inputRef) {
return this.bindCmShortcuts();
}
this.setState({ bound: true });
if (!this.mousetrap) {
this.mousetrap = new Mousetrap(this.props.inputRef);
}
this.mousetrap.bind('enter', (e) => {
e.preventDefault();
e.stopPropagation();
if (this.state.selected) {
this.unbindShortcuts();
this.props.onSelect(this.state.selected);
}
});
this.mousetrap.bind('tab', (e) => {
e.preventDefault();
e.stopPropagation();
this.nextAutocompleteSuggestion(false);
});
this.mousetrap.bind(['up', 'shift+tab'], (e) => {
e.preventDefault();
e.stopPropagation();
this.nextAutocompleteSuggestion(true);
});
this.mousetrap.bind('down', (e) => {
e.preventDefault();
e.stopPropagation();
this.nextAutocompleteSuggestion(false);
});
this.mousetrap.bind('esc', (e) => {
e.preventDefault();
e.stopPropagation();
this.props.onClear();
});
}
unbindShortcuts() {
if(!this.props.inputRef) {
this.unbindCmShortcuts();
}
if (!this.state.bound) {
return;
}
this.setState({ bound: false });
this.mousetrap.unbind('enter');
this.mousetrap.unbind('tab');
this.mousetrap.unbind(['up', 'shift+tab']);
this.mousetrap.unbind('down');
this.mousetrap.unbind('esc');
}
nextAutocompleteSuggestion(backward = false) {
const { suggestions } = this.state;
let idx = suggestions.findIndex(s => s === this.state.selected);
idx = backward ? idx - 1 : idx + 1;
idx = idx % Math.min(suggestions.length, 5);
if (idx < 0) {
idx = suggestions.length - 1;
}
this.setState({ selected: suggestions[idx] });
}
render() {
const { onSelect, contacts, popover, className } = this.props;
const { selected, suggestions } = this.state;
if (suggestions.length === 0) {
return null;
}
const popoverClasses = (popover && ' absolute ') || ' ';
return (
<div
style={
popover
? {
bottom: '90%',
left: '48px'
}
: {}
}
className={
'black white-d bg-white bg-gray0-d ' +
'w7 pv3 z-1 mt1 ba b--gray1-d b--gray4' +
popoverClasses +
className || ''
}
>
{suggestions.slice(0, 5).map(ship => (
<ShipSearchItem
onSelect={onSelect}
key={ship}
selected={selected}
contacts={contacts}
ship={ship}
/>
))}
</div>
);
}
}
export class ShipSearchInput extends Component {
constructor() {
super();
this.state = {
searchTerm: ''
};
this.inputRef = null;
this.popoverRef = null;
this.search = this.search.bind(this);
this.onClick = this.onClick.bind(this);
this.setInputRef = this.setInputRef.bind(this);
}
onClick(event) {
const { popoverRef } = this;
// Do nothing if clicking ref's element or descendent elements
if (!popoverRef || popoverRef.contains(event.target)) {
return;
}
this.props.onClear();
}
componentDidMount() {
document.addEventListener('mousedown', this.onClick);
document.addEventListener('touchstart', this.onClick);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onClick);
document.removeEventListener('touchstart', this.onClick);
}
setInputRef(ref) {
this.inputRef = ref;
if(ref) {
ref.focus();
}
// update this.inputRef prop
this.forceUpdate();
}
search(e) {
const searchTerm = e.target.value;
this.setState({ searchTerm });
}
render() {
const { state, props } = this;
return (
<div
ref={ref => (this.popoverRef = ref)}
style={{ top: '150%', left: '-80px' }}
className="b--gray2 b--solid ba absolute bg-white bg-gray0-d"
>
<textarea
style={{ resize: 'none', maxWidth: '200px' }}
className="ma2 pa2 b--gray4 ba b--solid w7 db bg-gray0-d white-d"
rows={1}
autocapitalise="none"
autoFocus={
/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
navigator.userAgent
)
? false
: true
}
placeholder="Search for a ship"
value={state.searchTerm}
onChange={this.search}
ref={this.setInputRef}
/>
<ShipSearch
contacts={props.contacts}
candidates={props.candidates}
searchTerm={deSig(state.searchTerm)}
inputRef={this.inputRef}
onSelect={props.onSelect}
onClear={props.onClear}
/>
</div>
);
}
}

View File

@ -1,42 +0,0 @@
import React, { Component } from 'react';
export class Welcome extends Component {
constructor() {
super();
this.state = {
show: true
};
this.disableWelcome = this.disableWelcome.bind(this);
}
disableWelcome() {
this.setState({ show: false });
localStorage.setItem('urbit-chat:wasWelcomed', JSON.stringify(true));
}
render() {
let wasWelcomed = localStorage.getItem('urbit-chat:wasWelcomed');
if (wasWelcomed === null) {
localStorage.setItem('urbit-chat:wasWelcomed', JSON.stringify(false));
wasWelcomed = false;
return wasWelcomed;
} else {
wasWelcomed = JSON.parse(wasWelcomed);
}
const inbox = this.props.inbox ? this.props.inbox : {};
return ((!wasWelcomed && this.state.show) && (inbox.length !== 0)) ? (
<div className="ma4 pa2 bg-welcome-green bg-gray1-d white-d">
<p className="f8 lh-copy">Chats are instant, linear modes of conversation. Many chats can be bundled under one group.</p>
<p className="f8 pt2 dib pointer bb"
onClick={(() => this.disableWelcome())}
>
Close this
</p>
</div>
) : <div />;
}
}
export default Welcome;

View File

@ -1,237 +0,0 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { InviteSearch } from '~/views/components/InviteSearch';
import urbitOb from 'urbit-ob';
import { deSig, cite } from '~/logic/lib/util';
export class NewDmScreen extends Component {
constructor(props) {
super(props);
this.state = {
ships: [],
station: null,
awaiting: false,
title: '',
idName: '',
description: ''
};
this.titleChange = this.titleChange.bind(this);
this.descriptionChange = this.descriptionChange.bind(this);
this.onClickCreate = this.onClickCreate.bind(this);
this.setInvite = this.setInvite.bind(this);
}
componentDidMount() {
const { props } = this;
if (props.autoCreate && urbitOb.isValidPatp(props.autoCreate)) {
const addedShip = this.state.ships;
addedShip.push(props.autoCreate.slice(1));
this.setState(
{
ships: addedShip,
awaiting: true
},
this.onClickCreate
);
}
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps !== props) {
const { station } = this.state;
if (station && station in props.inbox) {
this.setState({ awaiting: false });
props.history.push(`/~chat/room${station}`);
}
}
}
titleChange(event) {
const asciiSafe = event.target.value.toLowerCase()
.replace(/[^a-z0-9_-]/g, '-');
this.setState({
idName: asciiSafe,
title: event.target.value
});
}
descriptionChange(event) {
this.setState({
description: event.target.value
});
}
setInvite(value) {
this.setState({
ships: value.ships
});
}
onClickCreate() {
const { props, state } = this;
if (state.ships.length === 1) {
const station = `/~${window.ship}/dm--${state.ships[0]}`;
const theirStation = `/~${state.ships[0]}/dm--${window.ship}`;
if (station in props.inbox) {
props.history.push(`/~chat/room${station}`);
return;
}
if (theirStation in props.inbox) {
props.history.push(`/~chat/room${theirStation}`);
return;
}
const aud = state.ship !== window.ship ? [`~${state.ships[0]}`] : [];
let title = `${cite(window.ship)} <-> ${cite(state.ships[0])}`;
if (state.title !== '') {
title = state.title;
}
this.setState(
{
station, awaiting: true
},
() => {
const groupPath = `/ship/~${window.ship}/dm--${state.ships[0]}`;
props.api.chat.create(
title,
state.description,
station,
groupPath,
{ invite: { pending: aud } },
aud,
true,
false
);
}
);
}
if (state.ships.length > 1) {
const aud = state.ships.map(mem => `~${deSig(mem.trim())}`);
let title = 'Direct Message';
if (state.title !== '') {
title = state.title;
} else {
const asciiSafe = title.toLowerCase()
.replace(/[^a-z0-9~_.-]/g, '-');
this.setState({ idName: asciiSafe });
}
const station = `/~${window.ship}/${state.idName}-${Math.floor(Math.random() * 10000)}`;
this.setState(
{
station, awaiting: true
},
() => {
const groupPath = `/ship${station}`;
props.api.chat.create(
title,
state.description,
station,
groupPath,
{ invite: { pending: aud } },
aud,
true,
false
);
}
);
}
}
render() {
const { props, state } = this;
const createClasses = (state.idName || state.ships.length >= 1)
? 'pointer dib f9 green2 bg-gray0-d ba pv3 ph4 b--green2 mt4'
: 'pointer dib f9 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3 mt4';
const idClasses =
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 ' +
'focus-b--black focus-b--white-d mt1 ';
return (
<div
className={
'h-100 w-100 mw6 pa3 pt4 overflow-x-hidden ' +
'bg-gray0-d white-d flex flex-column'
}
>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<h2 className="mb3 f8">New Direct Message</h2>
<div className="w-100">
<p className="f8 mt4 db">
Name
<span className="gray3"> (Optional)</span>
</p>
<textarea
className={idClasses}
placeholder="The Passage"
rows={1}
style={{
resize: 'none'
}}
onChange={this.titleChange}
/>
<p className="f8 mt4 db">
Description
<span className="gray3"> (Optional)</span>
</p>
<textarea
className={idClasses}
placeholder="The most beautiful direct message"
rows={1}
style={{
resize: 'none'
}}
onChange={this.descriptionChange}
/>
<p className="f8 mt4 db">
Invite Members
</p>
<p className="f9 gray2 db mv1">
Selected ships will be invited to the direct message
</p>
<InviteSearch
groups={props.groups}
contacts={props.contacts}
associations={props.associations}
groupResults={false}
shipResults={true}
invites={{
groups: [],
ships: state.ships
}}
setInvite={this.setInvite}
/>
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}
>
Create Direct Message
</button>
<Spinner
awaiting={this.state.awaiting}
classes="mt4"
text="Creating Direct Message..."
/>
</div>
</div>
);
}
}

View File

@ -1,207 +0,0 @@
import React, { Component } from 'react';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { deSig } from '~/logic/lib/util';
export class NewScreen extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
description: '',
idName: '',
groups: [],
ships: [],
privacy: 'invite',
idError: false,
allowHistory: true,
createGroup: false,
awaiting: false
};
this.titleChange = this.titleChange.bind(this);
this.descriptionChange = this.descriptionChange.bind(this);
this.setInvite = this.setInvite.bind(this);
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (prevProps !== props) {
const station = `/~${window.ship}/${state.idName}`;
if (station in props.inbox) {
props.history.push('/~chat/room' + station);
}
}
}
titleChange(event) {
const asciiSafe = event.target.value.toLowerCase()
.replace(/[^a-z0-9_-]/g, '-');
this.setState({
idName: asciiSafe,
title: event.target.value
});
}
descriptionChange(event) {
this.setState({
description: event.target.value
});
}
setInvite(value) {
this.setState({
groups: value.groups,
ships: value.ships
});
}
onClickCreate() {
const { props, state } = this;
const grouped = (this.state.createGroup || (this.state.groups.length > 0));
if (!state.title) {
this.setState({
idError: true
});
return;
}
const station = `/${state.idName}` + (grouped ? `-${Math.floor(Math.random() * 10000)}` : '');
if (station in props.inbox) {
this.setState({
idError: true,
success: false
});
return;
}
const aud = state.ships.map(mem => `~${deSig(mem.trim())}`);
if (this.textarea) {
this.textarea.value = '';
}
const policy = state.privacy === 'invite' ? { invite: { pending: aud } } : { open: { banRanks: [], banned: [] } };
this.setState({
error: false,
success: true,
group: [],
ships: [],
awaiting: true
}, () => {
const appPath = `/~${window.ship}${station}`;
let groupPath = `/ship${appPath}`;
if (state.groups.length > 0) {
groupPath = state.groups[0];
}
const submit = props.api.chat.create(
state.title,
state.description,
appPath,
groupPath,
policy,
aud,
state.allowHistory,
state.createGroup
);
submit.then(() => {
this.setState({ awaiting: false });
props.history.push(`/~chat/room${appPath}`);
});
});
}
render() {
const { props, state } = this;
const createClasses = state.idName
? 'pointer db f9 green2 bg-gray0-d ba pv3 ph4 b--green2 mt4'
: 'pointer db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3 mt4';
const idClasses =
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 ' +
'focus-b--black focus-b--white-d mt1 ';
let idErrElem = (<span />);
if (state.idError) {
idErrElem = (
<span className="f9 inter red2 db pt2">
Chat must have a valid name.
</span>
);
}
return (
<div
className={
'h-100 w-100 mw6 pa3 pt4 overflow-x-hidden ' +
'bg-gray0-d white-d flex flex-column'
}
>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<h2 className="mb4 f8">New Group Chat</h2>
<div className="w-100">
<p className="f8 mt4 db">Name</p>
<textarea
className={idClasses}
placeholder="Secret Chat"
rows={1}
style={{
resize: 'none'
}}
onChange={this.titleChange}
/>
{idErrElem}
<p className="f8 mt4 db">
Description
<span className="gray3"> (Optional)</span>
</p>
<textarea
className={idClasses}
placeholder="The coolest chat"
rows={1}
style={{
resize: 'none'
}}
onChange={this.descriptionChange}
/>
<div className="mt4 db relative">
<p className="f8">
Select Group
</p>
<Link className="green2 absolute right-0 bottom-0 f9" to="/~groups/new">+New</Link>
<p className="f9 gray2 db mv1">
Chat will be added to selected group
</p>
</div>
<InviteSearch
groups={props.groups}
contacts={props.contacts}
associations={props.associations}
groupResults={true}
shipResults={false}
invites={{
groups: state.groups,
ships: []
}}
setInvite={this.setInvite}
/>
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}
>
Start Chat
</button>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating chat..." />
</div>
</div>
);
}
}

View File

@ -1,143 +0,0 @@
import React, { Component, Fragment } from 'react';
import { deSig } from '~/logic/lib/util';
import { MetadataSettings } from '~/views/components/metadata/settings';
import { Spinner } from '~/views/components/Spinner';
import ChatHeader from './lib/ChatHeader';
import { DeleteButton } from './lib/delete-button';
import { GroupifyButton } from './lib/groupify-button';
import { Text, Col, Box } from '@tlon/indigo-react';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
awaiting: false,
type: 'Editing chat...'
};
this.changeLoading = this.changeLoading.bind(this);
}
componentDidMount() {
if (this.state.isLoading && (this.props.station in this.props.inbox)) {
this.setState({ isLoading: false });
}
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (state.isLoading && !(props.station in props.inbox)) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~chat');
});
} else if (state.isLoading && (props.station in props.inbox)) {
this.setState({ isLoading: false });
}
}
changeLoading(isLoading, awaiting, type, closure) {
this.setState({
isLoading,
awaiting,
type
}, closure);
}
renderLoading() {
return (
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
);
}
renderNormal() {
const {
associations,
association,
contacts,
groups,
api,
station,
match,
history
} = this.props;
const isOwner = deSig(match.params.ship) === window.ship;
return (
<Fragment>
<Text display='block' pb='2' fontSize='1'>Chat Settings</Text>
<GroupifyButton
isOwner={isOwner}
association={association}
associations={associations}
contacts={contacts}
groups={groups}
api={api}
station={station}
changeLoading={this.changeLoading} />
<DeleteButton
isOwner={isOwner}
changeLoading={this.changeLoading}
station={station}
association={association}
contacts={contacts}
history={history}
api={api} />
<MetadataSettings
isOwner={isOwner}
changeLoading={this.changeLoading}
api={api}
association={association}
resource="chat"
app="chat"
module=""
/>
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
</Fragment>
);
}
render() {
const { state } = this;
const {
api,
group,
association,
station,
sidebarShown,
match,
location
} = this.props;
return (
<Col height='100%' width='100%' overflowX='hidden'>
<ChatHeader
match={match}
location={location}
api={api}
group={group}
association={association}
station={station}
sidebarShown={sidebarShown}
/>
<Box width='100%' pl='3' mt='3'>
{(state.isLoading) ? this.renderLoading() : this.renderNormal() }
</Box>
</Col>
);
}
}

View File

@ -1,114 +0,0 @@
import React, { Component } from 'react';
import Welcome from './lib/welcome';
import { alphabetiseAssociations } from '~/logic/lib/util';
import SidebarInvite from '~/views/components/Sidebar/SidebarInvite';
import { GroupItem } from './lib/group-item';
export class Sidebar extends Component {
onClickNew() {
this.props.history.push('/~chat/new');
}
render() {
const { props } = this;
const contactAssoc =
(props.associations && 'contacts' in props.associations)
? alphabetiseAssociations(props.associations.contacts) : {};
const chatAssoc =
(props.associations && 'chat' in props.associations)
? alphabetiseAssociations(props.associations.chat) : {};
const groupedChannels = {};
Object.keys(props.inbox).map((box) => {
const path = chatAssoc[box]
? chatAssoc[box]['group-path'] : box;
if (path in contactAssoc) {
if (groupedChannels[path]) {
const array = groupedChannels[path];
array.push(box);
groupedChannels[path] = array;
} else {
groupedChannels[path] = [box];
}
} else {
if (groupedChannels['dm']) {
const array = groupedChannels['dm'];
array.push(box);
groupedChannels['dm'] = array;
} else {
groupedChannels['dm'] = [box];
}
}
});
const sidebarInvites = Object.keys(props.invites)
.map((uid) => {
return (
<SidebarInvite
key={uid}
invite={props.invites[uid]}
onAccept={() => props.api.invite.accept('/chat', uid)}
onDecline={() => props.api.invite.decline('/chat', uid)}
/>
);
});
const groupedItems = Object.keys(contactAssoc)
.filter(each => (groupedChannels[each] || []).length !== 0)
.map((each, i) => {
const channels = groupedChannels[each] || [];
return(
<GroupItem
key={i}
index={i}
association={contactAssoc[each]}
chatMetadata={chatAssoc}
channels={channels}
inbox={props.inbox}
station={props.station}
unreads={props.unreads}
{...props}
/>
);
});
// add direct messages after groups
groupedItems.push(
<GroupItem
association={'dm'}
chatMetadata={chatAssoc}
channels={groupedChannels['dm']}
inbox={props.inbox}
station={props.station}
unreads={props.unreads}
index={'dm'}
key={'dm'}
{...props}
/>
);
return (
<div
className={`h-100-minus-96-s h-100 w-100 overflow-x-hidden flex
bg-gray0-d flex-column relative z1 lh-solid`}
>
<div className="w-100 bg-transparent pa4">
<a
className="dib f9 pointer green2 gray4-d mr4"
onClick={this.onClickNew.bind(this)}
>
New Group Chat
</a>
</div>
<div className="overflow-y-auto h-100">
<Welcome inbox={props.inbox} />
{sidebarInvites}
{groupedItems}
</div>
</div>
);
}
}

View File

@ -1,66 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import ErrorBoundary from '~/views/components/ErrorBoundary';
export class Skeleton extends Component {
render() {
// sidebar and chat panel conditional classes
const sidebarHide = (!this.props.sidebarShown)
? 'dn' : '';
const sidebarHideOnMobile = this.props.sidebarHideOnMobile
? 'dn-s' : '';
const chatHideOnMobile = this.props.chatHideonMobile
? 'dn-s' : '';
// mobile-specific navigation classes
const mobileNavClasses = classnames({
'dn': this.props.chatHideOnMobile,
'db dn-m dn-l dn-xl': !this.props.chatHideOnMobile,
'w-100 inter pt4 f8': !this.props.chatHideOnMobile
});
return (
// app outer skeleton
<div className='h-100 w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl'>
{/* app window borders */}
<div className='bg-white bg-gray0-d cf w-100 flex h-100 ba-m ba-l ba-xl b--gray4 b--gray1-d br1'>
{/* sidebar skeleton, hidden on mobile when in chat panel */}
<div
className={
`fl h-100 br b--gray4 b--gray1-d overflow-x-hidden
flex-basis-full-s flex-basis-250-m flex-basis-250-l
flex-basis-250-xl ` +
sidebarHide +
' ' +
sidebarHideOnMobile
}
>
{/* mobile-specific navigation */}
<div className={mobileNavClasses}>
<div className="bb b--gray4 b--gray1-d white-d inter f8 pl3 pb3">
All Chats
</div>
</div>
{/* sidebar component inside the sidebar skeleton */}
{this.props.sidebar}
</div>
{/* right-hand panel for chat, members, settings */}
<div
className={'h-100 fr ' + chatHideOnMobile}
style={{
flexGrow: 1,
width: 'calc(100% - 300px)'
}}
>
<ErrorBoundary>
{this.props.children}
</ErrorBoundary>
</div>
</div>
</div>
);
}
}

View File

@ -2,7 +2,6 @@ import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import styled from 'styled-components';
import { TwoPaneApp } from './TwoPaneApp';
import LaunchApp from '../apps/launch/app';
import DojoApp from '../apps/dojo/app';
import GroupsApp from '../apps/groups/app';
@ -50,21 +49,6 @@ export const Content = (props) => {
{...props} />
)}
/>
<Route
path={[
'/~chat',
'/~publish',
'/~link',
'/~graph'
]}
render={ p => (
<TwoPaneApp
history={p.history}
location={p.location}
match={p.match}
{...props} />
)}
/>
<Route
path="/~profile"
render={ p => (
@ -73,8 +57,6 @@ export const Content = (props) => {
/>
)}
/>
<Route
render={p => (
<ErrorComponent

View File

@ -503,7 +503,7 @@ export class InviteSearch extends Component<
return (
<div className='relative'>
<img
src='/~chat/img/search.png'
src='/~landscape/img/search.png'
className='absolute invert-d'
style={{
height: 16,

View File

@ -1,55 +0,0 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import LinksApp from '../apps/links/app';
import PublishApp from '../apps/publish/app';
import ChatApp from '../apps/chat/app';
import GraphApp from '../apps/graph/app';
export const TwoPaneApp = (props) => {
return (
<Switch>
<Route
path='/~chat'
render={p => (
<ChatApp
location={p.location}
match={p.match}
history={p.history}
{...props} />
)}
/>
<Route
path='/~link'
render={p => (
<LinksApp
location={p.location}
match={p.match}
history={p.history}
{...props} />
)}
/>
<Route
path='/~publish'
render={p => (
<PublishApp
location={p.location}
match={p.match}
history={p.history}
{...props} />
)}
/>
<Route
path='/~graph'
render={p => (
<GraphApp
location={p.location}
match={p.match}
history={p.history}
{...props} />
)}
/>
</Switch>
);
}