diff --git a/net/web/src/carousel/Carousel.jsx b/net/web/src/carousel/Carousel.jsx new file mode 100644 index 00000000..a8725551 --- /dev/null +++ b/net/web/src/carousel/Carousel.jsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Skeleton } from 'antd'; +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 }) { + const [slots, setSlots] = useState([]); + const [carouselRef, setCarouselRef] = useState(false); + const [itemIndex, setItemIndex] = useState(0); + const [scrollLeft, setScrollLeft] = useState('hidden'); + const [scrollRight, setScrollRight] = useState('hidden'); + const FUDGE = 1; + + let carousel = useRef(); + let itemWidth = useRef(new Map()); + + useEffect(() => { + setScroll('smooth'); + setArrows(); + }, [itemIndex, items]); + + useEffect(() => { + setScroll('auto'); + }, [carouselRef]); + + const updateItemIndex = (val) => { + setItemIndex((i) => { + if (i + val < 0) { + return 0; + } + return i + val; + }) + } + + const onLeft = () => { + if (itemIndex > 0) { + updateItemIndex(-1); + } + } + + const onRight = () => { + if(itemIndex + 1 < items.length) { + updateItemIndex(+1); + } + } + + const setScroll = (behavior) => { + let pos = FUDGE; + for (let i = 0; i < itemIndex; i++) { + pos += itemWidth.current.get(i) + 32; + } + if (carousel.current) { + carousel.current.scrollTo({ top: 0, left: pos, behavior }); + } + } + + const setArrows = () => { + if (itemIndex == 0) { + setScrollLeft('hidden'); + } + else { + setScrollLeft('unset'); + } + if (itemIndex + 1 >= items.length) { + setScrollRight('hidden'); + } + else { + setScrollRight('unset'); + } + } + + const RemoveItem = ({ index }) => { + if (itemRemove) { + return
itemRemove(index)}>
+ } + return <> + } + + useEffect(() => { + let assets = []; + if (ready) { + for (let i = 0; i < items.length; i++) { + assets.push(( + + {({ width, height }) => { + itemWidth.current.set(i, width); + return ( +
+
{ itemRenderer(items[i], i) }
+ +
+ ); + }} +
+ )); + } + if (items.length > 0) { + assets.push(
 
) + } + if (itemIndex >= items.length) { + if (items.length > 0) { + setItemIndex(items.length - 1); + } + else { + setItemIndex(0); + } + } + } + + setSlots(assets); + setScroll(); + setArrows(); + }, [ready, items]); + + const onRefSet = (r) => { + if (r != null) { + carousel.current = r; + setCarouselRef(true); + } + } + + if (!ready || error) { + return ( + + +
+
+
+
+
+ ) + } + + if (slots.length != 0) { + return ( + + +
+
+
+
+
+ ); + } + return <> + +} + diff --git a/net/web/src/carousel/Carousel.styled.js b/net/web/src/carousel/Carousel.styled.js new file mode 100644 index 00000000..7ea85639 --- /dev/null +++ b/net/web/src/carousel/Carousel.styled.js @@ -0,0 +1,89 @@ +import styled from 'styled-components'; + +export const CarouselWrapper = styled.div` + position: relative; + display: grid; + width: 100%; + height: 128px; + margin-top: 16px; + + .carousel { + display: flex; + flex-direction: row; + padding-left: 16px; + width: 100%; + overflow: hidden; + + /* hide scrollbar for IE, Edge and Firefox */ + -ms-overflow-style: none; + scrollbar-width: none; + } + + .status { + width: 128px; + height: 128px; + display: flex; + align-items: center; + justify-content: center; + color: #888888; + background-color: #eeeeee; + } + + .carousel::-webkit-scrollbar { + display: none; + } + + .arrows { + height: 100%; + display: flex; + flex-direction: column; + position: absolute; + } + + .arrow { + height: 50%; + background-color: #888888; + color: white; + font-size: 16px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + } + + .arrow:hover { + opacity: 1; + } + + .item { + margin-right: 32px; + position: relative; + } + + .delitem { + position: absolute; + top: 0; + right: 0; + background-color: #888888; + color: white; + border-bottom-left-radius: 2px; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; + } + + .asset { + height: 128px; + } + + .space { + height: 128px; + padding-left: 100%; + } + + .object { + height: 100%; + object-fit: contain; + } +`; + diff --git a/net/web/src/carousel/useCarousel.js b/net/web/src/carousel/useCarousel.js new file mode 100644 index 00000000..24d66170 --- /dev/null +++ b/net/web/src/carousel/useCarousel.js @@ -0,0 +1,21 @@ +import { useContext, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export function useCarousel() { + + const [state, setState] = useState({ + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const navigate = useNavigate(); + + const actions = { + }; + + return { state, actions }; +} + + diff --git a/net/web/src/session/conversation/Conversation.jsx b/net/web/src/session/conversation/Conversation.jsx index 4cce5336..97724e06 100644 --- a/net/web/src/session/conversation/Conversation.jsx +++ b/net/web/src/session/conversation/Conversation.jsx @@ -36,7 +36,7 @@ console.log(state);
- +
); diff --git a/net/web/src/session/conversation/addTopic/AddTopic.jsx b/net/web/src/session/conversation/addTopic/AddTopic.jsx index 69f39f71..2de97c18 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.jsx +++ b/net/web/src/session/conversation/addTopic/AddTopic.jsx @@ -4,18 +4,56 @@ import { Input, Menu, Dropdown } from 'antd'; import { useRef, useState } from 'react'; import { FontColorsOutlined, FontSizeOutlined, PaperClipOutlined, SendOutlined } from '@ant-design/icons'; import { SketchPicker } from "react-color"; +import { AudioFile } from './audioFile/AudioFile'; +import { VideoFile } from './videoFile/VideoFile'; +import { Carousel } from 'carousel/Carousel'; -export function AddTopic() { - - const { state, actions } = useAddTopic(); +export function AddTopic({ cardId, channelId }) { + const { state, actions } = useAddTopic(cardId, channelId); + const attachImage = useRef(null); + const attachAudio = useRef(null); + const attachVideo = useRef(null); const msg = useRef(); + const keyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { msg.current.blur(); } } + 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) { + return + } + if (item.audio) { + return actions.setLabel(index, label)}/> + } + if (item.video) { + return actions.setPosition(index, pos)} url={item.url} /> + } + return <> + } + + const removeItem = (index) => { + actions.removeAsset(index); + } + const picker = ( -
Add Image
+ onSelectImage(e)} style={{display: 'none'}}/> +
attachImage.current.click()}>Attach Image
-
Add Video
+ onSelectAudio(e)} style={{display: 'none'}}/> +
attachAudio.current.click()}>Attach Audio
-
Add Audio
+ onSelectVideo(e)} style={{display: 'none'}}/> +
attachVideo.current.click()}>Attach Video
); return ( - +
keyDown(e)} /> diff --git a/net/web/src/session/conversation/addTopic/AddTopic.styled.js b/net/web/src/session/conversation/addTopic/AddTopic.styled.js index cdbb209d..b849295d 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.styled.js +++ b/net/web/src/session/conversation/addTopic/AddTopic.styled.js @@ -28,8 +28,8 @@ export const AddTopicWrapper = styled.div` flex-align: center; justify-content: center; align-items: center; - width: 32px; - height: 32px; + width: 36px; + height: 36px; cursor: pointer; border: 1px solid ${Colors.divider}; background-color: ${Colors.white}; diff --git a/net/web/src/session/conversation/addTopic/audioFile/AudioFile.jsx b/net/web/src/session/conversation/addTopic/audioFile/AudioFile.jsx new file mode 100644 index 00000000..bb686dc1 --- /dev/null +++ b/net/web/src/session/conversation/addTopic/audioFile/AudioFile.jsx @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from 'react'; +import ReactResizeDetector from 'react-resize-detector'; +import { SoundOutlined } from '@ant-design/icons'; +import { AudioFileWrapper, LabelInput } from './AudioFile.styled'; + +export function AudioFile({ onLabel }) { + + const [state, setState] = useState({ height: 0 }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + return ( + + + {({ height }) => { + if (height != state.height) { + updateState({ height }); + } + return ( +
+ + onLabel(e.target.value)}/>; +
+ ) + }} +
+
+ ) +} + diff --git a/net/web/src/session/conversation/addTopic/audioFile/AudioFile.styled.js b/net/web/src/session/conversation/addTopic/audioFile/AudioFile.styled.js new file mode 100644 index 00000000..e736c655 --- /dev/null +++ b/net/web/src/session/conversation/addTopic/audioFile/AudioFile.styled.js @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import { Input } from 'antd'; + +export const AudioFileWrapper = styled.div` + position: relative; + height: 100%; + + .square { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: #444444; + } +`; + +export const LabelInput = styled(Input)` + position: absolute; + width: 100%; + bottom: 0; + text-align: center; + color: white; +` + diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 6e5fefc6..26cc3fab 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -1,16 +1,100 @@ import { useContext, useState } from 'react'; +import { CardContext } from 'context/CardContext'; +import { ChannelContext } from 'context/ChannelContext'; -export function useAddTopic() { +export function useAddTopic(cardId, channelId) { - const [state, setState] = useState({}); + const [state, setState] = useState({ + assets: [], + messageText: null, + textColor: '#444444', + textColorSet: false, + textSize: 14, + textSizeSet: false, + busy: false, + }); + + const card = useContext(CardContext); + const channel = useContext(ChannelContext); const updateState = (value) => { setState((s) => ({ ...s, ...value })); }; + const addAsset = (value) => { + setState((s) => { + let assets = [...s.assets, value]; + return { ...s, assets }; + }); + } + + const updateAsset = (index, value) => { + setState((s) => { + s.assets[index] = { ...s.assets[index], ...value }; + return { ...s }; + }); + } + + const removeAsset = (index) => { + setState((s) => { + s.assets.splice(index, 1); + let assets = [...s.assets]; + return { ...s, assets }; + }); + } + const actions = { - setTextColor: (textColor) => { - updateState({ textColor }); + addImage: (image) => { + let url = URL.createObjectURL(image); + addAsset({ image, url }) + }, + addVideo: (video) => { + let url = URL.createObjectURL(video); + addAsset({ video, url, position: 0 }) + }, + addAudio: (audio) => { + let url = URL.createObjectURL(audio); + addAsset({ audio, url, label: '' }) + }, + setLabel: (index, label) => { + updateAsset(index, { label }); + }, + setPosition: (index, position) => { + updateAsset(index, { position }); + }, + removeAsset: (idx) => { removeAsset(idx) }, + setTextColor: (value) => { + updateState({ textColorSet: true, textColor: value }); + }, + setMessageText: (value) => { + updateState({ messageText: value }); + }, + setTextSize: (value) => { + updateState({ textSizeSet: true, textSize: value }); + }, + addTopic: async () => { + if (!state.busy) { + updateState({ busy: true }); + try { + let message = { + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, + }; + if (cardId) { + await card.actions.addChannelTopic(cardId, channelId, message, state.assets); + } + else { + await channel.actions.addChannelTopic(channelId, message, state.assets); + } + updateState({ 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 }); + } }, }; diff --git a/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx new file mode 100644 index 00000000..4b306130 --- /dev/null +++ b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState, useRef } from 'react'; +import ReactPlayer from 'react-player' +import ReactResizeDetector from 'react-resize-detector'; +import { RightOutlined, LeftOutlined } from '@ant-design/icons'; +import { VideoFileWrapper, LabelInput } from './VideoFile.styled'; + +export function VideoFile({ url, onPosition }) { + + const [state, setState] = useState({ width: 0, height: 0 }); + const [playing, setPlaying] = useState(false); + const player = useRef(null); + const seek = useRef(0); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const onSeek = (offset) => { + if (player.current) { + let len = player.current.getDuration(); + if (len > 128) { + offset *= Math.floor(len / 128); + } + seek.current += offset; + if (seek.current < 0 || seek.current >= len) { + seek.current = 0; + } + onPosition(seek.current); + player.current.seekTo(seek.current, 'seconds'); + setPlaying(true); + } + } + + const onPause = () => { + setPlaying(false); + } + + return ( + + + {({ width, height }) => { + if (width != state.width || height != state.height) { + updateState({ width, height }); + } + return onPause()} onPlay={() => onPause()} /> + }} + +
+
+
+
onSeek(-1)}> + +
+
+
+
onSeek(1)}> + +
+
+
+
+
+ ) +} + diff --git a/net/web/src/session/conversation/addTopic/videoFile/VideoFile.styled.js b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.styled.js new file mode 100644 index 00000000..1126183f --- /dev/null +++ b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.styled.js @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +export const VideoFileWrapper = styled.div` + position: relative; + height: 100%; + + .overlay { + position: absolute; + top: 0; + height: 100%; + display: flex; + align-items: center; + + .arrows { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + + .left-arrow { + width: 50%; + display: flex; + justify-content: flex-begin; + } + + .right-arrow { + width: 50%; + display: flex; + justify-content: flex-end; + } + + .icon { + cursor: pointer; + } + } + } +`; + +