mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 01:52:42 +03:00
landscape: migrate global components to indigo-react
Fixes urbit/landscape#50.
This commit is contained in:
parent
2834d6b2e6
commit
d9d7edf720
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Text, Box, Col } from '@tlon/indigo-react';
|
import { Text, Box, Col, Button, BaseAnchor } from '@tlon/indigo-react';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@ -13,6 +13,8 @@ const Summary = styled.summary`
|
|||||||
color: ${ p => p.theme.colors.black };
|
color: ${ p => p.theme.colors.black };
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Details = styled.details``;
|
||||||
|
|
||||||
class ErrorComponent extends Component<ErrorProps> {
|
class ErrorComponent extends Component<ErrorProps> {
|
||||||
render () {
|
render () {
|
||||||
const { code, error, history, description } = this.props;
|
const { code, error, history, description } = this.props;
|
||||||
@ -25,20 +27,20 @@ class ErrorComponent extends Component<ErrorProps> {
|
|||||||
</Box>
|
</Box>
|
||||||
{ description && (<Box mb={4}><Text>{description}</Text></Box>) }
|
{ description && (<Box mb={4}><Text>{description}</Text></Box>) }
|
||||||
{error && (
|
{error && (
|
||||||
<Box mb={4} style={{maxWidth: '100%'}}>
|
<Box mb={4} style={{ maxWidth: '100%' }}>
|
||||||
<Box mb={2}>
|
<Box mb={2}>
|
||||||
<Text fontFamily="mono"><code>“{error.message}”</code></Text>
|
<Text mono>“{error.message}”</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<details>
|
<Details>
|
||||||
<Summary>Stack trace</Summary>
|
<Summary>Stack trace</Summary>
|
||||||
<Text><pre style={{ wordWrap: 'break-word', overflowX: 'scroll' }} className="tl">{error.stack}</pre></Text>
|
<Text mono p='1' borderRadius='1' display='block' overflow='auto' backgroundColor='washedGray' style={{ whiteSpace: 'pre', wordWrap: 'break-word' }}>{error.stack}</Text>
|
||||||
</details>
|
</Details>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Text mb={4} textAlign="center">If this is unexpected, email <code>support@tlon.io</code> or <a className="bb" href="https://github.com/urbit/urbit/issues/new/choose">submit an issue</a>.</Text>
|
<Text mb={4} textAlign="center">If this is unexpected, email <code>support@tlon.io</code> or <BaseAnchor color='black' href="https://github.com/urbit/urbit/issues/new/choose">submit an issue</BaseAnchor>.</Text>
|
||||||
{history.length > 1
|
{history.length > 1
|
||||||
? <button className="bg-light-green green2 pa2 pointer" onClick={() => history.go(-1) }>Go back</button>
|
? <Button primary onClick={() => history.go(-1) }>Go back</Button>
|
||||||
: <button className="bg-light-green green2 pa2 pointer" onClick={() => history.push('/') }>Go home</button>
|
: <Button primary onClick={() => history.push('/') }>Go home</Button>
|
||||||
}
|
}
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
@ -1,350 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import _, { capitalize } from 'lodash';
|
|
||||||
import { Virtuoso as VirtualList } from 'react-virtuoso';
|
|
||||||
|
|
||||||
import { cite, deSig } from '~/logic/lib/util';
|
|
||||||
import { roleForShip, resourceFromPath } from '~/logic/lib/group';
|
|
||||||
import {
|
|
||||||
Group,
|
|
||||||
InvitePolicy,
|
|
||||||
OpenPolicy,
|
|
||||||
roleTags,
|
|
||||||
Groups,
|
|
||||||
} from '~/types/group-update';
|
|
||||||
import { Path, PatpNoSig, Patp } from '~/types/noun';
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
|
||||||
import { Menu, MenuButton, MenuList, MenuItem, Text } from '@tlon/indigo-react';
|
|
||||||
import InviteSearch, { Invites } from './InviteSearch';
|
|
||||||
import { Spinner } from './Spinner';
|
|
||||||
import { Rolodex } from '~/types/contact-update';
|
|
||||||
import { Associations } from '~/types/metadata-update';
|
|
||||||
|
|
||||||
class GroupMember extends Component<{ ship: Patp; options: any[] }, {}> {
|
|
||||||
render() {
|
|
||||||
const { ship, options, children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex justify-between f9 items-center'>
|
|
||||||
<div className='flex flex-column flex-shrink-0'>
|
|
||||||
<Text mono mr='2'>{`${cite(ship)}`}</Text>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{options.length > 0 && (
|
|
||||||
<Menu>
|
|
||||||
<MenuButton width='min-content'>Options</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{options.map(({ onSelect, text }) => (
|
|
||||||
<MenuItem onSelect={onSelect}><Text fontsize='0'>{text}</Text></MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tag extends Component<{ description: string; onRemove?: () => any }, {}> {
|
|
||||||
render() {
|
|
||||||
const { description, onRemove } = this.props;
|
|
||||||
return (
|
|
||||||
<div className='br-pill ba b-black b--white-d r-full items-center ph2 f9 mr2 flex'>
|
|
||||||
<Text>{description}</Text>
|
|
||||||
{Boolean(onRemove) && (
|
|
||||||
<Text onClick={onRemove} ml='1' style={{ cursor: 'pointer' }}>
|
|
||||||
✗
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupViewAppTag {
|
|
||||||
tag: string;
|
|
||||||
app: string;
|
|
||||||
desc: string;
|
|
||||||
addDesc: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupViewProps {
|
|
||||||
group: Group;
|
|
||||||
groups: Groups;
|
|
||||||
contacts: Rolodex;
|
|
||||||
associations: Associations;
|
|
||||||
resourcePath: Path;
|
|
||||||
appTags?: GroupViewAppTag[];
|
|
||||||
api: GlobalApi;
|
|
||||||
className: string;
|
|
||||||
permissions?: boolean;
|
|
||||||
inviteShips: (ships: PatpNoSig[]) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GroupView extends Component<
|
|
||||||
GroupViewProps,
|
|
||||||
{ invites: Invites; awaiting: boolean }
|
|
||||||
> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.setInvites = this.setInvites.bind(this);
|
|
||||||
this.inviteShips = this.inviteShips.bind(this);
|
|
||||||
this.state = {
|
|
||||||
invites: {
|
|
||||||
ships: [],
|
|
||||||
groups: [],
|
|
||||||
},
|
|
||||||
awaiting: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
removeUser(who: PatpNoSig) {
|
|
||||||
return () => {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
this.props.api.groups.remove(resource, [`~${who}`]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
banUser(who: PatpNoSig) {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
this.props.api.groups.changePolicy(resource, {
|
|
||||||
open: {
|
|
||||||
banShips: [`~${who}`],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
allowUser(who: PatpNoSig) {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
this.props.api.groups.changePolicy(resource, {
|
|
||||||
open: {
|
|
||||||
allowShips: [`~${who}`],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeInvite(who: PatpNoSig) {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
this.props.api.groups.changePolicy(resource, {
|
|
||||||
invite: {
|
|
||||||
removeInvites: [`~${who}`],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTag(who: PatpNoSig, tag: any) {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
|
|
||||||
return this.props.api.groups.removeTag(resource, tag, [`~${who}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
addTag(who: PatpNoSig, tag: any) {
|
|
||||||
const resource = resourceFromPath(this.props.resourcePath);
|
|
||||||
return this.props.api.groups.addTag(resource, tag, [`~${who}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin(): boolean {
|
|
||||||
const role = roleForShip(this.props.group, window.ship);
|
|
||||||
return role === 'admin';
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsForShip(ship: Patp, missing: GroupViewAppTag[]) {
|
|
||||||
const { permissions, resourcePath, group } = this.props;
|
|
||||||
const resource = resourceFromPath(resourcePath);
|
|
||||||
let options: any[] = [];
|
|
||||||
if (!permissions) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
const role = roleForShip(group, ship);
|
|
||||||
const myRole = roleForShip(group, window.ship);
|
|
||||||
if (role === 'admin' || resource.ship === ship) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
'open' in group.policy // If blacklist, not whitelist
|
|
||||||
&& (this.isAdmin()) // And we can ban people (TODO: add || role === 'moderator')
|
|
||||||
&& ship !== window.ship // We can't ban ourselves
|
|
||||||
) {
|
|
||||||
options.unshift({ text: 'Ban', onSelect: () => this.banUser(ship) });
|
|
||||||
}
|
|
||||||
if (this.isAdmin() && !role) {
|
|
||||||
options = options.concat(
|
|
||||||
missing.map(({ addDesc, tag, app }) => ({
|
|
||||||
text: addDesc,
|
|
||||||
onSelect: () => this.addTag(ship, { tag, app }),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
options = options.concat(
|
|
||||||
roleTags.reduce(
|
|
||||||
(acc, role) => [
|
|
||||||
...acc,
|
|
||||||
{
|
|
||||||
text: `Make ${capitalize(role)}`,
|
|
||||||
onSelect: () => this.addTag(ship, { tag: role }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[] as any[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
doIfAdmin<Ret>(f: () => Ret) {
|
|
||||||
return this.isAdmin() ? f : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAppTags(ship: Patp): [GroupViewAppTag[], GroupViewAppTag[]] {
|
|
||||||
const { tags } = this.props.group;
|
|
||||||
const { appTags } = this.props;
|
|
||||||
|
|
||||||
return _.partition(appTags, ({ app, tag }) => {
|
|
||||||
return tags?.[app]?.[tag]?.has(ship);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
memberElements() {
|
|
||||||
const { group, permissions } = this.props;
|
|
||||||
const { members } = group;
|
|
||||||
const isAdmin = this.isAdmin();
|
|
||||||
return Array.from(members).map((ship) => {
|
|
||||||
const role = roleForShip(group, deSig(ship));
|
|
||||||
const onRoleRemove =
|
|
||||||
role && isAdmin
|
|
||||||
? () => {
|
|
||||||
this.removeTag(ship, { tag: role });
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
const [present, missing] = this.getAppTags(ship);
|
|
||||||
const options = this.optionsForShip(ship, missing);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GroupMember ship={ship} options={options}>
|
|
||||||
{((permissions && role) || present.length > 0) && (
|
|
||||||
<div className='flex mt1'>
|
|
||||||
{role && (
|
|
||||||
<Tag
|
|
||||||
onRemove={onRoleRemove}
|
|
||||||
description={capitalize(role)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{present.map((tag, idx) => (
|
|
||||||
<Tag
|
|
||||||
key={idx}
|
|
||||||
onRemove={this.doIfAdmin(() =>
|
|
||||||
this.removeTag(ship, tag)
|
|
||||||
)}
|
|
||||||
description={tag.desc}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</GroupMember>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setInvites(invites: Invites) {
|
|
||||||
this.setState({ invites });
|
|
||||||
}
|
|
||||||
|
|
||||||
inviteShips(invites: Invites) {
|
|
||||||
const { props, state } = this;
|
|
||||||
this.setState({ awaiting: true });
|
|
||||||
props.inviteShips(invites.ships).then(() => {
|
|
||||||
this.setState({ invites: { ships: [], groups: [] }, awaiting: false });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderInvites(policy: InvitePolicy) {
|
|
||||||
const { props, state } = this;
|
|
||||||
const ships = Array.from(policy.invite.pending || []);
|
|
||||||
|
|
||||||
const options = (ship: Patp) => [
|
|
||||||
{ text: 'Uninvite', onSelect: () => this.removeInvite(ship) },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-column'>
|
|
||||||
<div className='f9 gray2 mt6 mb3'>Pending</div>
|
|
||||||
{ships.map((ship) => (
|
|
||||||
<GroupMember key={ship} ship={ship} options={options(ship)} />
|
|
||||||
))}
|
|
||||||
{ships.length === 0 && <Text>No ships are pending</Text>}
|
|
||||||
{props.inviteShips && this.isAdmin() && (
|
|
||||||
<>
|
|
||||||
<div className='f9 gray2 mt6 mb3'>Invite</div>
|
|
||||||
<div style={{ width: 'calc(min(400px, 100%)' }}>
|
|
||||||
<InviteSearch
|
|
||||||
groups={props.groups}
|
|
||||||
contacts={props.contacts}
|
|
||||||
shipResults
|
|
||||||
groupResults={false}
|
|
||||||
invites={state.invites}
|
|
||||||
setInvite={this.setInvites}
|
|
||||||
associations={props.associations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
onClick={() => this.inviteShips(state.invites)}
|
|
||||||
className='db ba tc w-auto mr-auto mt2 ph2 black white-d f8 pointer'
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBanned(policy: OpenPolicy) {
|
|
||||||
const ships = Array.from(policy.open.banned || []);
|
|
||||||
|
|
||||||
const options = (ship: Patp) => [
|
|
||||||
{ text: 'Unban', onSelect: () => this.allowUser(ship) },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-column'>
|
|
||||||
<div className='f9 gray2 mt6 mb3'>Banned</div>
|
|
||||||
{ships.map((ship) => (
|
|
||||||
<GroupMember key={ship} ship={ship} options={options(ship)} />
|
|
||||||
))}
|
|
||||||
{ships.length === 0 && <Text>No ships are banned</Text>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { group, resourcePath, className } = this.props;
|
|
||||||
const resource = resourceFromPath(resourcePath);
|
|
||||||
const memberElements = this.memberElements();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className='flex flex-column'>
|
|
||||||
<Text gray display='block'>Host</Text>
|
|
||||||
<div className='flex justify-between mt3'>
|
|
||||||
<Text mono mr='2'>{cite(resource.ship)}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{'invite' in group.policy && this.renderInvites(group.policy)}
|
|
||||||
{'open' in group.policy && this.renderBanned(group.policy)}
|
|
||||||
<div className='flex flex-column'>
|
|
||||||
<div className='f9 gray2 mt6 mb3'>Members</div>
|
|
||||||
<VirtualList
|
|
||||||
style={{ height: '500px', width: '100%' }}
|
|
||||||
totalCount={memberElements.length}
|
|
||||||
item={(index) => <div key={index} className='flex flex-column pv3'>{memberElements[index]}</div>}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
awaiting={this.state.awaiting}
|
|
||||||
classes='mt4'
|
|
||||||
text='Inviting to chat...'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Label,
|
Label,
|
||||||
ErrorLabel,
|
ErrorLabel,
|
||||||
|
BaseInput
|
||||||
} from "@tlon/indigo-react";
|
} from "@tlon/indigo-react";
|
||||||
import { useField } from "formik";
|
import { useField } from "formik";
|
||||||
import { S3State } from "~/types/s3-update";
|
import { S3State } from "~/types/s3-update";
|
||||||
@ -75,7 +76,7 @@ export function ImageInput(props: ImageInputProps) {
|
|||||||
>
|
>
|
||||||
{uploading ? "Uploading" : "Upload"}
|
{uploading ? "Uploading" : "Upload"}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<BaseInput
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
type="file"
|
type="file"
|
||||||
id="fileElement"
|
id="fileElement"
|
||||||
|
@ -1,540 +0,0 @@
|
|||||||
import React, { Component, createRef } from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import Mousetrap from 'mousetrap';
|
|
||||||
import urbitOb from 'urbit-ob';
|
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
|
||||||
import { PatpNoSig, Path } from '~/types/noun';
|
|
||||||
import { Groups} from '~/types/group-update';
|
|
||||||
import { Rolodex, Contact } from '~/types/contact-update';
|
|
||||||
import { Associations } from '~/types/metadata-update';
|
|
||||||
|
|
||||||
export interface Invites {
|
|
||||||
ships: PatpNoSig[];
|
|
||||||
groups: string[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InviteSearchProps {
|
|
||||||
groups: Groups;
|
|
||||||
contacts: Rolodex;
|
|
||||||
groupResults: boolean;
|
|
||||||
shipResults: boolean;
|
|
||||||
invites: Invites;
|
|
||||||
setInvite: (inv: Invites) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
associations?: Associations;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InviteSearchState {
|
|
||||||
groups: string[][];
|
|
||||||
peers: PatpNoSig[];
|
|
||||||
contacts: Map<PatpNoSig, string[]>;
|
|
||||||
searchValue: string;
|
|
||||||
searchResults: Invites;
|
|
||||||
selected: PatpNoSig | Path | null;
|
|
||||||
inviteError: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InviteSearch extends Component<
|
|
||||||
InviteSearchProps,
|
|
||||||
InviteSearchState
|
|
||||||
> {
|
|
||||||
textarea: React.RefObject<HTMLTextAreaElement> = createRef();
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
groups: [],
|
|
||||||
peers: [],
|
|
||||||
contacts: new Map(),
|
|
||||||
searchValue: '',
|
|
||||||
searchResults: {
|
|
||||||
groups: [],
|
|
||||||
ships: [],
|
|
||||||
},
|
|
||||||
selected: null,
|
|
||||||
inviteError: false,
|
|
||||||
};
|
|
||||||
this.search = this.search.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.peerUpdate();
|
|
||||||
this.bindShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps !== this.props) {
|
|
||||||
this.peerUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peerUpdate() {
|
|
||||||
const groups = Array.from(Object.keys(this.props.contacts))
|
|
||||||
.filter((e) => !e.startsWith('/~/'))
|
|
||||||
.map((e) => {
|
|
||||||
const eachGroup: Path[] = [];
|
|
||||||
eachGroup.push(e);
|
|
||||||
if (this.props.associations) {
|
|
||||||
let name = e;
|
|
||||||
if (e in this.props.associations) {
|
|
||||||
name =
|
|
||||||
this.props.associations[e].metadata.title !== ''
|
|
||||||
? this.props.associations[e].metadata.title
|
|
||||||
: e;
|
|
||||||
}
|
|
||||||
eachGroup.push(name);
|
|
||||||
}
|
|
||||||
return Array.from(eachGroup);
|
|
||||||
});
|
|
||||||
|
|
||||||
let peers: PatpNoSig[] = [];
|
|
||||||
const peerSet = new Set<PatpNoSig>();
|
|
||||||
const contacts = new Map();
|
|
||||||
|
|
||||||
_.map(this.props.groups, (group, path) => {
|
|
||||||
if (group.members.size > 0) {
|
|
||||||
const groupEntries = group.members.values();
|
|
||||||
for (const member of groupEntries) {
|
|
||||||
peerSet.add(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupContacts = this.props.contacts[path];
|
|
||||||
|
|
||||||
if (groupContacts) {
|
|
||||||
const groupEntries = group.members.values();
|
|
||||||
for (const member of groupEntries) {
|
|
||||||
if (groupContacts[member]) {
|
|
||||||
if (contacts.has(member)) {
|
|
||||||
contacts
|
|
||||||
.get(member)
|
|
||||||
.push(groupContacts[member].nickname);
|
|
||||||
} else {
|
|
||||||
contacts.set(member, [
|
|
||||||
groupContacts[member].nickname,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
peers = Array.from(peerSet);
|
|
||||||
|
|
||||||
this.setState({ groups: groups, peers: peers, contacts: contacts });
|
|
||||||
}
|
|
||||||
|
|
||||||
search(event) {
|
|
||||||
const searchTerm = event.target.value.toLowerCase().replace('~', '');
|
|
||||||
const { state, props } = this;
|
|
||||||
|
|
||||||
this.setState({ searchValue: event.target.value });
|
|
||||||
|
|
||||||
if (searchTerm.length < 1) {
|
|
||||||
this.setState({ searchResults: { groups: [], ships: [] } });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchTerm.length > 0) {
|
|
||||||
if (state.inviteError === true) {
|
|
||||||
this.setState({ inviteError: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupMatches = !props.groupResults ? [] :
|
|
||||||
state.groups.filter((e) => {
|
|
||||||
return (
|
|
||||||
e[0].includes(searchTerm) || e[1].toLowerCase().includes(searchTerm)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let shipMatches = !props.shipResults ? [] :
|
|
||||||
state.peers.filter((e) => {
|
|
||||||
return (
|
|
||||||
e.includes(searchTerm) && !props.invites.ships.includes(e)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const contact of state.contacts.keys()) {
|
|
||||||
const thisContact = state.contacts.get(contact) || [];
|
|
||||||
const match = thisContact.filter((e) => {
|
|
||||||
return e.toLowerCase().includes(searchTerm);
|
|
||||||
});
|
|
||||||
if (match.length > 0) {
|
|
||||||
if (!(contact in shipMatches) && props.shipResults) {
|
|
||||||
shipMatches.push(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isValid = true;
|
|
||||||
if (!urbitOb.isValidPatp('~' + searchTerm)) {
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.shipResults && isValid && shipMatches.findIndex((s) => s === searchTerm) < 0) {
|
|
||||||
shipMatches.unshift(searchTerm);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selected } = state;
|
|
||||||
const groupIdx = groupMatches.findIndex(([path]) => path === selected);
|
|
||||||
const shipIdx = shipMatches.findIndex((ship) => ship === selected);
|
|
||||||
const staleSelection = groupIdx < 0 && shipIdx < 0;
|
|
||||||
if (!selected || staleSelection) {
|
|
||||||
const newSelection = _.get(groupMatches, '[0][0]') || shipMatches[0];
|
|
||||||
this.setState({ selected: newSelection });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchTerm.length < 3) {
|
|
||||||
groupMatches = groupMatches
|
|
||||||
.filter(([, name]) =>
|
|
||||||
name
|
|
||||||
.toLowerCase()
|
|
||||||
.split(' ')
|
|
||||||
.some((s) => s.startsWith(searchTerm))
|
|
||||||
)
|
|
||||||
.sort((a, b) => a[1].length - b[1].length);
|
|
||||||
|
|
||||||
shipMatches = shipMatches.slice(0, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
searchResults: { groups: groupMatches, ships: shipMatches },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindShortcuts() {
|
|
||||||
const mousetrap = Mousetrap(this.textarea.current);
|
|
||||||
mousetrap.bind(['down', 'tab'], (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.nextSelection();
|
|
||||||
});
|
|
||||||
|
|
||||||
mousetrap.bind(['up', 'shift+tab'], (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.nextSelection(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
mousetrap.bind('enter', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const { selected } = this.state;
|
|
||||||
if (selected && selected.startsWith('/')) {
|
|
||||||
this.addGroup(selected);
|
|
||||||
} else if (selected) {
|
|
||||||
this.addShip(selected);
|
|
||||||
}
|
|
||||||
this.setState({ selected: null });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
nextSelection(backward = false) {
|
|
||||||
const { selected, searchResults } = this.state;
|
|
||||||
const { ships, groups } = searchResults;
|
|
||||||
if (!selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let groupIdx = groups.findIndex(([path]) => path === selected);
|
|
||||||
let shipIdx = ships.findIndex((ship) => ship === selected);
|
|
||||||
if (groupIdx >= 0) {
|
|
||||||
backward ? groupIdx-- : groupIdx++;
|
|
||||||
let selected = _.get(groups, [groupIdx, 0]);
|
|
||||||
if (groupIdx === groups.length) {
|
|
||||||
selected = ships.length === 0 ? groups[0][0] : ships[0];
|
|
||||||
}
|
|
||||||
if (groupIdx < 0) {
|
|
||||||
selected =
|
|
||||||
ships.length === 0
|
|
||||||
? groups[groups.length - 1][0]
|
|
||||||
: ships[ships.length - 1];
|
|
||||||
}
|
|
||||||
this.setState({ selected });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (shipIdx >= 0) {
|
|
||||||
backward ? shipIdx-- : shipIdx++;
|
|
||||||
let selected = ships[shipIdx];
|
|
||||||
if (shipIdx === ships.length) {
|
|
||||||
selected = groups.length === 0 ? ships[0] : groups[0][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shipIdx < 0) {
|
|
||||||
selected =
|
|
||||||
groups.length === 0
|
|
||||||
? ships[ships.length - 1]
|
|
||||||
: groups[groups.length - 1][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ selected });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleteGroup() {
|
|
||||||
const { ships } = this.props.invites;
|
|
||||||
this.setState({
|
|
||||||
searchValue: '',
|
|
||||||
searchResults: { groups: [], ships: [] },
|
|
||||||
});
|
|
||||||
this.props.setInvite({ groups: [], ships: ships });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteShip(ship) {
|
|
||||||
let { groups, ships } = this.props.invites;
|
|
||||||
this.setState({
|
|
||||||
searchValue: '',
|
|
||||||
searchResults: { groups: [], ships: [] },
|
|
||||||
});
|
|
||||||
ships = ships.filter((e) => {
|
|
||||||
return e !== ship;
|
|
||||||
});
|
|
||||||
this.props.setInvite({ groups: groups, ships: ships });
|
|
||||||
}
|
|
||||||
|
|
||||||
addGroup(group) {
|
|
||||||
this.setState({
|
|
||||||
searchValue: '',
|
|
||||||
searchResults: { groups: [], ships: [] },
|
|
||||||
});
|
|
||||||
this.props.setInvite({ groups: [group], ships: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
addShip(ship) {
|
|
||||||
const { groups, ships } = this.props.invites;
|
|
||||||
this.setState({
|
|
||||||
searchValue: '',
|
|
||||||
searchResults: { groups: [], ships: [] },
|
|
||||||
});
|
|
||||||
if (!ships.includes(ship)) {
|
|
||||||
ships.push(ship);
|
|
||||||
}
|
|
||||||
if (groups.length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.props.setInvite({ groups: groups, ships: ships });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
submitShipToAdd(ship) {
|
|
||||||
const searchTerm = ship.toLowerCase().replace('~', '').trim();
|
|
||||||
let isValid = true;
|
|
||||||
if (!urbitOb.isValidPatp('~' + searchTerm)) {
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
if (!isValid) {
|
|
||||||
this.setState({ inviteError: true, searchValue: '' });
|
|
||||||
} else if (isValid) {
|
|
||||||
this.addShip(searchTerm);
|
|
||||||
this.setState({ searchValue: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props, state } = this;
|
|
||||||
let searchDisabled = props.disabled;
|
|
||||||
if (props.invites.groups) {
|
|
||||||
if (props.invites.groups.length > 0) {
|
|
||||||
searchDisabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let participants = <div />;
|
|
||||||
let searchResults = <div />;
|
|
||||||
|
|
||||||
let placeholder = '';
|
|
||||||
if (props.shipResults) {
|
|
||||||
placeholder = 'ships';
|
|
||||||
}
|
|
||||||
if (props.groupResults) {
|
|
||||||
if (placeholder.length > 0) {
|
|
||||||
placeholder = placeholder + ' or ';
|
|
||||||
}
|
|
||||||
placeholder = placeholder + 'existing groups';
|
|
||||||
}
|
|
||||||
placeholder = 'Search for ' + placeholder;
|
|
||||||
|
|
||||||
let invErrElem = <span />;
|
|
||||||
if (state.inviteError) {
|
|
||||||
invErrElem = (
|
|
||||||
<span className='f9 inter red2 db pt2'>
|
|
||||||
Invited ships must be validly formatted ship names.
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
state.searchResults.groups.length > 0 ||
|
|
||||||
state.searchResults.ships.length > 0
|
|
||||||
) {
|
|
||||||
const groupHeader =
|
|
||||||
state.searchResults.groups.length > 0 ? (
|
|
||||||
<p className='f9 gray2 ph3 pb2'>Groups</p>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
const shipHeader =
|
|
||||||
state.searchResults.ships.length > 0 ? (
|
|
||||||
<p className='f9 gray2 pv2 ph3'>Ships</p>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupResults = state.searchResults.groups.map((group) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={group[0]}
|
|
||||||
className={
|
|
||||||
'list white-d f8 pv2 ph3 pointer' +
|
|
||||||
' hover-bg-gray4 hover-bg-gray1-d ' +
|
|
||||||
(group[1] ? 'inter' : 'mono') +
|
|
||||||
(group[0] === state.selected ? ' bg-gray1-d bg-gray4' : '')
|
|
||||||
}
|
|
||||||
onClick={() => this.addGroup(group[0])}
|
|
||||||
>
|
|
||||||
<span className='mix-blend-diff white'>
|
|
||||||
{group[1] ? group[1] : group[0]}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const shipResults = Array.from(new Set(state.searchResults.ships)).map((ship) => {
|
|
||||||
const nicknames = (this.state.contacts.get(ship) || [])
|
|
||||||
.filter((e) => {
|
|
||||||
return !(e === '');
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={ship}
|
|
||||||
className={
|
|
||||||
'list mono white-d f8 pv1 ph3 pointer' +
|
|
||||||
' hover-bg-gray4 hover-bg-gray1-d relative' +
|
|
||||||
(ship === state.selected ? ' bg-gray1-d bg-gray4' : '')
|
|
||||||
}
|
|
||||||
onClick={(e) => this.addShip(ship)}
|
|
||||||
>
|
|
||||||
<Sigil
|
|
||||||
ship={'~' + ship}
|
|
||||||
size={24}
|
|
||||||
color='#000000'
|
|
||||||
classes='mix-blend-diff v-mid'
|
|
||||||
/>
|
|
||||||
<span className='v-mid ml2 mw5 truncate dib mix-blend-diff white'>
|
|
||||||
{'~' + ship}
|
|
||||||
</span>
|
|
||||||
<span className='absolute right-1 di truncate mw4 inter f9 pt1 mix-blend-diff white'>
|
|
||||||
{nicknames}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
searchResults = (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'absolute bg-white bg-gray0-d white-d' +
|
|
||||||
' pv3 z-1 w-100 mt1 ba b--white-d overflow-y-scroll mh-16'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{groupHeader}
|
|
||||||
{groupResults}
|
|
||||||
{shipHeader}
|
|
||||||
{shipResults}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupInvites = props.invites.groups || [];
|
|
||||||
const shipInvites = props.invites.ships || [];
|
|
||||||
|
|
||||||
if (groupInvites.length > 0 || shipInvites.length > 0) {
|
|
||||||
const groups = groupInvites.map((group) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={group[0]}
|
|
||||||
className={
|
|
||||||
'f9 mono black pa2 bg-gray5 bg-gray1-d' +
|
|
||||||
' ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{group}
|
|
||||||
<span
|
|
||||||
className='white-d ml3 mono pointer'
|
|
||||||
onClick={(e) => this.deleteGroup()}
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ships = shipInvites.map((ship) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={ship}
|
|
||||||
className={
|
|
||||||
'f9 mono black pa2 bg-gray5 bg-gray1-d' +
|
|
||||||
' ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{'~' + ship}
|
|
||||||
<span
|
|
||||||
className='white-d ml3 mono pointer'
|
|
||||||
onClick={(e) => this.deleteShip(ship)}
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
participants = (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d ' +
|
|
||||||
'white-d pa3 db w-100 inter'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className='db gray2'>Participants</span>
|
|
||||||
{groups} {ships}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative'>
|
|
||||||
<img
|
|
||||||
src='/~landscape/img/search.png'
|
|
||||||
className='absolute invert-d'
|
|
||||||
style={{
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
top: 14,
|
|
||||||
left: 12,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
ref={this.textarea}
|
|
||||||
className={
|
|
||||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100' +
|
|
||||||
' db focus-b--black focus-b--white-d'
|
|
||||||
}
|
|
||||||
placeholder={placeholder}
|
|
||||||
disabled={searchDisabled}
|
|
||||||
rows={1}
|
|
||||||
spellCheck={false}
|
|
||||||
style={{
|
|
||||||
resize: 'none',
|
|
||||||
paddingLeft: 36,
|
|
||||||
}}
|
|
||||||
onChange={this.search}
|
|
||||||
value={state.searchValue}
|
|
||||||
/>
|
|
||||||
{searchResults}
|
|
||||||
{participants}
|
|
||||||
{invErrElem}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InviteSearch;
|
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent, Fragment } from 'react';
|
import React, { PureComponent, Fragment } from 'react';
|
||||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||||
import { Button } from '@tlon/indigo-react';
|
import { BaseAnchor, BaseImage, Box, Button } from '@tlon/indigo-react';
|
||||||
import { hasProvider } from 'oembed-parser';
|
import { hasProvider } from 'oembed-parser';
|
||||||
import EmbedContainer from 'react-oembed-container';
|
import EmbedContainer from 'react-oembed-container';
|
||||||
import { memoize } from 'lodash';
|
import { memoize } from 'lodash';
|
||||||
@ -70,15 +70,15 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
|||||||
}
|
}
|
||||||
|
|
||||||
wrapInLink(contents) {
|
wrapInLink(contents) {
|
||||||
return (<a
|
return (<BaseAnchor
|
||||||
href={this.props.url}
|
href={this.props.url}
|
||||||
style={{ color: 'inherit' }}
|
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||||
className={`word-break-all ${(typeof contents === 'string') ? 'bb' : ''}`}
|
className={`word-break-all ${(typeof contents === 'string') ? 'bb' : ''}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</a>);
|
</BaseAnchor>);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -102,7 +102,7 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
|||||||
|
|
||||||
if (isImage && remoteContentPolicy.imageShown) {
|
if (isImage && remoteContentPolicy.imageShown) {
|
||||||
return this.wrapInLink(
|
return this.wrapInLink(
|
||||||
<img
|
<BaseImage
|
||||||
src={url}
|
src={url}
|
||||||
style={style}
|
style={style}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
@ -157,8 +157,11 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
|||||||
>
|
>
|
||||||
{this.state.unfold ? 'collapse' : 'expand'}
|
{this.state.unfold ? 'collapse' : 'expand'}
|
||||||
</Button> : null}
|
</Button> : null}
|
||||||
<div
|
<Box
|
||||||
className={'embed-container mb2 w-100 w-75-l w-50-xl ' + (this.state.unfold ? 'db' : 'dn')}
|
mb='2'
|
||||||
|
width='100%'
|
||||||
|
display={this.state.unfold ? 'block' : 'none'}
|
||||||
|
className='embed-container'
|
||||||
style={style}
|
style={style}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
{...oembedProps}
|
{...oembedProps}
|
||||||
@ -169,7 +172,7 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
|||||||
<div dangerouslySetInnerHTML={{__html: this.state.embed.html}}></div>
|
<div dangerouslySetInnerHTML={{__html: this.state.embed.html}}></div>
|
||||||
</EmbedContainer>
|
</EmbedContainer>
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</Box>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export class SidebarSwitcher extends Component {
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const classes = this.props.classes ? this.props.classes : '';
|
|
||||||
|
|
||||||
const style = this.props.style || {};
|
|
||||||
|
|
||||||
const paddingTop = this.props.classes ? '0px' : '8px';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes} style={{ paddingTop: paddingTop, ...style }}>
|
|
||||||
<a
|
|
||||||
className='pointer flex-shrink-0'
|
|
||||||
onClick={() => {
|
|
||||||
this.props.api.local.sidebarToggle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className='pr3 dn dib-m dib-l dib-xl'
|
|
||||||
src={
|
|
||||||
this.props.sidebarShown
|
|
||||||
? '/~landscape/img/ChatSwitcherLink.png'
|
|
||||||
: '/~landscape/img/ChatSwitcherClosed.png'
|
|
||||||
}
|
|
||||||
height='16'
|
|
||||||
width='16'
|
|
||||||
style={{
|
|
||||||
maxWidth: 16
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidebarSwitcher;
|
|
@ -1,6 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
|
import { Box } from '@tlon/indigo-react';
|
||||||
|
|
||||||
interface VirtualScrollerProps {
|
interface VirtualScrollerProps {
|
||||||
origin: 'top' | 'bottom';
|
origin: 'top' | 'bottom';
|
||||||
@ -332,13 +333,13 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{...style, ...{overflowY: 'scroll', transform }}}>
|
<Box overflowY='scroll' ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{ ...style, ...{ transform } }}>
|
||||||
<div ref={this.scrollContainer} style={{ transform }}>
|
<Box ref={this.scrollContainer} style={{ transform }}>
|
||||||
<div style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></div>
|
<Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box>
|
||||||
{indexesToRender.map(render)}
|
{indexesToRender.map(render)}
|
||||||
<div style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></div>
|
<Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Box, Text, Row, BaseInput } from '@tlon/indigo-react';
|
|
||||||
|
|
||||||
export class MetadataColor extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
color: props.initialValue
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeColor = this.changeColor.bind(this);
|
|
||||||
this.submitColor = this.submitColor.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { props } = this;
|
|
||||||
if (prevProps.initialValue !== props.initialValue) {
|
|
||||||
this.setState({ color: props.initialValue });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeColor(event) {
|
|
||||||
this.setState({ color: event.target.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
submitColor() {
|
|
||||||
const { props, state } = this;
|
|
||||||
|
|
||||||
let color = state.color;
|
|
||||||
if (color.startsWith('#')) {
|
|
||||||
color = state.color.substr(1);
|
|
||||||
}
|
|
||||||
const hexExp = /([0-9A-Fa-f]{6})/;
|
|
||||||
const hexTest = hexExp.exec(color);
|
|
||||||
if (!props.isDisabled && hexTest && (state.color !== props.initialValue)) {
|
|
||||||
props.setValue(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props, state } = this;
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
width='100%'
|
|
||||||
mb='3'
|
|
||||||
opacity={(props.isDisabled) ? '0.3' : '1'}
|
|
||||||
>
|
|
||||||
<Text my='1' display='block' fontSize='1'>Change color</Text>
|
|
||||||
<Text fontSize='0' gray display='block' mb='3'>Give this {props.resource} a color when viewing group channels</Text>
|
|
||||||
<Row
|
|
||||||
position='relative'
|
|
||||||
maxWidth='10rem'
|
|
||||||
width='100%'
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
position='absolute'
|
|
||||||
height='16px'
|
|
||||||
width='16px'
|
|
||||||
backgroundColor={state.color}
|
|
||||||
style={{ top: 18, left: 11 }}
|
|
||||||
/>
|
|
||||||
<BaseInput
|
|
||||||
pl='5'
|
|
||||||
fontSize='1'
|
|
||||||
border='1px solid'
|
|
||||||
borderColor='gray'
|
|
||||||
backgroundColor='white'
|
|
||||||
pt='3'
|
|
||||||
pb='3'
|
|
||||||
pr='3'
|
|
||||||
display='block'
|
|
||||||
width='100%'
|
|
||||||
flex='auto'
|
|
||||||
color='black'
|
|
||||||
mr='3'
|
|
||||||
value={state.color}
|
|
||||||
disabled={props.isDisabled}
|
|
||||||
onChange={this.changeColor}
|
|
||||||
onBlur={this.submitColor}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Box, Text, Row, BaseInput } from '@tlon/indigo-react';
|
|
||||||
|
|
||||||
export class MetadataInput extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: props.initialValue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { props } = this;
|
|
||||||
if (prevProps.initialValue !== props.initialValue) {
|
|
||||||
this.setState({ value: props.initialValue });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
isDisabled,
|
|
||||||
setValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
width='100%'
|
|
||||||
mb='3'
|
|
||||||
opacity={(isDisabled) ? '0.3' : '1'}
|
|
||||||
>
|
|
||||||
<Text display='block' fontSize='1' mb='1'>{title}</Text>
|
|
||||||
<Text display='block' mb='4' fontSize='0' gray>{description}</Text>
|
|
||||||
<Row
|
|
||||||
width='100%'
|
|
||||||
position='relative'
|
|
||||||
maxWidth='29rem'
|
|
||||||
>
|
|
||||||
<BaseInput
|
|
||||||
fontSize='1'
|
|
||||||
border='1px solid'
|
|
||||||
borderColor='gray'
|
|
||||||
backgroundColor='white'
|
|
||||||
p='3'
|
|
||||||
display='block'
|
|
||||||
color='black'
|
|
||||||
width='100'
|
|
||||||
flex='auto'
|
|
||||||
mr='3'
|
|
||||||
type="text"
|
|
||||||
value={this.state.value}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
this.setState({ value: e.target.value });
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if (!isDisabled) {
|
|
||||||
setValue(this.state.value || '');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { MetadataColor } from './color';
|
|
||||||
import { MetadataInput } from './input';
|
|
||||||
import { Box } from '@tlon/indigo-react';
|
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
|
||||||
|
|
||||||
|
|
||||||
export const MetadataSettings = (props) => {
|
|
||||||
const {
|
|
||||||
isOwner,
|
|
||||||
association,
|
|
||||||
changeLoading,
|
|
||||||
api,
|
|
||||||
resource,
|
|
||||||
app,
|
|
||||||
module
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const title =
|
|
||||||
(props.association && 'metadata' in props.association) ?
|
|
||||||
association.metadata.title : '';
|
|
||||||
const description =
|
|
||||||
(props.association && 'metadata' in props.association) ?
|
|
||||||
association.metadata.description : '';
|
|
||||||
const color =
|
|
||||||
(props.association && 'metadata' in props.association) ?
|
|
||||||
`#${uxToHex(props.association.metadata.color)}` : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box mt='6'>
|
|
||||||
<MetadataInput
|
|
||||||
title='Rename'
|
|
||||||
description={`Change the name of this ${resource}`}
|
|
||||||
isDisabled={!isOwner}
|
|
||||||
initialValue={title}
|
|
||||||
setValue={(val) => {
|
|
||||||
changeLoading(false, true, `Editing ${resource}...`, () => {
|
|
||||||
api.metadata.metadataAdd(
|
|
||||||
app,
|
|
||||||
association['app-path'],
|
|
||||||
association['group-path'],
|
|
||||||
val,
|
|
||||||
association.metadata.description,
|
|
||||||
association.metadata['date-created'],
|
|
||||||
uxToHex(association.metadata.color),
|
|
||||||
module
|
|
||||||
).then(() => {
|
|
||||||
changeLoading(false, false, '', () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}} />
|
|
||||||
<MetadataInput
|
|
||||||
title='Change description'
|
|
||||||
description={`Change the description of this ${resource}`}
|
|
||||||
isDisabled={!isOwner}
|
|
||||||
initialValue={description}
|
|
||||||
setValue={(val) => {
|
|
||||||
changeLoading(false, true, `Editing ${resource}...`, () => {
|
|
||||||
api.metadata.metadataAdd(
|
|
||||||
app,
|
|
||||||
association['app-path'],
|
|
||||||
association['group-path'],
|
|
||||||
association.metadata.title,
|
|
||||||
val,
|
|
||||||
association.metadata['date-created'],
|
|
||||||
uxToHex(association.metadata.color),
|
|
||||||
module
|
|
||||||
).then(() => {
|
|
||||||
changeLoading(false, false, '', () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}} />
|
|
||||||
<MetadataColor
|
|
||||||
initialValue={color}
|
|
||||||
isDisabled={!isOwner}
|
|
||||||
resource={resource}
|
|
||||||
setValue={(val) => {
|
|
||||||
changeLoading(false, true, `Editing ${resource}...`, () => {
|
|
||||||
props.api.metadata.metadataAdd(
|
|
||||||
app,
|
|
||||||
association['app-path'],
|
|
||||||
association['group-path'],
|
|
||||||
association.metadata.title,
|
|
||||||
association.metadata.description,
|
|
||||||
association.metadata['date-created'],
|
|
||||||
val,
|
|
||||||
module
|
|
||||||
).then(() => {
|
|
||||||
changeLoading(false, false, '', () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import { Box, Text, Icon } from "@tlon/indigo-react";
|
import { BaseInput, Box, Text, Icon } from "@tlon/indigo-react";
|
||||||
|
|
||||||
import S3Client from '~/logic/lib/s3';
|
import S3Client from '~/logic/lib/s3';
|
||||||
import { Spinner } from './Spinner';
|
import { Spinner } from './Spinner';
|
||||||
@ -117,7 +117,6 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
|
|||||||
this.setState({ isUploading: false });
|
this.setState({ isUploading: false });
|
||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
@ -141,8 +140,8 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
|
|||||||
const display = children || <Icon icon='ArrowNorth' />;
|
const display = children || <Icon icon='ArrowNorth' />;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<BaseInput
|
||||||
className="dn"
|
display='none'
|
||||||
type="file"
|
type="file"
|
||||||
id="fileElement"
|
id="fileElement"
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
@ -150,7 +149,7 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
|
|||||||
onChange={this.onChange.bind(this)} />
|
onChange={this.onChange.bind(this)} />
|
||||||
{this.state.isUploading
|
{this.state.isUploading
|
||||||
? <Spinner awaiting={true} classes={className} />
|
? <Spinner awaiting={true} classes={className} />
|
||||||
: <span className={`pointer ${className}`} onClick={this.onClick.bind(this)}>{display}</span>
|
: <Text cursor='pointer' className={className} onClick={this.onClick.bind(this)}>{display}</Text>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export class Toggle extends Component {
|
|
||||||
render() {
|
|
||||||
const switchClasses = (this.props.boolean)
|
|
||||||
? 'relative checked bg-green2 br3 h1 toggle v-mid z-0'
|
|
||||||
: 'relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
style={{ WebkitAppearance: 'none', width: 28 }}
|
|
||||||
className={switchClasses}
|
|
||||||
onChange={this.props.change}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Toggle;
|
|
Loading…
Reference in New Issue
Block a user