From 221b36895cd8356c65cdc5acf367cc9bce8d31c2 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Fri, 11 Aug 2023 14:33:43 -0700 Subject: [PATCH] adding generic file attachement to browser app --- .../internal/api_addChannelTopicBlock.go | 38 +++++++++--- net/web/src/context/useUploadContext.hook.js | 25 +++++++- .../conversation/addTopic/AddTopic.jsx | 20 ++++-- .../addTopic/binaryFile/BinaryFile.jsx | 29 +++++++++ .../addTopic/binaryFile/BinaryFile.styled.js | 50 +++++++++++++++ .../conversation/addTopic/useAddTopic.hook.js | 7 +++ .../conversation/topicItem/TopicItem.jsx | 4 ++ .../topicItem/binaryAsset/BinaryAsset.jsx | 42 +++++++++++++ .../binaryAsset/BinaryAsset.styled.js | 57 +++++++++++++++++ .../binaryAsset/useBinaryAsset.hook.js | 61 +++++++++++++++++++ .../topicItem/useTopicItem.hook.js | 11 +++- 11 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.jsx create mode 100644 net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.styled.js create mode 100644 net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.jsx create mode 100644 net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.styled.js create mode 100644 net/web/src/session/conversation/topicItem/binaryAsset/useBinaryAsset.hook.js diff --git a/net/server/internal/api_addChannelTopicBlock.go b/net/server/internal/api_addChannelTopicBlock.go index 6290f951..567ad94f 100644 --- a/net/server/internal/api_addChannelTopicBlock.go +++ b/net/server/internal/api_addChannelTopicBlock.go @@ -15,6 +15,7 @@ func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) { // scan parameters params := mux.Vars(r) topicID := params["topicID"] + body := r.FormValue("body") channelSlot, guid, code, err := getChannelSlot(r, true) if err != nil { @@ -57,14 +58,35 @@ func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) { garbageSync.Lock() defer garbageSync.Unlock() - // save new file - id := uuid.New().String() - path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id - crc, size, err := saveAsset(r.Body, path) - if err != nil { - ErrResponse(w, http.StatusInternalServerError, err) - return - } + // save new file + var crc uint32 + var size int64 + id := uuid.New().String() + path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id + if body == "multipart" { + if err := r.ParseMultipartForm(32 << 20); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + file, _, err := r.FormFile("asset") + if err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + defer file.Close() + crc, size, err = saveAsset(file, path) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + } else { + crc, size, err = saveAsset(r.Body, path) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + } asset := &store.Asset{} asset.AssetID = id diff --git a/net/web/src/context/useUploadContext.hook.js b/net/web/src/context/useUploadContext.hook.js index 01a12e30..af0557a8 100644 --- a/net/web/src/context/useUploadContext.hook.js +++ b/net/web/src/context/useUploadContext.hook.js @@ -232,8 +232,8 @@ async function upload(entry, update, complete) { entry.active = {}; try { if (file.encrypted) { - const { size, getEncryptedBlock, position, label, image, video, audio } = file; - const { data, type } = image ? { data: image, type: 'image' } : video ? { data: video, type: 'video' } : audio ? { data: audio, type: 'audio' } : {} + const { size, getEncryptedBlock, position, label, extension, image, video, audio, binary } = file; + const { data, type } = image ? { data: image, type: 'image' } : video ? { data: video, type: 'video' } : audio ? { data: audio, type: 'audio' } : { data: binary, type: 'binary' } const thumb = await getThumb(data, type, position); const parts = []; for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) { @@ -252,7 +252,7 @@ async function upload(entry, update, complete) { parts.push({ blockIv, partId: part.data.assetId }); } entry.assets.push({ - encrypted: { type, thumb, label, parts } + encrypted: { type, thumb, label, extension, parts } }); } else if (file.image) { @@ -314,6 +314,25 @@ async function upload(entry, update, complete) { } }); } + else if (file.binary) { + const formData = new FormData(); + formData.append('asset', file.binary); + let asset = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}&body=multipart`, formData, { + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + entry.active = { loaded, total } + update(); + }, + }); + entry.assets.push({ + binary: { + label: file.label, + extension: file.extension, + data: asset.data.assetId, + } + }); + } entry.active = null; upload(entry, update, complete); } diff --git a/net/web/src/session/conversation/addTopic/AddTopic.jsx b/net/web/src/session/conversation/addTopic/AddTopic.jsx index f00e42aa..483fd01d 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.jsx +++ b/net/web/src/session/conversation/addTopic/AddTopic.jsx @@ -2,10 +2,11 @@ import { AddTopicWrapper } from './AddTopic.styled'; import { useAddTopic } from './useAddTopic.hook'; import { Modal, Input, Menu, Dropdown, Spin } from 'antd'; import { useRef } from 'react'; -import { SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, SendOutlined } from '@ant-design/icons'; +import { FieldBinaryOutlined, SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, SendOutlined } from '@ant-design/icons'; import { SketchPicker } from "react-color"; import { AudioFile } from './audioFile/AudioFile'; import { VideoFile } from './videoFile/VideoFile'; +import { BinaryFile } from './binaryFile/BinaryFile'; import { Carousel } from 'carousel/Carousel'; import { Gluejar } from '@charliewilco/gluejar' @@ -17,6 +18,7 @@ export function AddTopic({ contentKey }) { const attachImage = useRef(null); const attachAudio = useRef(null); const attachVideo = useRef(null); + const attachBinary = useRef(null); const msg = useRef(); const keyDown = (e) => { @@ -66,6 +68,11 @@ export function AddTopic({ contentKey }) { attachVideo.current.value = ''; }; + const onSelectBinary = (e) => { + actions.addBinary(e.target.files[0]); + attachBinary.current.value = ''; + }; + const renderItem = (item, index) => { if (item.image) { return @@ -76,6 +83,9 @@ export function AddTopic({ contentKey }) { if (item.video) { return actions.setPosition(index, pos)} url={item.url} /> } + if (item.binary) { + return actions.setLabel(index, label)} label={item.label} extension={item.extension} url={item.url} /> + } return <> }; @@ -110,6 +120,7 @@ export function AddTopic({ contentKey }) { onSelectImage(e)} style={{display: 'none'}}/> onSelectAudio(e)} style={{display: 'none'}}/> onSelectVideo(e)} style={{display: 'none'}}/> + onSelectBinary(e)} style={{display: 'none'}}/> { state.assets.length > 0 && (
@@ -136,9 +147,10 @@ export function AddTopic({ contentKey }) {
)} - { (state.enableImage || state.enableVideo || state.enableAudio) && ( -
- )} +
attachBinary.current.click()}> + +
+
diff --git a/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.jsx b/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.jsx new file mode 100644 index 00000000..40d8ebdf --- /dev/null +++ b/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.jsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import { Input } from 'antd'; +import ReactResizeDetector from 'react-resize-detector'; +import { BinaryFileWrapper } from './BinaryFile.styled'; + +export function BinaryFile({ url, extension, label, onLabel }) { + + const [width, setWidth] = useState(0); + + return ( + + + {({ height }) => { + if (height !== width) { + setWidth(height); + } + return
+ }} + +
+
{ extension }
+
+ onLabel(e.target.value)} /> +
+
+ + ) +} + diff --git a/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.styled.js b/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.styled.js new file mode 100644 index 00000000..6101a3c3 --- /dev/null +++ b/net/web/src/session/conversation/addTopic/binaryFile/BinaryFile.styled.js @@ -0,0 +1,50 @@ +import styled from 'styled-components'; +import Colors from 'constants/Colors'; + +export const BinaryFileWrapper = styled.div` + position: relative; + height: 100%; + + .player { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: #aaaaaa; + } + + .background { + height: 100%; + object-fit: contain; + } + + .extension { + font-size: 32px; + color: ${Colors.white}; + } + + .label { + bottom: 0; + position: absolute; + width: 100%; + overflow: hidden; + text-align: center; + color: white; + background-color: #cccccc; + } + + .control { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } +`; + + diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 98255b17..165ff5b0 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -128,6 +128,13 @@ export function useAddTopic(contentKey) { asset.label = ''; addAsset(asset); }, + addBinary: async (binary) => { + const asset = await setUrl(binary); + asset.binary = binary; + asset.extension = binary.name.split('.').pop().toUpperCase(); + asset.label = binary.name.slice(0, -1 * (asset.extension.length + 1)); + addAsset(asset); + }, setLabel: (index, label) => { updateAsset(index, { label }); }, diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index fd10666d..57a775a2 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -2,6 +2,7 @@ import { TopicItemWrapper } from './TopicItem.styled'; import { VideoAsset } from './videoAsset/VideoAsset'; import { AudioAsset } from './audioAsset/AudioAsset'; import { ImageAsset } from './imageAsset/ImageAsset'; +import { BinaryAsset } from './binaryAsset/BinaryAsset'; import { Logo } from 'logo/Logo'; import { Space, Skeleton, Button, Modal, Input } from 'antd'; import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons'; @@ -61,6 +62,9 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) { if (asset.type === 'audio') { return } + if (asset.type === 'binary') { + return + } return <> } diff --git a/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.jsx b/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.jsx new file mode 100644 index 00000000..f9046d6b --- /dev/null +++ b/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.jsx @@ -0,0 +1,42 @@ +import React, { useState, useRef } from 'react'; +import { Progress, Modal, Spin } from 'antd'; +import ReactResizeDetector from 'react-resize-detector'; +import { DownloadOutlined } from '@ant-design/icons'; +import { BinaryAssetWrapper } from './BinaryAsset.styled'; +import { useBinaryAsset } from './useBinaryAsset.hook'; +import Colors from 'constants/Colors'; + +import background from 'images/audio.png'; + +export function BinaryAsset({ asset }) { + + const [width, setWidth] = useState(0); + + const { actions, state } = useBinaryAsset(asset); + + return ( + + + {({ height }) => { + if (height !== width) { + setWidth(height); + } + return
+ }} + +
+
{ asset.label }
+
+ +
+
+ { state.unsealing && ( + + )} +
+
{ asset.extension }
+
+ + ) +} + diff --git a/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.styled.js b/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.styled.js new file mode 100644 index 00000000..33eaf047 --- /dev/null +++ b/net/web/src/session/conversation/topicItem/binaryAsset/BinaryAsset.styled.js @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import Colors from 'constants/Colors'; + +export const BinaryAssetWrapper = styled.div` + position: relative; + height: 100%; + + .player { + position: absolute; + top: 0; + left: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #888888; + } + + .background { + height: 100%; + object-fit: contain; + } + + .unsealing { + padding-left: 8px; + padding-right: 8px; + width: 64px; + height: 16px; + } + + .label { + width: 100%; + overflow: hidden; + text-align: center; + color: white; + font-size: 14px; + text-overflow: ellipsis; + padding: 4px; + } + + .control { + width: 100%; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .extension { + width: 100%; + overflow: hidden; + text-align: center; + color: white; + font-size: 24px; + } +`; + diff --git a/net/web/src/session/conversation/topicItem/binaryAsset/useBinaryAsset.hook.js b/net/web/src/session/conversation/topicItem/binaryAsset/useBinaryAsset.hook.js new file mode 100644 index 00000000..e8005ce7 --- /dev/null +++ b/net/web/src/session/conversation/topicItem/binaryAsset/useBinaryAsset.hook.js @@ -0,0 +1,61 @@ +import { useState, useRef } from 'react'; + +export function useBinaryAsset(asset) { + + const index = useRef(0); + const updated = useRef(false); + + const [state, setState] = useState({ + error: false, + unsealing: false, + block: 0, + total: 0, + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + download: async () => { + if (asset.encrypted) { + if (!state.unsealing) { + try { + updateState({ error: false, unsealing: true }); + const view = index.current; + updateState({ active: true, ready: false, error: false, loading: true, url: null }); + const blob = await asset.getDecryptedBlob(() => view !== index.current, (block, total) => { + if (!updated.current || block == total) { + updated.current = true; + setTimeout(() => { + updated.current = false; + }, 1000); + updateState({ block, total }); + } + }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = `${asset.label}.${asset.extension.toLowerCase()}` + link.href = url; + link.click(); + URL.revokeObjectURL(url); + } + catch (err) { + console.log(err); + updateState({ error: true }); + } + updateState({ unsealing: false }); + } + } + else { + const link = document.createElement("a"); + link.download = `${asset.label}.${asset.extension.toLowerCase()}` + link.href = asset.data; + link.click(); + } + }, + }; + + return { state, actions }; +} + diff --git a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js index 9bb70a51..9d04cef0 100644 --- a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js +++ b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js @@ -29,7 +29,7 @@ export function useTopicItem(topic, contentKey) { topic.assets.forEach(asset => { if (asset.encrypted) { const encrypted = true; - const { type, thumb, label, parts } = asset.encrypted; + const { type, thumb, label, extension, parts } = asset.encrypted; const getDecryptedBlob = async (abort, progress) => { let pos = 0; let len = 0; @@ -59,7 +59,7 @@ export function useTopicItem(topic, contentKey) { } return new Blob([data]); } - assets.push({ type, thumb, label, encrypted, getDecryptedBlob }); + assets.push({ type, thumb, label, extension, encrypted, getDecryptedBlob }); } else { const encrypted = false @@ -82,6 +82,13 @@ export function useTopicItem(topic, contentKey) { const full = topic.assetUrl(asset.audio.full, topic.id); assets.push({ type, label, encrypted, full }); } + else if (asset.binary) { + const type = 'binary'; + const label = asset.binary.label; + const extension = asset.binary.extension; + const data = topic.assetUrl(asset.binary.data, topic.id); + assets.push({ type, label, extension, encrypted, data }); + } } }); updateState({ assets });