Page MenuHomePhabricator

D13994.diff
No OneTemporary

D13994.diff

diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js
--- a/lib/shared/thread-utils.js
+++ b/lib/shared/thread-utils.js
@@ -244,6 +244,18 @@
);
}
+function useViewerIsMember(
+ threadInfo: ?(ThreadInfo | LegacyRawThreadInfo | RawThreadInfo),
+): boolean {
+ return React.useMemo(() => {
+ return !!(
+ threadInfo &&
+ threadInfo.currentUser.role !== null &&
+ threadInfo.currentUser.role !== undefined
+ );
+ }, [threadInfo]);
+}
+
function isMemberActive(
memberInfo: MemberInfoWithPermissions | MinimallyEncodedThickMemberInfo,
): boolean {
@@ -1905,6 +1917,7 @@
useCommunityRootMembersToRole,
useThreadHasPermission,
viewerIsMember,
+ useViewerIsMember,
threadInChatList,
useIsThreadInChatList,
useThreadsInChatList,
diff --git a/native/components/community-list-item.react.js b/native/components/community-list-item.react.js
new file mode 100644
--- /dev/null
+++ b/native/components/community-list-item.react.js
@@ -0,0 +1,218 @@
+// @flow
+
+import * as React from 'react';
+import { useState } from 'react';
+import { View } from 'react-native';
+
+import {
+ leaveThreadActionTypes,
+ useLeaveThread,
+} from 'lib/actions/thread-actions.js';
+import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
+import { useJoinCommunity } from 'lib/shared/community-utils.js';
+import {
+ getCommunity,
+ useThreadHasPermission,
+ useViewerIsMember,
+} from 'lib/shared/thread-utils.js';
+import type {
+ JoinCommunityStep,
+ OngoingJoinCommunityData,
+} from 'lib/types/community-types';
+import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
+import { threadPermissions } from 'lib/types/thread-permission-types.js';
+import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
+import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
+
+import PrimaryButton from './primary-button.react.js';
+import SingleLine from './single-line.react.js';
+import ThreadAvatar from '../avatars/thread-avatar.react.js';
+import { nonThreadCalendarQuery } from '../navigation/nav-selectors.js';
+import { NavContext } from '../navigation/navigation-context.js';
+import { useSelector } from '../redux/redux-utils.js';
+import { useStyles } from '../themes/colors.js';
+import type { TextStyle, ViewStyle } from '../types/styles.js';
+
+type Props = {
+ +threadInfo: ThreadInfo,
+ +style?: ViewStyle,
+ +textStyle?: TextStyle,
+};
+
+function CommunityListItem(props: Props): React.Node {
+ const { threadInfo: initialThreadInfo, style, textStyle } = props;
+
+ // initialThreadInfo will not update if the user leaves or joins the thread,
+ // so we also need reduxThreadInfo to track thread membership and permissions
+ const reduxThreadInfo: ?ThreadInfo = useSelector(
+ state => threadInfoSelector(state)[initialThreadInfo.id],
+ );
+
+ const [threadInfo, setThreadInfo] = React.useState(initialThreadInfo);
+
+ React.useEffect(() => {
+ if (reduxThreadInfo) {
+ setThreadInfo(reduxThreadInfo);
+ } else {
+ setThreadInfo(initialThreadInfo);
+ }
+ }, [initialThreadInfo, reduxThreadInfo]);
+
+ const navContext = React.useContext(NavContext);
+ const calendarQuery = useSelector(state =>
+ nonThreadCalendarQuery({
+ redux: state,
+ navContext,
+ }),
+ );
+ const communityID = getCommunity(threadInfo);
+ const [ongoingJoinData, setOngoingJoinData] =
+ React.useState<?OngoingJoinCommunityData>(null);
+ const [step, setStep] = React.useState<JoinCommunityStep>('inactive');
+
+ const joinCommunity = useJoinCommunity({
+ communityID,
+ keyserverOverride: null,
+ calendarQuery,
+ ongoingJoinData,
+ setOngoingJoinData,
+ step,
+ setStep,
+ });
+
+ const canLeaveThread = useThreadHasPermission(
+ threadInfo,
+ threadPermissions.LEAVE_THREAD,
+ );
+
+ const styles = useStyles(unboundStyles);
+ const resolvedThreadInfo = useResolvedThreadInfo(threadInfo);
+
+ const isMember = useViewerIsMember(threadInfo);
+ const [joinStatus, setJoinStatus] = useState<
+ 'joined' | 'notJoined' | 'joining' | 'leaving',
+ >(`notJoined`);
+ React.useEffect(() => {
+ setJoinStatus(isMember ? 'joined' : 'notJoined');
+ }, [isMember]);
+
+ const handleJoin = React.useCallback(async () => {
+ setJoinStatus('joining');
+ try {
+ await joinCommunity();
+ setJoinStatus('joined');
+ } catch (error) {
+ console.error('Failed to join community:', error);
+ setJoinStatus('notJoined');
+ }
+ }, [joinCommunity]);
+
+ const dispatchActionPromise = useDispatchActionPromise();
+ const callLeaveThread = useLeaveThread();
+
+ const handleLeave = React.useCallback(async () => {
+ setJoinStatus('leaving');
+ try {
+ const leavePromise = callLeaveThread({ threadID: threadInfo.id });
+ void dispatchActionPromise(leaveThreadActionTypes, leavePromise);
+ await leavePromise;
+ setJoinStatus('notJoined');
+ } catch (error) {
+ console.error('Failed to leave community:', error);
+ setJoinStatus('joined');
+ }
+ }, [callLeaveThread, dispatchActionPromise, threadInfo.id]);
+
+ let buttonContent;
+ if (joinStatus === 'joined' && !canLeaveThread) {
+ buttonContent = (
+ <PrimaryButton
+ label="Leave"
+ variant="disabled"
+ style={styles.primaryButton}
+ textStyle={styles.buttonText}
+ onPress={() => {}}
+ />
+ );
+ } else if (joinStatus === 'joining' || joinStatus === 'leaving') {
+ buttonContent = (
+ <PrimaryButton
+ variant="loading"
+ style={styles.primaryButton}
+ textStyle={styles.buttonText}
+ onPress={() => {}}
+ />
+ );
+ } else if (joinStatus === 'joined') {
+ buttonContent = (
+ <PrimaryButton
+ onPress={handleLeave}
+ label="Leave"
+ variant="enabled"
+ style={[styles.primaryButton, styles.primaryButtonToggled]}
+ textStyle={[styles.buttonText, styles.buttonTextToggled]}
+ />
+ );
+ } else {
+ buttonContent = (
+ <PrimaryButton
+ onPress={handleJoin}
+ label="Join"
+ variant="enabled"
+ style={styles.primaryButton}
+ textStyle={styles.buttonText}
+ />
+ );
+ }
+
+ return (
+ <View style={[styles.container, style]}>
+ <ThreadAvatar size="S" threadInfo={resolvedThreadInfo} />
+ <SingleLine style={[styles.text, textStyle]}>
+ {resolvedThreadInfo.uiName}
+ </SingleLine>
+ {buttonContent}
+ </View>
+ );
+}
+
+const MemoizedCommunityListItem: React.ComponentType<Props> =
+ React.memo<Props>(CommunityListItem);
+
+const unboundStyles = {
+ activityIndicatorContainer: {
+ marginLeft: 10,
+ },
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ paddingVertical: 10,
+ },
+ text: {
+ color: 'modalForegroundLabel',
+ fontSize: 16,
+ paddingLeft: 9,
+ paddingRight: 12,
+ marginLeft: 8,
+ },
+ primaryButton: {
+ marginLeft: 'auto',
+ height: 24,
+ minWidth: 70,
+ },
+ primaryButtonToggled: {
+ backgroundColor: 'primaryButton',
+ borderColor: 'primaryButton',
+ },
+ buttonText: {
+ fontSize: 14,
+ padding: 2,
+ },
+ buttonTextToggled: {
+ color: 'primaryButtonText',
+ },
+};
+
+export default MemoizedCommunityListItem;
diff --git a/native/components/primary-button.react.js b/native/components/primary-button.react.js
--- a/native/components/primary-button.react.js
+++ b/native/components/primary-button.react.js
@@ -5,7 +5,7 @@
import Button from './button.react.js';
import { useColors, useStyles } from '../themes/colors.js';
-import type { ViewStyle } from '../types/styles';
+import type { ViewStyle, TextStyle } from '../types/styles';
type Props = {
+onPress: () => mixed,
@@ -13,9 +13,10 @@
+variant?: 'enabled' | 'disabled' | 'loading' | 'outline',
+children?: React.Node,
+style?: ViewStyle,
+ +textStyle?: TextStyle,
};
function PrimaryButton(props: Props): React.Node {
- const { onPress, label, variant } = props;
+ const { onPress, label, variant, textStyle } = props;
const styles = useStyles(unboundStyles);
const buttonStyle = React.useMemo(() => {
@@ -36,17 +37,21 @@
variant,
]);
const buttonTextStyle = React.useMemo(() => {
+ let baseStyle;
if (variant === 'disabled') {
- return [styles.buttonText, styles.disabledButtonText];
+ baseStyle = [styles.buttonText, styles.disabledButtonText];
} else if (variant === 'loading') {
- return [styles.buttonText, styles.invisibleLoadingText];
+ baseStyle = [styles.buttonText, styles.invisibleLoadingText];
+ } else {
+ baseStyle = [styles.buttonText];
}
- return styles.buttonText;
+ return textStyle ? [...baseStyle, textStyle] : baseStyle;
}, [
variant,
styles.buttonText,
styles.disabledButtonText,
styles.invisibleLoadingText,
+ textStyle,
]);
const colors = useColors();
diff --git a/native/themes/colors.js b/native/themes/colors.js
--- a/native/themes/colors.js
+++ b/native/themes/colors.js
@@ -120,6 +120,8 @@
panelSeparator: designSystemColors.shadesWhite60,
purpleLink: designSystemColors.violetDark100,
purpleButton: designSystemColors.violetDark100,
+ primaryButton: designSystemColors.shadesBlack95,
+ primaryButtonText: designSystemColors.shadesWhite100,
reactionSelectionPopoverItemBackground: designSystemColors.shadesBlack75,
redText: designSystemColors.errorPrimary,
spoiler: designSystemColors.spoilerColor,
@@ -223,6 +225,8 @@
panelSeparator: designSystemColors.shadesBlack75,
purpleLink: designSystemColors.violetLight100,
purpleButton: designSystemColors.violetDark100,
+ primaryButton: designSystemColors.shadesWhite100,
+ primaryButtonText: designSystemColors.shadesBlack95,
reactionSelectionPopoverItemBackground: designSystemColors.shadesBlack75,
redText: designSystemColors.errorPrimary,
spoiler: designSystemColors.spoilerColor,

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 22, 7:08 AM (19 m, 13 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2689952
Default Alt Text
D13994.diff (9 KB)

Event Timeline