mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 01:52:42 +03:00
launch + groups: address design critique
This commit is contained in:
parent
cc49b6cee3
commit
aa41b85d4b
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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 }}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user