mirror of
https://github.com/balzack/databag.git
synced 2025-02-14 12:39:17 +00:00
support adding new channels
This commit is contained in:
parent
0c29f66d1a
commit
ee6a9e4103
@ -30,6 +30,8 @@ const Colors = {
|
||||
statsDivider: '#afafaf',
|
||||
channel: '#f2f2f2',
|
||||
card: '#eeeeee',
|
||||
|
||||
selectHover: '#fafafa',
|
||||
};
|
||||
|
||||
export default Colors;
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
21
net/web/src/session/cardSelect/CardSelect.jsx
Normal file
21
net/web/src/session/cardSelect/CardSelect.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
8
net/web/src/session/cardSelect/CardSelect.styled.js
Normal file
8
net/web/src/session/cardSelect/CardSelect.styled.js
Normal 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;
|
||||
`;
|
34
net/web/src/session/cardSelect/selectItem/SelectItem.jsx
Normal file
34
net/web/src/session/cardSelect/selectItem/SelectItem.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`
|
@ -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 };
|
||||
}
|
||||
|
27
net/web/src/session/cardSelect/useCardSelect.hook.js
Normal file
27
net/web/src/session/cardSelect/useCardSelect.hook.js
Normal 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 };
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
`
|
||||
|
||||
|
29
net/web/src/session/channels/addChannel/AddChannel.jsx
Normal file
29
net/web/src/session/channels/addChannel/AddChannel.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
26
net/web/src/session/channels/addChannel/AddChannel.styled.js
Normal file
26
net/web/src/session/channels/addChannel/AddChannel.styled.js
Normal 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};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
@ -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 };
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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) => {
|
||||
|
@ -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)}
|
||||
|
Loading…
Reference in New Issue
Block a user