From e3483bcf735c2a65ab53d5ee10e43ca6c5e33864 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Wed, 7 Apr 2021 18:22:26 -0400 Subject: added figma styling --- src/components/messages/ChannelPreview.tsx | 136 +++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/components/messages/ChannelPreview.tsx (limited to 'src/components/messages/ChannelPreview.tsx') diff --git a/src/components/messages/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx new file mode 100644 index 00000000..11408dc1 --- /dev/null +++ b/src/components/messages/ChannelPreview.tsx @@ -0,0 +1,136 @@ +import {useNavigation} from '@react-navigation/core'; +import React, {useContext} from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useSelector} from 'react-redux'; +import {ChannelPreviewMessengerProps} from 'stream-chat-react-native'; +import {ChatContext} from '../../App'; +import {RootState} from '../../store/rootReducer'; +import { + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalReactionType, + LocalUserType, +} from '../../types'; +import {normalize, SCREEN_HEIGHT} from '../../utils'; + +const ChannelPreview: React.FC< + ChannelPreviewMessengerProps< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalReactionType, + LocalUserType + > +> = (props) => { + const navigation = useNavigation(); + const {setChannel} = useContext(ChatContext); + const {channel} = props; + const {userId: loggedInUserId} = useSelector( + (state: RootState) => state.user.user, + ); + const otherMembers = channel + ? Object.values(channel.state.members).filter( + (member) => member.user?.id !== loggedInUserId, + ) + : []; + const member = otherMembers.length === 1 ? otherMembers[0] : undefined; + const online = member?.user?.online; + const unread = channel.state.unreadCount > 0; + + return ( + { + setChannel(channel); + navigation.navigate('Chat'); + }}> + + + {online && } + + + + {member?.user?.first_name} {member?.user?.last_name} + + + {channel.state.messages[channel.state.messages.length - 1].text} + + + {unread && } + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', + height: Math.round(SCREEN_HEIGHT / 10), + alignItems: 'center', + paddingHorizontal: '8%', + paddingVertical: '5%', + }, + avatar: { + width: normalize(62), + height: normalize(62), + borderRadius: normalize(62) / 2, + }, + online: { + position: 'absolute', + backgroundColor: '#6EE7E7', + width: normalize(18), + height: normalize(18), + borderRadius: normalize(18) / 2, + borderColor: 'white', + borderWidth: 2, + bottom: 0, + right: 0, + }, + content: { + flex: 1, + height: '100%', + justifyContent: 'space-between', + flexDirection: 'column', + marginLeft: '5%', + }, + name: { + fontWeight: '500', + fontSize: normalize(14), + lineHeight: normalize(17), + }, + lastMessage: { + color: '#828282', + fontWeight: '500', + fontSize: normalize(12), + lineHeight: normalize(14), + }, + unread: { + fontWeight: '700', + color: 'black', + }, + purpleDot: { + backgroundColor: '#8F01FF', + width: normalize(10), + height: normalize(10), + borderRadius: normalize(10) / 2, + }, +}); + +export default ChannelPreview; -- cgit v1.2.3-70-g09d2 From bb16e95e15f6ea9b8941cae764570bdf7c0fdb59 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Thu, 8 Apr 2021 16:10:14 -0400 Subject: created util functions, updated isOnline --- src/components/messages/ChannelPreview.tsx | 3 +- src/screens/chat/ChatScreen.tsx | 11 ++++++ src/utils/messages.ts | 59 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/utils/messages.ts (limited to 'src/components/messages/ChannelPreview.tsx') diff --git a/src/components/messages/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx index 11408dc1..867e0a38 100644 --- a/src/components/messages/ChannelPreview.tsx +++ b/src/components/messages/ChannelPreview.tsx @@ -16,6 +16,7 @@ import { LocalUserType, } from '../../types'; import {normalize, SCREEN_HEIGHT} from '../../utils'; +import {isOnline} from '../../utils/messages'; const ChannelPreview: React.FC< ChannelPreviewMessengerProps< @@ -40,7 +41,7 @@ const ChannelPreview: React.FC< ) : []; const member = otherMembers.length === 1 ? otherMembers[0] : undefined; - const online = member?.user?.online; + const online = isOnline(member?.user?.last_active); const unread = channel.state.unreadCount > 0; return ( diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx index eeb1a7d6..8e6c1575 100644 --- a/src/screens/chat/ChatScreen.tsx +++ b/src/screens/chat/ChatScreen.tsx @@ -2,6 +2,7 @@ import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; import {StackNavigationProp, useHeaderHeight} from '@react-navigation/stack'; import React, {useContext} from 'react'; import {StyleSheet, View} from 'react-native'; +import {useSelector} from 'react-redux'; import { Channel, Chat, @@ -10,6 +11,7 @@ import { } from 'stream-chat-react-native'; import {ChatContext} from '../../App'; import {MainStackParams} from '../../routes'; +import {RootState} from '../../store/rootReducer'; type ChatScreenNavigationProp = StackNavigationProp; interface ChatScreenProps { @@ -22,6 +24,15 @@ const ChatScreen: React.FC = () => { const {channel, chatClient} = useContext(ChatContext); const headerHeight = useHeaderHeight(); const tabbarHeight = useBottomTabBarHeight(); + const {userId: loggedInUserId} = useSelector( + (state: RootState) => state.user.user, + ); + const otherMembers = channel + ? Object.values(channel.state.members).filter( + (member) => member.user?.id !== loggedInUserId, + ) + : []; + const member = otherMembers.length === 1 ? otherMembers[0] : undefined; return ( diff --git a/src/utils/messages.ts b/src/utils/messages.ts new file mode 100644 index 00000000..ae8e7cec --- /dev/null +++ b/src/utils/messages.ts @@ -0,0 +1,59 @@ +import moment from 'moment'; + +/** + * Finds the difference in time in minutes + * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z" + * @returns diff in minutes + */ +const _diffInMinutes = (lastActive: string | undefined) => { + if (!lastActive) { + return undefined; + } + return moment().diff(moment(lastActive), 'minutes'); +}; + +/** + * Formats the last activity status. + * - "Active now" (≤ 5 minutes) + * - "Seen X minutes ago" (5 > x ≥ 59 minutes) + * - "Seen X hours ago" (x = [1, 2]) + * - "Offline" + * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z" + * @returns + */ +export const formatLastSeenText = (lastActive: string | undefined) => { + const diff = _diffInMinutes(lastActive); + if (!diff) { + return 'Offline'; + } + if (diff <= 5) { + return 'Active now'; + } + if (diff <= 59) { + return `Seen ${diff} minutes ago`; + } + if (diff <= 180) { + const hours = Math.floor(diff / 60); + return `Seen ${hours} hours ago`; + } + return 'Offline'; +}; + +/** + * Checks if a lastActive timestamp is considered Online or not. + * + * A user is online if last active is ≤ 15 minutes. + * + * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z" + * @returns True if active + */ +export const isOnline = (lastActive: string | undefined) => { + if (!lastActive) { + return false; + } + const diff = _diffInMinutes(lastActive); + if (!diff) { + return false; + } + return diff <= 15; +}; -- cgit v1.2.3-70-g09d2 From fab86f9b874524a4beabb3c45a9e59e8b00ca495 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Thu, 8 Apr 2021 18:42:28 -0400 Subject: added chat header, created isMember util, fixed KB padding issue --- src/App.tsx | 15 ++---- src/components/messages/ChannelPreview.tsx | 25 ++++----- src/components/messages/ChatHeader.tsx | 84 ++++++++++++++++++++++++++++++ src/routes/main/MainStackScreen.tsx | 7 ++- src/screens/chat/ChatScreen.tsx | 31 +++++------ src/types/types.ts | 21 ++++---- src/utils/layouts.ts | 1 + src/utils/messages.ts | 24 +++++++++ 8 files changed, 151 insertions(+), 57 deletions(-) create mode 100644 src/components/messages/ChatHeader.tsx (limited to 'src/components/messages/ChannelPreview.tsx') diff --git a/src/App.tsx b/src/App.tsx index 9510c193..b8d64461 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,14 @@ import {NavigationContainer} from '@react-navigation/native'; import React, {useState} from 'react'; import {Provider} from 'react-redux'; -import {Channel as ChannelType, StreamChat} from 'stream-chat'; +import {StreamChat} from 'stream-chat'; import {OverlayProvider} from 'stream-chat-react-native'; import {STREAM_CHAT_API} from './constants'; import {navigationRef} from './RootNavigation'; import Routes from './routes'; import store from './store/configureStore'; import { + ChannelGroupedType, ChatContextType, LocalAttachmentType, LocalChannelType, @@ -21,17 +22,7 @@ import { export const ChatContext = React.createContext({} as ChatContextType); const App = () => { - const [channel, setChannel] = useState< - ChannelType< - LocalAttachmentType, - LocalChannelType, - LocalCommandType, - LocalEventType, - LocalMessageType, - LocalResponseType, - LocalUserType - > - >(); + const [channel, setChannel] = useState(); const chatClient = StreamChat.getInstance< LocalAttachmentType, LocalChannelType, diff --git a/src/components/messages/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx index 867e0a38..8ec6060a 100644 --- a/src/components/messages/ChannelPreview.tsx +++ b/src/components/messages/ChannelPreview.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/core'; import React, {useContext} from 'react'; import {Image, StyleSheet, Text, View} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import {useSelector} from 'react-redux'; +import {useSelector, useStore} from 'react-redux'; import {ChannelPreviewMessengerProps} from 'stream-chat-react-native'; import {ChatContext} from '../../App'; import {RootState} from '../../store/rootReducer'; @@ -16,7 +16,7 @@ import { LocalUserType, } from '../../types'; import {normalize, SCREEN_HEIGHT} from '../../utils'; -import {isOnline} from '../../utils/messages'; +import {getMember, isOnline} from '../../utils/messages'; const ChannelPreview: React.FC< ChannelPreviewMessengerProps< @@ -29,18 +29,11 @@ const ChannelPreview: React.FC< LocalUserType > > = (props) => { - const navigation = useNavigation(); const {setChannel} = useContext(ChatContext); + const state = useStore().getState(); + const navigation = useNavigation(); const {channel} = props; - const {userId: loggedInUserId} = useSelector( - (state: RootState) => state.user.user, - ); - const otherMembers = channel - ? Object.values(channel.state.members).filter( - (member) => member.user?.id !== loggedInUserId, - ) - : []; - const member = otherMembers.length === 1 ? otherMembers[0] : undefined; + const member = getMember(channel, state); const online = isOnline(member?.user?.last_active); const unread = channel.state.unreadCount > 0; @@ -55,8 +48,8 @@ const ChannelPreview: React.FC< @@ -71,7 +64,9 @@ const ChannelPreview: React.FC< - {channel.state.messages[channel.state.messages.length - 1].text} + {channel.state.messages.length > 0 + ? channel.state.messages[channel.state.messages.length - 1].text + : ''} {unread && } diff --git a/src/components/messages/ChatHeader.tsx b/src/components/messages/ChatHeader.tsx new file mode 100644 index 00000000..2bc096ec --- /dev/null +++ b/src/components/messages/ChatHeader.tsx @@ -0,0 +1,84 @@ +import React, {useContext} from 'react'; +import {Image, StyleSheet, View} from 'react-native'; +import {Text} from 'react-native-animatable'; +import {useStore} from 'react-redux'; +import {ChatContext} from '../../App'; +import {ChatHeaderHeight, normalize, StatusBarHeight} from '../../utils'; +import {formatLastSeenText, getMember, isOnline} from '../../utils/messages'; + +type ChatHeaderProps = {}; + +const ChatHeader: React.FC = () => { + const {channel} = useContext(ChatContext); + const state = useStore().getState(); + const member = getMember(channel, state); + const online = isOnline(member?.user?.last_active); + const lastSeen = formatLastSeenText(member?.user?.last_active); + + return ( + + + + {online && } + + + + {member?.user?.first_name} {member?.user?.last_name} + + {lastSeen} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + height: ChatHeaderHeight - StatusBarHeight, + flexDirection: 'row', + alignItems: 'center', + paddingLeft: '15%', + }, + avatar: { + width: normalize(40), + height: normalize(40), + borderRadius: normalize(40) / 2, + }, + online: { + position: 'absolute', + backgroundColor: '#6EE7E7', + width: normalize(16), + height: normalize(16), + borderRadius: normalize(16) / 2, + borderColor: 'white', + borderWidth: 3, + top: 0, + right: 0, + }, + content: { + flex: 1, + height: '80%', + justifyContent: 'space-between', + flexDirection: 'column', + marginLeft: '5%', + }, + name: { + fontWeight: '700', + fontSize: normalize(15), + lineHeight: normalize(18), + }, + lastSeen: { + color: '#828282', + fontWeight: '500', + fontSize: normalize(12), + lineHeight: normalize(14), + }, +}); + +export default ChatHeader; diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index 8068b893..48c57920 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -34,7 +34,7 @@ import { } from '../../screens'; import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders'; import {ScreenType} from '../../types'; -import {AvatarHeaderHeight, SCREEN_WIDTH} from '../../utils'; +import {AvatarHeaderHeight, ChatHeaderHeight, SCREEN_WIDTH} from '../../utils'; import {MainStack, MainStackParams} from './MainStackNavigator'; /** @@ -306,7 +306,10 @@ const MainStackScreen: React.FC = ({route}) => { ); diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx index 8e6c1575..386feb03 100644 --- a/src/screens/chat/ChatScreen.tsx +++ b/src/screens/chat/ChatScreen.tsx @@ -1,8 +1,8 @@ import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; -import {StackNavigationProp, useHeaderHeight} from '@react-navigation/stack'; +import {StackNavigationProp} from '@react-navigation/stack'; import React, {useContext} from 'react'; -import {StyleSheet, View} from 'react-native'; -import {useSelector} from 'react-redux'; +import {StyleSheet} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; import { Channel, Chat, @@ -10,8 +10,9 @@ import { MessageList, } from 'stream-chat-react-native'; import {ChatContext} from '../../App'; +import ChatHeader from '../../components/messages/ChatHeader'; import {MainStackParams} from '../../routes'; -import {RootState} from '../../store/rootReducer'; +import {isIPhoneX} from '../../utils'; type ChatScreenNavigationProp = StackNavigationProp; interface ChatScreenProps { @@ -22,33 +23,29 @@ interface ChatScreenProps { */ const ChatScreen: React.FC = () => { const {channel, chatClient} = useContext(ChatContext); - const headerHeight = useHeaderHeight(); const tabbarHeight = useBottomTabBarHeight(); - const {userId: loggedInUserId} = useSelector( - (state: RootState) => state.user.user, - ); - const otherMembers = channel - ? Object.values(channel.state.members).filter( - (member) => member.user?.id !== loggedInUserId, - ) - : []; - const member = otherMembers.length === 1 ? otherMembers[0] : undefined; return ( - + + - + {}} /> - + ); }; const styles = StyleSheet.create({ container: { backgroundColor: 'white', + flex: 1, }, }); diff --git a/src/types/types.ts b/src/types/types.ts index 582eefac..376c4be0 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -300,19 +300,18 @@ export type LocalMessageType = Record; export type LocalResponseType = Record; export type LocalReactionType = Record; export type LocalUserType = Record; +export type ChannelGroupedType = ChannelType< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType +>; export type ChatContextType = { - channel: - | ChannelType< - LocalAttachmentType, - LocalChannelType, - LocalCommandType, - LocalEventType, - LocalMessageType, - LocalResponseType, - LocalUserType - > - | undefined; + channel: ChannelGroupedType | undefined; setChannel: React.Dispatch< React.SetStateAction< | ChannelType< diff --git a/src/utils/layouts.ts b/src/utils/layouts.ts index e2f1f0b1..4d0d557d 100644 --- a/src/utils/layouts.ts +++ b/src/utils/layouts.ts @@ -31,6 +31,7 @@ export const StatusBarHeight = Platform.select({ }); export const AvatarHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.3; +export const ChatHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.1; /** * This is a function for normalizing the font size for different devices, based on iphone 8. diff --git a/src/utils/messages.ts b/src/utils/messages.ts index ae8e7cec..d63f2b7a 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,4 +1,6 @@ import moment from 'moment'; +import {RootState} from '../store/rootReducer'; +import {ChannelGroupedType} from '../types'; /** * Finds the difference in time in minutes @@ -57,3 +59,25 @@ export const isOnline = (lastActive: string | undefined) => { } return diff <= 15; }; + +/** + * Gets the other member in the channel. + * @param channel the current chat channel + * @param state the current redux state + * @returns other member or undefined + */ +export const getMember = ( + channel: ChannelGroupedType | undefined, + state: RootState, +) => { + if (!channel) { + return undefined; + } + const loggedInUserId = state.user.user.userId; + const otherMembers = channel + ? Object.values(channel.state.members).filter( + (member) => member.user?.id !== loggedInUserId, + ) + : []; + return otherMembers.length === 1 ? otherMembers[0] : undefined; +}; -- cgit v1.2.3-70-g09d2 From 98a31b59df5b51ea9488220d47bd7d60b3a268b9 Mon Sep 17 00:00:00 2001 From: Shravya Ramesh Date: Thu, 8 Apr 2021 17:11:28 -0700 Subject: minor styling and removed unused imports --- src/components/messages/ChannelPreview.tsx | 41 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 21 deletions(-) (limited to 'src/components/messages/ChannelPreview.tsx') diff --git a/src/components/messages/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx index 8ec6060a..312f879a 100644 --- a/src/components/messages/ChannelPreview.tsx +++ b/src/components/messages/ChannelPreview.tsx @@ -2,10 +2,10 @@ import {useNavigation} from '@react-navigation/core'; import React, {useContext} from 'react'; import {Image, StyleSheet, Text, View} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import {useSelector, useStore} from 'react-redux'; +import {useStore} from 'react-redux'; +import {usernameRegex} from 'src/constants'; import {ChannelPreviewMessengerProps} from 'stream-chat-react-native'; import {ChatContext} from '../../App'; -import {RootState} from '../../store/rootReducer'; import { LocalAttachmentType, LocalChannelType, @@ -15,20 +15,18 @@ import { LocalReactionType, LocalUserType, } from '../../types'; -import {normalize, SCREEN_HEIGHT} from '../../utils'; +import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import {getMember, isOnline} from '../../utils/messages'; -const ChannelPreview: React.FC< - ChannelPreviewMessengerProps< - LocalAttachmentType, - LocalChannelType, - LocalCommandType, - LocalEventType, - LocalMessageType, - LocalReactionType, - LocalUserType - > -> = (props) => { +const ChannelPreview: React.FC> = (props) => { const {setChannel} = useContext(ChatContext); const state = useStore().getState(); const navigation = useNavigation(); @@ -78,14 +76,14 @@ const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', - height: Math.round(SCREEN_HEIGHT / 10), + height: Math.round(SCREEN_HEIGHT / 9), + width: Math.round(SCREEN_WIDTH * 0.85), + alignSelf: 'center', alignItems: 'center', - paddingHorizontal: '8%', - paddingVertical: '5%', }, avatar: { - width: normalize(62), - height: normalize(62), + width: normalize(60), + height: normalize(60), borderRadius: normalize(62) / 2, }, online: { @@ -101,8 +99,7 @@ const styles = StyleSheet.create({ }, content: { flex: 1, - height: '100%', - justifyContent: 'space-between', + height: '60%', flexDirection: 'column', marginLeft: '5%', }, @@ -116,6 +113,7 @@ const styles = StyleSheet.create({ fontWeight: '500', fontSize: normalize(12), lineHeight: normalize(14), + paddingTop: '5%', }, unread: { fontWeight: '700', @@ -126,6 +124,7 @@ const styles = StyleSheet.create({ width: normalize(10), height: normalize(10), borderRadius: normalize(10) / 2, + marginLeft: '5%', }, }); -- cgit v1.2.3-70-g09d2