merging back refactored conversation

This commit is contained in:
balzack 2023-03-01 22:30:48 -08:00
parent 16f829d6a5
commit 803f6b84c3
24 changed files with 1193 additions and 6 deletions

View File

@ -159,6 +159,7 @@ export function useConversationContext() {
clearConversation: async () => { clearConversation: async () => {
conversationId.current = null; conversationId.current = null;
reset.current = true; reset.current = true;
await sync();
}, },
setChannelSubject: async (type, subject) => { setChannelSubject: async (type, subject) => {
const { cardId, channelId } = conversationId.current || {}; 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) => { const mapTopicEntry = (entry) => {
return { return {
topicId: entry.id, topicId: entry.id,

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useContext } from 'react'; import { useRef, useEffect, useState, useContext } from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { FlatList, View, Text, TouchableOpacity } from 'react-native';
import { ConversationContext } from 'context/ConversationContext'; import { ConversationContext } from 'context/ConversationContext';
import { useConversation } from './useConversation.hook'; import { useConversation } from './useConversation.hook';
import { styles } from './Conversation.styled'; import { styles } from './Conversation.styled';
@ -7,12 +7,37 @@ import { Colors } from 'constants/Colors';
import Ionicons from 'react-native-vector-icons/AntDesign'; import Ionicons from 'react-native-vector-icons/AntDesign';
import { Logo } from 'utils/Logo'; import { Logo } from 'utils/Logo';
import { AddTopic } from './addTopic/AddTopic'; import { AddTopic } from './addTopic/AddTopic';
import { TopicItem } from './topicItem/TopicItem';
export function Conversation({ navigation, cardId, channelId, closeConversation, openDetails }) { export function Conversation({ navigation, cardId, channelId, closeConversation, openDetails }) {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const conversation = useContext(ConversationContext); const conversation = useContext(ConversationContext);
const { state, actions } = useConversation(); 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(() => { useEffect(() => {
if (navigation) { if (navigation) {
@ -23,8 +48,8 @@ export function Conversation({ navigation, cardId, channelId, closeConversation,
</View> </View>
), ),
headerRight: () => ( headerRight: () => (
<TouchableOpacity onPress={openDetails}> <TouchableOpacity onPress={openDetails} style={styles.titlebutton}>
<Ionicons name={'setting'} size={24} color={Colors.primary} style={styles.titlebutton} /> <Ionicons name={'setting'} size={24} color={Colors.primary} />
</TouchableOpacity> </TouchableOpacity>
), ),
}); });
@ -58,7 +83,18 @@ export function Conversation({ navigation, cardId, channelId, closeConversation,
)} )}
<View style={styles.thread}> <View style={styles.thread}>
<View style={styles.messages}> <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> </View>
<AddTopic /> <AddTopic />
</View> </View>

View File

@ -8,6 +8,7 @@ export const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
flexShrink: 1,
}, },
header: { header: {
display: 'flex', display: 'flex',
@ -37,6 +38,7 @@ export const styles = StyleSheet.create({
}, },
titlebutton: { titlebutton: {
paddingRight: 8, paddingRight: 8,
width: 32,
}, },
headerclose: { headerclose: {
flexGrow: 1, flexGrow: 1,
@ -46,16 +48,19 @@ export const styles = StyleSheet.create({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
height: '100%',
}, },
thread: { thread: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
flexShrink: 1,
}, },
messages: { messages: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
flexShrink: 1,
}, },
}); });

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,22 @@ export function useConversation() {
const cards = card.state.cards; const cards = card.state.cards;
cardImageUrl = card.actions.getCardImageUrl; cardImageUrl = card.actions.getCardImageUrl;
const { logo, subject } = getChannelSubjectLogo(cardId, profileGuid, channel, cards, cardImageUrl); 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]); }, [conversation.state, card.state, profile.state]);