diff --git a/native/avatars/avatar.react.js b/native/avatars/avatar.react.js
index 1da6ed296..bf2b2f3e5 100644
--- a/native/avatars/avatar.react.js
+++ b/native/avatars/avatar.react.js
@@ -1,147 +1,142 @@
// @flow
import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import type { ResolvedClientAvatar } from 'lib/types/avatar-types.js';
import Multimedia from '../media/multimedia.react.js';
-export type AvatarSize =
- | 'micro'
- | 'small'
- | 'large'
- | 'profile'
- | 'profileLarge';
+export type AvatarSize = 'XS' | 'S' | 'M' | 'L' | 'XL';
type Props = {
+avatarInfo: ResolvedClientAvatar,
+size: AvatarSize,
};
function Avatar(props: Props): React.Node {
const { avatarInfo, size } = props;
const containerSizeStyle = React.useMemo(() => {
- if (size === 'micro') {
- return styles.micro;
- } else if (size === 'small') {
+ if (size === 'XS') {
+ return styles.xSmall;
+ } else if (size === 'S') {
return styles.small;
- } else if (size === 'large') {
+ } else if (size === 'M') {
+ return styles.medium;
+ } else if (size === 'L') {
return styles.large;
- } else if (size === 'profile') {
- return styles.profile;
}
- return styles.profileLarge;
+ return styles.xLarge;
}, [size]);
const emojiContainerStyle = React.useMemo(() => {
const containerStyles = [styles.emojiContainer, containerSizeStyle];
if (avatarInfo.type === 'emoji') {
const backgroundColor = { backgroundColor: `#${avatarInfo.color}` };
containerStyles.push(backgroundColor);
}
return containerStyles;
}, [avatarInfo, containerSizeStyle]);
const emojiSizeStyle = React.useMemo(() => {
- if (size === 'micro') {
- return styles.emojiMicro;
- } else if (size === 'small') {
+ if (size === 'XS') {
+ return styles.emojiXSmall;
+ } else if (size === 'S') {
return styles.emojiSmall;
- } else if (size === 'large') {
+ } else if (size === 'M') {
+ return styles.emojiMedium;
+ } else if (size === 'L') {
return styles.emojiLarge;
- } else if (size === 'profile') {
- return styles.emojiProfile;
}
- return styles.emojiProfileLarge;
+ return styles.emojiXLarge;
}, [size]);
const avatar = React.useMemo(() => {
if (avatarInfo.type === 'image') {
const avatarMediaInfo = {
type: 'photo',
uri: avatarInfo.uri,
};
return (
);
}
return (
{avatarInfo.emoji}
);
}, [
avatarInfo.emoji,
avatarInfo.type,
avatarInfo.uri,
containerSizeStyle,
emojiContainerStyle,
emojiSizeStyle,
]);
return avatar;
}
const styles = StyleSheet.create({
emojiContainer: {
alignItems: 'center',
justifyContent: 'center',
},
emojiLarge: {
- fontSize: 28,
+ fontSize: 64,
textAlign: 'center',
},
- emojiMicro: {
- fontSize: 9,
+ emojiMedium: {
+ fontSize: 28,
textAlign: 'center',
},
- emojiProfile: {
- fontSize: 64,
+ emojiSmall: {
+ fontSize: 14,
textAlign: 'center',
},
- emojiProfileLarge: {
+ emojiXLarge: {
fontSize: 80,
textAlign: 'center',
},
- emojiSmall: {
- fontSize: 14,
+ emojiXSmall: {
+ fontSize: 9,
textAlign: 'center',
},
imageContainer: {
overflow: 'hidden',
},
large: {
- borderRadius: 20,
- height: 40,
- width: 40,
- },
- micro: {
- borderRadius: 8,
- height: 16,
- width: 16,
- },
- profile: {
borderRadius: 45,
height: 90,
width: 90,
},
- profileLarge: {
- borderRadius: 56,
- height: 112,
- width: 112,
+ medium: {
+ borderRadius: 20,
+ height: 40,
+ width: 40,
},
small: {
borderRadius: 12,
height: 24,
width: 24,
},
+ xLarge: {
+ borderRadius: 56,
+ height: 112,
+ width: 112,
+ },
+ xSmall: {
+ borderRadius: 8,
+ height: 16,
+ width: 16,
+ },
});
export default Avatar;
diff --git a/native/avatars/edit-thread-avatar.react.js b/native/avatars/edit-thread-avatar.react.js
index 979a216af..d374ab2e2 100644
--- a/native/avatars/edit-thread-avatar.react.js
+++ b/native/avatars/edit-thread-avatar.react.js
@@ -1,121 +1,121 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { ActivityIndicator, TouchableOpacity, View } from 'react-native';
import { EditThreadAvatarContext } from 'lib/components/base-edit-thread-avatar-provider.react.js';
import type { RawThreadInfo, ThreadInfo } from 'lib/types/thread-types.js';
import {
useNativeSetThreadAvatar,
useSelectFromGalleryAndUpdateThreadAvatar,
useShowAvatarActionSheet,
} from './avatar-hooks.js';
import EditAvatarBadge from './edit-avatar-badge.react.js';
import ThreadAvatar from './thread-avatar.react.js';
import {
EmojiThreadAvatarCreationRouteName,
ThreadAvatarCameraModalRouteName,
} from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
type Props = {
+threadInfo: RawThreadInfo | ThreadInfo,
+disabled?: boolean,
};
function EditThreadAvatar(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const { threadInfo, disabled } = props;
const editThreadAvatarContext = React.useContext(EditThreadAvatarContext);
invariant(editThreadAvatarContext, 'editThreadAvatarContext should be set');
const { threadAvatarSaveInProgress } = editThreadAvatarContext;
const nativeSetThreadAvatar = useNativeSetThreadAvatar();
const selectFromGalleryAndUpdateThreadAvatar =
useSelectFromGalleryAndUpdateThreadAvatar();
const { navigate } = useNavigation();
const navigateToThreadEmojiAvatarCreation = React.useCallback(() => {
navigate<'EmojiThreadAvatarCreation'>({
name: EmojiThreadAvatarCreationRouteName,
params: {
threadInfo,
},
});
}, [navigate, threadInfo]);
const selectFromGallery = React.useCallback(
() => selectFromGalleryAndUpdateThreadAvatar(threadInfo.id),
[selectFromGalleryAndUpdateThreadAvatar, threadInfo.id],
);
const navigateToCamera = React.useCallback(() => {
navigate<'ThreadAvatarCameraModal'>({
name: ThreadAvatarCameraModalRouteName,
params: { threadID: threadInfo.id },
});
}, [navigate, threadInfo.id]);
const removeAvatar = React.useCallback(
() => nativeSetThreadAvatar(threadInfo.id, { type: 'remove' }),
[nativeSetThreadAvatar, threadInfo.id],
);
const actionSheetConfig = React.useMemo(() => {
const configOptions = [
{ id: 'emoji', onPress: navigateToThreadEmojiAvatarCreation },
{ id: 'image', onPress: selectFromGallery },
{ id: 'camera', onPress: navigateToCamera },
];
if (threadInfo.avatar) {
configOptions.push({ id: 'remove', onPress: removeAvatar });
}
return configOptions;
}, [
navigateToCamera,
navigateToThreadEmojiAvatarCreation,
removeAvatar,
selectFromGallery,
threadInfo.avatar,
]);
const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig);
let spinner;
if (threadAvatarSaveInProgress) {
spinner = (
);
}
return (
-
+
{spinner}
{!disabled ? : null}
);
}
const unboundStyles = {
spinnerContainer: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
};
export default EditThreadAvatar;
diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js
index 89424f670..0f5ac2c4e 100644
--- a/native/avatars/edit-user-avatar.react.js
+++ b/native/avatars/edit-user-avatar.react.js
@@ -1,157 +1,157 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { ActivityIndicator, TouchableOpacity, View } from 'react-native';
import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js';
import { useENSAvatar } from 'lib/hooks/ens-cache.js';
import { getETHAddressForUserInfo } from 'lib/shared/account-utils.js';
import type { GenericUserInfoWithAvatar } from 'lib/types/avatar-types.js';
import {
useNativeSetUserAvatar,
useSelectFromGalleryAndUpdateUserAvatar,
useShowAvatarActionSheet,
} from './avatar-hooks.js';
import EditAvatarBadge from './edit-avatar-badge.react.js';
import UserAvatar from './user-avatar.react.js';
import {
EmojiUserAvatarCreationRouteName,
UserAvatarCameraModalRouteName,
EmojiAvatarSelectionRouteName,
RegistrationUserAvatarCameraModalRouteName,
} from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
type Props =
| { +userID: ?string, +disabled?: boolean }
| {
+userInfo: ?GenericUserInfoWithAvatar,
+disabled?: boolean,
+prefetchedAvatarURI: ?string,
};
function EditUserAvatar(props: Props): React.Node {
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const { userAvatarSaveInProgress, getRegistrationModeEnabled } =
editUserAvatarContext;
const nativeSetUserAvatar = useNativeSetUserAvatar();
const selectFromGalleryAndUpdateUserAvatar =
useSelectFromGalleryAndUpdateUserAvatar();
const currentUserInfo = useSelector(state => state.currentUserInfo);
const userInfoProp = props.userInfo;
const userInfo: ?GenericUserInfoWithAvatar = userInfoProp ?? currentUserInfo;
const ethAddress = React.useMemo(
() => getETHAddressForUserInfo(userInfo),
[userInfo],
);
const fetchedENSAvatarURI = useENSAvatar(ethAddress);
const ensAvatarURI = fetchedENSAvatarURI ?? props.prefetchedAvatarURI;
const { navigate } = useNavigation();
const usernameOrEthAddress = userInfo?.username;
const navigateToEmojiSelection = React.useCallback(() => {
if (!getRegistrationModeEnabled()) {
navigate(EmojiUserAvatarCreationRouteName);
return;
}
navigate<'EmojiAvatarSelection'>({
name: EmojiAvatarSelectionRouteName,
params: { usernameOrEthAddress },
});
}, [navigate, getRegistrationModeEnabled, usernameOrEthAddress]);
const navigateToCamera = React.useCallback(() => {
navigate(
getRegistrationModeEnabled()
? RegistrationUserAvatarCameraModalRouteName
: UserAvatarCameraModalRouteName,
);
}, [navigate, getRegistrationModeEnabled]);
const setENSUserAvatar = React.useCallback(() => {
nativeSetUserAvatar({ type: 'ens' });
}, [nativeSetUserAvatar]);
const removeUserAvatar = React.useCallback(() => {
nativeSetUserAvatar({ type: 'remove' });
}, [nativeSetUserAvatar]);
const hasCurrentAvatar = !!userInfo?.avatar;
const actionSheetConfig = React.useMemo(() => {
const configOptions = [
{ id: 'emoji', onPress: navigateToEmojiSelection },
{ id: 'image', onPress: selectFromGalleryAndUpdateUserAvatar },
{ id: 'camera', onPress: navigateToCamera },
];
if (ensAvatarURI) {
configOptions.push({ id: 'ens', onPress: setENSUserAvatar });
}
if (hasCurrentAvatar) {
configOptions.push({ id: 'remove', onPress: removeUserAvatar });
}
return configOptions;
}, [
hasCurrentAvatar,
ensAvatarURI,
navigateToCamera,
navigateToEmojiSelection,
removeUserAvatar,
setENSUserAvatar,
selectFromGalleryAndUpdateUserAvatar,
]);
const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig);
const styles = useStyles(unboundStyles);
let spinner;
if (userAvatarSaveInProgress) {
spinner = (
);
}
const { userID } = props;
const userAvatar = userID ? (
-
+
) : (
-
+
);
const { disabled } = props;
return (
{userAvatar}
{spinner}
{!disabled ? : null}
);
}
const unboundStyles = {
spinnerContainer: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
};
export default EditUserAvatar;
diff --git a/native/avatars/emoji-avatar-creation.react.js b/native/avatars/emoji-avatar-creation.react.js
index f160116a1..79636e0e6 100644
--- a/native/avatars/emoji-avatar-creation.react.js
+++ b/native/avatars/emoji-avatar-creation.react.js
@@ -1,220 +1,220 @@
// @flow
import * as React from 'react';
import {
View,
Text,
TouchableWithoutFeedback,
ActivityIndicator,
} from 'react-native';
import type {
UpdateUserAvatarRequest,
ClientEmojiAvatar,
} from 'lib/types/avatar-types';
import Avatar from './avatar.react.js';
import Button from '../components/button.react.js';
import ColorRows from '../components/color-rows.react.js';
import EmojiKeyboard from '../components/emoji-keyboard.react.js';
import { useStyles } from '../themes/colors.js';
type Props = {
+saveAvatarCall: (newAvatarRequest: UpdateUserAvatarRequest) => mixed,
+saveAvatarCallLoading: boolean,
+savedEmojiAvatarFunc: () => ClientEmojiAvatar,
};
function EmojiAvatarCreation(props: Props): React.Node {
const { saveAvatarCall, saveAvatarCallLoading, savedEmojiAvatarFunc } = props;
const [pendingEmoji, setPendingEmoji] = React.useState(
() => savedEmojiAvatarFunc().emoji,
);
const [pendingColor, setPendingColor] = React.useState(
() => savedEmojiAvatarFunc().color,
);
const [emojiKeyboardOpen, setEmojiKeyboardOpen] =
React.useState(false);
const styles = useStyles(unboundStyles);
const onPressEditEmoji = React.useCallback(() => {
setEmojiKeyboardOpen(true);
}, []);
const onPressSetAvatar = React.useCallback(() => {
const newEmojiAvatarRequest = {
type: 'emoji',
emoji: pendingEmoji,
color: pendingColor,
};
saveAvatarCall(newEmojiAvatarRequest);
}, [pendingColor, pendingEmoji, saveAvatarCall]);
const onPressReset = React.useCallback(() => {
const resetEmojiAvatar = savedEmojiAvatarFunc();
setPendingEmoji(resetEmojiAvatar.emoji);
setPendingColor(resetEmojiAvatar.color);
}, [savedEmojiAvatarFunc]);
const onEmojiSelected = React.useCallback(emoji => {
setPendingEmoji(emoji.emoji);
}, []);
const onEmojiKeyboardClose = React.useCallback(
() => setEmojiKeyboardOpen(false),
[],
);
const stagedAvatarInfo: ClientEmojiAvatar = React.useMemo(
() => ({
type: 'emoji',
emoji: pendingEmoji,
color: pendingColor,
}),
[pendingColor, pendingEmoji],
);
const loadingContainer = React.useMemo(() => {
if (!saveAvatarCallLoading) {
return null;
}
return (
);
}, [saveAvatarCallLoading, styles.loadingContainer]);
const alreadySelectedEmojis = React.useMemo(
() => [pendingEmoji],
[pendingEmoji],
);
return (
-
+
{loadingContainer}
Edit Emoji
);
}
const unboundStyles = {
container: {
flexGrow: 1,
flex: 1,
justifyContent: 'space-between',
},
emojiAvatarCreationContainer: {
paddingTop: 16,
},
stagedAvatarSection: {
backgroundColor: 'panelForeground',
paddingVertical: 24,
alignItems: 'center',
},
editEmojiText: {
color: 'purpleLink',
marginTop: 16,
fontWeight: '500',
fontSize: 16,
lineHeight: 24,
textAlign: 'center',
},
colorRowsSection: {
paddingVertical: 24,
marginTop: 24,
backgroundColor: 'panelForeground',
alignItems: 'center',
},
selectedColorOuterRing: {
backgroundColor: 'modalSubtext',
},
buttonsContainer: {
flexGrow: 1,
paddingHorizontal: 16,
paddingBottom: 8,
justifyContent: 'flex-end',
},
saveButton: {
backgroundColor: 'purpleButton',
paddingVertical: 12,
borderRadius: 8,
},
saveButtonText: {
color: 'whiteText',
textAlign: 'center',
fontWeight: '500',
fontSize: 16,
lineHeight: 24,
},
resetButton: {
padding: 12,
borderRadius: 8,
marginTop: 8,
alignSelf: 'center',
},
resetButtonText: {
color: 'redText',
textAlign: 'center',
fontWeight: '500',
fontSize: 16,
lineHeight: 24,
},
loadingContainer: {
position: 'absolute',
backgroundColor: 'black',
width: 112,
height: 112,
borderRadius: 56,
opacity: 0.6,
justifyContent: 'center',
},
};
export default EmojiAvatarCreation;
diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js
index d1a66d317..0bbe08130 100644
--- a/native/chat/chat-thread-list-item.react.js
+++ b/native/chat/chat-thread-list-item.react.js
@@ -1,297 +1,297 @@
// @flow
import * as React from 'react';
import { Text, View } from 'react-native';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { shortAbsoluteDate } from 'lib/utils/date-utils.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js';
import MessagePreview from './message-preview.react.js';
import SwipeableThread from './swipeable-thread.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import Button from '../components/button.react.js';
import SingleLine from '../components/single-line.react.js';
import ThreadAncestorsLabel from '../components/thread-ancestors-label.react.js';
import UnreadDot from '../components/unread-dot.react.js';
import { useColors, useStyles } from '../themes/colors.js';
type Props = {
+data: ChatThreadItem,
+onPressItem: (
threadInfo: ThreadInfo,
pendingPersonalThreadUserInfo?: UserInfo,
) => void,
+onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void,
+onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
+currentlyOpenedSwipeableId: string,
};
function ChatThreadListItem({
data,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
currentlyOpenedSwipeableId,
}: Props): React.Node {
const styles = useStyles(unboundStyles);
const colors = useColors();
const lastMessage = React.useMemo(() => {
const mostRecentMessageInfo = data.mostRecentMessageInfo;
if (!mostRecentMessageInfo) {
return (
No messages
);
}
return (
);
}, [data.mostRecentMessageInfo, data.threadInfo, styles]);
const numOfSidebarsWithExtendedArrow =
data.sidebars.filter(sidebarItem => sidebarItem.type === 'sidebar').length -
1;
const sidebars = React.useMemo(
() =>
data.sidebars.map((sidebarItem, index) => {
if (sidebarItem.type === 'sidebar') {
const { type, ...sidebarInfo } = sidebarItem;
return (
);
} else if (sidebarItem.type === 'seeMore') {
return (
);
} else {
return ;
}
}),
[
currentlyOpenedSwipeableId,
data.sidebars,
data.threadInfo,
numOfSidebarsWithExtendedArrow,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
styles.spacer,
],
);
const onPress = React.useCallback(() => {
onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo);
}, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]);
const threadNameStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.threadName;
}
return [styles.threadName, styles.unreadThreadName];
}, [
data.threadInfo.currentUser.unread,
styles.threadName,
styles.unreadThreadName,
]);
const lastActivity = shortAbsoluteDate(data.lastUpdatedTime);
const lastActivityStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.lastActivity;
}
return [styles.lastActivity, styles.unreadLastActivity];
}, [
data.threadInfo.currentUser.unread,
styles.lastActivity,
styles.unreadLastActivity,
]);
const resolvedThreadInfo = useResolvedThreadInfo(data.threadInfo);
const unreadDot = React.useMemo(
() => (
),
[data.threadInfo.currentUser.unread, styles.avatarContainer],
);
const threadAvatar = React.useMemo(
() => (
-
+
),
[data.threadInfo, styles.avatarContainer],
);
const threadDetails = React.useMemo(
() => (
{resolvedThreadInfo.uiName}
{lastMessage}
{lastActivity}
),
[
data.threadInfo,
lastActivity,
lastActivityStyle,
lastMessage,
resolvedThreadInfo.uiName,
styles.row,
styles.threadDetails,
threadNameStyle,
],
);
const swipeableThreadContent = React.useMemo(
() => (
),
[
colors.listIosHighlightUnderlay,
onPress,
styles.container,
styles.content,
threadAvatar,
threadDetails,
unreadDot,
],
);
const swipeableThread = React.useMemo(
() => (
{swipeableThreadContent}
),
[
currentlyOpenedSwipeableId,
data.mostRecentNonLocalMessage,
data.threadInfo,
onSwipeableWillOpen,
swipeableThreadContent,
],
);
const chatThreadListItem = React.useMemo(
() => (
<>
{swipeableThread}
{sidebars}
>
),
[sidebars, swipeableThread],
);
return chatThreadListItem;
}
const chatThreadListItemHeight = 70;
const spacerHeight = 6;
const unboundStyles = {
container: {
height: chatThreadListItemHeight,
justifyContent: 'center',
backgroundColor: 'listBackground',
},
content: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
avatarContainer: {
marginLeft: 6,
marginBottom: 12,
},
threadDetails: {
paddingLeft: 12,
paddingRight: 18,
justifyContent: 'center',
flex: 1,
marginTop: 5,
},
lastActivity: {
color: 'listForegroundTertiaryLabel',
fontSize: 14,
marginLeft: 10,
},
unreadLastActivity: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
noMessages: {
color: 'listForegroundTertiaryLabel',
flex: 1,
fontSize: 14,
fontStyle: 'italic',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
threadName: {
color: 'listForegroundSecondaryLabel',
flex: 1,
fontSize: 21,
},
unreadThreadName: {
color: 'listForegroundLabel',
fontWeight: '500',
},
spacer: {
height: spacerHeight,
},
};
export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight };
diff --git a/native/chat/composed-message.react.js b/native/chat/composed-message.react.js
index eb67653f5..f0c27e514 100644
--- a/native/chat/composed-message.react.js
+++ b/native/chat/composed-message.react.js
@@ -1,415 +1,415 @@
// @flow
import Icon from '@expo/vector-icons/Feather.js';
import invariant from 'invariant';
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import {
useDerivedValue,
withTiming,
interpolateColor,
useAnimatedStyle,
} from 'react-native-reanimated';
import { getMessageLabel } from 'lib/shared/edit-messages-utils.js';
import { createMessageReply } from 'lib/shared/message-utils.js';
import { assertComposableMessageType } from 'lib/types/message-types.js';
import {
clusterEndHeight,
composedMessageStyle,
avatarOffset,
} from './chat-constants.js';
import { useComposedMessageMaxWidth } from './composed-message-width.js';
import { FailedSend } from './failed-send.react.js';
import { InlineEngagement } from './inline-engagement.react.js';
import { MessageEditingContext } from './message-editing-context.react.js';
import { MessageHeader } from './message-header.react.js';
import { useNavigateToSidebar } from './sidebar-navigation.js';
import SwipeableMessage from './swipeable-message.react.js';
import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import CommIcon from '../components/comm-icon.react.js';
import { InputStateContext } from '../input/input-state.js';
import { useColors } from '../themes/colors.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js';
type SwipeOptions = 'reply' | 'sidebar' | 'both' | 'none';
type Props = {
...React.ElementConfig,
+item: ChatMessageInfoItemWithHeight,
+sendFailed: boolean,
+focused: boolean,
+swipeOptions: SwipeOptions,
+shouldDisplayPinIndicator: boolean,
+children: React.Node,
};
const ConnectedComposedMessage: React.ComponentType = React.memo(
function ConnectedComposedMessage(props: Props) {
const composedMessageMaxWidth = useComposedMessageMaxWidth();
const colors = useColors();
const inputState = React.useContext(InputStateContext);
const navigateToSidebar = useNavigateToSidebar(props.item);
const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item);
const deliveryIconOpacity = useDeliveryIconOpacity(props.item);
const messageEditingContext = React.useContext(MessageEditingContext);
const progress = useDerivedValue(() => {
const isThisThread =
messageEditingContext?.editState.editedMessage?.threadID ===
props.item.threadInfo.id;
const isHighlighted =
messageEditingContext?.editState.editedMessage?.id ===
props.item.messageInfo.id && isThisThread;
return withTiming(isHighlighted ? 1 : 0);
});
const editedMessageStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
progress.value,
[0, 1],
['transparent', `#${props.item.threadInfo.color}40`],
);
return {
backgroundColor,
};
});
assertComposableMessageType(props.item.messageInfo.type);
const {
item,
sendFailed,
swipeOptions,
shouldDisplayPinIndicator,
children,
focused,
...viewProps
} = props;
const { hasBeenEdited, isPinned } = item;
const { id, creator } = item.messageInfo;
const { isViewer } = creator;
const alignStyle = isViewer
? styles.rightChatBubble
: styles.leftChatBubble;
const containerStyle = React.useMemo(() => {
let containerMarginBottom = 5;
if (item.endsCluster) {
containerMarginBottom += clusterEndHeight;
}
return { marginBottom: containerMarginBottom };
}, [item.endsCluster]);
const messageBoxContainerStyle = React.useMemo(
() => [
styles.messageBoxContainer,
isViewer ? styles.rightChatContainer : styles.leftChatContainer,
],
[isViewer],
);
const deliveryIcon = React.useMemo(() => {
if (!isViewer) {
return undefined;
}
let deliveryIconName;
let deliveryIconColor = `#${item.threadInfo.color}`;
if (id !== null && id !== undefined) {
deliveryIconName = 'check-circle';
} else if (sendFailed) {
deliveryIconName = 'x-circle';
deliveryIconColor = colors.redText;
} else {
deliveryIconName = 'circle';
}
const animatedStyle: AnimatedStyleObj = { opacity: deliveryIconOpacity };
return (
);
}, [
colors.redText,
deliveryIconOpacity,
id,
isViewer,
item.threadInfo.color,
sendFailed,
]);
const editInputMessage = inputState?.editInputMessage;
const reply = React.useCallback(() => {
invariant(editInputMessage, 'editInputMessage should be set in reply');
invariant(item.messageInfo.text, 'text should be set in reply');
editInputMessage({
message: createMessageReply(item.messageInfo.text),
mode: 'prepend',
});
}, [editInputMessage, item.messageInfo.text]);
const triggerReply =
swipeOptions === 'reply' || swipeOptions === 'both' ? reply : undefined;
const triggerSidebar =
swipeOptions === 'sidebar' || swipeOptions === 'both'
? navigateToSidebar
: undefined;
const avatar = React.useMemo(() => {
if (!isViewer && item.endsCluster) {
return (
-
+
);
} else if (!isViewer) {
return ;
} else {
return undefined;
}
}, [isViewer, item.endsCluster, item.messageInfo.creator.id]);
const pinIconPositioning = isViewer ? 'left' : 'right';
const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin';
const messageBoxTopLevelContainerStyle =
pinIconPositioning === 'left'
? styles.rightMessageBoxTopLevelContainerStyle
: styles.leftMessageBoxTopLevelContainerStyle;
const pinIcon = React.useMemo(() => {
if (!isPinned || !shouldDisplayPinIndicator) {
return undefined;
}
return (
);
}, [
isPinned,
item.threadInfo.color,
pinIconName,
shouldDisplayPinIndicator,
]);
const messageBoxStyle = React.useMemo(
() => ({
opacity: contentAndHeaderOpacity,
maxWidth: composedMessageMaxWidth,
}),
[composedMessageMaxWidth, contentAndHeaderOpacity],
);
const messageBox = React.useMemo(
() => (
{pinIcon}
{avatar}
{children}
),
[
avatar,
children,
isViewer,
item.threadInfo.color,
messageBoxContainerStyle,
messageBoxStyle,
messageBoxTopLevelContainerStyle,
pinIcon,
triggerReply,
triggerSidebar,
],
);
const inlineEngagement = React.useMemo(() => {
const label = getMessageLabel(hasBeenEdited, item.threadInfo.id);
if (
!item.threadCreatedFromMessage &&
Object.keys(item.reactions).length <= 0 &&
!label
) {
return undefined;
}
const positioning = isViewer ? 'right' : 'left';
return (
);
}, [
hasBeenEdited,
isViewer,
item.messageInfo,
item.reactions,
item.threadCreatedFromMessage,
item.threadInfo,
]);
const viewStyle = React.useMemo(() => {
const baseStyle = [styles.alignment];
if (__DEV__) {
return baseStyle;
}
if (item.messageShapeType === 'text') {
baseStyle.push({ height: item.contentHeight });
} else if (item.messageShapeType === 'multimedia') {
const height = item.inlineEngagementHeight
? item.contentHeight + item.inlineEngagementHeight
: item.contentHeight;
baseStyle.push({ height });
}
return baseStyle;
}, [
item.contentHeight,
item.inlineEngagementHeight,
item.messageShapeType,
]);
const messageHeaderStyle = React.useMemo(
() => ({
opacity: contentAndHeaderOpacity,
}),
[contentAndHeaderOpacity],
);
const animatedContainerStyle = React.useMemo(
() => [containerStyle, editedMessageStyle],
[containerStyle, editedMessageStyle],
);
const contentStyle = React.useMemo(
() => [styles.content, alignStyle],
[alignStyle],
);
const failedSend = React.useMemo(
() => (sendFailed ? : undefined),
[item, sendFailed],
);
const composedMessage = React.useMemo(() => {
return (
{deliveryIcon}
{messageBox}
{inlineEngagement}
{failedSend}
);
}, [
animatedContainerStyle,
contentStyle,
deliveryIcon,
failedSend,
focused,
inlineEngagement,
item,
messageBox,
messageHeaderStyle,
viewProps,
viewStyle,
]);
return composedMessage;
},
);
const styles = StyleSheet.create({
alignment: {
marginLeft: composedMessageStyle.marginLeft,
marginRight: composedMessageStyle.marginRight,
},
avatarContainer: {
marginRight: 8,
},
avatarOffset: {
width: avatarOffset,
},
content: {
alignItems: 'center',
flexDirection: 'row-reverse',
},
icon: {
fontSize: 16,
textAlign: 'center',
},
iconContainer: {
marginLeft: 2,
width: 16,
},
leftChatBubble: {
justifyContent: 'flex-end',
},
leftChatContainer: {
alignItems: 'flex-start',
},
leftMessageBoxTopLevelContainerStyle: {
flexDirection: 'row-reverse',
},
messageBoxContainer: {
marginRight: 5,
},
pinIconContainer: {
marginRight: 4,
marginTop: 4,
},
rightChatBubble: {
justifyContent: 'flex-start',
},
rightChatContainer: {
alignItems: 'flex-end',
},
rightMessageBoxTopLevelContainerStyle: {
flexDirection: 'row',
},
swipeableContainer: {
alignItems: 'flex-end',
flexDirection: 'row',
},
});
export default ConnectedComposedMessage;
diff --git a/native/chat/mention-typeahead-tooltip-button.react.js b/native/chat/mention-typeahead-tooltip-button.react.js
index b350e4671..b3fdee1ea 100644
--- a/native/chat/mention-typeahead-tooltip-button.react.js
+++ b/native/chat/mention-typeahead-tooltip-button.react.js
@@ -1,70 +1,67 @@
// @flow
import * as React from 'react';
import { Text } from 'react-native';
import type {
TypeaheadTooltipActionItem,
MentionTypeaheadSuggestionItem,
} from 'lib/shared/mention-utils.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import Button from '../components/button.react.js';
import { useStyles } from '../themes/colors.js';
type Props = {
+item: TypeaheadTooltipActionItem,
};
function MentionTypeaheadTooltipButton(props: Props): React.Node {
const { item } = props;
const styles = useStyles(unboundStyles);
let avatarComponent = null;
let typeaheadTooltipButtonText = null;
if (item.actionButtonContent.type === 'user') {
avatarComponent = (
-
+
);
typeaheadTooltipButtonText = item.actionButtonContent.userInfo.username;
} else if (item.actionButtonContent.type === 'chat') {
typeaheadTooltipButtonText = item.actionButtonContent.threadInfo.uiName;
avatarComponent = (
-
+
);
}
return (
);
}
const unboundStyles = {
button: {
alignItems: 'center',
flexDirection: 'row',
innerHeight: 24,
padding: 8,
color: 'typeaheadTooltipText',
},
buttonLabel: {
color: 'white',
fontSize: 16,
fontWeight: '400',
marginLeft: 8,
},
};
export default MentionTypeaheadTooltipButton;
diff --git a/native/chat/message-list-header-title.react.js b/native/chat/message-list-header-title.react.js
index 6c3162236..3b0fb9b65 100644
--- a/native/chat/message-list-header-title.react.js
+++ b/native/chat/message-list-header-title.react.js
@@ -1,107 +1,107 @@
// @flow
import {
HeaderTitle,
type HeaderTitleInputProps,
} from '@react-navigation/elements';
import * as React from 'react';
import { View } from 'react-native';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import { firstLine } from 'lib/utils/string-utils.js';
import type { ChatNavigationProp } from './chat.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import Button from '../components/button.react.js';
import { ThreadSettingsRouteName } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
type BaseProps = {
+threadInfo: ThreadInfo,
+navigate: $PropertyType, 'navigate'>,
+isSearchEmpty: boolean,
+areSettingsEnabled: boolean,
...HeaderTitleInputProps,
};
type Props = {
...BaseProps,
+styles: typeof unboundStyles,
+title: string,
};
class MessageListHeaderTitle extends React.PureComponent {
render() {
const {
threadInfo,
navigate,
isSearchEmpty,
areSettingsEnabled,
styles,
title,
...rest
} = this.props;
let avatar;
if (!isSearchEmpty) {
avatar = (
-
+
);
}
return (
);
}
onPress = () => {
const { threadInfo } = this.props;
this.props.navigate<'ThreadSettings'>({
name: ThreadSettingsRouteName,
params: { threadInfo },
key: `${ThreadSettingsRouteName}${threadInfo.id}`,
});
};
}
const unboundStyles = {
avatarContainer: {
marginRight: 8,
},
button: {
flex: 1,
},
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
};
const ConnectedMessageListHeaderTitle: React.ComponentType =
React.memo(function ConnectedMessageListHeaderTitle(
props: BaseProps,
) {
const styles = useStyles(unboundStyles);
const { uiName } = useResolvedThreadInfo(props.threadInfo);
const { isSearchEmpty } = props;
const title = isSearchEmpty ? 'New Message' : uiName;
return ;
});
export default ConnectedMessageListHeaderTitle;
diff --git a/native/chat/message-reactions-modal.react.js b/native/chat/message-reactions-modal.react.js
index c93b0373e..279855fa2 100644
--- a/native/chat/message-reactions-modal.react.js
+++ b/native/chat/message-reactions-modal.react.js
@@ -1,151 +1,151 @@
// @flow
import Icon from '@expo/vector-icons/FontAwesome.js';
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
import { View, Text, FlatList, TouchableHighlight } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { useMessageReactionsList } from 'lib/shared/reaction-utils.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import Modal from '../components/modal.react.js';
import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useColors, useStyles } from '../themes/colors.js';
export type MessageReactionsModalParams = {
+reactions: ReactionInfo,
};
type Props = {
+navigation: RootNavigationProp<'MessageReactionsModal'>,
+route: NavigationRoute<'MessageReactionsModal'>,
};
function MessageReactionsModal(props: Props): React.Node {
const { reactions } = props.route.params;
const styles = useStyles(unboundStyles);
const colors = useColors();
const navigation = useNavigation();
const modalSafeAreaEdges = React.useMemo(() => ['top'], []);
const modalContainerSafeAreaEdges = React.useMemo(() => ['bottom'], []);
const close = React.useCallback(() => navigation.goBack(), [navigation]);
const reactionsListData = useMessageReactionsList(reactions);
const renderItem = React.useCallback(
({ item }) => (
-
+
{item.username}
{item.reaction}
),
[
styles.reactionsListReactionText,
styles.reactionsListRowContainer,
styles.reactionsListUserInfoContainer,
styles.reactionsListUsernameText,
],
);
const itemSeperator = React.useCallback(() => {
return ;
}, [styles.reactionsListItemSeperator]);
return (
All reactions
);
}
const unboundStyles = {
modalStyle: {
// we need to set each margin property explicitly to override
marginLeft: 0,
marginRight: 0,
marginBottom: 0,
marginTop: 0,
justifyContent: 'flex-end',
flex: 0,
borderWidth: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
},
modalContainerStyle: {
justifyContent: 'flex-end',
},
modalContentContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
marginTop: 8,
},
reactionsListContentContainer: {
paddingBottom: 16,
},
reactionsListTitleText: {
color: 'modalForegroundLabel',
fontSize: 18,
},
reactionsListRowContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
reactionsListUserInfoContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
reactionsListUsernameText: {
color: 'modalForegroundLabel',
fontSize: 18,
marginLeft: 8,
},
reactionsListReactionText: {
fontSize: 18,
},
reactionsListItemSeperator: {
height: 16,
},
closeButton: {
borderRadius: 4,
width: 18,
height: 18,
alignItems: 'center',
},
closeIcon: {
color: 'modalBackgroundSecondaryLabel',
},
};
export default MessageReactionsModal;
diff --git a/native/chat/message-tooltip-button-avatar.react.js b/native/chat/message-tooltip-button-avatar.react.js
index d20452c4e..b905a13b8 100644
--- a/native/chat/message-tooltip-button-avatar.react.js
+++ b/native/chat/message-tooltip-button-avatar.react.js
@@ -1,38 +1,38 @@
// @flow
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { avatarOffset } from './chat-constants.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
type Props = {
+item: ChatMessageInfoItemWithHeight,
};
function MessageTooltipButtonAvatar(props: Props): React.Node {
const { item } = props;
if (item.messageInfo.creator.isViewer) {
return null;
}
return (
-
+
);
}
const styles = StyleSheet.create({
avatarContainer: {
bottom: 0,
left: -avatarOffset,
position: 'absolute',
},
});
const MemoizedMessageTooltipButtonAvatar: React.ComponentType =
React.memo(MessageTooltipButtonAvatar);
export default MemoizedMessageTooltipButtonAvatar;
diff --git a/native/chat/settings/thread-settings-child-thread.react.js b/native/chat/settings/thread-settings-child-thread.react.js
index 5e6a0b053..706e75aeb 100644
--- a/native/chat/settings/thread-settings-child-thread.react.js
+++ b/native/chat/settings/thread-settings-child-thread.react.js
@@ -1,83 +1,83 @@
// @flow
import * as React from 'react';
import { View, Platform } from 'react-native';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import ThreadAvatar from '../../avatars/thread-avatar.react.js';
import Button from '../../components/button.react.js';
import ThreadIcon from '../../components/thread-icon.react.js';
import ThreadPill from '../../components/thread-pill.react.js';
import { useColors, useStyles } from '../../themes/colors.js';
import { useNavigateToThread } from '../message-list-types.js';
type Props = {
+threadInfo: ThreadInfo,
+firstListItem: boolean,
+lastListItem: boolean,
};
function ThreadSettingsChildThread(props: Props): React.Node {
const { threadInfo } = props;
const navigateToThread = useNavigateToThread();
const onPress = React.useCallback(() => {
navigateToThread({ threadInfo });
}, [threadInfo, navigateToThread]);
const styles = useStyles(unboundStyles);
const colors = useColors();
const firstItem = props.firstListItem ? null : styles.topBorder;
const lastItem = props.lastListItem ? styles.lastButton : null;
return (
);
}
const unboundStyles = {
avatarContainer: {
marginRight: 8,
},
button: {
flex: 1,
flexDirection: 'row',
paddingVertical: 8,
paddingLeft: 12,
paddingRight: 10,
alignItems: 'center',
},
topBorder: {
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
container: {
backgroundColor: 'panelForeground',
flex: 1,
paddingHorizontal: 12,
},
lastButton: {
paddingBottom: Platform.OS === 'ios' ? 12 : 10,
paddingTop: 8,
},
leftSide: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
};
export default ThreadSettingsChildThread;
diff --git a/native/chat/settings/thread-settings-member.react.js b/native/chat/settings/thread-settings-member.react.js
index b4a0ad782..2b5fe92b4 100644
--- a/native/chat/settings/thread-settings-member.react.js
+++ b/native/chat/settings/thread-settings-member.react.js
@@ -1,291 +1,291 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import {
View,
Text,
Platform,
ActivityIndicator,
TouchableOpacity,
} from 'react-native';
import {
removeUsersFromThreadActionTypes,
changeThreadMemberRolesActionTypes,
} from 'lib/actions/thread-actions.js';
import { useENSNames } from 'lib/hooks/ens-cache.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { getAvailableThreadMemberActions } from 'lib/shared/thread-utils.js';
import { stringForUser } from 'lib/shared/user-utils.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import {
type ThreadInfo,
type RelativeMemberInfo,
} from 'lib/types/thread-types.js';
import type { ThreadSettingsNavigate } from './thread-settings.react.js';
import UserAvatar from '../../avatars/user-avatar.react.js';
import PencilIcon from '../../components/pencil-icon.react.js';
import SingleLine from '../../components/single-line.react.js';
import {
type KeyboardState,
KeyboardContext,
} from '../../keyboard/keyboard-state.js';
import {
OverlayContext,
type OverlayContextType,
} from '../../navigation/overlay-context.js';
import { ThreadSettingsMemberTooltipModalRouteName } from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { type Colors, useColors, useStyles } from '../../themes/colors.js';
import type { VerticalBounds } from '../../types/layout-types.js';
type BaseProps = {
+memberInfo: RelativeMemberInfo,
+threadInfo: ThreadInfo,
+canEdit: boolean,
+navigate: ThreadSettingsNavigate,
+firstListItem: boolean,
+lastListItem: boolean,
+verticalBounds: ?VerticalBounds,
+threadSettingsRouteKey: string,
};
type Props = {
...BaseProps,
// Redux state
+removeUserLoadingStatus: LoadingStatus,
+changeRoleLoadingStatus: LoadingStatus,
+colors: Colors,
+styles: typeof unboundStyles,
// withKeyboardState
+keyboardState: ?KeyboardState,
// withOverlayContext
+overlayContext: ?OverlayContextType,
};
class ThreadSettingsMember extends React.PureComponent {
editButton: ?React.ElementRef;
render() {
const userText = stringForUser(this.props.memberInfo);
let usernameInfo = null;
if (this.props.memberInfo.username) {
usernameInfo = (
{userText}
);
} else {
usernameInfo = (
{userText}
);
}
let editButton = null;
if (
this.props.removeUserLoadingStatus === 'loading' ||
this.props.changeRoleLoadingStatus === 'loading'
) {
editButton = (
);
} else if (
getAvailableThreadMemberActions(
this.props.memberInfo,
this.props.threadInfo,
this.props.canEdit,
).length !== 0
) {
editButton = (
);
}
const roleName =
this.props.memberInfo.role &&
this.props.threadInfo.roles[this.props.memberInfo.role].name;
const roleInfo = (
{roleName}
);
const firstItem = this.props.firstListItem
? null
: this.props.styles.topBorder;
const lastItem = this.props.lastListItem
? this.props.styles.lastInnerContainer
: null;
return (
-
+
{usernameInfo}
{editButton}
{roleInfo}
);
}
editButtonRef = (editButton: ?React.ElementRef) => {
this.editButton = editButton;
};
onEditButtonLayout = () => {};
onPressEdit = () => {
if (this.dismissKeyboardIfShowing()) {
return;
}
const {
editButton,
props: { verticalBounds },
} = this;
if (!editButton || !verticalBounds) {
return;
}
const { overlayContext } = this.props;
invariant(
overlayContext,
'ThreadSettingsMember should have OverlayContext',
);
overlayContext.setScrollBlockingModalStatus('open');
editButton.measure((x, y, width, height, pageX, pageY) => {
const coordinates = { x: pageX, y: pageY, width, height };
this.props.navigate<'ThreadSettingsMemberTooltipModal'>({
name: ThreadSettingsMemberTooltipModalRouteName,
params: {
presentedFrom: this.props.threadSettingsRouteKey,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs: getAvailableThreadMemberActions(
this.props.memberInfo,
this.props.threadInfo,
this.props.canEdit,
),
memberInfo: this.props.memberInfo,
threadInfo: this.props.threadInfo,
},
});
});
};
dismissKeyboardIfShowing = () => {
const { keyboardState } = this.props;
return !!(keyboardState && keyboardState.dismissKeyboardIfShowing());
};
}
const unboundStyles = {
anonymous: {
color: 'panelForegroundTertiaryLabel',
fontStyle: 'italic',
},
container: {
backgroundColor: 'panelForeground',
flex: 1,
paddingHorizontal: 12,
},
editButton: {
paddingLeft: 10,
},
topBorder: {
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
innerContainer: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 8,
},
lastInnerContainer: {
paddingBottom: Platform.OS === 'ios' ? 12 : 10,
},
role: {
color: 'panelForegroundTertiaryLabel',
flex: 1,
fontSize: 14,
paddingTop: 4,
},
row: {
flex: 1,
flexDirection: 'row',
},
userInfoContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
username: {
color: 'panelForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
lineHeight: 20,
marginLeft: 8,
},
};
const ConnectedThreadSettingsMember: React.ComponentType =
React.memo(function ConnectedThreadSettingsMember(
props: BaseProps,
) {
const memberID = props.memberInfo.id;
const removeUserLoadingStatus = useSelector(state =>
createLoadingStatusSelector(
removeUsersFromThreadActionTypes,
`${removeUsersFromThreadActionTypes.started}:${memberID}`,
)(state),
);
const changeRoleLoadingStatus = useSelector(state =>
createLoadingStatusSelector(
changeThreadMemberRolesActionTypes,
`${changeThreadMemberRolesActionTypes.started}:${memberID}`,
)(state),
);
const [memberInfo] = useENSNames([props.memberInfo]);
const colors = useColors();
const styles = useStyles(unboundStyles);
const keyboardState = React.useContext(KeyboardContext);
const overlayContext = React.useContext(OverlayContext);
return (
);
});
export default ConnectedThreadSettingsMember;
diff --git a/native/chat/settings/thread-settings-parent.react.js b/native/chat/settings/thread-settings-parent.react.js
index bad3f1e32..65f27a411 100644
--- a/native/chat/settings/thread-settings-parent.react.js
+++ b/native/chat/settings/thread-settings-parent.react.js
@@ -1,115 +1,115 @@
// @flow
import * as React from 'react';
import { Text, View } from 'react-native';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import ThreadAvatar from '../../avatars/thread-avatar.react.js';
import Button from '../../components/button.react.js';
import ThreadPill from '../../components/thread-pill.react.js';
import { useStyles } from '../../themes/colors.js';
import { useNavigateToThread } from '../message-list-types.js';
type ParentButtonProps = {
+parentThreadInfo: ThreadInfo,
};
function ParentButton(props: ParentButtonProps): React.Node {
const styles = useStyles(unboundStyles);
const navigateToThread = useNavigateToThread();
const onPressParentThread = React.useCallback(() => {
navigateToThread({ threadInfo: props.parentThreadInfo });
}, [props.parentThreadInfo, navigateToThread]);
return (
);
}
type ThreadSettingsParentProps = {
+threadInfo: ThreadInfo,
+parentThreadInfo: ?ThreadInfo,
};
function ThreadSettingsParent(props: ThreadSettingsParentProps): React.Node {
const { threadInfo, parentThreadInfo } = props;
const styles = useStyles(unboundStyles);
let parent;
if (parentThreadInfo) {
parent = ;
} else if (threadInfo.parentThreadID) {
parent = (
Secret parent
);
} else {
parent = (
No parent
);
}
return (
Parent
{parent}
);
}
const unboundStyles = {
avatarContainer: {
marginRight: 8,
},
currentValue: {
flex: 1,
},
currentValueText: {
color: 'panelForegroundSecondaryLabel',
fontFamily: 'Arial',
fontSize: 16,
margin: 0,
paddingRight: 0,
},
label: {
color: 'panelForegroundTertiaryLabel',
fontSize: 16,
width: 96,
},
noParent: {
fontStyle: 'italic',
paddingLeft: 2,
},
parentContainer: {
flexDirection: 'row',
},
row: {
backgroundColor: 'panelForeground',
flexDirection: 'row',
paddingHorizontal: 24,
paddingVertical: 4,
alignItems: 'center',
},
};
const ConnectedThreadSettingsParent: React.ComponentType =
React.memo(ThreadSettingsParent);
export default ConnectedThreadSettingsParent;
diff --git a/native/components/thread-list-thread.react.js b/native/components/thread-list-thread.react.js
index 2330d03f2..c62cdd34f 100644
--- a/native/components/thread-list-thread.react.js
+++ b/native/components/thread-list-thread.react.js
@@ -1,86 +1,86 @@
// @flow
import * as React from 'react';
import type { ThreadInfo, ResolvedThreadInfo } from 'lib/types/thread-types.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import Button from './button.react.js';
import SingleLine from './single-line.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import { type Colors, useStyles, useColors } from '../themes/colors.js';
import type { ViewStyle, TextStyle } from '../types/styles.js';
type SharedProps = {
+onSelect: (threadID: string) => void,
+style?: ViewStyle,
+textStyle?: TextStyle,
};
type BaseProps = {
...SharedProps,
+threadInfo: ThreadInfo,
};
type Props = {
...SharedProps,
+threadInfo: ResolvedThreadInfo,
+colors: Colors,
+styles: typeof unboundStyles,
};
class ThreadListThread extends React.PureComponent {
render() {
const { modalIosHighlightUnderlay: underlayColor } = this.props.colors;
return (
);
}
onSelect = () => {
this.props.onSelect(this.props.threadInfo.id);
};
}
const unboundStyles = {
button: {
alignItems: 'center',
flexDirection: 'row',
paddingLeft: 13,
},
text: {
color: 'modalForegroundLabel',
fontSize: 16,
paddingLeft: 9,
paddingRight: 12,
paddingVertical: 6,
},
};
const ConnectedThreadListThread: React.ComponentType =
React.memo(function ConnectedThreadListThread(props: BaseProps) {
const { threadInfo, ...rest } = props;
const styles = useStyles(unboundStyles);
const colors = useColors();
const resolvedThreadInfo = useResolvedThreadInfo(threadInfo);
return (
);
});
export default ConnectedThreadListThread;
diff --git a/native/components/user-list-user.react.js b/native/components/user-list-user.react.js
index 413650828..2fd28f775 100644
--- a/native/components/user-list-user.react.js
+++ b/native/components/user-list-user.react.js
@@ -1,98 +1,98 @@
// @flow
import * as React from 'react';
import { Text, Platform } from 'react-native';
import type { UserListItem, AccountUserInfo } from 'lib/types/user-types.js';
import Button from './button.react.js';
import SingleLine from './single-line.react.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import { type Colors, useColors, useStyles } from '../themes/colors.js';
import type { TextStyle } from '../types/styles.js';
import Alert from '../utils/alert.js';
// eslint-disable-next-line no-unused-vars
const getUserListItemHeight = (item: UserListItem): number => {
// TODO consider parent thread notice
return Platform.OS === 'ios' ? 31.5 : 33.5;
};
type BaseProps = {
+userInfo: UserListItem,
+onSelect: (user: AccountUserInfo) => void,
+textStyle?: TextStyle,
};
type Props = {
...BaseProps,
// Redux state
+colors: Colors,
+styles: typeof unboundStyles,
};
class UserListUser extends React.PureComponent {
render() {
const { userInfo } = this.props;
let notice = null;
if (userInfo.notice) {
notice = {userInfo.notice};
}
const { modalIosHighlightUnderlay: underlayColor } = this.props.colors;
return (
);
}
onSelect = () => {
const { userInfo } = this.props;
if (!userInfo.alert) {
const { alert, notice, disabled, ...accountUserInfo } = userInfo;
this.props.onSelect(accountUserInfo);
return;
}
Alert.alert(userInfo.alert.title, userInfo.alert.text, [{ text: 'OK' }], {
cancelable: true,
});
};
}
const unboundStyles = {
button: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
},
notice: {
color: 'modalForegroundSecondaryLabel',
fontStyle: 'italic',
},
text: {
color: 'modalForegroundLabel',
flex: 1,
fontSize: 16,
paddingHorizontal: 12,
paddingVertical: 6,
},
};
const ConnectedUserListUser: React.ComponentType =
React.memo(function ConnectedUserListUser(props: BaseProps) {
const colors = useColors();
const styles = useStyles(unboundStyles);
return ;
});
export { ConnectedUserListUser as UserListUser, getUserListItemHeight };
diff --git a/native/navigation/community-drawer-item.react.js b/native/navigation/community-drawer-item.react.js
index 50eb861b0..6c4e7fd2a 100644
--- a/native/navigation/community-drawer-item.react.js
+++ b/native/navigation/community-drawer-item.react.js
@@ -1,142 +1,142 @@
// @flow
import * as React from 'react';
import { View, TouchableOpacity } from 'react-native';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import { ExpandButton, ExpandButtonDisabled } from './expand-buttons.react.js';
import SubchannelsButton from './subchannels-button.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import type { MessageListParams } from '../chat/message-list-types.js';
import CommunityActionsButton from '../components/community-actions-button.react.js';
import SingleLine from '../components/single-line.react.js';
import { useStyles } from '../themes/colors.js';
import type { CommunityDrawerItemDataFlattened } from '../utils/drawer-utils.react.js';
export type DrawerItemProps = {
+itemData: CommunityDrawerItemDataFlattened,
+toggleExpanded: (threadID: string) => void,
+isExpanded: boolean,
+navigateToThread: (params: MessageListParams) => void,
};
function CommunityDrawerItem(props: DrawerItemProps): React.Node {
const {
itemData: {
threadInfo,
labelStyle,
hasSubchannelsButton,
hasChildren,
itemStyle,
},
navigateToThread,
isExpanded,
toggleExpanded,
} = props;
const styles = useStyles(unboundStyles);
const subchannelsButton = React.useMemo(() => {
if (isExpanded && hasSubchannelsButton) {
return (
);
}
return null;
}, [isExpanded, hasSubchannelsButton, styles.subchannelsButton, threadInfo]);
const onExpandToggled = React.useCallback(() => {
toggleExpanded(threadInfo.id);
}, [toggleExpanded, threadInfo.id]);
const itemExpandButton = React.useMemo(() => {
if (!hasChildren && !hasSubchannelsButton) {
return ;
}
return ;
}, [hasChildren, hasSubchannelsButton, onExpandToggled, isExpanded]);
const onPress = React.useCallback(() => {
navigateToThread({ threadInfo });
}, [navigateToThread, threadInfo]);
const { uiName } = useResolvedThreadInfo(threadInfo);
const containerStyle = React.useMemo(
() => [
styles.container,
{
paddingLeft: itemStyle.indentation,
},
styles[itemStyle.background],
],
[itemStyle.indentation, itemStyle.background, styles],
);
return (
{itemExpandButton}
-
+
{uiName}
{subchannelsButton}
);
}
const unboundStyles = {
container: {
paddingRight: 8,
},
none: {
paddingVertical: 2,
},
beginning: {
backgroundColor: 'drawerOpenCommunityBackground',
borderTopRightRadius: 8,
paddingTop: 2,
},
middle: {
backgroundColor: 'drawerOpenCommunityBackground',
},
end: {
backgroundColor: 'drawerOpenCommunityBackground',
borderBottomRightRadius: 8,
paddingBottom: 2,
},
avatarContainer: {
marginRight: 8,
},
threadEntry: {
flexDirection: 'row',
marginVertical: 6,
},
textTouchableWrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginRight: 24,
},
subchannelsButton: {
marginLeft: 24,
marginBottom: 6,
},
};
const MemoizedCommunityDrawerItem: React.ComponentType =
React.memo(CommunityDrawerItem);
export default MemoizedCommunityDrawerItem;
diff --git a/native/profile/relationship-list-item.react.js b/native/profile/relationship-list-item.react.js
index 6e66c09d6..c44eced7a 100644
--- a/native/profile/relationship-list-item.react.js
+++ b/native/profile/relationship-list-item.react.js
@@ -1,344 +1,344 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
import {
updateRelationshipsActionTypes,
updateRelationships,
} from 'lib/actions/relationship-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import {
type RelationshipRequest,
type RelationshipAction,
type RelationshipErrors,
userRelationshipStatus,
relationshipActions,
} from 'lib/types/relationship-types.js';
import type {
AccountUserInfo,
GlobalAccountUserInfo,
} from 'lib/types/user-types.js';
import {
type DispatchActionPromise,
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
import type { RelationshipListNavigate } from './relationship-list.react.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import PencilIcon from '../components/pencil-icon.react.js';
import SingleLine from '../components/single-line.react.js';
import {
type KeyboardState,
KeyboardContext,
} from '../keyboard/keyboard-state.js';
import {
OverlayContext,
type OverlayContextType,
} from '../navigation/overlay-context.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import {
RelationshipListItemTooltipModalRouteName,
FriendListRouteName,
BlockListRouteName,
} from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { type Colors, useColors, useStyles } from '../themes/colors.js';
import type { VerticalBounds } from '../types/layout-types.js';
import Alert from '../utils/alert.js';
type BaseProps = {
+userInfo: AccountUserInfo,
+lastListItem: boolean,
+verticalBounds: ?VerticalBounds,
+relationshipListRoute: NavigationRoute<'FriendList' | 'BlockList'>,
+navigate: RelationshipListNavigate,
+onSelect: (selectedUser: GlobalAccountUserInfo) => void,
};
type Props = {
...BaseProps,
// Redux state
+removeUserLoadingStatus: LoadingStatus,
+colors: Colors,
+styles: typeof unboundStyles,
// Redux dispatch functions
+dispatchActionPromise: DispatchActionPromise,
// async functions that hit server APIs
+updateRelationships: (
request: RelationshipRequest,
) => Promise,
// withOverlayContext
+overlayContext: ?OverlayContextType,
// withKeyboardState
+keyboardState: ?KeyboardState,
};
class RelationshipListItem extends React.PureComponent {
editButton = React.createRef>();
render() {
const {
lastListItem,
removeUserLoadingStatus,
userInfo,
relationshipListRoute,
} = this.props;
const relationshipsToEdit = {
[FriendListRouteName]: [userRelationshipStatus.FRIEND],
[BlockListRouteName]: [
userRelationshipStatus.BOTH_BLOCKED,
userRelationshipStatus.BLOCKED_BY_VIEWER,
],
}[relationshipListRoute.name];
const canEditFriendRequest = {
[FriendListRouteName]: true,
[BlockListRouteName]: false,
}[relationshipListRoute.name];
const borderBottom = lastListItem ? null : this.props.styles.borderBottom;
let editButton = null;
if (removeUserLoadingStatus === 'loading') {
editButton = (
);
} else if (relationshipsToEdit.includes(userInfo.relationshipStatus)) {
editButton = (
);
} else if (
userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED &&
canEditFriendRequest
) {
editButton = (
Accept
Reject
);
} else if (
userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT &&
canEditFriendRequest
) {
editButton = (
Cancel request
);
} else {
editButton = (
Add
);
}
return (
-
+
{this.props.userInfo.username}
{editButton}
);
}
onSelect = () => {
const { id, username } = this.props.userInfo;
this.props.onSelect({ id, username });
};
visibleEntryIDs() {
const { relationshipListRoute } = this.props;
const id = {
[FriendListRouteName]: 'unfriend',
[BlockListRouteName]: 'unblock',
}[relationshipListRoute.name];
return [id];
}
onPressEdit = () => {
if (this.props.keyboardState?.dismissKeyboardIfShowing()) {
return;
}
const {
editButton,
props: { verticalBounds },
} = this;
const { overlayContext, userInfo } = this.props;
invariant(
overlayContext,
'RelationshipListItem should have OverlayContext',
);
overlayContext.setScrollBlockingModalStatus('open');
if (!editButton.current || !verticalBounds) {
return;
}
const { relationshipStatus, ...restUserInfo } = userInfo;
const relativeUserInfo = {
...restUserInfo,
isViewer: false,
};
editButton.current.measure((x, y, width, height, pageX, pageY) => {
const coordinates = { x: pageX, y: pageY, width, height };
this.props.navigate<'RelationshipListItemTooltipModal'>({
name: RelationshipListItemTooltipModalRouteName,
params: {
presentedFrom: this.props.relationshipListRoute.key,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs: this.visibleEntryIDs(),
relativeUserInfo,
},
});
});
};
// We need to set onLayout in order to allow .measure() to be on the ref
onLayout = () => {};
onPressFriendUser = () => {
this.onPressUpdateFriendship(relationshipActions.FRIEND);
};
onPressUnfriendUser = () => {
this.onPressUpdateFriendship(relationshipActions.UNFRIEND);
};
onPressUpdateFriendship(action: RelationshipAction) {
const { id } = this.props.userInfo;
const customKeyName = `${updateRelationshipsActionTypes.started}:${id}`;
this.props.dispatchActionPromise(
updateRelationshipsActionTypes,
this.updateFriendship(action),
{ customKeyName },
);
}
async updateFriendship(action: RelationshipAction) {
try {
return await this.props.updateRelationships({
action,
userIDs: [this.props.userInfo.id],
});
} catch (e) {
Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], {
cancelable: true,
});
throw e;
}
}
}
const unboundStyles = {
editButton: {
paddingLeft: 10,
},
container: {
flex: 1,
paddingHorizontal: 12,
backgroundColor: 'panelForeground',
},
innerContainer: {
paddingVertical: 10,
paddingHorizontal: 12,
borderColor: 'panelForegroundBorder',
flexDirection: 'row',
},
borderBottom: {
borderBottomWidth: 1,
},
buttonContainer: {
flexDirection: 'row',
},
editButtonWithMargin: {
marginLeft: 15,
},
username: {
color: 'panelForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
lineHeight: 20,
marginLeft: 8,
},
blueAction: {
color: 'link',
fontSize: 16,
paddingLeft: 6,
},
redAction: {
color: 'redText',
fontSize: 16,
paddingLeft: 6,
},
};
const ConnectedRelationshipListItem: React.ComponentType =
React.memo(function ConnectedRelationshipListItem(
props: BaseProps,
) {
const removeUserLoadingStatus = useSelector(state =>
createLoadingStatusSelector(
updateRelationshipsActionTypes,
`${updateRelationshipsActionTypes.started}:${props.userInfo.id}`,
)(state),
);
const colors = useColors();
const styles = useStyles(unboundStyles);
const dispatchActionPromise = useDispatchActionPromise();
const boundUpdateRelationships = useServerCall(updateRelationships);
const overlayContext = React.useContext(OverlayContext);
const keyboardState = React.useContext(KeyboardContext);
return (
);
});
export default ConnectedRelationshipListItem;
diff --git a/native/roles/change-roles-screen.react.js b/native/roles/change-roles-screen.react.js
index 51e2af2cf..1eb07204a 100644
--- a/native/roles/change-roles-screen.react.js
+++ b/native/roles/change-roles-screen.react.js
@@ -1,303 +1,303 @@
// @flow
import { useActionSheet } from '@expo/react-native-action-sheet';
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, Platform, ActivityIndicator } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { changeThreadMemberRolesActionTypes } from 'lib/actions/thread-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { otherUsersButNoOtherAdmins } from 'lib/selectors/thread-selectors.js';
import { roleIsAdminRole } from 'lib/shared/thread-utils.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import type { RelativeMemberInfo, ThreadInfo } from 'lib/types/thread-types.js';
import { values } from 'lib/utils/objects.js';
import ChangeRolesHeaderRightButton from './change-roles-header-right-button.react.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import type { ChatNavigationProp } from '../chat/chat.react';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
export type ChangeRolesScreenParams = {
+threadInfo: ThreadInfo,
+memberInfo: RelativeMemberInfo,
+role: ?string,
};
type Props = {
+navigation: ChatNavigationProp<'ChangeRolesScreen'>,
+route: NavigationRoute<'ChangeRolesScreen'>,
};
const changeRolesLoadingStatusSelector = createLoadingStatusSelector(
changeThreadMemberRolesActionTypes,
);
function ChangeRolesScreen(props: Props): React.Node {
const { navigation, route } = props;
const { threadInfo, memberInfo, role } = props.route.params;
invariant(role, 'Role must be defined');
const changeRolesLoadingStatus: LoadingStatus = useSelector(
changeRolesLoadingStatusSelector,
);
const styles = useStyles(unboundStyles);
const [selectedRole, setSelectedRole] = React.useState(role);
const roleOptions = React.useMemo(
() =>
values(threadInfo.roles).map(threadRole => ({
id: threadRole.id,
name: threadRole.name,
})),
[threadInfo.roles],
);
const selectedRoleName = React.useMemo(
() => roleOptions.find(roleOption => roleOption.id === selectedRole)?.name,
[roleOptions, selectedRole],
);
const onRoleChange = React.useCallback(
(selectedIndex: ?number) => {
if (
selectedIndex === undefined ||
selectedIndex === null ||
selectedIndex === roleOptions.length
) {
return;
}
const newRole = roleOptions[selectedIndex].id;
setSelectedRole(newRole);
navigation.setParams({
threadInfo,
memberInfo,
role: newRole,
});
},
[navigation, setSelectedRole, roleOptions, memberInfo, threadInfo],
);
const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
const { showActionSheetWithOptions } = useActionSheet();
const insets = useSafeAreaInsets();
const showActionSheet = React.useCallback(() => {
const options =
Platform.OS === 'ios'
? [...roleOptions.map(roleOption => roleOption.name), 'Cancel']
: [...roleOptions.map(roleOption => roleOption.name)];
const cancelButtonIndex = Platform.OS === 'ios' ? options.length - 1 : -1;
const containerStyle = {
paddingBottom: insets.bottom,
};
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
containerStyle,
userInterfaceStyle: activeTheme ?? 'dark',
},
onRoleChange,
);
}, [
roleOptions,
onRoleChange,
insets.bottom,
activeTheme,
showActionSheetWithOptions,
]);
const otherUsersButNoOtherAdminsValue = useSelector(
otherUsersButNoOtherAdmins(threadInfo.id),
);
const memberIsAdmin = React.useMemo(() => {
invariant(memberInfo.role, 'Expected member role to be defined');
return roleIsAdminRole(threadInfo.roles[memberInfo.role]);
}, [threadInfo.roles, memberInfo.role]);
const shouldRoleChangeBeDisabled = React.useMemo(
() => otherUsersButNoOtherAdminsValue && memberIsAdmin,
[otherUsersButNoOtherAdminsValue, memberIsAdmin],
);
const roleSelector = React.useMemo(() => {
if (shouldRoleChangeBeDisabled) {
return (
{selectedRoleName}
);
}
return (
{selectedRoleName}
);
}, [showActionSheet, styles, selectedRoleName, shouldRoleChangeBeDisabled]);
const disabledRoleChangeMessage = React.useMemo(() => {
if (!shouldRoleChangeBeDisabled) {
return null;
}
return (
There must be at least one admin at any given time in a community.
);
}, [
shouldRoleChangeBeDisabled,
styles.disabledWarningBackground,
styles.infoIcon,
styles.disabledWarningText,
]);
React.useEffect(() => {
navigation.setOptions({
// eslint-disable-next-line react/display-name
headerRight: () => {
if (changeRolesLoadingStatus === 'loading') {
return (
);
}
return (
);
},
});
}, [
changeRolesLoadingStatus,
navigation,
styles.activityIndicator,
route,
shouldRoleChangeBeDisabled,
]);
return (
Members can only be assigned one role at a time. Changing a
member’s role will replace their previously assigned role.
-
+
{memberInfo.username}
{roleSelector}
{disabledRoleChangeMessage}
);
}
const unboundStyles = {
descriptionBackground: {
backgroundColor: 'panelForeground',
marginBottom: 20,
},
descriptionText: {
color: 'panelBackgroundLabel',
padding: 16,
fontSize: 14,
},
memberInfo: {
backgroundColor: 'panelForeground',
padding: 16,
marginBottom: 30,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
memberInfoUsername: {
color: 'panelForegroundLabel',
marginTop: 8,
fontSize: 18,
fontWeight: '500',
},
roleSelectorLabel: {
color: 'panelForegroundSecondaryLabel',
marginLeft: 8,
fontSize: 12,
},
roleSelector: {
backgroundColor: 'panelForeground',
marginTop: 8,
padding: 16,
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
},
currentRole: {
color: 'panelForegroundSecondaryLabel',
fontSize: 16,
},
disabledCurrentRole: {
color: 'disabledButton',
fontSize: 16,
},
pencilIcon: {
color: 'panelInputSecondaryForeground',
},
disabledPencilIcon: {
color: 'disabledButton',
},
disabledWarningBackground: {
backgroundColor: 'disabledButton',
padding: 16,
display: 'flex',
marginTop: 20,
flexDirection: 'row',
justifyContent: 'center',
width: '75%',
alignSelf: 'center',
},
disabledWarningText: {
color: 'panelForegroundSecondaryLabel',
fontSize: 14,
marginRight: 8,
display: 'flex',
},
infoIcon: {
color: 'panelForegroundSecondaryLabel',
marginRight: 8,
marginLeft: 8,
marginBottom: 12,
},
activityIndicator: {
paddingRight: 15,
},
};
export default ChangeRolesScreen;
diff --git a/native/user-profile/user-profile.react.js b/native/user-profile/user-profile.react.js
index e40476f5c..0661a2cd5 100644
--- a/native/user-profile/user-profile.react.js
+++ b/native/user-profile/user-profile.react.js
@@ -1,228 +1,228 @@
// @flow
import Clipboard from '@react-native-clipboard/clipboard';
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils.js';
import { useUserProfileThreadInfo } from 'lib/shared/thread-utils.js';
import { stringForUserExplicit } from 'lib/shared/user-utils.js';
import type { UserInfo } from 'lib/types/user-types';
import sleep from 'lib/utils/sleep.js';
import {
userProfileUserInfoContainerHeight,
userProfileBottomPadding,
userProfileMenuButtonHeight,
userProfileActionButtonHeight,
} from './user-profile-constants.js';
import UserProfileMessageButton from './user-profile-message-button.react.js';
import UserProfileRelationshipButton from './user-profile-relationship-button.react.js';
import UserAvatar from '../avatars/user-avatar.react.js';
import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js';
import SingleLine from '../components/single-line.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import { useStyles } from '../themes/colors.js';
type Props = {
+userInfo: ?UserInfo,
};
function UserProfile(props: Props): React.Node {
const { userInfo } = props;
const userProfileThreadInfo = useUserProfileThreadInfo(userInfo);
const usernameText = stringForUserExplicit(userInfo);
const [usernameCopied, setUsernameCopied] = React.useState(false);
const [
userProfileRelationshipButtonHeight,
setUserProfileRelationshipButtonHeight,
] = React.useState(0);
const bottomSheetContext = React.useContext(BottomSheetContext);
invariant(bottomSheetContext, 'bottomSheetContext should be set');
const { setContentHeight } = bottomSheetContext;
const insets = useSafeAreaInsets();
React.useLayoutEffect(() => {
let height =
insets.bottom +
userProfileUserInfoContainerHeight +
userProfileBottomPadding;
if (userProfileThreadInfo) {
height += userProfileMenuButtonHeight;
}
if (
userProfileThreadInfo &&
!relationshipBlockedInEitherDirection(userInfo?.relationshipStatus)
) {
// message button height + relationship button height
height +=
userProfileActionButtonHeight + userProfileRelationshipButtonHeight;
}
setContentHeight(height);
}, [
insets.bottom,
setContentHeight,
userInfo?.relationshipStatus,
userProfileRelationshipButtonHeight,
userProfileThreadInfo,
]);
const styles = useStyles(unboundStyles);
const onPressCopyUsername = React.useCallback(async () => {
Clipboard.setString(usernameText);
setUsernameCopied(true);
await sleep(3000);
setUsernameCopied(false);
}, [usernameText]);
const copyUsernameButton = React.useMemo(() => {
if (usernameCopied) {
return (
Username copied!
);
}
return (
Copy username
);
}, [
onPressCopyUsername,
styles.copyUsernameContainer,
styles.copyUsernameIcon,
styles.copyUsernameText,
usernameCopied,
]);
const messageButton = React.useMemo(() => {
if (
!userProfileThreadInfo ||
relationshipBlockedInEitherDirection(userInfo?.relationshipStatus)
) {
return null;
}
const { threadInfo, pendingPersonalThreadUserInfo } = userProfileThreadInfo;
return (
);
}, [userInfo?.relationshipStatus, userProfileThreadInfo]);
const relationshipButton = React.useMemo(() => {
if (
!userProfileThreadInfo ||
relationshipBlockedInEitherDirection(userInfo?.relationshipStatus)
) {
return null;
}
const { threadInfo, pendingPersonalThreadUserInfo } = userProfileThreadInfo;
return (
);
}, [userInfo?.relationshipStatus, userProfileThreadInfo]);
return (
-
+
{usernameText}
{copyUsernameButton}
{messageButton}
{relationshipButton}
);
}
const unboundStyles = {
container: {
paddingHorizontal: 16,
},
moreIcon: {
color: 'modalButtonLabel',
alignSelf: 'flex-end',
},
userInfoContainer: {
flexDirection: 'row',
},
usernameContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'flex-start',
paddingLeft: 16,
},
usernameText: {
color: 'modalForegroundLabel',
fontSize: 18,
fontWeight: '500',
},
copyUsernameContainer: {
flexDirection: 'row',
justifyContent: 'center',
paddingTop: 8,
},
copyUsernameIcon: {
color: 'purpleLink',
marginRight: 4,
},
copyUsernameText: {
color: 'purpleLink',
fontSize: 12,
},
messageButtonContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'purpleButton',
paddingVertical: 8,
marginTop: 16,
borderRadius: 8,
},
messageButtonIcon: {
color: 'floatingButtonLabel',
paddingRight: 8,
},
messageButtonText: {
color: 'floatingButtonLabel',
},
};
export default UserProfile;