mirror of
https://github.com/balzack/databag.git
synced 2025-02-14 12:39:17 +00:00
adding virtual list from main as starting point
This commit is contained in:
parent
994102f22d
commit
0032e03897
@ -3,12 +3,16 @@ import { SettingOutlined, RightOutlined, CloseOutlined } from '@ant-design/icons
|
||||
import { useConversation } from './useConversation.hook';
|
||||
import { Logo } from 'logo/Logo';
|
||||
import { AddTopic } from './addTopic/AddTopic';
|
||||
import { VirtualList } from './virtualList/VirtualList';
|
||||
import { TopicItem } from './topicItem/TopicItem';
|
||||
|
||||
export function Conversation({ closeConversation, openDetails, cardId, channelId }) {
|
||||
|
||||
const { state, actions } = useConversation(cardId, channelId);
|
||||
|
||||
console.log(state);
|
||||
const topicRenderer = (topic) => {
|
||||
return (<TopicItem host={cardId == null} topic={topic} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConversationWrapper>
|
||||
@ -31,6 +35,8 @@ console.log(state);
|
||||
)}
|
||||
</div>
|
||||
<div class="thread">
|
||||
<VirtualList id={channelId + cardId}
|
||||
items={state.topics} itemRenderer={topicRenderer} onMore={actions.more} />
|
||||
</div>
|
||||
<div class="divider">
|
||||
<div class="line" />
|
||||
|
@ -52,6 +52,7 @@ export const ConversationWrapper = styled.div`
|
||||
|
||||
.thread {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AddTopicWrapper } from './AddTopic.styled';
|
||||
import { useAddTopic } from './useAddTopic.hook';
|
||||
import { Input, Menu, Dropdown } from 'antd';
|
||||
import { Modal, Input, Menu, Dropdown } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, PaperClipOutlined, SendOutlined } from '@ant-design/icons';
|
||||
import { SketchPicker } from "react-color";
|
||||
@ -19,23 +19,37 @@ export function AddTopic({ cardId, channelId }) {
|
||||
const keyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
msg.current.blur();
|
||||
addTopic();
|
||||
}
|
||||
}
|
||||
|
||||
const addTopic = async () => {
|
||||
try {
|
||||
await actions.addTopic();
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Modal.error({
|
||||
title: 'Failed to Post Message',
|
||||
content: 'Please try again.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 renderItem = (item, index) => {
|
||||
if (item.image) {
|
||||
@ -48,11 +62,11 @@ export function AddTopic({ cardId, channelId }) {
|
||||
return <VideoFile onPosition={(pos) => actions.setPosition(index, pos)} url={item.url} />
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
actions.removeAsset(index);
|
||||
}
|
||||
};
|
||||
|
||||
const picker = (
|
||||
<Menu style={{ backgroundColor: 'unset', boxShadow: 'unset' }}>
|
||||
@ -84,7 +98,8 @@ export function AddTopic({ cardId, channelId }) {
|
||||
</div>
|
||||
<div class="message">
|
||||
<Input.TextArea ref={msg} placeholder="New Message" spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
autocapitalize="none" enterkeyhint="send" onKeyDown={(e) => keyDown(e)} />
|
||||
enterkeyhint="send" onKeyDown={(e) => keyDown(e)} onChange={(e) => actions.setMessageText(e.target.value)}
|
||||
value={state.messageText} autocapitalize="none" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="button space" onClick={() => attachImage.current.click()}>
|
||||
@ -98,17 +113,17 @@ export function AddTopic({ cardId, channelId }) {
|
||||
</div>
|
||||
<div class="bar space" />
|
||||
<div class="button space">
|
||||
<Dropdown overlay={sizer} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="topLeft">
|
||||
<FontSizeOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="button space">
|
||||
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="topLeft">
|
||||
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
|
||||
<FontColorsOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="button space">
|
||||
<Dropdown overlay={sizer} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
|
||||
<FontSizeOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="end">
|
||||
<div class="button"><SendOutlined /></div>
|
||||
<div class="button" onClick={addTopic}><SendOutlined /></div>
|
||||
</div>
|
||||
</div>
|
||||
</AddTopicWrapper>
|
||||
|
@ -74,8 +74,8 @@ export function useAddTopic(cardId, channelId) {
|
||||
},
|
||||
addTopic: async () => {
|
||||
if (!state.busy) {
|
||||
updateState({ busy: true });
|
||||
try {
|
||||
updateState({ busy: true });
|
||||
let message = {
|
||||
text: state.messageText,
|
||||
textColor: state.textColorSet ? state.textColor : null,
|
||||
@ -87,13 +87,17 @@ export function useAddTopic(cardId, channelId) {
|
||||
else {
|
||||
await channel.actions.addChannelTopic(channelId, message, state.assets);
|
||||
}
|
||||
updateState({ messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] });
|
||||
updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false,
|
||||
textSize: 12, textSizeSet: false, assets: [] });
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
window.alert("failed to add message");
|
||||
updateState({ busy: false });
|
||||
throw new Error("failed to post topic");
|
||||
}
|
||||
updateState({ busy: false });
|
||||
}
|
||||
else {
|
||||
throw new Error("operation in progress");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
164
net/web/src/session/conversation/topicItem/TopicItem.jsx
Normal file
164
net/web/src/session/conversation/topicItem/TopicItem.jsx
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TopicItemWrapper } from './TopicItem.styled';
|
||||
import { useTopicItem } from './useTopicItem.hook';
|
||||
import { VideoAsset } from './videoAsset/VideoAsset';
|
||||
import { AudioAsset } from './audioAsset/AudioAsset';
|
||||
import { ImageAsset } from './imageAsset/ImageAsset';
|
||||
import { Logo } from 'logo/Logo';
|
||||
import { Space, Skeleton, Button, Modal, Input } from 'antd';
|
||||
import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { Carousel } from 'carousel/Carousel';
|
||||
|
||||
export function TopicItem({ host, topic }) {
|
||||
|
||||
const { state, actions } = useTopicItem(topic);
|
||||
const [ edit, setEdit ] = useState(null);
|
||||
|
||||
let name = state.name ? state.name : state.handle;
|
||||
let nameClass = state.name ? 'set' : 'unset';
|
||||
let d = new Date();
|
||||
let offset = d.getTime() / 1000 - state.created;
|
||||
|
||||
if (name == null) {
|
||||
name = "unknown contact"
|
||||
nameClass = "unknown"
|
||||
}
|
||||
|
||||
const renderAsset = (asset) => {
|
||||
if (asset.image) {
|
||||
return <ImageAsset thumbUrl={actions.getAssetUrl(asset.image.thumb)}
|
||||
fullUrl={actions.getAssetUrl(asset.image.full)} />
|
||||
}
|
||||
if (asset.video) {
|
||||
return <VideoAsset thumbUrl={actions.getAssetUrl(asset.video.thumb)}
|
||||
lqUrl={actions.getAssetUrl(asset.video.lq)} hdUrl={actions.getAssetUrl(asset.video.hd)} />
|
||||
}
|
||||
if (asset.audio) {
|
||||
return <AudioAsset label={asset.audio.label} audioUrl={actions.getAssetUrl(asset.audio.full)} />
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
const removeTopic = () => {
|
||||
Modal.confirm({
|
||||
title: 'Do you want to delete this message?',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
okText: 'Yes, Delete',
|
||||
cancelText: 'No, Cancel',
|
||||
onOk() { actions.removeTopic() },
|
||||
});
|
||||
}
|
||||
|
||||
const Options = () => {
|
||||
if (state.editing) {
|
||||
return <></>;
|
||||
}
|
||||
if (state.owner) {
|
||||
return (
|
||||
<div class="buttons">
|
||||
<div class="button" onClick={() => actions.setEditing(true)}>
|
||||
<EditOutlined />
|
||||
</div>
|
||||
<div class="button" onClick={() => removeTopic()}>
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (host) {
|
||||
return (
|
||||
<div class="buttons">
|
||||
<div class="button" onClick={() => removeTopic()}>
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const Message = () => {
|
||||
if (state.editing) {
|
||||
return (
|
||||
<div class="editing">
|
||||
<Input.TextArea style={{ resize: 'none' }} defaultValue={state.message?.text} placeholder="message"
|
||||
style={{ color: state.textColor, fontSize: state.textSize }}
|
||||
onChange={(e) => actions.setEdit(e.target.value)} rows={3} bordered={false}/>
|
||||
<div class="controls">
|
||||
<Space>
|
||||
<Button onClick={() => actions.setEditing(false)}>Cancel</Button>
|
||||
<Button type="primary" onClick={() => actions.setMessage()} loading={state.body}>Save</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div style={{ color: state.textColor, fontSize: state.textSize }}>{ state.message?.text }</div>
|
||||
}
|
||||
|
||||
if (!state.confirmed) {
|
||||
return (
|
||||
<TopicItemWrapper>
|
||||
<div class="avatar">
|
||||
<Logo width={32} height={32} radius={4} imageUrl={state.imageUrl} />
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div class="info">
|
||||
<div class={nameClass}>{ name }</div>
|
||||
<div>{ getTime(offset) }</div>
|
||||
</div>
|
||||
<Skeleton size={'small'} active={true} />
|
||||
<div class="options">
|
||||
<Options />
|
||||
</div>
|
||||
</div>
|
||||
</TopicItemWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TopicItemWrapper>
|
||||
<div class="avatar">
|
||||
<Logo width={32} height={32} radius={4} imageUrl={state.imageUrl} />
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div class="info">
|
||||
<div class={nameClass}>{ name }</div>
|
||||
<div>{ getTime(offset) }</div>
|
||||
</div>
|
||||
{ state.assets.length > 0 && (
|
||||
<Carousel ready={state.ready} error={state.error} items={state.assets} itemRenderer={renderAsset} />
|
||||
)}
|
||||
<div class="message">
|
||||
<Message />
|
||||
</div>
|
||||
<div class="options">
|
||||
<Options />
|
||||
</div>
|
||||
</div>
|
||||
</TopicItemWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function getTime(offset) {
|
||||
if (offset < 1) {
|
||||
return ''
|
||||
}
|
||||
if (offset < 60) {
|
||||
return Math.floor(offset) + "s";
|
||||
}
|
||||
offset /= 60;
|
||||
if (offset < 60) {
|
||||
return Math.floor(offset) + "m";
|
||||
}
|
||||
offset /= 60;
|
||||
if (offset < 24) {
|
||||
return Math.floor(offset) + "h";
|
||||
}
|
||||
offset /= 24;
|
||||
if (offset < 366) {
|
||||
return Math.floor(offset) + "d";
|
||||
}
|
||||
offset /= 365.25;
|
||||
return Math.floor(offset) + "y";
|
||||
}
|
100
net/web/src/session/conversation/topicItem/TopicItem.styled.js
Normal file
100
net/web/src/session/conversation/topicItem/TopicItem.styled.js
Normal file
@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -0,0 +1,79 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ReactPlayer from 'react-player'
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { PlayCircleOutlined, MinusCircleOutlined, SoundOutlined } from '@ant-design/icons';
|
||||
import { AudioAssetWrapper } from './AudioAsset.styled';
|
||||
|
||||
export function AudioAsset({ label, audioUrl }) {
|
||||
|
||||
const [active, setActive] = useState(false);
|
||||
const [dimension, setDimension] = useState({});
|
||||
const [playing, setPlaying] = useState(true);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(false);
|
||||
setPlaying(false);
|
||||
setUrl(null);
|
||||
}, [label, audioUrl]);
|
||||
|
||||
const onReady = () => {
|
||||
if (!ready) {
|
||||
setReady(true);
|
||||
setPlaying(false);
|
||||
}
|
||||
}
|
||||
|
||||
const onActivate = () => {
|
||||
setUrl(audioUrl);
|
||||
setActive(true);
|
||||
}
|
||||
|
||||
const Control = () => {
|
||||
if (!ready) {
|
||||
return <></>
|
||||
}
|
||||
if (playing) {
|
||||
return (
|
||||
<div onClick={() => setPlaying(false)}>
|
||||
<MinusCircleOutlined style={{ fontSize: 48, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div onClick={() => setPlaying(true)}>
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AudioAssetWrapper>
|
||||
<ReactResizeDetector handleWidth={false} handleHeight={true}>
|
||||
{({ height }) => {
|
||||
if (height != dimension.height) {
|
||||
setDimension({ height });
|
||||
}
|
||||
return <div style={{ height: '100%', borderRadius: 4, width: dimension.height, backgroundColor: '#444444' }} />
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
<div class="player" style={{ width: dimension.height, height: dimension.height }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{ !active && (
|
||||
<div onClick={() => onActivate()}>
|
||||
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
{ active && (
|
||||
<Control />
|
||||
)}
|
||||
<ReactPlayer style={{ position: 'absolute', top: 0, visibility: 'hidden' }} playing={playing} height="100%" width="100%" controls="true" url={url} onReady={onReady} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">{ label }</div>
|
||||
</AudioAssetWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const AudioAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.player {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -0,0 +1,53 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { SelectOutlined, ExpandOutlined, MinusCircleOutlined, PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { ImageAssetWrapper } from './ImageAsset.styled';
|
||||
|
||||
export function ImageAsset({ thumbUrl, fullUrl }) {
|
||||
|
||||
const [state, setState] = useState({ width: 0, height: 0, popout: false });
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
const onPopOut = () => {
|
||||
if (state.width == 0 || state.height == 0) {
|
||||
updateState({ popout: true, popWidth: '50%', popHeight: '50%' });
|
||||
}
|
||||
else {
|
||||
if (state.width / state.height > window.innerWidth / window.innerHeight) {
|
||||
updateState({ popout: true, popWidth: '80%', popHeight: 'auto' });
|
||||
}
|
||||
else {
|
||||
let width = Math.floor(80 * (state.width / state.height) * (window.innerHeight / window.innerWidth));
|
||||
updateState({ popout: true, popWidth: width + '%', popHeight: 'auto' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageAssetWrapper>
|
||||
<ReactResizeDetector handleWidth={true} handleHeight={true}>
|
||||
{({ width, height }) => {
|
||||
if (width != state.width || height != state.height) {
|
||||
updateState({ width, height });
|
||||
}
|
||||
return <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
<div class="viewer">
|
||||
<div class="overlay" style={{ width: state.width, height: state.height }}>
|
||||
<div class="expand" onClick={() => onPopOut()}>
|
||||
<ExpandOutlined style={{ fontSize: 24, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal visible={state.popout} width={state.popWidth} height={state.popHeight} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={() => { updateState({ popout: false })}}>
|
||||
<img style={{ width: '100%', objectFit: 'contain' }} src={fullUrl} alt="" />
|
||||
</Modal>
|
||||
</ImageAssetWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ImageAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.viewer {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.viewer:hover .overlay {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.expand {
|
||||
padding-left: 4px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
126
net/web/src/session/conversation/topicItem/useTopicItem.hook.js
Normal file
126
net/web/src/session/conversation/topicItem/useTopicItem.hook.js
Normal file
@ -0,0 +1,126 @@
|
||||
import { useContext, useState, useEffect, useRef } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { ProfileContext } from 'context/ProfileContext';
|
||||
import { CardContext } from 'context/CardContext';
|
||||
|
||||
export function useTopicItem(topic) {
|
||||
|
||||
const [guid, setGuid] = useState(null);
|
||||
|
||||
const [state, setState] = useState({
|
||||
name: null,
|
||||
handle: null,
|
||||
imageUrl: null,
|
||||
message: null,
|
||||
created: null,
|
||||
confirmed: false,
|
||||
ready: false,
|
||||
error: false,
|
||||
owner: false,
|
||||
assets: [],
|
||||
editing: false,
|
||||
busy: false,
|
||||
textColor: '#444444',
|
||||
textSize: 14,
|
||||
});
|
||||
|
||||
const profile = useContext(ProfileContext);
|
||||
const card = useContext(CardContext);
|
||||
const conversation = useContext(ConversationContext);
|
||||
const editMessage = useRef(null);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let owner = false;
|
||||
if (profile.state.profile.guid == topic?.data?.topicDetail.guid) {
|
||||
owner = true;
|
||||
}
|
||||
|
||||
let textColor = '#444444';
|
||||
let textSize = 14;
|
||||
|
||||
if (!topic?.data) {
|
||||
console.log("invalid topic:", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
const { status, transform, data } = topic.data.topicDetail;
|
||||
let message;
|
||||
let ready = false;
|
||||
let error = false;
|
||||
let confirmed = false;
|
||||
let assets = [];
|
||||
if (status === 'confirmed') {
|
||||
confirmed = true;
|
||||
try {
|
||||
message = JSON.parse(data);
|
||||
if (message.textColor != null) {
|
||||
textColor = message.textColor;
|
||||
}
|
||||
if (message.textSize != null) {
|
||||
textSize = message.textSize;
|
||||
}
|
||||
if (message.assets) {
|
||||
assets = message.assets;
|
||||
delete message.assets;
|
||||
}
|
||||
if (transform === 'complete') {
|
||||
ready = true;
|
||||
}
|
||||
if (transform === 'error') {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.state.init && card.state.init && conversation.state.init) {
|
||||
const { guid, created } = topic.data.topicDetail;
|
||||
if (profile.state.profile.guid == guid) {
|
||||
const { name, handle, imageUrl } = profile.actions.getProfile();
|
||||
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created, owner, textColor, textSize });
|
||||
}
|
||||
else {
|
||||
const { name, handle, imageUrl } = card.actions.getCardProfileByGuid(guid);
|
||||
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created, owner, textColor, textSize });
|
||||
}
|
||||
}
|
||||
}, [profile, card, conversation, topic]);
|
||||
|
||||
const actions = {
|
||||
getAssetUrl: (assetId) => {
|
||||
return conversation.actions.getAssetUrl(topic?.id, assetId);
|
||||
},
|
||||
removeTopic: async () => {
|
||||
return await conversation.actions.removeTopic(topic.id);
|
||||
},
|
||||
setEditing: (editing) => {
|
||||
editMessage.current = state.message?.text;
|
||||
updateState({ editing });
|
||||
},
|
||||
setEdit: (edit) => {
|
||||
editMessage.current = edit;
|
||||
},
|
||||
setMessage: async () => {
|
||||
if (!state.busy) {
|
||||
updateState({ busy: true });
|
||||
try {
|
||||
await conversation.actions.setTopicSubject(topic.id,
|
||||
{ ...state.message, text: editMessage.current, assets: state.assets });
|
||||
updateState({ editing: false });
|
||||
}
|
||||
catch (err) {
|
||||
window.alert(err);
|
||||
}
|
||||
updateState({ busy: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import ReactPlayer from 'react-player'
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { SelectOutlined, ExpandOutlined, MinusCircleOutlined, PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { VideoAssetWrapper } from './VideoAsset.styled';
|
||||
|
||||
export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
|
||||
|
||||
const [state, setState] = useState({});
|
||||
const player = useRef(null);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}, [thumbUrl, hdUrl, lqUrl]);
|
||||
|
||||
const onPopOut = () => {
|
||||
if (state.width == 0 || state.height == 0) {
|
||||
updateState({ popout: true, popWidth: '50%', inline: false, popoutUrl: hdUrl, playing: false, inlineUrl: null });
|
||||
}
|
||||
else {
|
||||
if (state.width / state.height > window.innerWidth / window.innerHeight) {
|
||||
updateState({ popout: true, popWidth: '70%', inline: false, popoutUrl: hdUrl, playing: false, inlineUrl: null });
|
||||
}
|
||||
else {
|
||||
let width = Math.floor(70 * (state.width / state.height) * (window.innerHeight / window.innerWidth));
|
||||
updateState({ popout: true, popWidth: width + '%', inline: false, popoutUrl: hdUrl, playing: false, inlineUrl: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CenterButton = () => {
|
||||
if (!state.inline) {
|
||||
return (
|
||||
<div onClick={() => updateState({ inline: true, inlineUrl: lqUrl, playing: false })}>
|
||||
<SelectOutlined style={{ fontSize: 48, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state.playing) {
|
||||
return (
|
||||
<div onClick={() => updateState({ playing: false })}>
|
||||
<MinusCircleOutlined style={{ fontSize: 48, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div onClick={() => updateState({ playing: true })}>
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Controls = () => {
|
||||
return (
|
||||
<div>
|
||||
<div class="control">
|
||||
<CenterButton />
|
||||
</div>
|
||||
<div class="expand" onClick={() => onPopOut()}>
|
||||
<ExpandOutlined style={{ fontSize: 24, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VideoAssetWrapper>
|
||||
<ReactResizeDetector handleWidth={true} handleHeight={true}>
|
||||
{({ width, height }) => {
|
||||
if (width != state.width || height != state.height) {
|
||||
updateState({ width, height });
|
||||
}
|
||||
return <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
<div class="player" style={{ width: state.width, height: state.height }}>
|
||||
<ReactPlayer ref={player} controls={false} playing={state.playing}
|
||||
height="100%" width="100%" url={state.inlineUrl} />
|
||||
<Controls />
|
||||
</div>
|
||||
<Modal visible={state.popout} width={state.popWidth} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={() => { updateState({ popout: false })}}>
|
||||
<ReactPlayer controls={true} height="100%" width="100%" url={state.popoutUrl} />
|
||||
</Modal>
|
||||
</VideoAssetWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const VideoAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.playback {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player:hover .control {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.player:hover .expand {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.expand {
|
||||
padding-left: 4px;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
@ -2,6 +2,7 @@ import { useContext, useState, useEffect } from 'react';
|
||||
import { ViewportContext } from 'context/ViewportContext';
|
||||
import { CardContext } from 'context/CardContext';
|
||||
import { ChannelContext } from 'context/ChannelContext';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
|
||||
export function useConversation(cardId, channelId) {
|
||||
|
||||
@ -10,11 +11,13 @@ export function useConversation(cardId, channelId) {
|
||||
image: null,
|
||||
logo: null,
|
||||
subject: null,
|
||||
topics: [],
|
||||
});
|
||||
|
||||
const viewport = useContext(ViewportContext);
|
||||
const card = useContext(CardContext);
|
||||
const channel = useContext(ChannelContext);
|
||||
const conversation = useContext(ConversationContext);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
@ -59,8 +62,29 @@ export function useConversation(cardId, channelId) {
|
||||
updateState({ image, subject, logo });
|
||||
}, [cardId, channelId, card, channel]);
|
||||
|
||||
useEffect(() => {
|
||||
conversation.actions.setConversationId(cardId, channelId);
|
||||
}, [cardId, channelId]);
|
||||
|
||||
useEffect(() => {
|
||||
let topics = Array.from(conversation.state.topics.values()).sort((a, b) => {
|
||||
const aTimestamp = a?.data?.topicDetail?.created;
|
||||
const bTimestamp = b?.data?.topicDetail?.created;
|
||||
if(aTimestamp == bTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
if(aTimestamp == null || aTimestamp < bTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
updateState({ topics });
|
||||
}, [conversation]);
|
||||
|
||||
const actions = {
|
||||
more: () => {
|
||||
conversation.actions.addHistory();
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
|
344
net/web/src/session/conversation/virtualList/VirtualList.jsx
Normal file
344
net/web/src/session/conversation/virtualList/VirtualList.jsx
Normal file
@ -0,0 +1,344 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { VirtualListWrapper, VirtualItem } from './VirtualList.styled';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
|
||||
export function VirtualList({ id, items, itemRenderer, onMore }) {
|
||||
|
||||
const REDZONE = 1024; // recenter on canvas if in canvas edge redzone
|
||||
const HOLDZONE = 2048; // drop slots outside of holdzone of view
|
||||
const OVERSCAN = 1024; // add slots in overscan of view
|
||||
const DEFAULT_ITEM_HEIGHT = 256;
|
||||
const DEFAULT_LIST_HEIGHT = 4096;
|
||||
const GUTTER = 6;
|
||||
|
||||
const [ msg, setMsg ] = useState("YO");
|
||||
|
||||
const [ canvasHeight, setCanvasHeight ] = useState(16384);
|
||||
const [ slots, setSlots ] = useState(new Map());
|
||||
const [ scroll, setScroll ] = useState('hidden');
|
||||
|
||||
let update = useRef(0);
|
||||
let viewHeight = useRef(DEFAULT_LIST_HEIGHT);
|
||||
let latch = useRef(true);
|
||||
let scrollTop = useRef(0);
|
||||
let containers = useRef([]);
|
||||
let listRef = useRef();
|
||||
let key = useRef(null);
|
||||
let itemView = useRef([]);
|
||||
let nomore = useRef(false);
|
||||
|
||||
const addSlot = (id, slot) => {
|
||||
setSlots((m) => { m.set(id, slot); return new Map(m); })
|
||||
}
|
||||
|
||||
const updateSlot = (id, slot) => {
|
||||
setSlots((m) => { m.set(id, slot); return new Map(m); })
|
||||
}
|
||||
|
||||
const removeSlot = (id) => {
|
||||
setSlots((m) => { m.delete(id); return new Map(m); });
|
||||
}
|
||||
|
||||
const clearSlots = () => {
|
||||
setSlots((m) => { return new Map() })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateCanvas();
|
||||
}, [canvasHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (key.current != id) {
|
||||
key.current = id;
|
||||
latch.current = true;
|
||||
containers.current = [];
|
||||
clearSlots();
|
||||
}
|
||||
itemView.current = items;
|
||||
setItems();
|
||||
}, [items, id]);
|
||||
|
||||
const onScrollWheel = (e) => {
|
||||
latch.current = false;
|
||||
}
|
||||
|
||||
const onScrollView = (e) => {
|
||||
scrollTop.current = e.target.scrollTop;
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
}
|
||||
|
||||
const updateCanvas = () => {
|
||||
alignSlots();
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
latchScroll();
|
||||
}
|
||||
|
||||
const limitScroll = () => {
|
||||
let view = getPlacement();
|
||||
if (view && containers.current[containers.current.length - 1].index == itemView.current.length - 1) {
|
||||
if (view?.overscan?.bottom <= 0) {
|
||||
if (view.position.height < viewHeight.current) {
|
||||
if (scrollTop.current != view.position.top) {
|
||||
scrollTop.current = view.position.top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (scrollTop.current != view.position.bottom - viewHeight.current) {
|
||||
scrollTop.current = view.position.bottom - viewHeight.current;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
latch.current = true;
|
||||
}
|
||||
}
|
||||
if (view && containers.current[0].index == 0) {
|
||||
if (view?.overscan?.top <= 0) {
|
||||
scrollTop.current = containers.current[0].top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
if (!nomore.current) {
|
||||
nomore.current = true;
|
||||
onMore();
|
||||
setTimeout(() => {
|
||||
nomore.current = false;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadNextSlot = () => {
|
||||
let view = getPlacement();
|
||||
if (view) {
|
||||
if (view.overscan.top < OVERSCAN) {
|
||||
if (containers.current[0].index > 0 && containers.current[0].index < itemView.current.length) {
|
||||
let below = containers.current[0];
|
||||
let container = {
|
||||
top: below.top - (DEFAULT_ITEM_HEIGHT + 2 * GUTTER),
|
||||
height: DEFAULT_ITEM_HEIGHT,
|
||||
index: containers.current[0].index - 1,
|
||||
id: itemView.current[containers.current[0].index - 1].id,
|
||||
revision: itemView.current[containers.current[0].index - 1].revision,
|
||||
}
|
||||
containers.current.unshift(container);
|
||||
addSlot(container.id, getSlot(container))
|
||||
loadNextSlot();
|
||||
}
|
||||
}
|
||||
if (view.overscan.bottom < OVERSCAN) {
|
||||
if (containers.current[containers.current.length - 1].index + 1 < itemView.current.length) {
|
||||
let above = containers.current[containers.current.length - 1];
|
||||
let container = {
|
||||
top: above.top + above.height + 2 * GUTTER,
|
||||
height: DEFAULT_ITEM_HEIGHT,
|
||||
index: containers.current[containers.current.length - 1].index + 1,
|
||||
id: itemView.current[containers.current[containers.current.length - 1].index + 1].id,
|
||||
revision: itemView.current[containers.current[containers.current.length - 1].index + 1].revision,
|
||||
}
|
||||
containers.current.push(container);
|
||||
addSlot(container.id, getSlot(container))
|
||||
loadNextSlot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dropSlots = () => {
|
||||
while (containers.current.length > 0 &&
|
||||
containers.current[0].top + containers.current[0].height + 2 * GUTTER + HOLDZONE < scrollTop.current) {
|
||||
removeSlot(containers.current[0].id);
|
||||
containers.current.shift();
|
||||
}
|
||||
while (containers.current.length > 0 &&
|
||||
containers.current[containers.current.length - 1].top > scrollTop.current + viewHeight.current + HOLDZONE) {
|
||||
removeSlot(containers.current[containers.current.length - 1].id);
|
||||
containers.current.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const alignSlots = () => {
|
||||
if (containers.current.length > 1) {
|
||||
let mid = Math.floor(containers.current.length / 2);
|
||||
|
||||
let alignTop = containers.current[mid].top + containers.current[mid].height + 2 * GUTTER;
|
||||
for (let i = mid + 1; i < containers.current.length; i++) {
|
||||
if (containers.current[i].top != alignTop) {
|
||||
containers.current[i].top = alignTop;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
alignTop += containers.current[i].height + 2 * GUTTER;
|
||||
}
|
||||
|
||||
let alignBottom = containers.current[mid].top;
|
||||
for (let i = mid - 1; i >= 0; i--) {
|
||||
alignBottom -= (containers.current[i].height + 2 * GUTTER);
|
||||
if (containers.current[i].top != alignBottom) {
|
||||
containers.current[i].top = alignBottom;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const centerCanvas = () => {
|
||||
let view = getPlacement();
|
||||
if (view) {
|
||||
let height = canvasHeight;
|
||||
if (view.position.top < REDZONE) {
|
||||
let shift = height / 2;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top += shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current += shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
else if (view.position.bottom + REDZONE > height) {
|
||||
let shift = height / 2;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top -= shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current -= shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latchScroll = () => {
|
||||
if (latch.current) {
|
||||
let view = getPlacement();
|
||||
if (view) {
|
||||
if (view.position.height < viewHeight.current) {
|
||||
if (scrollTop.current != view.position.top) {
|
||||
scrollTop.current = view.position.top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (scrollTop.current != view.position.bottom - viewHeight.current) {
|
||||
scrollTop.current = view.position.bottom - viewHeight.current;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setItems = () => {
|
||||
|
||||
// align containers in case history was loaded
|
||||
if (containers.current.length > 0) {
|
||||
let container = containers.current[0];
|
||||
for (let j = 0; j < itemView.current.length; j++) {
|
||||
if (itemView.current[j].id == container.id) {
|
||||
for(let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].index = i + j;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove containers following any removed item
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
let container = containers.current[i];
|
||||
if (itemView.current.length <= container.index || itemView.current[container.index].id != container.id) {
|
||||
while (containers.current.length > i) {
|
||||
let popped = containers.current.pop();
|
||||
removeSlot(popped.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (itemView.current[container.index].revision != container.revision) {
|
||||
updateSlot(container.id, getSlot(containers.current[i]));
|
||||
containers.revision = itemView.current[container.index].revision;
|
||||
}
|
||||
}
|
||||
|
||||
// place first slot
|
||||
if (itemView.current.length > 0 && canvasHeight > 0) {
|
||||
let view = getPlacement();
|
||||
if (!view) {
|
||||
let pos = canvasHeight / 2;
|
||||
listRef.current.scrollTo({ top: pos, left: 0 });
|
||||
scrollTop.current = pos;
|
||||
|
||||
let container = {
|
||||
top: pos - DEFAULT_ITEM_HEIGHT,
|
||||
height: DEFAULT_ITEM_HEIGHT,
|
||||
index: itemView.current.length - 1,
|
||||
id: itemView.current[itemView.current.length - 1].id,
|
||||
revision: itemView.current[itemView.current.length - 1].revision,
|
||||
}
|
||||
|
||||
containers.current.push(container);
|
||||
addSlot(container.id, getSlot(container));
|
||||
}
|
||||
}
|
||||
|
||||
updateCanvas();
|
||||
}
|
||||
|
||||
const onItemHeight = (container, height) => {
|
||||
container.height = height;
|
||||
updateCanvas();
|
||||
}
|
||||
|
||||
const getSlot = (container) => {
|
||||
return (
|
||||
<VirtualItem style={{ top: container.top, paddingTop: GUTTER, paddingBottom: GUTTER }}>
|
||||
<ReactResizeDetector handleHeight={true}>
|
||||
{({ height }) => {
|
||||
if (typeof height !== 'undefined') {
|
||||
onItemHeight(container, height);
|
||||
}
|
||||
return itemRenderer(itemView.current[container.index]);
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
</VirtualItem>
|
||||
)
|
||||
}
|
||||
|
||||
const getPlacement = () => {
|
||||
if (containers.current.length == 0) {
|
||||
return null;
|
||||
}
|
||||
let top = containers.current[0].top;
|
||||
let bottom = containers.current[containers.current.length-1].top + containers.current[containers.current.length-1].height + 2 * GUTTER;
|
||||
let overTop = scrollTop.current - top;
|
||||
let overBottom = bottom - (scrollTop.current + viewHeight.current);
|
||||
return {
|
||||
position: { top, bottom, height: bottom - top },
|
||||
overscan: { top: overTop, bottom: overBottom }
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100%' }}>
|
||||
<ReactResizeDetector handleHeight={true} handleWidth={true}>
|
||||
{({ height }) => {
|
||||
if (height) {
|
||||
viewHeight.current = height;
|
||||
updateCanvas();
|
||||
}
|
||||
return (
|
||||
<VirtualListWrapper onScroll={onScrollView} onWheel={onScrollWheel} onTouchStart={onScrollWheel} >
|
||||
<div class="rollview" style={{ overflowY: 'auto' }} ref={listRef} onScroll={onScrollView}>
|
||||
<div class="roll" style={{ height: canvasHeight }}>
|
||||
{ slots.values() }
|
||||
</div>
|
||||
</div>
|
||||
</VirtualListWrapper>
|
||||
)
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const VirtualListWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.rollview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.rollview::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roll {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
|
||||
export const VirtualItem = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
background-color: #f0f5e0;
|
||||
}
|
||||
`;
|
@ -42,6 +42,8 @@ export const DetailsWrapper = styled.div`
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: scroll;
|
||||
|
||||
.label {
|
||||
padding-top: 16px;
|
||||
@ -52,8 +54,6 @@ export const DetailsWrapper = styled.div`
|
||||
|
||||
.members {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: scroll;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user