From 53e9457c935056bd84d860b1e7a6c874c2f4c1e9 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:27:44 -0500 Subject: [PATCH] Add compact view (#95) --- index.html | 1 + src/TabbedRoutes.tsx | 4 + src/features/comment/reply/CommentReply.tsx | 7 +- src/features/feed/PostCommentFeed.tsx | 42 +++- src/features/labels/links/shared.ts | 1 + src/features/post/inFeed/Post.tsx | 226 ++---------------- .../post/inFeed/compact/CompactPost.tsx | 94 ++++++++ .../post/inFeed/compact/Thumbnail.tsx | 66 +++++ src/features/post/inFeed/compact/self.svg | 1 + src/features/post/inFeed/large/LargePost.tsx | 210 ++++++++++++++++ src/features/post/shared/MoreActions.tsx | 13 +- src/features/post/shared/VoteButton.tsx | 4 +- src/features/settings/appearance/PostView.tsx | 27 +++ src/features/settings/appearance/TextSize.tsx | 2 +- .../settings/appearance/appearanceSlice.tsx | 32 ++- .../appearance/posts/PostsViewSelection.tsx | 29 +++ src/pages/settings/AppearancePage.tsx | 2 + src/pages/settings/PostAppearancePage.tsx | 32 +++ 18 files changed, 573 insertions(+), 220 deletions(-) create mode 100644 src/features/post/inFeed/compact/CompactPost.tsx create mode 100644 src/features/post/inFeed/compact/Thumbnail.tsx create mode 100644 src/features/post/inFeed/compact/self.svg create mode 100644 src/features/post/inFeed/large/LargePost.tsx create mode 100644 src/features/settings/appearance/PostView.tsx create mode 100644 src/features/settings/appearance/posts/PostsViewSelection.tsx create mode 100644 src/pages/settings/PostAppearancePage.tsx diff --git a/index.html b/index.html index 42c0829d11..c8d731cfcd 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ + {/* general routes for settings is only for apollo-migrate */} {...buildGeneralBrowseRoutes("settings")} + + + state.appearance.posts.type + ); - const renderItemContent = useCallback( + const borderCss = (() => { + switch (postAppearanceType) { + case "compact": + return undefined; + case "large": + return thickBorderCss; + } + })(); + + const renderItem = useCallback( (item: PostCommentItem) => { if (isPost(item)) return ( - + + ); + + return ; + }, + [communityName, borderCss] + ); + + const renderItemContent = useCallback( + (item: PostCommentItem) => { + if (postAppearanceType === "compact") + return ( + <> + {renderItem(item)} + + ); - return ; + return renderItem(item); }, - [communityName] + [postAppearanceType, renderItem] ); const fetchFn: FetchFn = useCallback( diff --git a/src/features/labels/links/shared.ts b/src/features/labels/links/shared.ts index ac0f73661d..bcac93ca37 100644 --- a/src/features/labels/links/shared.ts +++ b/src/features/labels/links/shared.ts @@ -5,4 +5,5 @@ export const StyledLink = styled(Link)` text-decoration: none; color: inherit; font-weight: 500; + white-space: nowrap; `; diff --git a/src/features/post/inFeed/Post.tsx b/src/features/post/inFeed/Post.tsx index 23262d2669..f9b627ad83 100644 --- a/src/features/post/inFeed/Post.tsx +++ b/src/features/post/inFeed/Post.tsx @@ -1,24 +1,12 @@ -import styled from "@emotion/styled"; -import { IonItem } from "@ionic/react"; import { PostView } from "lemmy-js-client"; -import { megaphone } from "ionicons/icons"; -import PreviewStats from "./PreviewStats"; -import Embed from "../shared/Embed"; -import { useEffect, useMemo, useState } from "react"; -import { css } from "@emotion/react"; -import { findLoneImage } from "../../../helpers/markdown"; -import { getHandle, isUrlImage, isUrlVideo } from "../../../helpers/lemmy"; -import { maxWidthCss } from "../../shared/AppContent"; -import Nsfw, { isNsfw } from "../../labels/Nsfw"; -import { VoteButton } from "../shared/VoteButton"; +import LargePost from "./large/LargePost"; +import { useAppSelector } from "../../../store"; +import CompactPost from "./compact/CompactPost"; import SlidingVote from "../../shared/sliding/SlidingPostVote"; -import MoreActions from "../shared/MoreActions"; +import { IonItem } from "@ionic/react"; +import styled from "@emotion/styled"; import { useBuildGeneralBrowseLink } from "../../../helpers/routes"; -import PersonLink from "../../labels/links/PersonLink"; -import InlineMarkdown from "../../shared/InlineMarkdown"; -import { AnnouncementIcon } from "../detail/PostDetail"; -import CommunityLink from "../../labels/links/CommunityLink"; -import Video from "../../shared/Video"; +import { getHandle } from "../../../helpers/lemmy"; const CustomIonItem = styled(IonItem)` --padding-start: 0; @@ -29,77 +17,7 @@ const CustomIonItem = styled(IonItem)` --background-hover: none; `; -const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; - gap: 0.75rem; - padding: 0.75rem; - - ${maxWidthCss} -`; - -const Details = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - - font-size: 0.8em; - color: var(--ion-color-medium); -`; - -const LeftDetails = styled.div` - display: flex; - flex-direction: column; - gap: 0.5rem; - - min-width: 0; -`; - -const RightDetails = styled.div` - display: flex; - align-items: center; - font-size: 1.5rem; - - > * { - padding: 0.5rem; - } -`; - -const CommunityName = styled.span` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const PostBody = styled.div` - font-size: 0.88em; - line-height: 1.25; - opacity: 0.6; - - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; -`; - -const ImageContainer = styled.div` - overflow: hidden; - margin: 0 -1rem; -`; - -const PostImage = styled.img<{ blur: boolean }>` - width: 100%; - max-width: none; - - ${({ blur }) => - blur && - css` - filter: blur(40px); - `} -`; - -interface PostProps { +export interface PostProps { post: PostView; /** @@ -110,136 +28,32 @@ interface PostProps { className?: string; } -export default function Post({ post, communityMode, className }: PostProps) { +export default function Post(props: PostProps) { const buildGeneralBrowseLink = useBuildGeneralBrowseLink(); - const markdownLoneImage = useMemo( - () => (post.post.body ? findLoneImage(post.post.body) : undefined), - [post] + const postAppearanceType = useAppSelector( + (state) => state.appearance.posts.type ); - const [blur, setBlur] = useState(isNsfw(post)); - - useEffect(() => { - setBlur(isNsfw(post)); - }, [post]); - function renderPostBody() { - if (post.post.url) { - if (isUrlImage(post.post.url)) { - return ( - - { - if (isNsfw(post)) { - e.stopPropagation(); - setBlur(!blur); - } - }} - /> - - ); - } - if (isUrlVideo(post.post.url)) { - return ( - - - ); - } + const postBody = (() => { + switch (postAppearanceType) { + case "large": + return ; + case "compact": + return ; } - - if (markdownLoneImage) - return ( - - { - if (isNsfw(post)) { - e.stopPropagation(); - setBlur(!blur); - } - }} - /> - - ); - - if (post.post.thumbnail_url && post.post.url) { - return ; - } - - if (post.post.body) { - return ( - <> - {post.post.url && } - - - {post.post.body} - - - ); - } - - if (post.post.url) { - return ; - } - } + })(); return ( - + {/* href=undefined: Prevent drag failure on firefox */} - -
- {post.post.name}{" "} - {isNsfw(post) && } -
- - {renderPostBody()} - -
- - - {post.counts.featured_community || - post.counts.featured_local ? ( - - ) : undefined} - {communityMode ? ( - - ) : ( - - )} - - - - - e.stopPropagation()}> - - - - -
-
+ {postBody}
); diff --git a/src/features/post/inFeed/compact/CompactPost.tsx b/src/features/post/inFeed/compact/CompactPost.tsx new file mode 100644 index 0000000000..189cce5866 --- /dev/null +++ b/src/features/post/inFeed/compact/CompactPost.tsx @@ -0,0 +1,94 @@ +import styled from "@emotion/styled"; +import { PostProps } from "../Post"; +import Thumbnail from "./Thumbnail"; +import { maxWidthCss } from "../../../shared/AppContent"; +import PreviewStats from "../PreviewStats"; +import MoreActions from "../../shared/MoreActions"; +import PersonLink from "../../../labels/links/PersonLink"; +import CommunityLink from "../../../labels/links/CommunityLink"; +import { VoteButton } from "../../shared/VoteButton"; + +const Container = styled.div` + display: flex; + align-items: flex-start; + padding: 12px; + gap: 12px; + line-height: 1.15; + + ${maxWidthCss} +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + gap: 0.5em; +`; + +const Aside = styled.div` + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + + color: var(--ion-color-medium); + font-size: 0.8em; +`; + +const Actions = styled.div` + display: flex; + align-items: center; + gap: 0.5em; + + white-space: nowrap; +`; + +const StyledMoreActions = styled(MoreActions)` + font-size: 1.3rem; + + margin: -0.5rem; + padding: 0.5rem; +`; + +const EndDetails = styled.div` + display: flex; + flex-direction: column; + font-size: 1.2rem; + + color: var(--ion-color-medium); + + margin-left: auto; +`; + +export default function CompactPost({ post, communityMode }: PostProps) { + return ( + + + + {post.post.name} + + + + + + + + ); +} diff --git a/src/features/post/inFeed/compact/Thumbnail.tsx b/src/features/post/inFeed/compact/Thumbnail.tsx new file mode 100644 index 0000000000..2dbcf68b5d --- /dev/null +++ b/src/features/post/inFeed/compact/Thumbnail.tsx @@ -0,0 +1,66 @@ +import styled from "@emotion/styled"; +import { ReactComponent as SelfSvg } from "./self.svg"; +import { PostView } from "lemmy-js-client"; +import Img from "../../detail/Img"; +import { isUrlImage } from "../../../../helpers/lemmy"; +import { useMemo } from "react"; +import { findLoneImage } from "../../../../helpers/markdown"; +import { css } from "@emotion/react"; +import { isNsfw } from "../../../labels/Nsfw"; + +const Container = styled.div` + display: flex; + align-items: center; + justify-content: center; + + flex: 0 0 auto; + + width: max(11%, 60px); + aspect-ratio: 1; + background: var(--ion-color-light); + border-radius: 8px; + + overflow: hidden; + + svg { + width: 60%; + opacity: 0.5; + } +`; + +const StyledImg = styled(Img)<{ blur: boolean }>` + width: 100%; + height: 100%; + object-fit: cover; + + ${({ blur }) => + blur && + css` + filter: blur(6px); + `} +`; + +interface ImgProps { + post: PostView; +} + +export default function Thumbnail({ post }: ImgProps) { + const markdownLoneImage = useMemo( + () => (post.post.body ? findLoneImage(post.post.body) : undefined), + [post] + ); + + const src = (() => { + if (post.post.url && isUrlImage(post.post.url)) return post.post.url; + + if (markdownLoneImage) return markdownLoneImage.url; + + post.post.thumbnail_url; + })(); + + return ( + src && e.stopPropagation()}> + {src ? : } + + ); +} diff --git a/src/features/post/inFeed/compact/self.svg b/src/features/post/inFeed/compact/self.svg new file mode 100644 index 0000000000..4456b3adbe --- /dev/null +++ b/src/features/post/inFeed/compact/self.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/features/post/inFeed/large/LargePost.tsx b/src/features/post/inFeed/large/LargePost.tsx new file mode 100644 index 0000000000..b72f74b8bb --- /dev/null +++ b/src/features/post/inFeed/large/LargePost.tsx @@ -0,0 +1,210 @@ +import styled from "@emotion/styled"; +import { megaphone } from "ionicons/icons"; +import PreviewStats from "../PreviewStats"; +import Embed from "../../shared/Embed"; +import { useEffect, useMemo, useState } from "react"; +import { css } from "@emotion/react"; +import { findLoneImage } from "../../../../helpers/markdown"; +import { isUrlImage, isUrlVideo } from "../../../../helpers/lemmy"; +import { maxWidthCss } from "../../../shared/AppContent"; +import Nsfw, { isNsfw } from "../../../labels/Nsfw"; +import { VoteButton } from "../../shared/VoteButton"; +import MoreActions from "../../shared/MoreActions"; +import PersonLink from "../../../labels/links/PersonLink"; +import InlineMarkdown from "../../../shared/InlineMarkdown"; +import { AnnouncementIcon } from "../../detail/PostDetail"; +import CommunityLink from "../../../labels/links/CommunityLink"; +import Video from "../../../shared/Video"; +import { PostProps } from "../Post"; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + gap: 0.75rem; + padding: 0.75rem; + + ${maxWidthCss} +`; + +const Details = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + font-size: 0.8em; + color: var(--ion-color-medium); +`; + +const LeftDetails = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + + min-width: 0; +`; + +const RightDetails = styled.div` + display: flex; + align-items: center; + font-size: 1.5rem; + + > * { + padding: 0.5rem; + } +`; + +const CommunityName = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const PostBody = styled.div` + font-size: 0.88em; + line-height: 1.25; + opacity: 0.6; + + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +`; + +const ImageContainer = styled.div` + overflow: hidden; + margin: 0 -1rem; +`; + +const PostImage = styled.img<{ blur: boolean }>` + width: 100%; + max-width: none; + + ${({ blur }) => + blur && + css` + filter: blur(40px); + `} +`; + +export default function LargePost({ post, communityMode }: PostProps) { + const markdownLoneImage = useMemo( + () => (post.post.body ? findLoneImage(post.post.body) : undefined), + [post] + ); + const [blur, setBlur] = useState(isNsfw(post)); + + useEffect(() => { + setBlur(isNsfw(post)); + }, [post]); + + function renderPostBody() { + if (post.post.url) { + if (isUrlImage(post.post.url)) { + return ( + + { + if (isNsfw(post)) { + e.stopPropagation(); + setBlur(!blur); + } + }} + /> + + ); + } + if (isUrlVideo(post.post.url)) { + return ( + + + ); + } + } + + if (markdownLoneImage) + return ( + + { + if (isNsfw(post)) { + e.stopPropagation(); + setBlur(!blur); + } + }} + /> + + ); + + if (post.post.thumbnail_url && post.post.url) { + return ; + } + + if (post.post.body) { + return ( + <> + {post.post.url && } + + + {post.post.body} + + + ); + } + + if (post.post.url) { + return ; + } + } + + return ( + +
+ {post.post.name}{" "} + {isNsfw(post) && } +
+ + {renderPostBody()} + +
+ + + {post.counts.featured_community || post.counts.featured_local ? ( + + ) : undefined} + {communityMode ? ( + + ) : ( + + )} + + + + + e.stopPropagation()}> + + + + +
+
+ ); +} diff --git a/src/features/post/shared/MoreActions.tsx b/src/features/post/shared/MoreActions.tsx index bddabedc67..a081064cf1 100644 --- a/src/features/post/shared/MoreActions.tsx +++ b/src/features/post/shared/MoreActions.tsx @@ -27,9 +27,10 @@ import { jwtSelector } from "../../auth/authSlice"; interface MoreActionsProps { post: PostView; + className?: string; } -export default function MoreActions({ post }: MoreActionsProps) { +export default function MoreActions({ post, className }: MoreActionsProps) { const buildGeneralBrowseLink = useBuildGeneralBrowseLink(); const dispatch = useAppDispatch(); const [open, setOpen] = useState(false); @@ -53,11 +54,19 @@ export default function MoreActions({ post }: MoreActionsProps) { return ( <> - setOpen(true)} /> + { + e.stopPropagation(); + setOpen(true); + }} + /> e.stopPropagation()} buttons={[ { text: myVote !== 1 ? "Upvote" : "Undo Upvote", diff --git a/src/features/post/shared/VoteButton.tsx b/src/features/post/shared/VoteButton.tsx index 9339774eee..994c7821a0 100644 --- a/src/features/post/shared/VoteButton.tsx +++ b/src/features/post/shared/VoteButton.tsx @@ -73,7 +73,9 @@ export function VoteButton({ type, postId }: VoteButtonProps) { return ( { + onClick={async (e) => { + e.stopPropagation(); + if (!jwt) return login({ presentingElement: pageContext.page }); try { diff --git a/src/features/settings/appearance/PostView.tsx b/src/features/settings/appearance/PostView.tsx new file mode 100644 index 0000000000..a769e026e1 --- /dev/null +++ b/src/features/settings/appearance/PostView.tsx @@ -0,0 +1,27 @@ +import { IonLabel, IonList } from "@ionic/react"; +import { ListHeader } from "./TextSize"; +import { InsetIonItem } from "../../user/Profile"; +import { useAppSelector } from "../../../store"; +import { startCase } from "lodash"; + +export default function PostView() { + const postsAppearanceType = useAppSelector( + (state) => state.appearance.posts.type + ); + + return ( + <> + + Posts + + + + Post Size + + {startCase(postsAppearanceType)} + + + + + ); +} diff --git a/src/features/settings/appearance/TextSize.tsx b/src/features/settings/appearance/TextSize.tsx index c4c2d2be3f..5003b98c3a 100644 --- a/src/features/settings/appearance/TextSize.tsx +++ b/src/features/settings/appearance/TextSize.tsx @@ -5,7 +5,7 @@ import { InsetIonItem } from "../../../pages/profile/ProfileFeedItemsPage"; import { useAppDispatch, useAppSelector } from "../../../store"; import { setFontSizeMultiplier, setUseSystemFontSize } from "./appearanceSlice"; -const ListHeader = styled.div` +export const ListHeader = styled.div` font-size: 0.8em; margin: 32px 0 -8px 32px; text-transform: uppercase; diff --git a/src/features/settings/appearance/appearanceSlice.tsx b/src/features/settings/appearance/appearanceSlice.tsx index 6f785716e0..925b066c0d 100644 --- a/src/features/settings/appearance/appearanceSlice.tsx +++ b/src/features/settings/appearance/appearanceSlice.tsx @@ -7,13 +7,27 @@ const STORAGE_KEYS = { FONT_SIZE_MULTIPLIER: "appearance--font-size-multiplier", USE_SYSTEM: "appearance--font-use-system", }, + POSTS: { + TYPE: "appearance--post-type", + }, +} as const; + +export const OPostAppearanceType = { + Compact: "compact", + Large: "large", } as const; +export type PostAppearanceType = + (typeof OPostAppearanceType)[keyof typeof OPostAppearanceType]; + interface AppearanceState { font: { fontSizeMultiplier: number; useSystemFontSize: boolean; }; + posts: { + type: PostAppearanceType; + }; } const initialState: AppearanceState = { @@ -21,6 +35,9 @@ const initialState: AppearanceState = { fontSizeMultiplier: 1, useSystemFontSize: false, }, + posts: { + type: "large", + }, }; const stateFromStorage: AppearanceState = merge(initialState, { @@ -28,6 +45,9 @@ const stateFromStorage: AppearanceState = merge(initialState, { fontSizeMultiplier: get(STORAGE_KEYS.FONT.FONT_SIZE_MULTIPLIER), useSystemFontSize: get(STORAGE_KEYS.FONT.USE_SYSTEM), }, + posts: { + type: get(STORAGE_KEYS.POSTS.TYPE), + }, }); export const appearanceSlice = createSlice({ @@ -44,12 +64,20 @@ export const appearanceSlice = createSlice({ set(STORAGE_KEYS.FONT.USE_SYSTEM, action.payload); }, + setPostAppearance(state, action: PayloadAction) { + state.posts.type = action.payload; + + set(STORAGE_KEYS.POSTS.TYPE, action.payload); + }, resetAppearance: () => initialState, }, }); -export const { setFontSizeMultiplier, setUseSystemFontSize } = - appearanceSlice.actions; +export const { + setFontSizeMultiplier, + setUseSystemFontSize, + setPostAppearance, +} = appearanceSlice.actions; export default appearanceSlice.reducer; diff --git a/src/features/settings/appearance/posts/PostsViewSelection.tsx b/src/features/settings/appearance/posts/PostsViewSelection.tsx new file mode 100644 index 0000000000..df6befb505 --- /dev/null +++ b/src/features/settings/appearance/posts/PostsViewSelection.tsx @@ -0,0 +1,29 @@ +import { IonList, IonRadio, IonRadioGroup } from "@ionic/react"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { InsetIonItem } from "../../../user/Profile"; +import { OPostAppearanceType, setPostAppearance } from "../appearanceSlice"; + +export default function PostsViewSelection() { + const dispatch = useAppDispatch(); + const postAppearanceType = useAppSelector( + (state) => state.appearance.posts.type + ); + + return ( + { + dispatch(setPostAppearance(e.target.value)); + }} + > + + + Large + + + Compact + + + + ); +} diff --git a/src/pages/settings/AppearancePage.tsx b/src/pages/settings/AppearancePage.tsx index ed1b233721..64b517b17c 100644 --- a/src/pages/settings/AppearancePage.tsx +++ b/src/pages/settings/AppearancePage.tsx @@ -8,6 +8,7 @@ import { } from "@ionic/react"; import AppContent from "../../features/shared/AppContent"; import TextSize from "../../features/settings/appearance/TextSize"; +import PostView from "../../features/settings/appearance/PostView"; export default function AppearancePage() { return ( @@ -23,6 +24,7 @@ export default function AppearancePage() { + ); diff --git a/src/pages/settings/PostAppearancePage.tsx b/src/pages/settings/PostAppearancePage.tsx new file mode 100644 index 0000000000..5482124cdb --- /dev/null +++ b/src/pages/settings/PostAppearancePage.tsx @@ -0,0 +1,32 @@ +import { + IonBackButton, + IonButtons, + IonHeader, + IonPage, + IonTitle, + IonToolbar, +} from "@ionic/react"; +import AppContent from "../../features/shared/AppContent"; +import PostsViewSelection from "../../features/settings/appearance/posts/PostsViewSelection"; + +export default function PostAppearancePage() { + return ( + + + + + + + + Posts + + + + + + + ); +}