mirror of
https://github.com/balzack/databag.git
synced 2025-02-14 12:39:17 +00:00
Merge branch 'mobile' into main
This commit is contained in:
commit
b3e1786cde
@ -3510,6 +3510,9 @@ components:
|
|||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
enum: [ pending, confirmed, requested, connecting, connected ]
|
enum: [ pending, confirmed, requested, connecting, connected ]
|
||||||
|
statusUpdated:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
notes:
|
notes:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package databag
|
package databag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
"databag/internal/store"
|
"databag/internal/store"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@ -57,6 +58,7 @@ func AddCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
Node: identity.Node,
|
Node: identity.Node,
|
||||||
ProfileRevision: identity.Revision,
|
ProfileRevision: identity.Revision,
|
||||||
Status: APPCardConfirmed,
|
Status: APPCardConfirmed,
|
||||||
|
StatusUpdated: time.Now().Unix(),
|
||||||
ViewRevision: 0,
|
ViewRevision: 0,
|
||||||
InToken: hex.EncodeToString(data),
|
InToken: hex.EncodeToString(data),
|
||||||
AccountID: account.GUID,
|
AccountID: account.GUID,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"databag/internal/store"
|
"databag/internal/store"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/theckman/go-securerandom"
|
"github.com/theckman/go-securerandom"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -105,6 +106,7 @@ func SetCardStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slot.Card.Status = status
|
slot.Card.Status = status
|
||||||
|
slot.Card.StatusUpdated = time.Now().Unix()
|
||||||
slot.Card.NotifiedView = viewRevision
|
slot.Card.NotifiedView = viewRevision
|
||||||
slot.Card.NotifiedArticle = articleRevision
|
slot.Card.NotifiedArticle = articleRevision
|
||||||
slot.Card.NotifiedChannel = channelRevision
|
slot.Card.NotifiedChannel = channelRevision
|
||||||
|
@ -56,6 +56,9 @@ func SetCloseMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
if res := tx.Model(&card).Update("status", APPCardConfirmed).Error; res != nil {
|
if res := tx.Model(&card).Update("status", APPCardConfirmed).Error; res != nil {
|
||||||
return res
|
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 {
|
if res := tx.Model(&card).Update("detail_revision", account.CardRevision+1).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
|
@ -67,6 +67,7 @@ func SetOpenMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
card.Node = connect.Node
|
card.Node = connect.Node
|
||||||
card.ProfileRevision = connect.ProfileRevision
|
card.ProfileRevision = connect.ProfileRevision
|
||||||
card.Status = APPCardPending
|
card.Status = APPCardPending
|
||||||
|
card.StatusUpdated = time.Now().Unix()
|
||||||
card.NotifiedProfile = connect.ProfileRevision
|
card.NotifiedProfile = connect.ProfileRevision
|
||||||
card.NotifiedArticle = connect.ArticleRevision
|
card.NotifiedArticle = connect.ArticleRevision
|
||||||
card.NotifiedView = connect.ViewRevision
|
card.NotifiedView = connect.ViewRevision
|
||||||
@ -124,9 +125,11 @@ func SetOpenMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if card.Status == APPCardConfirmed {
|
if card.Status == APPCardConfirmed {
|
||||||
card.Status = APPCardRequested
|
card.Status = APPCardRequested
|
||||||
|
card.StatusUpdated = time.Now().Unix()
|
||||||
}
|
}
|
||||||
if card.Status == APPCardConnecting {
|
if card.Status == APPCardConnecting {
|
||||||
card.Status = APPCardConnected
|
card.Status = APPCardConnected
|
||||||
|
card.StatusUpdated = time.Now().Unix()
|
||||||
}
|
}
|
||||||
card.OutToken = connect.Token
|
card.OutToken = connect.Token
|
||||||
card.DetailRevision = account.CardRevision + 1
|
card.DetailRevision = account.CardRevision + 1
|
||||||
|
@ -69,13 +69,14 @@ func getCardRevisionModel(slot *store.CardSlot) *Card {
|
|||||||
|
|
||||||
func getCardDetailModel(slot *store.CardSlot) *CardDetail {
|
func getCardDetailModel(slot *store.CardSlot) *CardDetail {
|
||||||
|
|
||||||
var groups []string
|
var groups []string = []string{}
|
||||||
for _, group := range slot.Card.Groups {
|
for _, group := range slot.Card.Groups {
|
||||||
groups = append(groups, group.GroupSlot.GroupSlotID)
|
groups = append(groups, group.GroupSlot.GroupSlotID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CardDetail{
|
return &CardDetail{
|
||||||
Status: slot.Card.Status,
|
Status: slot.Card.Status,
|
||||||
|
StatusUpdated: slot.Card.StatusUpdated,
|
||||||
Token: slot.Card.OutToken,
|
Token: slot.Card.OutToken,
|
||||||
Notes: slot.Card.Notes,
|
Notes: slot.Card.Notes,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
|
@ -113,6 +113,8 @@ type CardData struct {
|
|||||||
type CardDetail struct {
|
type CardDetail struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|
||||||
|
StatusUpdated int64 `json:"statusUpdated"`
|
||||||
|
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
|
|
||||||
Notes string `json:"notes,omitempty"`
|
Notes string `json:"notes,omitempty"`
|
||||||
|
@ -151,6 +151,7 @@ type Card struct {
|
|||||||
ProfileRevision int64 `gorm:"not null"`
|
ProfileRevision int64 `gorm:"not null"`
|
||||||
DetailRevision int64 `gorm:"not null;default:1"`
|
DetailRevision int64 `gorm:"not null;default:1"`
|
||||||
Status string `gorm:"not null"`
|
Status string `gorm:"not null"`
|
||||||
|
StatusUpdated int64
|
||||||
InToken string `gorm:"not null;index:cardguid,unique"`
|
InToken string `gorm:"not null;index:cardguid,unique"`
|
||||||
OutToken string
|
OutToken string
|
||||||
Notes string
|
Notes string
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<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="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="Databag"
|
name="Databag"
|
||||||
@ -26,7 +26,7 @@
|
|||||||
-->
|
-->
|
||||||
<title>Databag</title>
|
<title>Databag</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="background-color:#8fbea7;">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<!--
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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 { AppContextProvider } from 'context/AppContext';
|
||||||
import { AccountContextProvider } from 'context/AccountContext';
|
import { AccountContextProvider } from 'context/AccountContext';
|
||||||
import { ProfileContextProvider } from 'context/ProfileContext';
|
import { ProfileContextProvider } from 'context/ProfileContext';
|
||||||
@ -6,19 +9,16 @@ import { ArticleContextProvider } from 'context/ArticleContext';
|
|||||||
import { GroupContextProvider } from 'context/GroupContext';
|
import { GroupContextProvider } from 'context/GroupContext';
|
||||||
import { CardContextProvider } from 'context/CardContext';
|
import { CardContextProvider } from 'context/CardContext';
|
||||||
import { ChannelContextProvider } from 'context/ChannelContext';
|
import { ChannelContextProvider } from 'context/ChannelContext';
|
||||||
import { ConversationContextProvider } from 'context/ConversationContext';
|
|
||||||
import { StoreContextProvider } from 'context/StoreContext';
|
import { StoreContextProvider } from 'context/StoreContext';
|
||||||
import { UploadContextProvider } from 'context/UploadContext';
|
import { UploadContextProvider } from 'context/UploadContext';
|
||||||
import { Home } from './Home/Home';
|
import { ViewportContextProvider } from 'context/ViewportContext';
|
||||||
import { Admin } from './Admin/Admin';
|
import { ConversationContextProvider } from 'context/ConversationContext';
|
||||||
import { Login } from './Login/Login';
|
|
||||||
import { Create } from './Create/Create';
|
import { AppWrapper } from 'App.styled';
|
||||||
import { User } from './User/User';
|
import { Root } from './root/Root';
|
||||||
import { Profile } from './User/Profile/Profile';
|
import { Access } from './access/Access';
|
||||||
import { Contact } from './User/Contact/Contact';
|
import { Session } from './session/Session';
|
||||||
import { Conversation } from './User/Conversation/Conversation';
|
import { Admin } from './admin/Admin';
|
||||||
import { HashRouter as Router, Routes, Route } from "react-router-dom";
|
|
||||||
import 'antd/dist/antd.min.css';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
@ -31,35 +31,26 @@ function App() {
|
|||||||
<ProfileContextProvider>
|
<ProfileContextProvider>
|
||||||
<AccountContextProvider>
|
<AccountContextProvider>
|
||||||
<StoreContextProvider>
|
<StoreContextProvider>
|
||||||
|
<ViewportContextProvider>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<div style={{ position: 'absolute', width: '100vw', height: '100vh', backgroundColor: '#8fbea7' }}>
|
<AppWrapper>
|
||||||
<img src={login} alt="" style={{ position: 'absolute', width: '33%', bottom: 0, right: 0 }}/>
|
|
||||||
</div>
|
|
||||||
<div style={{ position: 'absolute', width: '100vw', height: '100vh' }}>
|
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={ <Home /> } />
|
<Route path="/" element={ <Root /> } />
|
||||||
<Route path="/login" element={ <Login /> } />
|
|
||||||
<Route path="/admin" element={ <Admin /> } />
|
<Route path="/admin" element={ <Admin /> } />
|
||||||
<Route path="/create" element={ <Create /> } />
|
<Route path="/login" element={ <Access mode="login" /> } />
|
||||||
<Route path="/user" element={ <User /> }>
|
<Route path="/create" element={ <Access mode="create" /> } />
|
||||||
<Route path="profile" element={<Profile />} />
|
<Route path="/session" element={
|
||||||
<Route path="contact/:guid" element={<Contact />} />
|
|
||||||
<Route path="conversation/:cardId/:channelId" element={
|
|
||||||
<ConversationContextProvider>
|
<ConversationContextProvider>
|
||||||
<Conversation />
|
<Session />
|
||||||
</ConversationContextProvider>
|
</ConversationContextProvider>
|
||||||
} />
|
}>
|
||||||
<Route path="conversation/:channelId" element={
|
|
||||||
<ConversationContextProvider>
|
|
||||||
<Conversation />
|
|
||||||
</ConversationContextProvider>
|
|
||||||
} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</AppWrapper>
|
||||||
</AppContextProvider>
|
</AppContextProvider>
|
||||||
|
</ViewportContextProvider>
|
||||||
</StoreContextProvider>
|
</StoreContextProvider>
|
||||||
</AccountContextProvider>
|
</AccountContextProvider>
|
||||||
</ProfileContextProvider>
|
</ProfileContextProvider>
|
||||||
|
7
net/web/src/App.styled.js
Normal file
7
net/web/src/App.styled.js
Normal 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%));
|
||||||
|
`;
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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 <></>
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const MembersWrapper = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
max-height: 240px;
|
|
||||||
overflow: auto;
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
`
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
@ -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 <></>
|
|
||||||
}
|
|
||||||
|
|
@ -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%;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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;
|
|
||||||
`;
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
57
net/web/src/access/Access.jsx
Normal file
57
net/web/src/access/Access.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
49
net/web/src/access/Access.styled.js
Normal file
49
net/web/src/access/Access.styled.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
79
net/web/src/access/createAccount/CreateAccount.jsx
Normal file
79
net/web/src/access/createAccount/CreateAccount.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
67
net/web/src/access/createAccount/CreateAccount.styled.js
Normal file
67
net/web/src/access/createAccount/CreateAccount.styled.js
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
111
net/web/src/access/createAccount/useCreateAccount.hook.js
Normal file
111
net/web/src/access/createAccount/useCreateAccount.hook.js
Normal 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 };
|
||||||
|
}
|
||||||
|
|
70
net/web/src/access/login/Login.jsx
Normal file
70
net/web/src/access/login/Login.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
58
net/web/src/access/login/Login.styled.js
Normal file
58
net/web/src/access/login/Login.styled.js
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext, useState, useEffect } from 'react';
|
import { useContext, useState, useEffect } from 'react';
|
||||||
import { AppContext } from 'context/AppContext';
|
import { AppContext } from 'context/AppContext';
|
||||||
import { useNavigate, useLocation, useParams } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
|
|
||||||
@ -8,19 +8,24 @@ export function useLogin() {
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
available: false,
|
available: false,
|
||||||
spinning: false,
|
disabled: true,
|
||||||
|
busy: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const app = useContext(AppContext);
|
const app = useContext(AppContext);
|
||||||
|
|
||||||
|
const updateState = (value) => {
|
||||||
|
setState((s) => ({ ...s, ...value }));
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
setUsername: (username) => {
|
setUsername: (username) => {
|
||||||
actions.updateState({ username });
|
updateState({ username });
|
||||||
},
|
},
|
||||||
setPassword: (password) => {
|
setPassword: (password) => {
|
||||||
actions.updateState({ password });
|
updateState({ password });
|
||||||
},
|
},
|
||||||
isDisabled: () => {
|
isDisabled: () => {
|
||||||
if (state.username === '' || state.password === '') {
|
if (state.username === '' || state.password === '') {
|
||||||
@ -32,58 +37,63 @@ export function useLogin() {
|
|||||||
navigate('/admin');
|
navigate('/admin');
|
||||||
},
|
},
|
||||||
onLogin: async () => {
|
onLogin: async () => {
|
||||||
if (!state.spinning && state.username != '' && state.password != '') {
|
if (!state.busy && state.username !== '' && state.password !== '') {
|
||||||
actions.updateState({ spinning: true })
|
updateState({ busy: true })
|
||||||
try {
|
try {
|
||||||
await app.actions.login(state.username, state.password)
|
await app.actions.login(state.username, state.password)
|
||||||
}
|
}
|
||||||
catch (err) {
|
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: () => {
|
onCreate: () => {
|
||||||
navigate('/create')
|
navigate('/create');
|
||||||
},
|
|
||||||
updateState: (value) => {
|
|
||||||
setState((s) => ({ ...s, ...value }));
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (app) {
|
if (app) {
|
||||||
if (app.state) {
|
if (app.state) {
|
||||||
if (app.state.access === 'user') {
|
if (app.state.access) {
|
||||||
navigate('/user')
|
navigate('/session')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let params = new URLSearchParams(search);
|
let params = new URLSearchParams(search);
|
||||||
let token = params.get("access");
|
let token = params.get("access");
|
||||||
if (token) {
|
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) {
|
if (app.actions && app.actions.available) {
|
||||||
const count = async () => {
|
const count = async () => {
|
||||||
|
try {
|
||||||
const available = await app.actions.available()
|
const available = await app.actions.available()
|
||||||
actions.updateState({ available: available !== 0 })
|
updateState({ available: available !== 0 })
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
count();
|
count();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [app])
|
}, [app, navigate, search])
|
||||||
|
|
||||||
return { state, actions };
|
return { state, actions };
|
||||||
}
|
}
|
||||||
|
|
49
net/web/src/admin/Admin.jsx
Normal file
49
net/web/src/admin/Admin.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
49
net/web/src/admin/Admin.styled.js
Normal file
49
net/web/src/admin/Admin.styled.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -2,7 +2,7 @@ import { DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayo
|
|||||||
import { Tooltip, Button, Modal, Input, InputNumber, Space, List } from 'antd';
|
import { Tooltip, Button, Modal, Input, InputNumber, Space, List } from 'antd';
|
||||||
import { SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
import { useDashboard } from './useDashboard.hook';
|
import { useDashboard } from './useDashboard.hook';
|
||||||
import { AccountItem } from './AccountItem/AccountItem';
|
import { AccountItem } from './accountItem/AccountItem';
|
||||||
|
|
||||||
export function Dashboard({ token, config, logout }) {
|
export function Dashboard({ token, config, logout }) {
|
||||||
|
|
@ -8,18 +8,17 @@ export const DashboardWrapper = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
background-color: #ffffff;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
min-width: 800px;
|
max-width: 100%;
|
||||||
max-width: 900px;
|
|
||||||
width: 50%;
|
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
|
|
||||||
.header {
|
.header {
|
@ -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 { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled';
|
||||||
import { useAccountItem } from './useAccountItem.hook';
|
import { useAccountItem } from './useAccountItem.hook';
|
||||||
import { CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
import { CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||||
@ -36,7 +36,7 @@ export function AccountItem({ token, item, remove }) {
|
|||||||
return (
|
return (
|
||||||
<AccountItemWrapper>
|
<AccountItemWrapper>
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<Avatar imageUrl={state.imageUrl} />
|
<Logo url={state.imageUrl} width={32} height={32} radius={4} />
|
||||||
</div>
|
</div>
|
||||||
<div class={state.activeClass}>
|
<div class={state.activeClass}>
|
||||||
<div class="handle">{ state.handle }</div>
|
<div class="handle">{ state.handle }</div>
|
@ -39,16 +39,23 @@ export const AccountItemWrapper = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guid {
|
.guid {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
59
net/web/src/admin/prompt/Prompt.jsx
Normal file
59
net/web/src/admin/prompt/Prompt.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
58
net/web/src/admin/prompt/Prompt.styled.js
Normal file
58
net/web/src/admin/prompt/Prompt.styled.js
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
75
net/web/src/admin/prompt/usePrompt.hook.js
Normal file
75
net/web/src/admin/prompt/usePrompt.hook.js
Normal 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 };
|
||||||
|
}
|
||||||
|
|
31
net/web/src/admin/useAdmin.hook.js
Normal file
31
net/web/src/admin/useAdmin.hook.js
Normal 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 };
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,14 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
|||||||
|
|
||||||
export async function addChannelTopic(token, channelId, message, assets ): string {
|
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}`,
|
let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`,
|
||||||
{ method: 'POST', body: JSON.stringify({}) });
|
{ method: 'POST', body: JSON.stringify({}) });
|
||||||
checkResponse(topic);
|
checkResponse(topic);
|
||||||
let slot = await topic.json();
|
let slot = await topic.json();
|
||||||
return slot.id;
|
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) => {
|
let subject = { data: JSON.stringify(message, (key, value) => {
|
||||||
if (value !== null) return value
|
if (value !== null) return value
|
||||||
}), datatype: 'superbasictopic' };
|
}), datatype: 'superbasictopic' };
|
||||||
|
@ -6,14 +6,14 @@ export async function addContactChannelTopic(server, token, channelId, message,
|
|||||||
host = `https://${server}`
|
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}`,
|
let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}`,
|
||||||
{ method: 'POST', body: JSON.stringify({}) });
|
{ method: 'POST', body: JSON.stringify({}) });
|
||||||
checkResponse(topic);
|
checkResponse(topic);
|
||||||
let slot = await topic.json();
|
let slot = await topic.json();
|
||||||
return slot.id;
|
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) => {
|
let subject = { data: JSON.stringify(message, (key, value) => {
|
||||||
if (value !== null) return value
|
if (value !== null) return value
|
||||||
}), datatype: 'superbasictopic' };
|
}), datatype: 'superbasictopic' };
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -4,8 +4,9 @@ import { CarouselWrapper } from './Carousel.styled';
|
|||||||
import { RightOutlined, LeftOutlined, CloseOutlined, PictureOutlined, FireOutlined } from '@ant-design/icons';
|
import { RightOutlined, LeftOutlined, CloseOutlined, PictureOutlined, FireOutlined } from '@ant-design/icons';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
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 [slots, setSlots] = useState([]);
|
||||||
|
const [padClass, setPadClass] = useState('');
|
||||||
const [carouselRef, setCarouselRef] = useState(false);
|
const [carouselRef, setCarouselRef] = useState(false);
|
||||||
const [itemIndex, setItemIndex] = useState(0);
|
const [itemIndex, setItemIndex] = useState(0);
|
||||||
const [scrollLeft, setScrollLeft] = useState('hidden');
|
const [scrollLeft, setScrollLeft] = useState('hidden');
|
||||||
@ -120,10 +121,8 @@ export function Carousel({ ready, error, items, itemRenderer, itemRemove }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ready || error) {
|
|
||||||
return (
|
return (
|
||||||
<CarouselWrapper>
|
<CarouselWrapper>
|
||||||
<div class="carousel">
|
|
||||||
{ error && (
|
{ error && (
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
|
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
|
||||||
@ -134,29 +133,16 @@ export function Carousel({ ready, error, items, itemRenderer, itemRemove }) {
|
|||||||
<PictureOutlined style={{ fontSize: 32 }} />
|
<PictureOutlined style={{ fontSize: 32 }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{ ready && !error && (
|
||||||
<div class="arrows">
|
<>
|
||||||
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: 'hidden' }} /></div>
|
<div class="carousel" style={{ paddingLeft: pad + 32 }} ref={onRefSet}>
|
||||||
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: 'hidden' }} /></div>
|
|
||||||
</div>
|
|
||||||
</CarouselWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slots.length != 0) {
|
|
||||||
return (
|
|
||||||
<CarouselWrapper>
|
|
||||||
<div class="carousel" ref={onRefSet}>
|
|
||||||
{slots}
|
{slots}
|
||||||
</div>
|
</div>
|
||||||
<div class="arrows">
|
<div class="left-arrow" onClick={onRight} style={{ marginLeft: pad, visibility: scrollRight }}><LeftOutlined /></div>
|
||||||
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: scrollRight }} /></div>
|
<div class="right-arrow" onClick={onLeft} style={{ visibility: scrollLeft }}><RightOutlined /></div>
|
||||||
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: scrollLeft }} /></div>
|
</>
|
||||||
</div>
|
)}
|
||||||
</CarouselWrapper>
|
</CarouselWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <></>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user