From f01fdf9efaa1d412ec9e539d4c832bd62529fe4e Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Wed, 21 Oct 2020 15:55:11 -0400 Subject: [PATCH] chat: create dm route, restore participants option --- .../src/views/apps/chat/ChatResource.tsx | 1 - .../apps/chat/components/ChatMessage.tsx | 6 - .../views/apps/chat/components/ChatWindow.tsx | 22 ++- .../apps/chat/components/overlay-sigil.js | 3 +- .../apps/chat/components/profile-overlay.js | 41 +----- .../views/landscape/components/NewChannel.tsx | 71 +++++----- .../landscape/components/Participants.tsx | 129 +++++++++--------- pkg/interface/src/views/landscape/index.tsx | 77 +++++++---- 8 files changed, 164 insertions(+), 186 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index b36a92a75..54b950480 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -114,7 +114,6 @@ export function ChatResource(props: ChatResourceProps) { group={group} ship={owner} station={station} - allStations={Object.keys(props.inbox)} api={props.api} hideNicknames={props.hideNicknames} hideAvatars={props.hideAvatars} diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 999e8eda3..487cd11d8 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -46,7 +46,6 @@ interface ChatMessageProps { className?: string; isPending: boolean; style?: any; - allStations: any; scrollWindow: HTMLDivElement; isLastMessage?: boolean; unreadMarkerRef: React.RefObject; @@ -87,7 +86,6 @@ export default class ChatMessage extends Component { scrollWindow, isLastMessage, unreadMarkerRef, - allStations, history, api } = this.props; @@ -118,7 +116,6 @@ export default class ChatMessage extends Component { style, containerClass, isPending, - allStations, history, api, scrollWindow @@ -165,7 +162,6 @@ interface MessageProps { containerClass: string; isPending: boolean; style: any; - allStations: any; measure(element): void; scrollWindow: HTMLDivElement; }; @@ -182,7 +178,6 @@ export class MessageWithSigil extends PureComponent { hideAvatars, remoteContentPolicy, measure, - allStations, history, api, scrollWindow @@ -218,7 +213,6 @@ export class MessageWithSigil extends PureComponent { hideAvatars={hideAvatars} hideNicknames={hideNicknames} scrollWindow={scrollWindow} - allStations={allStations} history={history} api={api} className="fl pr3 v-top bg-white bg-gray0-d pt1" diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index 5d83ab094..31b0ab6d0 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -39,7 +39,6 @@ type ChatWindowProps = RouteComponentProps<{ group: Group; ship: Patp; station: any; - allStations: any; api: GlobalApi; hideNicknames: boolean; hideAvatars: boolean; @@ -56,7 +55,7 @@ interface ChatWindowState { export default class ChatWindow extends Component { private virtualList: VirtualScroller | null; private unreadMarkerRef: React.RefObject; - + INITIALIZATION_MAX_TIME = 1500; constructor(props) { @@ -68,14 +67,14 @@ export default class ChatWindow extends Component { @@ -224,7 +223,7 @@ export default class ChatWindow extends Component a.number - b.number) .forEach(message => { @@ -267,8 +265,8 @@ export default class ChatWindow extends Component @@ -84,7 +84,6 @@ export class OverlaySigil extends PureComponent { association={props.association} group={props.group} onDismiss={this.profileHide} - allStations={allStations} hideAvatars={hideAvatars} hideNicknames={props.hideNicknames} history={props.history} diff --git a/pkg/interface/src/views/apps/chat/components/profile-overlay.js b/pkg/interface/src/views/apps/chat/components/profile-overlay.js index ca8be9e8e..2d9708cc8 100644 --- a/pkg/interface/src/views/apps/chat/components/profile-overlay.js +++ b/pkg/interface/src/views/apps/chat/components/profile-overlay.js @@ -12,7 +12,6 @@ export class ProfileOverlay extends PureComponent { this.popoverRef = React.createRef(); this.onDocumentClick = this.onDocumentClick.bind(this); - this.createAndRedirectToDM = this.createAndRedirectToDM.bind(this); } componentDidMount() { @@ -25,42 +24,6 @@ export class ProfileOverlay extends PureComponent { document.removeEventListener('touchstart', this.onDocumentClick); } - createAndRedirectToDM() { - const { api, ship, history, allStations } = this.props; - const station = `/~${window.ship}/dm--${ship}`; - const theirStation = `/~${ship}/dm--${window.ship}`; - - if (allStations.indexOf(station) !== -1) { - history.push(`/~landscape/home/resource/chat${station}`); - return; - } - - if (allStations.indexOf(theirStation) !== -1) { - history.push(`/~landscape/home/resource/chat${theirStation}`); - return; - } - - const groupPath = `/ship/~${window.ship}/dm--${ship}`; - const aud = ship !== window.ship ? [`~${ship}`] : []; - const title = `${cite(window.ship)} <-> ${cite(ship)}`; - - api.chat.create( - title, - '', - station, - groupPath, - { invite: { pending: aud } }, - aud, - true, - false - ); - - // TODO: make a pretty loading state - setTimeout(() => { - history.push(`/~landscape/home/resource/chat${station}`); - }, 5000); - } - onDocumentClick(event) { const { popoverRef } = this; // Do nothing if clicking ref's element or descendent elements @@ -72,7 +35,7 @@ export class ProfileOverlay extends PureComponent { } render() { - const { contact, ship, color, topSpace, bottomSpace, group, association, hideNicknames, hideAvatars, history } = this.props; + const { contact, ship, color, topSpace, bottomSpace, group, hideNicknames, hideAvatars, history } = this.props; let top, bottom; if (topSpace < OVERLAY_HEIGHT / 2) { @@ -132,7 +95,7 @@ export class ProfileOverlay extends PureComponent { )} {cite(`~${ship}`)} {!isOwn && ( - )} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index 7224d0b3d..677e15a2d 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -1,38 +1,36 @@ -import React, { useCallback } from "react"; +import React, { useCallback } from 'react'; import { Box, ManagedTextInputField as Input, Col, ManagedRadioButtonField as Radio, - Text, -} from "@tlon/indigo-react"; -import { Formik, Form } from "formik"; -import * as Yup from "yup"; -import GlobalApi from "~/logic/api/global"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import { FormError } from "~/views/components/FormError"; -import { RouteComponentProps } from "react-router-dom"; -import { stringToSymbol } from "~/logic/lib/util"; -import GroupSearch from "~/views/components/GroupSearch"; -import { Associations } from "~/types/metadata-update"; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import { Notebooks } from "~/types/publish-update"; -import { Groups } from "~/types/group-update"; -import { ShipSearch } from "~/views/components/ShipSearch"; -import { Rolodex } from "~/types"; + Text +} from '@tlon/indigo-react'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; +import GlobalApi from '~/logic/api/global'; +import { AsyncButton } from '~/views/components/AsyncButton'; +import { FormError } from '~/views/components/FormError'; +import { RouteComponentProps } from 'react-router-dom'; +import { stringToSymbol } from '~/logic/lib/util'; +import { Associations } from '~/types/metadata-update'; +import { useWaitForProps } from '~/logic/lib/useWaitForProps'; +import { Groups } from '~/types/group-update'; +import { ShipSearch } from '~/views/components/ShipSearch'; +import { Rolodex } from '~/types'; interface FormSchema { name: string; description: string; ships: string[]; - type: "chat" | "publish" | "links"; + type: 'chat' | 'publish' | 'links'; } const formSchema = Yup.object({ - name: Yup.string().required("Channel must have a name"), + name: Yup.string().required('Channel must have a name'), description: Yup.string(), ships: Yup.array(Yup.string()), - type: Yup.string().required("Must choose channel type"), + type: Yup.string().required('Must choose channel type') }); interface NewChannelProps { @@ -43,7 +41,6 @@ interface NewChannelProps { group?: string; } - export function NewChannel(props: NewChannelProps & RouteComponentProps) { const { history, api, group, workspace } = props; @@ -54,7 +51,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { try { const { name, description, type, ships } = values; switch (type) { - case "chat": + case 'chat': const appPath = `/~${window.ship}/${resId}`; const groupPath = group || `/ship${appPath}`; @@ -63,46 +60,46 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { description, appPath, groupPath, - { invite: { pending: ships.map((s) => `~${s}`) } }, - ships.map((s) => `~${s}`), + { invite: { pending: ships.map(s => `~${s}`) } }, + ships.map(s => `~${s}`), true, false ); break; - case "publish": + case 'publish': await props.api.publish.newBook(resId, name, description, group); break; - case "links": + case 'links': if (group) { await api.graph.createManagedGraph( resId, name, description, group, - "link" + 'link' ); } else { await api.graph.createUnmanagedGraph( resId, name, description, - { invite: { pending: ships.map((s) => `~${s}`) } }, - "link" + { invite: { pending: ships.map(s => `~${s}`) } }, + 'link' ); } break; default: - console.log("fallthrough"); + console.log('fallthrough'); } if (!group) { - await waiter((p) => !!p?.groups?.[`/ship/~${window.ship}/${resId}`]); + await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`])); } actions.setStatus({ success: null }); } catch (e) { console.error(e); - actions.setStatus({ error: "Channel creation failed" }); + actions.setStatus({ error: 'Channel creation failed' }); } }; return ( @@ -113,11 +110,11 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { diff --git a/pkg/interface/src/views/landscape/components/Participants.tsx b/pkg/interface/src/views/landscape/components/Participants.tsx index b9f27114d..60dfe0595 100644 --- a/pkg/interface/src/views/landscape/components/Participants.tsx +++ b/pkg/interface/src/views/landscape/components/Participants.tsx @@ -3,8 +3,8 @@ import React, { useMemo, useCallback, SyntheticEvent, - ChangeEvent, -} from "react"; + ChangeEvent +} from 'react'; import { Col, Box, @@ -14,23 +14,23 @@ import { Center, Button, Action, - StatelessTextInput as Input, -} from "@tlon/indigo-react"; -import _ from "lodash"; -import f from "lodash/fp"; -import VisibilitySensor from "react-visibility-sensor"; + StatelessTextInput as Input +} from '@tlon/indigo-react'; +import _ from 'lodash'; +import f from 'lodash/fp'; +import VisibilitySensor from 'react-visibility-sensor'; -import { Contact, Contacts } from "~/types/contact-update"; -import { Sigil } from "~/logic/lib/sigil"; -import { cite, uxToHex } from "~/logic/lib/util"; -import { Group, RoleTags } from "~/types/group-update"; -import { roleForShip, resourceFromPath } from "~/logic/lib/group"; -import { Association } from "~/types/metadata-update"; -import { useHistory, Link } from "react-router-dom"; -import { Dropdown } from "~/views/components/Dropdown"; -import GlobalApi from "~/logic/api/global"; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import styled from "styled-components"; +import { Contact, Contacts } from '~/types/contact-update'; +import { Sigil } from '~/logic/lib/sigil'; +import { cite, uxToHex } from '~/logic/lib/util'; +import { Group, RoleTags } from '~/types/group-update'; +import { roleForShip, resourceFromPath } from '~/logic/lib/group'; +import { Association } from '~/types/metadata-update'; +import { useHistory, Link } from 'react-router-dom'; +import { Dropdown } from '~/views/components/Dropdown'; +import GlobalApi from '~/logic/api/global'; +import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; +import styled from 'styled-components'; const TruncText = styled(Box)` white-space: nowrap; @@ -39,7 +39,7 @@ const TruncText = styled(Box)` `; type Participant = Contact & { patp: string; pending: boolean }; -type ParticipantsTabId = "total" | "pending" | "admin"; +type ParticipantsTabId = 'total' | 'pending' | 'admin'; const searchParticipant = (search: string) => (p: Participant) => { if (search.length == 0) { @@ -54,36 +54,36 @@ function getParticipants(cs: Contacts, group: Group) { const contacts: Participant[] = _.map(cs, (c, patp) => ({ ...c, patp, - pending: false, + pending: false })); - const members: Participant[] = _.map(Array.from(group.members), (m) => + const members: Participant[] = _.map(Array.from(group.members), m => emptyContact(m, false) ); - const allMembers = _.unionBy(contacts, members, "patp"); + const allMembers = _.unionBy(contacts, members, 'patp'); const pending: Participant[] = - "invite" in group.policy - ? _.map(Array.from(group.policy.invite.pending), (m) => + 'invite' in group.policy + ? _.map(Array.from(group.policy.invite.pending), m => emptyContact(m, true) ) : []; return [ - _.unionBy(allMembers, pending, "patp"), + _.unionBy(allMembers, pending, 'patp'), pending.length, - allMembers.length, + allMembers.length ] as const; } const emptyContact = (patp: string, pending: boolean): Participant => ({ - nickname: "", - email: "", - phone: "", - color: "", + nickname: '', + email: '', + phone: '', + color: '', avatar: null, - notes: "", - website: "", + notes: '', + website: '', patp, - pending, + pending }); const Tab = ({ selected, id, label, setSelected }) => ( @@ -95,7 +95,7 @@ const Tab = ({ selected, id, label, setSelected }) => ( cursor="pointer" onClick={() => setSelected(id)} > - {label} + {label} ); @@ -113,18 +113,18 @@ export function Participants(props: { (p: Participant) => boolean > = useMemo( () => ({ - total: (p) => !p.pending, - pending: (p) => p.pending, - admin: (p) => props.group.tags?.role?.admin?.has(p.patp), + total: p => !p.pending, + pending: p => p.pending, + admin: p => props.group.tags?.role?.admin?.has(p.patp) }), [props.group] ); const ourRole = roleForShip(props.group, window.ship); - const [filter, setFilter] = useState("total"); + const [filter, setFilter] = useState('total'); - const [search, _setSearch] = useState(""); + const [search, _setSearch] = useState(''); const setSearch = useMemo(() => _.debounce(_setSearch, 200), [_setSearch]); const onSearchChange = useCallback( (e: ChangeEvent) => { @@ -134,7 +134,7 @@ export function Participants(props: { ); const adminCount = props.group.tags?.role?.admin?.size || 0; - const isInvite = "invite" in props.group.policy; + const isInvite = 'invite' in props.group.policy; const [participants, pendingCount, memberCount] = getParticipants( props.contacts, @@ -156,7 +156,7 @@ export function Participants(props: { // TODO: remove when resolved const isSafari = useMemo(() => { const ua = window.navigator.userAgent; - return ua.includes("Safari") && !ua.includes("Chrome"); + return ua.includes('Safari') && !ua.includes('Chrome'); }, []); return ( @@ -166,7 +166,7 @@ export function Participants(props: { border={1} borderColor="washedGray" borderRadius={1} - position={isSafari ? "static" : "sticky"} + position={isSafari ? 'static' : 'sticky'} top="0px" mb={2} px={2} @@ -192,7 +192,7 @@ export function Participants(props: { selected={filter} setSelected={setFilter} id="admin" - label={`${adminCount} Admin${adminCount > 1 ? "s" : ""}`} + label={`${adminCount} Admin${adminCount > 1 ? 's' : ''}`} /> @@ -210,8 +210,8 @@ export function Participants(props: { @@ -224,7 +224,7 @@ export function Participants(props: { > {({ isVisible }) => isVisible ? ( - cs.map((c) => ( + cs.map(c => ( contact.pending - ? "pending" - : roleForShip(group, contact.patp) || "member", + ? 'pending' + : roleForShip(group, contact.patp) || 'member', [contact, group] ); const onPromote = useCallback(async () => { - const resource = resourceFromPath(association["group-path"]); - await api.groups.addTag(resource, { tag: "admin" }, [`~${contact.patp}`]); + const resource = resourceFromPath(association['group-path']); + await api.groups.addTag(resource, { tag: 'admin' }, [`~${contact.patp}`]); }, [api, association]); const onDemote = useCallback(async () => { - const resource = resourceFromPath(association["group-path"]); - await api.groups.removeTag(resource, { tag: "admin" }, [ - `~${contact.patp}`, + const resource = resourceFromPath(association['group-path']); + await api.groups.removeTag(resource, { tag: 'admin' }, [ + `~${contact.patp}` ]); }, [api, association]); const onBan = useCallback(async () => { - const resource = resourceFromPath(association["group-path"]); + const resource = resourceFromPath(association['group-path']); await api.groups.changePolicy(resource, { - open: { banShips: [`~${contact.patp}`] }, + open: { banShips: [`~${contact.patp}`] } }); }, [api, association]); const onKick = useCallback(async () => { - const resource = resourceFromPath(association["group-path"]); + const resource = resourceFromPath(association['group-path']); await api.groups.remove(resource, [`~${contact.patp}`]); }, [api, association]); @@ -319,7 +319,7 @@ function Participant(props: { @@ -340,7 +340,12 @@ function Participant(props: { gapY={2} p={2} > - {props.role === "admin" && ( + + + Send Message + + + {props.role === 'admin' && ( <> {!isInvite && ( <> @@ -352,7 +357,7 @@ function Participant(props: { )} - {role === "admin" ? ( + {role === 'admin' ? ( Demote from Admin @@ -372,7 +377,7 @@ function Participant(props: { ); @@ -382,7 +387,7 @@ function BlankParticipant({ length }) { return ( ); diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index b52a6f2a2..7122bad80 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -1,20 +1,17 @@ import React, { Component } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { Box, Center } from '@tlon/indigo-react'; import './css/custom.css'; -import { PatpNoSig, AppName } from '~/types/noun'; +import { PatpNoSig } from '~/types/noun'; import GlobalApi from '~/logic/api/global'; import { StoreState } from '~/logic/store/type'; -import GlobalSubscription from '~/logic/subscription/global'; -import { Resource } from '~/views/components/Resource'; -import { PopoverRoutes } from './components/PopoverRoutes'; -import { UnjoinedResource } from '~/views/components/UnjoinedResource'; import { GroupsPane } from './components/GroupsPane'; import { Workspace } from '~/types'; -import {NewGroup} from './components/NewGroup'; -import {JoinGroup} from './components/JoinGroup'; +import { NewGroup } from './components/NewGroup'; +import { JoinGroup } from './components/JoinGroup'; + +import { cite } from '~/logic/lib/util'; type LandscapeProps = StoreState & { @@ -34,25 +31,46 @@ export default class Landscape extends Component { this.props.subscription.startApp('publish'); this.props.subscription.startApp('graph'); this.props.api.publish.fetchNotebooks(); - + } + + createandRedirectToDM(api, ship, history, allStations) { + const station = `/~${window.ship}/dm--${ship}`; + const theirStation = `/~${ship}/dm--${window.ship}`; + + if (allStations.indexOf(station) !== -1) { + history.push(`/~landscape/home/resource/chat${station}`); + return; + } + + if (allStations.indexOf(theirStation) !== -1) { + history.push(`/~landscape/home/resource/chat${theirStation}`); + return; + } + + const groupPath = `/ship/~${window.ship}/dm--${ship}`; + const aud = ship !== window.ship ? [`~${ship}`] : []; + const title = `${cite(window.ship)} <-> ${cite(ship)}`; + + api.chat.create( + title, + '', + station, + groupPath, + { invite: { pending: aud } }, + aud, + true, + false + ); + + // TODO: make a pretty loading state + setTimeout(() => { + history.push(`/~landscape/home/resource/chat${station}`); + }, 5000); } render() { const { props } = this; - - const contacts = props.contacts || {}; - const defaultContacts = - (Boolean(props.contacts) && '/~/default' in props.contacts) ? - props.contacts['/~/default'] : {}; - - const invites = - (Boolean(props.invites) && '/contacts' in props.invites) ? - props.invites['/contacts'] : {}; - const s3 = props.s3 ? props.s3 : {}; - const groups = props.groups || {}; - const associations = props.associations || {}; - const { api } = props; - + const { api, inbox } = props; return ( @@ -73,7 +91,6 @@ export default class Landscape extends Component { { const ws: Workspace = { type: 'home' }; - return ( ); @@ -85,21 +102,27 @@ export default class Landscape extends Component { ); }} /> + { + const { ship } = routeProps.match.params; + return this.createandRedirectToDM(api, ship, routeProps.history, Object.keys(inbox)); + }} + /> { const { ship, name } = routeProps.match.params; const autojoin = ship && name ? `${ship}/${name}` : null; return ( -