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