support adding new channels

This commit is contained in:
Roland Osborne 2022-08-19 12:15:10 -07:00
parent 0c29f66d1a
commit ee6a9e4103
19 changed files with 405 additions and 17 deletions

View File

@ -30,6 +30,8 @@ const Colors = {
statsDivider: '#afafaf',
channel: '#f2f2f2',
card: '#eeeeee',
selectHover: '#fafafa',
};
export default Colors;

View File

@ -1,5 +1,5 @@
import { BottomNavWrapper } from './BottomNav.styled';
import { CommentOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons';
import { CommentOutlined, ContactsOutlined, UserOutlined } from '@ant-design/icons';
export function BottomNav({ state, actions }) {
@ -82,8 +82,8 @@ export function BottomNav({ state, actions }) {
{ (state.cards || state.contact) && !state.profile && (
<div class="nav-item">
<div class="nav-active">
<div class="nav-div-left">
<TeamOutlined />
<div class="nav-div-left bump">
<ContactsOutlined />
</div>
</div>
</div>
@ -91,8 +91,8 @@ export function BottomNav({ state, actions }) {
{ ((!state.cards && !state.contact) || state.profile) && (
<div class="nav-item" onClick={() => setCards()}>
<div class="nav-inactive">
<div class="nav-div-left">
<TeamOutlined />
<div class="nav-div-left bump">
<ContactsOutlined />
</div>
</div>
</div>

View File

@ -22,6 +22,10 @@ export const BottomNavWrapper = styled.div`
padding-bottom: 8px;
font-size: 20px;
cursor: pointer;
.bump {
font-size: 26px;
}
}
.nav-active {
@ -31,6 +35,10 @@ export const BottomNavWrapper = styled.div`
padding-top: 8px;
padding-bottom: 8px;
font-size: 24px;
.bump {
font-size: 30px;
}
}
.nav-div-right {

View File

@ -0,0 +1,21 @@
import { useState } from 'react';
import { List } from 'antd';
import { CardSelectWrapper } from './CardSelect.styled';
import { SelectItem } from './selectItem/SelectItem';
import { useCardSelect } from './useCardSelect.hook';
export function CardSelect({ filter, unknown, select, selected }) {
const { state, actions } = useCardSelect(filter);
return (
<CardSelectWrapper>
<List local={{ emptyText: '' }} itemLayout="horizontal" dataSource={state.cards} gutter="0"
renderItem={item => (
<SelectItem item={item} select={select} selected={selected} />
)}
/>
</CardSelectWrapper>
);
}

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const CardSelectWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;

View File

@ -0,0 +1,34 @@
import { Switch } from 'antd';
import { SelectItemWrapper } from './SelectItem.styled';
import { useSelectItem } from './useSelectItem.hook';
import { Logo } from 'logo/Logo';
export function SelectItem({ item, select, selected }) {
const { state, actions } = useSelectItem(item, selected);
const profile = item?.data?.cardProfile;
const detail = item?.data?.cardDetail;
const handle = () => {
if (profile?.node) {
return profile.handle + '@' + profile.node;
}
return profile?.handle;
}
return (
<SelectItemWrapper onClick={() => select(item.id)}>
<Logo url={state.logo} width={32} height={32} radius={8} />
<div class="details">
<div class="name">{ profile?.name }</div>
<div class="handle">{ handle() }</div>
</div>
{ select && (
<div class="switch">
<Switch checked={state.selected} onChange={() => select(item.id)} size="small" />
</div>
)}
</SelectItemWrapper>
);
}

View File

@ -0,0 +1,43 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const SelectItemWrapper = styled.div`
height: 48px;
width: 100%;
display: flex;
align-items: center;
padding-left: 8px;
padding-right: 8px;
cursor: pointer;
&:hover {
background-color: ${Colors.selectHover};
}
.details {
flex-grow: 1;
display: flex;
flex-direction: column;
padding-left: 16px;
justify-content: center;
min-width: 0;
.name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
}
.handle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
}
}
.switch {
flex-shrink: 0;
}
`

View File

@ -0,0 +1,31 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
export function useSelectItem(item, selected) {
const [state, setState] = useState({
logo: null,
selected: false,
busy: false,
});
const card = useContext(CardContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({ selected: selected.has(item.id) });
}, [selected]);
useEffect(() => {
updateState({ logo: card.actions.getImageUrl(item.id) });
}, [card, item]);
const actions = {
};
return { state, actions };
}

View File

@ -0,0 +1,27 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
export function useCardSelect(filter) {
const [state, setState] = useState({
cards: [],
});
const card = useContext(CardContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
let contacts = Array.from(card.state.cards.values());
let filtered = contacts.filter(filter);
updateState({ cards: filtered });
}, [card]);
const actions = {
};
return { state, actions };
}

View File

@ -1,15 +1,37 @@
import { Input, List } from 'antd';
import { ChannelsWrapper } from './Channels.styled';
import { Modal, Input, List, Button } from 'antd';
import { ChannelsWrapper, AddFooter } from './Channels.styled';
import { CommentOutlined, SearchOutlined } from '@ant-design/icons';
import { useChannels } from './useChannels.hook';
import { ChannelItem } from './channelItem/ChannelItem';
import { AddChannel } from './addChannel/AddChannel';
export function Channels({ open }) {
const { state, actions } = useChannels();
const addChannel = async () => {
try {
await actions.addChannel();
actions.clearShowAdd();
}
catch(err) {
console.log(err);
Modal.error({
title: 'Failed to Create Channel',
content: 'Please try again.',
});
}
};
const addFooter = (
<AddFooter>
<Button key="back" onClick={actions.clearShowAdd}>Cancel</Button>
<Button key="save" type="primary" loading={state.busy} onClick={addChannel}>Save</Button>
</AddFooter>
);
return (
<ChannelsWrapper onClick={open} >
<ChannelsWrapper>
<div class="search">
<div class="filter">
<Input bordered={false} allowClear={true} placeholder="Channels" prefix={<SearchOutlined />}
@ -17,7 +39,7 @@ export function Channels({ open }) {
</div>
{ state.display === 'small' && (
<div class="inline">
<div class="add">
<div class="add" onClick={actions.setShowAdd}>
<CommentOutlined />
<div class="label">New</div>
</div>
@ -27,18 +49,22 @@ export function Channels({ open }) {
<div class="view">
<List local={{ emptyText: '' }} itemLayout="horizontal" dataSource={state.channels} gutter="0"
renderItem={item => (
<ChannelItem item={item} />
<ChannelItem item={item} openChannel={open} />
)}
/>
</div>
{ state.display !== 'small' && (
<div class="bar">
<div class="add">
<div class="add" onClick={actions.setShowAdd}>
<CommentOutlined />
<div class="label">New Channel</div>
</div>
</div>
)}
<Modal title="New Channel" centered visible={state.showAdd} footer={addFooter}
onCancel={actions.clearShowAdd}>
<AddChannel state={state} actions={actions} />
</Modal>
</ChannelsWrapper>
);
}

View File

@ -77,3 +77,12 @@ export const ChannelsWrapper = styled.div`
}
}
`;
export const AddFooter = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
`

View File

@ -0,0 +1,29 @@
import { useState } from 'react';
import { Input, Select } from 'antd';
import { AddChannelWrapper } from './AddChannel.styled';
import { CardSelect } from '../../cardSelect/CardSelect';
export function AddChannel({ state, actions }) {
return (
<AddChannelWrapper>
<Input placeholder="Subject (optional)" spellCheck="false" autocapitalize="word"
value={state.subject} onChange={(e) => actions.setSubject(e.target.value)} />
<div class="members">
<span>Channel Members: </span>
{ state.members.size !== 0 && (
<span>{ state.members.size }</span>
)}
</div>
<div class="list">
<CardSelect
select={actions.onMember}
selected={state.members}
filter={(card) => card?.data?.cardDetail?.status === 'connected'}
unknown={0}
/>
</div>
</AddChannelWrapper>
);
}

View File

@ -0,0 +1,26 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const AddChannelWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.members {
margin-top: 16px;
width: 100%;
padding-left: 8px;
color: ${Colors.grey};
}
.list {
width: 100%;
min-height: 100px;
max-height: 200px;
overflow: scroll;
border: 1px solid ${Colors.divider};
}
`;

View File

@ -0,0 +1,28 @@
import { MemberOptionWrapper } from './MemberOption.styled';
import { useMemberOption } from './useMemberOption.hook';
import { Logo } from 'logo/Logo';
export function MemberOption({ item, close }) {
const { state, actions } = useMemberOption(item);
const profile = item?.data?.cardProfile;
const detail = item?.data?.cardDetail;
const handle = () => {
if (profile?.node) {
return profile.handle + '@' + profile.node;
}
return profile?.handle;
}
return (
<MemberOptionWrapper onClick={close}>
<Logo url={state.logo} width={32} height={32} radius={8} />
<div class="details">
<div class="name">{ profile?.name }</div>
<div class="handle">{ handle() }</div>
</div>
</MemberOptionWrapper>
);
}

View File

@ -0,0 +1,31 @@
import styled from 'styled-components';
export const MemberOptionWrapper = styled.div`
height: 48px;
width: 100%;
display: flex;
align-items: center;
.details {
flex-grow: 1;
display: flex;
flex-direction: column;
padding-left: 16px;
justify-content: center;
min-width: 0;
.name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
}
.handle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
}
}
`

View File

@ -0,0 +1,25 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
export function useMemberOption(item) {
const [state, setState] = useState({
logo: null,
});
const card = useContext(CardContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({ logo: card.actions.getImageUrl(item.id) });
}, [card, item]);
const actions = {
};
return { state, actions };
}

View File

@ -2,10 +2,10 @@ import { ChannelItemWrapper } from './ChannelItem.styled';
import { Logo } from 'logo/Logo';
import { AppstoreFilled, SolutionOutlined } from '@ant-design/icons';
export function ChannelItem({ item }) {
export function ChannelItem({ item, openChannel }) {
return (
<ChannelItemWrapper>
<ChannelItemWrapper onClick={openChannel}>
{ item.contacts.length === 0 && (
<div class="item">
<div class="logo">

View File

@ -12,8 +12,11 @@ export function useChannels() {
const [state, setState] = useState({
display: null,
channels: [],
busy: false }
);
showAdd: false,
busy: false,
members: new Set(),
subject: null,
});
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
@ -26,9 +29,46 @@ export function useChannels() {
}
const actions = {
addChannel: async () => {
if (!state.busy) {
try {
updateState({ busy: true });
let cards = Array.from(state.members.values());
await channel.actions.addChannel(cards, state.subject, null);
updateState({ busy: false });
}
catch(err) {
console.log(err);
updateState({ busy: false });
throw new Error("failed to create new channel");
}
}
else {
throw new Error("operation in progress");
}
},
onFilter: (value) => {
setFilter(value.toUpperCase());
},
setShowAdd: () => {
updateState({ showAdd: true });
},
clearShowAdd: () => {
updateState({ showAdd: false, members: new Set(), subject: null });
},
onMember: (string) => {
let members = new Set(state.members);
if (members.has(string)) {
members.delete(string);
}
else {
members.add(string);
}
updateState({ members });
},
setSubject: (subject) => {
updateState({ subject });
},
};
const setUpdated = (chan) => {

View File

@ -6,11 +6,11 @@ export function ProfileDetails({ state, actions }) {
<ProfileDetailsWrapper>
<div class="info">
<Input placeholder="Name" spellCheck="false" onChange={(e) => actions.setEditName(e.target.value)}
defaultValue={state.editName} autocapitalizate="word" />
defaultValue={state.editName} autocapitalize="word" />
</div>
<div class="info">
<Input placeholder="Location" spellCheck="false" onChange={(e) => actions.setEditLocation(e.target.value)}
defaultValue={state.editLocation} autocapitalizate="word" />
defaultValue={state.editLocation} autocapitalize="word" />
</div>
<div class="info">
<Input.TextArea placeholder="Description" onChange={(e) => actions.setEditDescription(e.target.value)}