Merge branch 'main' into fdroid

This commit is contained in:
Roland Osborne 2023-08-14 14:31:46 -07:00
commit fda8ee6d65
65 changed files with 788 additions and 87 deletions

View File

@ -2,7 +2,7 @@
[![contribute.design](https://contribute.design/api/shield/balzack/databag)](https://contribute.design/balzack/databag)
<div align="center">
<a href="#"><img src="/doc/icon.png" width="8%" style="border-radius:50%"></a>
<a href="#"><img src="/doc/icon_v2.png" width="8%" style="border-radius:50%"></a>
<h3 align="center">Databag</h3>
<p align="center">Communication for the Decentralized Web</p>
</div>
@ -89,6 +89,8 @@ Instructions for installing without a container on a Raspberry Pi Zero are [here
Instructions for installing without a container in AWS are [here](/doc/aws.md).
1-click installs in [CapRover](https://caprover.com/), [CasaOS](https://casaos.io), [Unraid](https://unraid.net/), [Runtipi](https://www.runtipi.io/)
## Audio and Video Calls
Databag provides audio and video calling and relies on a STUN/TURN relay server for NAT traversal. Testing was done with both [cuturn](https://github.com/coturn/coturn) and [pion](https://github.com/pion/turn) and should work with any implementation. Instructions for installing a coturn server are provided [here](https://gabrieltanner.org/blog/turn-server/).

View File

@ -622,7 +622,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.17;
MARKETING_VERSION = 1.19;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -656,7 +656,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.17;
MARKETING_VERSION = 1.19;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,103 +1,103 @@
{
"images" : [
{
"filename" : "40.png",
"filename" : "_40 2.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"filename" : "_60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "58.png",
"filename" : "_58 1.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"filename" : "_87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"filename" : "_80 1.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"filename" : "_120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "120 1.png",
"filename" : "_120 1.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"filename" : "_180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "40 1.png",
"filename" : "_40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29 1.png",
"filename" : "_29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58 1.png",
"filename" : "_58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40 2.png",
"filename" : "_40 1.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80 1.png",
"filename" : "_80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "76 1.png",
"filename" : "_76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152 1.png",
"filename" : "_152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167 1.png",
"filename" : "_167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"filename" : "_1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -163,7 +163,7 @@ async function upload(entry, update, complete) {
entry.active = {};
try {
if (file.encrypted) {
const { data, type, mime, size, getEncryptedBlock, position } = file;
const { data, type, mime, size, getEncryptedBlock, position, label, extension } = file;
const thumb = await getThumb(data, type, mime, size, position);
const parts = [];
for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) {
@ -182,17 +182,13 @@ async function upload(entry, update, complete) {
parts.push({ blockIv, partId: part.data.assetId });
}
entry.assets.push({
encrypted: { type, thumb, parts }
encrypted: { type, label, extension, thumb, parts }
});
}
else if (file.type === 'image') {
const formData = new FormData();
if (file.data.startsWith('file:')) {
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
}
else {
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
}
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"]));
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
@ -212,12 +208,8 @@ async function upload(entry, update, complete) {
}
else if (file.type === 'video') {
const formData = new FormData();
if (file.data.startsWith('file:')) {
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
}
else {
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
}
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
let thumb = 'vthumb;video;' + file.position;
let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb]));
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
@ -239,12 +231,8 @@ async function upload(entry, update, complete) {
}
else if (file.type === 'audio') {
const formData = new FormData();
if (file.data.startsWith('file:')) {
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
}
else {
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
}
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
let transform = encodeURIComponent(JSON.stringify(["acopy;audio"]));
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
@ -262,6 +250,27 @@ async function upload(entry, update, complete) {
}
});
}
else if (file.type === 'binary') {
const formData = new FormData();
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
let asset = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}&body=multipart`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
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);
}

View File

@ -12,6 +12,7 @@ import ColorPicker from 'react-native-wheel-color-picker'
import { VideoFile } from './videoFile/VideoFile';
import { AudioFile } from './audioFile/AudioFile';
import { ImageFile } from './imageFile/ImageFile';
import { BinaryFile } from './binaryFile/BinaryFile';
export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
@ -80,15 +81,28 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
const addAudio = async () => {
try {
const audio = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
type: DocumentPicker.types.audio,
})
actions.addAudio(audio.fileCopyUri, audio.name.replace(/\.[^/.]+$/, ""));
} catch (err) {
console.log(err);
}
const audio = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
type: DocumentPicker.types.audio,
})
actions.addAudio(audio.fileCopyUri, audio.name.replace(/\.[^/.]+$/, ""));
} catch (err) {
console.log(err);
}
}
const addBinary = async () => {
try {
const binary = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
type: DocumentPicker.types.allFiles,
})
actions.addBinary(binary.fileCopyUri, binary.name);
} catch (err) {
console.log(err);
}
}
const remove = (item) => {
@ -123,7 +137,13 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
if (item.type === 'audio') {
return (
<AudioFile path={item.data} label={item.label} remove={() => remove(item)}
setLabel={(label) => actions.setAudioLabel(item.key, label)} />
setLabel={(label) => actions.setLabel(item.key, label)} />
)
}
if (item.type === 'binary') {
return (
<BinaryFile path={item.data} label={item.label} extension={item.extension} remove={() => remove(item)}
setLabel={(label) => actions.setLabel(item.key, label)} />
)
}
else {
@ -171,9 +191,10 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
<MatIcons name="music-box-outline" size={20} color={Colors.text} />
</TouchableOpacity>
)}
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
<View style={styles.divider} />
)}
<TouchableOpacity style={styles.addButton} onPress={addBinary}>
<MatIcons name="all-inclusive-box-outline" size={20} color={Colors.text} />
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.addButton} onPress={actions.showFontSize}>
<MatIcons name="format-size" size={20} color={Colors.text} />
</TouchableOpacity>

View File

@ -0,0 +1,14 @@
import { Image, Text, View, TextInput, TouchableOpacity } from 'react-native';
import { styles } from './BinaryFile.styled';
export function BinaryFile({ path, remove, extension, label, setLabel }) {
return (
<TouchableOpacity style={styles.binary} onLongPress={remove}>
<TextInput style={ styles.input } value={ label } onChangeText={setLabel}
multiline={true} autoCapitalize={'none'} placeholder="Binary Label" />
<View style={styles.extension}>
<Text style={styles.label}>{ extension }</Text>
</View>
</TouchableOpacity>
)
}

View File

@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
binary: {
width: 92,
height: 92,
backgroundColor: '#888888',
borderRadius: 4,
marginRight: 16,
display: 'flex',
alignItems: 'center',
},
input: {
maxHeight: 50,
textAlign: 'center',
padding: 4,
color: 'white',
},
extension: {
flexGrow: 1,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'flex-end',
},
label: {
color: 'white',
fontSize: 24,
},
})

View File

@ -162,7 +162,19 @@ export function useAddTopic(contentKey) {
asset.type = 'audio';
asset.label = label;
updateState({ assets: [ ...state.assets, asset ] });
},
},
addBinary: async (data, name) => {
assetId.current++;
const asset = await setAsset(data);
asset.key = assetId.current;
asset.type = 'binary';
asset.extension = name.split('.').pop().toUpperCase();
asset.label = name.slice(0, -1 * (asset.extension.length + 1));
console.log(asset);
updateState({ assets: [ ...state.assets, asset ] });
},
setVideoPosition: (key, position) => {
updateState({ assets: state.assets.map((item) => {
if(item.key === key) {
@ -172,7 +184,7 @@ export function useAddTopic(contentKey) {
})
});
},
setAudioLabel: (key, label) => {
setLabel: (key, label) => {
updateState({ assets: state.assets.map((item) => {
if(item.key === key) {
return { ...item, label };

View File

@ -9,9 +9,11 @@ import avatar from 'images/avatar.png';
import { VideoThumb } from './videoThumb/VideoThumb';
import { AudioThumb } from './audioThumb/AudioThumb';
import { ImageThumb } from './imageThumb/ImageThumb';
import { BinaryThumb } from './binaryThumb/BinaryThumb';
import { ImageAsset } from './imageAsset/ImageAsset';
import { AudioAsset } from './audioAsset/AudioAsset';
import { VideoAsset } from './videoAsset/VideoAsset';
import { BinaryAsset } from './binaryAsset/BinaryAsset';
import Carousel from 'react-native-reanimated-carousel';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
@ -120,7 +122,10 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
<VideoThumb url={thumb.item.thumb} onAssetView={() => actions.showCarousel(thumb.index)} />
)}
{ thumb.item.type === 'audio' && (
<AudioThumb labe={thumb.item.label} onAssetView={() => actions.showCarousel(thumb.index)} />
<AudioThumb label={thumb.item.label} onAssetView={() => actions.showCarousel(thumb.index)} />
)}
{ thumb.item.type === 'binary' && (
<BinaryThumb label={thumb.item.label} extension={thumb.item.extension} onAssetView={() => actions.showCarousel(thumb.index)} />
)}
</View>
);
@ -234,6 +239,9 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
{ state.assets[index].type === 'audio' && (
<AudioAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
)}
{ state.assets[index].type === 'binary' && (
<BinaryAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
)}
</View>
)} />
</GestureHandlerRootView>

View File

@ -0,0 +1,56 @@
import { ActivityIndicator, Alert, View, Text, TouchableOpacity } from 'react-native';
import { useEffect, useRef } from 'react';
import Colors from 'constants/Colors';
import { useBinaryAsset } from './useBinaryAsset.hook';
import { styles } from './BinaryAsset.styled';
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import AntIcons from 'react-native-vector-icons/AntDesign';
export function BinaryAsset({ asset, dismiss }) {
const { state, actions } = useBinaryAsset();
const download = async () => {
try {
const ext = asset.extension.toLowerCase();
const url = asset.encrypted ? asset.decrypted : asset.data;
await actions.download(asset.label, ext, asset.encrypted, url);
}
catch (err) {
Alert.alert(
'Download Failed',
'Please try again.'
)
}
};
return (
<View style={{ ...styles.container, width: state.width, height: state.height }}>
<Text style={styles.label} ellipsizeMode='tail' numberOfLines={1}>{ asset.label }</Text>
<TouchableOpacity style={styles.close} onPress={dismiss}>
<MatIcons name="window-close" size={32} color={Colors.white} />
</TouchableOpacity>
<View style={styles.action}>
{ asset.encrypted && !asset.decrypted && (
<TouchableOpacity style={styles.loading} onPress={dismiss}>
<ActivityIndicator color={Colors.white} size="large" />
{ asset.total > 1 && (
<Text style={styles.decrypting}>{ asset.block } / { asset.total }</Text>
)}
</TouchableOpacity>
)}
{ !state.downloading && (!asset.encrypted || asset.decrypted) && (
<TouchableOpacity onPress={download}>
<AntIcons name="download" size={64} color={Colors.white} />
</TouchableOpacity>
)}
{ state.downloading && (
<ActivityIndicator color={Colors.white} size="large" />
)}
</View>
<Text style={styles.extension}>{ asset.extension }</Text>
</View>
);
}

View File

@ -0,0 +1,61 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
container: {
position: 'relative',
borderRadius: 8,
backgroundColor: Colors.grey,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
},
label: {
textAlign: 'center',
fontSize: 20,
paddingTop: 8,
paddingLeft: 48,
paddingRight: 48,
color: Colors.white,
},
action: {
display: 'flex',
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
},
copied: {
height: 32,
paddingTop: 8,
},
copiedText: {
color: Colors.white,
fontSize: 18,
},
extension: {
textAlign: 'center',
fontSize: 48,
paddingBottom: 8,
paddingLeft: 48,
paddingRight: 48,
color: Colors.white,
},
close: {
position: 'absolute',
top: 0,
right: 0,
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 8,
paddingRight: 8,
},
decrypting: {
fontVariant: ["tabular-nums"],
paddingTop: 16,
fontSize: 12,
color: '#888888',
},
})

View File

@ -0,0 +1,75 @@
import { useState, useRef, useEffect, useContext } from 'react';
import { ConversationContext } from 'context/ConversationContext';
import { Image } from 'react-native';
import { useWindowDimensions, Platform } from 'react-native';
import RNFetchBlob from "rn-fetch-blob";
import Share from 'react-native-share';
import RNFS from 'react-native-fs';
export function useBinaryAsset() {
const [state, setState] = useState({
width: 1,
height: 1,
downloading: false,
});
const dimensions = useWindowDimensions();
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
const { width, height } = dimensions;
if (width < height) {
updateState({ width, height: width });
}
else {
updateState({ widht: height, height });
}
}, [dimensions]);
const actions = {
download: async (label, extension, cached, url) => {
if (!state.downloading) {
try {
updateState({ downloading: true });
let src;
if (cached) {
src = url
}
else {
const blob = await RNFetchBlob.config({ fileCache: true }).fetch("GET", url);
src = blob.path();
}
const path = `${RNFetchBlob.fs.dirs.DocumentDir}`
const dst = `${path}/${label}.${extension.toLowerCase()}`
if (RNFetchBlob.fs.exists(dst)) {
RNFetchBlob.fs.unlink(dst);
}
await RNFetchBlob.fs.mv(src, dst);
try {
await Share.open({ url: `file://${dst}` })
}
catch (err) {
console.log(err);
}
RNFetchBlob.fs.unlink(dst);
updateState({ downloading: false });
}
catch (err) {
console.log(err);
updateState({ downloading: false });
throw new Error('download failed');
}
}
}
};
return { state, actions };
}

View File

@ -0,0 +1,20 @@
import { View, Text, Image } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { styles } from './BinaryThumb.styled';
import Colors from 'constants/Colors';
import AntIcons from 'react-native-vector-icons/AntDesign';
export function BinaryThumb({ label, extension, onAssetView }) {
return (
<TouchableOpacity activeOpacity={1} style={styles.canvas} onPress={onAssetView}>
<Text style={styles.label} ellipsizeMode='tail' numberOfLines={1}>{ label }</Text>
<View style={styles.action}>
<AntIcons name="download" size={28} color={Colors.white} />
</View>
<Text style={styles.extension}>{ extension }</Text>
</TouchableOpacity>
);
}

View File

@ -0,0 +1,29 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
canvas: {
borderRadius: 4,
width: 92,
height: 92,
marginRight: 16,
backgroundColor: Colors.grey,
display: 'flex',
alignItems: 'center',
},
action: {
flexGrow: 1,
},
label: {
textAlign: 'center',
color: Colors.white,
padding: 4,
fontSize: 14,
},
extension: {
textAlign: 'center',
color: Colors.white,
fontSize: 18,
}
})

View File

@ -61,8 +61,8 @@ export function useTopicItem(item, hosting, remove, contentKey) {
const asset = parsed[i];
if (asset.encrypted) {
const encrypted = true;
const { type, thumb, label, parts } = asset.encrypted;
assets.push({ type, thumb, label, encrypted, decrypted: null, parts });
const { type, thumb, label, extension, parts } = asset.encrypted;
assets.push({ type, thumb, label, extension, encrypted, decrypted: null, parts });
}
else {
const encrypted = false
@ -85,6 +85,12 @@ export function useTopicItem(item, hosting, remove, contentKey) {
const full = conversation.actions.getTopicAssetUrl(item.topicId, asset.audio.full);
assets.push({ type, label, encrypted, full });
}
else if (asset.binary) {
const type = 'binary';
const { label, extension } = asset.binary;
const data = conversation.actions.getTopicAssetUrl(item.topicId, asset.binary.data);
assets.push({ type, label, extension, data });
}
}
};
}

BIN
doc/icon_v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -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

View File

@ -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);
}

View File

@ -1,15 +1,16 @@
import { AlertIcon, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd';
import { ExclamationCircleOutlined, SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { useDashboard } from './useDashboard.hook';
import { AccountItem } from './accountItem/AccountItem';
import { CopyButton } from './copyButton/CopyButton';
export function Dashboard() {
const { state, actions } = useDashboard();
const onClipboard = (value) => {
navigator.clipboard.writeText(value);
const onClipboard = async (value) => {
await navigator.clipboard.writeText(value);
};
const createLink = () => {
@ -193,14 +194,12 @@ export function Dashboard() {
<div className="url">
<div className="label">Browser Link:</div>
<div className="link">{createLink()}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(createLink())} />
<CopyButton onCopy={async () => await onClipboard(createLink())} />
</div>
<div className="url">
<div className="label">App Token:</div>
<div className="token">{state.createToken}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(state.createToken)} />
<CopyButton onCopy={async () => await onClipboard(state.createToken)} />
</div>
</CreateLayout>
</Modal>

View File

@ -1,8 +1,9 @@
import { Logo } from 'logo/Logo';
import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled';
import { useAccountItem } from './useAccountItem.hook';
import { ExclamationCircleOutlined, CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { ExclamationCircleOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip, Button } from 'antd';
import { CopyButton } from '../copyButton/CopyButton';
export function AccountItem({ item, remove }) {
@ -132,14 +133,12 @@ export function AccountItem({ item, remove }) {
<div className="url">
<div className="label">Browser Link:</div>
<div className="link">{accessLink()}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(accessLink())}/>
<CopyButton onCopy={async () => await onClipboard(accessLink())} />
</div>
<div className="url">
<div className="label">App Token:</div>
<div className="token">{state.accessToken}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(state.accessToken)} />
<CopyButton onCopy={async () => await onClipboard(state.accessToken)} />
</div>
</AccessLayout>
</Modal>

View File

@ -0,0 +1,15 @@
import { useCopyButton } from './useCopyButton.hook';
import { Tooltip, Button } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
export function CopyButton({ onCopy }) {
const { state, actions } = useCopyButton();
return (
<Tooltip color={state.color} title={state.message} trigger={[]} open={state.show} placement="topRight">
<Button icon={<CopyOutlined />} size="small" onClick={() => actions.copy(onCopy)} />
</Tooltip>
);
}

View File

@ -0,0 +1,38 @@
import { useState, useRef } from 'react';
import Colors from 'constants/Colors';
export function useCopyButton() {
const [state, setState] = useState({
color: Colors.background,
message: 'copeid',
show: false,
});
const timeout = useRef();
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
copy: async (onCopy) => {
try {
await onCopy();
updateState({ show: true, message: 'copied', color: Colors.background });
}
catch {
updateState({ show: true, message: 'failed to copy', color: Colors.alert });
}
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
updateState({ show: false });
}, 1500);
updateState({ show: true });
}
}
return { state, actions };
}

View File

@ -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 <img style={{ height: 128, objectFit: 'contain' }} src={item.url} alt="" />
@ -76,6 +83,9 @@ export function AddTopic({ contentKey }) {
if (item.video) {
return <VideoFile onPosition={(pos) => actions.setPosition(index, pos)} url={item.url} />
}
if (item.binary) {
return <BinaryFile onLabel={(label) => actions.setLabel(index, label)} label={item.label} extension={item.extension} url={item.url} />
}
return <></>
};
@ -110,6 +120,7 @@ export function AddTopic({ contentKey }) {
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="audio/*" ref={attachAudio} onChange={e => onSelectAudio(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => onSelectVideo(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="*/*" ref={attachBinary} onChange={e => onSelectBinary(e)} style={{display: 'none'}}/>
{ state.assets.length > 0 && (
<div class="assets">
<Carousel pad={32} items={state.assets} itemRenderer={renderItem} itemRemove={removeItem} />
@ -136,9 +147,10 @@ export function AddTopic({ contentKey }) {
<SoundOutlined />
</div>
)}
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
<div class="bar space" />
)}
<div class="button space" onClick={() => attachBinary.current.click()}>
<FieldBinaryOutlined />
</div>
<div class="bar space" />
<div class="button space">
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
<FontColorsOutlined />

View File

@ -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 (
<BinaryFileWrapper>
<ReactResizeDetector handleWidth={false} handleHeight={true}>
{({ height }) => {
if (height !== width) {
setWidth(height);
}
return <div style={{ height: '100%', width: width }} />
}}
</ReactResizeDetector>
<div class="player" style={{ width: width, height: width }}>
<div class="extension">{ extension }</div>
<div class="label">
<Input bordered={false} size="small" defaultValue={label} onChange={(e) => onLabel(e.target.value)} />
</div>
</div>
</BinaryFileWrapper>
)
}

View File

@ -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;
}
`;

View File

@ -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 });
},

View File

@ -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 <AudioAsset asset={asset} />
}
if (asset.type === 'binary') {
return <BinaryAsset asset={asset} />
}
return <></>
}

View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import { Progress } 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';
export function BinaryAsset({ asset }) {
const [width, setWidth] = useState(0);
const { actions, state } = useBinaryAsset(asset);
return (
<BinaryAssetWrapper>
<ReactResizeDetector handleWidth={false} handleHeight={true}>
{({ height }) => {
if (height !== width) {
setWidth(height);
}
return <div style={{ height: '100%', width: width }} />
}}
</ReactResizeDetector>
<div class="player" style={{ backgroundColor: '#888888', borderRadius: 4, width: width, height: width }}>
<div class="label">{ asset.label }</div>
<div class="control" onClick={actions.download}>
<DownloadOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
</div>
<div class="unsealing">
{ state.unsealing && (
<Progress percent={Math.floor(100 * state.block / state.total)} size="small" showInfo={false} trailColor={Colors.white} strokeColor={Colors.background} />
)}
</div>
<div class="extension">{ asset.extension }</div>
</div>
</BinaryAssetWrapper>
)
}

View File

@ -0,0 +1,56 @@
import styled from 'styled-components';
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;
}
`;

View File

@ -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 };
}

View File

@ -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 });