diff --git a/apps/tlon-mobile/cosmos.imports.ts b/apps/tlon-mobile/cosmos.imports.ts index 4fad2ab1c8..4a6e0809ea 100644 --- a/apps/tlon-mobile/cosmos.imports.ts +++ b/apps/tlon-mobile/cosmos.imports.ts @@ -3,16 +3,18 @@ import { RendererConfig, UserModuleWrappers } from 'react-cosmos-core'; import * as fixture0 from './src/App.fixture'; -import * as fixture27 from './src/fixtures/ActionSheet.fixture'; -import * as fixture26 from './src/fixtures/AudioEmbed.fixture'; -import * as fixture25 from './src/fixtures/Button.fixture'; -import * as fixture24 from './src/fixtures/Channel.fixture'; -import * as fixture23 from './src/fixtures/ChannelDivider.fixture'; -import * as fixture22 from './src/fixtures/ChannelHeader.fixture'; -import * as fixture21 from './src/fixtures/ChannelSwitcherSheet.fixture'; -import * as fixture20 from './src/fixtures/ChatMessage.fixture'; -import * as fixture19 from './src/fixtures/ChatReference.fixture'; -import * as fixture18 from './src/fixtures/ContactList.fixture'; +import * as fixture29 from './src/fixtures/ActionSheet.fixture'; +import * as fixture28 from './src/fixtures/AudioEmbed.fixture'; +import * as fixture27 from './src/fixtures/BlockSectionList.fixture'; +import * as fixture26 from './src/fixtures/Button.fixture'; +import * as fixture25 from './src/fixtures/Channel.fixture'; +import * as fixture24 from './src/fixtures/ChannelDivider.fixture'; +import * as fixture23 from './src/fixtures/ChannelHeader.fixture'; +import * as fixture22 from './src/fixtures/ChannelSwitcherSheet.fixture'; +import * as fixture21 from './src/fixtures/ChatMessage.fixture'; +import * as fixture20 from './src/fixtures/ChatReference.fixture'; +import * as fixture19 from './src/fixtures/ContactList.fixture'; +import * as fixture18 from './src/fixtures/DetailView.fixture'; import * as fixture17 from './src/fixtures/GalleryPost.fixture'; import * as fixture16 from './src/fixtures/GroupList.fixture'; import * as fixture15 from './src/fixtures/GroupListItem.fixture'; @@ -56,16 +58,18 @@ const fixtures = { 'src/fixtures/GroupListItem.fixture.tsx': { module: fixture15 }, 'src/fixtures/GroupList.fixture.tsx': { module: fixture16 }, 'src/fixtures/GalleryPost.fixture.tsx': { module: fixture17 }, - 'src/fixtures/ContactList.fixture.tsx': { module: fixture18 }, - 'src/fixtures/ChatReference.fixture.tsx': { module: fixture19 }, - 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture20 }, - 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture21 }, - 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture22 }, - 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture23 }, - 'src/fixtures/Channel.fixture.tsx': { module: fixture24 }, - 'src/fixtures/Button.fixture.tsx': { module: fixture25 }, - 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture26 }, - 'src/fixtures/ActionSheet.fixture.tsx': { module: fixture27 }, + 'src/fixtures/DetailView.fixture.tsx': { module: fixture18 }, + 'src/fixtures/ContactList.fixture.tsx': { module: fixture19 }, + 'src/fixtures/ChatReference.fixture.tsx': { module: fixture20 }, + 'src/fixtures/ChatMessage.fixture.tsx': { module: fixture21 }, + 'src/fixtures/ChannelSwitcherSheet.fixture.tsx': { module: fixture22 }, + 'src/fixtures/ChannelHeader.fixture.tsx': { module: fixture23 }, + 'src/fixtures/ChannelDivider.fixture.tsx': { module: fixture24 }, + 'src/fixtures/Channel.fixture.tsx': { module: fixture25 }, + 'src/fixtures/Button.fixture.tsx': { module: fixture26 }, + 'src/fixtures/BlockSectionList.fixture.tsx': { module: fixture27 }, + 'src/fixtures/AudioEmbed.fixture.tsx': { module: fixture28 }, + 'src/fixtures/ActionSheet.fixture.tsx': { module: fixture29 }, }; const decorators = { diff --git a/apps/tlon-mobile/src/fixtures/DetailView.fixture.tsx b/apps/tlon-mobile/src/fixtures/DetailView.fixture.tsx new file mode 100644 index 0000000000..eb90423e88 --- /dev/null +++ b/apps/tlon-mobile/src/fixtures/DetailView.fixture.tsx @@ -0,0 +1,83 @@ +import { PostScreenView } from '@tloncorp/ui/src'; + +import { FixtureWrapper } from './FixtureWrapper'; +import { + createFakePost, + createFakePosts, + tlonLocalBulletinBoard, + tlonLocalGettingStarted, +} from './fakeData'; + +const notebookPost = createFakePost('note'); +const notebookReplies = createFakePosts(notebookPost.replyCount ?? 5, 'reply'); +const galleryPost = createFakePost( + 'block', + undefined, + 'https://togten.com:9001/finned-palmer/finned-palmer/2024.3.19..21.2.17..5581.0624.dd2f.1a9f-image.png' +); +const galleryReplies = createFakePosts(galleryPost.replyCount ?? 5, 'reply'); + +const NotebookDetailViewFixture = () => { + return ( + + {}} + groupMembers={[]} + negotiationMatch={true} + editPost={() => {}} + uploadInfo={{ + uploading: false, + uploadedImage: null, + imageAttachment: null, + setAttachments: () => {}, + resetImageAttachment: () => {}, + canUpload: true, + }} + storeDraft={() => {}} + clearDraft={() => {}} + getDraft={async () => ({})} + goBack={() => {}} + /> + + ); +}; + +const GalleryDetailViewFixture = () => { + return ( + + {}} + groupMembers={[]} + negotiationMatch={true} + editPost={() => {}} + uploadInfo={{ + uploading: false, + uploadedImage: null, + imageAttachment: null, + setAttachments: () => {}, + resetImageAttachment: () => {}, + canUpload: true, + }} + storeDraft={() => {}} + clearDraft={() => {}} + getDraft={async () => ({})} + goBack={() => {}} + /> + + ); +}; + +export default { + notebook: NotebookDetailViewFixture, + galleryPost: GalleryDetailViewFixture, +}; diff --git a/packages/editor/src/MessageInputEditor.tsx b/packages/editor/src/MessageInputEditor.tsx index 03d92c7eb7..69061595da 100644 --- a/packages/editor/src/MessageInputEditor.tsx +++ b/packages/editor/src/MessageInputEditor.tsx @@ -75,7 +75,11 @@ export const MessageInputEditor = () => { style={{ overflow: 'auto', height: 'auto', - // making this explicit + paddingTop: 2, + paddingBottom: 2, + paddingLeft: 12, + paddingRight: 12, + minHeight: 32, fontSize: 16, color: useIsDark() ? 'white' : 'black', fontFamily: diff --git a/packages/editor/src/index.css b/packages/editor/src/index.css index bb2b4504e1..6a5590347e 100644 --- a/packages/editor/src/index.css +++ b/packages/editor/src/index.css @@ -12,3 +12,11 @@ background-color: #6161611a; border-radius: 4px; } + +p { + display: block; + margin-block-start: 12px; + margin-block-end: 12px; + margin-inline-start: 0px; + margin-inline-end: 0px; +} diff --git a/packages/shared/src/logic/utils.ts b/packages/shared/src/logic/utils.ts index ca51477259..8bc6b431dc 100644 --- a/packages/shared/src/logic/utils.ts +++ b/packages/shared/src/logic/utils.ts @@ -334,6 +334,33 @@ export const textPostIsLinkedImage = (post: db.Post): boolean => { return false; }; +export const textPostIsReference = (post: db.Post): boolean => { + const { inlines, references } = extractContentTypesFromPost(post); + if (references.length === 0) { + return false; + } + + if (inlines.length === 2) { + const [first] = inlines; + const isRefString = + typeof first === 'string' && REF_REGEX.test(first as string); + + if (isRefString) { + return true; + } + } + + if ( + inlines.length === 1 && + typeof inlines[0] === 'object' && + 'break' in inlines[0] + ) { + return true; + } + + return false; +}; + export const getCompositeGroups = ( groups: db.Group[], base: Partial[] diff --git a/packages/ui/src/assets/icons/Dots.svg b/packages/ui/src/assets/icons/Dots.svg new file mode 100644 index 0000000000..29c7e0eb93 --- /dev/null +++ b/packages/ui/src/assets/icons/Dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index 95382d91b8..b9fd9e1cec 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -2,6 +2,7 @@ export { default as Bold } from './Bold.svg'; export { default as BlockQuote } from './BlockQuote.svg'; export { default as BulletList } from './BulletList.svg'; export { default as FontSize } from './FontSize.svg'; +export { default as Dots } from './Dots.svg'; export { default as Italic } from './Italic.svg'; export { default as Keyboard } from './Keyboard.svg'; export { default as Strikethrough } from './Strikethrough.svg'; diff --git a/packages/ui/src/components/ActionSheet.tsx b/packages/ui/src/components/ActionSheet.tsx index efbb75b8ff..3c4ff3ede1 100644 --- a/packages/ui/src/components/ActionSheet.tsx +++ b/packages/ui/src/components/ActionSheet.tsx @@ -44,6 +44,7 @@ const ActionSheetActionFrame = styled(Stack, { context: ActionSheetActionContext, padding: '$l', borderWidth: 1, + gap: '$s', borderRadius: '$l', pressStyle: { backgroundColor: '$positiveBackground', @@ -52,7 +53,7 @@ const ActionSheetActionFrame = styled(Stack, { default: { true: { backgroundColor: '$background', - borderColor: '$tertiaryText', + borderColor: '$shadow', }, }, success: { diff --git a/packages/ui/src/components/AuthorRow.tsx b/packages/ui/src/components/AuthorRow.tsx index 113ca1e853..13bce6df8d 100644 --- a/packages/ui/src/components/AuthorRow.tsx +++ b/packages/ui/src/components/AuthorRow.tsx @@ -2,7 +2,7 @@ import { utils } from '@tloncorp/shared'; import * as db from '@tloncorp/shared/dist/db'; import { useMemo } from 'react'; -import { SizableText, View, XStack } from '../core'; +import { SizableText, SizeTokens, View, XStack } from '../core'; import { Avatar } from './Avatar'; import ContactName from './ContactName'; import { ListItem } from './ListItem'; @@ -30,8 +30,7 @@ export default function AuthorRow({ sent, roles, type, - parentPost, - setShowComments, + detailView, width, }: { author?: db.Contact | null; @@ -40,9 +39,8 @@ export default function AuthorRow({ roles?: string[]; deliveryStatus?: db.PostDeliveryStatus | null; type?: db.PostType; - parentPost?: db.Post; - setShowComments?: (show: boolean) => void; - width?: number; + detailView?: boolean; + width?: SizeTokens; }) { const timeDisplay = useMemo(() => { const date = new Date(sent); @@ -50,37 +48,12 @@ export default function AuthorRow({ }, [sent]); const firstRole = roles?.[0]; - if (parentPost) { + if (detailView) { return ( - setShowComments?.(true)}> - - - - - - - - {timeDisplay} - - {parentPost.replyCount ?? 0} - - - + + + + ); } @@ -123,10 +96,7 @@ export default function AuthorRow({ return ( - - - {timeDisplay} - + ); } diff --git a/packages/ui/src/components/Channel/ChannelHeader.tsx b/packages/ui/src/components/Channel/ChannelHeader.tsx index 441e3dd3e9..85e91eba9e 100644 --- a/packages/ui/src/components/Channel/ChannelHeader.tsx +++ b/packages/ui/src/components/Channel/ChannelHeader.tsx @@ -1,11 +1,16 @@ +import * as db from '@tloncorp/shared/dist/db'; +import { useMemo, useState } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Dots } from '../../assets/icons'; import { Channel as ChannelIcon, ChevronLeft, Search, } from '../../assets/icons'; import { SizableText, Spinner, View, XStack } from '../../core'; +import { ActionSheet } from '../ActionSheet'; +import { getPostActions } from '../ChatMessage/ChatMessageActions/MessageActions'; import { IconButton } from '../IconButton'; export function ChannelHeader({ @@ -16,6 +21,10 @@ export function ChannelHeader({ showPickerButton, showSpinner, showSearchButton = true, + showMenuButton = false, + post, + channelType, + currentUserId, }: { title: string; goBack?: () => void; @@ -24,8 +33,31 @@ export function ChannelHeader({ showPickerButton?: boolean; showSpinner?: boolean; showSearchButton?: boolean; + showMenuButton?: boolean; + post?: db.Post; + channelType?: db.ChannelType; + currentUserId?: string; }) { const insets = useSafeAreaInsets(); + const [showActionSheet, setShowActionSheet] = useState(false); + + const postActions = useMemo(() => { + if (!post || !channelType || !currentUserId) return []; + return getPostActions(post, channelType).filter((action) => { + switch (action.id) { + case 'startThread': + // if undelivered or already in a thread, don't show reply + return false; + case 'edit': + // only show edit for current user's posts + return post.authorId === currentUserId; + // TODO: delete case should only be shown for admins or the author + default: + return true; + } + }); + }, [post, channelType, currentUserId]); + return ( - - - - + + {goBack && ( + + + + )} )} + {showMenuButton && ( + setShowActionSheet(true)}> + + + )} + + {postActions.map((action) => ( + ({})}> + {action.label} + + ))} + ); } diff --git a/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx b/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx index 48d31fcbab..1e435b6c0f 100644 --- a/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx +++ b/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx @@ -71,7 +71,7 @@ interface ChannelAction { label: string; actionType?: 'destructive'; } -function getPostActions( +export function getPostActions( post: db.Post, channelType: db.ChannelType ): ChannelAction[] { diff --git a/packages/ui/src/components/CommentsScrollerSheet.tsx b/packages/ui/src/components/CommentsScrollerSheet.tsx deleted file mode 100644 index fd64dd9463..0000000000 --- a/packages/ui/src/components/CommentsScrollerSheet.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type * as api from '@tloncorp/shared/dist/api'; -import type * as db from '@tloncorp/shared/dist/db'; -import * as urbit from '@tloncorp/shared/dist/urbit'; -import { useState } from 'react'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { getTokenValue } from 'tamagui'; - -import { View } from '../core'; -import { ActionSheet } from './ActionSheet'; -import AuthorRow from './AuthorRow'; -import Scroller from './Channel/Scroller'; -import { ChatMessage } from './ChatMessage'; -import { MessageInput } from './MessageInput'; - -export default function CommentsScrollerSheet({ - open, - setOpen, - channelId, - currentUserId, - editingPost, - setEditingPost, - sendReply, - editPost, - parentPost, - posts, - uploadInfo, - groupMembers, - storeDraft, - clearDraft, - getDraft, - onPressImage, -}: { - open: boolean; - setOpen: (open: boolean) => void; - channelId: string; - currentUserId: string; - editingPost?: db.Post; - setEditingPost?: (post: db.Post | undefined) => void; - sendReply: (content: urbit.Story, channelId: string) => void; - editPost: (post: db.Post, content: urbit.Story) => void; - parentPost: db.Post; - posts: db.Post[]; - uploadInfo: api.UploadInfo; - groupMembers: db.ChatMember[]; - storeDraft: (draft: urbit.JSONContent) => void; - clearDraft: () => void; - getDraft: () => Promise; - onPressImage?: (post: db.Post, uri?: string) => void; -}) { - const [inputShouldBlur, setInputShouldBlur] = useState(false); - const { bottom } = useSafeAreaInsets(); - - return ( - - - - - - - - - - ); -} diff --git a/packages/ui/src/components/ContentRenderer.tsx b/packages/ui/src/components/ContentRenderer.tsx index 63edff3250..6266365f2d 100644 --- a/packages/ui/src/components/ContentRenderer.tsx +++ b/packages/ui/src/components/ContentRenderer.tsx @@ -367,12 +367,14 @@ export function InlineContent({ viewMode = 'chat', onPressImage, onLongPress, + serif = false, }: { inline: Inline | null; color?: ColorTokens; viewMode?: PostViewMode; onPressImage?: (src: string) => void; onLongPress?: () => void; + serif?: boolean; }) { if (inline === null) { return null; @@ -390,6 +392,7 @@ export function InlineContent({ color={color} lineHeight="$m" fontSize={viewMode === 'block' ? '$s' : '$m'} + fontFamily={serif ? '$serif' : '$body'} > {inline} @@ -400,7 +403,13 @@ export function InlineContent({ return ( <> {inline.bold.map((s, k) => ( - + ))} @@ -412,7 +421,13 @@ export function InlineContent({ return ( <> {inline.italics.map((s, k) => ( - + ))} @@ -426,6 +441,7 @@ export function InlineContent({ {inline.strike.map((s, k) => ( ; + return ( + + ); } if (isTask(inline)) { return ( - + {inline.task.checked ? '☑' : '☐'} {inline.task.content.map((s, k) => ( @@ -598,6 +625,7 @@ const LineRenderer = memo( color = '$primaryText', isNotice = false, viewMode = 'chat', + serif = false, }: { inlines: Inline[]; onLongPress?: () => void; @@ -605,6 +633,7 @@ const LineRenderer = memo( color?: ColorTokens; isNotice?: boolean; viewMode?: PostViewMode; + serif?: boolean; }) => { const inlineElements: ReactElement[][] = []; let currentLine: ReactElement[] = []; @@ -631,6 +660,7 @@ const LineRenderer = memo( color={isNotice ? '$secondaryText' : color} fontSize={viewMode === 'block' || isNotice ? '$s' : '$m'} lineHeight="$m" + fontFamily={serif ? '$serif' : '$body'} > {inline} @@ -654,6 +684,7 @@ const LineRenderer = memo( )} @@ -666,6 +697,7 @@ const LineRenderer = memo( key={`ship-${index}`} userId={inline.ship} fontSize={isNotice ? '$s' : 'unset'} + fontFamily={serif ? '$serif' : '$body'} /> ); } else { @@ -677,6 +709,7 @@ const LineRenderer = memo( color={color} onPressImage={onPressImage} onLongPress={onLongPress} + serif={serif} /> ); } @@ -703,6 +736,7 @@ const LineRenderer = memo( key={`line-${index}`} flexWrap="wrap" lineHeight="$m" + fontFamily={serif ? '$serif' : '$body'} > {line} @@ -813,6 +847,7 @@ export default function ContentRenderer({ onPressImage={onPressImage} onLongPress={onLongPress} viewMode={viewMode} + serif /> ); } diff --git a/packages/ui/src/components/DetailView/DetailView.tsx b/packages/ui/src/components/DetailView/DetailView.tsx new file mode 100644 index 0000000000..b2ab8f10e0 --- /dev/null +++ b/packages/ui/src/components/DetailView/DetailView.tsx @@ -0,0 +1,172 @@ +import { makePrettyShortDate } from '@tloncorp/shared/dist'; +import type * as api from '@tloncorp/shared/dist/api'; +import * as db from '@tloncorp/shared/dist/db'; +import * as urbit from '@tloncorp/shared/dist/urbit'; +import { PropsWithChildren, useMemo, useState } from 'react'; +import { FlatList } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { getTokenValue, withStaticProperties } from 'tamagui'; + +import { Text, View, YStack } from '../../core'; +import AuthorRow from '../AuthorRow'; +import Scroller from '../Channel/Scroller'; +import { ChatMessage } from '../ChatMessage'; +import { MessageInput } from '../MessageInput'; +import { DEFAULT_MESSAGE_INPUT_HEIGHT } from '../MessageInput/index.native'; +import { NavBar } from '../NavBar'; + +export interface DetailViewProps { + post: db.Post; + children?: JSX.Element; + currentUserId?: string; + editingPost?: db.Post; + setEditingPost?: (post: db.Post | undefined) => void; + editPost?: (post: db.Post, content: urbit.Story) => void; + sendReply: (content: urbit.Story, channelId: string) => void; + groupMembers: db.ChatMember[]; + posts?: db.Post[]; + onPressImage?: (post: db.Post, imageUri?: string) => void; + uploadInfo: api.UploadInfo; + storeDraft: (draft: urbit.JSONContent) => void; + clearDraft: () => void; + getDraft: () => Promise; + goBack?: () => void; +} + +const DetailViewMetaDataComponent = ({ + post, + showReplyCount = false, +}: { + post: db.Post; + showReplyCount?: boolean; +}) => { + const dateDisplay = useMemo(() => { + const date = new Date(post.sentAt); + + return makePrettyShortDate(date); + }, [post.sentAt]); + + return ( + + + + {dateDisplay} + + {showReplyCount && ( + + {post.replyCount} replies + + )} + + ); +}; + +const DetailViewHeaderComponentFrame = ({ + replyCount, + children, +}: PropsWithChildren<{ + replyCount: number; +}>) => { + return ( + + + {children} + + + + {replyCount} replies + + + + ); +}; + +const DetailViewFrameComponent = ({ + post, + currentUserId, + editingPost, + setEditingPost, + editPost, + sendReply, + groupMembers, + posts, + onPressImage, + uploadInfo, + storeDraft, + clearDraft, + getDraft, + children, + goBack, +}: DetailViewProps) => { + const [navHeight, setNavHeight] = useState(DEFAULT_MESSAGE_INPUT_HEIGHT); + const [inputShouldBlur, setInputShouldBlur] = useState(false); + const { bottom } = useSafeAreaInsets(); + return ( + + ( + + + + )} + /> + + + + + ); +}; + +export const DetailView = withStaticProperties(DetailViewFrameComponent, { + MetaData: DetailViewMetaDataComponent, + Header: DetailViewHeaderComponentFrame, +}); diff --git a/packages/ui/src/components/DetailView/GalleryDetailView.tsx b/packages/ui/src/components/DetailView/GalleryDetailView.tsx new file mode 100644 index 0000000000..0edaa2b249 --- /dev/null +++ b/packages/ui/src/components/DetailView/GalleryDetailView.tsx @@ -0,0 +1,188 @@ +import { + extractContentTypesFromPost, + findFirstImageBlock, + isImagePost, + isReferencePost, + isTextPost, + textPostIsLinkedImage, + textPostIsReference, + tiptap, +} from '@tloncorp/shared/dist'; +import * as urbit from '@tloncorp/shared/dist/urbit'; +import { useCallback, useMemo } from 'react'; +import { Dimensions } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { Image, Text, View, YStack } from '../../core'; +import ContentReference from '../ContentReference'; +import ContentRenderer from '../ContentRenderer'; +import { DetailView, DetailViewProps } from './DetailView'; + +export default function GalleryDetailView({ + post, + currentUserId, + editingPost, + setEditingPost, + editPost, + sendReply, + groupMembers, + posts, + onPressImage, + uploadInfo, + storeDraft, + clearDraft, + getDraft, + goBack, +}: DetailViewProps) { + // We want the content of the detail view to take up 70% of the screen height + const HEIGHT_DETAIL_VIEW_CONTENT = Dimensions.get('window').height * 0.5; + // We want the content of the detail view to take up 100% of the screen width + const WIDTH_DETAIL_VIEW_CONTENT = Dimensions.get('window').width; + + const { inlines, references, blocks } = useMemo( + () => extractContentTypesFromPost(post), + [post] + ); + + const postIsJustImage = useMemo(() => isImagePost(post), [post]); + const postIsJustText = useMemo(() => isTextPost(post), [post]); + const postIsJustReference = useMemo(() => isReferencePost(post), [post]); + + const image = useMemo( + () => (postIsJustImage ? findFirstImageBlock(blocks)?.image : null), + [blocks, postIsJustImage] + ); + + const textPostIsJustLinkedImage = useMemo( + () => textPostIsLinkedImage(post), + [post] + ); + + const textPostIsJustReference = useMemo( + () => textPostIsReference(post), + [post] + ); + + const linkedImage = useMemo( + () => + textPostIsJustLinkedImage + ? (inlines[0] as urbit.Link).link.href + : undefined, + [inlines, textPostIsJustLinkedImage] + ); + + const handleImagePressed = useCallback( + (uri: string) => { + onPressImage?.(post, uri); + }, + [onPressImage, post] + ); + + if ( + !postIsJustImage && + !postIsJustText && + !postIsJustReference && + !textPostIsJustReference + ) { + // This should never happen, but if it does, we should log it + const content = JSON.parse(post.content as string); + console.log('Unsupported post type', { + post, + content, + }); + + return ( + + Unsupported post type + + ); + } + + return ( + + + + {(postIsJustImage || textPostIsJustLinkedImage) && ( + + handleImagePressed(postIsJustImage ? image!.src : linkedImage!) + } + > + + + {inlines.length > 0 && !textPostIsJustLinkedImage && ( + + {tiptap.inlineToString(inlines[0])} + + )} + + + )} + {postIsJustText && + !textPostIsJustLinkedImage && + !textPostIsJustReference && ( + + + + + + )} + {(postIsJustReference || textPostIsJustReference) && ( + + + + + + )} + + + + + ); +} diff --git a/packages/ui/src/components/DetailView/NotebookDetailView.tsx b/packages/ui/src/components/DetailView/NotebookDetailView.tsx new file mode 100644 index 0000000000..ed39b37011 --- /dev/null +++ b/packages/ui/src/components/DetailView/NotebookDetailView.tsx @@ -0,0 +1,86 @@ +import { useCallback } from 'react'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { getTokenValue } from 'tamagui'; + +import { Image, Text, View } from '../../core'; +import ContentRenderer from '../ContentRenderer'; +import { DetailView, DetailViewProps } from './DetailView'; + +const IMAGE_HEIGHT = 268; + +export default function NotebookDetailView({ + post, + currentUserId, + editingPost, + setEditingPost, + editPost, + sendReply, + groupMembers, + posts, + onPressImage, + uploadInfo, + storeDraft, + clearDraft, + getDraft, + goBack, +}: DetailViewProps) { + const handleImagePressed = useCallback(() => { + if (post.image) { + onPressImage?.(post, post.image); + } + }, [post, onPressImage]); + + if (!post) { + return null; + } + + return ( + + + {post.image && ( + + + + + + )} + {post.title && ( + + {post.title} + + )} + + + + + + + ); +} diff --git a/packages/ui/src/components/DetailView/index.tsx b/packages/ui/src/components/DetailView/index.tsx new file mode 100644 index 0000000000..6c34d72485 --- /dev/null +++ b/packages/ui/src/components/DetailView/index.tsx @@ -0,0 +1,3 @@ +import NotebookDetailView from './NotebookDetailView'; + +export { NotebookDetailView }; diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx index 6b2739e303..4f26d3d6f1 100644 --- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx +++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx @@ -5,20 +5,15 @@ import { isReferencePost, isTextPost, textPostIsLinkedImage, - tiptap, + textPostIsReference, } from '@tloncorp/shared/dist'; import * as db from '@tloncorp/shared/dist/db'; -import { Link } from 'packages/shared/dist/urbit'; -import { useCallback, useMemo } from 'react'; -import { - Dimensions, - Image, - ImageBackground, - TouchableOpacity, -} from 'react-native'; +import { Link } from '@tloncorp/shared/dist/urbit'; +import { useMemo } from 'react'; +import { Dimensions, ImageBackground } from 'react-native'; import { getTokenValue } from 'tamagui'; -import { LinearGradient, Text, View, YStack } from '../../core'; +import { LinearGradient, Text, View } from '../../core'; import AuthorRow from '../AuthorRow'; import ContentReference from '../ContentReference'; import ContentRenderer from '../ContentRenderer'; @@ -28,14 +23,10 @@ export default function GalleryPost({ post, onPress, onLongPress, - onPressImage, - detailView = false, }: { post: db.Post; onPress?: (post: db.Post) => void; onLongPress?: (post: db.Post) => void; - onPressImage?: (post: db.Post, imageUri?: string) => void; - detailView?: boolean; }) { // We want to show two posts per row in the gallery view, each with a margin of 16 // and padding of 16 on all sides. This means that the width of each post should be @@ -44,10 +35,6 @@ export default function GalleryPost({ const postMargin = getTokenValue('$l'); const HEIGHT_AND_WIDTH = (Dimensions.get('window').width - 2 * postPadding - 2 * postMargin) / 2; - // We want the content of the detail view to take up 70% of the screen height - const HEIGHT_DETAIL_VIEW_CONTENT = Dimensions.get('window').height * 0.7; - // We want the content of the detail view to take up 100% of the screen width - const WIDTH_DETAIL_VIEW_CONTENT = Dimensions.get('window').width; const { inlines, references, blocks } = useMemo( () => extractContentTypesFromPost(post), @@ -68,25 +55,32 @@ export default function GalleryPost({ [post] ); + const textPostIsJustReference = useMemo( + () => textPostIsReference(post), + [post] + ); + const linkedImage = useMemo( () => textPostIsJustLinkedImage ? (inlines[0] as Link).link.href : undefined, [inlines, textPostIsJustLinkedImage] ); - const handleImagePressed = useCallback( - (uri: string) => { - onPressImage?.(post, uri); - }, - [onPressImage, post] - ); - - if (!postIsJustImage && !postIsJustText && !postIsJustReference) { + if ( + !postIsJustImage && + !postIsJustText && + !postIsJustReference && + !textPostIsJustReference + ) { // This should never happen, but if it does, we should log it const content = JSON.parse(post.content as string); console.log('Unsupported post type', { post, content, + postIsJustText, + postIsJustImage, + postIsJustReference, + textPostIsJustReference, }); return ( @@ -102,77 +96,46 @@ export default function GalleryPost({ onLongPress={() => onLongPress?.(post)} > - {(postIsJustImage || textPostIsJustLinkedImage) && - (detailView ? ( - - handleImagePressed(postIsJustImage ? image!.src : linkedImage!) - } - > - - - {inlines.length > 0 && !textPostIsJustLinkedImage && ( - - {tiptap.inlineToString(inlines[0])} - - )} - - - ) : ( - - {!detailView && ( - - - - )} - - ))} - {postIsJustText && !textPostIsJustLinkedImage && ( - + + + + + )} + {postIsJustText && + !textPostIsJustLinkedImage && + !textPostIsJustReference && ( - - {!detailView && ( + + - )} - - {!detailView && ( + - )} - - )} - {postIsJustReference && ( + + )} + {(postIsJustReference || textPostIsJustReference) && ( - - {!detailView && ( - - )} + void; size?: SizeTokens; color?: ThemeTokens | ColorTokens; - backgroundColor?: ThemeTokens | ColorTokens; + backgroundColor?: ThemeTokens | ColorTokens | 'unset'; backgroundColorOnPress?: ThemeTokens | ColorTokens; radius?: RadiusTokens; disabled?: boolean; diff --git a/packages/ui/src/components/MessageInput/AttachmentButton.native.tsx b/packages/ui/src/components/MessageInput/AttachmentButton.native.tsx index b5b34c32f9..224f4ab314 100644 --- a/packages/ui/src/components/MessageInput/AttachmentButton.native.tsx +++ b/packages/ui/src/components/MessageInput/AttachmentButton.native.tsx @@ -20,7 +20,10 @@ export default function AttachmentButton({ ) : ( - setShowInputSelector(true)}> + setShowInputSelector(true)} + > )} diff --git a/packages/ui/src/components/MessageInput/MessageInputBase.tsx b/packages/ui/src/components/MessageInput/MessageInputBase.tsx index df675dabd7..0479bfdf7e 100644 --- a/packages/ui/src/components/MessageInput/MessageInputBase.tsx +++ b/packages/ui/src/components/MessageInput/MessageInputBase.tsx @@ -5,7 +5,7 @@ import { JSONContent, Story } from '@tloncorp/shared/dist/urbit'; import { PropsWithChildren, useMemo } from 'react'; import { SpaceTokens } from 'tamagui'; -import { ArrowUp, Checkmark, Close } from '../../assets/icons'; +import { ArrowUp, Checkmark, ChevronLeft, Close } from '../../assets/icons'; import { ThemeTokens, View, XStack, YStack } from '../../core'; import FloatingActionButton from '../FloatingActionButton'; import { Icon } from '../Icon'; @@ -38,6 +38,10 @@ export interface MessageInputProps { image?: UploadedFile; showToolbar?: boolean; channelType?: db.ChannelType; + initialHeight?: number; + // for external access to height + setHeight?: (height: number) => void; + goBack?: () => void; ref?: React.RefObject<{ editor: EditorBridge | null; setEditor: (editor: EditorBridge) => void; @@ -59,6 +63,7 @@ export const MessageInputContainer = ({ isEditing = false, cancelEditing, onPressEdit, + goBack, }: PropsWithChildren<{ onPressSend: () => void; uploadInfo?: UploadInfo; @@ -73,6 +78,7 @@ export const MessageInputContainer = ({ isEditing?: boolean; cancelEditing?: () => void; onPressEdit?: () => void; + goBack?: () => void; }>) => { const hasUploadedImage = useMemo( () => !!(uploadInfo?.uploadedImage && uploadInfo.uploadedImage.url !== ''), @@ -101,16 +107,23 @@ export const MessageInputContainer = ({ alignItems="flex-end" justifyContent="space-between" > + {goBack ? ( + + + + + + ) : null} {isEditing ? ( - - + + ) : null} {hasUploadedImage ? null : uploadInfo?.canUpload && showAttachmentButton ? ( - + ) : null} @@ -131,12 +144,13 @@ export const MessageInputContainer = ({ )} ) : ( - + {disableSend ? null : ( {isEditing ? : } diff --git a/packages/ui/src/components/MessageInput/index.native.tsx b/packages/ui/src/components/MessageInput/index.native.tsx index 9c72a84445..d93122131b 100644 --- a/packages/ui/src/components/MessageInput/index.native.tsx +++ b/packages/ui/src/components/MessageInput/index.native.tsx @@ -85,7 +85,7 @@ const getInjectedJS = (bridgeExtensions: BridgeExtension[]) => { // 52 accounts for the 16px padding around the text within the input // and the 20px line height of the text. 16 + 20 + 16 = 52 -const DEFAULT_CONTAINER_HEIGHT = 52; +export const DEFAULT_MESSAGE_INPUT_HEIGHT = 44; export interface MessageInputHandle { editor: EditorBridge | null; @@ -111,12 +111,15 @@ export const MessageInput = forwardRef( showAttachmentButton = true, floatingActionButton = false, backgroundColor = '$secondaryBackground', - paddingHorizontal = '$l', + paddingHorizontal, + initialHeight = DEFAULT_MESSAGE_INPUT_HEIGHT, placeholder = 'Message', bigInput = false, title, image, channelType, + setHeight, + goBack, }, ref ) => { @@ -130,9 +133,7 @@ export const MessageInput = forwardRef( })); const [hasSetInitialContent, setHasSetInitialContent] = useState(false); - const [containerHeight, setContainerHeight] = useState( - DEFAULT_CONTAINER_HEIGHT - ); + const [containerHeight, setContainerHeight] = useState(initialHeight); const { bottom, top } = useSafeAreaInsets(); const { height } = useWindowDimensions(); const headerHeight = 48; @@ -349,7 +350,6 @@ export const MessageInput = forwardRef( // @ts-expect-error setContent does accept JSONContent editor.setContent(newJson); - // editor.setSelection(initialSelection.from, initialSelection.to); } } } @@ -567,6 +567,7 @@ export const MessageInput = forwardRef( if (type === 'contentHeight') { setContainerHeight(payload); + setHeight?.(payload); return; } @@ -579,7 +580,7 @@ export const MessageInput = forwardRef( e.onEditorMessage && e.onEditorMessage({ type, payload }, editor); }); }, - [editor, handleAddNewLine, handlePaste] + [editor, handleAddNewLine, handlePaste, setHeight, webviewRef] ); const tentapInjectedJs = useMemo( @@ -620,6 +621,7 @@ export const MessageInput = forwardRef( disableSend={ editorIsEmpty || (channelType === 'notebook' && titleIsEmpty) } + goBack={goBack} > @@ -29,7 +33,7 @@ const NavBar = React.memo(function NavBar(props: { alignItems="flex-start" paddingTop={'$m'} > - {props.children} + {children} diff --git a/packages/ui/src/components/NotebookPost/NotebookDetailView.tsx b/packages/ui/src/components/NotebookPost/NotebookDetailView.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/ui/src/components/NotebookPost/NotebookPost.tsx b/packages/ui/src/components/NotebookPost/NotebookPost.tsx index e06b4ca0ca..5ff92676a9 100644 --- a/packages/ui/src/components/NotebookPost/NotebookPost.tsx +++ b/packages/ui/src/components/NotebookPost/NotebookPost.tsx @@ -1,13 +1,9 @@ import { makePrettyShortDate } from '@tloncorp/shared/dist'; import * as db from '@tloncorp/shared/dist/db'; -import { ScrollView } from 'moti'; import { useCallback, useMemo } from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { getTokenValue } from 'tamagui'; -import { Image, Text, View, XStack, YStack } from '../../core'; +import { Image, Text, YStack } from '../../core'; import AuthorRow from '../AuthorRow'; -import ContentRenderer from '../ContentRenderer'; import Pressable from '../Pressable'; const IMAGE_HEIGHT = 268; @@ -16,8 +12,6 @@ export default function NotebookPost({ post, onPress, onLongPress, - onPressImage, - detailView = false, showReplies = true, showAuthor = true, smallImage = false, @@ -43,59 +37,26 @@ export default function NotebookPost({ onLongPress?.(post); }, [post, onLongPress]); - const handleImagePressed = useCallback(() => { - if (post.image) { - onPressImage?.(post, post.image); - } - }, [post, onPressImage]); - if (!post) { return null; } - if (detailView) { - return ( - - - {post.image && ( - - - - - - )} - - {post.title && ( - - {post.title} - - )} - - {dateDisplay} - - - - - - ); - } - return ( onPress?.(post)} onLongPress={handleLongPress} delayLongPress={250} > - + {post.image && ( )} - - {post.title && ( - - {post.title} - - )} - - {dateDisplay} + {post.title && ( + + {post.title} + + )} + {showAuthor && ( + + )} + + {dateDisplay} + + {showReplies && ( + + {post.replyCount} replies - - - {showAuthor && ( - - )} - {showReplies && ( - - - {post.replyCount} comments - - - )} - + )} ); diff --git a/packages/ui/src/components/PostScreenView.tsx b/packages/ui/src/components/PostScreenView.tsx index e47599d1eb..133a8f0ae3 100644 --- a/packages/ui/src/components/PostScreenView.tsx +++ b/packages/ui/src/components/PostScreenView.tsx @@ -11,15 +11,13 @@ import { CalmProvider, CalmState, ContactsProvider } from '../contexts'; import { ReferencesProvider } from '../contexts/references'; import { Text, View, YStack } from '../core'; import * as utils from '../utils'; -import AuthorRow, { AUTHOR_ROW_HEIGHT_DETAIL_VIEW } from './AuthorRow'; import { ChannelHeader } from './Channel/ChannelHeader'; import Scroller from './Channel/Scroller'; import UploadedImagePreview from './Channel/UploadedImagePreview'; import { ChatMessage } from './ChatMessage'; -import CommentsScrollerSheet from './CommentsScrollerSheet'; -import { GalleryPost } from './GalleryPost'; +import { NotebookDetailView } from './DetailView'; +import GalleryDetailView from './DetailView/GalleryDetailView'; import { MessageInput } from './MessageInput'; -import { NotebookPost } from './NotebookPost'; export function PostScreenView({ currentUserId, @@ -61,7 +59,6 @@ export function PostScreenView({ negotiationMatch: boolean; }) { const [inputShouldBlur, setInputShouldBlur] = useState(false); - const [showComments, setShowComments] = useState(false); const canWrite = utils.useCanWrite(channel, currentUserId); const isChatChannel = channel ? getIsChatChannel(channel) : true; const postsWithoutParent = useMemo( @@ -69,49 +66,69 @@ export function PostScreenView({ [posts, parentPost] ); + const { bottom } = useSafeAreaInsets(); + const headerTitle = isChatChannel ? `Thread: ${channel?.title ?? null}` - : parentPost?.title - ? parentPost.title - : `Post: ${channel?.title ?? null}`; - - const { bottom } = useSafeAreaInsets(); + : 'Post'; return ( - + {parentPost && channel.type === 'gallery' && ( - - - + )} {parentPost && channel.type === 'notebook' && ( - - - + )} {uploadInfo.imageAttachment ? ( 1 && channel && isChatChannel && ( - + + + ) )} - {parentPost && ( - - )} {negotiationMatch && !editingPost && channel && canWrite && ( {isChatChannel ? ( @@ -179,14 +178,6 @@ export function PostScreenView({ clearDraft={clearDraft} getDraft={getDraft} /> - ) : parentPost ? ( - ) : null} )} diff --git a/packages/ui/src/tamagui.config.ts b/packages/ui/src/tamagui.config.ts index 8f2279cc3d..0820474d73 100644 --- a/packages/ui/src/tamagui.config.ts +++ b/packages/ui/src/tamagui.config.ts @@ -98,6 +98,7 @@ export const themes = { background: '#1A1818', transparentBackground: 'rgba(24, 24, 24, 0)', secondaryBackground: '#322E2E', + shadow: 'rgba(0, 0, 0, 0.08)', tertiaryText: '#808080', border: '#333333', secondaryBorder: '#4C4C4C', @@ -119,6 +120,7 @@ export const themes = { background: '#FFFFFF', transparentBackground: 'rgba(255, 255, 255, 0)', secondaryBackground: '#F5F5F5', + shadow: 'rgba(24, 24, 24, 0.08)', tertiaryText: '#999999', border: '#E5E5E5', secondaryBorder: '#CCCCCC', @@ -139,6 +141,7 @@ export const themes = { secondaryText: '#75C4D3', background: '#005073', secondaryBackground: '#0076A3', + shadow: 'rgba(0, 0, 0, 0.08)', tertiaryText: '#58C9E8', border: '#0076A3', secondaryBorder: '#4C4C4C', @@ -159,6 +162,7 @@ export const themes = { secondaryText: '#9C9588', background: '#E6D5B8', secondaryBackground: '#F0E5CF', + shadow: 'rgba(0, 0, 0, 0.08)', tertiaryText: '#A89F91', border: '#F0E5CF', secondaryBorder: '#4C4C4C', @@ -179,6 +183,7 @@ export const themes = { secondaryText: '#849E8B', background: '#2F4F2F', secondaryBackground: '#567856', + shadow: 'rgba(0, 0, 0, 0.08)', tertiaryText: '#738C70', border: '#567856', secondaryBorder: '#4C4C4C', @@ -199,6 +204,7 @@ export const themes = { secondaryText: '#B3ADA4', background: '#3E423A', secondaryBackground: '#767C74', + shadow: 'rgba(0, 0, 0, 0.08)', tertiaryText: '#91988F', border: '#767C74', secondaryBorder: '#4C4C4C', @@ -226,6 +232,7 @@ export const systemFont = createFont({ l: 17, // xl is used for emoji-only messages xl: 36, + '2xl': 44, }, lineHeight: { s: 22, @@ -243,6 +250,33 @@ export const systemFont = createFont({ }, }); +export const serifFont = createFont({ + family: 'Georgia', + size: { + xs: 12, + s: 14, + m: 16, + true: 16, + l: 17, + xl: 32, + '2xl': 44, + }, + lineHeight: { + s: 22, + m: 24, + true: 24, + }, + weight: { + s: '400', + m: 'regular', + true: 'regular', + l: 'medium', + }, + letterSpacing: { + l: 0, + }, +}); + export const monoFont = createFont({ family: 'Menlo-Regular', size: { @@ -251,6 +285,7 @@ export const monoFont = createFont({ true: 15, l: 15, xl: 15, + '2xl': 15, }, lineHeight: { l: 19, @@ -268,6 +303,7 @@ export const fonts = { heading: systemFont, body: systemFont, mono: monoFont, + serif: serifFont, // === };