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', whiteSpace: 'nowrap',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
overflow: 'hidden', overflow: 'hidden',
height: 16,
}, },
messageUnset: { messageUnset: {
fontSize: 12, fontSize: 12,

View File

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

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react'; 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 {styles} from './Conversation.styled';
import {useConversation} from './useConversation.hook'; import {useConversation} from './useConversation.hook';
import {Message} from '../message/Message'; import {Message} from '../message/Message';
@ -9,6 +9,8 @@ import { Colors } from '../constants/Colors';
import { Confirm } from '../confirm/Confirm'; import { Confirm } from '../confirm/Confirm';
import ColorPicker from 'react-native-wheel-color-picker' import ColorPicker from 'react-native-wheel-color-picker'
import {BlurView} from '@react-native-community/blur'; import {BlurView} from '@react-native-community/blur';
import ImagePicker from 'react-native-image-crop-picker'
import { ImageFile } from './imageFile/ImageFile';
const SCROLL_THRESHOLD = 16; const SCROLL_THRESHOLD = 16;
@ -102,6 +104,24 @@ export function Conversation({close}: {close: ()=>void}) {
scrollOffset.current = offset; 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 ( return (
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 50} style={styles.conversation}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 50} style={styles.conversation}>
<SafeAreaView style={styles.header}> <SafeAreaView style={styles.header}>
@ -170,13 +190,18 @@ export function Conversation({close}: {close: ()=>void}) {
<Divider style={styles.border} bold={true} /> <Divider style={styles.border} bold={true} />
<Confirm show={alert} params={alertParams} /> <Confirm show={alert} params={alertParams} />
<View style={styles.add}> <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 }} <TextInput multiline={true} mode="outlined" style={{ ...styles.message, fontSize: state.textSize }}
textColor={state.textColorSet ? state.textColor : undefined} outlineColor="transparent" activeOutlineColor="transparent"spellcheck={false} 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} 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)} /> cursorColor={state.textColorSet ? state.textColor : undefined} value={state.message} onChangeText={value => actions.setMessage(value)} />
<View style={styles.controls}> <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="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="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> <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: [], subjectNames: [],
unknownContacts: 0, unknownContacts: 0,
message: '', 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', textColor: '#444444',
textColorSet: false, textColorSet: false,
textSize: 16, textSize: 16,
@ -178,13 +178,13 @@ export function useConversation() {
const uploadAssets = state.assets.map(asset => { const uploadAssets = state.assets.map(asset => {
if (asset.type === 'image') { if (asset.type === 'image') {
if (sealed) { if (sealed) {
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}`, thumb: () => getImageThumb(asset.file) }, { type: TransformType.Thumb, appId: `it${sources.length}`, thumb: () => getImageThumb(asset.path) },
{ type: TransformType.Copy, appId: `ic${sources.length}` } { type: TransformType.Copy, appId: `ic${sources.length}` }
]}); ]});
return { encrypted: { type: 'image', thumb: `it${sources.length-1}`, parts: `ic${sources.length-1}` } }; return { encrypted: { type: 'image', thumb: `it${sources.length-1}`, parts: `ic${sources.length-1}` } };
} else { } 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.Thumb, appId: `it${sources.length}` },
{ type: TransformType.Copy, appId: `ic${sources.length}` } { type: TransformType.Copy, appId: `ic${sources.length}` }
]}); ]});
@ -194,19 +194,19 @@ export function useConversation() {
if (sealed) { if (sealed) {
const videoThumb = async () => { const videoThumb = async () => {
try { try {
return await getVideoThumb(asset.file, asset.position); return await getVideoThumb(asset.path, asset.position);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
return placeholder; 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.Thumb, appId: `vt${sources.length}`, thumb: videoThumb },
{ type: TransformType.Copy, appId: `vc${sources.length}` } { type: TransformType.Copy, appId: `vc${sources.length}` }
]}); ]});
return { encrypted: { type: 'video', thumb: `vt${sources.length-1}`, parts: `vc${sources.length-1}` } }; return { encrypted: { type: 'video', thumb: `vt${sources.length-1}`, parts: `vc${sources.length-1}` } };
} else { } 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.Thumb, appId: `vt${sources.length}`, position: asset.position},
{ type: TransformType.HighQuality, appId: `vh${sources.length}` }, { type: TransformType.HighQuality, appId: `vh${sources.length}` },
{ type: TransformType.LowQuality, appId: `vl${sources.length}` } { type: TransformType.LowQuality, appId: `vl${sources.length}` }
@ -215,26 +215,26 @@ export function useConversation() {
} }
} else if (asset.type === 'audio') { } else if (asset.type === 'audio') {
if (sealed) { 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}` } { type: TransformType.Copy, appId: `ac${sources.length}` }
]}); ]});
return { encrypted: { type: 'audio', label: asset.label, parts: `ac${sources.length-1}` } }; return { encrypted: { type: 'audio', label: asset.label, parts: `ac${sources.length-1}` } };
} else { } 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}` } { type: TransformType.Copy, appId: `ac${sources.length}` }
]}); ]});
return { audio: { label: asset.label, full: `ac${sources.length-1}` } }; return { audio: { label: asset.label, full: `ac${sources.length-1}` } };
} }
} else { } else {
const extension = asset.file.name.split('.').pop(); const extension = asset.path.name.split('.').pop();
const label = asset.file.name.split('.').shift(); const label = asset.path.name.split('.').shift();
if (sealed) { 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}` } { type: TransformType.Copy, appId: `bc${sources.length}` }
]}); ]});
return { encrypted: { type: 'binary', label, extension, parts: `bc${sources.length-1}` } }; return { encrypted: { type: 'binary', label, extension, parts: `bc${sources.length-1}` } };
} else { } 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}` } { type: TransformType.Copy, appId: `bc${sources.length}` }
]}); ]});
return { binary: { label, extension, data: `bc${sources.length-1}` } }; return { binary: { label, extension, data: `bc${sources.length-1}` } };
@ -282,9 +282,9 @@ export function useConversation() {
updateState({ message: '', assets: [], progress: 0 }); updateState({ message: '', assets: [], progress: 0 });
} }
}, },
addImage: (file: File) => { addImage: (path: string, mime: string) => {
const type = 'image'; const type = 'image';
updateState({ assets: [ ...state.assets, { type, file } ]}); updateState({ assets: [ ...state.assets, { type, path, mime } ]});
}, },
addVideo: (file: File) => { addVideo: (file: File) => {
const type = 'video'; 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; const { cardId, channelId, connection } = this;
if (!connection) { if (!connection) {
throw new Error('disconnected from channel'); throw new Error('disconnected from channel');
@ -312,7 +312,11 @@ export class FocusModule implements Focus {
const params = `${cardId ? 'contact' : 'agent'}=${token}&body=multipart` const params = `${cardId ? 'contact' : 'agent'}=${token}&body=multipart`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/blocks?${params}` const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/blocks?${params}`
const formData = new FormData(); 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) { return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest(); 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; const { cardId, channelId, connection } = this;
if (!connection) { if (!connection) {
throw new Error('disconnected from channel'); 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 params = `${cardId ? 'contact' : 'agent'}=${token}&transforms=${encodeURIComponent(JSON.stringify(transforms))}`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/assets?${params}` const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/assets?${params}`
const formData = new FormData(); 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) { return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();

View File

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