launch + groups: address design critique

This commit is contained in:
Liam Fitzgerald 2020-10-02 16:31:47 +10:00
parent cc49b6cee3
commit aa41b85d4b
11 changed files with 126 additions and 55 deletions

View File

@ -59,7 +59,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
default:
throw new Error("Invalid app name");
}
history.push(`/~groups${association?.['group-path']}`);
history.push(`/~groups${association?.["group-path"]}`);
}, [api, association]);
const onDelete = useCallback(async () => {
@ -77,12 +77,13 @@ export function ChannelMenu(props: ChannelMenuProps) {
default:
throw new Error("Invalid app name");
}
history.push(`/~groups${association?.["group-path"]}`);
}, [api, association]);
return (
<Dropdown
options={
<Col>
<Col bg="white" border={1} borderRadius={1} borderColor="lightGray">
{isOurs ? (
<>
<ChannelMenuItem color="red" icon="TrashCan">
@ -100,7 +101,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
</>
) : (
<ChannelMenuItem color="red" bottom icon="ArrowEast">
<Action m="2" destructive onClick={onUnsubscribe}>
<Action bg="white" m="2" destructive onClick={onUnsubscribe}>
Unsubscribe from Channel
</Action>
</ChannelMenuItem>
@ -111,7 +112,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
alignY="top"
width="250px"
>
<Icon icon="Menu" stroke="gray" />
<Icon display="block" icon="Menu" stroke="gray" />
</Dropdown>
);
}

View File

@ -15,7 +15,8 @@ import { Group, GroupPolicy } from "~/types/group-update";
import { Enc } from "~/types/noun";
import { Association } from "~/types/metadata-update";
import GlobalApi from "~/logic/api/global";
import { resourceFromPath } from "~/logic/lib/group";
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
interface FormSchema {
name: string;
@ -35,7 +36,8 @@ interface GroupSettingsProps {
api: GlobalApi;
}
export function GroupSettings(props: GroupSettingsProps) {
const { metadata } = props.association;
const { group, association } = props;
const { metadata } = association;
const currentPrivate = "invite" in props.group.policy;
const initialValues: FormSchema = {
name: metadata.title,
@ -66,7 +68,12 @@ export function GroupSettings(props: GroupSettingsProps) {
}
};
const onDelete = async () => {};
const onDelete = async () => {
await props.api.contacts.delete(association["group-path"]);
};
const disabled =
resourceFromPath(association["group-path"]).ship.slice(1) !== window.ship &&
roleForShip(group, window.ship) !== "admin";
return (
<Box height="100%" overflowY="auto">
@ -85,33 +92,45 @@ export function GroupSettings(props: GroupSettingsProps) {
my={3}
mx={4}
>
<Col>
<Label>Delete Group</Label>
<Label gray mt="2">
Permanently delete this group. (All current members will no
longer see this group.)
</Label>
<Button onClick={onDelete} mt={2} destructive>
Delete this group
</Button>
</Col>
<Box borderBottom={1} borderBottomColor="washedGray" />
{!disabled && (
<>
<Col>
<Label>Delete Group</Label>
<Label gray mt="2">
Permanently delete this group. (All current members will no
longer see this group.)
</Label>
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
Delete this group
</StatelessAsyncButton>
</Col>
<Box borderBottom={1} borderBottomColor="washedGray" />
</>
)}
<Input
id="name"
label="Group Name"
caption="The name for your group to be called by"
disabled={disabled}
/>
<Input
id="description"
label="Group Description"
caption="The description of your group"
disabled={disabled}
/>
<Checkbox
id="isPrivate"
label="Private group"
caption="If enabled, users must be invited to join the group"
disabled={disabled}
/>
<AsyncButton primary loadingText="Updating.." border>
<AsyncButton
disabled={disabled}
primary
loadingText="Updating.."
border
>
Save
</AsyncButton>
<FormError message="Failed to update settings" />

View File

@ -66,13 +66,16 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
</Link>
</Box>
)}
<Box mr={2}>{title}</Box>
<Box pr={1} mr={2}>
<Text>{title}</Text>
</Box>
{atRoot && (
<>
<TruncatedBox
display={["none", "block"]}
maxWidth="50%"
maxWidth="60%"
flexShrink={1}
title={association?.metadata?.description}
color="gray"
>
{association?.metadata?.description}

View File

@ -12,13 +12,13 @@ import Tiles from './components/tiles';
import Welcome from './components/welcome';
import Groups from './components/Groups';
const Tile = ({ children, bg, to, borderRadius = 1, ...rest }) => (
const Tile = ({ children, bg, to, ...rest }) => (
<Box
m={2}
bg="white"
width="126px"
height="126px"
borderRadius={borderRadius}
borderRadius={2}
overflow="hidden"
{...rest}>
<Link to={to}>
@ -57,7 +57,6 @@ export default class LaunchApp extends React.Component {
<Row flexWrap="wrap" mb={4} pitch={4}>
<Tile
border={1}
borderRadius={1}
bg="washedGreen"
borderColor="green"
to="/~groups/home"

View File

@ -44,7 +44,6 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const groups = Object.values(props?.associations?.contacts || {})
.sort(sortGroupsAlph)
.sort(sortGroupsRecent(recentGroups))
.slice(0,5);
return (
<Box

View File

@ -46,7 +46,7 @@ export default class BasicTile extends React.PureComponent {
return (
<Tile>
<div className={classnames('w-100 h-100 relative ba b--black b--gray1-d bg-gray0-d',
<div className={classnames('w-100 h-100 relative ba b--gray3 b--gray2-d bg-gray0-d br2',
{ 'bg-white': props.title !== 'Dojo',
'bg-black': props.title === 'Dojo' })}
>{tile}</div>

View File

@ -11,7 +11,7 @@ export default class CustomTile extends React.PureComponent {
return (
<Tile>
<div className={"w-100 h-100 relative bg-white bg-gray0-d ba " +
"b--black b--gray1-d"}>
"b--black br2 b--gray1-d"}>
<img
className="absolute invert-d"
style={{ left: 38, top: 38 }}

View File

@ -117,7 +117,7 @@ export default class WeatherTile extends React.Component {
return (
<Tile>
<div
className={'relative ' + weatherStyle.text}
className={'relative br2 ba b--gray3 b--gray2-d ' + weatherStyle.text}
style={{
width: 126,
height: 126,
@ -151,9 +151,7 @@ export default class WeatherTile extends React.Component {
);
}
return this.renderWrapper(
<div className={'pa2 w-100 h-100 bg-white bg-gray0-d black white-d ' +
'b--black b--gray1-d ba'}
>
<div className="pa2 w-100 h-100 bg-white bg-gray0-d black white-d ">
<a
className="f9 black white-d pointer absolute"
style={{ top: 8 }}
@ -205,7 +203,7 @@ export default class WeatherTile extends React.Component {
renderNoData() {
return this.renderWrapper((
<div
className={'pa2 w-100 h-100 b--black b--gray1-d ba ' +
className={'pa2 w-100 h-100 ' +
'bg-white bg-gray0-d black white-d'}
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
>
@ -230,7 +228,7 @@ export default class WeatherTile extends React.Component {
const da = moment.unix(d.sunsetTime).format('h:mm a') || '';
return this.renderWrapper(
<div className="w-100 h-100 b--black b--gray1-d ba"
<div className="w-100 h-100"
style={{ backdropFilter: 'blur(80px)' }}
>
<p className="f9 absolute" style={{ left: 8, top: 8 }}>
@ -269,7 +267,7 @@ export default class WeatherTile extends React.Component {
if (this.props.location) {
return this.renderWrapper((
<div
className={'pa2 w-100 h-100 b--black b--gray1-d ba ' +
className={'pa2 w-100 h-100 ' +
'bg-white bg-gray0-d black white-d'}>
<p className="f9 absolute"
style={{ left: 8, top: 8 }}

View File

@ -27,6 +27,26 @@ textarea, select, input, button { outline: none; }
mix-blend-mode: difference;
}
button {
background-color: transparent;
}
/* stolen from indigo-react reset.css
* TODO: remove and add reset.css properly
*/
@keyframes loadingSpinnerRotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* dark */
@media all and (prefers-color-scheme: dark) {
body {
@ -53,4 +73,4 @@ textarea, select, input, button { outline: none; }
.hover-bg-gray1-d:hover {
background-color: #4d4d4d;
}
}
}

View File

@ -23,6 +23,7 @@ import { UnjoinedResource } from "./UnjoinedResource";
import { InvitePopover } from "../apps/groups/components/InvitePopover";
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
import { NewChannel } from "../apps/groups/components/lib/NewChannel";
import { Loading } from './Loading';
import "~/views/apps/links/css/custom.css";
import "~/views/apps/publish/css/custom.css";
@ -103,7 +104,7 @@ export function GroupsPane(props: GroupsPaneProps) {
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
if (!association) {
return <Box>Loading</Box>;
return <Loading />;
}
return (
@ -144,6 +145,7 @@ export function GroupsPane(props: GroupsPaneProps) {
baseUrl={baseUrl}
>
<UnjoinedResource
graphKeys={props.graphKeys}
notebooks={props.notebooks}
inbox={props.inbox}
baseUrl={baseUrl}

View File

@ -1,52 +1,82 @@
import React from "react";
import React, { useEffect, useMemo } from "react";
import { Association } from "~/types/metadata-update";
import { Box, Text, Button, Col, Center } from "@tlon/indigo-react";
import { Link, useHistory } from "react-router-dom";
import GlobalApi from "~/logic/api/global";
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
import { StatelessAsyncButton as AsyncButton } from './StatelessAsyncButton';
import {Notebooks, Graphs, Inbox} from "~/types";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import {
StatelessAsyncButton as AsyncButton,
StatelessAsyncButton,
} from "./StatelessAsyncButton";
import { Notebooks, Graphs, Inbox } from "~/types";
interface UnjoinedResourceProps {
association: Association;
api: GlobalApi;
baseUrl: string;
notebooks: Notebooks;
graphs: Graphs;
graphKeys: Set<string>;
inbox: Inbox;
}
function isJoined(app: string, path: string) {
return function (
props: Pick<UnjoinedResourceProps, "inbox" | "graphKeys" | "notebooks">
) {
let ship, name;
switch (app) {
case "link":
[, , ship, name] = path.split("/");
return props.graphKeys.has(path);
case "publish":
[, ship, name] = path.split("/");
return !!props.notebooks[ship.slice(1)][name];
case "chat":
return !!props.inbox[path];
default:
console.log("Bad app name");
return false;
}
};
}
export function UnjoinedResource(props: UnjoinedResourceProps) {
const { api } = props;
const { api, notebooks, graphKeys, inbox } = props;
const history = useHistory();
const appPath = props.association["app-path"];
const appName = props.association["app-name"];
const { title, description, module } = props.association.metadata;
const waiter = useWaitForProps(props);
const app = module || appName;
const app = useMemo(() => module || appName, [props.association]);
const onJoin = async () => {
let ship, name;
switch(app) {
case 'link':
[,,ship,name] = appPath.split('/');
switch (app) {
case "link":
[, , ship, name] = appPath.split("/");
await api.graph.joinGraph(ship, name);
break;
case 'publish':
[,ship,name] = appPath.split('/');
case "publish":
[, ship, name] = appPath.split("/");
await api.publish.subscribeNotebook(ship.slice(1), name);
await waiter(p => !!p?.notebooks?.[ship]?.[name])
break;
case 'chat':
[,ship,name] = appPath.split('/');
case "chat":
[, ship, name] = appPath.split("/");
await api.chat.join(ship, appPath, true);
await waiter(p => !!p?.inbox?.[appPath])
break;
default:
throw new Error("Unknown resource type");
}
await waiter(isJoined(app, appPath));
history.push(`${props.baseUrl}/resource/${app}${appPath}`);
};
useEffect(() => {
if (isJoined(app, appPath)({ inbox, graphKeys, notebooks })) {
history.push(`${props.baseUrl}/resource/${app}${appPath}`);
}
}, [props.association, inbox, graphKeys, notebooks]);
return (
<Center p={6}>
<Col maxWidth="400px" p={4} border={1} borderColor="washedGray">
@ -56,9 +86,9 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
<Box mb={4}>
<Text color="gray">{description}</Text>
</Box>
<AsyncButton onClick={onJoin} mx="auto" border>
Join Channel
</AsyncButton>
<StatelessAsyncButton onClick={onJoin} mx="auto" border>
Join Channel
</StatelessAsyncButton>
</Col>
</Center>
);