support unsealed image asset upload

This commit is contained in:
balzack 2025-01-02 11:34:43 -08:00
parent f1033777db
commit 25e4e68ded
6 changed files with 66 additions and 22 deletions

View File

@ -14,6 +14,7 @@ export const styles = StyleSheet.create({
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
height: 16,
},
messageUnset: {
fontSize: 12,

View File

@ -11,6 +11,15 @@ export const styles = StyleSheet.create({
right: 0,
backgroundColor: 'transparent',
},
carousel: {
paddingLeft: 8,
paddingBottom: 8,
},
assets: {
display: 'flex',
flexDirection: 'row',
gap: 16,
},
modal: {
width: '100%',
height: '100%',

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import {KeyboardAvoidingView, Modal, Platform, SafeAreaView, Pressable, View, FlatList, TouchableOpacity} from 'react-native';
import {KeyboardAvoidingView, Modal, Platform, ScrollView, SafeAreaView, Pressable, View, FlatList, TouchableOpacity} from 'react-native';
import {styles} from './Conversation.styled';
import {useConversation} from './useConversation.hook';
import {Message} from '../message/Message';
@ -9,6 +9,8 @@ import { Colors } from '../constants/Colors';
import { Confirm } from '../confirm/Confirm';
import ColorPicker from 'react-native-wheel-color-picker'
import {BlurView} from '@react-native-community/blur';
import ImagePicker from 'react-native-image-crop-picker'
import { ImageFile } from './imageFile/ImageFile';
const SCROLL_THRESHOLD = 16;
@ -102,6 +104,24 @@ export function Conversation({close}: {close: ()=>void}) {
scrollOffset.current = offset;
}
const addImage = async () => {
try {
const { path, mime } = await ImagePicker.openPicker({ mediaType: 'photo' });
actions.addImage(`file://${path}`, mime);
}
catch (err) {
console.log(err);
}
}
const media = state.assets.map((asset, index) => {
if (asset.type === 'image') {
return <ImageFile key={index} path={asset.path} disabled={false} remove={()=>{}} />
} else {
return <></>
}
});
return (
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 50} style={styles.conversation}>
<SafeAreaView style={styles.header}>
@ -170,13 +190,18 @@ export function Conversation({close}: {close: ()=>void}) {
<Divider style={styles.border} bold={true} />
<Confirm show={alert} params={alertParams} />
<View style={styles.add}>
{ media.length > 0 && (
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false} style={styles.carousel} contentContainerStyle={styles.assets}>
{ media }
</ScrollView>
)}
<TextInput multiline={true} mode="outlined" style={{ ...styles.message, fontSize: state.textSize }}
textColor={state.textColorSet ? state.textColor : undefined} outlineColor="transparent" activeOutlineColor="transparent"spellcheck={false}
autoComplete="off" autoCapitalize="none" autoCorrect={false} placeholder={state.strings.newMessage} placeholderTextColor={state.textColorSet ? state.textColor : undefined}
cursorColor={state.textColorSet ? state.textColor : undefined} value={state.message} onChangeText={value => actions.setMessage(value)} />
<View style={styles.controls}>
<Pressable style={styles.control}><Surface style={styles.surface} elevation={2}><Icon style={styles.button} source="camera" size={24} color={Colors.primary} /></Surface></Pressable>
<Pressable style={styles.control} onPress={addImage}><Surface style={styles.surface} elevation={2}><Icon style={styles.button} source="camera" size={24} color={Colors.primary} /></Surface></Pressable>
<Pressable style={styles.control}><Surface style={styles.surface} elevation={2}><Icon style={styles.button} source="video-outline" size={24} color={Colors.primary} /></Surface></Pressable>
<Pressable style={styles.control}><Surface style={styles.surface} elevation={2}><Icon style={styles.button} source="volume-high" size={24} color={Colors.primary} /></Surface></Pressable>
<Pressable style={styles.control}><Surface style={styles.surface} elevation={2}><Icon style={styles.button} source="file-outline" size={24} color={Colors.primary} /></Surface></Pressable>

View File

@ -44,7 +44,7 @@ export function useConversation() {
subjectNames: [],
unknownContacts: 0,
message: '',
assets: [] as {type: string, file: File, position?: number, label?: string}[],
assets: [] as {type: string, path: string, mime?: string, position?: number, label?: string}[],
textColor: '#444444',
textColorSet: false,
textSize: 16,
@ -178,13 +178,13 @@ export function useConversation() {
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) },
sources.push({ type: AssetType.Image, source: asset.path, transforms: [
{ type: TransformType.Thumb, appId: `it${sources.length}`, thumb: () => getImageThumb(asset.path) },
{ 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: [
sources.push({ type: AssetType.Image, source: asset.path, transforms: [
{ type: TransformType.Thumb, appId: `it${sources.length}` },
{ type: TransformType.Copy, appId: `ic${sources.length}` }
]});
@ -194,19 +194,19 @@ export function useConversation() {
if (sealed) {
const videoThumb = async () => {
try {
return await getVideoThumb(asset.file, asset.position);
return await getVideoThumb(asset.path, asset.position);
} catch (err) {
console.log(err);
return placeholder;
}
};
sources.push({ type: AssetType.Video, source: asset.file, transforms: [
sources.push({ type: AssetType.Video, source: asset.path, transforms: [
{ type: TransformType.Thumb, appId: `vt${sources.length}`, thumb: videoThumb },
{ 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: [
sources.push({ type: AssetType.Video, source: asset.path, 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}` }
@ -215,26 +215,26 @@ export function useConversation() {
}
} else if (asset.type === 'audio') {
if (sealed) {
sources.push({ type: AssetType.Audio, source: asset.file, transforms: [
sources.push({ type: AssetType.Audio, source: asset.path, transforms: [
{ type: TransformType.Copy, appId: `ac${sources.length}` }
]});
return { encrypted: { type: 'audio', label: asset.label, parts: `ac${sources.length-1}` } };
} else {
sources.push({ type: AssetType.Video, source: asset.file, transforms: [
sources.push({ type: AssetType.Video, source: asset.path, transforms: [
{ type: TransformType.Copy, appId: `ac${sources.length}` }
]});
return { audio: { label: asset.label, full: `ac${sources.length-1}` } };
}
} else {
const extension = asset.file.name.split('.').pop();
const label = asset.file.name.split('.').shift();
const extension = asset.path.name.split('.').pop();
const label = asset.path.name.split('.').shift();
if (sealed) {
sources.push({ type: AssetType.Binary, source: asset.file, transforms: [
sources.push({ type: AssetType.Binary, source: asset.path, transforms: [
{ type: TransformType.Copy, appId: `bc${sources.length}` }
]});
return { encrypted: { type: 'binary', label, extension, parts: `bc${sources.length-1}` } };
} else {
sources.push({ type: AssetType.Binary, source: asset.file, transforms: [
sources.push({ type: AssetType.Binary, source: asset.path, transforms: [
{ type: TransformType.Copy, appId: `bc${sources.length}` }
]});
return { binary: { label, extension, data: `bc${sources.length-1}` } };
@ -282,9 +282,9 @@ export function useConversation() {
updateState({ message: '', assets: [], progress: 0 });
}
},
addImage: (file: File) => {
addImage: (path: string, mime: string) => {
const type = 'image';
updateState({ assets: [ ...state.assets, { type, file } ]});
updateState({ assets: [ ...state.assets, { type, path, mime } ]});
},
addVideo: (file: File) => {
const type = 'video';

View File

@ -303,7 +303,7 @@ export class FocusModule implements Focus {
});
}
private mirrorFile(source: any, topicId: string, progress: (percent: number)=>boolean): Promise<string> {
private mirrorFile(source: File|string, topicId: string, progress: (percent: number)=>boolean): Promise<string> {
const { cardId, channelId, connection } = this;
if (!connection) {
throw new Error('disconnected from channel');
@ -312,7 +312,11 @@ export class FocusModule implements Focus {
const params = `${cardId ? 'contact' : 'agent'}=${token}&body=multipart`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/blocks?${params}`
const formData = new FormData();
formData.append('asset', source);
if (typeof source === 'string') { // file path used in mobile
formData.append("asset", {uri: source, name: 'asset', type: 'application/octent-stream'});
} else { // file object used in browser
formData.append('asset', source);
}
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
@ -336,7 +340,7 @@ export class FocusModule implements Focus {
});
}
private transformFile(source: any, topicId: string, transforms: string[], progress: (percent: number)=>boolean): Promise<{assetId: string, transform: string}[]> {
private transformFile(source: File|string, topicId: string, transforms: string[], progress: (percent: number)=>boolean): Promise<{assetId: string, transform: string}[]> {
const { cardId, channelId, connection } = this;
if (!connection) {
throw new Error('disconnected from channel');
@ -345,7 +349,12 @@ export class FocusModule implements Focus {
const params = `${cardId ? 'contact' : 'agent'}=${token}&transforms=${encodeURIComponent(JSON.stringify(transforms))}`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/assets?${params}`
const formData = new FormData();
formData.append('asset', source);
if (typeof source === 'string') { // file path used in mobile
formData.append("asset", {uri: source, name: 'asset', type: 'application/octent-stream'});
} else { // file object used in browser
formData.append('asset', source);
}
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();

View File

@ -148,7 +148,7 @@ export enum AssetType {
export type AssetSource = {
type: AssetType;
source: any;
source: File|string;
transforms: {type: TransformType, appId: string, position?: number, thumb?: ()=>Promise<string>}[],
}