mirror of
https://github.com/balzack/databag.git
synced 2025-02-11 19:19:16 +00:00
merging back refactored conversation
This commit is contained in:
parent
16f829d6a5
commit
803f6b84c3
@ -159,6 +159,7 @@ export function useConversationContext() {
|
||||
clearConversation: async () => {
|
||||
conversationId.current = null;
|
||||
reset.current = true;
|
||||
await sync();
|
||||
},
|
||||
setChannelSubject: async (type, subject) => {
|
||||
const { cardId, channelId } = conversationId.current || {};
|
||||
@ -394,6 +395,13 @@ export function useConversationContext() {
|
||||
}
|
||||
}
|
||||
|
||||
const getTopicAssetUrl = (cardId, channelId, topicId, assetId) => {
|
||||
if (cardId) {
|
||||
return card.actions.getTopicAssetUrl(cardId, channelId, topicId, assetId);
|
||||
}
|
||||
return channel.actions.getTopicAssetUrl(channelId, topicId, assetId);
|
||||
}
|
||||
|
||||
const mapTopicEntry = (entry) => {
|
||||
return {
|
||||
topicId: entry.id,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { useRef, useEffect, useState, useContext } from 'react';
|
||||
import { FlatList, View, Text, TouchableOpacity } from 'react-native';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { useConversation } from './useConversation.hook';
|
||||
import { styles } from './Conversation.styled';
|
||||
@ -7,12 +7,37 @@ import { Colors } from 'constants/Colors';
|
||||
import Ionicons from 'react-native-vector-icons/AntDesign';
|
||||
import { Logo } from 'utils/Logo';
|
||||
import { AddTopic } from './addTopic/AddTopic';
|
||||
import { TopicItem } from './topicItem/TopicItem';
|
||||
|
||||
export function Conversation({ navigation, cardId, channelId, closeConversation, openDetails }) {
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
const conversation = useContext(ConversationContext);
|
||||
const { state, actions } = useConversation();
|
||||
const ref = useRef();
|
||||
|
||||
const latch = () => {
|
||||
if (!state.momentum) {
|
||||
actions.latch();
|
||||
ref.current.scrollToIndex({ animated: true, index: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
const updateTopic = async () => {
|
||||
try {
|
||||
await actions.updateTopic();
|
||||
actions.hideEdit();
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Alert.alert(
|
||||
'Failed to Update Message',
|
||||
'Please try again.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (navigation) {
|
||||
@ -23,8 +48,8 @@ export function Conversation({ navigation, cardId, channelId, closeConversation,
|
||||
</View>
|
||||
),
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={openDetails}>
|
||||
<Ionicons name={'setting'} size={24} color={Colors.primary} style={styles.titlebutton} />
|
||||
<TouchableOpacity onPress={openDetails} style={styles.titlebutton}>
|
||||
<Ionicons name={'setting'} size={24} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
@ -58,7 +83,18 @@ export function Conversation({ navigation, cardId, channelId, closeConversation,
|
||||
)}
|
||||
<View style={styles.thread}>
|
||||
<View style={styles.messages}>
|
||||
<Text>Conversation</Text>
|
||||
<FlatList style={styles.conversation}
|
||||
contentContainerStyle={styles.topics}
|
||||
data={state.topics}
|
||||
inverted={true}
|
||||
initialNumToRender={16}
|
||||
renderItem={({item}) => <TopicItem item={item} focused={item.topicId === state.focus}
|
||||
focus={() => actions.setFocus(item.topicId)} hosting={state.host == null}
|
||||
sealed={state.sealed} sealKey={state.sealKey}
|
||||
remove={actions.removeTopic} update={actions.editTopic} block={actions.blockTopic}
|
||||
report={actions.reportTopic} />}
|
||||
keyExtractor={item => item.topicId}
|
||||
/>
|
||||
</View>
|
||||
<AddTopic />
|
||||
</View>
|
||||
|
@ -8,6 +8,7 @@ export const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexShrink: 1,
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
@ -37,6 +38,7 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
titlebutton: {
|
||||
paddingRight: 8,
|
||||
width: 32,
|
||||
},
|
||||
headerclose: {
|
||||
flexGrow: 1,
|
||||
@ -46,16 +48,19 @@ export const styles = StyleSheet.create({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
},
|
||||
thread: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
messages: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
214
app/mobile/src/session/conversation/topicItem/TopicItem.jsx
Normal file
214
app/mobile/src/session/conversation/topicItem/TopicItem.jsx
Normal file
@ -0,0 +1,214 @@
|
||||
import { KeyboardAvoidingView, FlatList, View, Text, TextInput, Modal, Image, Alert } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { useTopicItem } from './useTopicItem.hook';
|
||||
import { styles } from './TopicItem.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
import AntIcons from 'react-native-vector-icons/AntDesign';
|
||||
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import avatar from 'images/avatar.png';
|
||||
import { VideoThumb } from './videoThumb/VideoThumb';
|
||||
import { AudioThumb } from './audioThumb/AudioThumb';
|
||||
import { ImageThumb } from './imageThumb/ImageThumb';
|
||||
import { ImageAsset } from './imageAsset/ImageAsset';
|
||||
import { AudioAsset } from './audioAsset/AudioAsset';
|
||||
import { VideoAsset } from './videoAsset/VideoAsset';
|
||||
|
||||
export function TopicItem({ item, focused, focus, hosting, sealed, sealKey, remove, update, block, report }) {
|
||||
|
||||
const { state, actions } = useTopicItem(item, hosting, remove, sealed, sealKey);
|
||||
|
||||
const erase = () => {
|
||||
Alert.alert(
|
||||
"Removing Message",
|
||||
"Confirm?",
|
||||
[
|
||||
{ text: "Cancel",
|
||||
onPress: () => {},
|
||||
},
|
||||
{ text: "Remove",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await remove(item.topicId);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Alert.alert(
|
||||
'Failed to Remove Message',
|
||||
'Please try again.'
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const reportMessage = () => {
|
||||
Alert.alert(
|
||||
"Report Message",
|
||||
"Confirm?",
|
||||
[
|
||||
{ text: "Cancel",
|
||||
onPress: () => {},
|
||||
},
|
||||
{ text: "Report",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await report(item.topicId);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Alert.alert(
|
||||
'Failed to Report Message',
|
||||
'Please try again.'
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const hideMessage = () => {
|
||||
Alert.alert(
|
||||
"Blocking Message",
|
||||
"Confirm?",
|
||||
[
|
||||
{ text: "Cancel",
|
||||
onPress: () => {},
|
||||
},
|
||||
{ text: "Block",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await block(item.topicId);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Alert.alert(
|
||||
'Failed to Block Message',
|
||||
'Please try again.'
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const renderAsset = (asset) => {
|
||||
return (
|
||||
<View style={styles.frame}>
|
||||
{ asset.item.image && (
|
||||
<ImageAsset topicId={item.topicId} asset={asset.item.image} dismiss={actions.hideCarousel} />
|
||||
)}
|
||||
{ asset.item.video && (
|
||||
<VideoAsset topicId={item.topicId} asset={asset.item.video} dismiss={actions.hideCarousel} />
|
||||
)}
|
||||
{ asset.item.audio && (
|
||||
<AudioAsset topicId={item.topicId} asset={asset.item.audio} dismiss={actions.hideCarousel} />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const renderThumb = (thumb) => {
|
||||
return (
|
||||
<View>
|
||||
{ thumb.item.image && (
|
||||
<ImageThumb topicId={item.topicId} asset={thumb.item.image} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.video && (
|
||||
<VideoThumb topicId={item.topicId} asset={thumb.item.video} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.audio && (
|
||||
<AudioThumb topicId={item.topicId} asset={thumb.item.audio} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.item} onPress={focus}>
|
||||
<View style={styles.header}>
|
||||
{ state.logo !== 'avatar' && state.logo && (
|
||||
<Image source={{ uri: state.logo }} style={{ width: 28, height: 28, borderRadius: 6 }} />
|
||||
)}
|
||||
{ (state.logo === 'avatar' || !state.logo) && (
|
||||
<Image source={avatar} style={{ width: 28, height: 28, borderRadius: 6 }} />
|
||||
)}
|
||||
<Text style={{ ...styles.name, color: state.nameSet ? Colors.text : Colors.grey }}>{ state.name }</Text>
|
||||
<Text style={styles.timestamp}>{ state.timestamp }</Text>
|
||||
</View>
|
||||
{ state.status === 'confirmed' && (
|
||||
<>
|
||||
{ state.transform === 'complete' && state.assets && (
|
||||
<FlatList contentContainerStyle={styles.carousel}
|
||||
data={state.assets}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
renderItem={renderThumb}
|
||||
/>
|
||||
)}
|
||||
{ state.transform === 'incomplete' && (
|
||||
<View style={styles.status}>
|
||||
<MatIcons name="cloud-refresh" size={32} color={Colors.background} />
|
||||
</View>
|
||||
)}
|
||||
{ state.transform === 'error' && (
|
||||
<View style={styles.status}>
|
||||
<MatIcons name="weather-cloudy-alert" size={32} color={Colors.alert} />
|
||||
</View>
|
||||
)}
|
||||
{ state.message && !state.sealed && (
|
||||
<Text style={{ ...styles.message, fontSize: state.fontSize, color: state.fontColor }}>{ state.message }</Text>
|
||||
)}
|
||||
{ state.sealed && (
|
||||
<Text style={ styles.sealed }>sealed message</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ state.status !== 'confirmed' && (
|
||||
<View style={styles.status}>
|
||||
<MatIcons name="cloud-refresh" size={32} color={Colors.divider} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{ focused && (
|
||||
<View style={styles.focused}>
|
||||
{ state.editable && (
|
||||
<TouchableOpacity style={styles.icon} onPress={() => update(item.topicId, state.editData)}>
|
||||
<AntIcons name="edit" size={24} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.editable && (
|
||||
<TouchableOpacity style={styles.icon} onPress={hideMessage}>
|
||||
<MatIcons name="block-helper" size={18} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.editable && (
|
||||
<TouchableOpacity style={styles.icon} onPress={reportMessage}>
|
||||
<MatIcons name="flag-outline" size={18} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.deletable && (
|
||||
<TouchableOpacity style={styles.icon} onPress={erase}>
|
||||
<MatIcons name="delete-outline" size={24} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={state.carousel}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={actions.hideCarousel}
|
||||
>
|
||||
<View style={styles.modal}>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
paddingTop: 8,
|
||||
},
|
||||
item: {
|
||||
borderTopWidth: 1,
|
||||
borderColor: Colors.white,
|
||||
paddingTop: 8,
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 16,
|
||||
},
|
||||
name: {
|
||||
paddingLeft: 8,
|
||||
},
|
||||
timestamp: {
|
||||
paddingLeft: 8,
|
||||
fontSize: 11,
|
||||
paddingTop: 2,
|
||||
color: Colors.grey,
|
||||
},
|
||||
carousel: {
|
||||
paddingLeft: 52,
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
modal: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
frame: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
status: {
|
||||
paddingLeft: 52,
|
||||
},
|
||||
sealed: {
|
||||
paddingRight: 16,
|
||||
paddingLeft: 52,
|
||||
color: Colors.grey,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
focused: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 16,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
borderRadius: 4,
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
message: {
|
||||
paddingRight: 16,
|
||||
paddingLeft: 52,
|
||||
color: Colors.fontColor,
|
||||
},
|
||||
save: {
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: Colors.primary,
|
||||
width: 72,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
saveText: {
|
||||
color: Colors.white,
|
||||
},
|
||||
cancel: {
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.lightgrey,
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
width: 72,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputField: {
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.lightgrey,
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
marginBottom: 8,
|
||||
maxHeight: 92,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
input: {
|
||||
fontSize: 14,
|
||||
flexGrow: 1,
|
||||
},
|
||||
editControls: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
editWrapper: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(52, 52, 52, 0.8)'
|
||||
},
|
||||
editContainer: {
|
||||
backgroundColor: Colors.formBackground,
|
||||
padding: 16,
|
||||
width: '80%',
|
||||
maxWidth: 400,
|
||||
},
|
||||
editHeader: {
|
||||
fontSize: 18,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
editMembers: {
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.lightgrey,
|
||||
borderRadius: 4,
|
||||
marginBottom: 8,
|
||||
height: 250,
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { Image, View, Text, TouchableOpacity } from 'react-native';
|
||||
import { useRef } from 'react';
|
||||
import Colors from 'constants/Colors';
|
||||
import Video from 'react-native-video';
|
||||
import { useAudioAsset } from './useAudioAsset.hook';
|
||||
import { styles } from './AudioAsset.styled';
|
||||
import Icons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import audio from 'images/audio.png';
|
||||
|
||||
export function AudioAsset({ topicId, asset, dismiss }) {
|
||||
|
||||
const { state, actions } = useAudioAsset(topicId, asset);
|
||||
|
||||
const player = useRef(null);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image source={audio} style={{ width: state.width, height: state.height }} resizeMode={'contain'} />
|
||||
<Text style={styles.label}>{ asset.label }</Text>
|
||||
{ !state.playing && state.loaded && (
|
||||
<TouchableOpacity style={styles.control} onPress={actions.play}>
|
||||
<Icons name="play-circle-outline" size={92} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.playing && state.loaded && (
|
||||
<TouchableOpacity style={styles.control} onPress={actions.pause}>
|
||||
<Icons name="pause-circle-outline" size={92} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.close} onPress={dismiss}>
|
||||
<Icons name="window-close" size={32} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Video ref={player} source={{ uri: state.url }} isLooping={true}
|
||||
shouldPlay={state.playing} onLoad={actions.loaded} style={styles.player} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
backgroundColor: Colors.formBackground,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
control: {
|
||||
position: 'absolute',
|
||||
paddingRight: 8,
|
||||
paddingTop: 4,
|
||||
},
|
||||
label: {
|
||||
position: 'absolute',
|
||||
textAlign: 'center',
|
||||
fontSize: 20,
|
||||
paddingTop: 8,
|
||||
top: 0,
|
||||
paddingLeft: 48,
|
||||
paddingRight: 48,
|
||||
},
|
||||
close: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
player: {
|
||||
display: 'none',
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export function useAudioAsset(topicId, asset) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
url: null,
|
||||
playing: false,
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
const closing = useRef(null);
|
||||
const conversation = useContext(ConversationContext);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const frameRatio = dimensions.width / dimensions.height;
|
||||
if (frameRatio > 1) {
|
||||
//height constrained
|
||||
const height = 0.9 * dimensions.height;
|
||||
const width = height;
|
||||
updateState({ width, height });
|
||||
}
|
||||
else {
|
||||
//width constrained
|
||||
const width = 0.9 * dimensions.width;
|
||||
const height = width;
|
||||
updateState({ width, height });
|
||||
}
|
||||
}, [dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.full);
|
||||
updateState({ url });
|
||||
}, [topicId, conversation, asset]);
|
||||
|
||||
const actions = {
|
||||
play: () => {
|
||||
updateState({ playing: true });
|
||||
},
|
||||
pause: () => {
|
||||
updateState({ playing: false });
|
||||
},
|
||||
loaded: () => {
|
||||
updateState({ loaded: true });
|
||||
}
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { View, Text, Image } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { styles } from './AudioThumb.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
import audio from 'images/audio.png';
|
||||
|
||||
export function AudioThumb({ topicId, asset, onAssetView }) {
|
||||
|
||||
return (
|
||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||
<Image source={audio} style={{ borderRadius: 4, width: 92, height: 92, marginRight: 16, backgroundColor: Colors.lightgrey }} resizeMode={'cover'} />
|
||||
{ asset.label && (
|
||||
<View style={styles.overlay}>
|
||||
<Text style={styles.label}>{ asset.label }</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
top: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
paddingRight: 16,
|
||||
maxHeight: 50,
|
||||
},
|
||||
label: {
|
||||
textOverlay: 'center',
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { View, Image, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import { useImageAsset } from './useImageAsset.hook';
|
||||
import { styles } from './ImageAsset.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export function ImageAsset({ topicId, asset, dismiss }) {
|
||||
const { state, actions } = useImageAsset(topicId, asset);
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={dismiss}>
|
||||
<Image source={{ uri: state.url }} onLoad={actions.loaded} onError={actions.failed}
|
||||
style={{ borderRadius: 4, width: state.imageWidth, height: state.imageHeight }} resizeMode={'cover'} />
|
||||
{ state.failed && (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator color={Colors.alert} size="large" />
|
||||
</View>
|
||||
)}
|
||||
{ !state.loaded && !state.failed && (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator color={Colors.white} size="large" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
},
|
||||
overlay: {
|
||||
marginRight: 16,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
borderTopLeftRadius: 4,
|
||||
backgroundColor: Colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.divider,
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export function useImageAsset(topicId, asset) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
frameWidth: 1,
|
||||
frameHeight: 1,
|
||||
imageRatio: 1,
|
||||
imageWidth: 1,
|
||||
imageHeight: 1,
|
||||
url: null,
|
||||
loaded: false,
|
||||
failed: false,
|
||||
});
|
||||
|
||||
const conversation = useContext(ConversationContext);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state.loaded) {
|
||||
const frameRatio = state.frameWidth / state.frameHeight;
|
||||
if (frameRatio > state.imageRatio) {
|
||||
//height constrained
|
||||
const height = 0.9 * state.frameHeight;
|
||||
const width = height * state.imageRatio;
|
||||
updateState({ imageWidth: width, imageHeight: height });
|
||||
}
|
||||
else {
|
||||
//width constrained
|
||||
const width = 0.9 * state.frameWidth;
|
||||
const height = width / state.imageRatio;
|
||||
updateState({ imageWidth: width, imageHeight: height });
|
||||
}
|
||||
}
|
||||
}, [state.frameWidth, state.frameHeight, state.imageRatio, state.loaded]);
|
||||
|
||||
useEffect(() => {
|
||||
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height });
|
||||
}, [dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.full);
|
||||
updateState({ url });
|
||||
}, [topicId, conversation, asset]);
|
||||
|
||||
const actions = {
|
||||
loaded: (e) => {
|
||||
const { width, height } = e.nativeEvent.source;
|
||||
updateState({ loaded: true, imageRatio: width / height });
|
||||
},
|
||||
failed: () => {
|
||||
updateState({ failed: true });
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { Image } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { useImageThumb } from './useImageThumb.hook';
|
||||
import { styles } from './ImageThumb.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export function ImageThumb({ topicId, asset, onAssetView }) {
|
||||
const { state, actions } = useImageThumb(topicId, asset);
|
||||
|
||||
return (
|
||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||
<Image source={{ uri: state.url }} style={{ borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }}
|
||||
onLoad={actions.loaded} resizeMode={'cover'} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
marginRight: 16,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
borderTopLeftRadius: 4,
|
||||
backgroundColor: Colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.divider,
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
|
||||
export function useImageThumb(topicId, asset) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
ratio: 1,
|
||||
url: null,
|
||||
});
|
||||
|
||||
const conversation = useContext(ConversationContext);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb);
|
||||
updateState({ url });
|
||||
}, [topicId, conversation, asset]);
|
||||
|
||||
const actions = {
|
||||
loaded: (e) => {
|
||||
const { width, height } = e.nativeEvent.source;
|
||||
updateState({ ratio: width / height });
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,177 @@
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { CardContext } from 'context/CardContext';
|
||||
import { ProfileContext } from 'context/ProfileContext';
|
||||
import moment from 'moment';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import Colors from 'constants/Colors';
|
||||
import { getCardByGuid } from 'context/cardUtil';
|
||||
|
||||
export function useTopicItem(item, hosting, remove, sealed, sealKey) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
name: null,
|
||||
nameSet: null,
|
||||
known: null,
|
||||
logo: null,
|
||||
timestamp: null,
|
||||
message: null,
|
||||
carousel: false,
|
||||
carouselIndex: 0,
|
||||
width: null,
|
||||
height: null,
|
||||
activeId: null,
|
||||
fontSize: 14,
|
||||
fontColor: Colors.text,
|
||||
editable: false,
|
||||
deletable: false,
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const conversation = useContext(ConversationContext);
|
||||
const profile = useContext(ProfileContext);
|
||||
const card = useContext(CardContext);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateState({ width: dimensions.width, height: dimensions.height });
|
||||
}, [dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const { topicId, detail, unsealedDetail } = item;
|
||||
const { guid, dataType, data, status, transform } = detail;
|
||||
|
||||
let name, nameSet, known, logo;
|
||||
const identity = profile.state?.identity;
|
||||
if (guid === identity.guid) {
|
||||
known = true;
|
||||
if (identity.name) {
|
||||
name = identity.name;
|
||||
}
|
||||
else {
|
||||
name = `${identity.handle}@${identity.node}`;
|
||||
}
|
||||
const img = profile.state.imageUrl;
|
||||
if (img) {
|
||||
logo = img;
|
||||
}
|
||||
else {
|
||||
logo = 'avatar';
|
||||
}
|
||||
}
|
||||
else {
|
||||
const contact = getCardByGuid(card.state.cards, guid)?.card;
|
||||
if (contact) {
|
||||
logo = contact.profile?.imageSet ? card.actions.getCardImageUrl(contact.cardId) : null;
|
||||
|
||||
known = true;
|
||||
if (contact.profile.name) {
|
||||
name = contact.profile.name;
|
||||
nameSet = true;
|
||||
}
|
||||
else {
|
||||
name = `${contact.profile.handle}@${contact.profile.node}`;
|
||||
nameSet = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
name = "unknown";
|
||||
nameSet = false;
|
||||
known = false;
|
||||
logo = null;
|
||||
}
|
||||
}
|
||||
|
||||
let parsed, sealed, message, assets, fontSize, fontColor;
|
||||
if (dataType === 'superbasictopic') {
|
||||
try {
|
||||
sealed = false;
|
||||
parsed = JSON.parse(data);
|
||||
message = parsed.text;
|
||||
assets = parsed.assets;
|
||||
if (parsed.textSize === 'small') {
|
||||
fontSize = 10;
|
||||
}
|
||||
else if (parsed.textSize === 'large') {
|
||||
fontSize = 20;
|
||||
}
|
||||
else {
|
||||
fontSize = 14;
|
||||
}
|
||||
if (parsed.textColor) {
|
||||
fontColor = parsed.textColor;
|
||||
}
|
||||
else {
|
||||
fontColor = Colors.text;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
else if (dataType === 'sealedtopic') {
|
||||
if (unsealedDetail) {
|
||||
sealed = false;
|
||||
parsed = unsealedDetail.message;
|
||||
message = parsed?.text;
|
||||
if (parsed?.textSize === 'small') {
|
||||
fontSize = 10;
|
||||
}
|
||||
else if (parsed?.textSize === 'large') {
|
||||
fontSize = 20;
|
||||
}
|
||||
else {
|
||||
fontSize = 14;
|
||||
}
|
||||
if (parsed?.textColor) {
|
||||
fontColor = parsed?.textColor;
|
||||
}
|
||||
else {
|
||||
fontColor = Colors.text;
|
||||
}
|
||||
}
|
||||
else {
|
||||
conversation.actions.unsealTopic(topicId, sealKey);
|
||||
sealed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp;
|
||||
const date = new Date(item.detail.created * 1000);
|
||||
const now = new Date();
|
||||
const offset = now.getTime() - date.getTime();
|
||||
if(offset < 86400000) {
|
||||
timestamp = moment(date).format('h:mma');
|
||||
}
|
||||
else if (offset < 31449600000) {
|
||||
timestamp = moment(date).format('M/DD');
|
||||
}
|
||||
else {
|
||||
timestamp = moment(date).format('M/DD/YYYY');
|
||||
}
|
||||
|
||||
const editable = detail.guid === identity.guid && parsed;
|
||||
const deletable = editable || hosting;
|
||||
|
||||
updateState({ logo, name, nameSet, known, sealed, message, fontSize, fontColor, timestamp, transform, status, assets, deletable, editable, editData: parsed, editMessage: message });
|
||||
}, [sealKey, card, item]);
|
||||
|
||||
const actions = {
|
||||
showCarousel: (index) => {
|
||||
updateState({ carousel: true, carouselIndex: index });
|
||||
},
|
||||
hideCarousel: () => {
|
||||
updateState({ carousel: false });
|
||||
},
|
||||
setActive: (activeId) => {
|
||||
updateState({ activeId });
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native';
|
||||
import Colors from 'constants/Colors';
|
||||
import Video from 'react-native-video';
|
||||
import { useVideoAsset } from './useVideoAsset.hook';
|
||||
import { styles } from './VideoAsset.styled';
|
||||
import Icons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export function VideoAsset({ topicId, asset, dismiss }) {
|
||||
|
||||
const { state, actions } = useVideoAsset(topicId, asset);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.container} onPress={actions.showControls}>
|
||||
<Video source={{ uri: state.url }} style={{ width: state.width, height: state.height }} resizeMode={'cover'}
|
||||
onReadyForDisplay={(e) => actions.setResolution(e.naturalSize.width, e.naturalSize.height)}
|
||||
onLoad={actions.loaded} isLooping={true} shouldPlay={state.playing} resizeMode="contain" />
|
||||
{ (!state.playing || state.controls) && (
|
||||
<View style={{ ...styles.overlay, width: state.width, height: state.height }} />
|
||||
)}
|
||||
{ !state.playing && state.loaded && (
|
||||
<TouchableOpacity style={styles.control} onPress={actions.play}>
|
||||
<Icons name="play-circle-outline" size={92} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.controls && state.playing && state.loaded && (
|
||||
<TouchableOpacity style={styles.control} onPress={actions.pause}>
|
||||
<Icons name="pause-circle-outline" size={92} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ (state.controls || !state.playing) && state.loaded && (
|
||||
<TouchableOpacity style={styles.close} onPress={dismiss}>
|
||||
<Icons name="window-close" size={32} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{ !state.loaded && (
|
||||
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
||||
<ActivityIndicator color={Colors.white} size="large" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
control: {
|
||||
position: 'absolute',
|
||||
paddingRight: 8,
|
||||
paddingTop: 4,
|
||||
},
|
||||
close: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export function useVideoAsset(topicId, asset) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
frameWidth: 1,
|
||||
frameHeight: 1,
|
||||
videoRatio: 1,
|
||||
width: 1,
|
||||
url: null,
|
||||
playing: false,
|
||||
loaded: false,
|
||||
controls: false,
|
||||
display: { display: 'none' },
|
||||
});
|
||||
|
||||
const controls = useRef(null);
|
||||
const conversation = useContext(ConversationContext);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const frameRatio = state.frameWidth / state.frameHeight;
|
||||
if (frameRatio > state.videoRatio) {
|
||||
//height constrained
|
||||
const height = 0.9 * state.frameHeight;
|
||||
const width = height * state.videoRatio;
|
||||
updateState({ width, height });
|
||||
}
|
||||
else {
|
||||
//width constrained
|
||||
const width = 0.9 * state.frameWidth;
|
||||
const height = width / state.videoRatio;
|
||||
updateState({ width, height });
|
||||
}
|
||||
}, [state.frameWidth, state.frameHeight, state.videoRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height });
|
||||
}, [dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.hd);
|
||||
updateState({ url });
|
||||
}, [topicId, conversation, asset]);
|
||||
|
||||
const actions = {
|
||||
setResolution: (width, height) => {
|
||||
updateState({ display: {}, videoRatio: width / height });
|
||||
},
|
||||
loaded: () => {
|
||||
updateState({ loaded: true });
|
||||
},
|
||||
play: () => {
|
||||
actions.showControls();
|
||||
updateState({ playing: true });
|
||||
},
|
||||
pause: () => {
|
||||
updateState({ playing: false });
|
||||
},
|
||||
showControls: () => {
|
||||
clearTimeout(controls.current);
|
||||
updateState({ controls: true });
|
||||
controls.current = setTimeout(() => {
|
||||
updateState({ controls: false });
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { View, Image } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { useVideoThumb } from './useVideoThumb.hook';
|
||||
import { styles } from './VideoThumb.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
import AntIcons from 'react-native-vector-icons/AntDesign';
|
||||
|
||||
export function VideoThumb({ topicId, asset, onAssetView }) {
|
||||
const { state, actions } = useVideoThumb(topicId, asset);
|
||||
|
||||
return (
|
||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||
<Image source={{ uri: state.url }} style={{ borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }} resizeMode={'cover'} />
|
||||
<View style={styles.overlay}>
|
||||
<AntIcons name="caretright" size={20} color={Colors.white} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
marginRight: 16,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 4,
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
|
||||
export function useVideoThumb(topicId, asset) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
ratio: 1,
|
||||
url: null,
|
||||
});
|
||||
|
||||
const conversation = useContext(ConversationContext);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb);
|
||||
if (url) {
|
||||
Image.getSize(url, (width, height) => {
|
||||
updateState({ url, ratio: width / height });
|
||||
});
|
||||
}
|
||||
}, [topicId, conversation, asset]);
|
||||
|
||||
const actions = {
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -25,7 +25,22 @@ export function useConversation() {
|
||||
const cards = card.state.cards;
|
||||
cardImageUrl = card.actions.getCardImageUrl;
|
||||
const { logo, subject } = getChannelSubjectLogo(cardId, profileGuid, channel, cards, cardImageUrl);
|
||||
updateState({ logo, subject });
|
||||
|
||||
const items = Array.from(conversation.state.topics.values());
|
||||
const sorted = items.sort((a, b) => {
|
||||
const aTimestamp = a?.detail?.created;
|
||||
const bTimestamp = b?.detail?.created;
|
||||
if(aTimestamp === bTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
if(aTimestamp == null || aTimestamp < bTimestamp) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
const filtered = sorted.filter(item => !(item.blocked === 1));
|
||||
|
||||
updateState({ logo, subject, topics: filtered });
|
||||
}, [conversation.state, card.state, profile.state]);
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user