Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better tipping data display #4779

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 14 additions & 4 deletions apps/api/src/db/seeds/seedAllowedTokens.ts
@@ -1,3 +1,5 @@
import { IS_MAINNET } from '@hey/data/constants';

import { prisma } from '../seed';

const seedAllowedTokens = async (): Promise<number> => {
Expand All @@ -11,7 +13,9 @@ const seedAllowedTokens = async (): Promise<number> => {
symbol: 'USDT'
},
{
contractAddress: '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
contractAddress: IS_MAINNET
? '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063'
: '0x001B3B4d0F3714Ca98ba10F6042DaEbF0B1B7b6F',
createdAt: new Date('2023-12-03 07:33:27.293'),
decimals: 18,
name: 'DAI Stablecoin',
Expand All @@ -25,21 +29,27 @@ const seedAllowedTokens = async (): Promise<number> => {
symbol: 'pointless'
},
{
contractAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
contractAddress: IS_MAINNET
? '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
createdAt: new Date('2023-12-03 07:31:55.117'),
decimals: 6,
name: 'USD Coin',
symbol: 'USDC'
},
{
contractAddress: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
contractAddress: IS_MAINNET
? '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270'
: '0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889',
createdAt: new Date('2024-10-03 07:30:03.438'),
decimals: 18,
name: 'Wrapped Matic',
symbol: 'WMATIC'
},
{
contractAddress: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619',
contractAddress: IS_MAINNET
? '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619'
: '0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa',
createdAt: new Date('2023-12-03 07:34:34.354'),
decimals: 18,
name: 'Wrapped Ether',
Expand Down
11 changes: 9 additions & 2 deletions apps/web/src/components/Publication/Collectors.tsx
Expand Up @@ -5,7 +5,11 @@ import ProfileListShimmer from '@components/Shared/Shimmer/ProfileListShimmer';
import UserProfile from '@components/Shared/UserProfile';
import { ArrowLeftIcon, RectangleStackIcon } from '@heroicons/react/24/outline';
import { ProfileLinkSource } from '@hey/data/tracking';
import { LimitType, useWhoActedOnPublicationQuery } from '@hey/lens';
import {
LimitType,
OpenActionCategoryType,
useWhoActedOnPublicationQuery
} from '@hey/lens';
import { Card, EmptyState, ErrorMessage } from '@hey/ui';
import Link from 'next/link';
import { Virtuoso } from 'react-virtuoso';
Expand All @@ -21,7 +25,10 @@ const Collectors: FC<CollectorsProps> = ({ publicationId }) => {
// Variables
const request: WhoActedOnPublicationRequest = {
limit: LimitType.TwentyFive,
on: publicationId
on: publicationId,
where: {
anyOf: [{ category: OpenActionCategoryType.Collect }]
}
};

const { data, error, fetchMore, loading } = useWhoActedOnPublicationQuery({
Expand Down
@@ -1,14 +1,18 @@
import type {
AnyPublication,
UnknownOpenActionModuleSettings
} from '@hey/lens';
import type { FC } from 'react';

import { PUBLICATION } from '@hey/data/tracking';
import { VerifiedOpenActionModules } from '@hey/data/verified-openaction-modules';
import { TipIcon } from '@hey/icons';
import { TipIconSolid } from '@hey/icons/src/TipIconSolid';
import {
type AnyPublication,
type UnknownOpenActionModuleSettings,
useUnknownOpenActionDataQuery
} from '@hey/lens';
import nFormatter from '@hey/lib/nFormatter';
import { isMirrorPublication } from '@hey/lib/publicationHelpers';
import { Modal, Tooltip } from '@hey/ui';
import cn from '@hey/ui/cn';
import { Leafwatch } from '@lib/leafwatch';
import { motion } from 'framer-motion';
import { useState } from 'react';
Expand All @@ -29,6 +33,13 @@ const TipOpenAction: FC<TipOpenActionProps> = ({
? publication?.mirrorOn
: publication;

const { data: tipData } = useUnknownOpenActionDataQuery({
variables: {
module: VerifiedOpenActionModules.Tip,
pubId: targetPublication.id
}
});

Comment on lines +36 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this is interesting, just found out that it gonna rate limit users. When someone opens a feed and keeps scrolling, it gonna makes 1 extra API call per post. This need to be fixed by somehow using GraphQL alias

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to switch this PR to a draft since the current approach is not feasible due to rate limiting and partial alias support in graphql-codegen. (more here)

const module = targetPublication.openActionModules.find(
(module) => module.contract.address === VerifiedOpenActionModules.Tip
);
Expand All @@ -37,13 +48,30 @@ const TipOpenAction: FC<TipOpenActionProps> = ({
return null;
}

const hasTipped =
(tipData?.publication &&
'operations' in tipData.publication &&
Boolean(tipData.publication?.operations.actedOn.length)) ||
false;

const countTips =
(tipData?.publication &&
'operations' in tipData.publication &&
tipData.publication.stats.countOpenActions) ||
0;

const iconClassName = isFullPublication
? 'w-[17px] sm:w-[20px]'
: 'w-[15px] sm:w-[18px]';

return (
<>
<div className="ld-text-gray-500">
<div
className={cn(
hasTipped ? 'text-brand-500' : 'ld-text-gray-500',
'flex items-center space-x-1'
)}
>
<motion.button
aria-label="Tip"
className="rounded-full p-1.5 outline-offset-2 hover:bg-gray-300/20"
Expand All @@ -56,9 +84,18 @@ const TipOpenAction: FC<TipOpenActionProps> = ({
whileTap={{ scale: 0.9 }}
>
<Tooltip content="Tip" placement="top" withDelay>
<TipIcon className={iconClassName} />
{hasTipped ? (
<TipIconSolid className={iconClassName} />
) : (
<TipIcon className={iconClassName} />
)}
</Tooltip>
</motion.button>
{countTips > 0 && !isFullPublication ? (
<span className="text-[11px] sm:text-xs">
{nFormatter(countTips)}
</span>
) : null}
</div>
<Modal
icon={<TipIcon className="size-5" />}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/Publication/OpenAction/index.tsx
Expand Up @@ -46,7 +46,7 @@ const OpenAction: FC<OpenActionProps> = ({ publication, showCount }) => {
)}
>
<motion.button
aria-label="Action"
aria-label="Collect"
className={cn(
hasActed ? 'hover:bg-brand-300/20' : 'hover:bg-gray-300/20',
'rounded-full p-1.5 outline-offset-2'
Expand All @@ -61,7 +61,7 @@ const OpenAction: FC<OpenActionProps> = ({ publication, showCount }) => {
>
<Tooltip
content={`${humanize(countOpenActions)} ${plur(
'Action',
'Collect',
countOpenActions
)}`}
placement="top"
Expand Down
33 changes: 31 additions & 2 deletions apps/web/src/components/Publication/PublicationStats.tsx
@@ -1,6 +1,10 @@
import type { PublicationStats as IPublicationStats } from '@hey/lens';
import type { FC } from 'react';

import { VerifiedOpenActionModules } from '@hey/data/verified-openaction-modules';
import {
type PublicationStats as IPublicationStats,
useUnknownOpenActionDataQuery
} from '@hey/lens';
import getPublicationsViews from '@hey/lib/getPublicationsViews';
import nFormatter from '@hey/lib/nFormatter';
import Link from 'next/link';
Expand All @@ -18,6 +22,19 @@ const PublicationStats: FC<PublicationStatsProps> = ({
}) => {
const [views, setViews] = useState<number>(0);

const { data: tipData } = useUnknownOpenActionDataQuery({
variables: {
module: VerifiedOpenActionModules.Tip,
pubId: publicationId
}
});

const countTips =
(tipData?.publication &&
'operations' in tipData.publication &&
tipData.publication.stats.countOpenActions) ||
0;

useEffect(() => {
// Get Views
getPublicationsViews([publicationId]).then((viewsResponse) => {
Expand All @@ -35,7 +52,8 @@ const PublicationStats: FC<PublicationStatsProps> = ({
quotes > 0 ||
countOpenActions > 0 ||
bookmarks > 0 ||
views > 0;
views > 0 ||
countTips > 0;

if (!showStats) {
return null;
Expand Down Expand Up @@ -91,6 +109,17 @@ const PublicationStats: FC<PublicationStatsProps> = ({
{plur('Collect', countOpenActions)}
</Link>
) : null}
{countTips > 0 ? (
<Link
className="outline-offset-2"
href={`/posts/${publicationId}/tippers`}
>
<b className="text-black dark:text-white">
{nFormatter(countTips)}
</b>{' '}
{plur('Tip', countTips)}
</Link>
) : null}
{bookmarks > 0 ? (
<span>
<b className="text-black dark:text-white">
Expand Down
111 changes: 111 additions & 0 deletions apps/web/src/components/Publication/Tippers.tsx
@@ -0,0 +1,111 @@
import type { Profile, WhoActedOnPublicationRequest } from '@hey/lens';
import type { FC } from 'react';

import ProfileListShimmer from '@components/Shared/Shimmer/ProfileListShimmer';
import UserProfile from '@components/Shared/UserProfile';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
import { ProfileLinkSource } from '@hey/data/tracking';
import { VerifiedOpenActionModules } from '@hey/data/verified-openaction-modules';
import { TipIcon } from '@hey/icons';
import { LimitType, useWhoActedOnPublicationQuery } from '@hey/lens';
import { Card, EmptyState, ErrorMessage } from '@hey/ui';
import Link from 'next/link';
import { Virtuoso } from 'react-virtuoso';
import { useProfileStore } from 'src/store/persisted/useProfileStore';

interface TippersProps {
publicationId: string;
}

const Tippers: FC<TippersProps> = ({ publicationId }) => {
const { currentProfile } = useProfileStore();

// Variables
const request: WhoActedOnPublicationRequest = {
limit: LimitType.TwentyFive,
on: publicationId,
where: {
anyOf: [{ address: VerifiedOpenActionModules.Tip }]
}
};

const { data, error, fetchMore, loading } = useWhoActedOnPublicationQuery({
skip: !publicationId,
variables: { request }
});

const profiles = data?.whoActedOnPublication?.items;
const pageInfo = data?.whoActedOnPublication?.pageInfo;
const hasMore = pageInfo?.next;

const onEndReached = async () => {
if (!hasMore) {
return;
}

await fetchMore({
variables: { request: { ...request, cursor: pageInfo?.next } }
});
};

if (loading) {
return <ProfileListShimmer />;
}

if (profiles?.length === 0) {
return (
<div className="p-5">
<EmptyState
hideCard
icon={<TipIcon className="size-8" />}
message="No tippers."
/>
</div>
);
}

if (error) {
return (
<ErrorMessage
className="m-5"
error={error}
title="Failed to load tippers"
/>
);
}

return (
<Card>
<div className="flex items-center space-x-3 p-5">
<Link href={`/posts/${publicationId}`}>
<ArrowLeftIcon className="size-5" />
</Link>
<b className="text-lg">Tipped by</b>
</div>
<div className="divider" />
<Virtuoso
className="virtual-divider-list-window"
computeItemKey={(index, profile) => `${profile.id}-${index}`}
data={profiles}
endReached={onEndReached}
itemContent={(_, profile) => {
return (
<div className="p-5">
<UserProfile
hideFollowButton={currentProfile?.id === profile.id}
hideUnfollowButton={currentProfile?.id === profile.id}
profile={profile as Profile}
showBio
showUserPreview={false}
source={ProfileLinkSource.Tips}
/>
</div>
);
}}
useWindowScroll
/>
</Card>
);
};

export default Tippers;
6 changes: 5 additions & 1 deletion apps/web/src/components/Publication/index.tsx
Expand Up @@ -41,6 +41,7 @@ import OnchainMeta from './OnchainMeta';
import Quotes from './Quotes';
import RelevantPeople from './RelevantPeople';
import PublicationPageShimmer from './Shimmer';
import Tippers from './Tippers';

interface HiddenCommentFeedState {
setShowHiddenComments: (show: boolean) => void;
Expand Down Expand Up @@ -70,6 +71,7 @@ const ViewPublication: NextPage = () => {
const showMirrors = pathname === '/posts/[id]/mirrors';
const showLikes = pathname === '/posts/[id]/likes';
const showCollectors = pathname === '/posts/[id]/collectors';
const showTippers = pathname === '/posts/[id]/tippers';

useEffect(() => {
Leafwatch.track(PAGEVIEW, {
Expand Down Expand Up @@ -101,7 +103,7 @@ const ViewPublication: NextPage = () => {
if (!isReady || loading) {
return (
<PublicationPageShimmer
profileList={showMirrors || showLikes || showCollectors}
profileList={showMirrors || showLikes || showCollectors || showTippers}
publicationList={showQuotes}
/>
);
Expand Down Expand Up @@ -138,6 +140,8 @@ const ViewPublication: NextPage = () => {
<Mirrors publicationId={targetPublication.id} />
) : showCollectors ? (
<Collectors publicationId={targetPublication.id} />
) : showTippers ? (
<Tippers publicationId={targetPublication.id} />
) : (
<>
<Card>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/pages/posts/[id]/tippers.tsx
@@ -0,0 +1,3 @@
import ViewPublication from '@components/Publication';

export default ViewPublication;
1 change: 1 addition & 0 deletions packages/data/tracking.ts
Expand Up @@ -261,5 +261,6 @@ export enum ProfileLinkSource {
RelevantPeople = 'relevant-people',
Search = 'search',
StaffPicks = 'staff-picks',
Tips = 'tips',
WhoToFollow = 'who-to-follow'
}