From 9f5a85ece711e9b5c1d86baa169b2584e9a392c3 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Thu, 26 Sep 2024 17:33:22 -0700 Subject: [PATCH 001/104] packages/shared: add PostHog error logger --- packages/shared/src/debug.ts | 11 ++++ packages/shared/src/store/errorLogging.ts | 66 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/shared/src/store/errorLogging.ts diff --git a/packages/shared/src/debug.ts b/packages/shared/src/debug.ts index 4bb8e06622..caa77dd57d 100644 --- a/packages/shared/src/debug.ts +++ b/packages/shared/src/debug.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; import { useLiveRef } from './logic/utilHooks'; +import { ErrorLogger } from './store/errorLogging'; import { useCurrentSession } from './store/session'; const customLoggers = new Set(); @@ -67,6 +68,16 @@ export function createDevLogger(tag: string, enabled: boolean) { }); } + if (prop === 'logError') { + const errorLogger = new ErrorLogger(tag, console); + if (typeof args[0] === 'string') { + errorLogger.report(new Error(args[0])); + } else { + errorLogger.report(new Error('Unknown error')); + } + return; + } + if ( (enabled || customLoggers.has(tag)) && process.env.NODE_ENV !== 'production' diff --git a/packages/shared/src/store/errorLogging.ts b/packages/shared/src/store/errorLogging.ts new file mode 100644 index 0000000000..df631c3917 --- /dev/null +++ b/packages/shared/src/store/errorLogging.ts @@ -0,0 +1,66 @@ +import PostHog from 'posthog-js'; + +import { getCurrentBreadcrumbs } from '../debug'; + +let posthogInstance: typeof PostHog | null = null; + +export function initializeErrorLogger(apiKey: string) { + const instance = PostHog.init(apiKey, { + api_host: 'https://eu.posthog.com', + autocapture: false, + }); + + if (instance) { + posthogInstance = instance; + } else { + posthogInstance = null; + } +} + +export class ErrorLogger { + private name: string; + private baseLogger?: Console; + private logs: string[] = []; + private error: Error | null = null; + + constructor(name: string, baseLogger?: Console) { + this.name = name; + this.baseLogger = baseLogger; + this.error = null; + this.logs = []; + } + + public log(message: string) { + this.logs.push(`(${this.name}): ${message}`); + this.baseLogger?.log(`(${this.name}): ${message}`); + } + + private getErrorToSubmit(): Error { + if (this.error) { + return this.error; + } + return new Error(this.name); + } + + public async report(error: Error | null) { + this.error = error; + const errorToSubmit = this.getErrorToSubmit(); + const crumbs = getCurrentBreadcrumbs(); + + if (posthogInstance) { + posthogInstance.capture('app_error', { + message: errorToSubmit.message, + breadcrumbs: crumbs, + logs: this.logs, + }); + } else { + console.warn('PostHog is not initialized'); + } + + if (!__DEV__) { + console.warn(`New error report: ${errorToSubmit.message}`); + console.log('Debug Breadcrumbs:'); + crumbs.forEach((crumb) => console.log(crumb)); + } + } +} From ee1539734202f856183185fba3fb6ecdae9ba604 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Thu, 26 Sep 2024 17:34:00 -0700 Subject: [PATCH 002/104] tlon-mobile: initialize error logger at app root --- apps/tlon-mobile/src/App.main.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/tlon-mobile/src/App.main.tsx b/apps/tlon-mobile/src/App.main.tsx index 7719595235..8731b3863c 100644 --- a/apps/tlon-mobile/src/App.main.tsx +++ b/apps/tlon-mobile/src/App.main.tsx @@ -11,6 +11,7 @@ import { } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import ErrorBoundary from '@tloncorp/app/ErrorBoundary'; +import { POST_HOG_API_KEY } from '@tloncorp/app/constants'; import { BranchProvider, useBranch } from '@tloncorp/app/contexts/branch'; import { ShipProvider, useShip } from '@tloncorp/app/contexts/ship'; import { SignupProvider } from '@tloncorp/app/contexts/signup'; @@ -21,6 +22,7 @@ import { Provider as TamaguiProvider } from '@tloncorp/app/provider'; import { FeatureFlagConnectedInstrumentationProvider } from '@tloncorp/app/utils/perf'; import { posthogAsync } from '@tloncorp/app/utils/posthog'; import { QueryClientProvider, queryClient } from '@tloncorp/shared/dist/api'; +import { initializeErrorLogger } from '@tloncorp/shared/src/store/errorLogging'; import { LoadingSpinner, PortalProvider, Text, View } from '@tloncorp/ui'; import { usePreloadedEmojis } from '@tloncorp/ui'; import { PostHogProvider } from 'posthog-react-native'; @@ -73,6 +75,8 @@ const App = ({ usePreloadedEmojis(); + initializeErrorLogger(POST_HOG_API_KEY); + useEffect(() => { const unsubscribeFromNetInfo = NetInfo.addEventListener( ({ isConnected }) => { From a673c3f66a7839914ff8670b02574247f083fee2 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Thu, 26 Sep 2024 17:35:11 -0700 Subject: [PATCH 003/104] ChatListScreen: log error if connection not established in 10s --- packages/app/features/top/ChatListScreen.tsx | 37 +++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/app/features/top/ChatListScreen.tsx b/packages/app/features/top/ChatListScreen.tsx index 6d08c352a2..0ec6416c5b 100644 --- a/packages/app/features/top/ChatListScreen.tsx +++ b/packages/app/features/top/ChatListScreen.tsx @@ -1,7 +1,9 @@ +import { useIsFocused } from '@react-navigation/native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import * as db from '@tloncorp/shared/dist/db'; import * as logic from '@tloncorp/shared/dist/logic'; import * as store from '@tloncorp/shared/dist/store'; +import { createDevLogger } from '@tloncorp/shared/src/debug'; import { AddGroupSheet, Button, @@ -24,7 +26,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { TLON_EMPLOYEE_GROUP } from '../../constants'; import { useChatSettingsNavigation } from '../../hooks/useChatSettingsNavigation'; import { useCurrentUserId } from '../../hooks/useCurrentUser'; -import { useIsFocused } from '@react-navigation/native'; import { useFeatureFlag } from '../../lib/featureFlags'; import type { RootStackParamList } from '../../navigation/types'; import { identifyTlonEmployee } from '../../utils/posthog'; @@ -77,6 +78,7 @@ export default function ChatListScreen(props: Props) { ); const [showFilters, setShowFilters] = useState(false); const isFocused = useIsFocused(); + const logger = createDevLogger('ChatListScreen', true); const { data: pins } = store.usePins({ enabled: isFocused, }); @@ -105,6 +107,39 @@ export default function ChatListScreen(props: Props) { return null; }, [connStatus, chats]); + /* Log an error if this screen takes more than 30 seconds to resolve to "Connected" */ + const connectionTimeout = useRef(null); + const connectionAttempts = useRef(0); + + useEffect(() => { + const checkConnection = () => { + if (connStatus === 'Connected') { + if (connectionTimeout.current) { + clearTimeout(connectionTimeout.current); + } + connectionAttempts.current = 0; + } else { + connectionAttempts.current += 1; + if (connectionAttempts.current >= 10) { + logger.error('Connection not established within 10 seconds'); + if (connectionTimeout.current) { + clearTimeout(connectionTimeout.current); + } + } else { + connectionTimeout.current = setTimeout(checkConnection, 10000); + } + } + }; + + checkConnection(); + + return () => { + if (connectionTimeout.current) { + clearTimeout(connectionTimeout.current); + } + }; + }, [connStatus, logger]); + const resolvedChats = useMemo(() => { return { pinned: chats?.pinned ?? [], From de5ddeb4d2efb0db511535841dc74d927337303c Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 16:34:18 -0700 Subject: [PATCH 004/104] small code movement to keep next diff clean --- packages/ui/src/components/Channel/index.tsx | 47 +++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 75b71ba4b2..fd179d21db 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -395,17 +395,21 @@ export function Channel({ showAttachmentButton={channel.type !== 'gallery'} /> )} - {!isChatChannel && canWrite && !showBigInput && ( - - {channel.type === 'gallery' && - (showAddGalleryPost || - isUploadingGalleryImage) ? null : ( + + {!isChatChannel && + canWrite && + !showBigInput && + !( + channel.type === 'gallery' && + (showAddGalleryPost || isUploadingGalleryImage) + ) && ( + channel.type === 'gallery' @@ -420,9 +424,18 @@ export function Channel({ /> } /> - )} - + + )} + + {channel.type === 'gallery' && canWrite && ( + )} + {!negotiationMatch && isChatChannel && canWrite && ( )} @@ -432,14 +445,6 @@ export function Channel({ {!negotiationMatch && isChatChannel && canWrite && ( )} - {channel.type === 'gallery' && canWrite && ( - - )} {headerMode === 'next' ? ( From d2ab28902c160cf9cf6eb968802b2ff475ea76fc Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 17:17:05 -0700 Subject: [PATCH 005/104] Pull chat draft input into isolated component --- packages/ui/src/components/Channel/index.tsx | 46 +++++++++++++++++-- .../src/components/draftInputs/ChatInput.tsx | 42 +++++++++++++++++ .../ui/src/components/draftInputs/index.ts | 2 + .../ui/src/components/draftInputs/shared.ts | 22 +++++++++ 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/components/draftInputs/ChatInput.tsx create mode 100644 packages/ui/src/components/draftInputs/index.ts create mode 100644 packages/ui/src/components/draftInputs/shared.ts diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index fd179d21db..3c652261d4 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -34,6 +34,10 @@ import { Icon } from '../Icon'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; import { MessageInput } from '../MessageInput'; import { NotebookPost } from '../NotebookPost'; +import { + ChatInput, + DraftInputContext, +} from '../draftInputs'; import { ChannelFooter } from './ChannelFooter'; import { ChannelHeader } from './ChannelHeader'; import { DmInviteOptions } from './DmInviteOptions'; @@ -238,6 +242,36 @@ export function Channel({ [setEditingPost, channel.type] ); + const draftInputContext = useMemo( + (): DraftInputContext => ({ + channel, + clearDraft, + editPost, + editingPost, + getDraft, + group, + onSent: handleMessageSent, + send: messageSender, + setEditingPost, + setShouldBlur: setInputShouldBlur, + shouldBlur: inputShouldBlur, + storeDraft, + }), + [ + channel, + clearDraft, + editPost, + editingPost, + getDraft, + group, + handleMessageSent, + inputShouldBlur, + messageSender, + setEditingPost, + storeDraft, + ] + ); + return ( @@ -373,9 +407,8 @@ export function Channel({ {negotiationMatch && !channel.isDmInvite && !editingPost && - (isChatChannel || - (channel.type === 'gallery' && - isUploadingGalleryImage)) && + channel.type === 'gallery' && + isUploadingGalleryImage && canWrite && ( )} + {isChatChannel && + negotiationMatch && + !channel.isDmInvite && + !editingPost && ( + + )} + {!isChatChannel && canWrite && !showBigInput && diff --git a/packages/ui/src/components/draftInputs/ChatInput.tsx b/packages/ui/src/components/draftInputs/ChatInput.tsx new file mode 100644 index 0000000000..c77b16ef56 --- /dev/null +++ b/packages/ui/src/components/draftInputs/ChatInput.tsx @@ -0,0 +1,42 @@ +import { MessageInput } from '../MessageInput'; +import { DraftInputContext } from './shared'; + +export function ChatInput({ + draftInputContext, +}: { + draftInputContext: DraftInputContext; +}) { + const { + channel, + clearDraft, + editPost, + editingPost, + getDraft, + group, + onSent, + send, + setEditingPost, + setShouldBlur, + shouldBlur, + storeDraft, + } = draftInputContext; + return ( + + ); +} diff --git a/packages/ui/src/components/draftInputs/index.ts b/packages/ui/src/components/draftInputs/index.ts new file mode 100644 index 0000000000..39e19f34dc --- /dev/null +++ b/packages/ui/src/components/draftInputs/index.ts @@ -0,0 +1,2 @@ +export { DraftInputContext } from './shared'; +export { ChatInput } from './ChatInput'; diff --git a/packages/ui/src/components/draftInputs/shared.ts b/packages/ui/src/components/draftInputs/shared.ts new file mode 100644 index 0000000000..73c199370d --- /dev/null +++ b/packages/ui/src/components/draftInputs/shared.ts @@ -0,0 +1,22 @@ +import * as db from '@tloncorp/shared/dist/db'; +import { JSONContent, Story } from '@tloncorp/shared/dist/urbit'; +import { Dispatch, SetStateAction } from 'react'; + +export interface DraftInputContext { + channel: db.Channel; + clearDraft: () => void; + editPost: (post: db.Post, content: Story) => Promise; + editingPost?: db.Post; + getDraft: () => Promise; + group: db.Group | null; + onSent?: () => void; + send: ( + content: Story, + channelId: string, + metadata?: db.PostMetadata + ) => Promise; + setEditingPost?: (update: db.Post | undefined) => void; + setShouldBlur: Dispatch>; + shouldBlur: boolean; + storeDraft: (content: JSONContent) => void; +} From 0b910688747ac96cfb6d38ec82dae6d9ec2e4343 Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 20:41:21 -0700 Subject: [PATCH 006/104] Pull gallery draft input into own component --- packages/ui/src/components/Channel/index.tsx | 216 ++++++------------ .../DraftInputConnectedBigInput.tsx | 68 ++++++ .../components/draftInputs/GalleryInput.tsx | 130 +++++++++++ .../ui/src/components/draftInputs/index.ts | 4 +- .../ui/src/components/draftInputs/shared.ts | 4 +- 5 files changed, 274 insertions(+), 148 deletions(-) create mode 100644 packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx create mode 100644 packages/ui/src/components/draftInputs/GalleryInput.tsx diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 3c652261d4..70991578db 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -24,25 +24,23 @@ import { Attachment, AttachmentProvider } from '../../contexts/attachment'; import { RequestsProvider } from '../../contexts/requests'; import { ScrollContextProvider } from '../../contexts/scroll'; import * as utils from '../../utils'; -import AddGalleryPost from '../AddGalleryPost'; -import { BigInput } from '../BigInput'; import { ChatMessage } from '../ChatMessage'; import { FloatingActionButton } from '../FloatingActionButton'; import { GalleryPost } from '../GalleryPost'; import { GroupPreviewAction, GroupPreviewSheet } from '../GroupPreviewSheet'; import { Icon } from '../Icon'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; -import { MessageInput } from '../MessageInput'; import { NotebookPost } from '../NotebookPost'; import { ChatInput, + DraftInputConnectedBigInput, DraftInputContext, + GalleryInput, } from '../draftInputs'; import { ChannelFooter } from './ChannelFooter'; import { ChannelHeader } from './ChannelHeader'; import { DmInviteOptions } from './DmInviteOptions'; import { EmptyChannelNotice } from './EmptyChannelNotice'; -import GalleryImagePreview from './GalleryImagePreview'; import Scroller, { ScrollAnchor } from './Scroller'; export { INITIAL_POSTS_PER_PAGE } from './Scroller'; @@ -132,7 +130,6 @@ export function Channel({ const [activeMessage, setActiveMessage] = useState(null); const [inputShouldBlur, setInputShouldBlur] = useState(false); const [showBigInput, setShowBigInput] = useState(false); - const [showAddGalleryPost, setShowAddGalleryPost] = useState(false); const [groupPreview, setGroupPreview] = useState(null); const disableNicknames = !!useCalm()?.disableNicknames; const title = channel ? utils.getChannelTitle(channel, disableNicknames) : ''; @@ -217,31 +214,22 @@ export function Channel({ ); const [isUploadingGalleryImage, setIsUploadingGalleryImage] = useState(false); - const handleGalleryImageSet = useCallback( - (assets?: ImagePickerAsset[] | null) => { - setIsUploadingGalleryImage(!!assets); - }, - [] - ); - - const handleGalleryPreviewClosed = useCallback(() => { - setIsUploadingGalleryImage(false); - }, []); - - const handleMessageSent = useCallback(() => { - setIsUploadingGalleryImage(false); - }, []); const handleSetEditingPost = useCallback( (post: db.Post | undefined) => { setEditingPost?.(post); - if (channel.type === 'gallery' || channel.type === 'notebook') { + if (channel.type === 'notebook') { setShowBigInput(true); } }, [setEditingPost, channel.type] ); + /** when `null`, input is not shown or presentation is unknown */ + const [draftInputPresentationMode, setDraftInputPresentationMode] = useState< + null | 'fullscreen' | 'inline' + >(null); + const draftInputContext = useMemo( (): DraftInputContext => ({ channel, @@ -250,7 +238,7 @@ export function Channel({ editingPost, getDraft, group, - onSent: handleMessageSent, + onPresentationModeChange: setDraftInputPresentationMode, send: messageSender, setEditingPost, setShouldBlur: setInputShouldBlur, @@ -264,7 +252,6 @@ export function Channel({ editingPost, getDraft, group, - handleMessageSent, inputShouldBlur, messageSender, setEditingPost, @@ -323,111 +310,61 @@ export function Channel({ - {showBigInput ? ( - - - - ) : isUploadingGalleryImage ? ( - - ) : ( - - {channel && posts && ( - 0 - ? initialChannelUnread?.firstUnreadPostId - : null - } - unreadCount={ - initialChannelUnread?.countWithoutThreads ?? - 0 - } - onPressPost={ - isChatChannel ? undefined : goToPost - } - onPressReplies={goToPost} - onPressImage={goToImageViewer} - onEndReached={onScrollEndReached} - onStartReached={onScrollStartReached} - onPressRetry={onPressRetry} - onPressDelete={onPressDelete} - activeMessage={activeMessage} - setActiveMessage={setActiveMessage} - ref={flatListRef} - /> - )} - - )} + { + // weird predicate is negation of above - this should go away when notebook input is implemented! + (!(channel.type !== 'gallery' && showBigInput) || + draftInputPresentationMode === 'fullscreen') && ( + + {channel && posts && ( + 0 + ? initialChannelUnread?.firstUnreadPostId + : null + } + unreadCount={ + initialChannelUnread?.countWithoutThreads ?? + 0 + } + onPressPost={ + isChatChannel ? undefined : goToPost + } + onPressReplies={goToPost} + onPressImage={goToImageViewer} + onEndReached={onScrollEndReached} + onStartReached={onScrollStartReached} + onPressRetry={onPressRetry} + onPressDelete={onPressDelete} + activeMessage={activeMessage} + setActiveMessage={setActiveMessage} + ref={flatListRef} + /> + )} + + ) + } - {negotiationMatch && - !channel.isDmInvite && - !editingPost && - channel.type === 'gallery' && - isUploadingGalleryImage && - canWrite && ( - - )} + + {channel.type === 'notebook' && showBigInput && ( + + )} {isChatChannel && negotiationMatch && @@ -436,13 +373,13 @@ export function Channel({ )} - {!isChatChannel && + {channel.type === 'gallery' && canWrite && ( + + )} + + {channel.type === 'notebook' && canWrite && - !showBigInput && - !( - channel.type === 'gallery' && - (showAddGalleryPost || isUploadingGalleryImage) - ) && ( + !showBigInput && ( - channel.type === 'gallery' - ? setShowAddGalleryPost(true) - : setShowBigInput(true) - } + onPress={() => setShowBigInput(true)} icon={ )} - {channel.type === 'gallery' && canWrite && ( - - )} - {!negotiationMatch && isChatChannel && canWrite && ( )} diff --git a/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx b/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx new file mode 100644 index 0000000000..6352d8a397 --- /dev/null +++ b/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx @@ -0,0 +1,68 @@ +import { Dispatch, SetStateAction } from 'react'; +import { AnimatePresence, View } from 'tamagui'; + +import { BigInput } from '../BigInput'; +import { DraftInputContext } from './shared'; + +/** + * `BigInput` set up for multiple kinds of draft inputs. + */ +export function DraftInputConnectedBigInput({ + draftInputContext, + setShowBigInput, +}: { + draftInputContext: DraftInputContext; + // TODO: I think this is only used to dismiss big input on send - remove and just dismiss in `onSent` callback + setShowBigInput: Dispatch>; +}) { + const { + channel, + clearDraft, + editPost, + editingPost, + getDraft, + group, + send, + setEditingPost, + setShouldBlur, + shouldBlur, + storeDraft, + } = draftInputContext; + + return ( + + + + + + ); +} diff --git a/packages/ui/src/components/draftInputs/GalleryInput.tsx b/packages/ui/src/components/draftInputs/GalleryInput.tsx new file mode 100644 index 0000000000..2578b15e72 --- /dev/null +++ b/packages/ui/src/components/draftInputs/GalleryInput.tsx @@ -0,0 +1,130 @@ +import { + isChatChannel as getIsChatChannel, + useChannel as useChannelFromStore, + useGroupPreview, + usePostReference as usePostReferenceHook, + usePostWithRelations, +} from '@tloncorp/shared/dist'; +import * as db from '@tloncorp/shared/dist/db'; +import { JSONContent, Story } from '@tloncorp/shared/dist/urbit'; +import { ImagePickerAsset } from 'expo-image-picker'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FlatList } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { AnimatePresence, SizableText, View, YStack } from 'tamagui'; + +import AddGalleryPost from '../AddGalleryPost'; +import GalleryImagePreview from '../Channel/GalleryImagePreview'; +import { FloatingActionButton } from '../FloatingActionButton'; +import { Icon } from '../Icon'; +import { MessageInput } from '../MessageInput'; +import { DraftInputConnectedBigInput } from './DraftInputConnectedBigInput'; +import { DraftInputContext } from './shared'; + +export function GalleryInput({ + draftInputContext, +}: { + draftInputContext: DraftInputContext; +}) { + const { + channel, + clearDraft, + editPost, + editingPost, + getDraft, + group, + onPresentationModeChange, + send, + setEditingPost, + setShouldBlur, + shouldBlur, + storeDraft, + } = draftInputContext; + const safeAreaInsets = useSafeAreaInsets(); + + const [showBigInput, setShowBigInput] = useState(false); + const [showAddGalleryPost, setShowAddGalleryPost] = useState(false); + const [isUploadingGalleryImage, setIsUploadingGalleryImage] = useState(false); + + const handleGalleryPreviewClosed = useCallback(() => { + setIsUploadingGalleryImage(false); + }, []); + const handleGalleryImageSet = useCallback( + (assets?: ImagePickerAsset[] | null) => { + setIsUploadingGalleryImage(!!assets); + }, + [] + ); + + // Notify host when presenting/dismissing big input + useEffect(() => { + onPresentationModeChange?.(showBigInput ? 'fullscreen' : 'inline'); + }, [showBigInput, onPresentationModeChange]); + + // Use big input when editing a post + const isEditingPost = editingPost != null; + useEffect(() => { + setShowBigInput(isEditingPost); + }, [isEditingPost]); + + return ( + <> + + {showBigInput && ( + + )} + + {isUploadingGalleryImage && ( + + )} + + + {!showBigInput && !showAddGalleryPost && !isUploadingGalleryImage && ( + + setShowAddGalleryPost(true)} + icon={} + /> + + )} + + {!editingPost && isUploadingGalleryImage && ( + { + setIsUploadingGalleryImage(false); + }} + showInlineAttachments={false} + showAttachmentButton={false} + /> + )} + + + + ); +} diff --git a/packages/ui/src/components/draftInputs/index.ts b/packages/ui/src/components/draftInputs/index.ts index 39e19f34dc..da4d922546 100644 --- a/packages/ui/src/components/draftInputs/index.ts +++ b/packages/ui/src/components/draftInputs/index.ts @@ -1,2 +1,4 @@ -export { DraftInputContext } from './shared'; export { ChatInput } from './ChatInput'; +export { DraftInputConnectedBigInput } from './DraftInputConnectedBigInput'; +export { DraftInputContext } from './shared'; +export { GalleryInput } from './GalleryInput'; diff --git a/packages/ui/src/components/draftInputs/shared.ts b/packages/ui/src/components/draftInputs/shared.ts index 73c199370d..82a2bab9c5 100644 --- a/packages/ui/src/components/draftInputs/shared.ts +++ b/packages/ui/src/components/draftInputs/shared.ts @@ -9,7 +9,9 @@ export interface DraftInputContext { editingPost?: db.Post; getDraft: () => Promise; group: db.Group | null; - onSent?: () => void; + onPresentationModeChange?: ( + presentationMode: 'inline' | 'fullscreen' + ) => void; send: ( content: Story, channelId: string, From 933266a96693326b54620cc87f835170ccf53271 Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 20:53:24 -0700 Subject: [PATCH 007/104] Pull notebook draft input into own component --- packages/ui/src/components/Channel/index.tsx | 133 +++++++----------- .../components/draftInputs/NotebookInput.tsx | 56 ++++++++ .../ui/src/components/draftInputs/index.ts | 1 + 3 files changed, 107 insertions(+), 83 deletions(-) create mode 100644 packages/ui/src/components/draftInputs/NotebookInput.tsx diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 70991578db..87de4c66a6 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -25,17 +25,15 @@ import { RequestsProvider } from '../../contexts/requests'; import { ScrollContextProvider } from '../../contexts/scroll'; import * as utils from '../../utils'; import { ChatMessage } from '../ChatMessage'; -import { FloatingActionButton } from '../FloatingActionButton'; import { GalleryPost } from '../GalleryPost'; import { GroupPreviewAction, GroupPreviewSheet } from '../GroupPreviewSheet'; -import { Icon } from '../Icon'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; import { NotebookPost } from '../NotebookPost'; import { ChatInput, - DraftInputConnectedBigInput, DraftInputContext, GalleryInput, + NotebookInput, } from '../draftInputs'; import { ChannelFooter } from './ChannelFooter'; import { ChannelHeader } from './ChannelHeader'; @@ -218,11 +216,8 @@ export function Channel({ const handleSetEditingPost = useCallback( (post: db.Post | undefined) => { setEditingPost?.(post); - if (channel.type === 'notebook') { - setShowBigInput(true); - } }, - [setEditingPost, channel.type] + [setEditingPost] ); /** when `null`, input is not shown or presentation is unknown */ @@ -310,62 +305,51 @@ export function Channel({ - { - // weird predicate is negation of above - this should go away when notebook input is implemented! - (!(channel.type !== 'gallery' && showBigInput) || - draftInputPresentationMode === 'fullscreen') && ( - - {channel && posts && ( - 0 - ? initialChannelUnread?.firstUnreadPostId - : null - } - unreadCount={ - initialChannelUnread?.countWithoutThreads ?? - 0 - } - onPressPost={ - isChatChannel ? undefined : goToPost - } - onPressReplies={goToPost} - onPressImage={goToImageViewer} - onEndReached={onScrollEndReached} - onStartReached={onScrollStartReached} - onPressRetry={onPressRetry} - onPressDelete={onPressDelete} - activeMessage={activeMessage} - setActiveMessage={setActiveMessage} - ref={flatListRef} - /> - )} - - ) - } + {draftInputPresentationMode !== 'fullscreen' && ( + + {channel && posts && ( + 0 + ? initialChannelUnread?.firstUnreadPostId + : null + } + unreadCount={ + initialChannelUnread?.countWithoutThreads ?? + 0 + } + onPressPost={ + isChatChannel ? undefined : goToPost + } + onPressReplies={goToPost} + onPressImage={goToImageViewer} + onEndReached={onScrollEndReached} + onStartReached={onScrollStartReached} + onPressRetry={onPressRetry} + onPressDelete={onPressDelete} + activeMessage={activeMessage} + setActiveMessage={setActiveMessage} + ref={flatListRef} + /> + )} + + )} - {channel.type === 'notebook' && showBigInput && ( - - )} - {isChatChannel && negotiationMatch && !channel.isDmInvite && @@ -377,28 +361,11 @@ export function Channel({ )} - {channel.type === 'notebook' && - canWrite && - !showBigInput && ( - - setShowBigInput(true)} - icon={ - - } - /> - - )} + {channel.type === 'notebook' && canWrite && ( + + )} {!negotiationMatch && isChatChannel && canWrite && ( diff --git a/packages/ui/src/components/draftInputs/NotebookInput.tsx b/packages/ui/src/components/draftInputs/NotebookInput.tsx new file mode 100644 index 0000000000..a4c3028361 --- /dev/null +++ b/packages/ui/src/components/draftInputs/NotebookInput.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { View } from 'tamagui'; + +import { FloatingActionButton } from '../FloatingActionButton'; +import { Icon } from '../Icon'; +import { DraftInputConnectedBigInput } from './DraftInputConnectedBigInput'; +import { DraftInputContext } from './shared'; + +export function NotebookInput({ + draftInputContext, +}: { + draftInputContext: DraftInputContext; +}) { + const { editingPost, onPresentationModeChange } = draftInputContext; + + const [showBigInput, setShowBigInput] = useState(false); + const safeAreaInsets = useSafeAreaInsets(); + + // Notify host when presenting/dismissing big input + useEffect(() => { + onPresentationModeChange?.(showBigInput ? 'fullscreen' : 'inline'); + }, [showBigInput, onPresentationModeChange]); + + // Use big input when editing a post + const isEditingPost = editingPost != null; + useEffect(() => { + setShowBigInput(isEditingPost); + }, [isEditingPost]); + + return ( + <> + {showBigInput && ( + + )} + + {!showBigInput && ( + + setShowBigInput(true)} + icon={} + /> + + )} + + ); +} diff --git a/packages/ui/src/components/draftInputs/index.ts b/packages/ui/src/components/draftInputs/index.ts index da4d922546..b93c6cafb4 100644 --- a/packages/ui/src/components/draftInputs/index.ts +++ b/packages/ui/src/components/draftInputs/index.ts @@ -2,3 +2,4 @@ export { ChatInput } from './ChatInput'; export { DraftInputConnectedBigInput } from './DraftInputConnectedBigInput'; export { DraftInputContext } from './shared'; export { GalleryInput } from './GalleryInput'; +export { NotebookInput } from './NotebookInput'; From c899ec534ac34efaeefa2456d31bc6f39d96be14 Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 20:55:41 -0700 Subject: [PATCH 008/104] Remove duplicate NegotionMismatchNotice --- packages/ui/src/components/Channel/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 87de4c66a6..1e969bef33 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -367,9 +367,6 @@ export function Channel({ /> )} - {!negotiationMatch && isChatChannel && canWrite && ( - - )} {channel.isDmInvite && ( )} From f65ced027f24e2e2df748335e18a20dd30c187e9 Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 21:02:22 -0700 Subject: [PATCH 009/104] move editing check into input; restore canWrite check; remove `onSent` --- packages/ui/src/components/Channel/index.tsx | 2 +- packages/ui/src/components/draftInputs/ChatInput.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 1e969bef33..4a8f218d70 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -353,7 +353,7 @@ export function Channel({ {isChatChannel && negotiationMatch && !channel.isDmInvite && - !editingPost && ( + canWrite && ( )} diff --git a/packages/ui/src/components/draftInputs/ChatInput.tsx b/packages/ui/src/components/draftInputs/ChatInput.tsx index c77b16ef56..7a6373f906 100644 --- a/packages/ui/src/components/draftInputs/ChatInput.tsx +++ b/packages/ui/src/components/draftInputs/ChatInput.tsx @@ -13,13 +13,16 @@ export function ChatInput({ editingPost, getDraft, group, - onSent, send, setEditingPost, setShouldBlur, shouldBlur, storeDraft, } = draftInputContext; + if (editingPost != null) { + return null; + } + return ( From fd147c2c2c4cbd00ae8887ea359ab4724b2d0771 Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 21:07:38 -0700 Subject: [PATCH 010/104] Structure channel-level input conditionals --- packages/ui/src/components/Channel/index.tsx | 38 +++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 4a8f218d70..43f545fd64 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -350,29 +350,35 @@ export function Channel({ )} - {isChatChannel && - negotiationMatch && - !channel.isDmInvite && - canWrite && ( - - )} + {canWrite && ( + <> + {isChatChannel && + !channel.isDmInvite && + (negotiationMatch ? ( + + ) : ( + + ))} - {channel.type === 'gallery' && canWrite && ( - - )} + {channel.type === 'gallery' && ( + + )} - {channel.type === 'notebook' && canWrite && ( - + {channel.type === 'notebook' && ( + + )} + )} {channel.isDmInvite && ( )} - {!negotiationMatch && isChatChannel && canWrite && ( - - )} {headerMode === 'next' ? ( From 05c35d7d5d178865745863dfe0c5ab721cfbe7fb Mon Sep 17 00:00:00 2001 From: David Lee Date: Sat, 28 Sep 2024 21:10:03 -0700 Subject: [PATCH 011/104] Remove unnecessary `key` prop --- .../src/components/draftInputs/DraftInputConnectedBigInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx b/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx index 6352d8a397..92161aabf7 100644 --- a/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx +++ b/packages/ui/src/components/draftInputs/DraftInputConnectedBigInput.tsx @@ -32,7 +32,6 @@ export function DraftInputConnectedBigInput({ return ( Date: Mon, 30 Sep 2024 13:21:26 -0700 Subject: [PATCH 012/104] Add ParentAgnosticKeyboardAvoidingView --- apps/tlon-mobile/cosmos.imports.ts | 242 +++++++++--------- ...ntAgnosticKeyboardAvoidingView.fixture.tsx | 85 ++++++ .../ParentAgnosticKeyboardAvoidingView.tsx | 90 +++++++ packages/ui/src/index.tsx | 1 + 4 files changed, 290 insertions(+), 128 deletions(-) create mode 100644 apps/tlon-mobile/src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx create mode 100644 packages/ui/src/components/ParentAgnosticKeyboardAvoidingView.tsx diff --git a/apps/tlon-mobile/cosmos.imports.ts b/apps/tlon-mobile/cosmos.imports.ts index 04069af491..5a00a032c4 100644 --- a/apps/tlon-mobile/cosmos.imports.ts +++ b/apps/tlon-mobile/cosmos.imports.ts @@ -1,149 +1,135 @@ // This file is automatically generated by Cosmos. Add it to .gitignore and // only edit if you know what you're doing. + import { RendererConfig, UserModuleWrappers } from 'react-cosmos-core'; import * as fixture0 from './src/App.fixture'; -import * as fixture52 from './src/fixtures/ActionSheet/AddGalleryPostSheet.fixture'; -import * as fixture51 from './src/fixtures/ActionSheet/AttachmentSheet.fixture'; -import * as fixture50 from './src/fixtures/ActionSheet/ChannelSortActionsSheet.fixture'; -import * as fixture49 from './src/fixtures/ActionSheet/CreateChannelSheet.fixture'; -import * as fixture48 from './src/fixtures/ActionSheet/DeleteSheet.fixture'; -import * as fixture47 from './src/fixtures/ActionSheet/EditSectionNameSheet.fixture'; -import * as fixture46 from './src/fixtures/ActionSheet/GenericActionSheet.fixture'; -import * as fixture45 from './src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture'; -import * as fixture44 from './src/fixtures/ActionSheet/GroupPreviewSheet.fixture'; -import * as fixture43 from './src/fixtures/ActionSheet/ProfileSheet.fixture'; -import * as fixture42 from './src/fixtures/ActionSheet/SendPostRetrySheet.fixture'; -import * as fixture38 from './src/fixtures/Activity.fixture'; -import * as fixture37 from './src/fixtures/AddGroupSheet.fixture'; -import * as fixture36 from './src/fixtures/AttachmentPreviewList.fixture'; -import * as fixture35 from './src/fixtures/AudioEmbed.fixture'; -import * as fixture34 from './src/fixtures/Avatar.fixture'; -import * as fixture33 from './src/fixtures/BlockSectionList.fixture'; -import * as fixture32 from './src/fixtures/Button.fixture'; -import * as fixture31 from './src/fixtures/Channel.fixture'; -import * as fixture30 from './src/fixtures/ChannelDivider.fixture'; -import * as fixture29 from './src/fixtures/ChannelHeader.fixture'; -import * as fixture28 from './src/fixtures/ChannelSwitcherSheet.fixture'; -import * as fixture27 from './src/fixtures/ChatMessage.fixture'; -import * as fixture26 from './src/fixtures/ContactList.fixture'; -import * as fixture25 from './src/fixtures/CreateGroup.fixture'; -import * as fixture41 from './src/fixtures/DetailView/ChatDetailView.fixture'; -import * as fixture40 from './src/fixtures/DetailView/GalleryDetailView.fixture'; -import * as fixture39 from './src/fixtures/DetailView/NotebookDetailView.fixture'; -import * as fixture24 from './src/fixtures/FindGroups.fixture'; -import * as fixture23 from './src/fixtures/Form.fixture'; -import * as fixture22 from './src/fixtures/GalleryPost.fixture'; -import * as fixture21 from './src/fixtures/GroupList.fixture'; -import * as fixture20 from './src/fixtures/GroupListItem.fixture'; -import * as fixture19 from './src/fixtures/ImageViewer.fixture'; -import * as fixture18 from './src/fixtures/Input.fixture'; -import * as fixture17 from './src/fixtures/InputToolbar.fixture'; -import * as fixture16 from './src/fixtures/InviteUsersSheet.fixture'; -import * as fixture15 from './src/fixtures/MessageActions.fixture'; -import * as fixture14 from './src/fixtures/MessageInput.fixture'; -import * as fixture13 from './src/fixtures/MetaEditorScreen.fixture'; -import * as fixture12 from './src/fixtures/OutsideEmbed.fixture'; -import * as fixture11 from './src/fixtures/PostReference.fixture'; -import * as fixture10 from './src/fixtures/PostScreen.fixture'; -import * as fixture9 from './src/fixtures/ProfileBlock.fixture'; -import * as fixture8 from './src/fixtures/ProfileSheet.fixture'; -import * as fixture7 from './src/fixtures/ReferenceSkeleton.fixture'; -import * as fixture6 from './src/fixtures/ScreenHeader.fixture'; -import * as fixture5 from './src/fixtures/SearchBar.fixture'; -import * as fixture4 from './src/fixtures/Text.fixture'; -import * as fixture3 from './src/fixtures/UserProfileScreen.fixture'; -import * as fixture2 from './src/fixtures/VideoEmbed.fixture'; -import * as fixture1 from './src/fixtures/ViewReactionsSheet.fixture'; +import * as fixture1 from './src/fixtures/YStackKavDemo.fixture'; +import * as fixture2 from './src/fixtures/ViewReactionsSheet.fixture'; +import * as fixture3 from './src/fixtures/VideoEmbed.fixture'; +import * as fixture4 from './src/fixtures/UserProfileScreen.fixture'; +import * as fixture5 from './src/fixtures/Text.fixture'; +import * as fixture6 from './src/fixtures/SearchBar.fixture'; +import * as fixture7 from './src/fixtures/ScreenHeader.fixture'; +import * as fixture8 from './src/fixtures/ReferenceSkeleton.fixture'; +import * as fixture9 from './src/fixtures/ProfileSheet.fixture'; +import * as fixture10 from './src/fixtures/ProfileBlock.fixture'; +import * as fixture11 from './src/fixtures/PostScreen.fixture'; +import * as fixture12 from './src/fixtures/PostReference.fixture'; +import * as fixture13 from './src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture'; +import * as fixture14 from './src/fixtures/OutsideEmbed.fixture'; +import * as fixture15 from './src/fixtures/MetaEditorScreen.fixture'; +import * as fixture16 from './src/fixtures/MessageInput.fixture'; +import * as fixture17 from './src/fixtures/MessageActions.fixture'; +import * as fixture18 from './src/fixtures/InviteUsersSheet.fixture'; +import * as fixture19 from './src/fixtures/InputToolbar.fixture'; +import * as fixture20 from './src/fixtures/Input.fixture'; +import * as fixture21 from './src/fixtures/ImageViewer.fixture'; +import * as fixture22 from './src/fixtures/GroupListItem.fixture'; +import * as fixture23 from './src/fixtures/GroupList.fixture'; +import * as fixture24 from './src/fixtures/GalleryPost.fixture'; +import * as fixture25 from './src/fixtures/Form.fixture'; +import * as fixture26 from './src/fixtures/FindGroups.fixture'; +import * as fixture27 from './src/fixtures/CreateGroup.fixture'; +import * as fixture28 from './src/fixtures/ContactList.fixture'; +import * as fixture29 from './src/fixtures/ChatMessage.fixture'; +import * as fixture30 from './src/fixtures/ChannelSwitcherSheet.fixture'; +import * as fixture31 from './src/fixtures/ChannelHeader.fixture'; +import * as fixture32 from './src/fixtures/ChannelDivider.fixture'; +import * as fixture33 from './src/fixtures/Channel.fixture'; +import * as fixture34 from './src/fixtures/Button.fixture'; +import * as fixture35 from './src/fixtures/BlockSectionList.fixture'; +import * as fixture36 from './src/fixtures/Avatar.fixture'; +import * as fixture37 from './src/fixtures/AudioEmbed.fixture'; +import * as fixture38 from './src/fixtures/AttachmentPreviewList.fixture'; +import * as fixture39 from './src/fixtures/AddGroupSheet.fixture'; +import * as fixture40 from './src/fixtures/Activity.fixture'; +import * as fixture41 from './src/fixtures/DetailView/NotebookDetailView.fixture'; +import * as fixture42 from './src/fixtures/DetailView/GalleryDetailView.fixture'; +import * as fixture43 from './src/fixtures/DetailView/ChatDetailView.fixture'; +import * as fixture44 from './src/fixtures/ActionSheet/SendPostRetrySheet.fixture'; +import * as fixture45 from './src/fixtures/ActionSheet/ProfileSheet.fixture'; +import * as fixture46 from './src/fixtures/ActionSheet/GroupPreviewSheet.fixture'; +import * as fixture47 from './src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture'; +import * as fixture48 from './src/fixtures/ActionSheet/GenericActionSheet.fixture'; +import * as fixture49 from './src/fixtures/ActionSheet/EditSectionNameSheet.fixture'; +import * as fixture50 from './src/fixtures/ActionSheet/DeleteSheet.fixture'; +import * as fixture51 from './src/fixtures/ActionSheet/CreateChannelSheet.fixture'; +import * as fixture52 from './src/fixtures/ActionSheet/ChannelSortActionsSheet.fixture'; +import * as fixture53 from './src/fixtures/ActionSheet/AttachmentSheet.fixture'; +import * as fixture54 from './src/fixtures/ActionSheet/AddGalleryPostSheet.fixture'; + import * as decorator0 from './src/fixtures/cosmos.decorator'; export const rendererConfig: RendererConfig = { - playgroundUrl: 'http://localhost:5000', - rendererUrl: null, + "playgroundUrl": "http://localhost:5001", + "rendererUrl": null }; const fixtures = { 'src/App.fixture.tsx': { module: fixture0 }, - 'src/fixtures/ViewReactionsSheet.fixture.tsx': { module: fixture1 }, - 'src/fixtures/VideoEmbed.fixture.tsx': { module: fixture2 }, - 'src/fixtures/UserProfileScreen.fixture.tsx': { module: fixture3 }, - 'src/fixtures/Text.fixture.tsx': { module: fixture4 }, - 'src/fixtures/SearchBar.fixture.tsx': { module: fixture5 }, - 'src/fixtures/ScreenHeader.fixture.tsx': { module: fixture6 }, - 'src/fixtures/ReferenceSkeleton.fixture.tsx': { module: fixture7 }, - 'src/fixtures/ProfileSheet.fixture.tsx': { module: fixture8 }, - 'src/fixtures/ProfileBlock.fixture.tsx': { module: fixture9 }, - 'src/fixtures/PostScreen.fixture.tsx': { module: fixture10 }, - 'src/fixtures/PostReference.fixture.tsx': { module: fixture11 }, - 'src/fixtures/OutsideEmbed.fixture.tsx': { module: fixture12 }, - 'src/fixtures/MetaEditorScreen.fixture.tsx': { module: fixture13 }, - 'src/fixtures/MessageInput.fixture.tsx': { module: fixture14 }, - 'src/fixtures/MessageActions.fixture.tsx': { module: fixture15 }, - 'src/fixtures/InviteUsersSheet.fixture.tsx': { module: fixture16 }, - 'src/fixtures/InputToolbar.fixture.tsx': { module: fixture17 }, - 'src/fixtures/Input.fixture.tsx': { module: fixture18 }, - 'src/fixtures/ImageViewer.fixture.tsx': { module: fixture19 }, - 'src/fixtures/GroupListItem.fixture.tsx': { module: fixture20 }, - 'src/fixtures/GroupList.fixture.tsx': { module: fixture21 }, - 'src/fixtures/GalleryPost.fixture.tsx': { module: fixture22 }, - 'src/fixtures/Form.fixture.tsx': { module: fixture23 }, - 'src/fixtures/FindGroups.fixture.tsx': { module: fixture24 }, - 'src/fixtures/CreateGroup.fixture.tsx': { module: fixture25 }, - 'src/fixtures/ContactList.fixture.tsx': { module: fixture26 }, - 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture27 }, - 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture28 }, - 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture29 }, - 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture30 }, - 'src/fixtures/Channel.fixture.tsx': { module: fixture31 }, - 'src/fixtures/Button.fixture.tsx': { module: fixture32 }, - 'src/fixtures/BlockSectionList.fixture.tsx': { module: fixture33 }, - 'src/fixtures/Avatar.fixture.tsx': { module: fixture34 }, - 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture35 }, - 'src/fixtures/AttachmentPreviewList.fixture.tsx': { module: fixture36 }, - 'src/fixtures/AddGroupSheet.fixture.tsx': { module: fixture37 }, - 'src/fixtures/Activity.fixture.tsx': { module: fixture38 }, - 'src/fixtures/DetailView/NotebookDetailView.fixture.tsx': { - module: fixture39, - }, - 'src/fixtures/DetailView/GalleryDetailView.fixture.tsx': { - module: fixture40, - }, - 'src/fixtures/DetailView/ChatDetailView.fixture.tsx': { module: fixture41 }, - 'src/fixtures/ActionSheet/SendPostRetrySheet.fixture.tsx': { - module: fixture42, - }, - 'src/fixtures/ActionSheet/ProfileSheet.fixture.tsx': { module: fixture43 }, - 'src/fixtures/ActionSheet/GroupPreviewSheet.fixture.tsx': { - module: fixture44, - }, - 'src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture.tsx': { - module: fixture45, - }, - 'src/fixtures/ActionSheet/GenericActionSheet.fixture.tsx': { - module: fixture46, - }, - 'src/fixtures/ActionSheet/EditSectionNameSheet.fixture.tsx': { - module: fixture47, - }, - 'src/fixtures/ActionSheet/DeleteSheet.fixture.tsx': { module: fixture48 }, - 'src/fixtures/ActionSheet/CreateChannelSheet.fixture.tsx': { - module: fixture49, - }, - 'src/fixtures/ActionSheet/ChannelSortActionsSheet.fixture.tsx': { - module: fixture50, - }, - 'src/fixtures/ActionSheet/AttachmentSheet.fixture.tsx': { module: fixture51 }, - 'src/fixtures/ActionSheet/AddGalleryPostSheet.fixture.tsx': { - module: fixture52, - }, + 'src/fixtures/YStackKavDemo.fixture.tsx': { module: fixture1 }, + 'src/fixtures/ViewReactionsSheet.fixture.tsx': { module: fixture2 }, + 'src/fixtures/VideoEmbed.fixture.tsx': { module: fixture3 }, + 'src/fixtures/UserProfileScreen.fixture.tsx': { module: fixture4 }, + 'src/fixtures/Text.fixture.tsx': { module: fixture5 }, + 'src/fixtures/SearchBar.fixture.tsx': { module: fixture6 }, + 'src/fixtures/ScreenHeader.fixture.tsx': { module: fixture7 }, + 'src/fixtures/ReferenceSkeleton.fixture.tsx': { module: fixture8 }, + 'src/fixtures/ProfileSheet.fixture.tsx': { module: fixture9 }, + 'src/fixtures/ProfileBlock.fixture.tsx': { module: fixture10 }, + 'src/fixtures/PostScreen.fixture.tsx': { module: fixture11 }, + 'src/fixtures/PostReference.fixture.tsx': { module: fixture12 }, + 'src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx': { module: fixture13 }, + 'src/fixtures/OutsideEmbed.fixture.tsx': { module: fixture14 }, + 'src/fixtures/MetaEditorScreen.fixture.tsx': { module: fixture15 }, + 'src/fixtures/MessageInput.fixture.tsx': { module: fixture16 }, + 'src/fixtures/MessageActions.fixture.tsx': { module: fixture17 }, + 'src/fixtures/InviteUsersSheet.fixture.tsx': { module: fixture18 }, + 'src/fixtures/InputToolbar.fixture.tsx': { module: fixture19 }, + 'src/fixtures/Input.fixture.tsx': { module: fixture20 }, + 'src/fixtures/ImageViewer.fixture.tsx': { module: fixture21 }, + 'src/fixtures/GroupListItem.fixture.tsx': { module: fixture22 }, + 'src/fixtures/GroupList.fixture.tsx': { module: fixture23 }, + 'src/fixtures/GalleryPost.fixture.tsx': { module: fixture24 }, + 'src/fixtures/Form.fixture.tsx': { module: fixture25 }, + 'src/fixtures/FindGroups.fixture.tsx': { module: fixture26 }, + 'src/fixtures/CreateGroup.fixture.tsx': { module: fixture27 }, + 'src/fixtures/ContactList.fixture.tsx': { module: fixture28 }, + 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture29 }, + 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture30 }, + 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture31 }, + 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture32 }, + 'src/fixtures/Channel.fixture.tsx': { module: fixture33 }, + 'src/fixtures/Button.fixture.tsx': { module: fixture34 }, + 'src/fixtures/BlockSectionList.fixture.tsx': { module: fixture35 }, + 'src/fixtures/Avatar.fixture.tsx': { module: fixture36 }, + 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture37 }, + 'src/fixtures/AttachmentPreviewList.fixture.tsx': { module: fixture38 }, + 'src/fixtures/AddGroupSheet.fixture.tsx': { module: fixture39 }, + 'src/fixtures/Activity.fixture.tsx': { module: fixture40 }, + 'src/fixtures/DetailView/NotebookDetailView.fixture.tsx': { module: fixture41 }, + 'src/fixtures/DetailView/GalleryDetailView.fixture.tsx': { module: fixture42 }, + 'src/fixtures/DetailView/ChatDetailView.fixture.tsx': { module: fixture43 }, + 'src/fixtures/ActionSheet/SendPostRetrySheet.fixture.tsx': { module: fixture44 }, + 'src/fixtures/ActionSheet/ProfileSheet.fixture.tsx': { module: fixture45 }, + 'src/fixtures/ActionSheet/GroupPreviewSheet.fixture.tsx': { module: fixture46 }, + 'src/fixtures/ActionSheet/GroupJoinRequestSheet.fixture.tsx': { module: fixture47 }, + 'src/fixtures/ActionSheet/GenericActionSheet.fixture.tsx': { module: fixture48 }, + 'src/fixtures/ActionSheet/EditSectionNameSheet.fixture.tsx': { module: fixture49 }, + 'src/fixtures/ActionSheet/DeleteSheet.fixture.tsx': { module: fixture50 }, + 'src/fixtures/ActionSheet/CreateChannelSheet.fixture.tsx': { module: fixture51 }, + 'src/fixtures/ActionSheet/ChannelSortActionsSheet.fixture.tsx': { module: fixture52 }, + 'src/fixtures/ActionSheet/AttachmentSheet.fixture.tsx': { module: fixture53 }, + 'src/fixtures/ActionSheet/AddGalleryPostSheet.fixture.tsx': { module: fixture54 } }; const decorators = { - 'src/fixtures/cosmos.decorator.tsx': { module: decorator0 }, + 'src/fixtures/cosmos.decorator.tsx': { module: decorator0 } }; export const moduleWrappers: UserModuleWrappers = { lazy: false, fixtures, - decorators, + decorators }; diff --git a/apps/tlon-mobile/src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx b/apps/tlon-mobile/src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx new file mode 100644 index 0000000000..0e1d4fdbdc --- /dev/null +++ b/apps/tlon-mobile/src/fixtures/ParentAgnosticKeyboardAvoidingView.fixture.tsx @@ -0,0 +1,85 @@ +import { ParentAgnosticKeyboardAvoidingView } from '@tloncorp/ui'; +import { useFixtureSelect } from 'react-cosmos/client'; +import { + Button, + Keyboard, + KeyboardAvoidingView, + TextInput, +} from 'react-native'; +import { ScrollView, View as TamaguiView, Text, YStack } from 'tamagui'; + +import { FixtureWrapper } from './FixtureWrapper'; + +export default function ParentAgnosticKeyboardAvoidingViewDemo() { + const [componentType, setComponentType] = useFixtureSelect( + 'KAV implementation', + { + defaultValue: 'ParentAgnosticKeyboardAvoidingView', + options: ['ParentAgnosticKeyboardAvoidingView', 'KeyboardAvoidingView'], + } + ); + + const KAVComponent = + componentType === 'ParentAgnosticKeyboardAvoidingView' + ? ParentAgnosticKeyboardAvoidingView + : KeyboardAvoidingView; + + return ( + + 0` would break layout. + paddingTop={100} + backgroundColor="$background" + flex={1} + > + + + + ); } From 4a81f9531d768b57dc8c0111dc86ec674f1bfb4a Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 30 Sep 2024 19:52:57 -0400 Subject: [PATCH 026/104] InviteUsersWidget: drastically simplify interaction --- .../ui/src/components/InviteUsersWidget.tsx | 109 ++---------------- 1 file changed, 11 insertions(+), 98 deletions(-) diff --git a/packages/ui/src/components/InviteUsersWidget.tsx b/packages/ui/src/components/InviteUsersWidget.tsx index 3cf0ddd1ab..a3c3cf0f47 100644 --- a/packages/ui/src/components/InviteUsersWidget.tsx +++ b/packages/ui/src/components/InviteUsersWidget.tsx @@ -1,16 +1,11 @@ import * as db from '@tloncorp/shared/dist/db'; import * as store from '@tloncorp/shared/dist/store'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Share } from 'react-native'; -import { isWeb } from 'tamagui'; +import React, { useCallback, useMemo, useState } from 'react'; -import { useBranchDomain, useBranchKey, useCurrentUserId } from '../contexts'; -import { useCopy } from '../hooks/useCopy'; import { ActionSheet } from './ActionSheet'; import { Button } from './Button'; import { ContactBook } from './ContactBook'; -import { Icon } from './Icon'; -import { LoadingSpinner } from './LoadingSpinner'; +import { InviteFriendsToTlonButton } from './InviteFriendsToTlonButton'; const InviteUsersWidgetComponent = ({ group, @@ -20,105 +15,29 @@ const InviteUsersWidgetComponent = ({ onInviteComplete: () => void; }) => { const [invitees, setInvitees] = useState([]); - const currentUser = useCurrentUserId(); - const branchDomain = useBranchDomain(); - const branchKey = useBranchKey(); - const { status, shareUrl, toggle, describe } = store.useLureLinkStatus({ - flag: group.id, - branchDomain: branchDomain, - branchKey: branchKey, - }); - const { doCopy } = useCopy(shareUrl || ''); - const currentUserIsAdmin = useMemo( - () => - group?.members?.some( - (m) => - m.contactId === currentUser && - m.roles?.some((r) => r.roleId === 'admin') - ) ?? false, - [currentUser, group?.members] - ); - - const handleInviteButtonPress = useCallback(async () => { - if (invitees.length === 0 && shareUrl && status === 'ready') { - if (isWeb) { - if (navigator.share !== undefined) { - await navigator.share({ - title: `Join ${group.title} on Tlon`, - url: shareUrl, - }); - return; - } - - doCopy(); - return; - } - - await Share.share({ - message: `Join ${group.title} on Tlon: ${shareUrl}`, - title: `Join ${group.title} on Tlon`, - }); - - return; - } + const handleInviteGroupMembers = useCallback(async () => { await store.inviteGroupMembers({ groupId: group.id, contactIds: invitees, }); onInviteComplete(); - }, [ - invitees, - group.id, - onInviteComplete, - shareUrl, - group.title, - doCopy, - status, - ]); - - useEffect(() => { - const meta = { - title: group.title ?? '', - description: group.description ?? '', - cover: group.coverImage ?? '', - image: group.iconImage ?? '', - }; - - const toggleLink = async () => { - await toggle(meta); - }; - if (status === 'disabled' && currentUserIsAdmin) { - toggleLink(); - } - if (status === 'stale') { - describe(meta); - } - }, [ - group, - branchDomain, - branchKey, - toggle, - status, - currentUserIsAdmin, - describe, - ]); + }, [invitees, group.id, onInviteComplete]); const buttonText = useMemo(() => { - if (invitees.length === 0 && status === 'ready') { - return `Invite friends that aren't on Tlon`; - } - if (invitees.length === 0) { - return `Invite`; + return `Select people to invite`; } return `Invite ${invitees.length} and continue`; - }, [invitees, status]); + }, [invitees]); return ( <> + + + From e3481eca494b73e05f1da92872d0e8f50ffb9134 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 00:34:55 -0700 Subject: [PATCH 027/104] add invite link screen --- apps/tlon-mobile/src/App.main.tsx | 5 + .../Onboarding/InventoryCheckScreen.tsx | 2 +- .../screens/Onboarding/InviteLinkScreen.tsx | 181 ++++++++++++++++++ apps/tlon-mobile/src/types.ts | 1 + packages/app/contexts/branch.tsx | 24 ++- packages/shared/src/logic/branch.ts | 2 +- packages/shared/src/logic/deeplinks.ts | 41 +++- packages/ui/src/components/Form/inputs.tsx | 32 +++- 8 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx diff --git a/apps/tlon-mobile/src/App.main.tsx b/apps/tlon-mobile/src/App.main.tsx index 7719595235..25cca6511c 100644 --- a/apps/tlon-mobile/src/App.main.tsx +++ b/apps/tlon-mobile/src/App.main.tsx @@ -34,6 +34,7 @@ import AuthenticatedApp from './components/AuthenticatedApp'; import { CheckVerifyScreen } from './screens/Onboarding/CheckVerifyScreen'; import { EULAScreen } from './screens/Onboarding/EULAScreen'; import { InventoryCheckScreen } from './screens/Onboarding/InventoryCheckScreen'; +import { InviteLinkScreen } from './screens/Onboarding/InviteLinkScreen'; import { JoinWaitListScreen } from './screens/Onboarding/JoinWaitListScreen'; import { RequestPhoneVerifyScreen } from './screens/Onboarding/RequestPhoneVerifyScreen'; import { ReserveShipScreen } from './screens/Onboarding/ReserveShipScreen'; @@ -110,6 +111,10 @@ const App = ({ component={SignUpEmailScreen} /> + { if (enabled) { navigation.navigate('SignUpEmail'); } else { - navigation.navigate('JoinWaitList', {}); + navigation.navigate('InviteLink'); } } catch (err) { console.error('Error checking hosting availability:', err); diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx new file mode 100644 index 0000000000..e3a185df8c --- /dev/null +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -0,0 +1,181 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { + BRANCH_DOMAIN, + BRANCH_KEY, + EMAIL_REGEX, +} from '@tloncorp/app/constants'; +import { useBranch, useLureMetadata } from '@tloncorp/app/contexts/branch'; +import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; +import { + DeepLinkData, + createInviteLinkRegex, + extractNormalizedInviteLink, + getMetadaFromInviteLink, +} from '@tloncorp/shared/dist'; +import { + ActionSheet, + AppInviteDisplay, + Field, + FieldLabel, + GenericHeader, + PrimaryButton, + SizableText, + TextInput, + TextInputWithButton, + View, + YStack, +} from '@tloncorp/ui'; +import { useCallback, useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Keyboard } from 'react-native'; + +import type { OnboardingStackParamList } from '../../types'; + +// const onSubmit = async (data: FormData) => { +// try { +// await addUserToWaitlist({ email: data.inviteLink }); +// trackOnboardingAction({ +// actionName: 'Waitlist Joined', +// }); +// navigation.popToTop(); +// } catch (err) { +// console.error('Error joining waitlist:', err); +// if (err instanceof Error) { +// setRemoteError(err.message); +// trackError(err); +// } +// } +// }; + +type Props = NativeStackScreenProps; + +type FormData = { + inviteLink: string; +}; + +export const InviteLinkScreen = ({ navigation }: Props) => { + const lureMeta = useLureMetadata(); + const { setLure } = useBranch(); + const [hasInvite, setHasInvite] = useState(Boolean(lureMeta)); + + const { + control, + handleSubmit, + formState: { errors, isValid }, + setValue, + watch, + trigger, + } = useForm(); + + // watch for changes to the input & check for valid invite links + const inviteLinkValue = watch('inviteLink'); + useEffect(() => { + async function handleInviteLinkChange() { + const extractedLink = extractNormalizedInviteLink( + inviteLinkValue, + BRANCH_DOMAIN + ); + if (extractedLink) { + // check for invite link + const inviteLinkMeta = await getMetadaFromInviteLink( + extractedLink, + BRANCH_KEY + ); + if (inviteLinkMeta) { + // set the lure + console.log(`got a lure!`, inviteLinkMeta); + setLure(inviteLinkMeta as DeepLinkData); + } else { + trigger('inviteLink'); + } + } + } + handleInviteLinkChange(); + }, [inviteLinkValue, setLure, trigger]); + + // if at any point we have invite metadata, notify & allow them to proceed + // to signup + useEffect(() => { + if (lureMeta) { + console.log(`we have an invite now!`); + setHasInvite(true); + } + }, [lureMeta]); + + // handle paste button click + const onHandlePasteClick = useCallback(async () => { + const clipboardContents = await Clipboard.getString(); + setValue('inviteLink', clipboardContents); + }, [setValue]); + + // https://sa96e.test-app.link/0v3.u6rbg.974h9.um97p.et55s.ohkg3 + + return ( + + navigation.goBack()} + /> + Keyboard.dismiss()} + // backgroundColor="orange" + flex={1} + > + {!hasInvite ? ( + <> + + If someone invited you to Tlon, you can skip the waitlist. Click + your invite link now or paste it below. + + ( + + + + )} + /> + + navigation.navigate('JoinWaitList', {})} + > + I don't have an invite + + + ) : ( + <> + + Invite found! + + + navigation.navigate('SignUpEmail')}> + Sign up + + + )} + + + ); +}; diff --git a/apps/tlon-mobile/src/types.ts b/apps/tlon-mobile/src/types.ts index c5e32bce9f..c822fc1849 100644 --- a/apps/tlon-mobile/src/types.ts +++ b/apps/tlon-mobile/src/types.ts @@ -98,6 +98,7 @@ export type OnboardingStackParamList = { SignUpEmail: undefined; EULA: undefined; SignUpPassword: { email: string }; + InviteLink: undefined; JoinWaitList: { email?: string }; RequestPhoneVerify: { user: User }; CheckVerify: { user: User }; diff --git a/packages/app/contexts/branch.tsx b/packages/app/contexts/branch.tsx index 05ac82ade5..9b49c63680 100644 --- a/packages/app/contexts/branch.tsx +++ b/packages/app/contexts/branch.tsx @@ -1,5 +1,5 @@ import { DeepLinkMetadata, createDevLogger } from '@tloncorp/shared/dist'; -import { extractLureMetadata } from '@tloncorp/shared/src/logic'; +import { DeepLinkData, extractLureMetadata } from '@tloncorp/shared/src/logic'; import { type ReactNode, createContext, @@ -30,6 +30,7 @@ type State = Lure & { }; type ContextValue = State & { + setLure: (metadata: DeepLinkData) => void; clearLure: () => void; clearDeepLink: () => void; }; @@ -165,6 +166,26 @@ export const BranchProvider = ({ children }: { children: ReactNode }) => { }; }, [isAuthenticated]); + const setLure = useCallback( + (metadata: DeepLinkData) => { + const nextLure: Lure = { + lure: { + ...metadata, + id: metadata.lure as string, + // if not already authenticated, we should run Lure's invite auto-join capability after signing in + shouldAutoJoin: !isAuthenticated, + }, + priorityToken: undefined, + }; + setState({ + ...nextLure, + deepLinkPath: undefined, + }); + saveLure(nextLure); + }, + [isAuthenticated] + ); + const clearLure = useCallback(() => { console.debug('[branch] Clearing lure state'); setState((curr) => ({ @@ -189,6 +210,7 @@ export const BranchProvider = ({ children }: { children: ReactNode }) => { deepLinkPath, lure, priorityToken, + setLure, clearLure, clearDeepLink, }} diff --git a/packages/shared/src/logic/branch.ts b/packages/shared/src/logic/branch.ts index 626a413a28..01496ba102 100644 --- a/packages/shared/src/logic/branch.ts +++ b/packages/shared/src/logic/branch.ts @@ -61,7 +61,7 @@ export interface DeepLinkMetadata { invitedGroupIconImageUrl?: string; invitedGroupiconImageColor?: string; } -interface DeepLinkData extends DeepLinkMetadata { +export interface DeepLinkData extends DeepLinkMetadata { $desktop_url: string; $canonical_url: string; wer?: string; diff --git a/packages/shared/src/logic/deeplinks.ts b/packages/shared/src/logic/deeplinks.ts index e5ddad05c4..34605feeba 100644 --- a/packages/shared/src/logic/deeplinks.ts +++ b/packages/shared/src/logic/deeplinks.ts @@ -1,6 +1,6 @@ import { ContentReference } from '../api'; import { citeToPath } from '../urbit'; -import { getBranchLinkMeta, isLureMeta } from './branch'; +import { DeepLinkMetadata, getBranchLinkMeta, isLureMeta } from './branch'; export async function getReferenceFromDeeplink( url: string, @@ -23,3 +23,42 @@ export async function getReferenceFromDeeplink( return null; } + +export async function getMetadaFromInviteLink( + url: string, + branchKey: string +): Promise { + const linkMeta = await getBranchLinkMeta(url, branchKey); + if (linkMeta && typeof linkMeta === 'object') { + if (isLureMeta(linkMeta)) { + return linkMeta; + } + } + return null; +} + +export function createInviteLinkRegex(branchDomain: string) { + return new RegExp( + `^(https?://)?(${branchDomain}/|tlon\\.network/lure/)0v[^/]+$` + ); +} + +export function extractNormalizedInviteLink( + url: string, + branchDomain: string +): string | null { + const INVITE_LINK_REGEX = new RegExp( + `^(https?://)?(${branchDomain}/|tlon\\.network/lure/)0v[^/]+$` + ); + + const match = url.match(INVITE_LINK_REGEX); + if (match) { + const parts = match[0].split('/'); + const token = parts[parts.length - 1]; + if (token) { + return `https://${branchDomain}/${token}`; + } + } + + return null; +} diff --git a/packages/ui/src/components/Form/inputs.tsx b/packages/ui/src/components/Form/inputs.tsx index a7537d1071..b372b2b315 100644 --- a/packages/ui/src/components/Form/inputs.tsx +++ b/packages/ui/src/components/Form/inputs.tsx @@ -1,5 +1,6 @@ import React, { ComponentProps, ReactElement } from 'react'; import { TextInput as BaseTextInput } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; import { ScrollView, View, XStack, YStack, styled } from 'tamagui'; import { Button } from '../Button'; @@ -10,7 +11,6 @@ import { Text } from '../TextV2'; import { FieldContext } from './Form'; // Text input - export const TextInput = React.memo( styled( BaseTextInput, @@ -45,6 +45,36 @@ export const TextInput = React.memo( ) ); +interface TextInputWithButtonProps extends ComponentProps { + buttonText: string; + onButtonPress: () => void; +} + +export const TextInputWithButton: React.FC = + React.memo(function TextInputWithButtonRaw({ + buttonText, + onButtonPress, + ...textInputProps + }) { + return ( + + + + + ); + }); + // Toggle group export const ToggleGroupInput = ({ From ed83d2cf4c5402db373937163e4c2bee6c90855f Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 00:40:16 -0700 Subject: [PATCH 028/104] cleanup --- .../screens/Onboarding/InviteLinkScreen.tsx | 24 +------------------ packages/ui/src/components/Form/inputs.tsx | 2 +- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index e3a185df8c..70ee72b588 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -6,22 +6,17 @@ import { EMAIL_REGEX, } from '@tloncorp/app/constants'; import { useBranch, useLureMetadata } from '@tloncorp/app/contexts/branch'; -import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { DeepLinkData, - createInviteLinkRegex, extractNormalizedInviteLink, getMetadaFromInviteLink, } from '@tloncorp/shared/dist'; import { - ActionSheet, AppInviteDisplay, Field, - FieldLabel, GenericHeader, PrimaryButton, SizableText, - TextInput, TextInputWithButton, View, YStack, @@ -32,22 +27,6 @@ import { Keyboard } from 'react-native'; import type { OnboardingStackParamList } from '../../types'; -// const onSubmit = async (data: FormData) => { -// try { -// await addUserToWaitlist({ email: data.inviteLink }); -// trackOnboardingAction({ -// actionName: 'Waitlist Joined', -// }); -// navigation.popToTop(); -// } catch (err) { -// console.error('Error joining waitlist:', err); -// if (err instanceof Error) { -// setRemoteError(err.message); -// trackError(err); -// } -// } -// }; - type Props = NativeStackScreenProps; type FormData = { @@ -61,8 +40,7 @@ export const InviteLinkScreen = ({ navigation }: Props) => { const { control, - handleSubmit, - formState: { errors, isValid }, + formState: { errors }, setValue, watch, trigger, diff --git a/packages/ui/src/components/Form/inputs.tsx b/packages/ui/src/components/Form/inputs.tsx index b372b2b315..4c90cd287a 100644 --- a/packages/ui/src/components/Form/inputs.tsx +++ b/packages/ui/src/components/Form/inputs.tsx @@ -1,6 +1,5 @@ import React, { ComponentProps, ReactElement } from 'react'; import { TextInput as BaseTextInput } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; import { ScrollView, View, XStack, YStack, styled } from 'tamagui'; import { Button } from '../Button'; @@ -50,6 +49,7 @@ interface TextInputWithButtonProps extends ComponentProps { onButtonPress: () => void; } +// Needs polish, I know we just talked about Ochre conformance plz forgive export const TextInputWithButton: React.FC = React.memo(function TextInputWithButtonRaw({ buttonText, From 79f0dd7519f29b6751cdd5718d529d6de24dd2e6 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 00:43:24 -0700 Subject: [PATCH 029/104] remove copy handle --- apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index 70ee72b588..8e82292110 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -87,8 +87,6 @@ export const InviteLinkScreen = ({ navigation }: Props) => { setValue('inviteLink', clipboardContents); }, [setValue]); - // https://sa96e.test-app.link/0v3.u6rbg.974h9.um97p.et55s.ohkg3 - return ( Date: Tue, 1 Oct 2024 00:52:39 -0700 Subject: [PATCH 030/104] comments --- apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index 8e82292110..88e4d311b2 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -55,14 +55,11 @@ export const InviteLinkScreen = ({ navigation }: Props) => { BRANCH_DOMAIN ); if (extractedLink) { - // check for invite link const inviteLinkMeta = await getMetadaFromInviteLink( extractedLink, BRANCH_KEY ); if (inviteLinkMeta) { - // set the lure - console.log(`got a lure!`, inviteLinkMeta); setLure(inviteLinkMeta as DeepLinkData); } else { trigger('inviteLink'); @@ -76,7 +73,6 @@ export const InviteLinkScreen = ({ navigation }: Props) => { // to signup useEffect(() => { if (lureMeta) { - console.log(`we have an invite now!`); setHasInvite(true); } }, [lureMeta]); From baf1ec8c0bf9f40a70a892eea4262c3a25285e42 Mon Sep 17 00:00:00 2001 From: James Acklin <748181+jamesacklin@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:01:23 -0400 Subject: [PATCH 031/104] Update packages/ui/src/components/InviteFriendsToTlonButton.tsx Co-authored-by: Patrick O'Sullivan --- packages/ui/src/components/InviteFriendsToTlonButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/InviteFriendsToTlonButton.tsx b/packages/ui/src/components/InviteFriendsToTlonButton.tsx index 9bea707e0b..2d3eb53132 100644 --- a/packages/ui/src/components/InviteFriendsToTlonButton.tsx +++ b/packages/ui/src/components/InviteFriendsToTlonButton.tsx @@ -68,8 +68,8 @@ export function InviteFriendsToTlonButton({ group }: { group?: db.Group }) { }, [group, branchDomain, branchKey, toggle, status, isGroupAdmin, describe]); if ( - group?.privacy === 'private' || - (group?.privacy === 'secret' && !isGroupAdmin) + (group?.privacy === 'private' || + group?.privacy === 'secret') && !isGroupAdmin ) { return Only administrators may invite people to this group.; } From cfa1b2416829c7713f48739a5605ac0b43d67a5d Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 08:18:42 -0700 Subject: [PATCH 032/104] validate against real regex, no required empty state error --- .../src/screens/Onboarding/InviteLinkScreen.tsx | 16 +++++++--------- packages/shared/src/logic/deeplinks.ts | 7 +++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index 88e4d311b2..d89d4e139f 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -1,13 +1,10 @@ import Clipboard from '@react-native-clipboard/clipboard'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { - BRANCH_DOMAIN, - BRANCH_KEY, - EMAIL_REGEX, -} from '@tloncorp/app/constants'; +import { BRANCH_DOMAIN, BRANCH_KEY } from '@tloncorp/app/constants'; import { useBranch, useLureMetadata } from '@tloncorp/app/contexts/branch'; import { DeepLinkData, + createInviteLinkRegex, extractNormalizedInviteLink, getMetadaFromInviteLink, } from '@tloncorp/shared/dist'; @@ -27,6 +24,8 @@ import { Keyboard } from 'react-native'; import type { OnboardingStackParamList } from '../../types'; +const INVITE_LINK_REGEX = createInviteLinkRegex(BRANCH_DOMAIN); + type Props = NativeStackScreenProps; type FormData = { @@ -61,10 +60,10 @@ export const InviteLinkScreen = ({ navigation }: Props) => { ); if (inviteLinkMeta) { setLure(inviteLinkMeta as DeepLinkData); - } else { - trigger('inviteLink'); + return; } } + trigger('inviteLink'); } handleInviteLinkChange(); }, [inviteLinkValue, setLure, trigger]); @@ -107,9 +106,8 @@ export const InviteLinkScreen = ({ navigation }: Props) => { control={control} name="inviteLink" rules={{ - required: 'Please provide a valid invite link.', pattern: { - value: EMAIL_REGEX, + value: INVITE_LINK_REGEX, message: 'Invite link not recognized.', }, }} diff --git a/packages/shared/src/logic/deeplinks.ts b/packages/shared/src/logic/deeplinks.ts index 34605feeba..d7349c958b 100644 --- a/packages/shared/src/logic/deeplinks.ts +++ b/packages/shared/src/logic/deeplinks.ts @@ -47,11 +47,10 @@ export function extractNormalizedInviteLink( url: string, branchDomain: string ): string | null { - const INVITE_LINK_REGEX = new RegExp( - `^(https?://)?(${branchDomain}/|tlon\\.network/lure/)0v[^/]+$` - ); - + if (!url) return null; + const INVITE_LINK_REGEX = createInviteLinkRegex(branchDomain); const match = url.match(INVITE_LINK_REGEX); + if (match) { const parts = match[0].split('/'); const token = parts[parts.length - 1]; From c8fe4a7e461a59fafd21494833255a77800867bc Mon Sep 17 00:00:00 2001 From: Dan Brewster Date: Tue, 1 Oct 2024 11:31:41 -0400 Subject: [PATCH 033/104] add groups sheet style tweaks --- .../src/fixtures/AddGroupSheet.fixture.tsx | 2 +- .../src/fixtures/FindGroups.fixture.tsx | 2 +- apps/tlon-mobile/src/types.ts | 3 + .../top/ContactHostedGroupsScreen.tsx | 59 ++++++++++ .../app/features/top/FindGroupsScreen.tsx | 9 +- packages/app/hooks/useGroupNavigation.ts | 8 ++ packages/app/navigation/RootStack.tsx | 5 + packages/app/navigation/types.ts | 4 +- packages/shared/src/api/groupsApi.ts | 2 +- packages/shared/src/db/queries.ts | 25 ++++ packages/shared/src/store/dbHooks.ts | 6 +- .../AddChats/ViewUserGroupsWidget.tsx | 96 +++++++-------- packages/ui/src/components/AddGroupSheet.tsx | 105 +++++++++-------- packages/ui/src/components/Avatar.tsx | 9 +- packages/ui/src/components/Button.tsx | 39 +++---- packages/ui/src/components/ContactBook.tsx | 43 ++++--- packages/ui/src/components/ContactRow.tsx | 2 +- .../ui/src/components/CreateGroupView.tsx | 75 +++++------- .../src/components/Emoji/EmojiPickerSheet.tsx | 2 +- packages/ui/src/components/FindGroupsView.tsx | 110 +++++------------- packages/ui/src/components/Form/inputs.tsx | 89 +++++++++----- packages/ui/src/components/SearchBar.tsx | 60 +++++----- packages/ui/src/components/SectionList.tsx | 9 +- packages/ui/src/index.tsx | 1 + 24 files changed, 412 insertions(+), 353 deletions(-) create mode 100644 packages/app/features/top/ContactHostedGroupsScreen.tsx diff --git a/apps/tlon-mobile/src/fixtures/AddGroupSheet.fixture.tsx b/apps/tlon-mobile/src/fixtures/AddGroupSheet.fixture.tsx index 3c2d649e4f..acc727ee65 100644 --- a/apps/tlon-mobile/src/fixtures/AddGroupSheet.fixture.tsx +++ b/apps/tlon-mobile/src/fixtures/AddGroupSheet.fixture.tsx @@ -6,7 +6,7 @@ import { initialContacts } from './fakeData'; export default { basic: ( - + {}} diff --git a/apps/tlon-mobile/src/fixtures/FindGroups.fixture.tsx b/apps/tlon-mobile/src/fixtures/FindGroups.fixture.tsx index d7fc516998..e6ff61f75c 100644 --- a/apps/tlon-mobile/src/fixtures/FindGroups.fixture.tsx +++ b/apps/tlon-mobile/src/fixtures/FindGroups.fixture.tsx @@ -7,7 +7,7 @@ export default { basic: ( - {}} onGroupAction={() => {}} /> + {}} goToContactHostedGroups={() => {}} /> ), diff --git a/apps/tlon-mobile/src/types.ts b/apps/tlon-mobile/src/types.ts index c5e32bce9f..3aefbc85b0 100644 --- a/apps/tlon-mobile/src/types.ts +++ b/apps/tlon-mobile/src/types.ts @@ -27,6 +27,9 @@ export type RootStackParamList = { selectedPostId?: string | null; }; FindGroups: undefined; + ContactHostedGroups: { + contactId: string; + }; CreateGroup: undefined; GroupChannels: { group: db.Group; diff --git a/packages/app/features/top/ContactHostedGroupsScreen.tsx b/packages/app/features/top/ContactHostedGroupsScreen.tsx new file mode 100644 index 0000000000..4b44c710c8 --- /dev/null +++ b/packages/app/features/top/ContactHostedGroupsScreen.tsx @@ -0,0 +1,59 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import * as db from '@tloncorp/shared/dist/db'; +import { + GenericHeader, + GroupPreviewAction, + GroupPreviewSheet, + ViewUserGroupsWidget, + useContactName, +} from '@tloncorp/ui'; +import { useCallback, useState } from 'react'; +import { View } from 'tamagui'; + +import { useGroupActions } from '../../hooks/useGroupActions'; +import { RootStackParamList } from '../../navigation/types'; + +type Props = NativeStackScreenProps; + +export function ContactHostedGroupsScreen({ route, navigation }: Props) { + const contactId = route.params.contactId; + const contactName = useContactName(contactId); + const { performGroupAction } = useGroupActions(); + const [groupForPreview, setGroupForPreview] = useState(null); + const goBack = useCallback(() => navigation.goBack(), [navigation]); + + const onSelectGroup = useCallback( + (group: db.Group) => { + setGroupForPreview(group); + }, + [setGroupForPreview] + ); + + const handleGroupAction = useCallback( + (action: GroupPreviewAction, group: db.Group) => { + performGroupAction(action, group); + setGroupForPreview(null); + }, + [performGroupAction] + ); + + return ( + + + + { + if (!open) { + setGroupForPreview(null); + } + }} + onActionComplete={handleGroupAction} + group={groupForPreview ?? undefined} + /> + + ); +} diff --git a/packages/app/features/top/FindGroupsScreen.tsx b/packages/app/features/top/FindGroupsScreen.tsx index 3219d74b7d..2290e44ff0 100644 --- a/packages/app/features/top/FindGroupsScreen.tsx +++ b/packages/app/features/top/FindGroupsScreen.tsx @@ -1,18 +1,17 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { FindGroupsView } from '@tloncorp/ui'; -import { useGroupActions } from '../../hooks/useGroupActions'; +import { useGroupNavigation } from '../../hooks/useGroupNavigation'; import type { RootStackParamList } from '../../navigation/types'; type Props = NativeStackScreenProps; export function FindGroupsScreen({ navigation }: Props) { - const { performGroupAction } = useGroupActions(); - + const { goToContactHostedGroups } = useGroupNavigation(); return ( navigation.goBack()} - onGroupAction={performGroupAction} + goBack={() => navigation.goBack()} + goToContactHostedGroups={goToContactHostedGroups} /> ); } diff --git a/packages/app/hooks/useGroupNavigation.ts b/packages/app/hooks/useGroupNavigation.ts index f44cef9cc0..3068f1bd3d 100644 --- a/packages/app/hooks/useGroupNavigation.ts +++ b/packages/app/hooks/useGroupNavigation.ts @@ -19,8 +19,16 @@ export const useGroupNavigation = () => { navigation.navigate('ChatList'); }, [navigation]); + const goToContactHostedGroups = useCallback( + ({ contactId }: { contactId: string }) => { + navigation.navigate('ContactHostedGroups', { contactId }); + }, + [navigation] + ); + return { goToChannel, goToHome, + goToContactHostedGroups, }; }; diff --git a/packages/app/navigation/RootStack.tsx b/packages/app/navigation/RootStack.tsx index df6896bfba..4f02a1d84f 100644 --- a/packages/app/navigation/RootStack.tsx +++ b/packages/app/navigation/RootStack.tsx @@ -16,6 +16,7 @@ import { ActivityScreen } from '../features/top/ActivityScreen'; import ChannelScreen from '../features/top/ChannelScreen'; import ChannelSearchScreen from '../features/top/ChannelSearchScreen'; import ChatListScreen from '../features/top/ChatListScreen'; +import { ContactHostedGroupsScreen } from '../features/top/ContactHostedGroupsScreen'; import { CreateGroupScreen } from '../features/top/CreateGroupScreen'; import { FindGroupsScreen } from '../features/top/FindGroupsScreen'; import { GroupChannelsScreen } from '../features/top/GroupChannelsScreen'; @@ -70,6 +71,10 @@ export function RootStack() { {/* individual screens */} + diff --git a/packages/app/navigation/types.ts b/packages/app/navigation/types.ts index 5bdd468c71..c37b3d9427 100644 --- a/packages/app/navigation/types.ts +++ b/packages/app/navigation/types.ts @@ -1,6 +1,5 @@ import type { NavigatorScreenParams } from '@react-navigation/native'; import type * as db from '@tloncorp/shared/dist/db'; -import type { WebViewProps } from 'react-native-webview'; export type SignUpExtras = { nickname?: string; @@ -28,6 +27,9 @@ export type RootStackParamList = { selectedPostId?: string | null; }; FindGroups: undefined; + ContactHostedGroups: { + contactId: string; + }; CreateGroup: undefined; GroupChannels: { group: db.Group; diff --git a/packages/shared/src/api/groupsApi.ts b/packages/shared/src/api/groupsApi.ts index f2cbb4456f..c2f53cec20 100644 --- a/packages/shared/src/api/groupsApi.ts +++ b/packages/shared/src/api/groupsApi.ts @@ -330,7 +330,7 @@ export const findGroupsHostedBy = async (userId: string) => { logger.log('findGroupsHostedBy result', result); - return result; + return toClientGroupsFromPreview(result); }; export const createGroup = async ({ diff --git a/packages/shared/src/db/queries.ts b/packages/shared/src/db/queries.ts index f630696f32..ea51c8117b 100644 --- a/packages/shared/src/db/queries.ts +++ b/packages/shared/src/db/queries.ts @@ -531,6 +531,31 @@ export const insertGroups = createWriteQuery( ] ); +export const insertGroupPreviews = createWriteQuery( + 'insert group previews', + ({ groups }: { groups: Group[] }, ctx: QueryCtx) => { + return withTransactionCtx(ctx, async (txCtx) => { + if (groups.length === 0) return; + for (const group of groups) { + await txCtx.db + .insert($groups) + .values(group) + .onConflictDoUpdate({ + target: $groups.id, + set: conflictUpdateSet( + $groups.iconImage, + $groups.coverImage, + $groups.title, + $groups.description, + $groups.privacy + ), + }); + } + }); + }, + ['groups'] +); + export const updateGroup = createWriteQuery( 'updateGroup', async (group: Partial & { id: string }, ctx: QueryCtx) => { diff --git a/packages/shared/src/store/dbHooks.ts b/packages/shared/src/store/dbHooks.ts index e58e2ff0c5..a5e1750d77 100644 --- a/packages/shared/src/store/dbHooks.ts +++ b/packages/shared/src/store/dbHooks.ts @@ -389,12 +389,10 @@ export const useGroupsHostedBy = (userId: string) => { queryFn: async () => { // query backend for all groups the ship hosts const groups = await api.findGroupsHostedBy(userId); - - const clientGroups = api.toClientGroupsFromPreview(groups); // insert any we didn't already have - await db.insertGroups({ groups: clientGroups, overWrite: false }); + await db.insertGroupPreviews({ groups }); - const groupIds = clientGroups.map((g) => g.id); + const groupIds = groups.map((g) => g.id); const groupPreviews = await db.getGroupPreviews(groupIds); return groupPreviews; }, diff --git a/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx b/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx index 10a3531ef6..3ca08de14a 100644 --- a/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx +++ b/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx @@ -1,17 +1,13 @@ import * as db from '@tloncorp/shared/dist/db'; import * as store from '@tloncorp/shared/dist/store'; import { useCallback, useMemo, useRef } from 'react'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - ScrollView, -} from 'react-native'; -import { SizableText, View, XStack, YStack } from 'tamagui'; +import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { ScrollView, View, YStack } from 'tamagui'; import { Badge } from '../Badge'; -import ContactName from '../ContactName'; -import { ListItem, ListItemProps } from '../ListItem'; -import { LoadingSpinner } from '../LoadingSpinner'; +import { GroupListItem, ListItem, ListItemProps } from '../ListItem'; +import { Text } from '../TextV2'; export function ViewUserGroupsWidget({ userId, @@ -42,61 +38,55 @@ export function ViewUserGroupsWidget({ [onScrollChange] ); + const insets = useSafeAreaInsets(); + return ( - {isLoading ? ( - - - - Loading groups hosted by{' '} - - - - ) : isError ? ( - - - Error loading groups hosted by{' '} - - - - ) : ( + { <> - - - Groups hosted by{' '} - : - - - {data && data.length > 0 ? ( + {isLoading ? ( + + ) : isError ? ( + + ) : data && data.length > 0 ? ( data?.map((group) => ( onSelectGroup(group)} + onPress={onSelectGroup} /> )) ) : ( - - hosts no groups - + )} - )} + } ); } +function PlaceholderMessage({ text }: { text: string }) { + return ( + + + {text} + + + ); +} + function GroupPreviewListItem({ model, onPress }: ListItemProps) { const badgeText = useMemo(() => { if (model.currentUserIsMember) { @@ -106,16 +96,16 @@ function GroupPreviewListItem({ model, onPress }: ListItemProps) { }, [model.currentUserIsMember, model.privacy]); return ( - onPress?.(model)}> - - - {model.title} - - {badgeText && ( - - - - )} - + + + + ) : undefined + } + /> ); } diff --git a/packages/ui/src/components/AddGroupSheet.tsx b/packages/ui/src/components/AddGroupSheet.tsx index 4bc8beed37..3916528273 100644 --- a/packages/ui/src/components/AddGroupSheet.tsx +++ b/packages/ui/src/components/AddGroupSheet.tsx @@ -1,11 +1,11 @@ -import { QueryClientProvider, queryClient } from '@tloncorp/shared/dist/api'; import { useCallback, useEffect, useState } from 'react'; -import { YStack, isWeb } from 'tamagui'; +import { View, YStack, isWeb } from 'tamagui'; -import { AppDataContextProvider, useContacts } from '../contexts'; import { triggerHaptic } from '../utils'; import { ActionSheet } from './ActionSheet'; import { ContactBook } from './ContactBook'; +import { IconType } from './Icon'; +import { ListItem } from './ListItem'; export function AddGroupSheet({ open, @@ -22,7 +22,6 @@ export function AddGroupSheet({ }) { const [screenScrolling, setScreenScrolling] = useState(false); const [screenKey, setScreenKey] = useState(0); - const contacts = useContacts(); const dismiss = useCallback(() => { onOpenChange(false); @@ -55,52 +54,58 @@ export function AddGroupSheet({ snapPoints={[90]} snapPointsMode="percent" > - - - - - - - New Message - - - navigateToCreateGroup(), - }} - /> - navigateToFindGroups(), - }} - /> - - } - /> - - - - - + + + + + + + } + /> + ); } + +function QuickAction({ + onPress, + icon, + title, + subtitle, +}: { + onPress: () => void; + icon: IconType; + title: string; + subtitle?: string; +}) { + return ( + + + + {title} + {subtitle} + + + ); +} diff --git a/packages/ui/src/components/Avatar.tsx b/packages/ui/src/components/Avatar.tsx index 1da8783009..e5a43bbd71 100644 --- a/packages/ui/src/components/Avatar.tsx +++ b/packages/ui/src/components/Avatar.tsx @@ -214,20 +214,25 @@ export const ImageAvatar = function ImageAvatarComponent({ } & AvatarProps) { const calmSettings = useCalm(); const [loadFailed, setLoadFailed] = useState(false); - + const [isLoading, setIsLoading] = useState(true); const handleLoadError = useCallback(() => { setLoadFailed(true); }, []); + const handleLoadEnd = useCallback(() => setIsLoading(false), []); return imageUrl && (props.ignoreCalm || !calmSettings.disableAvatars) && !loadFailed ? ( - + { + return isHero + ? { + color: props.disabled ? '$secondaryText' : '$background', + width: '100%', + textAlign: 'center', + fontWeight: '400', + } + : {}; }, heroDestructive: { true: { @@ -172,12 +174,7 @@ export const ButtonText = styled(Text, { fontWeight: '500', }, }, - - // disabled: { - // true: { - // color: '$tertiaryText', - // }, - // }, + disabled: {} as Record<'true' | 'false', ViewStyle>, } as const, }); diff --git a/packages/ui/src/components/ContactBook.tsx b/packages/ui/src/components/ContactBook.tsx index 129ad3ddc1..e71978d1c5 100644 --- a/packages/ui/src/components/ContactBook.tsx +++ b/packages/ui/src/components/ContactBook.tsx @@ -1,19 +1,22 @@ import * as db from '@tloncorp/shared/dist/db'; import { useCallback, useMemo, useRef, useState } from 'react'; import { + Insets, Keyboard, NativeScrollEvent, NativeSyntheticEvent, SectionListRenderItemInfo, + StyleProp, + ViewStyle, } from 'react-native'; -import { View, XStack } from 'tamagui'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { View, XStack, useStyle } from 'tamagui'; import { useContactIndex, useContacts } from '../contexts'; import { useAlphabeticallySegmentedContacts, useSortedContacts, } from '../hooks/contactSorters'; -import { TextButton } from './Buttons'; import { ContactRow } from './ContactRow'; import { SearchBar } from './SearchBar'; import { BlockSectionList } from './SectionList'; @@ -25,8 +28,6 @@ export function ContactBook({ multiSelect = false, onSelectedChange, onScrollChange, - showCancelButton = false, - onPressCancel, explanationComponent, quickActions, }: { @@ -36,8 +37,6 @@ export function ContactBook({ multiSelect?: boolean; onSelectedChange?: (selected: string[]) => void; onScrollChange?: (scrolling: boolean) => void; - showCancelButton?: boolean; - onPressCancel?: () => void; explanationComponent?: React.ReactElement; quickActions?: React.ReactElement; }) { @@ -48,8 +47,6 @@ export function ContactBook({ contactsIndex ?? {} ); - const Explanation = () => explanationComponent ?? null; - const [query, setQuery] = useState(''); const queryContacts = useSortedContacts({ contacts: contacts ?? [], @@ -97,6 +94,7 @@ export function ContactBook({ selectable={multiSelect} selected={isSelected} onPress={handleSelect} + pressStyle={{ backgroundColor: '$shadow' }} /> ); }, @@ -121,7 +119,18 @@ export function ContactBook({ [onScrollChange] ); - const QuickActions = () => quickActions ?? null; + const insets = useSafeAreaInsets(); + + const contentContainerStyle = useStyle({ + paddingBottom: insets.bottom, + paddingTop: '$s', + paddingHorizontal: '$xl', + }) as StyleProp; + + const scrollIndicatorInsets = useStyle({ + bottom: insets.bottom, + top: '$xl', + }) as Insets; return ( @@ -129,33 +138,33 @@ export function ContactBook({ - {showCancelButton && ( - onPressCancel?.()}>Cancel - )} )} {!showSearchResults && explanationComponent ? ( - + explanationComponent ) : ( )} diff --git a/packages/ui/src/components/ContactRow.tsx b/packages/ui/src/components/ContactRow.tsx index cdbd25e920..7b128982da 100644 --- a/packages/ui/src/components/ContactRow.tsx +++ b/packages/ui/src/components/ContactRow.tsx @@ -32,7 +32,7 @@ function ContactRowItemRaw({ ); return ( - + diff --git a/packages/ui/src/components/CreateGroupView.tsx b/packages/ui/src/components/CreateGroupView.tsx index fd18b1a115..d1c2f77b42 100644 --- a/packages/ui/src/components/CreateGroupView.tsx +++ b/packages/ui/src/components/CreateGroupView.tsx @@ -1,13 +1,8 @@ import * as db from '@tloncorp/shared/dist/db'; import { useCallback, useState } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Text, View, YStack } from 'tamagui'; +import { View, YStack } from 'tamagui'; -import { - AppDataContextProvider, - useContacts, - useCurrentUserId, -} from '../contexts'; import { CreateGroupWidget } from './AddChats'; import { Button } from './Button'; import { TextButton } from './Buttons'; @@ -26,8 +21,6 @@ export function CreateGroupView({ const { bottom } = useSafeAreaInsets(); const [screen, setScreen] = useState('InviteUsers'); const [invitees, setInvitees] = useState([]); - const contacts = useContacts(); - const currentUserId = useCurrentUserId(); const handleCreatedGroup = useCallback( ({ channel }: { channel: db.Channel }) => { @@ -57,44 +50,36 @@ export function CreateGroupView({ ) : null } /> - - {screen === 'InviteUsers' ? ( - - - - Select members - - - - - - ) : ( - + - )} - + + + + + ) : ( + + )} ); } diff --git a/packages/ui/src/components/Emoji/EmojiPickerSheet.tsx b/packages/ui/src/components/Emoji/EmojiPickerSheet.tsx index a1ed6769be..40da5700ff 100644 --- a/packages/ui/src/components/Emoji/EmojiPickerSheet.tsx +++ b/packages/ui/src/components/Emoji/EmojiPickerSheet.tsx @@ -130,7 +130,7 @@ export function EmojiPickerSheet( marginHorizontal="$m" onChangeQuery={handleQueryChange} onFocus={handleInputFocus} - areaProps={{ spellCheck: false, autoComplete: 'off' }} + inputProps={{ spellCheck: false, autoComplete: 'off' }} /> Keyboard.dismiss()}> diff --git a/packages/ui/src/components/FindGroupsView.tsx b/packages/ui/src/components/FindGroupsView.tsx index 6bcddc245c..633ce4031b 100644 --- a/packages/ui/src/components/FindGroupsView.tsx +++ b/packages/ui/src/components/FindGroupsView.tsx @@ -1,98 +1,40 @@ -import * as db from '@tloncorp/shared/dist/db'; -import { useCallback, useState } from 'react'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Text, View, XStack, YStack } from 'tamagui'; +import { useCallback } from 'react'; +import { Text, View, YStack } from 'tamagui'; -import { - AppDataContextProvider, - useContacts, - useCurrentUserId, -} from '../contexts'; -import { ViewUserGroupsWidget } from './AddChats'; import { ContactBook } from './ContactBook'; -import { GroupPreviewAction, GroupPreviewSheet } from './GroupPreviewSheet'; -import { Icon } from './Icon'; - -const GroupJoinExplanation = () => ( - - On Tlon, people host groups. - Look for groups hosted by people above. - -); - -type screens = 'FindGroups' | 'ViewGroupsByContact'; +import { GenericHeader } from './GenericHeader'; export function FindGroupsView({ - onCancel, - onGroupAction, + goBack, + goToContactHostedGroups, }: { - onCancel: () => void; - onGroupAction: (action: GroupPreviewAction, group: db.Group) => void; + goBack: () => void; + goToContactHostedGroups: (params: { contactId: string }) => void; }) { - const { top } = useSafeAreaInsets(); - const contacts = useContacts(); - const currentUserId = useCurrentUserId(); - const [screen, setScreen] = useState('FindGroups'); - const [viewGroupsForContact, setViewGroupsForContact] = useState< - string | null - >(null); - const [groupForPreview, setGroupForPreview] = useState(null); - - const onSelectGroup = useCallback( - (group: db.Group) => { - setGroupForPreview(group); + const handleContactSelected = useCallback( + (contactId: string) => { + console.log('go to contact hosted groups', contactId); + goToContactHostedGroups({ contactId: contactId }); }, - [setGroupForPreview] - ); - - const handleGroupAction = useCallback( - (action: GroupPreviewAction, group: db.Group) => { - onGroupAction(action, group); - setGroupForPreview(null); - }, - [onGroupAction] + [goToContactHostedGroups] ); return ( - - {screen === 'FindGroups' ? ( - - { - setViewGroupsForContact(contact); - setScreen('ViewGroupsByContact'); - }} - showCancelButton - onPressCancel={onCancel} - explanationComponent={} - /> - - ) : ( - <> - - setScreen('FindGroups')} /> - - - - )} - { - if (!open) { - setGroupForPreview(null); - } - }} - onActionComplete={handleGroupAction} - group={groupForPreview ?? undefined} + + + } /> ); } + +const GroupJoinExplanation = () => ( + + On Tlon, people host groups. + Look for groups hosted by people above. + +); diff --git a/packages/ui/src/components/Form/inputs.tsx b/packages/ui/src/components/Form/inputs.tsx index a7537d1071..737269fb99 100644 --- a/packages/ui/src/components/Form/inputs.tsx +++ b/packages/ui/src/components/Form/inputs.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, ReactElement } from 'react'; -import { TextInput as BaseTextInput } from 'react-native'; +import { TextInput as RNTextInput } from 'react-native'; import { ScrollView, View, XStack, YStack, styled } from 'tamagui'; import { Button } from '../Button'; @@ -9,40 +9,67 @@ import { useBoundHandler } from '../ListItem/listItemUtils'; import { Text } from '../TextV2'; import { FieldContext } from './Form'; +const StyledTextInput = styled( + RNTextInput, + {}, + { + isInput: true, + accept: { + placeholderTextColor: 'color', + selectionColor: 'color', + } as const, + } +); + // Text input -export const TextInput = React.memo( - styled( - BaseTextInput, - { - context: FieldContext, - color: '$primaryText', - borderRadius: '$l', - borderWidth: 1, - borderColor: '$border', - placeholderTextColor: '$tertiaryText', - fontSize: '$l', - padding: '$xl', - fontFamily: '$body', - textAlignVertical: 'top', - variants: { - accent: { - negative: { - backgroundColor: '$negativeBackground', - color: '$negativeActionText', - borderColor: '$negativeBorder', - }, - }, +export const BaseTextInput = styled(StyledTextInput, { + context: FieldContext, + color: '$primaryText', + borderRadius: '$l', + borderWidth: 1, + borderColor: '$border', + placeholderTextColor: '$tertiaryText', + fontSize: '$l', + padding: '$xl', + fontFamily: '$body', + textAlignVertical: 'top', + variants: { + accent: { + negative: { + backgroundColor: '$negativeBackground', + color: '$negativeActionText', + borderColor: '$negativeBorder', }, }, - { - isInput: true, - accept: { - placeholderTextColor: 'color', - selectionColor: 'color', - } as const, - } - ) + }, +}); + +export const TextInput = React.memo(BaseTextInput); + +export const TextInputWithIcon = React.memo( + BaseTextInput.styleable<{ icon: IconType }>(({ icon, ...props }, ref) => { + return ( + + + + + ); + }) ); // Toggle group diff --git a/packages/ui/src/components/SearchBar.tsx b/packages/ui/src/components/SearchBar.tsx index 7bf9fc74ff..4e9b9f7752 100644 --- a/packages/ui/src/components/SearchBar.tsx +++ b/packages/ui/src/components/SearchBar.tsx @@ -1,8 +1,8 @@ import { debounce } from 'lodash'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; -import { Input as TInput, View } from 'tamagui'; -import { Circle } from 'tamagui'; +import { Circle, View, XStack } from 'tamagui'; +import { TextInput, TextInputWithIcon } from './Form'; import { Icon } from './Icon'; import { Input } from './Input'; @@ -10,13 +10,13 @@ export function SearchBar({ placeholder, onChangeQuery, debounceTime = 300, - areaProps, + inputProps, ...rest }: { placeholder?: string; onChangeQuery: (query: string) => void; debounceTime?: number; - areaProps?: ComponentProps; + inputProps?: ComponentProps; } & ComponentProps) { const [value, setValue] = useState(''); const debouncedOnChangeQuery = useMemo( @@ -51,34 +51,34 @@ export function SearchBar({ flexDirection="column" justifyContent="center" alignItems="center" + {...rest} > - - - - - - - - onTextChange('')} - disabled={value === ''} - opacity={value === '' ? 0 : undefined} + + onTextChange('')} + disabled={value === ''} + opacity={value === '' ? 0 : undefined} + > + - - - - - + + + ); } diff --git a/packages/ui/src/components/SectionList.tsx b/packages/ui/src/components/SectionList.tsx index 4e3100c493..44c1aa330e 100644 --- a/packages/ui/src/components/SectionList.tsx +++ b/packages/ui/src/components/SectionList.tsx @@ -1,24 +1,23 @@ import { getRadius } from '@tamagui/get-token'; import { useCallback, useMemo } from 'react'; import { - Platform, SectionList, SectionListData, SectionListProps, SectionListRenderItemInfo, } from 'react-native'; import { View, styled, withStaticProperties } from 'tamagui'; -import { SizableText } from 'tamagui'; + +import { Text } from './TextV2'; const SectionListHeaderFrame = styled(View, { paddingHorizontal: '$l', paddingVertical: '$m', }); -const SectionListHeaderText = styled(SizableText, { - size: '$s', +const SectionListHeaderText = styled(Text, { + size: '$label/s', color: '$secondaryText', - lineHeight: Platform.OS === 'ios' ? 0 : undefined, }); export const SectionListHeader = withStaticProperties(SectionListHeaderFrame, { diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index fdf658531d..a2f4c1c794 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -23,6 +23,7 @@ export * from './components/ChatMessage/ChatMessageActions/Component'; export * from './components/ChatOptionsSheet'; export * from './components/ContactBook'; export * from './components/ContactList'; +export { useContactName } from './components/ContactNameV2'; export { default as ContactName } from './components/ContactName'; export * from './components/ContactRow'; export * from './components/ContentReference'; From 2816b219e4ea1580863ab3e88420ea73b1427627 Mon Sep 17 00:00:00 2001 From: David Lee Date: Tue, 1 Oct 2024 09:31:49 -0700 Subject: [PATCH 034/104] Fix lint --- packages/ui/src/components/draftInputs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/draftInputs/index.ts b/packages/ui/src/components/draftInputs/index.ts index fe2f87a2d6..1553fcfc4b 100644 --- a/packages/ui/src/components/draftInputs/index.ts +++ b/packages/ui/src/components/draftInputs/index.ts @@ -1,4 +1,4 @@ +export type { DraftInputContext } from './shared'; export { ChatInput } from './ChatInput'; -export { DraftInputContext } from './shared'; export { GalleryInput } from './GalleryInput'; export { NotebookInput } from './NotebookInput'; From 33f95aa3e07c9f0096db74433e23dad295168215 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 09:34:11 -0700 Subject: [PATCH 035/104] fix text alignment for text button field on Android --- .../src/screens/Onboarding/InviteLinkScreen.tsx | 1 - packages/ui/src/components/Form/inputs.tsx | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index d89d4e139f..a7c6c19e3a 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -93,7 +93,6 @@ export const InviteLinkScreen = ({ navigation }: Props) => { padding="$2xl" gap="$2xl" onPress={() => Keyboard.dismiss()} - // backgroundColor="orange" flex={1} > {!hasInvite ? ( diff --git a/packages/ui/src/components/Form/inputs.tsx b/packages/ui/src/components/Form/inputs.tsx index 4c90cd287a..ab01e365d9 100644 --- a/packages/ui/src/components/Form/inputs.tsx +++ b/packages/ui/src/components/Form/inputs.tsx @@ -63,7 +63,13 @@ export const TextInputWithButton: React.FC = borderRadius="$l" padding="$l" > - + , [])); + * // make sure to use `useMemo` to avoid re-registering the item on every render! + * ``` + */ +export function ChannelHeaderItemsProvider({ + children, +}: { + children: JSX.Element; +}) { + const [items, setItems] = useState([]); + const registerItem = useCallback( + ({ item }: { item: JSX.Element }) => { + setItems((prev) => [...prev, item]); + return { + remove: () => { + setItems((prev) => prev.filter((i) => i !== item)); + }, + }; + }, + [setItems] + ); + return ( + + {children} + + ); +} + +export function useRegisterChannelHeaderItem(item: JSX.Element | null) { + const registerItem = useContext(ChannelHeaderItemsContext)?.registerItem; + + // NB: Since we're mutating the ChannelHeaderItemsContext in this effect, we + // need to be careful about the dependencies to avoid recursively updating on + // every change to the context. We avoid this by (1) defining `registerItem` + // using a `useCallback`, and (2) only listing `registerItem` as a dependency + // of the effect (and importantly not `items` nor the full context value). + useEffect(() => { + if (registerItem == null || item == null) { + return; + } + const { remove } = registerItem({ item }); + return remove; + }, [registerItem, item]); +} + export function ChannelHeader({ title, mode = 'default', @@ -15,8 +81,6 @@ export function ChannelHeader({ goBack, goToSearch, showSpinner, - showAddButton = false, - onPressAddButton, showSearchButton = true, showMenuButton = false, }: { @@ -27,8 +91,6 @@ export function ChannelHeader({ goBack?: () => void; goToSearch?: () => void; showSpinner?: boolean; - showAddButton?: boolean; - onPressAddButton?: () => void; showSearchButton?: boolean; showMenuButton?: boolean; post?: db.Post; @@ -39,6 +101,8 @@ export function ChannelHeader({ chatOptionsSheetRef.current?.open(channel.id, channel.type); }, [channel.id, channel.type]); + const contextItems = useContext(ChannelHeaderItemsContext)?.items ?? []; + if (mode === 'next') { return ; } @@ -60,15 +124,7 @@ export function ChannelHeader({ )} - {showAddButton && ( - - )} + {contextItems} {showMenuButton && ( + ), + [showBigInput] + ) + ); + useImperativeHandle(draftInputRef, () => ({ exitFullscreen: () => { setShowBigInput(false); diff --git a/packages/ui/src/components/draftInputs/NotebookInput.tsx b/packages/ui/src/components/draftInputs/NotebookInput.tsx index 80ada5f466..b6148a873c 100644 --- a/packages/ui/src/components/draftInputs/NotebookInput.tsx +++ b/packages/ui/src/components/draftInputs/NotebookInput.tsx @@ -1,10 +1,12 @@ -import { useEffect, useImperativeHandle, useState } from 'react'; +import { useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { SafeAreaView, useSafeAreaInsets, } from 'react-native-safe-area-context'; import { View } from 'tamagui'; +import { Button } from '../Button'; +import { useRegisterChannelHeaderItem } from '../Channel/ChannelHeader'; import { FloatingActionButton } from '../FloatingActionButton'; import { Icon } from '../Icon'; import { ParentAgnosticKeyboardAvoidingView } from '../ParentAgnosticKeyboardAvoidingView'; @@ -32,6 +34,22 @@ export function NotebookInput({ setShowBigInput(isEditingPost); }, [isEditingPost]); + useRegisterChannelHeaderItem( + useMemo( + () => + showBigInput ? null : ( + + ), + [showBigInput] + ) + ); + useImperativeHandle(draftInputRef, () => ({ exitFullscreen: () => { setShowBigInput(false); From 8887f41346eb1af5e594fba7ba336bd4a1bd748e Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 1 Oct 2024 10:37:40 -0700 Subject: [PATCH 037/104] copy --- .../src/screens/Onboarding/InviteLinkScreen.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx index a7c6c19e3a..26fdbb887e 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InviteLinkScreen.tsx @@ -97,10 +97,15 @@ export const InviteLinkScreen = ({ navigation }: Props) => { > {!hasInvite ? ( <> - - If someone invited you to Tlon, you can skip the waitlist. Click - your invite link now or paste it below. - + + + We're growing slowly. Invites let you skip the waitlist + because we know someone wants to talk to you here. + + + Click your invite link now or paste it below. + + Date: Tue, 1 Oct 2024 18:01:32 +0000 Subject: [PATCH 038/104] update glob: [skip actions] --- desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index 21c18fb17e..f0da80939d 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0vgsiup.8b5ag.nf309.99824.uuril.glob' 0vgsiup.8b5ag.nf309.99824.uuril] + glob-http+['https://bootstrap.urbit.org/glob-0v5.vab6j.2vk03.rko2j.tofis.nafte.glob' 0v5.vab6j.2vk03.rko2j.tofis.nafte] base+'groups' version+[6 4 0] website+'https://tlon.io' From 7a8d0baa7199a2ef18e059061e8ca9301d8011c8 Mon Sep 17 00:00:00 2001 From: Dan Brewster Date: Tue, 1 Oct 2024 11:16:07 -0400 Subject: [PATCH 039/104] condense header components --- .../src/fixtures/GroupList.fixture.tsx | 12 +- .../screens/Onboarding/CheckVerifyScreen.tsx | 8 +- .../src/screens/Onboarding/EULAScreen.tsx | 6 +- .../Onboarding/InventoryCheckScreen.tsx | 8 +- .../screens/Onboarding/JoinWaitListScreen.tsx | 6 +- .../Onboarding/RequestPhoneVerifyScreen.tsx | 18 +-- .../Onboarding/ResetPasswordScreen.tsx | 18 +-- .../screens/Onboarding/SetNicknameScreen.tsx | 12 +- .../screens/Onboarding/SetTelemetryScreen.tsx | 20 +-- .../screens/Onboarding/ShipLoginScreen.tsx | 18 +-- .../screens/Onboarding/SignUpEmailScreen.tsx | 23 ++- .../Onboarding/SignUpPasswordScreen.tsx | 21 ++- .../screens/Onboarding/TlonLoginScreen.tsx | 18 +-- .../features/groups/GroupPrivacyScreen.tsx | 15 +- .../app/features/settings/AppInfoScreen.tsx | 8 +- .../features/settings/BlockedUsersScreen.tsx | 9 +- .../features/settings/ManageAccountScreen.tsx | 14 +- .../PushNotificationSettingsScreen.tsx | 6 +- .../features/settings/UserBugReportScreen.tsx | 9 +- packages/app/features/top/ChatListScreen.tsx | 61 ++++---- .../src/components/Channel/ChannelHeader.tsx | 37 ++--- packages/ui/src/components/Channel/index.tsx | 16 +- .../components/ChannelMembersScreenView.tsx | 6 +- packages/ui/src/components/ChatList.tsx | 146 +++++++++++------- .../ui/src/components/CreateGroupView.tsx | 8 +- .../src/components/EditProfileScreenView.tsx | 30 +--- .../src/components/FeatureFlagScreenView.tsx | 4 +- packages/ui/src/components/GenericHeader.tsx | 92 ----------- .../components/GroupChannelsScreenView.tsx | 48 +++--- .../src/components/GroupMembersScreenView.tsx | 6 +- .../ManageChannels/EditChannelScreenView.tsx | 9 +- .../ManageChannelsScreenView.tsx | 4 +- .../src/components/MetaEditorScreenView.tsx | 36 +++-- .../ui/src/components/ProfileScreenView.tsx | 4 +- packages/ui/src/components/ScreenHeader.tsx | 121 +++++++++++---- .../src/components/UserProfileScreenView.tsx | 5 +- packages/ui/src/index.tsx | 2 +- 37 files changed, 411 insertions(+), 473 deletions(-) delete mode 100644 packages/ui/src/components/GenericHeader.tsx diff --git a/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx b/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx index 7235088bb6..aa74cee4d8 100644 --- a/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx +++ b/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx @@ -102,7 +102,9 @@ export default { {}} - showFilters={false} + searchQuery="" + onSearchQueryChange={() => {}} + showSearchInput={false} pinned={[groupWithLongTitle, groupWithImage].map((g) => makeChannelSummary({ group: g }) )} @@ -121,7 +123,7 @@ export default { {}} - showFilters={false} + showSearchInput={false} pinned={[dmSummary, groupDmSummary]} unpinned={[ groupWithColorAndNoImage, @@ -130,6 +132,8 @@ export default { groupWithNoColorOrImage, ].map((g) => makeChannelSummary({ group: g }))} pendingChats={[]} + searchQuery="" + onSearchQueryChange={() => {}} /> ), @@ -138,10 +142,12 @@ export default { {}} - showFilters={false} + showSearchInput={false} pinned={[]} unpinned={[]} pendingChats={[]} + searchQuery="" + onSearchQueryChange={() => {}} /> ), diff --git a/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx index d07a5b5664..963c57ccba 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/CheckVerifyScreen.tsx @@ -10,7 +10,7 @@ import { formatPhoneNumber } from '@tloncorp/app/utils/string'; import { Button, Field, - GenericHeader, + ScreenHeader, SizableText, Text, TextInput, @@ -123,10 +123,10 @@ export const CheckVerifyScreen = ({ return ( - navigation.goBack()} - showSpinner={isSubmitting} + backAction={() => navigation.goBack()} + isLoading={isSubmitting} /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx index 576a5fdf82..01d237c9e8 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/EULAScreen.tsx @@ -1,5 +1,5 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { GenericHeader, SizableText, View, YStack } from '@tloncorp/ui'; +import { ScreenHeader, SizableText, View, YStack } from '@tloncorp/ui'; import { ScrollView } from 'react-native'; import type { OnboardingStackParamList } from '../../types'; @@ -9,10 +9,10 @@ type Props = NativeStackScreenProps; export const EULAScreen = ({ navigation }: Props) => { return ( - navigation.goBack()} + backAction={() => navigation.goBack()} /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/InventoryCheckScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/InventoryCheckScreen.tsx index 9ac68fbce8..c835efe18f 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/InventoryCheckScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/InventoryCheckScreen.tsx @@ -3,9 +3,9 @@ import { useSignupParams } from '@tloncorp/app/contexts/branch'; import { getHostingAvailability } from '@tloncorp/app/lib/hostingApi'; import { trackError } from '@tloncorp/app/utils/posthog'; import { - GenericHeader, Icon, PrimaryButton, + ScreenHeader, SizableText, Text, View, @@ -48,11 +48,11 @@ export const InventoryCheckScreen = ({ navigation }: Props) => { return ( - navigation.goBack()} - showSpinner={isChecking} + backAction={() => navigation.goBack()} + isLoading={isChecking} /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx index 06ad570443..93ca184470 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/JoinWaitListScreen.tsx @@ -4,8 +4,8 @@ import { addUserToWaitlist } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { Field, - GenericHeader, PrimaryButton, + ScreenHeader, SizableText, TextInput, View, @@ -48,10 +48,10 @@ export const JoinWaitListScreen = ({ navigation }: Props) => { return ( - navigation.goBack()} + backAction={() => navigation.goBack()} /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx index 8e2b960e44..46f5bbf06b 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/RequestPhoneVerifyScreen.tsx @@ -3,11 +3,9 @@ import { useIsDarkMode } from '@tloncorp/app/hooks/useIsDarkMode'; import { requestPhoneVerify } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { - Button, Field, - GenericHeader, + ScreenHeader, SizableText, - Text, View, YStack, useTheme, @@ -78,15 +76,15 @@ export const RequestPhoneVerifyScreen = ({ return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ - + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ + + Next + } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/ResetPasswordScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/ResetPasswordScreen.tsx index d8e06b5cc1..6257fb164f 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/ResetPasswordScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/ResetPasswordScreen.tsx @@ -3,12 +3,10 @@ import { EMAIL_REGEX } from '@tloncorp/app/constants'; import { requestPasswordReset } from '@tloncorp/app/lib/hostingApi'; import { trackError } from '@tloncorp/app/utils/posthog'; import { - Button, Field, - GenericHeader, KeyboardAvoidingView, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, @@ -66,16 +64,16 @@ export const ResetPasswordScreen = ({ return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ isValid && ( - + + Submit + ) } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx index 61d2109d97..2143f8c081 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SetNicknameScreen.tsx @@ -3,11 +3,9 @@ import { useSignupContext } from '@tloncorp/app/contexts/signup'; import { requestNotificationToken } from '@tloncorp/app/lib/notifications'; import { trackError } from '@tloncorp/app/utils/posthog'; import { - Button, Field, - GenericHeader, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, @@ -85,14 +83,10 @@ export const SetNicknameScreen = ({ return ( - - Next - - } + rightControls={} /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/SetTelemetryScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SetTelemetryScreen.tsx index 2761bc9096..b813d6be0a 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SetTelemetryScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SetTelemetryScreen.tsx @@ -1,14 +1,6 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useSignupContext } from '@tloncorp/app/contexts/signup'; -import { - Button, - GenericHeader, - SizableText, - Text, - View, - XStack, - YStack, -} from '@tloncorp/ui'; +import { ScreenHeader, SizableText, View, XStack, YStack } from '@tloncorp/ui'; import { usePostHog } from 'posthog-react-native'; import { useCallback, useState } from 'react'; import { Switch } from 'react-native'; @@ -43,13 +35,13 @@ export const SetTelemetryScreen = ({ return ( - - Next - + rightControls={ + + Next + } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx index 3514f3059f..84d6f2093f 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/ShipLoginScreen.tsx @@ -10,15 +10,13 @@ import { getShipFromCookie } from '@tloncorp/app/utils/ship'; import { transformShipURL } from '@tloncorp/app/utils/string'; import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api'; import { - Button, CheckboxInput, Field, - GenericHeader, Icon, KeyboardAvoidingView, ListItem, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, @@ -125,17 +123,17 @@ export const ShipLoginScreen = ({ navigation }: Props) => { return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ isValid && watch('eulaAgreed') && ( - + + Connect + ) } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx index 8b68985687..7a45acf8bd 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SignUpEmailScreen.tsx @@ -8,19 +8,16 @@ import { getHostingAvailability } from '@tloncorp/app/lib/hostingApi'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { AppInviteDisplay, - Button, - GenericHeader, + Field, KeyboardAvoidingView, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, } from '@tloncorp/ui'; -import { Field } from '@tloncorp/ui'; import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { Controller } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import type { OnboardingStackParamList } from '../../types'; @@ -89,16 +86,16 @@ export const SignUpEmailScreen = ({ navigation, route: { params } }: Props) => { return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ isValid && ( - + + Next + ) } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx index 25fc4d6558..81514a199c 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/SignUpPasswordScreen.tsx @@ -4,9 +4,8 @@ import { initClient, } from '@google-cloud/recaptcha-enterprise-react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { DEFAULT_LURE, RECAPTCHA_SITE_KEY } from '@tloncorp/app/constants'; +import { RECAPTCHA_SITE_KEY } from '@tloncorp/app/constants'; import { - useBranch, useLureMetadata, useSignupParams, } from '@tloncorp/app/contexts/branch'; @@ -19,15 +18,13 @@ import { isEulaAgreed, setEulaAgreed } from '@tloncorp/app/utils/eula'; import { trackError, trackOnboardingAction } from '@tloncorp/app/utils/posthog'; import { AppInviteDisplay, - Button, CheckboxInput, Field, - GenericHeader, Icon, KeyboardAvoidingView, ListItem, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, @@ -182,17 +179,17 @@ export const SignUpPasswordScreen = ({ return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ isValid && watch('eulaAgreed') && ( - + + Next + ) } /> diff --git a/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx b/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx index 1ebcc91ee9..56aa9e3b3e 100644 --- a/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx +++ b/apps/tlon-mobile/src/screens/Onboarding/TlonLoginScreen.tsx @@ -15,15 +15,13 @@ import { isEulaAgreed, setEulaAgreed } from '@tloncorp/app/utils/eula'; import { getShipUrl } from '@tloncorp/app/utils/ship'; import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api'; import { - Button, CheckboxInput, Field, - GenericHeader, Icon, KeyboardAvoidingView, ListItem, + ScreenHeader, SizableText, - Text, TextInput, View, YStack, @@ -149,17 +147,17 @@ export const TlonLoginScreen = ({ navigation }: Props) => { return ( - navigation.goBack()} - showSpinner={isSubmitting} - rightContent={ + backAction={() => navigation.goBack()} + isLoading={isSubmitting} + rightControls={ isValid && watch('eulaAgreed') && ( - + + Connect + ) } /> diff --git a/packages/app/features/groups/GroupPrivacyScreen.tsx b/packages/app/features/groups/GroupPrivacyScreen.tsx index 9aee055e30..29f4886ffe 100644 --- a/packages/app/features/groups/GroupPrivacyScreen.tsx +++ b/packages/app/features/groups/GroupPrivacyScreen.tsx @@ -1,23 +1,18 @@ -import { GroupPrivacy } from '@tloncorp/shared/dist/db/schema'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { GroupPrivacy } from '@tloncorp/shared/dist/db/schema'; import * as store from '@tloncorp/shared/dist/store'; import { - GenericHeader, GroupPrivacySelector, + ScreenHeader, View, triggerHaptic, } from '@tloncorp/ui'; import { useCallback } from 'react'; -import { GroupSettingsStackParamList } from '../../navigation/types'; import { useGroupContext } from '../../hooks/useGroupContext'; +import { GroupSettingsStackParamList } from '../../navigation/types'; - -type Props = NativeStackScreenProps< - GroupSettingsStackParamList, - 'Privacy' ->; - +type Props = NativeStackScreenProps; export function GroupPrivacyScreen(props: Props) { const { groupId } = props.route.params; @@ -35,7 +30,7 @@ export function GroupPrivacyScreen(props: Props) { return ( - + {group ? ( - props.navigation.goBack()} + props.navigation.goBack()} /> diff --git a/packages/app/features/settings/BlockedUsersScreen.tsx b/packages/app/features/settings/BlockedUsersScreen.tsx index 7ec5eeef40..a49cdedf87 100644 --- a/packages/app/features/settings/BlockedUsersScreen.tsx +++ b/packages/app/features/settings/BlockedUsersScreen.tsx @@ -8,7 +8,6 @@ import { Alert } from 'react-native'; import { useCurrentUserId } from '../../hooks/useCurrentUser'; import { RootStackParamList } from '../../navigation/types'; - type Props = NativeStackScreenProps; export function BlockedUsersScreen(props: Props) { @@ -38,10 +37,10 @@ export function BlockedUsersScreen(props: Props) { return ( - - props.navigation.goBack()} /> - Blocked Users - + props.navigation.goBack()} + title="Blocked users" + /> - - - {goingBack ? ( + ) : ( - )} - - Manage Account - + ) + } + title="Manage account" + /> {isWeb && (