Skip to content

Commit

Permalink
Allow showing downvotes separately or hiding scores (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsammelson authored Jul 20, 2023
1 parent b97088d commit 55e9fe8
Show file tree
Hide file tree
Showing 24 changed files with 358 additions and 257 deletions.
60 changes: 51 additions & 9 deletions src/features/gallery/GalleryPostActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { useAppSelector } from "../../store";
import { useBuildGeneralBrowseLink } from "../../helpers/routes";
import { getHandle } from "../../helpers/lemmy";
import MoreActions from "../post/shared/MoreActions";
import { calculateCurrentVotesCount } from "../../helpers/vote";
import {
calculateTotalScore,
calculateSeparateScore,
} from "../../helpers/vote";
import { useLocation } from "react-router";
import { useContext } from "react";
import React, { useContext } from "react";
import { GalleryContext } from "./GalleryProvider";
import { OVoteDisplayMode } from "../../services/db";

const Container = styled.div`
display: flex;
Expand All @@ -37,13 +41,11 @@ interface GalleryPostActionsProps {
}

export default function GalleryPostActions({ post }: GalleryPostActionsProps) {
const postVotesById = useAppSelector((state) => state.post.postVotesById);
const buildGeneralBrowseLink = useBuildGeneralBrowseLink();
const link = buildGeneralBrowseLink(
`/c/${getHandle(post.community)}/comments/${post.post.id}`
);
const router = useIonRouter();
const score = calculateCurrentVotesCount(post, postVotesById);
const location = useLocation();
const { close } = useContext(GalleryContext);

Expand All @@ -53,11 +55,7 @@ export default function GalleryPostActions({ post }: GalleryPostActionsProps) {

return (
<Container onClick={(e) => e.stopPropagation()}>
<Section>
<VoteButton type="up" postId={post.post.id} />
<Amount>{score}</Amount>
<VoteButton type="down" postId={post.post.id} />
</Section>
<Voting post={post} />
<div
onClick={() => {
close();
Expand All @@ -77,3 +75,47 @@ export default function GalleryPostActions({ post }: GalleryPostActionsProps) {
</Container>
);
}

function Voting({ post }: GalleryPostActionsProps): React.ReactElement {
const postVotesById = useAppSelector((state) => state.post.postVotesById);

const voteDisplayMode = useAppSelector(
(state) => state.settings.appearance.voting.voteDisplayMode
);

switch (voteDisplayMode) {
case OVoteDisplayMode.Total: {
const score = calculateTotalScore(post, postVotesById);

return (
<Section>
<VoteButton type="up" postId={post.post.id} />
<Amount>{score}</Amount>
<VoteButton type="down" postId={post.post.id} />
</Section>
);
}
case OVoteDisplayMode.Separate: {
const { upvotes, downvotes } = calculateSeparateScore(
post,
postVotesById
);

return (
<Section>
<VoteButton type="up" postId={post.post.id} />
<Amount>{upvotes}</Amount>
<VoteButton type="down" postId={post.post.id} />
<Amount>{downvotes}</Amount>
</Section>
);
}
case OVoteDisplayMode.Hide:
return (
<Section>
<VoteButton type="up" postId={post.post.id} />
<VoteButton type="down" postId={post.post.id} />
</Section>
);
}
}
130 changes: 93 additions & 37 deletions src/features/labels/Vote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,44 @@ import { IonIcon, useIonToast } from "@ionic/react";
import { arrowDownSharp, arrowUpSharp } from "ionicons/icons";
import styled from "@emotion/styled";
import { voteOnPost } from "../post/postSlice";
import { useContext } from "react";
import React, { useContext } from "react";
import { voteOnComment } from "../comment/commentSlice";
import { voteError } from "../../helpers/toastMessages";
import { PageContext } from "../auth/PageContext";
import { calculateCurrentVotesCount } from "../../helpers/vote";
import {
calculateTotalScore,
calculateSeparateScore,
} from "../../helpers/vote";
import { CommentView, PostView } from "lemmy-js-client";
import { OVoteDisplayMode } from "../../services/db";

const Container = styled.div<{ vote: 1 | -1 | 0 | undefined }>`
const Container = styled.div<{
vote?: 1 | -1 | 0;
voteRepresented?: 1 | -1;
}>`
display: flex;
align-items: center;
gap: 0.25rem;
&& {
color: ${({ vote }) => {
switch (vote) {
case 1:
return "var(--ion-color-primary)";
case -1:
return "var(--ion-color-danger)";
color: ${({ vote, voteRepresented }) => {
if (voteRepresented === undefined || vote === voteRepresented) {
switch (vote) {
case 1:
return "var(--ion-color-primary)";
case -1:
return "var(--ion-color-danger)";
}
}
}};
}
`;

interface VoteProps {
item: PostView | CommentView;
className?: string;
}

export default function Vote({ item, className }: VoteProps) {
export default function Vote({ item }: VoteProps): React.ReactElement {
const [present] = useIonToast();
const dispatch = useAppDispatch();
const votesById = useAppSelector((state) =>
Expand All @@ -42,38 +50,86 @@ export default function Vote({ item, className }: VoteProps) {
);
const id = "comment" in item ? item.comment.id : item.post.id;

const myVote = votesById[id] ?? item.my_vote;
const score = calculateCurrentVotesCount(item, votesById);
const myVote = votesById[id] ?? (item.my_vote as -1 | 0 | 1 | undefined) ?? 0;

const { presentLoginIfNeeded } = useContext(PageContext);

return (
<Container
className={className}
vote={myVote as 1 | 0 | -1}
onClick={async (e) => {
e.stopPropagation();
e.preventDefault();
async function onVote(e: React.MouseEvent, vote: 0 | 1 | -1) {
e.stopPropagation();
e.preventDefault();

if (presentLoginIfNeeded()) return;
if (presentLoginIfNeeded()) return;

let dispatcherFn;
if ("comment" in item) {
dispatcherFn = voteOnComment;
} else {
dispatcherFn = voteOnPost;
}
let dispatcherFn;
if ("comment" in item) {
dispatcherFn = voteOnComment;
} else {
dispatcherFn = voteOnPost;
}

try {
await dispatch(dispatcherFn(id, myVote ? 0 : 1));
} catch (error) {
present(voteError);
try {
await dispatch(dispatcherFn(id, vote));
} catch (error) {
present(voteError);
throw error;
}
}

throw error;
}
}}
>
<IonIcon icon={myVote === -1 ? arrowDownSharp : arrowUpSharp} /> {score}
</Container>
const voteDisplayMode = useAppSelector(
(state) => state.settings.appearance.voting.voteDisplayMode
);

switch (voteDisplayMode) {
case OVoteDisplayMode.Separate: {
const { upvotes, downvotes } = calculateSeparateScore(item, votesById);
return (
<>
<Container
vote={myVote}
voteRepresented={1}
onClick={async (e) => {
await onVote(e, myVote === 1 ? 0 : 1);
}}
>
<IonIcon icon={arrowUpSharp} /> {upvotes}
</Container>
<Container
vote={myVote}
voteRepresented={-1}
onClick={async (e) => {
await onVote(e, myVote === -1 ? 0 : -1);
}}
>
<IonIcon icon={arrowDownSharp} /> {downvotes}
</Container>
</>
);
}
case OVoteDisplayMode.Hide:
return (
<Container
vote={myVote}
onClick={async (e) => {
await onVote(e, myVote ? 0 : 1);
}}
>
<IonIcon icon={myVote === -1 ? arrowDownSharp : arrowUpSharp} />
</Container>
);
// Total score
default: {
const score = calculateTotalScore(item, votesById);
return (
<Container
vote={myVote}
onClick={async (e) => {
await onVote(e, myVote ? 0 : 1);
}}
>
<IonIcon icon={myVote === -1 ? arrowDownSharp : arrowUpSharp} />{" "}
{score}
</Container>
);
}
}
}
2 changes: 2 additions & 0 deletions src/features/settings/appearance/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Posts from "./posts/Posts";
import System from "./system/System";
import CompactSettings from "./CompactSettings";
import GeneralAppearance from "./General";
import Votes from "./Votes";

export default function AppearanceSettings() {
return (
Expand All @@ -11,6 +12,7 @@ export default function AppearanceSettings() {
<GeneralAppearance />
<Posts />
<CompactSettings />
<Votes />
<System />
</>
);
Expand Down
66 changes: 13 additions & 53 deletions src/features/settings/appearance/CompactSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,39 @@
import type { IonActionSheetCustomEvent } from "@ionic/core";
import {
ActionSheetButton,
IonLabel,
IonList,
IonActionSheet,
IonToggle,
} from "@ionic/react";
import { ListHeader } from "./TextSize";
import { IonLabel, IonList, IonToggle } from "@ionic/react";
import { InsetIonItem } from "../../user/Profile";
import { useAppSelector, useAppDispatch } from "../../../store";
import { useState } from "react";
import { startCase } from "lodash";
import { setShowVotingButtons, setThumbnailPosition } from "../settingsSlice";
import {
OCompactThumbnailPositionType,
CompactThumbnailPositionType,
} from "../../../services/db";
import { OverlayEventDetail } from "@ionic/react/dist/types/components/react-component-lib/interfaces";

const BUTTONS: ActionSheetButton<CompactThumbnailPositionType>[] =
Object.values(OCompactThumbnailPositionType).map(function (
compactThumbnailPositionType
) {
return {
text: startCase(compactThumbnailPositionType),
data: compactThumbnailPositionType,
} as ActionSheetButton<CompactThumbnailPositionType>;
});
import { ListHeader } from "../shared/formatting";
import SettingSelector from "../shared/SettingSelector";

export default function CompactSettings() {
const [open, setOpen] = useState(false);

const dispatch = useAppDispatch();
const compactThumbnailsPositionType = useAppSelector(
const compactThumbnailsPosition = useAppSelector(
(state) => state.settings.appearance.compact.thumbnailsPosition
);

const compactShowVotingButtons = useAppSelector(
(state) => state.settings.appearance.compact.showVotingButtons
);

const ThumbnailPositionSelector =
SettingSelector<CompactThumbnailPositionType>;

return (
<>
<ListHeader>
<IonLabel>Compact Posts</IonLabel>
</ListHeader>
<IonList inset>
<InsetIonItem button onClick={() => setOpen(true)}>
<IonLabel>Thumbnail Position</IonLabel>
<IonLabel slot="end" color="medium">
{startCase(compactThumbnailsPositionType)}
</IonLabel>
<IonActionSheet
cssClass="left-align-buttons"
isOpen={open}
onDidDismiss={() => setOpen(false)}
onWillDismiss={(
e: IonActionSheetCustomEvent<
OverlayEventDetail<CompactThumbnailPositionType>
>
) => {
if (e.detail.data) {
dispatch(setThumbnailPosition(e.detail.data));
}
}}
header="Position"
buttons={BUTTONS.map((b) => ({
...b,
role:
compactThumbnailsPositionType === b.data
? "selected"
: undefined,
}))}
/>
</InsetIonItem>
<ThumbnailPositionSelector
title="Thumbnail Position"
selected={compactThumbnailsPosition}
setSelected={setThumbnailPosition}
options={OCompactThumbnailPositionType}
/>
<InsetIonItem>
<IonLabel>Show Voting Buttons</IonLabel>
<IonToggle
Expand Down
9 changes: 1 addition & 8 deletions src/features/settings/appearance/General.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import styled from "@emotion/styled";
import { IonLabel, IonList, IonToggle } from "@ionic/react";
import { InsetIonItem } from "../../../pages/profile/ProfileFeedItemsPage";
import { useAppDispatch, useAppSelector } from "../../../store";
import { setUserInstanceUrlDisplay } from "../settingsSlice";
import { OInstanceUrlDisplayMode } from "../../../services/db";

export const ListHeader = styled.div`
font-size: 0.8em;
margin: 32px 0 -8px 32px;
text-transform: uppercase;
color: var(--ion-color-medium);
`;
import { ListHeader } from "../shared/formatting";

export default function GeneralAppearance() {
const dispatch = useAppDispatch();
Expand Down
Loading

0 comments on commit 55e9fe8

Please sign in to comment.