mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 17:41:33 +03:00
interface: removed unused Chat components
This commit is contained in:
parent
1b73029bc9
commit
ba7055d1e4
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user