Skip to content

Commit

Permalink
feat: added message components
Browse files Browse the repository at this point in the history
  • Loading branch information
bang9 committed Mar 19, 2022
1 parent 0ff1809 commit 682cdb4
Show file tree
Hide file tree
Showing 28 changed files with 904 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -1,53 +1,78 @@
import isSameDay from 'date-fns/isSameDay';
import React, { useCallback, useEffect, useRef } from 'react';
import { FlatList as DefaultFlatList, FlatListProps, ListRenderItem, View } from 'react-native';
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { FlatList, FlatListProps, ListRenderItem, Platform, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { Text } from '@sendbird/uikit-react-native-foundation';
import { ChannelFrozenBanner, createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
import type { SendbirdMessage } from '@sendbird/uikit-utils';
import { isMyMessage } from '@sendbird/uikit-utils';
import { isMyMessage, messageKeyExtractor } from '@sendbird/uikit-utils';

import { useLocalization } from '../../../contexts/Localization';
import type { GroupChannelProps } from '../types';

const HANDLE_NEXT_MSG_SEPARATELY = Platform.select({ android: true, ios: false });
const GroupChannelMessageList: React.FC<GroupChannelProps['MessageList']> = ({
channel,
messages,
renderMessage,
nextMessages,
newMessagesFromNext,
onBottomReached,
onTopReached,
NewMessageTooltip,
NewMessagesTooltip,
ScrollToBottomTooltip,
}) => {
const { LABEL } = useLocalization();
const { colors } = useUIKitTheme();
const { left, right } = useSafeAreaInsets();
const [scrollLeaveBottom, setScrollLeaveBottom] = useState(false);
const scrollRef = useRef<CustomFlatListRef>(null);

// NOTE: Cannot wrap with useCallback, because prevMessage (always getting from fresh messages)
const renderItem: ListRenderItem<SendbirdMessage> = ({ item, index }) => {
const prevMessage = messages[index + 1];

const sameDay = isSameDay(item.createdAt, prevMessage?.createdAt ?? 0);
const separator = sameDay ? null : (
<Text>--- {LABEL.GROUP_CHANNEL.FRAGMENT.LIST_DATE_SEPARATOR(new Date(item.createdAt))} ---</Text>
);

return (
<View style={{ flexDirection: 'row' }}>
{separator}
{renderMessage(item)}
<Text>{index}</Text>
</View>
);
};
const renderItem: ListRenderItem<SendbirdMessage> = ({ item, index }) => (
<View style={{}}>{renderMessage(item, messages[index + 1], messages[index - 1])}</View>
);

const [newMessages, setNewMessages] = useState(() => newMessagesFromNext);

useEffect(() => {
if (HANDLE_NEXT_MSG_SEPARATELY) return;
newMessagesFromNext.length !== 0 && setNewMessages((prev) => prev.concat(newMessagesFromNext));
onBottomReached();
}, [newMessagesFromNext]);

const onLeaveScrollBottom = useCallback((val: boolean) => {
if (!HANDLE_NEXT_MSG_SEPARATELY) setNewMessages([]);
setScrollLeaveBottom(val);
}, []);

return (
<View style={{ flex: 1 }}>
<FlatList
<View style={{ flex: 1, paddingLeft: left, paddingRight: right }}>
{channel.isFrozen && (
<ChannelFrozenBanner style={styles.frozenBanner} text={LABEL.GROUP_CHANNEL.FRAGMENT.LIST_BANNER_FROZEN} />
)}
<CustomFlatList
ref={scrollRef}
data={messages}
nextMessages={nextMessages}
renderItem={renderItem}
keyExtractor={messageKeyExtractor}
onBottomReached={onBottomReached}
onTopReached={onTopReached}
contentContainerStyle={[channel.isFrozen && styles.frozenListPadding, { backgroundColor: colors.background }]}
onLeaveScrollBottom={onLeaveScrollBottom}
/>
{NewMessageTooltip && (
<View style={{ position: 'absolute', bottom: 10, alignSelf: 'center' }}>
<NewMessageTooltip newMessages={newMessagesFromNext} />
{NewMessagesTooltip && (
<View style={styles.newMsgTooltip}>
<NewMessagesTooltip
visible={scrollLeaveBottom}
onPress={() => scrollRef.current?.scrollToBottom(false)}
newMessages={HANDLE_NEXT_MSG_SEPARATELY ? newMessagesFromNext : newMessages}
/>
</View>
)}
{ScrollToBottomTooltip && (
<View style={styles.scrollTooltip}>
<ScrollToBottomTooltip visible={scrollLeaveBottom} onPress={() => scrollRef.current?.scrollToBottom(true)} />
</View>
)}
</View>
Expand All @@ -62,25 +87,47 @@ type Props = Omit<FlatListProps<SendbirdMessage>, 'onEndReached'> & {
onBottomReached: () => void;
onTopReached: () => void;
nextMessages: SendbirdMessage[];
onLeaveScrollBottom: (value: boolean) => void;
};
const FlatList: React.FC<Props> = ({ onTopReached, nextMessages, onBottomReached, onScroll, ...props }) => {
const scrollRef = useRef<DefaultFlatList<SendbirdMessage>>(null);
const BOTTOM_DETECT_THRESHOLD = 15;
type CustomFlatListRef = { scrollToBottom: (animated?: boolean) => void };
const CustomFlatList = forwardRef<CustomFlatListRef, Props>(function CustomFlatList(
{ onTopReached, nextMessages, onBottomReached, onLeaveScrollBottom, onScroll, ...props },
ref,
) {
const { select } = useUIKitTheme();
const scrollRef = useRef<FlatList<SendbirdMessage>>(null);
const yPos = useRef(0);

useImperativeHandle(
ref,
() => ({
scrollToBottom: (animated = true) => scrollRef.current?.scrollToOffset({ animated, offset: 0 }),
}),
[],
);

useEffect(() => {
const latestMessage = nextMessages[nextMessages.length - 1];
if (!latestMessage) return;

if (hasReachedToBottom(yPos.current)) {
onBottomReached();
} else if (isMyMessage(latestMessage)) {
scrollRef.current?.scrollToEnd({ animated: false });
scrollRef.current?.scrollToOffset({ animated: false, offset: 0 });
}
}, [onBottomReached, nextMessages]);

const _onScroll: Props['onScroll'] = useCallback(
(event) => {
yPos.current = event.nativeEvent.contentOffset.y;
const { contentOffset } = event.nativeEvent;
if (BOTTOM_DETECT_THRESHOLD < yPos.current && contentOffset.y <= BOTTOM_DETECT_THRESHOLD) {
onLeaveScrollBottom(false);
} else if (BOTTOM_DETECT_THRESHOLD < contentOffset.y && yPos.current <= BOTTOM_DETECT_THRESHOLD) {
onLeaveScrollBottom(true);
}

yPos.current = contentOffset.y;

onScroll?.(event);
if (hasReachedToBottom(yPos.current)) onBottomReached();
Expand All @@ -89,15 +136,47 @@ const FlatList: React.FC<Props> = ({ onTopReached, nextMessages, onBottomReached
);

return (
<DefaultFlatList
<FlatList
{...props}
ref={scrollRef}
// FIXME: Inverted FlatList performance issue on Android {@link https://github.com/facebook/react-native/issues/30034}
inverted
// FIXME: maintainVisibleContentPosition is not working on Android {@link https://github.com/facebook/react-native/issues/25239}
maintainVisibleContentPosition={{ minIndexForVisible: 1, autoscrollToTopThreshold: BOTTOM_DETECT_THRESHOLD - 5 }}
ref={scrollRef}
bounces={false}
keyboardDismissMode={'on-drag'}
indicatorStyle={select({ light: 'black', dark: 'white' })}
removeClippedSubviews
onEndReachedThreshold={0.25}
onEndReached={onTopReached}
onScroll={_onScroll}
/>
);
};
});

const styles = createStyleSheet({
frozenBanner: {
position: 'absolute',
zIndex: 999,
top: 8,
left: 8,
right: 8,
},
frozenListPadding: {
paddingBottom: 32,
},
newMsgTooltip: {
position: 'absolute',
zIndex: 999,
bottom: 10,
alignSelf: 'center',
},
scrollTooltip: {
position: 'absolute',
zIndex: 999,
bottom: 10,
right: 16,
},
});

export default GroupChannelMessageList;
20 changes: 17 additions & 3 deletions packages/uikit-react-native-core/src/domain/groupChannel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export type GroupChannelProps = {
prevMessage?: SendbirdMessage;
enableMessageGrouping?: boolean;
}>;
NewMessageTooltip?: GroupChannelProps['MessageList']['NewMessageTooltip'];
NewMessagesTooltip?: GroupChannelProps['MessageList']['NewMessagesTooltip'];
ScrollToBottomTooltip?: GroupChannelProps['MessageList']['ScrollToBottomTooltip'];

Header?: GroupChannelProps['Header']['Header'];
onPressHeaderLeft: GroupChannelProps['Header']['onPressHeaderLeft'];
Expand All @@ -39,14 +40,27 @@ export type GroupChannelProps = {
onPressHeaderRight: () => void;
};
MessageList: {
channel: Sendbird.GroupChannel;
messages: SendbirdMessage[];
nextMessages: SendbirdMessage[];
newMessagesFromNext: SendbirdMessage[];
onTopReached: () => void;
onBottomReached: () => void;

renderMessage: (message: SendbirdMessage, prevMessage?: SendbirdMessage) => React.ReactElement | null;
NewMessageTooltip: null | CommonComponent<{ newMessages: SendbirdMessage[] }>;
renderMessage: (
message: SendbirdMessage,
prevMessage?: SendbirdMessage,
nextMessage?: SendbirdMessage,
) => React.ReactElement | null;
NewMessagesTooltip: null | CommonComponent<{
visible: boolean;
onPress: () => void;
newMessages: SendbirdMessage[];
}>;
ScrollToBottomTooltip: null | CommonComponent<{
visible: boolean;
onPress: () => void;
}>;
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface LabelSet {
DIALOG_MESSAGE_COPY: string;
/** @domain GroupChannel > Fragment > Dialog > Message > Edit */
DIALOG_MESSAGE_EDIT: string;
/** @domain GroupChannel > Fragment > Dialog > Message > Save */
DIALOG_MESSAGE_SAVE: string;
/** @domain GroupChannel > Fragment > Dialog > Message > Delete */
DIALOG_MESSAGE_DELETE: string;
/** @domain GroupChannel > Fragment > Dialog > Message > Delete > Confirm title */
Expand Down Expand Up @@ -165,6 +167,7 @@ export const createBaseLabel = ({ dateLocale, overrides }: LabelCreateOptions):

DIALOG_MESSAGE_COPY: 'Copy',
DIALOG_MESSAGE_EDIT: 'Edit',
DIALOG_MESSAGE_SAVE: 'Save',
DIALOG_MESSAGE_DELETE: 'Delete',
DIALOG_MESSAGE_DELETE_CONFIRM_TITLE: 'Delete message?',
DIALOG_MESSAGE_DELETE_CONFIRM_OK: 'Delete',
Expand Down
8 changes: 5 additions & 3 deletions packages/uikit-react-native-foundation/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as Avatar } from './ui/Avatar';
export { default as Badge } from './ui/Badge';
export { default as BottomSheet } from './ui/BottomSheet';
export { default as Button } from './ui/Button';
export { default as ChannelFrozenBanner } from './ui/ChannelFrozenBanner';
export { DialogProvider, useActionMenu, useAlert, usePrompt } from './ui/Dialog';
export { default as Divider } from './ui/Divider';
export { default as Header } from './ui/Header';
Expand All @@ -25,12 +26,13 @@ export { default as Text } from './ui/Text';
export { default as TextInput } from './ui/TextInput';

/** Styles **/
export { default as useHeaderStyle } from './styles/useHeaderStyle';
export { default as getDefaultHeaderHeight } from './styles/getDefaultHeaderHeight';
export { default as createAppearanceHelper } from './styles/createAppearanceHelper';
export { default as createScaleFactor } from './styles/createScaleFactor';
export { default as createStyleSheet } from './styles/createStyleSheet';
export { default as getDefaultHeaderHeight } from './styles/getDefaultHeaderHeight';
export { HeaderStyleContext, HeaderStyleProvider } from './styles/HeaderStyleContext';
export { themeFactory } from './styles/themeFactory';
export { default as useHeaderStyle } from './styles/useHeaderStyle';

/** Types **/
export type {
Expand All @@ -40,7 +42,7 @@ export type {
FontAttributes,
BaseHeaderProps,
UIKitAppearance,
AppearanceHelper,
UIKitColors,
ComponentColorTree,
PaletteInterface,
} from './types';
16 changes: 12 additions & 4 deletions packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ const DarkUIKitTheme = themeFactory({
enabled: {
textMsg: palette.onBackgroundDark01,
textEdited: palette.onBackgroundDark02,
textDate: palette.onBackgroundDark03,
textTime: palette.onBackgroundDark03,
textSenderName: palette.onBackgroundDark02,
background: palette.background400,
},
pressed: {
textMsg: palette.onBackgroundDark01,
textEdited: palette.onBackgroundDark02,
textDate: palette.onBackgroundDark03,
textTime: palette.onBackgroundDark03,
textSenderName: palette.onBackgroundDark02,
background: palette.primary500,
},
Expand All @@ -136,19 +136,27 @@ const DarkUIKitTheme = themeFactory({
enabled: {
textMsg: palette.onBackgroundLight01,
textEdited: palette.onBackgroundLight02,
textDate: palette.onBackgroundDark03,
textTime: palette.onBackgroundDark03,
textSenderName: palette.transparent,
background: palette.primary200,
},
pressed: {
textMsg: palette.onBackgroundLight01,
textEdited: palette.onBackgroundLight02,
textDate: palette.onBackgroundDark03,
textTime: palette.onBackgroundDark03,
textSenderName: palette.transparent,
background: palette.primary300,
},
},
},
dateSeparator: {
default: {
none: {
text: palette.onBackgroundDark02,
background: palette.overlay02,
},
},
},
},
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ const LightUIKitTheme = themeFactory({
enabled: {
textMsg: palette.onBackgroundLight01,
textEdited: palette.onBackgroundLight02,
textDate: palette.onBackgroundLight03,
textTime: palette.onBackgroundLight03,
textSenderName: palette.onBackgroundLight02,
background: palette.background100,
},
pressed: {
textMsg: palette.onBackgroundLight01,
textEdited: palette.onBackgroundLight02,
textDate: palette.onBackgroundLight03,
textTime: palette.onBackgroundLight03,
textSenderName: palette.onBackgroundLight02,
background: palette.primary100,
},
Expand All @@ -136,19 +136,27 @@ const LightUIKitTheme = themeFactory({
enabled: {
textMsg: palette.onBackgroundDark01,
textEdited: palette.onBackgroundDark02,
textDate: palette.onBackgroundLight03,
textTime: palette.onBackgroundLight03,
textSenderName: palette.transparent,
background: palette.primary300,
},
pressed: {
textMsg: palette.onBackgroundDark01,
textEdited: palette.onBackgroundDark02,
textDate: palette.onBackgroundLight03,
textTime: palette.onBackgroundLight03,
textSenderName: palette.transparent,
background: palette.primary400,
},
},
},
dateSeparator: {
default: {
none: {
text: palette.onBackgroundDark01,
background: palette.overlay02,
},
},
},
},
}),
});
Expand Down
Loading

0 comments on commit 682cdb4

Please sign in to comment.