mirror of
https://github.com/balzack/databag.git
synced 2025-05-04 23:45:21 +00:00
sending video attachments
This commit is contained in:
parent
21b4d1a4b6
commit
0c2de2e00d
@ -28,6 +28,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-easy-crop": "^5.0.8",
|
"react-easy-crop": "^5.0.8",
|
||||||
|
"react-image-file-resizer": "^0.4.8",
|
||||||
"react-resize-detector": "^11.0.1",
|
"react-resize-detector": "^11.0.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"redaxios": "^0.5.1",
|
"redaxios": "^0.5.1",
|
||||||
|
@ -245,9 +245,11 @@ export function useContent() {
|
|||||||
addTopic: async (sealed: boolean, subject: string, contacts: string[]) => {
|
addTopic: async (sealed: boolean, subject: string, contacts: string[]) => {
|
||||||
const content = app.state.session.getContent()
|
const content = app.state.session.getContent()
|
||||||
if (sealed) {
|
if (sealed) {
|
||||||
return await content.addChannel(true, 'sealed', { subject }, contacts)
|
const topic = await content.addChannel(true, 'sealed', { subject }, contacts)
|
||||||
|
return topic.id;
|
||||||
} else {
|
} else {
|
||||||
return await content.addChannel(false, 'superbasic', { subject }, contacts)
|
const topic = await content.addChannel(false, 'superbasic', { subject }, contacts)
|
||||||
|
return topic.id;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { Divider, Text, Textarea, ActionIcon, Loader } from '@mantine/core'
|
|||||||
import { Message } from '../message/Message';
|
import { Message } from '../message/Message';
|
||||||
import { modals } from '@mantine/modals'
|
import { modals } from '@mantine/modals'
|
||||||
import { ImageFile } from './imageFile/ImageFile';
|
import { ImageFile } from './imageFile/ImageFile';
|
||||||
|
import { VideoFile } from './videoFile/VideoFile';
|
||||||
|
|
||||||
const PAD_HEIGHT = (1024 - 64);
|
const PAD_HEIGHT = (1024 - 64);
|
||||||
const LOAD_DEBOUNCE = 1000;
|
const LOAD_DEBOUNCE = 1000;
|
||||||
@ -26,6 +27,19 @@ export function Conversation() {
|
|||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const { state, actions } = useConversation();
|
const { state, actions } = useConversation();
|
||||||
const attachImage = useRef({ click: ()=>{} } as HTMLInputElement);
|
const attachImage = useRef({ click: ()=>{} } as HTMLInputElement);
|
||||||
|
const attachVideo = useRef({ click: ()=>{} } as HTMLInputElement);
|
||||||
|
|
||||||
|
const addImage = (image: File | undefined) => {
|
||||||
|
if (image) {
|
||||||
|
actions.addImage(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addVideo = (video: File | undefined) => {
|
||||||
|
if (video) {
|
||||||
|
actions.addVideo(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!sending) {
|
if (!sending) {
|
||||||
@ -95,11 +109,16 @@ export function Conversation() {
|
|||||||
const media = state.assets.map((asset, index: number) => {
|
const media = state.assets.map((asset, index: number) => {
|
||||||
if (asset.type === 'image') {
|
if (asset.type === 'image') {
|
||||||
return <ImageFile key={index} source={asset.file} />
|
return <ImageFile key={index} source={asset.file} />
|
||||||
|
} else if (asset.type === 'video') {
|
||||||
|
return <VideoFile key={index} source={asset.file} thumbPosition={(position: number) => actions.setThumbPosition(index, position)} disabled={sending} />
|
||||||
} else {
|
} else {
|
||||||
return <div key={index}></div>
|
return <div key={index}></div>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(state.assets);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.conversation}>
|
<div className={classes.conversation}>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
@ -159,33 +178,34 @@ export function Conversation() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
<div className={classes.add}>
|
<div className={classes.add}>
|
||||||
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => actions.addImage(e.target.files[0])} style={{display: 'none'}}/>
|
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => addImage(e.target?.files?.[0])} style={{display: 'none'}}/>
|
||||||
|
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => addVideo(e.target?.files?.[0])} style={{display: 'none'}}/>
|
||||||
<div className={classes.files}>
|
<div className={classes.files}>
|
||||||
{ media }
|
{ media }
|
||||||
</div>
|
</div>
|
||||||
<Textarea className={classes.message} placeholder={state.strings.newMessage} value={state.message} onChange={(event) => actions.setMessage(event.currentTarget.value)} disabled={!state.detail || state.detail.locked} />
|
<Textarea className={classes.message} placeholder={state.strings.newMessage} value={state.message} onChange={(event) => actions.setMessage(event.currentTarget.value)} disabled={!state.detail || state.detail.locked || sending} />
|
||||||
<div className={classes.controls}>
|
<div className={classes.controls}>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked} onClick={() => attachImage.current.click()}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending} onClick={() => attachImage.current.click()}>
|
||||||
<IconCamera />
|
<IconCamera />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending} onClick={() => attachVideo.current.click()}>
|
||||||
<IconVideo />
|
<IconVideo />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending}>
|
||||||
<IconDisc />
|
<IconDisc />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending}>
|
||||||
<IconFile />
|
<IconFile />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Divider size="sm" orientation="vertical" />
|
<Divider size="sm" orientation="vertical" />
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending}>
|
||||||
<IconTextSize />
|
<IconTextSize />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked}>
|
<ActionIcon className={classes.attach} variant="light" disabled={!state.detail || state.detail.locked || sending}>
|
||||||
<IconTextColor />
|
<IconTextColor />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<div className={classes.send}>
|
<div className={classes.send}>
|
||||||
<ActionIcon className={classes.attach} variant="light" disabled={!state.message || !state.detail || state.detail.locked} onClick={sendMessage} loading={sending}>
|
<ActionIcon className={classes.attach} variant="light" disabled={(!state.message && state.assets.length === 0) || !state.detail || state.detail.locked || sending} onClick={sendMessage} loading={sending}>
|
||||||
<IconSend />
|
<IconSend />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 64px;
|
height: 92px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,81 @@
|
|||||||
import { useState, useContext, useEffect, useRef } from 'react'
|
import { useState, useContext, useEffect, useRef } from 'react'
|
||||||
import { AppContext } from '../context/AppContext'
|
import { AppContext } from '../context/AppContext'
|
||||||
import { DisplayContext } from '../context/DisplayContext'
|
import { DisplayContext } from '../context/DisplayContext'
|
||||||
import { Focus, FocusDetail, Topic, Profile, Card, AssetSource, HostingMode, TransformType } from 'databag-client-sdk'
|
import { Focus, FocusDetail, Topic, Profile, Card, AssetType, AssetSource, HostingMode, TransformType } from 'databag-client-sdk'
|
||||||
import { ContextType } from '../context/ContextType'
|
import { ContextType } from '../context/ContextType'
|
||||||
|
import Resizer from "react-image-file-resizer";
|
||||||
|
import failed from '../images/failed.png'
|
||||||
|
|
||||||
const img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'
|
const IMAGE_SCALE_SIZE = (128 * 1024);
|
||||||
|
const GIF_TYPE = 'image/gif';
|
||||||
|
const WEBP_TYPE = 'image/webp';
|
||||||
const LOAD_DEBOUNCE = 1000;
|
const LOAD_DEBOUNCE = 1000;
|
||||||
|
|
||||||
|
function getImageThumb(file: File) {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
if ((file.type === GIF_TYPE || file.type === WEBP_TYPE) && file.size < IMAGE_SCALE_SIZE) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = function () {
|
||||||
|
resolve(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.onerror = function (error) {
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Resizer.imageFileResizer(file, 192, 192, 'JPEG', 50, 0,
|
||||||
|
uri => {
|
||||||
|
resolve(uri as string);
|
||||||
|
}, 'base64', 128, 128 );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoThumb(file: File, position: number) {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
var video = document.createElement("video");
|
||||||
|
var timeupdate = function (ev: any) {
|
||||||
|
video.removeEventListener("timeupdate", timeupdate);
|
||||||
|
video.pause();
|
||||||
|
setTimeout(() => {
|
||||||
|
var canvas = document.createElement("canvas");
|
||||||
|
if (!canvas) {
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
if (video.videoWidth > video.videoHeight) {
|
||||||
|
canvas.width = 192;
|
||||||
|
canvas.height = Math.floor((192 * video.videoHeight / video.videoWidth));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas.height = 192;
|
||||||
|
canvas.width = Math.floor((192 * video.videoWidth / video.videoHeight));
|
||||||
|
}
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
var image = canvas.toDataURL("image/jpeg", 0.75);
|
||||||
|
resolve(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.remove();
|
||||||
|
video.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
video.addEventListener("timeupdate", timeupdate);
|
||||||
|
video.preload = "metadata";
|
||||||
|
video.src = url;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.currentTime = position;
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useConversation() {
|
export function useConversation() {
|
||||||
const app = useContext(AppContext) as ContextType
|
const app = useContext(AppContext) as ContextType
|
||||||
const display = useContext(DisplayContext) as ContextType
|
const display = useContext(DisplayContext) as ContextType
|
||||||
@ -38,6 +106,13 @@ export function useConversation() {
|
|||||||
setState((s) => ({ ...s, ...value }))
|
setState((s) => ({ ...s, ...value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateAsset = (index, value) => {
|
||||||
|
setState((s) => {
|
||||||
|
s.assets[index] = { ...s.assets[index], ...value };
|
||||||
|
return { ...s };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { layout, strings } = display.state
|
const { layout, strings } = display.state
|
||||||
updateState({ layout, strings })
|
updateState({ layout, strings })
|
||||||
@ -113,6 +188,9 @@ export function useConversation() {
|
|||||||
setMessage: (message: string) => {
|
setMessage: (message: string) => {
|
||||||
updateState({ message });
|
updateState({ message });
|
||||||
},
|
},
|
||||||
|
setThumbPosition: (index: number, position: number) => {
|
||||||
|
updateAsset(index, { position });
|
||||||
|
},
|
||||||
more: async () => {
|
more: async () => {
|
||||||
const focus = app.state.focus;
|
const focus = app.state.focus;
|
||||||
if (focus) {
|
if (focus) {
|
||||||
@ -129,33 +207,101 @@ export function useConversation() {
|
|||||||
const focus = app.state.focus;
|
const focus = app.state.focus;
|
||||||
const sealed = state.detail?.sealed ? true : false;
|
const sealed = state.detail?.sealed ? true : false;
|
||||||
if (focus) {
|
if (focus) {
|
||||||
const subject = (assets: {assetId: string, appId: string}[]) => ({ text: state.message });
|
const sources = [] as AssetSource[];
|
||||||
|
const uploadAssets = state.assets.map(asset => {
|
||||||
|
if (asset.type === 'image') {
|
||||||
|
if (sealed) {
|
||||||
|
sources.push({ type: AssetType.Image, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Thumb, appId: `it${sources.length}`, thumb: () => getImageThumb(asset.file) },
|
||||||
|
{ type: TransformType.Copy, appId: `ic${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { encrypted: { type: 'image', thumb: `it${sources.length-1}`, parts: `ic${sources.length-1}` } };
|
||||||
|
} else {
|
||||||
|
sources.push({ type: AssetType.Image, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Thumb, appId: `it${sources.length}` },
|
||||||
|
{ type: TransformType.Copy, appId: `ic${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { image: { thumb: `it${sources.length-1}`, full: `ic${sources.length-1}` } };
|
||||||
|
}
|
||||||
|
} else if (asset.type === 'video') {
|
||||||
|
if (sealed) {
|
||||||
|
sources.push({ type: AssetType.Video, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Thumb, appId: `vt${sources.length}`, thumb: () => getVideoThumb(asset.file, asset.position) },
|
||||||
|
{ type: TransformType.Copy, appId: `vc${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { encrypted: { type: 'video', thumb: `vt${sources.length-1}`, parts: `vc${sources.length-1}` } };
|
||||||
|
} else {
|
||||||
|
sources.push({ type: AssetType.Video, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Thumb, appId: `vt${sources.length}`, position: asset.position},
|
||||||
|
{ type: TransformType.HighQuality, appId: `vh${sources.length}` },
|
||||||
|
{ type: TransformType.LowQuality, appId: `vl${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { video: { thumb: `vt${sources.length-1}`, hd: `vh${sources.length-1}`, lq: `vl${sources.length-1}` } };
|
||||||
|
}
|
||||||
|
} else if (asset.type === 'audio') {
|
||||||
|
if (sealed) {
|
||||||
|
sources.push({ type: AssetType.Audio, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Copy, appId: `ac${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { encrypted: { type: 'audio', parts: `ac${sources.length-1}` } };
|
||||||
|
} else {
|
||||||
|
sources.push({ type: AssetType.Video, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Copy, appId: `ac${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { audio: { full: `ac${sources.length-1}` } };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sealed) {
|
||||||
|
sources.push({ type: AssetType.Binary, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Copy, appId: `bc${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { encrypted: { type: 'binary', parts: `bc${sources.length-1}` } };
|
||||||
|
} else {
|
||||||
|
sources.push({ type: AssetType.Binary, source: asset.file, transforms: [
|
||||||
|
{ type: TransformType.Copy, appId: `bc${sources.length}` }
|
||||||
|
]});
|
||||||
|
return { binary: { data: `bc${sources.length-1}` } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const subject = (uploaded: {assetId: string, appId: string}[]) => {
|
||||||
|
const assets = uploadAssets.map(asset => {
|
||||||
|
if(asset.encrypted) {
|
||||||
|
const type = asset.encrypted.type;
|
||||||
|
const thumb = uploaded.find(upload => upload.appId === asset.encrypted.thumb)?.assetId;
|
||||||
|
const parts = uploaded.find(upload => upload.appId === asset.encrypted.parts)?.assetId;
|
||||||
|
return { encrypted: { type, thumb, parts }};
|
||||||
|
} else if (asset.image) {
|
||||||
|
const thumb = uploaded.find(upload => upload.appId === asset.image.thumb)?.assetId;
|
||||||
|
const full = uploaded.find(upload => upload.appId === asset.image.full)?.assetId;
|
||||||
|
return { image: { thumb, full } };
|
||||||
|
} else if(asset.video) {
|
||||||
|
const thumb = uploaded.find(upload => upload.appId === asset.video.thumb)?.assetId;
|
||||||
|
const hd = uploaded.find(upload => upload.appId === asset.video.hd)?.assetId;
|
||||||
|
const lq = uploaded.find(upload => upload.appId === asset.video.lq)?.assetId;
|
||||||
|
return { video: { thumb, hd, lq } };
|
||||||
|
} else if (asset.audio) {
|
||||||
|
const full = uploaded.find(upload => upload.appId === asset.audio.full)?.assetId;
|
||||||
|
return { audio: { full } };
|
||||||
|
} else {
|
||||||
|
const data = uploaded.find(upload => upload.appId === asset.binary.data)?.assetId;
|
||||||
|
return { binary: { data } };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { text: state.message, assets };
|
||||||
|
}
|
||||||
const progress = (precent: number) => {};
|
const progress = (precent: number) => {};
|
||||||
await focus.addTopic(sealed, sealed ? 'sealedtopic' : 'superbasictopic', subject, [], progress);
|
await focus.addTopic(sealed, sealed ? 'sealedtopic' : 'superbasictopic', subject, sources, progress);
|
||||||
updateState({ message: '' });
|
updateState({ message: '', assets: [] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addImage: (file: File) => {
|
addImage: (file: File) => {
|
||||||
const type = 'image';
|
const type = 'image';
|
||||||
updateState({ assets: [ ...state.assets, { type, file } ]});
|
updateState({ assets: [ ...state.assets, { type, file } ]});
|
||||||
},
|
},
|
||||||
add: async (file: File) => {
|
addVideo: (file: File) => {
|
||||||
const focus = app.state.focus;
|
const type = 'video';
|
||||||
if (focus) {
|
updateState({ assets: [ ...state.assets, { type, file } ]});
|
||||||
const asset = {
|
|
||||||
name: 'topic',
|
|
||||||
mimeType: 'image',
|
|
||||||
extension: 'jpg',
|
|
||||||
source: file,
|
|
||||||
transforms: [ {type: TransformType.Thumb, thumb: ()=>(img), appId: '1'}, {type: TransformType.Copy, appId: '2'}],
|
|
||||||
}
|
|
||||||
const topicId = await focus.addTopic(true, 'sealedtopic', (assets: {assetId: string, appId: string}[])=>{
|
|
||||||
console.log(assets);
|
|
||||||
return { text: 'almost done', assets: [{ encrypted: { type: 'image', thumb: '0', parts: '1' } }] };
|
|
||||||
}, [asset], (percent: number)=>{
|
|
||||||
console.log(percent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
.asset {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 16px;
|
||||||
|
height: 92px;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
width: auto;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
app/client/web/src/conversation/videoFile/VideoFile.tsx
Normal file
53
app/client/web/src/conversation/videoFile/VideoFile.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { ActionIcon, Image } from '@mantine/core'
|
||||||
|
import { useVideoFile } from './useVideoFile.hook';
|
||||||
|
import classes from './VideoFile.module.css'
|
||||||
|
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
|
||||||
|
|
||||||
|
export function VideoFile({ source, thumbPosition, disabled }: {source: File, thumbPosition: (position: number)=>void, disabled: boolean}) {
|
||||||
|
const { state, actions } = useVideoFile(source);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const position = useRef(0);
|
||||||
|
const player = useRef();
|
||||||
|
|
||||||
|
const seek = (offset: number) => {
|
||||||
|
if (player.current) {
|
||||||
|
const len = player.current.duration;
|
||||||
|
if (len > 16) {
|
||||||
|
position.current += offset * Math.floor(len / 16);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
position.current += offset;
|
||||||
|
}
|
||||||
|
if (position.current < 0 || position.current >= len) {
|
||||||
|
position.current = 0;
|
||||||
|
}
|
||||||
|
thumbPosition(position.current);
|
||||||
|
player.current.currentTime = position.current;
|
||||||
|
player.current.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPause = () => {
|
||||||
|
player.current.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.asset}>
|
||||||
|
{ state.videoUrl && (
|
||||||
|
<video ref={player} muted onLoadedMetadata={() => setLoaded(true)} onPlay={onPause} src={state.videoUrl} width={'auto'} height={'100%'} playsinline="true" />
|
||||||
|
)}
|
||||||
|
{ loaded && !disabled && (
|
||||||
|
<ActionIcon className={classes.right} variant="light" onClick={() => seek(1)}>
|
||||||
|
<IconChevronRight />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
{ loaded && !disabled && (
|
||||||
|
<ActionIcon className={classes.left} variant="light" onClick={() => seek(-1)}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export function useVideoFile(source: File) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
videoUrl: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const updateState = (value: any) => {
|
||||||
|
setState((s) => ({ ...s, ...value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const videoUrl = URL.createObjectURL(source);
|
||||||
|
updateState({ videoUrl });
|
||||||
|
return () => { URL.revokeObjectURL(videoUrl) };
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state, actions }
|
||||||
|
}
|
@ -60,6 +60,12 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.incomplete {
|
||||||
|
margin-left: 72px;
|
||||||
|
margin-right: 32px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -9,7 +9,6 @@ import { VideoAsset } from './videoAsset/VideoAsset';
|
|||||||
import { BinaryAsset } from './binaryAsset/BinaryAsset';
|
import { BinaryAsset } from './binaryAsset/BinaryAsset';
|
||||||
import type { MediaAsset } from '../conversation/Conversation';
|
import type { MediaAsset } from '../conversation/Conversation';
|
||||||
import { useMessage } from './useMessage.hook';
|
import { useMessage } from './useMessage.hook';
|
||||||
import failed from '../images/failed.png'
|
|
||||||
import { IconForbid, IconTrash, IconEdit, IconAlertSquareRounded, IconChevronLeft, IconChevronRight, IconFileAlert } from '@tabler/icons-react';
|
import { IconForbid, IconTrash, IconEdit, IconAlertSquareRounded, IconChevronLeft, IconChevronRight, IconFileAlert } from '@tabler/icons-react';
|
||||||
import { useResizeDetector } from 'react-resize-detector';
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
|
|
||||||
@ -126,7 +125,9 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !locked && media.length > 0 && transform === 'incomplete' && (
|
{ !locked && media.length > 0 && transform === 'incomplete' && (
|
||||||
<Skeleton height={64} circle mb="xl" />
|
<div className={classes.incomplete}>
|
||||||
|
<Skeleton height={64} circle mb="xl" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !locked && media.length > 0 && transform !== 'complete' && transform !== 'incomplete' && (
|
{ !locked && media.length > 0 && transform !== 'complete' && transform !== 'incomplete' && (
|
||||||
<div className={classes.failed}>
|
<div className={classes.failed}>
|
||||||
|
@ -26,7 +26,7 @@ export function useMessage() {
|
|||||||
const now = Math.floor((new Date()).getTime() / 1000)
|
const now = Math.floor((new Date()).getTime() / 1000)
|
||||||
const date = new Date(created * 1000);
|
const date = new Date(created * 1000);
|
||||||
const offset = now - created;
|
const offset = now - created;
|
||||||
if(offset < 86400) {
|
if(offset < 43200) {
|
||||||
if (state.timeFormat === '12h') {
|
if (state.timeFormat === '12h') {
|
||||||
return date.toLocaleTimeString("en-US", {hour: 'numeric', minute:'2-digit'});
|
return date.toLocaleTimeString("en-US", {hour: 'numeric', minute:'2-digit'});
|
||||||
}
|
}
|
||||||
|
13234
app/client/web/yarn.lock
13234
app/client/web/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -450,8 +450,9 @@ export class FocusModule implements Focus {
|
|||||||
transforms.push('icopy;photo');
|
transforms.push('icopy;photo');
|
||||||
transformMap.set('icopy;photo', transform.appId);
|
transformMap.set('icopy;photo', transform.appId);
|
||||||
} else if (transform.type === TransformType.Thumb && asset.type === AssetType.Video) {
|
} else if (transform.type === TransformType.Thumb && asset.type === AssetType.Video) {
|
||||||
transforms.push('vthumb;video');
|
const transformKey = `vthumb;video;${ transform.position ? transform.position : 0}`;
|
||||||
transformMap.set('vthumb;video', transform.appId);
|
transforms.push(transformKey);
|
||||||
|
transformMap.set(transformKey, transform.appId);
|
||||||
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Video) {
|
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Video) {
|
||||||
transforms.push('vcopy;video');
|
transforms.push('vcopy;video');
|
||||||
transformMap.set('vcopy;video', transform.appId);
|
transformMap.set('vcopy;video', transform.appId);
|
||||||
@ -487,7 +488,7 @@ export class FocusModule implements Focus {
|
|||||||
}
|
}
|
||||||
if (transformMap.has(transformAsset.transform)) {
|
if (transformMap.has(transformAsset.transform)) {
|
||||||
const appId = transformMap.get(transformAsset.transform) || '' //or to make build happy
|
const appId = transformMap.get(transformAsset.transform) || '' //or to make build happy
|
||||||
appAsset.push({appId, assetId: transformAsset.assetId });
|
appAsset.push({appId, assetId: assetItem.assetId });
|
||||||
assetItems.push(assetItem);
|
assetItems.push(assetItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user