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 (
+
+
+ {error && (
+
+
+
+ )}
+ {!ready && !error && (
+
+ )}
+
+
+
+ )
+ }
+
+ if (slots.length != 0) {
+ return (
+
+
+ {slots}
+
+
+
+ );
+ }
+ 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 = (
);
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()} />
+ }}
+
+
+
+ )
+}
+
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;
+ }
+ }
+ }
+`;
+
+