Skip to content

Commit

Permalink
feat(uikit): added group channel operators fragment
Browse files Browse the repository at this point in the history
  • Loading branch information
bang9 committed Oct 28, 2022
1 parent 4213e6d commit c7f6626
Show file tree
Hide file tree
Showing 18 changed files with 366 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { __domain__Contexts } from '../module/moduleContext';
import type { __domain__Props } from '../types';

const __domain__Header = (_: __domain__Props['Header']) => {
const { headerTitle } = useContext(__domain__Contexts['Fragment']);
const { headerTitle } = useContext(__domain__Contexts.Fragment);
const { HeaderComponent } = useHeaderStyle();
return <HeaderComponent title={headerTitle} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GroupChannelModerationsContexts } from '../module/moduleContext';
import type { GroupChannelModerationsProps } from '../types';

const GroupChannelModerationsHeader = ({ onPressHeaderLeft }: GroupChannelModerationsProps['Header']) => {
const { headerTitle } = useContext(GroupChannelModerationsContexts['Fragment']);
const { headerTitle } = useContext(GroupChannelModerationsContexts.Fragment);
const { HeaderComponent } = useHeaderStyle();
return <HeaderComponent title={headerTitle} left={<Icon icon={'arrow-left'} />} onPressLeft={onPressHeaderLeft} />;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useContext } from 'react';

import { Icon, useHeaderStyle } from '@sendbird/uikit-react-native-foundation';

import { GroupChannelOperatorsContexts } from '../module/moduleContext';
import type { GroupChannelOperatorsProps } from '../types';

const GroupChannelOperatorsHeader = ({
onPressHeaderLeft,
onPressHeaderRight,
}: GroupChannelOperatorsProps['Header']) => {
const { headerTitle } = useContext(GroupChannelOperatorsContexts.Fragment);
const { HeaderComponent } = useHeaderStyle();
return (
<HeaderComponent
title={headerTitle}
left={<Icon icon={'arrow-left'} />}
onPressLeft={onPressHeaderLeft}
right={<Icon icon={'plus'} />}
onPressRight={onPressHeaderRight}
/>
);
};

export default GroupChannelOperatorsHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { FlatList, ListRenderItem } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import type { SendbirdMember } from '@sendbird/uikit-utils';
import { useFreshCallback } from '@sendbird/uikit-utils';

import type { GroupChannelOperatorsProps } from '../types';

const GroupChannelOperatorsList = ({
operators,
renderUser,
ListEmptyComponent,
}: GroupChannelOperatorsProps['List']) => {
const renderItem: ListRenderItem<SendbirdMember> = useFreshCallback(({ item }) => renderUser?.({ user: item }));
const { left, right } = useSafeAreaInsets();

return (
<FlatList
data={operators}
renderItem={renderItem}
contentContainerStyle={{ paddingLeft: left, paddingRight: right, flexGrow: 1 }}
ListEmptyComponent={ListEmptyComponent}
/>
);
};

export default GroupChannelOperatorsList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';

import TypedPlaceholder from '../../../components/TypedPlaceholder';

const GroupChannelOperatorsStatusEmpty = () => {
return (
<View style={styles.container}>
<TypedPlaceholder type={'no-users'} />
</View>
);
};

const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default GroupChannelOperatorsStatusEmpty;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';

import TypedPlaceholder from '../../../components/TypedPlaceholder';

const GroupChannelOperatorsStatusLoading = () => {
return (
<View style={styles.container}>
<TypedPlaceholder type={'loading'} />
</View>
);
};

const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

export default GroupChannelOperatorsStatusLoading;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as GroupChannelOperatorsList } from './component/GroupChannelOperatorsList';
export { default as GroupChannelOperatorsHeader } from './component/GroupChannelOperatorsHeader';
export { default as GroupChannelOperatorsStatusLoading } from './component/GroupChannelOperatorsStatusLoading';
export { default as GroupChannelOperatorsStatusEmpty } from './component/GroupChannelOperatorsStatusEmpty';
export { default as createGroupChannelOperatorsModule } from './module/createGroupChannelOperatorsModule';
export { GroupChannelOperatorsContextsProvider, GroupChannelOperatorsContexts } from './module/moduleContext';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import GroupChannelOperatorsHeader from '../component/GroupChannelOperatorsHeader';
import GroupChannelOperatorsList from '../component/GroupChannelOperatorsList';
import GroupChannelOperatorsStatusEmpty from '../component/GroupChannelOperatorsStatusEmpty';
import GroupChannelOperatorsStatusLoading from '../component/GroupChannelOperatorsStatusLoading';
import type { GroupChannelOperatorsModule } from '../types';
import { GroupChannelOperatorsContextsProvider } from './moduleContext';

const createGroupChannelOperatorsModule = ({
Header = GroupChannelOperatorsHeader,
List = GroupChannelOperatorsList,
StatusLoading = GroupChannelOperatorsStatusLoading,
StatusEmpty = GroupChannelOperatorsStatusEmpty,
Provider = GroupChannelOperatorsContextsProvider,
...module
}: Partial<GroupChannelOperatorsModule> = {}): GroupChannelOperatorsModule => {
return { Header, List, Provider, StatusEmpty, StatusLoading, ...module };
};

export default createGroupChannelOperatorsModule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { createContext } from 'react';

import ProviderLayout from '../../../components/ProviderLayout';
import { useLocalization } from '../../../hooks/useContext';
import type { GroupChannelOperatorsContextsType, GroupChannelOperatorsModule } from '../types';

export const GroupChannelOperatorsContexts: GroupChannelOperatorsContextsType = {
Fragment: createContext({
headerTitle: '',
}),
};

export const GroupChannelOperatorsContextsProvider: GroupChannelOperatorsModule['Provider'] = ({ children }) => {
const { STRINGS } = useLocalization();
return (
<ProviderLayout>
<GroupChannelOperatorsContexts.Fragment.Provider
value={{ headerTitle: STRINGS.GROUP_CHANNEL_OPERATORS.HEADER_TITLE }}
>
{children}
</GroupChannelOperatorsContexts.Fragment.Provider>
</ProviderLayout>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type React from 'react';

import type { SendbirdGroupChannel, SendbirdMember } from '@sendbird/uikit-utils';

import type { CommonComponent } from '../../types';

export type GroupChannelOperatorsProps = {
Fragment: {
channel: SendbirdGroupChannel;
onPressHeaderLeft: GroupChannelOperatorsProps['Header']['onPressHeaderLeft'];
onPressHeaderRight: GroupChannelOperatorsProps['Header']['onPressHeaderRight'];
renderUser?: GroupChannelOperatorsProps['List']['renderUser'];
};
Header: {
onPressHeaderLeft: () => void;
onPressHeaderRight: () => void;
};
List: {
renderUser: (props: { user: SendbirdMember }) => React.ReactElement | null;
operators: SendbirdMember[];
ListEmptyComponent?: React.ReactElement;
};
};

/**
* Internal context for GroupChannelOperators
* For example, the developer can create a custom header
* with getting data from the domain context
* */
export type GroupChannelOperatorsContextsType = {
Fragment: React.Context<{
headerTitle: string;
}>;
};
export interface GroupChannelOperatorsModule {
Provider: CommonComponent;
Header: CommonComponent<GroupChannelOperatorsProps['Header']>;
List: CommonComponent<GroupChannelOperatorsProps['List']>;
StatusEmpty: CommonComponent;
StatusLoading: CommonComponent;
}

export type GroupChannelOperatorsFragment = CommonComponent<GroupChannelOperatorsProps['Fragment']>;
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,17 @@ const createGroupChannelMembersFragment = (
<UserActionBar
muted={user.isMuted}
uri={user.profileUrl}
label={user.role === 'operator' ? STRINGS.GROUP_CHANNEL_MEMBERS.USER_BAR_OPERATOR : ''}
label={user.role === 'operator' ? STRINGS.LABELS.USER_BAR_OPERATOR : ''}
name={
(user.nickname || STRINGS.LABELS.USER_NO_NAME) +
(user.userId === currentUser?.userId ? STRINGS.GROUP_CHANNEL_MEMBERS.USER_BAR_ME_POSTFIX : '')
(user.userId === currentUser?.userId ? STRINGS.LABELS.USER_BAR_ME_POSTFIX : '')
}
disabled={user.userId === currentUser?.userId}
onPressActionMenu={ifOperator(channel.myRole, () => {
const menuItems: ActionMenuItem['menuItems'] = [];

menuItems.push({
title: ifOperator(
user.role,
STRINGS.GROUP_CHANNEL_MEMBERS.DIALOG_USER_UNREGISTER_OPERATOR,
STRINGS.GROUP_CHANNEL_MEMBERS.DIALOG_USER_REGISTER_AS_OPERATOR,
),
title: ifOperator(user.role, STRINGS.LABELS.UNREGISTER_OPERATOR, STRINGS.LABELS.REGISTER_AS_OPERATOR),
onPress: ifOperator(
user.role,
() => channel.removeOperators([user.userId]),
Expand All @@ -97,11 +93,7 @@ const createGroupChannelMembersFragment = (

if (!channel.isBroadcast) {
menuItems.push({
title: ifMuted(
user.isMuted,
STRINGS.GROUP_CHANNEL_MEMBERS.DIALOG_USER_UNMUTE,
STRINGS.GROUP_CHANNEL_MEMBERS.DIALOG_USER_MUTE,
),
title: ifMuted(user.isMuted, STRINGS.LABELS.UNMUTE, STRINGS.LABELS.MUTE),
onPress: ifMuted(
user.isMuted,
() => channel.unmuteUser(user),
Expand All @@ -111,7 +103,7 @@ const createGroupChannelMembersFragment = (
}

menuItems.push({
title: STRINGS.GROUP_CHANNEL_MEMBERS.DIALOG_USER_BAN,
title: STRINGS.LABELS.BAN,
style: 'destructive',
onPress: () => channel.banUser(user),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';

import { useChannelHandler } from '@sendbird/uikit-chat-hooks';
import { useActionMenu } from '@sendbird/uikit-react-native-foundation';
import { ifOperator, useForceUpdate, useFreshCallback, useUniqId } from '@sendbird/uikit-utils';

import UserActionBar from '../components/UserActionBar';
import { createGroupChannelOperatorsModule } from '../domain/groupChannelOperators';
import type { GroupChannelOperatorsFragment, GroupChannelOperatorsModule } from '../domain/groupChannelOperators/types';
import { useLocalization, useProfileCard, useSendbirdChat } from '../hooks/useContext';

const name = 'createGroupChannelOperatorsFragment';
const createGroupChannelOperatorsFragment = (
initModule?: Partial<GroupChannelOperatorsModule>,
): GroupChannelOperatorsFragment => {
const GroupChannelOperatorsModule = createGroupChannelOperatorsModule(initModule);

return ({ channel, onPressHeaderLeft, onPressHeaderRight, renderUser }) => {
const uniqId = useUniqId(name);
const forceUpdate = useForceUpdate();

const { STRINGS } = useLocalization();
const { sdk, currentUser } = useSendbirdChat();
const { openMenu } = useActionMenu();
const { show } = useProfileCard();

useChannelHandler(sdk, `${name}_${uniqId}`, {
onUserLeft(channel) {
if (channel.url === channel.url) forceUpdate();
},
onUserBanned(channel) {
if (channel.url === channel.url) forceUpdate();
},
onOperatorUpdated(channel) {
if (channel.url === channel.url) forceUpdate();
},
});

const _renderUser: NonNullable<typeof renderUser> = useFreshCallback((props) => {
if (renderUser) return renderUser(props);

const { user } = props;
return (
<UserActionBar
muted={false}
uri={user.profileUrl}
label={user.role === 'operator' ? STRINGS.LABELS.USER_BAR_OPERATOR : ''}
name={
(user.nickname || STRINGS.LABELS.USER_NO_NAME) +
(user.userId === currentUser?.userId ? STRINGS.LABELS.USER_BAR_ME_POSTFIX : '')
}
disabled={user.userId === currentUser?.userId}
onPressActionMenu={ifOperator(channel.myRole, () => {
openMenu({
title: user.nickname || STRINGS.LABELS.USER_NO_NAME,
menuItems: [
{
title: ifOperator(user.role, STRINGS.LABELS.UNREGISTER_OPERATOR, STRINGS.LABELS.REGISTER_AS_OPERATOR),
onPress: ifOperator(
user.role,
() => channel.removeOperators([user.userId]),
() => channel.addOperators([user.userId]),
),
},
],
});
})}
onPressAvatar={() => show(user)}
/>
);
});

return (
<GroupChannelOperatorsModule.Provider>
<GroupChannelOperatorsModule.Header
onPressHeaderLeft={onPressHeaderLeft}
onPressHeaderRight={async () => onPressHeaderRight()}
/>

<GroupChannelOperatorsModule.List
operators={channel.members.filter((it) => it.role === 'operator')}
renderUser={_renderUser}
ListEmptyComponent={<GroupChannelOperatorsModule.StatusEmpty />}
/>
</GroupChannelOperatorsModule.Provider>
);
};
};

export default createGroupChannelOperatorsFragment;
1 change: 1 addition & 0 deletions packages/uikit-react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { default as createGroupChannelInviteFragment } from './fragments/createG
export { default as createGroupChannelListFragment } from './fragments/createGroupChannelListFragment';
export { default as createGroupChannelMembersFragment } from './fragments/createGroupChannelMembersFragment';
export { default as createGroupChannelModerationsFragment } from './fragments/createGroupChannelModerationsFragment';
export { default as createGroupChannelOperatorsFragment } from './fragments/createGroupChannelOperatorsFragment';

/** Context **/
export { SendbirdChatContext, SendbirdChatProvider } from './contexts/SendbirdChatCtx';
Expand Down
Loading

0 comments on commit c7f6626

Please sign in to comment.