mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 10:02:47 +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()} />
|
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
||||||
: null}
|
: null}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Root background={background} >
|
<Root background={background}>
|
||||||
<Router>
|
<Router>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<StatusBarWithRouter
|
<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
|
<img
|
||||||
className="invert-d"
|
className="invert-d"
|
||||||
src="/~chat/img/ImageUpload.png"
|
src="/~landscape/img/ImageUpload.png"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
/>
|
/>
|
||||||
@ -238,7 +238,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
|||||||
width: '14px',
|
width: '14px',
|
||||||
}}
|
}}
|
||||||
onClick={this.toggleCode}
|
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" />
|
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,7 +127,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount } = this.props;
|
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount } = this.props;
|
||||||
|
|
||||||
if (isChatMissing) {
|
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) {
|
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) {
|
||||||
this.setState({ fetchPending: false });
|
this.setState({ fetchPending: false });
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export const BacklogElement = (props) => {
|
|||||||
"white-d flex items-center"
|
"white-d flex items-center"
|
||||||
}>
|
}>
|
||||||
<img className="invert-d spin-active v-mid"
|
<img className="invert-d spin-active v-mid"
|
||||||
src="/~chat/img/Spinner.png"
|
src="/~landscape/img/Spinner.png"
|
||||||
width={16}
|
width={16}
|
||||||
height={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>;
|
img = <Link to={`/~groups/view${association['group-path']}/${ship}`}>{img}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: create a chat DM
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={this.popoverRef}
|
ref={this.popoverRef}
|
||||||
@ -85,7 +87,7 @@ export class ProfileOverlay extends PureComponent {
|
|||||||
<div className="mono gray2">{cite(`~${ship}`)}</div>
|
<div className="mono gray2">{cite(`~${ship}`)}</div>
|
||||||
{!isOwn && (
|
{!isOwn && (
|
||||||
<Link
|
<Link
|
||||||
to={`/~chat/new/dm/~${ship}`}
|
to={`/~todo/~${ship}`}
|
||||||
className="b--green0 b--green2-d b--solid ba green2 mt3 tc pa2 pointer db"
|
className="b--green0 b--green2-d b--solid ba green2 mt3 tc pa2 pointer db"
|
||||||
>
|
>
|
||||||
Send Message
|
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 { Route, Switch } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { TwoPaneApp } from './TwoPaneApp';
|
|
||||||
import LaunchApp from '../apps/launch/app';
|
import LaunchApp from '../apps/launch/app';
|
||||||
import DojoApp from '../apps/dojo/app';
|
import DojoApp from '../apps/dojo/app';
|
||||||
import GroupsApp from '../apps/groups/app';
|
import GroupsApp from '../apps/groups/app';
|
||||||
@ -50,21 +49,6 @@ export const Content = (props) => {
|
|||||||
{...props} />
|
{...props} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path={[
|
|
||||||
'/~chat',
|
|
||||||
'/~publish',
|
|
||||||
'/~link',
|
|
||||||
'/~graph'
|
|
||||||
]}
|
|
||||||
render={ p => (
|
|
||||||
<TwoPaneApp
|
|
||||||
history={p.history}
|
|
||||||
location={p.location}
|
|
||||||
match={p.match}
|
|
||||||
{...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/~profile"
|
path="/~profile"
|
||||||
render={ p => (
|
render={ p => (
|
||||||
@ -73,8 +57,6 @@ export const Content = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
render={p => (
|
render={p => (
|
||||||
<ErrorComponent
|
<ErrorComponent
|
||||||
|
@ -503,7 +503,7 @@ export class InviteSearch extends Component<
|
|||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<img
|
<img
|
||||||
src='/~chat/img/search.png'
|
src='/~landscape/img/search.png'
|
||||||
className='absolute invert-d'
|
className='absolute invert-d'
|
||||||
style={{
|
style={{
|
||||||
height: 16,
|
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