Merge branch 'mobile' into main

This commit is contained in:
Roland Osborne 2022-08-24 14:08:12 -07:00
commit b3e1786cde
208 changed files with 6278 additions and 4626 deletions

View File

@ -3510,6 +3510,9 @@ components:
status:
type: string
enum: [ pending, confirmed, requested, connecting, connected ]
statusUpdated:
type: integer
format: int64
token:
type: string
notes:

View File

@ -1,6 +1,7 @@
package databag
import (
"time"
"databag/internal/store"
"encoding/hex"
"errors"
@ -57,6 +58,7 @@ func AddCard(w http.ResponseWriter, r *http.Request) {
Node: identity.Node,
ProfileRevision: identity.Revision,
Status: APPCardConfirmed,
StatusUpdated: time.Now().Unix(),
ViewRevision: 0,
InToken: hex.EncodeToString(data),
AccountID: account.GUID,

View File

@ -4,6 +4,7 @@ import (
"databag/internal/store"
"encoding/hex"
"errors"
"time"
"github.com/gorilla/mux"
"github.com/theckman/go-securerandom"
"gorm.io/gorm"
@ -105,6 +106,7 @@ func SetCardStatus(w http.ResponseWriter, r *http.Request) {
}
}
slot.Card.Status = status
slot.Card.StatusUpdated = time.Now().Unix()
slot.Card.NotifiedView = viewRevision
slot.Card.NotifiedArticle = articleRevision
slot.Card.NotifiedChannel = channelRevision

View File

@ -56,6 +56,9 @@ func SetCloseMessage(w http.ResponseWriter, r *http.Request) {
if res := tx.Model(&card).Update("status", APPCardConfirmed).Error; res != nil {
return res
}
if res := tx.Model(&card).Update("status_updated", time.Now().Unix()).Error; res != nil {
return res
}
}
if res := tx.Model(&card).Update("detail_revision", account.CardRevision+1).Error; res != nil {
return res

View File

@ -67,6 +67,7 @@ func SetOpenMessage(w http.ResponseWriter, r *http.Request) {
card.Node = connect.Node
card.ProfileRevision = connect.ProfileRevision
card.Status = APPCardPending
card.StatusUpdated = time.Now().Unix()
card.NotifiedProfile = connect.ProfileRevision
card.NotifiedArticle = connect.ArticleRevision
card.NotifiedView = connect.ViewRevision
@ -124,9 +125,11 @@ func SetOpenMessage(w http.ResponseWriter, r *http.Request) {
}
if card.Status == APPCardConfirmed {
card.Status = APPCardRequested
card.StatusUpdated = time.Now().Unix()
}
if card.Status == APPCardConnecting {
card.Status = APPCardConnected
card.StatusUpdated = time.Now().Unix()
}
card.OutToken = connect.Token
card.DetailRevision = account.CardRevision + 1

View File

@ -69,13 +69,14 @@ func getCardRevisionModel(slot *store.CardSlot) *Card {
func getCardDetailModel(slot *store.CardSlot) *CardDetail {
var groups []string
var groups []string = []string{}
for _, group := range slot.Card.Groups {
groups = append(groups, group.GroupSlot.GroupSlotID)
}
return &CardDetail{
Status: slot.Card.Status,
StatusUpdated: slot.Card.StatusUpdated,
Token: slot.Card.OutToken,
Notes: slot.Card.Notes,
Groups: groups,

View File

@ -113,7 +113,9 @@ type CardData struct {
type CardDetail struct {
Status string `json:"status"`
Token string `json:"token,omitempty"`
StatusUpdated int64 `json:"statusUpdated"`
Token string `json:"token,omitempty"`
Notes string `json:"notes,omitempty"`

View File

@ -151,6 +151,7 @@ type Card struct {
ProfileRevision int64 `gorm:"not null"`
DetailRevision int64 `gorm:"not null;default:1"`
Status string `gorm:"not null"`
StatusUpdated int64
InToken string `gorm:"not null;index:cardguid,unique"`
OutToken string
Notes string

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta
name="Databag"
@ -26,7 +26,7 @@
-->
<title>Databag</title>
</head>
<body>
<body style="background-color:#8fbea7;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--

View File

@ -1,51 +0,0 @@
import React from 'react'
import { Input, Button, Space } from 'antd';
import { AdminWrapper, LoginWrapper, TokenInput } from './Admin.styled';
import { useAdmin } from './useAdmin.hook';
import { Dashboard } from './Dashboard/Dashboard';
import { UserOutlined } from '@ant-design/icons';
export function Admin() {
const { state, actions } = useAdmin()
if (state.unclaimed == null) {
return <></>
}
if (state.unclaimed) {
return (
<LoginWrapper>
<div className="login">
<Space>
<TokenInput placeholder="Admin Token" spellcheck="false" onChange={(e) => actions.setToken(e.target.value)} />
<Button loading={state.busy} type="primary" onClick={() => actions.setAccess()}>Set</Button>
</Space>
</div>
</LoginWrapper>
);
}
if (!state.access) {
return (
<LoginWrapper>
<div class="user" onClick={() => actions.onUser()}>
<UserOutlined />
</div>
<div className="login">
<Space>
<TokenInput placeholder="Admin Token" spellcheck="false" onChange={(e) => actions.setToken(e.target.value)} />
<Button loading={state.busy} type="primary" onClick={() => actions.getAccess()}>Go</Button>
</Space>
</div>
</LoginWrapper>
);
}
return (
<AdminWrapper>
<Dashboard token={state.token} config={state.config} logout={() => actions.logout()} />
</AdminWrapper>
)
}

View File

@ -1,57 +0,0 @@
import { Input } from 'antd';
import styled from 'styled-components';
export const AdminWrapper = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #8fbea7;
webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.dashboard {
width: 80%;
min-width: 400px;
max-width: 800px;
max-height: 80%;
background-color: #eeeeee;
border-radius: 4px;
}
`;
export const LoginWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.login {
padding: 8px;
display: flex;
flex-direction: row;
background-color: #eeeeee;
border-radius: 4px;
}
.user {
position: absolute;
top: 0px;
right: 0px;
padding: 16px;
color: #555555;
font-size: 20px;
cursor: pointer;
}
`;
export const TokenInput = styled(Input.Password)`
width: 300px;
`;

View File

@ -1,72 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getNodeStatus } from 'api/getNodeStatus';
import { setNodeStatus } from 'api/setNodeStatus';
import { getNodeConfig } from 'api/getNodeConfig';
export function useAdmin() {
const [state, setState] = useState({
unclaimed: null,
access: null,
token: null,
busy: false,
});
const navigate = useNavigate();
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const checkStatus = async () => {
try {
let status = await getNodeStatus();
updateState({ unclaimed: status });
}
catch(err) {
console.log(err);
window.alert(err);
}
}
useEffect(() => {
checkStatus();
}, []);
const actions = {
setToken: (value) => {
updateState({ token: value });
},
setAccess: async () => {
try {
await setNodeStatus(state.token);
let config = await getNodeConfig(state.token);
updateState({ access: state.token, unclaimed: false, config });
}
catch(err) {
console.log(err);
window.alert(err);
}
},
getAccess: async () => {
try {
let config = await getNodeConfig(state.token);
updateState({ access: state.token, config });
}
catch(err) {
console.log(err);
window.alert(err);
}
},
onUser: () => {
navigate('/login');
},
logout: () => {
updateState({ access: null, token: null });
},
};
return { state, actions };
}

View File

@ -1,4 +1,7 @@
import login from './login.png';
import 'antd/dist/antd.min.css';
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import { AppContextProvider } from 'context/AppContext';
import { AccountContextProvider } from 'context/AccountContext';
import { ProfileContextProvider } from 'context/ProfileContext';
@ -6,19 +9,16 @@ import { ArticleContextProvider } from 'context/ArticleContext';
import { GroupContextProvider } from 'context/GroupContext';
import { CardContextProvider } from 'context/CardContext';
import { ChannelContextProvider } from 'context/ChannelContext';
import { ConversationContextProvider } from 'context/ConversationContext';
import { StoreContextProvider } from 'context/StoreContext';
import { UploadContextProvider } from 'context/UploadContext';
import { Home } from './Home/Home';
import { Admin } from './Admin/Admin';
import { Login } from './Login/Login';
import { Create } from './Create/Create';
import { User } from './User/User';
import { Profile } from './User/Profile/Profile';
import { Contact } from './User/Contact/Contact';
import { Conversation } from './User/Conversation/Conversation';
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import 'antd/dist/antd.min.css';
import { ViewportContextProvider } from 'context/ViewportContext';
import { ConversationContextProvider } from 'context/ConversationContext';
import { AppWrapper } from 'App.styled';
import { Root } from './root/Root';
import { Access } from './access/Access';
import { Session } from './session/Session';
import { Admin } from './admin/Admin';
function App() {
@ -31,35 +31,26 @@ function App() {
<ProfileContextProvider>
<AccountContextProvider>
<StoreContextProvider>
<AppContextProvider>
<div style={{ position: 'absolute', width: '100vw', height: '100vh', backgroundColor: '#8fbea7' }}>
<img src={login} alt="" style={{ position: 'absolute', width: '33%', bottom: 0, right: 0 }}/>
</div>
<div style={{ position: 'absolute', width: '100vw', height: '100vh' }}>
<Router>
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/login" element={ <Login /> } />
<Route path="/admin" element={ <Admin /> } />
<Route path="/create" element={ <Create /> } />
<Route path="/user" element={ <User /> }>
<Route path="profile" element={<Profile />} />
<Route path="contact/:guid" element={<Contact />} />
<Route path="conversation/:cardId/:channelId" element={
<ViewportContextProvider>
<AppContextProvider>
<AppWrapper>
<Router>
<Routes>
<Route path="/" element={ <Root /> } />
<Route path="/admin" element={ <Admin /> } />
<Route path="/login" element={ <Access mode="login" /> } />
<Route path="/create" element={ <Access mode="create" /> } />
<Route path="/session" element={
<ConversationContextProvider>
<Conversation />
<Session />
</ConversationContextProvider>
} />
<Route path="conversation/:channelId" element={
<ConversationContextProvider>
<Conversation />
</ConversationContextProvider>
} />
</Route>
</Routes>
</Router>
</div>
</AppContextProvider>
}>
</Route>
</Routes>
</Router>
</AppWrapper>
</AppContextProvider>
</ViewportContextProvider>
</StoreContextProvider>
</AccountContextProvider>
</ProfileContextProvider>

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export const AppWrapper = styled.div`
position: absolute;
width: 100vw;
height: calc(100vh - calc(100vh - 100%));
`;

View File

@ -1,31 +0,0 @@
import React from 'react'
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useCreate } from './useCreate.hook';
import { CreateWrapper, CreateInput, CreatePassword, CreateLogin, CreateEnter, CreateSpin } from './Create.styled';
export function Create() {
const { state, actions } = useCreate()
return (
<CreateWrapper>
<div class="container">
<div class="header">databag</div>
<div class="subheader">
<span class="subheader-text">Communication for the Decentralized Web</span>
</div>
<CreateInput size="large" spellCheck="false" placeholder="username" prefix={<UserOutlined />}
onChange={(e) => actions.setUsername(e.target.value)} value={state.username}
addonAfter={state.conflict} />
<CreatePassword size="large" spellCheck="false" placeholder="password" prefix={<LockOutlined />}
onChange={(e) => actions.setPassword(e.target.value)} value={state.password} />
<CreatePassword size="large" spellCheck="false" placeholder="confirm password" prefix={<LockOutlined />}
onChange={(e) => actions.setConfirmed(e.target.value)} value={state.confirmed} />
<CreateEnter type="primary" onClick={() => actions.onCreate()} disabled={actions.isDisabled()}>
<span>Create Account</span>
</CreateEnter>
</div>
<CreateLogin type="text" onClick={() => actions.onLogin()}>Account Sign In</CreateLogin>
<CreateSpin size="large" spinning={state.spinning} />
</CreateWrapper>
)
}

View File

@ -1,67 +0,0 @@
import { Input, Button, Spin } from 'antd';
import styled from 'styled-components';
export const CreateWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.container {
background-color: #ffffff;
display: flex;
flex-direction: column;
padding: 16px;
border-radius: 4px;
max-width: 500px;
width: 50%;
}
.header {
text-align: center;
font-size: 2em;
font-weight: bold;
color: #555555
}
.subheader {
font-size: 0.8em;
display: flex;
border-bottom: 1px solid black;
color: #444444
padding-left: 16px
padding-right: 16px;
}
.subheader-text {
text-align: center;
width: 100%;
}
`;
export const CreateInput = styled(Input)`
margin-top: 16px;
`;
export const CreatePassword = styled(Input.Password)`
margin-top: 16px;
`;
export const CreateEnter = styled(Button)`
align-self: center;
margin-top: 16px;
min-width: 128px;
width: 33%;
`;
export const CreateLogin = styled(Button)`
margin-top: 4px;
`;
export const CreateSpin = styled(Spin)`
position: absolute;
z-index: 10;
`;

View File

@ -1,99 +0,0 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate, useLocation } from "react-router-dom";
export function useCreate() {
const [checked, setChecked] = useState(true)
const [state, setState] = useState({
username: '',
password: '',
confirmed: '',
conflict: '',
spinning: false,
token: null,
});
const navigate = useNavigate();
const app = useContext(AppContext);
const { search } = useLocation();
const debounce = useRef(null)
const actions = {
setUsername: (username) => {
actions.updateState({ username });
usernameSet(username)
},
setPassword: (password) => {
actions.updateState({ password });
},
setConfirmed: (confirmed) => {
actions.updateState({ confirmed });
},
isDisabled: () => {
if (state.username !== '' && state.password !== '' && state.password === state.confirmed &&
checked && state.conflict === '') {
return false
}
return true
},
onLogin: async () => {
navigate('/login')
},
onCreate: async () => {
if (!state.spinning) {
actions.updateState({ spinning: true })
try {
await app.actions.create(state.username, state.password, state.token)
}
catch (err) {
window.alert(err);
}
actions.updateState({ spinning: false })
}
},
updateState: (value) => {
setState((s) => ({ ...s, ...value }));
},
};
const usernameSet = (name) => {
setChecked(false)
clearTimeout(debounce.current)
debounce.current = setTimeout(async () => {
if (app.actions && app.actions.username) {
if (name === '') {
setChecked(true)
actions.updateState({ conflict: '' })
}
else {
let valid = await app.actions.username(name, state.token)
setChecked(true)
if (!valid) {
actions.updateState({ conflict: 'not available' })
} else {
actions.updateState({ conflict: '' })
}
}
}
}, 500)
}
useEffect(() => {
if (app) {
if (app.state) {
if (app.state.access === 'user') {
navigate('/user')
}
else {
let params = new URLSearchParams(search);
let token = params.get("add");
if (token) {
actions.updateState({ token });
}
}
}
}
}, [app])
return { state, actions };
}

View File

@ -1,26 +0,0 @@
import React, { useContext, useEffect } from 'react'
import { useNavigate } from "react-router-dom";
import { AppContext } from 'context/AppContext';
export function Home() {
const navigate = useNavigate();
const app = useContext(AppContext);
useEffect(() => {
if (app?.state) {
if (app.state.access == null) {
navigate('/login')
}
else if (app.state.access === 'user') {
navigate('/user')
}
else if (app.state.access === 'admin') {
navigate('/admin')
}
}
}, [app])
return <></>
}

View File

@ -1,40 +0,0 @@
import React from 'react'
import { SettingOutlined, UserOutlined, LockOutlined } from '@ant-design/icons';
import { useLogin } from './useLogin.hook';
import { LoginWrapper, LoginInput, LoginPassword, LoginCreate, LoginEnter, LoginSpin } from './Login.styled';
export function Login(props) {
const { state, actions } = useLogin()
const keyDown = (e) => {
if (e.key === 'Enter') {
actions.onLogin()
}
}
return(
<LoginWrapper>
<div class="settings" onClick={() => actions.onSettings()}>
<SettingOutlined />
</div>
<div class="container">
<div class="header">databag</div>
<div class="subheader">
<span class="subheader-text">Communication for the Decentralized Web</span>
</div>
<LoginInput size="large" spellCheck="false" placeholder="username" prefix={<UserOutlined />}
onChange={(e) => actions.setUsername(e.target.value)} value={state.username} onKeyDown={(e) => keyDown(e)}/>
<LoginPassword size="large" spellCheck="false" placeholder="password" prefix={<LockOutlined />}
onChange={(e) => actions.setPassword(e.target.value)} value={state.password} onKeyDown={(e) => keyDown(e)}/>
<LoginEnter type="primary" onClick={() => actions.onLogin()} disabled={actions.isDisabled()}>
<span>Sign In</span>
</LoginEnter>
</div>
<LoginCreate type="text" onClick={() => actions.onCreate()} disabled={!state.available}>
<span>Create Account</span>
</LoginCreate>
<LoginSpin size="large" spinning={state.spinning} />
</LoginWrapper>
);
}

View File

@ -1,76 +0,0 @@
import { Input, Button, Spin } from 'antd';
import styled from 'styled-components';
export const LoginWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.container {
background-color: #ffffff;
display: flex;
flex-direction: column;
padding: 16px;
border-radius: 4px;
max-width: 500px;
width: 50%;
}
.header {
text-align: center;
font-size: 2em;
font-weight: bold;
color: #555555
}
.subheader {
font-size: 0.8em;
display: flex;
border-bottom: 1px solid black;
color: #444444
padding-left: 16px
padding-right: 16px;
}
.subheader-text {
text-align: center;
width: 100%;
}
.settings {
position: absolute;
top: 0px;
right: 0px;
padding: 16px;
color: #555555;
font-size: 20px;
cursor: pointer;
}
`;
export const LoginInput = styled(Input)`
margin-top: 16px;
`;
export const LoginPassword = styled(Input.Password)`
margin-top: 16px;
`;
export const LoginEnter = styled(Button)`
align-self: center;
margin-top: 16px;
min-width: 128px;
width: 33%;
`;
export const LoginCreate = styled(Button)`
margin-top: 4px;
`;
export const LoginSpin = styled(Spin)`
position: absolute;
z-index: 10;
`;

View File

@ -1,20 +0,0 @@
import React, { useState } from 'react'
import { UserOutlined } from '@ant-design/icons';
import { LogoWrapper } from './Logo.styled';
export function Logo({ imageSet, imageUrl }) {
if (!imageSet) {
return (
<LogoWrapper>
<UserOutlined />
</LogoWrapper>
)
} else {
return (
<LogoWrapper>
<img class='logo' src={ imageUrl } alt='' />
</LogoWrapper>
);
}
}

View File

@ -1,19 +0,0 @@
import styled from 'styled-components';
export const LogoWrapper = styled.div`
border-radius: 4px;
overflow: hidden;
width: 100%;
height: 100%;
display: flex;
font-size: 24px;
color: #888888;
align-items: center;
justify-content: center;
.logo {
width: 100%;
height: 100%;
}
`;

View File

@ -1,196 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { ExclamationCircleOutlined, CloseOutlined, UserOutlined } from '@ant-design/icons';
import { useContact } from './useContact.hook';
import { Button, Checkbox, Modal } from 'antd'
import { ContactWrapper, ProfileButton, CloseButton, ContactSpin } from './Contact.styled';
export function Contact() {
const { state, actions } = useContact();
const Logo = () => {
if (state.imageUrl != null) {
if (state.imageUrl === '') {
return <div class="logo"><UserOutlined /></div>
}
return <img class="logo" src={ state.imageUrl } alt="" />
}
return <></>
}
const Name = () => {
if (state.name == '' || state.name == null) {
return <span class="unset">Name</span>
}
return <span>{ state.name }</span>
}
const Location = () => {
if (state.location == '' || state.location == null) {
return <span class="unset">Location</span>
}
return <span>{ state.location }</span>
}
const Description = () => {
if (state.description == '' || state.description == null) {
return <span class="unset">Description</span>
}
return <span>{ state.description }</span>
}
const showDisconnect = () => {
Modal.confirm({
title: 'Do you want to disconnect from this contact?',
icon: <ExclamationCircleOutlined />,
okText: 'Yes, Disconnect',
cancelText: 'No, Cancel',
onOk() { actions.disconnect() },
});
};
const Disconnect = () => {
if (state.showButtons.disconnect) {
return <ProfileButton ghost onClick={() => showDisconnect()}>Disconnect</ProfileButton>
}
return <></>
}
const showDisconnectRemove = () => {
Modal.confirm({
title: 'Do you want to remove this contact?',
icon: <ExclamationCircleOutlined />,
okText: 'Yes, Remove',
cancelText: 'No, Cancel',
onOk() { actions.disconnectRemove() },
});
};
const DisconnectRemove = () => {
if (state.showButtons.disconnectRemove) {
return <ProfileButton ghost onClick={() => showDisconnectRemove()}>Remove Contact</ProfileButton>
}
return <></>
}
const showRemove = () => {
Modal.confirm({
title: 'Do you want to remove this contact??',
icon: <ExclamationCircleOutlined />,
okText: 'Yes, Remove',
cancelText: 'No, Cancel',
onOk() { actions.remove() },
});
};
const Remove = () => {
if (state.showButtons.remove) {
return <ProfileButton ghost onClick={() => showRemove()}>Remove Contact</ProfileButton>
}
return <></>
}
const Cancel = () => {
if (state.showButtons.cancel) {
return <ProfileButton ghost onClick={() => actions.disconnect()}>Cancel Request</ProfileButton>
}
return <></>
}
const Ignore = () => {
if (state.showButtons.ignore) {
return <ProfileButton ghost onClick={() => actions.remove()}>Ignore Request</ProfileButton>
}
return <></>
}
const Deny = () => {
if (state.showButtons.deny) {
return <ProfileButton ghost onClick={() => actions.disconnect()}>Ignore Request</ProfileButton>
}
return <></>
}
const Save = () => {
if (state.showButtons.save) {
return <ProfileButton ghost onClick={() => actions.save()}>Save Contact</ProfileButton>
}
return <></>
}
const Confirm = () => {
if (state.showButtons.confirm) {
return <ProfileButton ghost onClick={() => actions.confirm()}>Save Contact</ProfileButton>
}
return <></>
}
const ConfirmConnect = () => {
if (state.showButtons.confirmConnect) {
return <ProfileButton ghost onClick={() => actions.connect()}>Save & Connect</ProfileButton>
}
return <></>
}
const SaveRequest = () => {
if (state.showButtons.saveRequest) {
return <ProfileButton ghost onClick={() => actions.saveConnect()}>Save & Connect</ProfileButton>
}
return <></>
}
const Connect = () => {
if (state.showButtons.connect) {
return <ProfileButton ghost onClick={() => actions.connect()}>Connect</ProfileButton>
}
return <></>
}
const Accept = () => {
if (state.showButtons.accept) {
return <ProfileButton ghost onClick={() => actions.connect()}>Accept Connection</ProfileButton>
}
return <></>
}
return (
<ContactWrapper>
<div class="header">
<div class="title">{ state.handle }</div>
<div class="buttons">
<ContactSpin size="large" spinning={state.busy} />
<Remove />
<DisconnectRemove />
<Disconnect />
<Confirm />
<Cancel />
<Ignore />
<Deny />
<Save />
<ConfirmConnect />
<SaveRequest />
<Connect />
<Accept />
</div>
<CloseButton type="text" class="close" size={'large'} onClick={() => actions.close()} icon={<CloseOutlined />} />
</div>
<div class="container">
<div class="profile">
<div class="avatar">
<Logo />
</div>
<div class="block">
<span class="label">status: { state.status }</span>
</div>
<div class="details">
<div class="name"><Name /></div>
<div class="location"><Location /></div>
<div class="description"><Description /></div>
</div>
</div>
<div class="contact"></div>
</div>
</ContactWrapper>
)
}

View File

@ -1,167 +0,0 @@
import styled from 'styled-components';
import { Button, Spin } from 'antd';
export const ContactWrapper = styled.div`
display: flex;
width: 100%;
height: 100%;
background-color: #f6f5ed;
flex-direction: column;
align-items: center;
overflow: hidden;
.header {
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
background-color: #888888;
height: 64px;
padding-right: 16px;
padding-left: 16px;
}
.title {
height: 64px;
flex-grow: 1;
text-align: center;
font-size: 2em;
font-weight: bold;
display: flex;
align-items: center;
justify-content: flex-begin;
color: white;
padding-left: 16px;
}
.close {
font-size: 24px;
color: white;
}
.contact {
display: flex;
flex-direction: column;
align-items: flex-begin;
flex: 3
}
.control {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 8px;
}
.status {
color: #444444;
}
.buttons {
display: flex;
flex-direction: row;
margin-right: 32px;
align-items: center;
}
.profile {
display: flex;
flex-direction: column;
align-items: flex-end;
flex: 2
}
.container {
display: flex;
flex-direction: row;
padding: 32px;
width: 100%;
overflow: auto;
}
.avatar {
color: #888888;
height: 192px;
min-height: 192px;
width: 192px;
min-width: 192px;
font-size: 8em;
border-radius: 8px;
overflow: hidden;
border: 1px solid #888888;
}
.logo {
width: 192px;
height 192px;
display: flex;
align-items: center;
justify-content: center;
}
.unset {
font-style: italic;
color: #dddddd;
}
.label {
padding-right: 4;
font-size: 1em;
color: #888888;
}
.details {
padding: 16px;
border-right: 0.5px solid #aaaaaa;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.name {
font-size: 1.5em;
padding-bottom: 16px;
text-align: right;
}
.location {
font-size: 1.2em;
padding-bottom: 16px;
text-align: right;
}
.description {
font-size: 1em;
padding-bottom: 16px;
text-align: right;
}
.block {
border-bottom: 0.5px solid #aaaaaa;
display: flex;
flex-direction: row;
margin-top: 32px;
align-items: center;
justify-content: flex-end;
width: 50%;
}
`;
export const CloseButton = styled(Button)`
font-size: 24px;
color: white;
`;
export const ProfileButton = styled(Button)`
text-align: center;
margin-left: 8px;
margin-right: 8px;
`;
export const ContactSpin = styled(Spin)`
padding-right: 32px;
`;

View File

@ -1,213 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { getListingMessage } from 'api/getListingMessage';
import { getListingImageUrl } from 'api/getListingImageUrl';
export function useContact() {
const [state, setState] = useState({
status: null,
handle: '',
name: '',
location: '',
description: '',
imageUrl: null,
node: '',
cardId: '',
showButtons: {},
busy: false,
});
const data = useLocation();
const { guid } = useParams();
const navigate = useNavigate();
const card = useContext(CardContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
close: () => {
navigate('/user')
},
save: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
let message = await getListingMessage(state.node, guid);
await card.actions.addCard(message);
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
confirm: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await card.actions.setCardConfirmed(state.cardId);
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
connect: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await card.actions.setCardConnecting(state.cardId);
let message = await card.actions.getCardOpenMessage(state.cardId);
let contact = await card.actions.setCardOpenMessage(state.node, message);
if (contact.status === 'connected') {
await card.actions.setCardConnected(state.cardId, contact.token, contact.viewRevision, contact.articleRevision, contact.channelRevision, contact.profileRevision);
}
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
disconnect: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await card.actions.setCardConfirmed(state.cardId);
try {
let message = await card.actions.getCardCloseMessage(state.cardId);
await card.actions.setCardCloseMessage(state.node, message);
}
catch (err) {
console.log(err);
}
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
remove: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await card.actions.removeCard(state.cardId);
navigate('/user');
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
disconnectRemove: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await card.actions.setCardConfirmed(state.cardId);
try {
let message = await card.actions.getCardCloseMessage(state.cardId);
await card.actions.setCardCloseMessage(state.node, message);
await card.actions.removeCard(state.cardId);
navigate('/user');
}
catch (err) {
console.log(err);
}
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
saveConnect: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
let profile = await getListingMessage(state.node, guid);
let added = await card.actions.addCard(profile);
await card.actions.setCardConnecting(added.id);
let open = await card.actions.getCardOpenMessage(added.id);
let contact = await card.actions.setCardOpenMessage(state.node, open);
if (contact.status === 'connected') {
await card.actions.setCardConnected(added.id, contact.token, contact.viewRevision, contact.articleRevision, contact.channelRevision, contact.profileRevision);
}
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
};
const updateContact = () => {
let contact = card.actions.getCardByGuid(guid);
if (contact) {
let profile = contact.data.cardProfile;
updateState({ cardId: contact.id });
updateState({ handle: profile.handle });
updateState({ name: profile.name });
updateState({ description: profile.description });
updateState({ location: profile.location });
updateState({ node: profile.node });
if (contact.data.cardProfile.imageSet) {
updateState({ imageUrl: card.actions.getImageUrl(contact.id) });
}
else {
updateState({ imageUrl: '' });
}
let status = contact.data.cardDetail.status;
if (status === 'connected') {
updateState({ status: 'connected' });
updateState({ showButtons: { disconnect: true, disconnectRemove: true }});
}
if (status === 'connecting') {
updateState({ status: 'connecting' });
updateState({ showButtons: { cancel: true, disconnectRemove: true }});
}
if (status === 'pending') {
updateState({ status: 'pending' });
updateState({ showButtons: { ignore: true, confirm: true, confirmConnect: true }});
}
if (status === 'confirmed') {
updateState({ status: 'confirmed' });
updateState({ showButtons: { remove: true, connect: true }});
}
if (status === 'requested') {
updateState({ status: 'requested' });
updateState({ showButtons: { deny: true, accept: true }});
}
}
else if (data.state) {
updateState({ handle: data.state.handle });
updateState({ name: data.state.name });
updateState({ description: data.state.description });
updateState({ location: data.state.location });
updateState({ node: data.state.node });
if (data.state.imageSet) {
updateState({ imageUrl: getListingImageUrl(data.state.node, guid, data.state.revision) });
}
else {
updateState({ imageUrl: '' });
}
updateState({ status: 'unsaved' });
updateState({ showButtons: { save: true, saveRequest: true }});
}
}
useEffect(() => {
if (card.state.init) {
updateContact();
}
}, [card, guid])
return { state, actions };
}

View File

@ -1,138 +0,0 @@
import React, { useState, useRef } from 'react';
import ReactPlayer from 'react-player'
import { SketchPicker } from "react-color";
import { Button, Dropdown, Input, Tooltip, Menu } from 'antd';
import { AddTopicWrapper, BusySpin } from './AddTopic.styled';
import { Carousel } from '../../../Carousel/Carousel';
import { useAddTopic } from './useAddTopic.hook';
import { FontColorsOutlined, FontSizeOutlined, PaperClipOutlined, SendOutlined } from '@ant-design/icons';
import { AudioFile } from './AudioFile/AudioFile';
import { VideoFile } from './VideoFile/VideoFile';
export function AddTopic() {
let [ items, setItems] = useState([]);
const { state, actions } = useAddTopic();
const attachImage = useRef(null);
const attachAudio = useRef(null);
const attachVideo = useRef(null);
const onSelectImage = (e) => {
actions.addImage(e.target.files[0]);
attachImage.current.value = '';
}
const onSelectAudio = (e) => {
actions.addAudio(e.target.files[0]);
attachAudio.current.value = '';
}
const onSelectVideo = (e) => {
actions.addVideo(e.target.files[0]);
attachVideo.current.value = '';
}
const menu = (
<Menu>
<Menu.Item key="0">
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/>
<div onClick={() => attachImage.current.click()}>Attach Image</div>
</Menu.Item>
<Menu.Item key="1">
<input type='file' name="asset" accept="audio/*" ref={attachAudio} onChange={e => onSelectAudio(e)} style={{display: 'none'}}/>
<div onClick={() => attachAudio.current.click()}>Attach Audio</div>
</Menu.Item>
<Menu.Item key="2">
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => onSelectVideo(e)} style={{display: 'none'}}/>
<div onClick={() => attachVideo.current.click()}>Attach Video</div>
</Menu.Item>
</Menu>
);
const picker = (
<Menu style={{ backgroundColor: 'unset', boxShadow: 'unset' }}>
<SketchPicker disableAlpha={true}
color={state.textColor}
onChange={(color) => {
actions.setTextColor(color.hex);
}} />
</Menu>
);
const sizer = (
<Menu>
<Menu.Item key={8}><div onClick={() => actions.setTextSize(8)}>Small</div></Menu.Item>
<Menu.Item key={14}><div onClick={() => actions.setTextSize(14)}>Medium</div></Menu.Item>
<Menu.Item key={20}><div onClick={() => actions.setTextSize(20)}>Large</div></Menu.Item>
</Menu>
);
const onSend = () => {
if (state.messageText || state.assets.length) {
actions.addTopic();
}
}
const onKey = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (state.messageText) {
actions.addTopic();
}
}
}
const renderItem = (item, index) => {
if (item.image) {
return <img style={{ height: '100%', objectFit: 'contain' }} src={item.url} alt="" />
}
if (item.audio) {
return <AudioFile onLabel={(label) => actions.setLabel(index, label)}/>
}
if (item.video) {
return <VideoFile onPosition={(pos) => actions.setPosition(index, pos)} url={item.url} />
}
return <></>
}
const removeItem = (index) => {
actions.removeAsset(index);
}
return (
<AddTopicWrapper>
<div class="container noselect">
<div class="carousel">
<Carousel ready={true} items={state.assets} itemRenderer={renderItem} itemRemove={removeItem} />
</div>
<div class="input">
<Input.TextArea placeholder="Message" autoSize={{ minRows: 2, maxRows: 6 }} onKeyPress={onKey}
style={{ color: state.textColor, fontSize: state.textSize }}
onChange={(e) => actions.setMessageText(e.target.value)} value={state.messageText} />
</div>
<div class="buttons">
<div class="option">
<Dropdown overlay={menu} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="topRight">
<Button icon={<PaperClipOutlined />} size="large" />
</Dropdown>
</div>
<div class="option">
<Dropdown overlay={sizer} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="topRight">
<Button icon={<FontSizeOutlined />} size="large" />
</Dropdown>
</div>
<div class="option">
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="topRight">
<Button icon={<FontColorsOutlined />} size="large" />
</Dropdown>
</div>
<div class="send">
<Button icon={<SendOutlined />} onClick={onSend} size="large" loading={state.busy} />
</div>
</div>
</div>
</AddTopicWrapper>
);
}

View File

@ -1,54 +0,0 @@
import { Spin } from 'antd';
import styled from 'styled-components';
export const AddTopicWrapper = styled.div`
width: 100%;
.container {
overflow: hidden;
display: flex;
flex-direction: column;
padding-bottom: 24px;
border-top: 1px solid #dddddd;
}
.input {
margin-top: 16px;
padding-left: 16px;
padding-right: 16px;
width: 100%;
}
.carousel {
padding-left: 16px;
}
.buttons {
padding-left: 16px;
padding-right: 16px;
width: 100%;
display: flex;
flex-direction: row;
}
.option {
padding-right: 4px;
padding-top: 4px;
}
.send {
display: flex;
flex-grow: 1;
justify-content: flex-end;
align-items: center;
padding-top: 4px;
}
`;
export const BusySpin = styled(Spin)`
display: flex;
position: absolute;
right: 64px;
x-index: 10;
`;

View File

@ -1,149 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { ExclamationCircleOutlined, CloseOutlined, UserOutlined } from '@ant-design/icons';
import { useConversation } from './useConversation.hook';
import { Button, Input, Progress, Checkbox, Modal, Spin, Tooltip } from 'antd'
import { ConversationWrapper, ConversationButton, EditButton, CloseButton, ListItem, BusySpin, Offsync } from './Conversation.styled';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { AddTopic } from './AddTopic/AddTopic';
import { VirtualList } from 'VirtualList/VirtualList';
import { TopicItem } from './TopicItem/TopicItem';
import { Members } from './Members/Members';
import { EditOutlined, HomeOutlined, DatabaseOutlined } from '@ant-design/icons';
export function Conversation() {
const { state, actions } = useConversation();
const [ showEdit, setShowEdit ] = useState(false);
const [ showMembers, setShowMembers ] = useState(false);
const [ editSubject, setEditSubject ] = useState(null);
const [ subject, setSubject ] = useState(null);
useEffect(() => {
if (state.subject) {
setSubject(state.subject);
}
else {
setSubject(state.contacts);
}
}, [state]);
const onMoreTopics = () => {
actions.more();
}
const topicRenderer = (topic) => {
return (<TopicItem host={state.cardId == null} topic={topic} />)
}
const onSaveSubject = () => {
actions.setSubject(editSubject);
setShowEdit(false);
}
const onEdit = () => {
setEditSubject(state.subject);
setShowEdit(true);
}
const uploadProgress = () => {
let progress = [];
for (let entry of state.progress) {
if (entry.error) {
progress.push(
<div class="progress">
<div class="index">{ entry.index }/{ entry.count }</div>
<Progress percent={100} size="small" status="exception" showInfo={false} />
<Button type="link" onClick={() => actions.cancel(entry.topicId)}>Clear</Button>
</div>
);
}
else {
progress.push(
<div class="progress">
<div class="index">{ entry.index }/{ entry.count }</div>
<Progress percent={Math.floor(100 * entry.active?.loaded / entry.active?.total)} size="small" showInfo={false} />
<Button type="link" onClick={() => actions.cancel(entry.topicId)}>Cancel</Button>
</div>
);
}
}
return progress;
}
const onMembers = () => {
setShowMembers(true);
}
const Icon = () => {
if (state.cardId) {
return <DatabaseOutlined />
}
return <HomeOutlined />
}
const Edit = () => {
if (state.cardId) {
return <></>
}
return (
<EditButton type="text" size={'large'} onClick={() => onEdit()} icon={<EditOutlined />} />
)
}
const showDelete = () => {
Modal.confirm({
title: 'Do you want to delete this conversation?',
icon: <ExclamationCircleOutlined />,
okText: 'Yes, Delete',
cancelText: 'No, Cancel',
onOk() { actions.remove() },
});
};
return (
<ConversationWrapper>
<div class="header">
<div class="title">
<Icon />
<div class="subject">{ subject }</div>
<Edit />
{ state.error && (
<Tooltip placement="right" title="sync failed: click to retry">
<Offsync onClick={() => {actions.resync()}}>
<ExclamationCircleOutlined />
</Offsync>
</Tooltip>
)}
</div>
<div class="control">
<div class="buttons">
<ConversationButton ghost onClick={() => onMembers()}>Members</ConversationButton>
<ConversationButton ghost onClick={() => showDelete()}>Delete</ConversationButton>
</div>
<CloseButton type="text" class="close" size={'large'}
onClick={() => actions.close()} icon={<CloseOutlined />} />
</div>
</div>
<div class="thread">
{ state.progress && (
<div class="uploading">
{ uploadProgress() }
</div>
)}
<VirtualList id={state.channelId + state.cardId}
items={state.topics} itemRenderer={topicRenderer} onMore={onMoreTopics} />
<BusySpin size="large" delay="1000" spinning={state.loading} />
</div>
<AddTopic />
<Modal title="Conversation Members" visible={showMembers} centered onCancel={() => setShowMembers(false)}
width={400} bodyStyle={{ padding: 0 }} footer={[]} >
<Members host={state.cardId} members={state.members} />
</Modal>
<Modal title="Edit Subject" visible={showEdit} centered
okText="Save" onOk={() => onSaveSubject()} onCancel={() => setShowEdit(false)}>
<Input placeholder="Subject" onChange={(e) => setEditSubject(e.target.value)} value={editSubject} />
</Modal>
</ConversationWrapper>
)
}

View File

@ -1,140 +0,0 @@
import styled from 'styled-components';
import { Button, Spin } from 'antd';
export const ConversationWrapper = styled.div`
display: flex;
width: 100%;
height: 100%;
background-color: #f6f5ed;
flex-direction: column;
align-items: center;
overflow: hidden;
.edit {
font-size: 18px;
color: white;
}
.header {
flex-grow: 1;
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
background-color: #888888;
height: 64px;
padding-right: 16px;
padding-left: 16px;
}
.title {
display: flex;
flex-direction: row;
flex-shrink: 1;
jsutify-content: flex-begin;
height: 64px;
align-items: center;
color: white;
font-size: 1.5em;
min-width: 0;
padding-right: 8px;
}
.control {
display: flex;
flex-direction: row;
flex-grow: 1;
justify-content: flex-end;
}
.subject {
padding-left: 16px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.buttons {
display: flex;
flex-direction: row;
margin-right: 16px;
align-items: center;
}
.close {
font-size: 24px;
color: white;
}
.thread {
position: relative;
display: flex;
flex-grow: 1;
flex-direction: column;
width: 100%;
overflow: auto;
.uploading {
position: absolute;
top: 0px;
right: 0px;
display: flex;
flex-direction: column;
z-index: 10;
border-bottom: 1px solid #888888;
border-left: 1px solid #888888;
border-bottom-left-radius: 4px;
padding-left: 8px;
background-color: #ffffff;
.progress {
width: 250px;
display: flex;
flex-direction: row;
align-items: center;
.index {
display: flex;
width: 64px;
justify-content: center;
color: #444444;
font-size: 12px;
}
}
}
}
`;
export const ConversationButton = styled(Button)`
text-align: center;
margin-left: 8px;
margin-right: 8px;
`
export const EditButton = styled(Button)`
font-size: 24px;
color: white;
`;
export const CloseButton = styled(Button)`
font-size: 24px;
color: white;
`;
export const ListItem = styled.div`
dispaly: flex;
flex-direction: row;
width: 64px;
`;
export const BusySpin = styled(Spin)`
position: absolute;
left: calc(50% - 16px);
top: calc(50% - 16px);
`;
export const Offsync = styled.div`
padding-left: 8px;
color: #ff8888;
cursor: pointer;
`

View File

@ -1,44 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { Avatar } from 'avatar/Avatar';
import { MemberItemWrapper, CheckIcon, UncheckIcon } from './MemberItem.styled';
import { useMemberItem } from './useMemberItem.hook';
import { Button } from 'antd';
export function MemberItem({ readonly, item }) {
const { state, actions } = useMemberItem({ item });
const SetMembership = () => {
if (readonly) {
return <></>
}
if (item.member) {
return <Button type="link" icon={<CheckIcon />} loading={state.busy}
onClick={() => actions.clearMembership()} />
}
return <Button type="link" icon={<UncheckIcon />} loading={state.busy}
onClick={() => actions.setMembership()} />
}
const Unknown = () => {
if (state.handle) {
return <></>;
}
return <div class="unknown">unknown contact</div>;
}
return (
<MemberItemWrapper>
<div class="avatar">
<Avatar imageUrl={state.imageUrl} />
</div>
<div class="label">
<div class="name">{state.name}</div>
<div class="handle">{state.handle}</div>
<Unknown />
</div>
<SetMembership />
</MemberItemWrapper>
)
}

View File

@ -1,51 +0,0 @@
import styled from 'styled-components';
import { CheckSquareOutlined, BorderOutlined } from '@ant-design/icons';
export const MemberItemWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: row;
overflow: hidden;
padding-left: 16px;
padding-right: 16px;
padding-top: 2px;
padding-bottom: 2px;
align-items: center;
border-bottom: 1px solid #eeeeee;
.avatar {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
}
.label {
padding-left: 8px;
padding-right: 8px;
display: flex;
flex-direction: column;
flex-grow: 1;
.handle {
font-size: 0.8em;
font-weight: bold;
}
.unknown {
font-style: italic;
color: #AAAAAA;
}
.name {
}
}
`;
export const CheckIcon = styled(CheckSquareOutlined)`
font-size: 20px;
`;
export const UncheckIcon = styled(BorderOutlined)`
font-size: 20px;
`;

View File

@ -1,63 +0,0 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext';
import { ConversationContext } from 'context/ConversationContext';
export function useMemberItem({ item }) {
const [state, setState] = useState({
imageUrl: null,
name: null,
handle: null,
busy: false,
});
const card = useContext(CardContext);
const profile = useContext(ProfileContext);
const conversation = useContext(ConversationContext);
useEffect(() => {
let handle = item.card?.data.cardProfile.handle;
if (item.card?.data.cardProfile.node != profile.state?.profile.node) {
handle += '@' + item.card?.data.cardProfile.node;
}
updateState({
imageUrl: card.actions.getImageUrl(item.card?.id),
name: item.card?.data.cardProfile.name,
handle,
});
}, [item, card, profile]);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
setMembership: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
conversation.actions.setChannelCard(item.card.id);
}
catch(err) {
window.alert(err);
}
updateState({ busy: false });
}
},
clearMembership: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
conversation.actions.clearChannelCard(item.card.id);
}
catch(err) {
window.alert(err);
}
updateState({ busy: false });
}
},
};
return { state, actions };
}

View File

@ -1,24 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { useMembers } from './useMembers.hook';
import { List } from 'antd';
import { MemberItem } from './MemberItem/MemberItem';
import { MembersWrapper } from './Members.styled';
export function Members({ host, members }) {
const { state, actions } = useMembers({ host, members });
return (
<MembersWrapper>
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.contacts}
renderItem={item => (
<MemberItem readonly={state.readonly} item={item} />
)}
/>
</MembersWrapper>
)
}

View File

@ -1,8 +0,0 @@
import styled from 'styled-components';
export const MembersWrapper = styled.div`
width: 100%;
max-height: 240px;
overflow: auto;
`;

View File

@ -1,67 +0,0 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { CardContext } from 'context/CardContext';
export function useMembers({ host, members }) {
const [state, setState] = useState({
readonly: false,
contacts: [],
});
const card = useContext(CardContext);
useEffect(() => {
let readonly;
if (host) {
readonly = true;
}
else {
readonly = false;
}
let contacts = [];
if (readonly) {
members.forEach((value) => {
contacts.push({ member: true, card: card.actions.getCardByGuid(value) });
});
}
else {
card.state.cards.forEach((value, key, map) => {
contacts.push({ member: members.has(value.data.cardProfile.guid), card: value });
});
}
contacts.sort((a, b) => {
let aName = a.card?.data?.cardProfile?.name?.toLowerCase();
let bName = b.card?.data?.cardProfile?.name?.toLowerCase();
if (aName == null && bName == null) {
return 0;
}
if (aName == null && bName != null) {
return 1;
}
if (aName != null && bName == null) {
return -1;
}
if (aName < bName) {
return -1;
}
if (aName > bName) {
return 1;
}
return 0;
});
updateState({ readonly, contacts });
}, [host, members]);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
};
return { state, actions };
}

View File

@ -1,100 +0,0 @@
import styled from 'styled-components';
export const TopicItemWrapper = styled.div`
display: flex;
flex-direction: row;
width: 100%;
padding-left: 8px;
.avatar {
height: 32px;
width: 32px;
}
.topic {
display: flex;
flex-direction: column;
padding-left: 8px;
flex-grow: 1;
&:hover .options {
visibility: visible;
}
.options {
position: absolute;
top: 0;
right: 0;
visibility: hidden;
.buttons {
display: flex;
flex-direction: row;
border-radius: 4px;
background-color: #eeeeee;
border: 1px solid #555555;
margin-top: 2px;
.button {
font-size: 14px;
margin-left: 8px;
margin-right: 8px;
cursor: pointer;
}
}
}
.info {
display: flex;
flex-direction: row;
line-height: 1;
.comments {
padding-left: 8px;
cursor: pointer;
color: #888888;
}
.set {
font-weight: bold;
color: #444444;
padding-right: 8px;
}
.unset {
font-weight: bold;
font-style: italic;
color: #888888;
padding-right: 8px;
}
.unknown {
font-style: italic;
color: #aaaaaa;
padding-right: 8px;
}
}
.message {
padding-top: 6px;
padding-right: 16px;
white-space: pre-line;
.editing {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid #aaaaaa;
width: 100%;
.controls {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-bottom: 8px;
padding-right: 8px;
}
}
}
}
`;

View File

@ -1,96 +0,0 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { ConversationContext } from 'context/ConversationContext';
import { StoreContext } from 'context/StoreContext';
import { UploadContext } from 'context/UploadContext';
export function useConversation() {
const [state, setState] = useState({
loading: true,
cardId: null,
channelId: null,
subject: null,
contacts: null,
topics: [],
});
const { cardId, channelId } = useParams();
const navigate = useNavigate();
const conversation = useContext(ConversationContext);
const store = useContext(StoreContext);
const upload = useContext(UploadContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
if (cardId) {
updateState({ progress: upload.state.progress.get(`${cardId}:${channelId}`) });
}
else {
updateState({ progress: upload.state.progress.get(`:${channelId}`) });
}
}, [upload]);
const actions = {
close: () => {
navigate('/user')
},
setSubject: async (subject) => {
await conversation.actions.setChannelSubject(subject);
},
remove: async () => {
await conversation.actions.removeConversation();
navigate('/user');
},
more: () => {
conversation.actions.addHistory();
},
resync: () => {
conversation.actions.resync();
},
cancel: (topicId) => {
if (cardId) {
upload.actions.cancelContactTopic(cardId, channelId, topicId);
}
else {
upload.actions.cancelTopic(channelId, topicId);
}
},
};
useEffect(() => {
conversation.actions.setConversationId(cardId, channelId);
}, [cardId, channelId]);
useEffect(() => {
let topics = Array.from(conversation.state.topics.values()).sort((a, b) => {
if (a?.data?.topicDetail?.created > b?.data?.topicDetail?.created) {
return 1;
}
if (a?.data?.topicDetail?.created < b?.data?.topicDetail?.created) {
return -1;
}
return 0;
});
updateState({
loading: conversation.state.loading,
subject: conversation.state.subject,
contacts: conversation.state.contacts,
cardId: conversation.state.cardId,
channelId: conversation.state.channelId,
members: conversation.state.members,
error: conversation.state.error,
topics,
});
if (conversation.state.init) {
const channel = conversation.state.channelId;
const card = conversation.state.cardId;
store.actions.setValue(`${channel}::${card}`, conversation.state.revision);
}
}, [conversation]);
return { state, actions };
}

View File

@ -1,126 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { EditIcon, ProfileWrapper, CloseButton, ModalFooter, SelectButton } from './Profile.styled';
import { UserOutlined, CloseOutlined, EditOutlined } from '@ant-design/icons';
import { useProfile } from './useProfile.hook';
import { Button, Checkbox, Modal } from 'antd'
import { ProfileInfo } from './ProfileInfo/ProfileInfo';
import { ProfileImage } from './ProfileImage/ProfileImage';
export function Profile(props) {
const [ logoVisible, setLogoVisible ] = useState(false);
const [ infoVisible, setInfoVisible ] = useState(false);
const { state, actions } = useProfile();
const imageFile = useRef(null)
const Logo = () => {
if (state.imageUrl != null) {
if (state.imageUrl === '') {
return <div class="logo"><UserOutlined /></div>
}
return <img class="logo" src={ state.imageUrl } alt="" />
}
return <></>
}
const Name = () => {
if (state.name == '' || state.name == null) {
return <span class="unset">Name</span>
}
return <span>{ state.name }</span>
}
const Location = () => {
if (state.location == '' || state.location == null) {
return <span class="unset">Location</span>
}
return <span>{ state.location }</span>
}
const Description = () => {
if (state.description == '' || state.description == null) {
return <span class="unset">Description</span>
}
return <span>{ state.description }</span>
}
const onProfileSave = async () => {
if (await actions.setProfileData()) {
setInfoVisible(false);
}
}
const onImageSave = async () => {
if (await actions.setProfileImage()) {
setLogoVisible(false);
}
}
const onSelectImage = () => {
imageFile.current.click();
};
const selected = (e) => {
var reader = new FileReader();
reader.onload = () => {
actions.setModalImage(reader.result);
}
reader.readAsDataURL(e.target.files[0]);
}
const onSearchable = (flag) => {
actions.setSearchable(flag);
}
const Footer = (
<ModalFooter>
<input type='file' id='file' accept="image/*" ref={imageFile} onChange={e => selected(e)} style={{display: 'none'}}/>
<div class="select">
<Button key="select" class="select" onClick={() => onSelectImage()}>Select Image</Button>
</div>
<Button key="back" onClick={() => setLogoVisible(false)}>Cancel</Button>
<Button key="save" type="primary" onClick={() => onImageSave()}>Save</Button>
</ModalFooter>
);
return (
<ProfileWrapper>
<div class="header">
<div class="title">{ state.handle }</div>
<CloseButton type="text" class="close" size={'large'} onClick={() => actions.close()} icon={<CloseOutlined />} />
</div>
<div class="container">
<div class="profile">
<div class="registry">
<span class="search">Listed in Registry</span>
<Checkbox checked={state.searchable} onChange={(e) => onSearchable(e.target.checked)} />
</div>
<div class="avatar" onClick={() => setLogoVisible(true)}>
<div class="logoedit">
<EditIcon />
</div>
<Logo />
</div>
<div class="block" onClick={() => setInfoVisible(true)}>
<span class="label">details:</span>
<EditIcon class="detailedit" />
</div>
<div class="details">
<div class="name"><Name /></div>
<div class="location"><Location /></div>
<div class="description"><Description /></div>
</div>
</div>
<div class="contact"></div>
</div>
<Modal title="Profile Details" centered visible={infoVisible} okText="Save"
onOk={() => onProfileSave()} onCancel={() => setInfoVisible(false)}>
<ProfileInfo state={state} actions={actions} />
</Modal>
<Modal title="Profile Image" centered visible={logoVisible} footer={Footer} onCancel={() => setLogoVisible(false)}>
<ProfileImage state={state} actions={actions} />
</Modal>
</ProfileWrapper>
)
}

View File

@ -1,182 +0,0 @@
import { Input, Button, Spin } from 'antd';
import styled from 'styled-components';
import { EditOutlined } from '@ant-design/icons';
export const ProfileWrapper = styled.div`
display: flex;
width: 100%;
height: 100%;
background-color: #f6f5ed;
flex-direction: column;
align-items: center;
overflow: hidden;
.header {
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
background-color: #888888;
height: 64px;
padding-right: 16px;
padding-left: 16px;
}
.title {
height: 64px;
flex-grow: 1;
text-align: center;
font-size: 2em;
color: white;
font-weight: bold;
display: flex;
align-items: center;
justify-content: flex-begin;
padding-left: 16px;
}
.close {
font-size: 24px;
color: #aaaaaa;
}
.container {
display: flex;
flex-direction: row;
padding: 32px;
width: 100%;
overflow: auto;
}
.profile {
display: flex;
flex-direction: column;
align-items: flex-end;
flex: 2
}
.contact {
display: flex;
flex-direction: column;
align-items: flex-begin;
flex: 3
}
.registry {
display: flex;
flex-direction: row;
padding-bottom: 8px;
}
.search {
padding-right: 6px;
}
.logo {
width: 100%;
height: 192px;
width: 192px;
border: 1px solid #dddddd;
border-radius: 8px;
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
}
.avatar {
color: #888888;
height: 192px;
font-size: 8em;
display: flex;
justify-content: flex-end;
}
.logoedit {
align-self: flex-end;
font-size: 16px;
position: relative;
padding-right: 8px;
cursor: pointer;
background: #f6f5ed;
padding-left: 8px;
border-radius: 4px;
border: 1px solid #dddddd;
z-index: 10;
left: 192px;
}
.detailedit {
font-size: 16px;
}
.unset {
font-style: italic;
color: #dddddd;
}
.label {
padding-right: 8px;
font-size: 1em;
font-weight: bold;
color: #888888;
}
.details {
padding: 16px;
border-right: 0.5px solid #aaaaaa;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.name {
font-size: 1.5em;
padding-bottom: 16px;
text-align: right;
}
.location {
font-size: 1.2em;
padding-bottom: 16px;
text-align: right;
}
.description {
font-size: 1em;
padding-bottom: 16px;
text-align: right;
}
.block {
border-bottom: 0.5px solid #aaaaaa;
display: flex;
flex-direction: row;
margin-top: 32px;
align-items: center;
justify-content: flex-end;
width: 50%;
cursor: pointer;
}
`;
export const ModalFooter = styled.div`
width: 100%;
display: flex;
.select {
display: flex;
flex-grow: 1;
}
`
export const CloseButton = styled(Button)`
font-size: 24px;
color: white;
`;
export const EditIcon = styled(EditOutlined)`
color: #1890ff;
`;

View File

@ -1,30 +0,0 @@
import React, { useState, useCallback } from 'react';
import Cropper from 'react-easy-crop'
import { UserOutlined } from '@ant-design/icons';
import { ProfileSpin, ProfileImageWrapper, ProfileDefaultImage } from './ProfileImage.styled';
export function ProfileImage({ state, actions }) {
const [crop, setCrop] = useState({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1)
const onCropComplete = useCallback((area, crop) => {
actions.setModalCrop(crop.width, crop.height, crop.x, crop.y)
});
const Logo = () => {
if (state.modalImage == null) {
return <ProfileDefaultImage class="logo"><UserOutlined /></ProfileDefaultImage>
}
return <></>
}
return (
<ProfileImageWrapper>
<Cropper image={state.modalImage} crop={crop} zoom={zoom} aspect={1}
onCropChange={setCrop} onCropComplete={onCropComplete} onZoomChange={setZoom} />
<Logo />
<ProfileSpin size="large" spinning={state.modalBusy} />
</ProfileImageWrapper>
)
}

View File

@ -1,31 +0,0 @@
import { Spin } from 'antd';
import styled from 'styled-components';
export const ProfileImageWrapper = styled.div`
position: relative;
height: 200px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
export const ProfileDefaultImage = styled.div`
width: 192px;
height: 192px;
border: 1px solid #dddddd;
border-radius: 8px;
align-items: center;
display: flex;
justify-content: center;
position: absolute;
color: #888888;
font-size: 6em;
cursor: pointer;
`;
export const ProfileSpin = styled(Spin)`
position: absolute;
x-index: 10;
`;

View File

@ -1,17 +0,0 @@
import React from 'react';
import { ProfileInfoWrapper, ProfileInput, ProfileDescription, ProfileSpin } from './ProfileInfo.styled';
export function ProfileInfo({ state, actions }) {
return (
<ProfileInfoWrapper>
<ProfileInput size="large" spellCheck="false" placeholder="Name"
onChange={(e) => actions.setModalName(e.target.value)} value={state.modalName} />
<ProfileInput size="large" spellCheck="false" placeholder="Location"
onChange={(e) => actions.setModalLocation(e.target.value)} value={state.modalLocation} />
<ProfileDescription spellCheck="false" placeholder="Description" autoSize={{ minRows: 2, maxRows: 6 }}
onChange={(e) => actions.setModalDescription(e.target.value)} value={state.modalDescription} />
<ProfileSpin size="large" spinning={state.modalBusy} />
</ProfileInfoWrapper>
)
}

View File

@ -1,23 +0,0 @@
import { Input, TextArea, Spin } from 'antd';
import styled from 'styled-components';
export const ProfileInfoWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
`;
export const ProfileDescription = styled(Input.TextArea)`
margin-top: 16px;
`;
export const ProfileInput = styled(Input)`
margin-top: 16px;
`;
export const ProfileSpin = styled(Spin)`
position: absolute;
z-index: 10;
`;

View File

@ -1,141 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { ProfileContext } from 'context/ProfileContext';
import { AccountContext } from 'context/AccountContext';
import { useNavigate } from "react-router-dom";
const IMAGE_DIM = 256;
export function useProfile() {
const [state, setState] = useState({
name: '',
handle: '',
description: '',
location: '',
imageUrl: null,
searchable: false,
modalBusy: false,
modalName: '',
modalLocation: '',
modalDescription: '',
modalImage: null,
crop: { w :0, h: 0, x: 0, y: 0 }
});
const navigate = useNavigate();
const profile = useContext(ProfileContext);
const account = useContext(AccountContext);
console.log("ACCOUNT:", account);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
close: () => {
navigate('/user')
},
setModalName: (value) => {
updateState({ modalName: value });
},
setModalLocation: (value) => {
updateState({ modalLocation: value });
},
setModalDescription: (value) => {
updateState({ modalDescription: value });
},
setModalImage: (value) => {
updateState({ modalImage: value });
},
setModalCrop: (w, h, x, y) => {
updateState({ crop: { w: w, h: h, x: x, y: y } });
},
setProfileData: async () => {
let set = false
if(!state.modalBusy) {
updateState({ modalBusy: true });
try {
await profile.actions.setProfileData(state.modalName, state.modalLocation, state.modalDescription);
set = true
}
catch (err) {
window.alert(err)
}
updateState({ modalBusy: false });
}
return set
},
setSearchable: async (flag) => {
try {
await account.actions.setSearchable(flag);
}
catch (err) {
window.alert(err);
}
},
setProfileImage: async () => {
let set = false
if(!state.modalBusy) {
updateState({ modalBusy: true });
try {
const processImg = () => {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => {
var canvas = document.createElement("canvas");
var context = canvas.getContext('2d');
canvas.width = IMAGE_DIM;
canvas.height = IMAGE_DIM;
context.imageSmoothingQuality = "medium";
context.drawImage(img, state.crop.x, state.crop.y, state.crop.w, state.crop.h,
0, 0, IMAGE_DIM, IMAGE_DIM);
resolve(canvas.toDataURL());
}
img.onerror = reject;
img.src = state.modalImage;
});
};
let dataUrl = await processImg();
let data = dataUrl.split(",")[1];
await profile.actions.setProfileImage(data);
set = true
}
catch (err) {
window.alert(err);
}
updateState({ modalBusy: false });
}
return set;
},
};
useEffect(() => {
if (profile?.state?.profile) {
let identity = profile.state.profile;
if (identity.image != null) {
updateState({ imageUrl: profile.actions.profileImageUrl() })
updateState({ modalImage: profile.actions.profileImageUrl() })
} else {
updateState({ imageUrl: '' })
updateState({ modalImage: null })
}
updateState({ name: identity.name });
updateState({ modalName: identity.name });
updateState({ handle: identity.handle });
updateState({ description: identity.description });
updateState({ modalDescription: identity.description });
updateState({ location: identity.location });
updateState({ modalLocation: identity.location });
}
}, [profile]);
useEffect(() => {
if (account?.state?.status) {
let status = account.state.status;
updateState({ searchable: status.searchable });
}
}, [account])
return { state, actions };
}

View File

@ -1,89 +0,0 @@
import React, { useEffect } from 'react'
import { CardsWrapper, CardItem, Offsync } from './Cards.styled';
import { Drawer, List, Tooltip } from 'antd';
import { Registry } from './Registry/Registry';
import { useCards } from './useCards.hook';
import { Logo } from '../../../../Logo/Logo';
import { ExclamationCircleOutlined } from '@ant-design/icons';
export function Cards({ showRegistry }) {
const { state, actions } = useCards();
const onSelect = (item) => {
actions.select(item);
}
const cardProfile = (item) => {
if (item?.data?.cardProfile) {
return item.data.cardProfile;
}
return {}
}
const cardImage = (item) => {
if (actions?.getImageUrl) {
return actions.getImageUrl(item.id);
}
return null
}
const cardHandle = (item) => {
const profile = item.data?.cardProfile
if (profile) {
if (profile.node == state.node) {
return profile.handle;
}
return profile.handle + '@' + profile.node;
}
return null;
}
const Resync = (id) => {
actions.resync(id);
}
return (
<CardsWrapper>
<Drawer
placement="right"
closable={false}
visible={showRegistry}
getContainer={false}
contentWrapperStyle={{ width: '100%' }}
bodyStyle={{ backgroundColor: '#f6f5ed', paddingLeft: 16, paddingRight: 16, paddingTop: 16, paddingBottom: 0 }}
style={{ position: 'absolute' }}
>
<Registry />
</Drawer>
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.cards}
gutter="0"
renderItem={item => (
<CardItem onClick={() => onSelect(item)}>
<div class="item">
<div class="logo">
<Logo imageUrl={cardImage(item)}
imageSet={cardProfile(item).imageSet} />
</div>
{item.error && (
<Tooltip placement="topLeft" title="sync failed: click to retry">
<Offsync onClick={(e) => {e.stopPropagation(); Resync(item.id)}} >
<ExclamationCircleOutlined />
</Offsync>
</Tooltip>
)}
<div class="username">
<span class="name">{ cardProfile(item).name }</span>
<span class="handle">{ cardHandle(item) }</span>
</div>
</div>
</CardItem>
)}
/>
</CardsWrapper>
)
}

View File

@ -1,58 +0,0 @@
import { Button, List } from 'antd';
import styled from 'styled-components';
export const CardsWrapper = styled.div`
position: relative;
height: calc(100vh - 127px);
width: 100%;
overflow-y: auto;
overflow-x: hidden;;
text-align: center;
padding-top: 16px;
`;
export const CardItem = styled(List.Item)`
padding-left: 16px;
padding-right: 16px;
padding-top: 4px;
padding-bottom: 4px;
cursor: pointer;
&:hover {
background-color: #f0f5e0;
}
.item {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.logo {
width: 36px;
height: 36px;
}
.username {
display: flex;
flex-direction: column;
padding-left: 16px;
text-align: right;
flex-grow: 1;
}
.name {
font-size: 1em;
}
.handle {
font-size: 0.7em;
font-weight: bold;
}
`;
export const Offsync = styled.div`
padding-left: 4px;
color: #ff8888;
`

View File

@ -1,61 +0,0 @@
import React from 'react';
import { RegistryWrapper, RegistryItem } from './Registry.styled';
import { useRegistry } from './useRegistry.hook';
import { Button, Input, List } from 'antd';
import { Logo } from '../../../../../Logo/Logo';
import { SearchOutlined, MoreOutlined } from '@ant-design/icons';
export function Registry() {
const { state, actions } = useRegistry();
const onSelect = (item) => {
actions.select(item);
};
const registryImage = (item) => {
if (actions?.getRegistryImageUrl) {
return actions.getRegistryImageUrl(item.guid);
}
return null
}
return (
<RegistryWrapper>
{ state.server && (
<Input.Search placeholder="Server" value={state.server}
onChange={(e) => actions.setServer(e.target.value)}
onSearch={actions.getRegistry} style={{ width: '100%' }} />
)}
{ !state.server && (
<div class="local">
<div class="local-name">{ window.location.host }</div>
<Button icon={<SearchOutlined />} onClick={actions.getRegistry}></Button>
</div>
)}
<div class="contacts">
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.profiles}
gutter="0"
renderItem={item => (
<RegistryItem onClick={() => onSelect(item)}>
<div class="item">
<div class="logo">
<Logo imageUrl={registryImage(item)}
imageSet={item.imageSet} />
</div>
<div class="username">
<span class="handle">{ item.handle }</span>
<span class="name">{ item.name }</span>
</div>
</div>
</RegistryItem>
)}
/>
</div>
</RegistryWrapper>
);
}

View File

@ -1,77 +0,0 @@
import styled from 'styled-components';
import { List } from 'antd';
export const RegistryWrapper = styled.div`
position: relative;
text-align: center;
display: flex;
overflow-y: auto;
overflow-x: hidden;
flex-direction: column;
.local {
display: flex;
flex-directionL row;
align-items: center;
justify-content: center;
.local-name {
border: 1px solid rgb(221, 221, 221);
height: 32px;
display: flex;
align-items: center;
padding-right: 8px;
padding-left: 8px;
background-color: rgb(238, 238, 238);
}
}
.contacts {
flex-grow: 1
background-color: #fefefe;
border-radius-bottom-right: 8px;
border-radius-bottom-left: 8px;
height: calc(100vh - 175px);
overflow: auto;
}
.item {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.logo {
width: 36px;
height: 36px;
}
.username {
display: flex;
flex-direction: column;
padding-left: 16px;
text-align: right;
flex-grow: 1;
}
.name {
font-size: 1em;
}
.handle {
font-size: 0.9em;
font-weight: bold;
}
`;
export const RegistryItem = styled(List.Item)`
padding-top: 4px;
padding-bottom: 4px;
cursor: pointer;
&:hover {
background-color: #f0f5e0;
}
`;

View File

@ -1,61 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { ProfileContext } from 'context/ProfileContext';
import { useNavigate } from "react-router-dom";
import { getListing } from 'api/getListing';
import { getListingImageUrl } from 'api/getListingImageUrl';
export function useRegistry() {
const [state, setState] = useState({
server: '',
busy: false,
profiles: [],
});
const navigate = useNavigate();
const profile = useContext(ProfileContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
if (profile?.state?.profile) {
let identity = profile.state.profile;
if (identity.node == null || identity.node == '') {
updateState({ server: null });
}
else {
updateState({ server: identity.node });
}
}
}, [profile]);
const actions = {
setServer: (server) => {
updateState({ server: server });
},
getRegistry: async () => {
if (!state.busy && state.server != '') {
updateState({ busy: true });
try {
let profiles = await getListing(state.server)
updateState({ profiles: profiles.filter(contact => contact.guid !== profile.state.profile.guid) });
}
catch (err) {
window.alert(err)
}
updateState({ busy: false });
}
},
getRegistryImageUrl: (guid) => {
return getListingImageUrl(state.server, guid);
},
select: (contact) => {
navigate(`/user/contact/${contact.guid}`, { state: contact });
}
}
return { state, actions };
}

View File

@ -1,59 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext';
import { useNavigate } from 'react-router-dom';
export function useCards() {
const [state, setState] = useState({
cards: [],
node: null,
});
const navigate = useNavigate();
const card = useContext(CardContext);
const profile = useContext(ProfileContext);
const actions = {
getImageUrl: card.actions.getImageUrl,
select: (contact) => {
navigate(`/user/contact/${contact.data.cardProfile.guid}`);
},
resync: (id) => {
card.actions.resync(id);
}
};
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({ cards: Array.from(card.state.cards.values()).sort((a, b) => {
let aName = a.data?.cardProfile?.name?.toLowerCase();
let bName = b.data?.cardProfile?.name?.toLowerCase();
if (aName == null && bName == null) {
return 0;
}
if (aName == null && bName != null) {
return 1;
}
if (aName != null && bName == null) {
return -1;
}
if (aName < bName) {
return -1;
}
if (aName > bName) {
return 1;
}
return 0;
})});
}, [card])
useEffect(() => {
updateState({ node: profile.state?.profile?.node });
}, [profile])
return { state, actions };
}

View File

@ -1,41 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Space, Button, Select, Modal, Collapse, Input } from 'antd';
import { SelectItem, ConversationWrapper, Description, BusySpin } from './AddChannel.styled';
import { Logo } from '../../../../../Logo/Logo';
export function AddChannel({ state, actions }) {
const [ options, setOptions ] = useState([]);
useEffect(() => {
let contacts = [];
let cards = actions.getCards();
for (let card of cards) {
let handle = card.data.cardProfile.handle;
if (state.node != card.data.cardProfile.node) {
handle += '@' + card.data.cardProfile.node;
}
contacts.push({ value: card.id, label: handle });
}
setOptions(contacts);
}, [actions]);
return (
<ConversationWrapper>
<Space direction="vertical">
<Input placeholder="Subject (optional)" onChange={(e) => actions.setStartSubject(e.target.value)}
value={state.startSubject} />
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Select Contacts"
defaultValue={[]}
options={options}
onChange={(value) => actions.setStartCards(value)}
optionLabelProp="label"
/>
</Space>
<BusySpin size="large" spinning={state.busy} />
</ConversationWrapper>
)
}

View File

@ -1,25 +0,0 @@
import styled from 'styled-components';
import { Input, Spin } from 'antd';
export const ConversationWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
export const SelectItem = styled.div`
width: 100%;
display: flex;
flex-direction: row;
`;
export const Description = styled(Input.TextArea)`
margin-top: 16px;
`;
export const BusySpin = styled(Spin)`
position: absolute;
align-self: center;
z-index: 10;
`

View File

@ -1,25 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useChannelItem } from './useChannelItem.hook';
import { ChannelItemWrapper, Marker } from './ChannelItem.styled';
import { ChannelLogo } from './ChannelLogo/ChannelLogo';
import { ChannelLabel } from './ChannelLabel/ChannelLabel';
export function ChannelItem({ item }) {
let { state, actions } = useChannelItem(item);
const onSelect = () => {
actions.select(item);
}
return (
<ChannelItemWrapper onClick={() => onSelect()}>
<ChannelLogo item={item} />
{item.updated && (
<Marker />
)}
<ChannelLabel item={item} />
</ChannelItemWrapper>
)
}

View File

@ -1,24 +0,0 @@
import styled from 'styled-components';
import { List } from 'antd';
import { StarTwoTone } from '@ant-design/icons';
export const ChannelItemWrapper = styled(List.Item)`
padding-left: 16px;
padding-right: 16px;
padding-top: 4px;
padding-bottom: 4px;
height: 48px;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
&:hover {
background-color: #f0f5e0;
}
`;
export const Marker = styled(StarTwoTone)`
position: relative;
left: -16px;
top: 8px;
`

View File

@ -1,78 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useChannelLabel } from './useChannelLabel.hook';
import { LabelWrapper } from './ChannelLabel.styled';
import { HomeOutlined, DatabaseOutlined } from '@ant-design/icons';
export function ChannelLabel({ item }) {
const [host, setHost] = useState(null);
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const { state, actions } = useChannelLabel();
useEffect(() => {
try {
if (item.data.channelSummary.lastTopic.dataType === 'superbasictopic') {
let msg = JSON.parse(item.data.channelSummary.lastTopic.data);
setMessage(msg.text);
}
else {
setMessage('');
}
}
catch (err) {
console.log(err);
setMessage('');
}
let contacts = [];
if (item?.guid) {
setHost(false);
contacts.push(actions.getCardByGuid(item.guid)?.data?.cardProfile?.handle);
}
else {
setHost(true);
}
for (let member of item.data.channelDetail.members) {
let contact = actions.getCardByGuid(member)?.data?.cardProfile?.handle;
if (contact) {
contacts.push(contact);
}
}
if (item?.data?.channelDetail?.data) {
try {
let data = JSON.parse(item.data.channelDetail.data);
if (data.subject != '' && data.subject != null) {
setSubject(data.subject);
return
}
}
catch (err) {
console.log(err);
setSubject(null);
}
}
setSubject(contacts.join(', '));
}, [item, state]);
const Host = () => {
if (host) {
return <HomeOutlined />
}
return <DatabaseOutlined />
}
return (
<LabelWrapper>
<div class="title">
<div class="subject">{subject}</div>
<Host />
</div>
<div class="message">{message}</div>
</LabelWrapper>
)
}

View File

@ -1,38 +0,0 @@
import styled from 'styled-components';
export const LabelWrapper = styled.div`
display: flex;
width: calc(100% - 48px);
flex-direction: column;
justify-content: center;
flex-grow: 1;
overflow: hidden;
color: #444444;
.title {
display: flex;
flex-direction: row;
align-items: center;
.subject {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: right;
padding-right: 8px;
font-weight: bold;
}
}
.message {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: right;
padding-right: 8px;
color: #888888;
}
`;

View File

@ -1,33 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
import { ChannelContext } from 'context/ChannelContext';
import { ProfileContext } from 'context/ProfileContext';
import { getCardImageUrl } from 'api/getCardImageUrl';
export function useChannelLabel() {
const [state, setState] = useState({
guid: null
});
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
const profile = useContext(ProfileContext);
const actions = {
getCardByGuid: card.actions.getCardByGuid,
};
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
if (card.state.init && profile.state.init) {
updateState({ guid: profile.state.profile.guid });
}
}, [channel, card, profile])
return { state, actions };
}

View File

@ -1,135 +0,0 @@
import React, { useEffect, useState, useContext } from 'react'
import { DisconnectOutlined, UserOutlined } from '@ant-design/icons';
import { LogoWrapper, Contact, Host, ChannelImage } from './ChannelLogo.styled';
import { useChannelLogo } from './useChannelLogo.hook';
export function ChannelLogo({ item }) {
const [home, setHome] = useState(false);
const [host, setHost] = useState(null);
const [members, setMembers] = useState([]);
const { state, actions } = useChannelLogo();
useEffect(() => {
if (item?.guid) {
setHome(false);
setHost(actions.getCardByGuid(item.guid));
let contacts = [];
for (let member of item.data.channelDetail.members) {
if (member != state.guid) {
contacts.push(actions.getCardByGuid(member));
}
}
setMembers(contacts);
}
else {
setHome(true);
let contacts = [];
for (let member of item.data.channelDetail.members) {
contacts.push(actions.getCardByGuid(member));
}
setMembers(contacts);
}
}, [item?.data?.channelDetail?.members, state]);
const Logo = ({card}) => {
if (card?.data?.cardProfile?.imageSet) {
let imageUrl = actions.getCardImageUrl(card?.id, card?.revision);
return <ChannelImage src={ imageUrl } alt='' />
}
return <UserOutlined />
}
if (members.length == 0 && home) {
return (
<LogoWrapper>
<div class="container">
<div class="large contact"><DisconnectOutlined /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 0 && !home) {
return (
<LogoWrapper>
<div class="container">
<div class="large host"><Logo card={host} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 1 && home) {
return (
<LogoWrapper>
<div class="container">
<div class="large contact"><Logo card={members[0]} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 1 && !home) {
return (
<LogoWrapper>
<div class="grid">
<div class="medium host topleft"><Logo card={host} /></div>
<div class="medium contact bottomright"><Logo card={members[0]} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 2 && home) {
return (
<LogoWrapper>
<div class="grid">
<div class="medium host topleft"><Logo card={members[0]} /></div>
<div class="medium contact bottomright"><Logo card={members[1]} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 2 && !home) {
return (
<LogoWrapper>
<div class="grid">
<div class="small host topleft"><Logo card={host} /></div>
<div class="small contact topright"><Logo card={members[0]} /></div>
<div class="small contact bottom"><Logo card={members[1]} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length == 3 && home) {
return (
<LogoWrapper>
<div class="grid">
<div class="small contact topleft"><Logo card={members[0]} /></div>
<div class="small contact topright"><Logo card={members[1]} /></div>
<div class="small contact bottom"><Logo card={members[2]} /></div>
</div>
</LogoWrapper>
)
}
else if (members.length > 2 && !home) {
return (
<LogoWrapper>
<div class="grid">
<div class="medium host topleft"><Logo card={host} /></div>
<div class="medium contact bottomright">{members.length}</div>
</div>
</LogoWrapper>
)
}
else if (members.length > 3 && home) {
return (
<LogoWrapper>
<div class="container">
<div class="large contact">{members.length}</div>
</div>
</LogoWrapper>
)
}
return <></>
}

View File

@ -1,90 +0,0 @@
import styled from 'styled-components';
export const LogoWrapper = styled.div`
height: 48px;
width: 48px;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
.container {
display: flex;
align-items: center;
justify-content: center;
}
.grid {
position: relative;
margin-left: 2px;
margin-right: 2px;
width: 44px;
height: 40px;
}
.large {
border-radius: 18px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.medium {
border-radius: 16px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.small {
border-radius: 16px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.host {
overflow: hidden;
border: 2px solid #88cc88;
}
.contact {
overflow: hidden;
border: 1px solid #777777;
}
.topleft {
position: absolute;
top: 0px;
left: 0px;
}
.topright {
position: absolute;
top: 0px;
right: 0px;
}
.bottomright {
position: absolute;
bottom: 0px;
right: 0px;
}
.bottom {
position: absolute;
bottom: 0px;
left: 12px;
}
`;
export const ChannelImage = styled.img`
width: 100%;
height: 100%;
`;

View File

@ -1,30 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext';
export function useChannelLogo() {
const [state, setState] = useState({
guid: null
});
const card = useContext(CardContext);
const profile = useContext(ProfileContext);
const actions = {
getCardImageUrl: card.actions.getImageUrl,
getCardByGuid: card.actions.getCardByGuid,
};
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
if (card.state.init && profile.state.init) {
updateState({ guid: profile.state.profile.guid })
}
}, [card, profile])
return { state, actions };
}

View File

@ -1,28 +0,0 @@
import { useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export function useChannelItem(item) {
const [state, setState] = useState({
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const navigate = useNavigate();
const actions = {
select: (item) => {
if (item.guid) {
navigate(`/user/conversation/${item.cardId}/${item.id}`);
}
else {
navigate(`/user/conversation/${item.id}`);
}
},
};
return { state, actions };
}

View File

@ -1,35 +0,0 @@
import React, { useEffect, useState } from 'react'
import { ChannelsWrapper } from './Channels.styled';
import { List, Button, Select, Modal } from 'antd';
import { useChannels } from './useChannels.hook';
import { AddChannel } from './AddChannel/AddChannel';
import { ChannelItem } from './ChannelItem/ChannelItem';
export function Channels({ showAdd, setShowAdd }) {
const { state, actions } = useChannels();
const onStart = async () => {
if (await actions.addChannel()) {
setShowAdd(false);
}
}
return (
<ChannelsWrapper>
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.channels}
gutter="0"
renderItem={item => (
<ChannelItem item={item} />
)}
/>
<Modal title="Start Conversation" visible={showAdd} centered
okText="Start" onOk={() => onStart()} onCancel={() => setShowAdd(false)}>
<AddChannel state={state} actions={actions} />
</Modal>
</ChannelsWrapper>
)
}

View File

@ -1,13 +0,0 @@
import styled from 'styled-components';
import { List } from 'antd';
export const ChannelsWrapper = styled.div`
position: relative;
height: calc(100vh - 127px);
width: 100%;
overflow: auto;
text-align: center;
border-radius: 2px;
padding-top: 16px;
`;

View File

@ -1,117 +0,0 @@
import { useContext, useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext';
import { ChannelContext } from 'context/ChannelContext';
import { StoreContext } from 'context/StoreContext';
export function useChannels() {
const [state, setState] = useState({
channels: [],
startCards: [],
startSubject: '',
startDescription: '',
node: null,
busy: false,
});
let cardChannels = useRef([]);
let channels = useRef([]);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const navigate = useNavigate();
const card = useContext(CardContext);
const profile = useContext(ProfileContext);
const channel = useContext(ChannelContext);
const store = useContext(StoreContext);
const actions = {
getCardImageUrl: card.actions.getImageUrl,
getCards: () => {
let cards = Array.from(card.state.cards.values())
return cards.filter(c => c.data.cardDetail.status == 'connected');
},
setStartCards: (cards) => updateState({ startCards: cards }),
setStartSubject: (value) => updateState({ startSubject: value }),
setStartDescription: (value) => updateState({ startDescription: value }),
addChannel: async () => {
let done = false;
if (!state.busy) {
updateState({ busy: true });
try {
let added = await channel.actions.addChannel(state.startCards, state.startSubject, state.startDescription);
done = true;
navigate(`/user/conversation/${added.id}`);
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
return done;
}
};
const setUpdated = (chan) => {
const login = store.state['login:timestamp'];
const update = chan?.data?.channelSummary?.lastTopic?.created;
if (!update || (login && update < login)) {
chan.updated = false;
return;
}
let key = `${chan.id}::${chan.cardId}`
if (store.state[key] && store.state[key] == chan.revision) {
chan.updated = false;
}
else {
chan.updated = true;
}
}
useEffect(() => {
let merged = [ ...channels.current, ...cardChannels.current ];
merged.forEach(c => { setUpdated(c) });
}, [store]);
useEffect(() => {
cardChannels.current = [];
card.state.cards.forEach((value, key, map) => {
cardChannels.current.push(...Array.from(value.channels.values()));
});
cardChannels.current.forEach(c => { setUpdated(c) });
let merged = [ ...channels.current, ...cardChannels.current ];
merged.sort((a, b) => {
if (a?.data?.channelSummary?.lastTopic?.created > b?.data?.channelSummary?.lastTopic?.created) {
return -1;
}
return 1;
});
updateState({ channels: merged });
}, [card])
useEffect(() => {
channels.current = Array.from(channel.state.channels.values());
channels.current.forEach(c => { setUpdated(c) });
let merged = [ ...channels.current, ...cardChannels.current ];
merged.sort((a, b) => {
if (a?.data?.channelSummary?.lastTopic?.created > b?.data?.channelSummary?.lastTopic?.created) {
return -1;
}
return 1;
});
updateState({ channels: merged });
}, [channel])
useEffect(() => {
updateState({ node: profile.state.profile.node });
}, [profile])
return { state, actions };
}

View File

@ -1,64 +0,0 @@
import { Avatar, Image } from 'antd';
import React, { useState, useEffect, useRef } from 'react'
import { ContactsWrapper, AddButton } from './Contacts.styled';
import { useContacts } from './useContacts.hook';
import { Tabs, Button, Tooltip } from 'antd';
import { Cards } from './Cards/Cards';
import { Channels } from './Channels/Channels';
import { TeamOutlined, CommentOutlined } from '@ant-design/icons';
const { TabPane } = Tabs;
export function Contacts() {
const { state, actions } = useContacts()
const [addButton, setAddButton] = useState(<></>);
const [showRegistry, setShowRegistry] = useState(false);
const [startConversation, setStartConversation] = useState(false);
let registry = useRef(false);
const onShowRegistry = () => {
registry.current = !registry.current;
setShowRegistry(registry.current);
}
const addUser = (
<Tooltip placement="right" title="Add Contact">
<AddButton type="primary" onClick={() => onShowRegistry()} icon={<TeamOutlined />} />
</Tooltip>
)
const addConversation = (
<Tooltip placement="right" title="Add Conversation">
<AddButton type="primary" onClick={() => setStartConversation(true)} icon={<CommentOutlined />} />
</Tooltip>
)
const onTab = (key) => {
registry.current = false;
setShowRegistry(false);
if (key === "contact") {
setAddButton(addUser);
}
else {
setAddButton(addConversation);
}
}
useEffect(() => {
setAddButton(addConversation);
}, []);
return (
<ContactsWrapper>
<Tabs onChange={onTab} tabBarStyle={{ marginBottom: 0, paddingLeft: 16, paddingRight: 16 }} tabBarExtraContent={addButton} defaultActiveKey="conversation">
<TabPane tab="Contacts" key="contact">
<Cards showRegistry={showRegistry} />
</TabPane>
<TabPane tab="Conversations" key="conversation">
<Channels showAdd={startConversation} setShowAdd={setStartConversation} />
</TabPane>
</Tabs>
</ContactsWrapper>
)
}

View File

@ -1,12 +0,0 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const ContactsWrapper = styled.div`
width: 100%;
background-color: #f6f5ed;
padding-top: 16px;
`;
export const AddButton = styled(Button)`
border-radius: 8px;
`;

View File

@ -1,14 +0,0 @@
import { useContext, useState, useEffect } from 'react';
export function useContacts() {
const [state, setState] = useState({});
const actions = {};
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
return { state, actions };
}

View File

@ -1,88 +0,0 @@
import { Avatar, Space, Tooltip, Image, Modal, Form, Input, Button } from 'antd';
import React, { useState } from 'react'
import { IdentityWrapper, IdentityDropdown, MenuWrapper, AlertIcon } from './Identity.styled';
import { ExclamationCircleOutlined, RightOutlined, EditOutlined, UserOutlined, LockOutlined } from '@ant-design/icons';
import { useIdentity } from './useIdentity.hook';
import { Menu, Dropdown } from 'antd';
import { Logo } from '../../../Logo/Logo';
export function Identity() {
const { state, actions } = useIdentity()
const showLogout = () => {
Modal.confirm({
title: 'Do you want to logout?',
icon: <ExclamationCircleOutlined />,
okText: 'Yes, Logout',
cancelText: 'No, Cancel',
onOk() { actions.logout() },
});
};
const menu = (
<MenuWrapper>
<Menu.Item key="0">
<div onClick={() => actions.editProfile()}>Edit Profile</div>
</Menu.Item>
<Menu.Item key="1">
<div onClick={() => actions.setShowLogin(true)}>Change Login</div>
</Menu.Item>
<Menu.Item key="2">
<div onClick={() => showLogout()}>Sign Out</div>
</Menu.Item>
</MenuWrapper>
);
const Disconnected = () => {
if(state.disconnected) {
return (
<Tooltip placement="right" title="Disconnected">
<AlertIcon />
</Tooltip>
)
}
return <></>;
}
return (
<IdentityWrapper>
<IdentityDropdown overlay={menu} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="rightTop">
<div class="container">
<div class="avatar">
<Logo imageSet={state.image!=null} imageUrl={state.imageUrl} />
</div>
<div class="username">
<span class="name">{ state.name }</span>
<div class="handle">
<Disconnected />
<span>{ state.handle }</span>
</div>
</div>
<RightOutlined />
</div>
</IdentityDropdown>
<Modal title="Account Login" visible={state.showLogin} centered okText="Save"
onCancel={() => actions.setShowLogin(false)} loading={state.busy}
footer={[
<Button key="back" onClick={() => actions.setShowLogin(false)}>Cancel</Button>,
<Button key="save" type="primary" onClick={() => actions.setLogin()}>Save</Button>
]}>
<Space direction="vertical" style={{ width: '100%' }}>
<Input size="large" spelleCheck="false" placeholder="Username" prefix={<UserOutlined />}
onChange={(e) => actions.setUsername(e.target.value)} defaultValue={state.handle}
addonAfter={state.usernameStatus} />
<Input.Password size="large" spelleCheck="false" placeholder="Password" prefix={<LockOutlined />}
onChange={(e) => actions.setPassword(e.target.value)}
addonAfter={state.passwordStatus} />
<Input.Password size="large" spelleCheck="false" placeholder="Confirm Password" prefix={<LockOutlined />}
onChange={(e) => actions.setConfirm(e.target.value)}
addonAfter={state.confirmStatus} />
</Space>
</Modal>
</IdentityWrapper>
)
}

View File

@ -1,85 +0,0 @@
import { Menu, Button, Dropdown } from 'antd';
import styled from 'styled-components';
import { ExclamationCircleOutlined } from '@ant-design/icons';
export const IdentityWrapper = styled.div`
border-bottom: 1px solid #8fbea7;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
background-color: #f6f5ed;
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-left: 16px;
padding-right: 16px;
padding-top: 4px;
padding-bottom: 4px;
background-color: #f6f5ed;
}
.logo {
width: 100%;
height: 100%;
}
.username {
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding-right: 8px;
}
.avatar {
color: #888888;
font-size: 3em;
width: 48px;
height: 48px;
border-radius: 6px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.name {
font-size: 1.25em;
color: #444444;
}
.handle {
font-size: 1em;
color: #444444;
font-weight: bold;
}
`;
export const AlertIcon = styled(ExclamationCircleOutlined)`
color: red;
margin-right: 6px;
`;
export const MenuWrapper = styled(Menu)`
border-radius: 4px;
`;
export const IdentityDropdown = styled(Dropdown)`
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 16px;
padding-right: 16px;
`;

View File

@ -1,132 +0,0 @@
import { useContext, useState, useRef, useEffect } from 'react';
import { AppContext } from 'context/AppContext';
import { ProfileContext } from 'context/ProfileContext';
import { AccountContext } from 'context/AccountContext';
import { useNavigate } from "react-router-dom";
import { getUsername } from 'api/getUsername';
export function useIdentity() {
const [state, setState] = useState({
name: '',
handle: '',
domain: '',
imageUrl: null,
image: null,
username: null,
usernameStatus: null,
password: null,
passwordStatus: null,
confirm: null,
confirmStatus: null,
busy: false,
showLogin: false,
disconnected: false,
});
const navigate = useNavigate();
const profile = useContext(ProfileContext);
const account = useContext(AccountContext);
const app = useContext(AppContext);
const debounce = useRef(null);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
logout: async () => {
app.actions.logout();
navigate('/');
},
editProfile: () => {
navigate('/user/profile');
},
setUsername: (value) => {
if (debounce.current) {
clearTimeout(debounce.current);
}
updateState({ username: value });
if (state.handle.toLowerCase() == value.toLowerCase() || value == null || value == '') {
updateState({ usernameStatus: null });
return;
}
debounce.current = setTimeout(async () => {
let available = await getUsername(value);
if (available) {
updateState({ usernameStatus: null });
}
else {
updateState({ usernameStatus: 'not available' });
}
}, 500);
},
setPassword: (value) => {
updateState({ password: value });
},
setConfirm: (value) => {
updateState({ confirm: value });
},
setShowLogin: (value) => {
updateState({ showLogin: value });
},
setLogin: async () => {
if (state.username == null || state.username == '') {
updateState({ usernameStatus: 'username required' });
throw 'username required';
}
else {
updateState({ usernameStatus: null });
}
if (state.password == null || state.password == '') {
updateState({ passwordStatus: 'password required' });
throw 'password required';
}
else {
updateState({ passwordStatus: null });
}
if (state.confirm != state.password) {
updateState({ confirmStatus: 'password mismatch' });
throw 'password mismatch';
}
else {
updateState({ confirmStatus: null });
}
if (!state.busy) {
updateState({ busy: true });
try {
await account.actions.setLogin(state.username, state.password);
updateState({ showLogin: false });
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
};
useEffect(() => {
if (app.state) {
if (app.state.access != 'user') {
navigate('/');
}
updateState({ disconnected: app.state.disconnected });
}
}, [app]);
useEffect(() => {
if (profile?.state?.profile) {
let identity = profile.state.profile;
updateState({ imageUrl: profile.actions.profileImageUrl() })
updateState({ image: identity.image });
updateState({ name: identity.name });
updateState({ handle: identity.handle });
updateState({ username: identity.handle });
updateState({ domain: identity.node });
}
}, [profile])
return { state, actions };
}

View File

@ -1,14 +0,0 @@
import React from 'react'
import { SideBarWrapper } from './SideBar.styled';
import { Identity } from './Identity/Identity';
import { Contacts } from './Contacts/Contacts';
export function SideBar() {
return (
<SideBarWrapper>
<Identity />
<Contacts />
</SideBarWrapper>
)
}

View File

@ -1,11 +0,0 @@
import styled from 'styled-components';
export const SideBarWrapper = styled.div`
width: 20%;
height: 100%;
min-width: 260px;
max-width: 300px;
flex-shrink 0;
border-right: 1px solid #8fbea7;
background-color: #8fbea7;
`;

View File

@ -1,26 +0,0 @@
import React from 'react'
import { Outlet } from 'react-router-dom';
import { useUser } from './useUser.hook';
import { Button } from 'antd';
import { UserWrapper } from './User.styled';
import { SideBar } from './SideBar/SideBar';
import connect from '../connect.png';
export function User() {
const { state, actions } = useUser()
return (
<UserWrapper>
<SideBar />
<div class="canvas">
<img class="connect" src={connect} alt="" />
<div class="page">
<Outlet />
</div>
</div>
</UserWrapper>
)
}

View File

@ -1,45 +0,0 @@
import { Input, Button, Spin } from 'antd';
import styled from 'styled-components';
export const UserWrapper = styled.div`
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f6f5ed;
webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.canvas {
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
background-color: #8fbea7;
align-items: center;
justify-content: center;
min-width: 0;
}
.connect {
position: absolute;
width: 40%;
height: 40%;
object-fit: contain;
}
.page {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
z-index: 1;
}
`;

View File

@ -1,14 +0,0 @@
import { useContext, useState, useEffect } from 'react';
export function useUser() {
const [state, setState] = useState({});
const actions = {
updateState: (value) => {
setState((s) => ({ ...s, ...value }));
},
};
return { state, actions };
}

View File

@ -0,0 +1,57 @@
import React, { useContext, useEffect } from 'react';
import { useNavigate } from "react-router-dom";
import { AppContext } from 'context/AppContext';
import { ViewportContext } from 'context/ViewportContext';
import { AccessWrapper } from './Access.styled';
import { Login } from './login/Login';
import { CreateAccount } from './createAccount/CreateAccount';
import login from 'images/login.png'
export function Access({ mode }) {
const navigate = useNavigate();
const app = useContext(AppContext);
const viewport = useContext(ViewportContext);
useEffect(() => {
if (app.state) {
if (app.state.access) {
navigate('/session');
}
}
}, [app, navigate]);
const Prompt = () => {
if (mode === 'login') {
return <Login />
}
if (mode === 'create') {
return <CreateAccount />
}
return <></>
}
return (
<AccessWrapper>
{ (viewport.state.display === 'large' || viewport.state.display === 'xlarge') && (
<div class="split-layout">
<div class="left">
<img class="splash" src={login} alt="Databag Splash" />
</div>
<div class="right">
<Prompt />
</div>
</div>
)}
{ (viewport.state.display === 'medium' || viewport.state.display === 'small') && (
<div class="full-layout">
<div class="center">
<Prompt />
</div>
</div>
)}
</AccessWrapper>
);
}

View File

@ -0,0 +1,49 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const AccessWrapper = styled.div`
height: 100%;
.full-layout {
width: 100%;
height: 100%;
padding: 8px;
.center {
width: 100%;
height: 100%;
border-radius: 4px;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
.split-layout {
display: flex;
flex-direction: row;
height: 100%;
.left {
width: 50%;
height: 100%;
padding: 32px;
.splash {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.right {
width: 50%;
height: 100%;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
`;

View File

@ -0,0 +1,79 @@
import { Button, Modal, Form, Input } from 'antd';
import { SettingOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
import { CreateAccountWrapper } from './CreateAccount.styled';
import { useCreateAccount } from './useCreateAccount.hook';
export function CreateAccount() {
const { state, actions } = useCreateAccount();
const create = async () => {
try {
await actions.onCreateAccount();
}
catch(err) {
Modal.error({
title: 'Create Account Error',
content: 'Please check with you administrator.',
});
}
}
const keyDown = (e) => {
if (e.key === 'Enter') {
create()
}
}
return (
<CreateAccountWrapper>
<div class="app-title">
<span>Databag</span>
<div class="settings" onClick={() => actions.onSettings()}>
<SettingOutlined />
</div>
</div>
<div class="form-title">Create Account</div>
<div class="form-form">
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username" validateStatus={state.validateStatus} help={state.help}>
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setUsername(e.target.value)}
autocomplete="username" autocapitalize="none" onKeyDown={(e) => keyDown(e)} prefix={<UserOutlined />} size="large" />
</Form.Item>
<div class="form-space"></div>
<Form.Item name="password">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setPassword(e.target.value)}
autocomplete="new-password" onKeyDown={(e) => keyDown(e)} prefix={<LockOutlined />} size="large" />
</Form.Item>
<Form.Item name="confirm">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setConfirm(e.target.value)}
autocomplete="new-password" onKeyDown={(e) => keyDown(e)} prefix={<LockOutlined />} size="large" />
</Form.Item>
<div class="form-button">
<div class="form-create">
<Button type="primary" block onClick={create} disabled={ actions.isDisabled()}
loading={state.busy} size="middle">
Create Account
</Button>
</div>
</div>
<div class="form-button">
<div class="form-login">
<Button type="link" block onClick={(e) => actions.onLogin()}>
Account Login
</Button>
</div>
</div>
</Form>
</div>
</CreateAccountWrapper>
);
};

View File

@ -0,0 +1,67 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const CreateAccountWrapper = styled.div`
max-width: 400px;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
.app-title {
font-size: 24px;
display: flex;
align-items: flex-start;
justify-content: center;
flex: 1;
color: ${Colors.grey};
.settings {
color: ${Colors.grey};
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
cursor: pointer;
margin: 16px;
}
}
.form-title {
font-size: 32px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.form-form {
flex: 2;
flex-grow: 1;
.form-space {
height: 8px;
}
.form-button {
display: flex;
align-items: center;
justify-content: center;
.form-create {
width: 50%;
}
.form-login {
padding-bottom: 8px;
}
}
}
.form-submit {
background-color: #444444;
}
`;

View File

@ -0,0 +1,111 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate, useLocation } from "react-router-dom";
export function useCreateAccount() {
const [checked, setChecked] = useState(true);
const [state, setState] = useState({
username: '',
password: '',
confirm: '',
busy: false,
validatetatus: 'success',
help: '',
});
const navigate = useNavigate();
const { search } = useLocation();
const app = useContext(AppContext);
const debounce = useRef(null);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const usernameSet = (name) => {
setChecked(false)
clearTimeout(debounce.current)
debounce.current = setTimeout(async () => {
if (app.actions?.username && name !== '') {
try {
let valid = await app.actions.username(name, state.token)
if (!valid) {
updateState({ validateStatus: 'error', help: 'Username is not available' })
}
else {
updateState({ validateStatus: 'success', help: '' })
}
setChecked(true)
}
catch(err) {
console.log(err);
}
}
else {
updateState({ validateStatus: 'success', help: '' });
setChecked(true);
}
}, 500)
}
const actions = {
setUsername: (username) => {
updateState({ username });
usernameSet(username);
},
setPassword: (password) => {
updateState({ password });
},
setConfirm: (confirm) => {
updateState({ confirm });
},
isDisabled: () => {
if (state.username === '' || state.password === '' || state.password !== state.confirm || !checked ||
state.validateStatus === 'error') {
return true
}
return false
},
onSettings: () => {
navigate('/admin');
},
onCreateAccount: async () => {
if (!state.busy && state.username !== '' && state.password !== '' && state.password === state.confirm) {
updateState({ busy: true })
try {
await app.actions.create(state.username, state.password)
}
catch (err) {
console.log(err);
updateState({ busy: false })
throw new Error('create failed: check with your admin');
}
updateState({ busy: false })
}
},
onLogin: () => {
navigate('/login');
},
};
useEffect(() => {
if (app) {
if (app.state) {
if (app.state.access) {
navigate('/session')
}
}
else {
let params = new URLSearchParams(search);
let token = params.get("add");
if (token) {
updateState({ token });
}
}
}
}, [app, navigate, search])
return { state, actions };
}

View File

@ -0,0 +1,70 @@
import { Button, Modal, Form, Input } from 'antd';
import { SettingOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginWrapper } from './Login.styled';
import { useLogin } from './useLogin.hook';
export function Login() {
const { state, actions } = useLogin();
const login = async () => {
try {
await actions.onLogin();
}
catch(err) {
Modal.error({
title: 'Login Error',
content: 'Please confirm your username and password.',
});
}
}
const keyDown = (e) => {
if (e.key === 'Enter') {
login()
}
}
return (
<LoginWrapper>
<div class="app-title">
<span>Databag</span>
<div class="settings" onClick={() => actions.onSettings()}>
<SettingOutlined />
</div>
</div>
<div class="form-title">Login</div>
<div class="form-form">
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username">
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setUsername(e.target.value)}
autocomplete="username" autocapitalize="none" onKeyDown={(e) => keyDown(e)} prefix={<UserOutlined />} size="large" />
</Form.Item>
<Form.Item name="password">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setPassword(e.target.value)}
autocomplete="current-password" onKeyDown={(e) => keyDown(e)} prefix={<LockOutlined />} size="large" />
</Form.Item>
<div class="form-button">
<div class="form-login">
<Button type="primary" block onClick={login} disabled={ actions.isDisabled()}
size="middle" loading={state.busy}>
Login
</Button>
</div>
</div>
<div class="form-button">
<Button type="link" block disabled={ !state.available } onClick={(e) => actions.onCreate()}>
Create Account
</Button>
</div>
</Form>
</div>
</LoginWrapper>
);
};

View File

@ -0,0 +1,58 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const LoginWrapper = styled.div`
max-width: 400px;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
.app-title {
font-size: 24px;
display: flex;
align-items: flex-start;
justify-content: center;
flex: 1;
color: ${Colors.grey};
.settings {
color: ${Colors.grey};
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
cursor: pointer;
margin: 16px;
}
}
.form-title {
font-size: 32px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.form-form {
flex: 2;
.form-button {
display: flex;
align-items: center;
justify-content: center;
.form-login {
width: 50%;
}
}
}
.form-submit {
background-color: #444444;
}
`;

View File

@ -1,26 +1,31 @@
import { useContext, useState, useEffect } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
export function useLogin() {
const [state, setState] = useState({
username: '',
password: '',
available: false,
spinning: false,
disabled: true,
busy: false,
});
const navigate = useNavigate();
const { search } = useLocation();
const app = useContext(AppContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
setUsername: (username) => {
actions.updateState({ username });
updateState({ username });
},
setPassword: (password) => {
actions.updateState({ password });
updateState({ password });
},
isDisabled: () => {
if (state.username === '' || state.password === '') {
@ -32,58 +37,63 @@ export function useLogin() {
navigate('/admin');
},
onLogin: async () => {
if (!state.spinning && state.username != '' && state.password != '') {
actions.updateState({ spinning: true })
if (!state.busy && state.username !== '' && state.password !== '') {
updateState({ busy: true })
try {
await app.actions.login(state.username, state.password)
}
catch (err) {
window.alert(err)
console.log(err);
updateState({ busy: false })
throw new Error('login failed: check your username and password');
}
actions.updateState({ spinning: false })
updateState({ busy: false })
}
},
onAccess: async (token) => {
actions.updateState({ spinning: true })
try {
await app.actions.access(token)
}
catch (err) {
window.alert(err);
}
actions.updateState({ spinning: false })
},
onCreate: () => {
navigate('/create')
},
updateState: (value) => {
setState((s) => ({ ...s, ...value }));
navigate('/create');
},
};
useEffect(() => {
if (app) {
if (app.state) {
if (app.state.access === 'user') {
navigate('/user')
if (app.state.access) {
navigate('/session')
}
else {
let params = new URLSearchParams(search);
let token = params.get("access");
if (token) {
actions.onAccess(token);
const access = async () => {
updateState({ busy: true })
try {
await app.actions.access(token)
}
catch (err) {
console.log(err);
}
updateState({ busy: false })
}
access();
}
}
}
if (app.actions && app.actions.available) {
const count = async () => {
const available = await app.actions.available()
actions.updateState({ available: available !== 0 })
try {
const available = await app.actions.available()
updateState({ available: available !== 0 })
}
catch(err) {
console.log(err);
}
}
count();
}
}
}, [app])
}, [app, navigate, search])
return { state, actions };
}

View File

@ -0,0 +1,49 @@
import React, { useContext, useEffect } from 'react';
import { useNavigate } from "react-router-dom";
import { AppContext } from 'context/AppContext';
import { ViewportContext } from 'context/ViewportContext';
import { useAdmin } from './useAdmin.hook';
import { AdminWrapper } from './Admin.styled';
import { Prompt } from './prompt/Prompt';
import { Dashboard } from './dashboard/Dashboard';
import login from 'images/login.png'
export function Admin({ mode }) {
const { state, actions } = useAdmin();
return (
<AdminWrapper>
{ (state.display === 'large' || state.display === 'xlarge') && (
<div class="split-layout">
<div class="left">
<img class="splash" src={login} alt="Databag Splash" />
</div>
<div class="right">
{ state.token == null && (
<Prompt login={actions.login} />
)}
{ state.token != null && (
<Dashboard token={state.token} config={state.config} logout={actions.logout} />
)}
</div>
</div>
)}
{ (state.display === 'medium' || state.display === 'small') && (
<div class="full-layout">
<div class="center">
{ state.token == null && (
<Prompt login={actions.login} />
)}
{ state.token != null && (
<Dashboard token={state.token} config={state.config} logout={actions.logout} />
)}
</div>
</div>
)}
</AdminWrapper>
);
}

View File

@ -0,0 +1,49 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const AdminWrapper = styled.div`
height: 100%;
.full-layout {
width: 100%;
height: 100%;
padding: 8px;
.center {
width: 100%;
height: 100%;
border-radius: 4px;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
.split-layout {
display: flex;
flex-direction: row;
height: 100%;
.left {
width: 50%;
height: 100%;
padding: 32px;
.splash {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.right {
width: 50%;
height: 100%;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
`;

View File

@ -2,7 +2,7 @@ import { DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayo
import { Tooltip, Button, Modal, Input, InputNumber, Space, List } from 'antd';
import { SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { useDashboard } from './useDashboard.hook';
import { AccountItem } from './AccountItem/AccountItem';
import { AccountItem } from './accountItem/AccountItem';
export function Dashboard({ token, config, logout }) {

View File

@ -8,18 +8,17 @@ export const DashboardWrapper = styled.div`
justify-content: center;
width: 100%;
height: 100%;
padding-left: 8px;
padding-right: 8px;
.container {
background-color: #ffffff;
display: flex;
flex-direction: column;
padding-top: 16px;
padding-left: 16px;
padding-right: 16px;
border-radius: 4px;
min-width: 800px;
max-width: 900px;
width: 50%;
max-width: 100%;
max-height: 80%;
.header {

View File

@ -1,4 +1,4 @@
import { Avatar } from 'avatar/Avatar';
import { Logo } from 'logo/Logo';
import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled';
import { useAccountItem } from './useAccountItem.hook';
import { CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
@ -36,7 +36,7 @@ export function AccountItem({ token, item, remove }) {
return (
<AccountItemWrapper>
<div class="avatar">
<Avatar imageUrl={state.imageUrl} />
<Logo url={state.imageUrl} width={32} height={32} radius={4} />
</div>
<div class={state.activeClass}>
<div class="handle">{ state.handle }</div>

View File

@ -39,16 +39,23 @@ export const AccountItemWrapper = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
}
.handle {
font-size: 0.8em;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.guid {
font-size: 0.8em;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.control {

View File

@ -0,0 +1,59 @@
import { Button, Modal, Form, Input } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { PromptWrapper } from './Prompt.styled';
import { usePrompt } from './usePrompt.hook';
export function Prompt({ login }) {
const { state, actions } = usePrompt();
const setLogin = async () => {
try {
let config = await actions.onLogin();
login(state.password, config);
}
catch(err) {
Modal.error({
title: 'Prompt Error',
content: 'Please confirm your admin password.',
});
}
}
const keyDown = (e) => {
if (e.key === 'Enter') {
login()
}
}
return (
<PromptWrapper>
<div class="app-title">
<span>Databag</span>
<div class="user" onClick={() => actions.onUser()}>
<UserOutlined />
</div>
</div>
<div class="form-title">Admin Login</div>
<div class="form-form">
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="password">
<Input.Password placeholder="Admin Password" spellCheck="false" onChange={(e) => actions.setPassword(e.target.value)}
autocomplete="current-password" onKeyDown={(e) => keyDown(e)} prefix={<LockOutlined />} size="large" />
</Form.Item>
<div class="form-button">
<div class="form-login">
<Button type="primary" block onClick={setLogin} size="middle" loading={state.busy}>
Login
</Button>
</div>
</div>
</Form>
</div>
</PromptWrapper>
);
};

View File

@ -0,0 +1,58 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const PromptWrapper = styled.div`
max-width: 400px;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
.app-title {
font-size: 24px;
display: flex;
align-items: flex-start;
justify-content: center;
flex: 1;
color: ${Colors.grey};
.user {
color: ${Colors.grey};
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
cursor: pointer;
margin: 16px;
}
}
.form-title {
font-size: 32px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.form-form {
flex: 2;
.form-button {
display: flex;
align-items: center;
justify-content: center;
.form-login {
width: 50%;
}
}
}
.form-submit {
background-color: #444444;
}
`;

View File

@ -0,0 +1,75 @@
import { useContext, useState, useEffect } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate, useLocation } from "react-router-dom";
import { getNodeStatus } from 'api/getNodeStatus';
import { setNodeStatus } from 'api/setNodeStatus';
import { getNodeConfig } from 'api/getNodeConfig';
export function usePrompt() {
const [state, setState] = useState({
password: null,
placeholder: '',
unclaimed: null,
busy: false,
});
const navigate = useNavigate();
const app = useContext(AppContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const checkStatus = async () => {
try {
let status = await getNodeStatus();
updateState({ uncliamed: status });
}
catch(err) {
console.log("failed to check node status");
}
};
useEffect(() => {
checkStatus();
}, []);
const actions = {
setPassword: (password) => {
updateState({ password });
},
onUser: () => {
navigate('/login');
},
onLogin: async () => {
if (!state.busy) {
try {
updateState({ busy: true });
if (state.unclaimed === true) {
await setNodeStatus(state.password);
return await getNodeConfig(state.password);
}
else {
return await getNodeConfig(state.password);
}
updateState({ busy: false });
}
catch (err) {
console.log(err);
updateState({ busy: false });
throw new Error("access denied");
}
}
else {
throw new Error("operation in progress");
}
},
};
useEffect(() => {
}, [app, navigate])
return { state, actions };
}

View File

@ -0,0 +1,31 @@
import { useContext, useState, useEffect } from 'react';
import { ViewportContext } from 'context/ViewportContext';
export function useAdmin() {
const [state, setState] = useState({
display: null,
});
const viewport = useContext(ViewportContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({ display: viewport.state.display });
}, [viewport]);
const actions = {
login: (token, config) => {
updateState({ token, config });
},
logout: () => {
updateState({ token: null, config: null });
},
};
return { state, actions };
}

View File

@ -2,14 +2,14 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function addChannelTopic(token, channelId, message, assets ): string {
if (message == null && (assets == null || assets.length == 0)) {
if (message == null && (assets == null || assets.length === 0)) {
let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`,
{ method: 'POST', body: JSON.stringify({}) });
checkResponse(topic);
let slot = await topic.json();
return slot.id;
}
else if (assets == null || assets.length == 0) {
else if (assets == null || assets.length === 0) {
let subject = { data: JSON.stringify(message, (key, value) => {
if (value !== null) return value
}), datatype: 'superbasictopic' };

View File

@ -6,14 +6,14 @@ export async function addContactChannelTopic(server, token, channelId, message,
host = `https://${server}`
}
if (message == null && (assets == null || assets.length == 0)) {
if (message == null && (assets == null || assets.length === 0)) {
let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}`,
{ method: 'POST', body: JSON.stringify({}) });
checkResponse(topic);
let slot = await topic.json();
return slot.id;
}
else if (assets == null || assets.length == 0) {
else if (assets == null || assets.length === 0) {
let subject = { data: JSON.stringify(message, (key, value) => {
if (value !== null) return value
}), datatype: 'superbasictopic' };

View File

@ -1,20 +0,0 @@
import React, { useState } from 'react'
import { UserOutlined } from '@ant-design/icons';
import { AvatarWrapper } from './Avatar.styled';
export function Avatar({ imageUrl }) {
if (imageUrl == null) {
return (
<AvatarWrapper>
<UserOutlined />
</AvatarWrapper>
)
} else {
return (
<AvatarWrapper>
<img class='avatar' src={ imageUrl } alt='' />
</AvatarWrapper>
);
}
}

View File

@ -1,17 +0,0 @@
import styled from 'styled-components';
export const AvatarWrapper = styled.div`
border-radius: 4px;
overflow: hidden;
height: 100%;
display: flex;
font-size: 24px;
align-items: center;
justify-content: center;
.avatar {
object-fit: contain;
height: 100%;
}
`;

View File

@ -4,8 +4,9 @@ import { CarouselWrapper } from './Carousel.styled';
import { RightOutlined, LeftOutlined, CloseOutlined, PictureOutlined, FireOutlined } from '@ant-design/icons';
import ReactResizeDetector from 'react-resize-detector';
export function Carousel({ ready, error, items, itemRenderer, itemRemove }) {
export function Carousel({ pad, ready, error, items, itemRenderer, itemRemove }) {
const [slots, setSlots] = useState([]);
const [padClass, setPadClass] = useState('');
const [carouselRef, setCarouselRef] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [scrollLeft, setScrollLeft] = useState('hidden');
@ -120,43 +121,28 @@ export function Carousel({ ready, error, items, itemRenderer, itemRemove }) {
}
}
if (!ready || error) {
return (
<CarouselWrapper>
<div class="carousel">
{error && (
<div class="status">
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
</div>
)}
{!ready && !error && (
<div class="status">
<PictureOutlined style={{ fontSize: 32 }} />
</div>
)}
return (
<CarouselWrapper>
{ error && (
<div class="status">
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
</div>
<div class="arrows">
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: 'hidden' }} /></div>
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: 'hidden' }} /></div>
)}
{ !ready && !error && (
<div class="status">
<PictureOutlined style={{ fontSize: 32 }} />
</div>
</CarouselWrapper>
)
}
if (slots.length != 0) {
return (
<CarouselWrapper>
<div class="carousel" ref={onRefSet}>
{slots}
</div>
<div class="arrows">
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: scrollRight }} /></div>
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: scrollLeft }} /></div>
</div>
</CarouselWrapper>
);
}
return <></>
)}
{ ready && !error && (
<>
<div class="carousel" style={{ paddingLeft: pad + 32 }} ref={onRefSet}>
{slots}
</div>
<div class="left-arrow" onClick={onRight} style={{ marginLeft: pad, visibility: scrollRight }}><LeftOutlined /></div>
<div class="right-arrow" onClick={onLeft} style={{ visibility: scrollLeft }}><RightOutlined /></div>
</>
)}
</CarouselWrapper>
);
}

Some files were not shown because too many files have changed in this diff Show More