@@ -91,8 +91,8 @@ export function BottomNav({ state, actions }) {
{ ((!state.cards && !state.contact) || state.profile) && (
setCards()}>
diff --git a/net/web/src/session/bottomNav/BottomNav.styled.js b/net/web/src/session/bottomNav/BottomNav.styled.js
index 759a0150..7262a191 100644
--- a/net/web/src/session/bottomNav/BottomNav.styled.js
+++ b/net/web/src/session/bottomNav/BottomNav.styled.js
@@ -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 {
diff --git a/net/web/src/session/cardSelect/CardSelect.jsx b/net/web/src/session/cardSelect/CardSelect.jsx
new file mode 100644
index 00000000..52bda1b1
--- /dev/null
+++ b/net/web/src/session/cardSelect/CardSelect.jsx
@@ -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 (
+
+ (
+
+ )}
+ />
+
+ );
+}
+
diff --git a/net/web/src/session/cardSelect/CardSelect.styled.js b/net/web/src/session/cardSelect/CardSelect.styled.js
new file mode 100644
index 00000000..4b11f14b
--- /dev/null
+++ b/net/web/src/session/cardSelect/CardSelect.styled.js
@@ -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;
+`;
diff --git a/net/web/src/session/cardSelect/selectItem/SelectItem.jsx b/net/web/src/session/cardSelect/selectItem/SelectItem.jsx
new file mode 100644
index 00000000..6f6cf507
--- /dev/null
+++ b/net/web/src/session/cardSelect/selectItem/SelectItem.jsx
@@ -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 (
+
select(item.id)}>
+
+
+
{ profile?.name }
+
{ handle() }
+
+ { select && (
+
+ select(item.id)} size="small" />
+
+ )}
+
+ );
+}
+
diff --git a/net/web/src/session/cardSelect/selectItem/SelectItem.styled.js b/net/web/src/session/cardSelect/selectItem/SelectItem.styled.js
new file mode 100644
index 00000000..1a0e1fee
--- /dev/null
+++ b/net/web/src/session/cardSelect/selectItem/SelectItem.styled.js
@@ -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;
+ }
+`
diff --git a/net/web/src/session/cardSelect/selectItem/useSelectItem.hook.js b/net/web/src/session/cardSelect/selectItem/useSelectItem.hook.js
new file mode 100644
index 00000000..ca79c3e8
--- /dev/null
+++ b/net/web/src/session/cardSelect/selectItem/useSelectItem.hook.js
@@ -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 };
+}
+
diff --git a/net/web/src/session/cardSelect/useCardSelect.hook.js b/net/web/src/session/cardSelect/useCardSelect.hook.js
new file mode 100644
index 00000000..3e0dd883
--- /dev/null
+++ b/net/web/src/session/cardSelect/useCardSelect.hook.js
@@ -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 };
+}
+
diff --git a/net/web/src/session/channels/Channels.jsx b/net/web/src/session/channels/Channels.jsx
index 175384c9..3a4bf987 100644
--- a/net/web/src/session/channels/Channels.jsx
+++ b/net/web/src/session/channels/Channels.jsx
@@ -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 = (
+
+
+
+
+ );
+
return (
-
+
}
@@ -17,7 +39,7 @@ export function Channels({ open }) {
{ state.display === 'small' && (
-
+
@@ -27,18 +49,22 @@ export function Channels({ open }) {
(
-
+
)}
/>
{ state.display !== 'small' && (
-
)}
+
+
+
);
}
diff --git a/net/web/src/session/channels/Channels.styled.js b/net/web/src/session/channels/Channels.styled.js
index fbd8e3a6..7e60c5c8 100644
--- a/net/web/src/session/channels/Channels.styled.js
+++ b/net/web/src/session/channels/Channels.styled.js
@@ -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;
+`
+
diff --git a/net/web/src/session/channels/addChannel/AddChannel.jsx b/net/web/src/session/channels/addChannel/AddChannel.jsx
new file mode 100644
index 00000000..c6092404
--- /dev/null
+++ b/net/web/src/session/channels/addChannel/AddChannel.jsx
@@ -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 (
+
+ actions.setSubject(e.target.value)} />
+
+ Channel Members:
+ { state.members.size !== 0 && (
+ { state.members.size }
+ )}
+
+
+ card?.data?.cardDetail?.status === 'connected'}
+ unknown={0}
+ />
+
+
+ );
+}
+
diff --git a/net/web/src/session/channels/addChannel/AddChannel.styled.js b/net/web/src/session/channels/addChannel/AddChannel.styled.js
new file mode 100644
index 00000000..9ab2566e
--- /dev/null
+++ b/net/web/src/session/channels/addChannel/AddChannel.styled.js
@@ -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};
+ }
+`;
+
+
diff --git a/net/web/src/session/channels/addChannel/memberOption/MemberOption.jsx b/net/web/src/session/channels/addChannel/memberOption/MemberOption.jsx
new file mode 100644
index 00000000..8ff0462e
--- /dev/null
+++ b/net/web/src/session/channels/addChannel/memberOption/MemberOption.jsx
@@ -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 (
+
+
+
+
{ profile?.name }
+
{ handle() }
+
+
+ );
+}
+
diff --git a/net/web/src/session/channels/addChannel/memberOption/MemberOption.styled.js b/net/web/src/session/channels/addChannel/memberOption/MemberOption.styled.js
new file mode 100644
index 00000000..cac39de5
--- /dev/null
+++ b/net/web/src/session/channels/addChannel/memberOption/MemberOption.styled.js
@@ -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;
+ }
+ }
+`
diff --git a/net/web/src/session/channels/addChannel/memberOption/useMemberOption.hook.js b/net/web/src/session/channels/addChannel/memberOption/useMemberOption.hook.js
new file mode 100644
index 00000000..b78705ad
--- /dev/null
+++ b/net/web/src/session/channels/addChannel/memberOption/useMemberOption.hook.js
@@ -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 };
+}
+
diff --git a/net/web/src/session/channels/channelItem/ChannelItem.jsx b/net/web/src/session/channels/channelItem/ChannelItem.jsx
index e239b7b2..236f3b0a 100644
--- a/net/web/src/session/channels/channelItem/ChannelItem.jsx
+++ b/net/web/src/session/channels/channelItem/ChannelItem.jsx
@@ -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 (
-
+
{ item.contacts.length === 0 && (
diff --git a/net/web/src/session/channels/useChannels.hook.js b/net/web/src/session/channels/useChannels.hook.js
index 9dfac0a6..3b55c68e 100644
--- a/net/web/src/session/channels/useChannels.hook.js
+++ b/net/web/src/session/channels/useChannels.hook.js
@@ -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) => {
diff --git a/net/web/src/session/profile/profileDetails/ProfileDetails.jsx b/net/web/src/session/profile/profileDetails/ProfileDetails.jsx
index e4608937..31aaa69d 100644
--- a/net/web/src/session/profile/profileDetails/ProfileDetails.jsx
+++ b/net/web/src/session/profile/profileDetails/ProfileDetails.jsx
@@ -6,11 +6,11 @@ export function ProfileDetails({ state, actions }) {
actions.setEditName(e.target.value)}
- defaultValue={state.editName} autocapitalizate="word" />
+ defaultValue={state.editName} autocapitalize="word" />
actions.setEditLocation(e.target.value)}
- defaultValue={state.editLocation} autocapitalizate="word" />
+ defaultValue={state.editLocation} autocapitalize="word" />
actions.setEditDescription(e.target.value)}