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,
// ===
};