diff --git a/web/avatars/emoji-avatar-selection-modal.react.js b/web/avatars/emoji-avatar-selection-modal.react.js index 17f77e4e7..d6253c5ac 100644 --- a/web/avatars/emoji-avatar-selection-modal.react.js +++ b/web/avatars/emoji-avatar-selection-modal.react.js @@ -1,158 +1,158 @@ // @flow import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import type { ClientAvatar, ClientEmojiAvatar, } from 'lib/types/avatar-types.js'; import Avatar from './avatar.react.js'; import css from './emoji-avatar-selection-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; -import Tabs from '../components/tabs.react.js'; +import Tabs from '../components/tabs-legacy.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Modal from '../modals/modal.react.js'; import ColorSelector from '../modals/threads/color-selector.react.js'; type TabType = 'emoji' | 'color'; type Props = { +currentAvatar: ClientAvatar, +defaultAvatar: ClientEmojiAvatar, +setEmojiAvatar: (pendingEmojiAvatar: ClientEmojiAvatar) => Promise, +avatarSaveInProgress: boolean, }; function EmojiAvatarSelectionModal(props: Props): React.Node { const { popModal } = useModalContext(); const { currentAvatar, defaultAvatar, setEmojiAvatar, avatarSaveInProgress } = props; const [updateAvatarStatus, setUpdateAvatarStatus] = React.useState(); const [pendingAvatarEmoji, setPendingAvatarEmoji] = React.useState( currentAvatar.type === 'emoji' ? currentAvatar.emoji : defaultAvatar.emoji, ); const [pendingAvatarColor, setPendingAvatarColor] = React.useState( currentAvatar.type === 'emoji' ? currentAvatar.color : defaultAvatar.color, ); const pendingEmojiAvatar: ClientEmojiAvatar = React.useMemo( () => ({ type: 'emoji', emoji: pendingAvatarEmoji, color: pendingAvatarColor, }), [pendingAvatarColor, pendingAvatarEmoji], ); const onEmojiSelect = React.useCallback( (selection: { +native: string, ... }) => { setUpdateAvatarStatus(); setPendingAvatarEmoji(selection.native); }, [], ); const onColorSelection = React.useCallback((hex: string) => { setUpdateAvatarStatus(); setPendingAvatarColor(hex); }, []); const onSaveAvatar = React.useCallback(async () => { try { await setEmojiAvatar(pendingEmojiAvatar); setUpdateAvatarStatus('success'); } catch { setUpdateAvatarStatus('failure'); } }, [setEmojiAvatar, pendingEmojiAvatar]); let saveButtonContent; let buttonColor; if (avatarSaveInProgress) { buttonColor = buttonThemes.standard; saveButtonContent = ; } else if (updateAvatarStatus === 'success') { buttonColor = buttonThemes.success; saveButtonContent = ( <> {'Avatar update succeeded.'} ); } else if (updateAvatarStatus === 'failure') { buttonColor = buttonThemes.danger; saveButtonContent = ( <> {'Avatar update failed. Please try again.'} ); } else { buttonColor = buttonThemes.standard; saveButtonContent = 'Save Avatar'; } const [currentTabType, setCurrentTabType] = React.useState('emoji'); return (
); } export default EmojiAvatarSelectionModal; diff --git a/web/chat/chat-tabs.react.js b/web/chat/chat-tabs.react.js index c94fddedc..d35c9749a 100644 --- a/web/chat/chat-tabs.react.js +++ b/web/chat/chat-tabs.react.js @@ -1,79 +1,79 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { unreadBackgroundCount } from 'lib/selectors/thread-selectors.js'; import css from './chat-tabs.css'; import ChatThreadList from './chat-thread-list.react.js'; import ChatThreadTab from './chat-thread-tab.react.js'; import { ThreadListContext } from './thread-list-provider.js'; -import Tabs from '../components/tabs.react.js'; +import Tabs from '../components/tabs-legacy.react.js'; import { useSelector } from '../redux/redux-utils.js'; function ChatTabs(): React.Node { let backgroundTitle = 'Background'; const unreadBackgroundCountVal = useSelector(unreadBackgroundCount); if (unreadBackgroundCountVal) { backgroundTitle += ` (${unreadBackgroundCountVal})`; } const threadListContext = React.useContext(ThreadListContext); invariant( threadListContext, 'threadListContext should be set in ChatThreadList', ); const { activeTab, setActiveTab } = threadListContext; const setActiveChatTab = React.useCallback( (newTab: 'Background' | 'Focus') => { setActiveTab(newTab); }, [setActiveTab], ); const chatThreadList = React.useMemo( () => (
), [], ); const focusTabsItem = React.useMemo( () => ( }> {chatThreadList} ), [chatThreadList], ); const backgroundTabsItem = React.useMemo( () => ( } > {chatThreadList} ), [backgroundTitle, chatThreadList], ); return (
{focusTabsItem} {backgroundTabsItem}
); } export default ChatTabs; diff --git a/web/components/tabs.react.js b/web/components/tabs-legacy.react.js similarity index 100% rename from web/components/tabs.react.js rename to web/components/tabs-legacy.react.js diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js index 5162f0cc3..9f9f280cf 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -1,192 +1,192 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useFetchThreadMedia } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { encryptedMediaBlobURI, encryptedVideoThumbnailBlobURI, } from 'lib/media/media-utils.js'; import type { Media } from 'lib/types/media-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import GalleryItem from './thread-settings-media-gallery-item.react.js'; import css from './thread-settings-media-gallery.css'; -import Tabs from '../../../components/tabs.react.js'; +import Tabs from '../../../components/tabs-legacy.react.js'; import MultimediaModal from '../../../media/multimedia-modal.react.js'; import Modal from '../../modal.react.js'; type MediaGalleryTab = 'All' | 'Images' | 'Videos'; type ThreadSettingsMediaGalleryModalProps = { +onClose: () => void, +parentThreadInfo: ThreadInfo, +limit: number, +activeTab: MediaGalleryTab, }; function ThreadSettingsMediaGalleryModal( props: ThreadSettingsMediaGalleryModalProps, ): React.Node { const { pushModal } = useModalContext(); const { onClose, parentThreadInfo, limit, activeTab } = props; const { id: threadID } = parentThreadInfo; const modalName = 'Media'; const callFetchThreadMedia = useFetchThreadMedia(); const [mediaInfos, setMediaInfos] = React.useState<$ReadOnlyArray>([]); const [tab, setTab] = React.useState(activeTab); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; void fetchData(); }, [callFetchThreadMedia, threadID, limit]); const onClick = React.useCallback( (media: Media) => { const thumbHash = media.thumbnailThumbHash ?? media.thumbHash; let mediaInfo = { thumbHash, dimensions: media.dimensions, }; if (media.type === 'photo' || media.type === 'video') { const { uri, thumbnailURI } = media; mediaInfo = { ...mediaInfo, type: media.type, uri, thumbnailURI, }; } else { const { encryptionKey, thumbnailEncryptionKey } = media; const thumbnailBlobURI = media.type === 'encrypted_video' ? encryptedVideoThumbnailBlobURI(media) : null; mediaInfo = { ...mediaInfo, type: media.type, blobURI: encryptedMediaBlobURI(media), encryptionKey, thumbnailBlobURI, thumbnailEncryptionKey, }; } pushModal(); }, [pushModal], ); const mediaGalleryItems = React.useMemo(() => { let filteredMediaInfos = mediaInfos; if (tab === 'Images') { filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'photo' || mediaInfo.type === 'encrypted_photo', ); } else if (tab === 'Videos') { filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'video' || mediaInfo.type === 'encrypted_video', ); } return filteredMediaInfos.map((media, i) => { let imageSource; if (media.type === 'photo') { imageSource = { kind: 'plain', uri: media.uri, thumbHash: media.thumbHash, }; } else if (media.type === 'video') { imageSource = { kind: 'plain', uri: media.thumbnailURI, thumbHash: media.thumbnailThumbHash, }; } else if (media.type === 'encrypted_photo') { imageSource = { kind: 'encrypted', blobURI: encryptedMediaBlobURI(media), encryptionKey: media.encryptionKey, thumbHash: media.thumbHash, }; } else { imageSource = { kind: 'encrypted', blobURI: encryptedVideoThumbnailBlobURI(media), encryptionKey: media.thumbnailEncryptionKey, thumbHash: media.thumbnailThumbHash, }; } return ( onClick(media)} imageSource={imageSource} imageCSSClass={css.media} imageContainerCSSClass={css.mediaContainer} /> ); }); }, [tab, mediaInfos, onClick]); const handleScroll = React.useCallback( async (event: SyntheticEvent) => { const container = event.target; invariant(container instanceof HTMLDivElement, 'target not div'); // Load more data when the user is within 1000 pixels of the end const buffer = 1000; if ( container.scrollHeight - container.scrollTop > container.clientHeight + buffer ) { return; } const result = await callFetchThreadMedia({ threadID, limit, offset: mediaInfos.length, }); setMediaInfos([...mediaInfos, ...result.media]); }, [callFetchThreadMedia, threadID, limit, mediaInfos], ); return (
{mediaGalleryItems}
{mediaGalleryItems}
{mediaGalleryItems}
); } export default ThreadSettingsMediaGalleryModal; diff --git a/web/modals/threads/members/members-modal.react.js b/web/modals/threads/members/members-modal.react.js index 487d77af1..ebe01f472 100644 --- a/web/modals/threads/members/members-modal.react.js +++ b/web/modals/threads/members/members-modal.react.js @@ -1,142 +1,142 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { roleIsAdminRole, threadHasPermission, } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { type RelativeMemberInfo } from 'lib/types/thread-types.js'; import { useRolesFromCommunityThreadInfo } from 'lib/utils/role-utils.js'; import { AddMembersModal } from './add-members-modal.react.js'; import ThreadMembersList from './members-list.react.js'; import css from './members-modal.css'; import Button from '../../../components/button.react.js'; -import Tabs from '../../../components/tabs.react.js'; +import Tabs from '../../../components/tabs-legacy.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import SearchModal from '../../search-modal.react.js'; type ContentProps = { +searchText: string, +threadID: string, }; function ThreadMembersModalContent(props: ContentProps): React.Node { const { threadID, searchText } = props; const [tab, setTab] = React.useState<'All Members' | 'Admins'>('All Members'); const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); const { members: threadMembersNotFiltered } = threadInfo; const userSearchIndex = useUserSearchIndex(threadMembersNotFiltered); const userIDs = React.useMemo( () => userSearchIndex.getSearchResults(searchText), [searchText, userSearchIndex], ); const allMembers = React.useMemo( () => threadMembersNotFiltered.filter( (member: RelativeMemberInfo) => searchText.length === 0 || userIDs.includes(member.id), ), [searchText.length, threadMembersNotFiltered, userIDs], ); const roles = useRolesFromCommunityThreadInfo(threadInfo, allMembers); const adminMembers = React.useMemo( () => allMembers.filter((member: RelativeMemberInfo) => roleIsAdminRole(roles.get(member.id)), ), [allMembers, roles], ); const allUsersTab = React.useMemo( () => ( ), [allMembers, threadInfo], ); const allAdminsTab = React.useMemo( () => ( ), [adminMembers, threadInfo], ); const { pushModal, popModal } = useModalContext(); const onClickAddMembers = React.useCallback(() => { pushModal(); }, [popModal, pushModal, threadID]); const canAddMembers = threadHasPermission( threadInfo, threadPermissions.ADD_MEMBERS, ); const addMembersButton = React.useMemo(() => { if (!canAddMembers) { return null; } return (
); }, [canAddMembers, onClickAddMembers]); return (
{allUsersTab} {allAdminsTab}
{addMembersButton}
); } type Props = { +threadID: string, +onClose: () => void, }; function ThreadMembersModal(props: Props): React.Node { const { onClose, threadID } = props; const renderModalContent = React.useCallback( (searchText: string) => ( ), [threadID], ); return ( {renderModalContent} ); } export default ThreadMembersModal; diff --git a/web/modals/threads/settings/thread-settings-modal.react.js b/web/modals/threads/settings/thread-settings-modal.react.js index 8182d03f2..af283c69a 100644 --- a/web/modals/threads/settings/thread-settings-modal.react.js +++ b/web/modals/threads/settings/thread-settings-modal.react.js @@ -1,239 +1,239 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { deleteThreadActionTypes, changeThreadSettingsActionTypes, } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { getAvailableRelationshipButtons } from 'lib/shared/relationship-utils.js'; import { threadHasPermission, getSingleOtherUser, threadUIName, } from 'lib/shared/thread-utils.js'; import type { RelationshipButton } from 'lib/types/relationship-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadChanges, type ThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import ThreadSettingsDeleteTab from './thread-settings-delete-tab.react.js'; import ThreadSettingsGeneralTab from './thread-settings-general-tab.react.js'; import css from './thread-settings-modal.css'; import ThreadSettingsPrivacyTab from './thread-settings-privacy-tab.react.js'; import ThreadSettingsRelationshipTab from './thread-settings-relationship-tab.react.js'; -import Tabs from '../../../components/tabs.react.js'; +import Tabs from '../../../components/tabs-legacy.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import Modal from '../../modal.react.js'; type TabType = 'general' | 'privacy' | 'delete' | 'relationship'; type BaseProps = { +threadID: string, }; const deleteThreadLoadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const changeThreadSettingsLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, ); const ConnectedThreadSettingsModal: React.ComponentType = React.memo(function ConnectedThreadSettingsModal(props) { const changeInProgress = useSelector( state => deleteThreadLoadingStatusSelector(state) === 'loading' || changeThreadSettingsLoadingStatusSelector(state) === 'loading', ); const threadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[props.threadID], ); const modalContext = useModalContext(); const [errorMessage, setErrorMessage] = React.useState(''); const [currentTabType, setCurrentTabType] = React.useState('general'); const [queuedChanges, setQueuedChanges] = React.useState( Object.freeze({}), ); const threadInfoWithNoName = React.useMemo(() => { invariant(threadInfo, 'threadInfo should exist in threadInfoWithNoName'); if (threadInfo.name === null || threadInfo.name === undefined) { return threadInfo; } // Branching on `minimallyEncoded` to appease `flow`. const withNoName = threadInfo.minimallyEncoded ? { ...threadInfo, name: undefined } : { ...threadInfo, name: undefined }; return { ...withNoName, uiName: threadUIName(withNoName), }; }, [threadInfo]); const resolvedThreadInfo = useResolvedThreadInfo(threadInfoWithNoName); const namePlaceholder = resolvedThreadInfo.uiName; const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const userInfos = useSelector(state => state.userStore.userInfos); const otherMemberID = React.useMemo(() => { if (!threadInfo) { return null; } return getSingleOtherUser(threadInfo, viewerID); }, [threadInfo, viewerID]); const otherUserInfo = otherMemberID ? userInfos[otherMemberID] : null; const availableRelationshipActions = React.useMemo(() => { if (!otherUserInfo) { return ([]: RelationshipButton[]); } return getAvailableRelationshipButtons(otherUserInfo); }, [otherUserInfo]); const hasPermissionForTab = React.useCallback( // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return (thread: ThreadInfo, tab: TabType) => { if (tab === 'general') { return ( threadHasPermission(thread, threadPermissions.EDIT_THREAD_NAME) || threadHasPermission(thread, threadPermissions.EDIT_THREAD_COLOR) || threadHasPermission( thread, threadPermissions.EDIT_THREAD_DESCRIPTION, ) ); } else if (tab === 'privacy') { return threadHasPermission( thread, threadPermissions.EDIT_PERMISSIONS, ); } else if (tab === 'delete') { return threadHasPermission(thread, threadPermissions.DELETE_THREAD); } else if (tab === 'relationship') { return true; } invariant(false, `invalid tab: ${tab}`); }, [], ); React.useEffect(() => { if ( threadInfo && currentTabType !== 'general' && !hasPermissionForTab(threadInfo, currentTabType) ) { setCurrentTabType('general'); } }, [currentTabType, hasPermissionForTab, threadInfo]); React.useEffect(() => () => setErrorMessage(''), [currentTabType]); if (!threadInfo) { return (

You no longer have permission to view this chat

); } const tabs = [
, ]; // This UI needs to be updated to handle sidebars but we haven't gotten // there yet. We'll probably end up ripping it out anyways, so for now we // are just hiding the privacy tab for any thread that was created as a // sidebar const canSeePrivacyTab = (queuedChanges['parentThreadID'] ?? threadInfo['parentThreadID']) && !threadInfo.sourceMessageID && (threadInfo.type === threadTypes.COMMUNITY_OPEN_SUBTHREAD || threadInfo.type === threadTypes.COMMUNITY_SECRET_SUBTHREAD); if (canSeePrivacyTab) { tabs.push(
, ); } if (availableRelationshipActions.length > 0 && otherUserInfo) { tabs.push(
{errorMessage}
, ); } const canDeleteThread = hasPermissionForTab(threadInfo, 'delete'); if (canDeleteThread) { tabs.push(
, ); } return (
{tabs}
); }); export default ConnectedThreadSettingsModal; diff --git a/web/modals/threads/sidebars/sidebars-modal.react.js b/web/modals/threads/sidebars/sidebars-modal.react.js index 0745e3736..5ae00f98e 100644 --- a/web/modals/threads/sidebars/sidebars-modal.react.js +++ b/web/modals/threads/sidebars/sidebars-modal.react.js @@ -1,80 +1,80 @@ // @flow import * as React from 'react'; import { useFilteredChildThreads } from 'lib/hooks/child-threads.js'; import { threadInChatList, threadIsSidebar } from 'lib/shared/thread-utils.js'; import SidebarList from './sidebar-list.react.js'; import css from './sidebars-modal.css'; -import Tabs from '../../../components/tabs.react.js'; +import Tabs from '../../../components/tabs-legacy.react.js'; import SearchModal from '../../search-modal.react.js'; type SidebarTab = 'All Threads' | 'My Threads'; type ContentProps = { +searchText: string, +threadID: string, +defaultTab: SidebarTab, }; function SidebarsModalContent(props: ContentProps): React.Node { const { searchText, threadID, defaultTab } = props; const [tab, setTab] = React.useState(defaultTab); const sidebarList = useFilteredChildThreads(threadID, { predicate: threadIsSidebar, searchText, }); const sidebarsChatListVisibleInChat = sidebarList.filter(chatItem => threadInChatList(chatItem.threadInfo), ); return (
); } type Props = { +threadID: string, +onClose: () => void, +defaultTab?: SidebarTab, }; function SidebarsModal(props: Props): React.Node { const { threadID, onClose, defaultTab = 'All Threads' } = props; const sidebarsContent = React.useCallback( (searchText: string) => ( ), [defaultTab, threadID], ); return ( {sidebarsContent} ); } export default SidebarsModal;