diff --git a/packages/frontend/src/api/hooks/useActivityLogger.ts b/packages/frontend/src/api/hooks/useActivityLogger.ts index 8aefb78a..b0dadc60 100644 --- a/packages/frontend/src/api/hooks/useActivityLogger.ts +++ b/packages/frontend/src/api/hooks/useActivityLogger.ts @@ -3,7 +3,11 @@ import { EActivityLogEventTypes, EActivityLogObjectTypes, } from "src/api/services/activity-log/types"; -import { ECookieChoice, EWalletConnectionMethod } from "src/api/types"; +import { + ECookieChoice, + EWalletConnectionMethod, + TSuperfeedItem, +} from "src/api/types"; import { Logger } from "../utils/logging"; interface IActivityLogger { @@ -11,6 +15,7 @@ interface IActivityLogger { logViewVisited: (id: number) => void; logKeywordSelected: (id: number) => void; logWalletConnection: (method: EWalletConnectionMethod) => void; + logShareSuperfeedItem: (item: TSuperfeedItem) => void; } export const useActivityLogger = (): IActivityLogger => { @@ -103,10 +108,37 @@ export const useActivityLogger = (): IActivityLogger => { ); }; + const logShareSuperfeedItem = (item: TSuperfeedItem) => { + sendActivityLog({ + event_type: EActivityLogEventTypes.ShareSuperfeedItem, + object_type: EActivityLogObjectTypes.ShareSuperfeedItem, + object_id: item.id, + data: { + title: item.title, + text: item.shortDescription ?? item.title, + url: item.url, + }, + }) + .unwrap() + .then((resp) => + Logger.debug( + "useActivityLogger::logShareSuperfeedItem: updated share superfeed item activity log", + resp + ) + ) + .catch((err) => + Logger.error( + "useActivityLogger::logShareSuperfeedItem: error updating share superfeed item activity log", + err + ) + ); + }; + return { logViewVisited, logCookieChoice, logKeywordSelected, logWalletConnection, + logShareSuperfeedItem, }; }; diff --git a/packages/frontend/src/api/services/activity-log/types.ts b/packages/frontend/src/api/services/activity-log/types.ts index 8121ea17..d45159d7 100644 --- a/packages/frontend/src/api/services/activity-log/types.ts +++ b/packages/frontend/src/api/services/activity-log/types.ts @@ -4,6 +4,7 @@ export enum EActivityLogEventTypes { KeywordSelected = "KEYWORD_SELECTED", CookieChoiceSet = "COOKIE_CHOICE_SET", WalletConnection = "WALLET_CONNECT", + ShareSuperfeedItem = "SHARE_ITEM", } export enum EActivityLogObjectTypes { @@ -11,6 +12,7 @@ export enum EActivityLogObjectTypes { Widget, View, WalletConnection, + ShareSuperfeedItem, } export type TKeywordActivityLog = { @@ -45,12 +47,20 @@ export type TWalletConnectionActivityLog = { data?: JSONValue; }; +export type TShareSuperfeedItemActivityLog = { + event_type: EActivityLogEventTypes.ShareSuperfeedItem; + object_type: EActivityLogObjectTypes.ShareSuperfeedItem; + object_id: number; + data: JSONValue; +}; + export type TRemoteActivityLog = | TKeywordActivityLog | TWidgetActivityLog | TViewActivityLog | TCookieActivityLog - | TWalletConnectionActivityLog; + | TWalletConnectionActivityLog + | TShareSuperfeedItemActivityLog; export type TActivityLogRequest = TRemoteActivityLog; export type TActivityLogResponse = TRemoteActivityLog; diff --git a/packages/frontend/src/api/utils/shareUtils.test.ts b/packages/frontend/src/api/utils/shareUtils.test.ts new file mode 100644 index 00000000..87bffd77 --- /dev/null +++ b/packages/frontend/src/api/utils/shareUtils.test.ts @@ -0,0 +1,33 @@ +import { canShare, shareData } from "./shareUtils"; + +describe("shareUtils", () => { + describe("canShare", () => { + it("should return true if the Web Share API is supported", () => { + expect(canShare()).toBe(true); + }); + + it("should return false if the Web Share API is not supported", () => { + // @ts-expect-error navigator is read-only, we're just testing + navigator.share = undefined; + expect(canShare()).toBe(false); + }); + }); + + describe("shareData", () => { + it("should share the provided data using the Web Share API", () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(shareData({ title: "Test", text: "Test" })).resolves.toBe( + undefined + ); + }); + + it("should not share the data if the Web Share API is not supported", () => { + // @ts-expect-error navigator is read-only, we're just testing + navigator.share = undefined; + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(shareData({ title: "Test", text: "Test" })).rejects.toThrow( + "Sharing not supported" + ); + }); + }); +}); diff --git a/packages/frontend/src/api/utils/shareUtils.ts b/packages/frontend/src/api/utils/shareUtils.ts new file mode 100644 index 00000000..471e8fc3 --- /dev/null +++ b/packages/frontend/src/api/utils/shareUtils.ts @@ -0,0 +1,20 @@ +/** + * Checks if the current environment supports the Web Share API. + * + * @returns Returns true if the Web Share API is supported, otherwise false. + */ +export function canShare() { + return navigator.share !== undefined && navigator.canShare !== undefined; +} + +/** + * Shares the provided data using the Web Share API if it is supported by the browser. + * + * @param data - The data to be shared. + */ +export function shareData(data: ShareData) { + if (canShare() && navigator.canShare(data)) { + return navigator.share(data); + } + return Promise.reject(new Error("Sharing not supported")); +} diff --git a/packages/frontend/src/mobile-components/Superfeed.tsx b/packages/frontend/src/mobile-components/Superfeed.tsx index c58a051f..07b2f84e 100644 --- a/packages/frontend/src/mobile-components/Superfeed.tsx +++ b/packages/frontend/src/mobile-components/Superfeed.tsx @@ -12,6 +12,7 @@ interface ISuperfeedModule { feed: TSuperfeedItem[] | undefined; handlePaginate: (type: "next" | "previous") => void; toggleShowFeedFilters: () => void; + onShareItem: (item: TSuperfeedItem) => Promise; selectedPodcast: TSuperfeedItem | null; setSelectedPodcast: React.Dispatch< React.SetStateAction @@ -63,6 +64,7 @@ const SuperfeedModule: FC = ({ toggleShowFeedFilters, selectedPodcast, setSelectedPodcast, + onShareItem, }) => { const handleScrollEvent = useCallback( ({ currentTarget }: FormEvent) => { @@ -85,6 +87,8 @@ const SuperfeedModule: FC = ({ item={item} selectedPodcast={selectedPodcast} setSelectedPodcast={setSelectedPodcast} + onLike={() => {}} + onShare={() => onShareItem(item)} /> ))} diff --git a/packages/frontend/src/mobile-components/feed/BlogCard.tsx b/packages/frontend/src/mobile-components/feed/BlogCard.tsx index 4267d9a5..daa61e48 100644 --- a/packages/frontend/src/mobile-components/feed/BlogCard.tsx +++ b/packages/frontend/src/mobile-components/feed/BlogCard.tsx @@ -14,7 +14,9 @@ import { export const BlogCard: FC<{ item: TSuperfeedItem; -}> = ({ item }) => { + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -27,7 +29,6 @@ export const BlogCard: FC<{ shortDescription, date, } = item; - const onLike = () => {}; const isLiked = false; return ( @@ -72,7 +73,7 @@ export const BlogCard: FC<{ { ); }; -export const EventCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { +export const EventCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -56,7 +60,6 @@ export const EventCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { const location = "location"; const category = "category"; - const onLike = () => {}; const isLiked = false; return ( @@ -116,7 +119,7 @@ export const EventCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { >; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; } export const FeedCard: FC = ({ item, selectedPodcast, setSelectedPodcast, + onLike, + onShare, }) => { switch (item.type) { case EFeedItemType.NEWS: - return ; + return ; case EFeedItemType.BLOG: - return ; + return ; case EFeedItemType.FORUM: - return ; + return ; case EFeedItemType.PERSON: - return ; + return ; case EFeedItemType.VIDEO: - return ; + return ; case EFeedItemType.PODCAST: return ( ); case EFeedItemType.EVENT: - return ; + return ; case EFeedItemType.MEME: - return ; + return ; case EFeedItemType.IMAGE: - return ; + return ; case EFeedItemType.REDDIT: - return ; + return ; case EFeedItemType.DISCORD: - return ; + return ; case EFeedItemType.MARKET: - return ; + return ; case EFeedItemType.TVL: - return ; + return ; default: return null; } diff --git a/packages/frontend/src/mobile-components/feed/FeedElements.tsx b/packages/frontend/src/mobile-components/feed/FeedElements.tsx index 7a474f3d..2cc21454 100644 --- a/packages/frontend/src/mobile-components/feed/FeedElements.tsx +++ b/packages/frontend/src/mobile-components/feed/FeedElements.tsx @@ -17,9 +17,9 @@ export const getFeedItemIcon = (type: EFeedItemType, isDown?: boolean) => { }; export const ActionButtons: FC<{ - onLike: () => void; - onCommentClick: () => void; - onShare: () => void; + onLike: () => MaybeAsync; + onCommentClick: () => MaybeAsync; + onShare: () => MaybeAsync; likes: number; comments: number; isLiked: boolean; diff --git a/packages/frontend/src/mobile-components/feed/ImageCard.tsx b/packages/frontend/src/mobile-components/feed/ImageCard.tsx index 20cf505e..c9dc5647 100644 --- a/packages/frontend/src/mobile-components/feed/ImageCard.tsx +++ b/packages/frontend/src/mobile-components/feed/ImageCard.tsx @@ -13,7 +13,11 @@ import { getFeedItemIcon, } from "./FeedElements"; -export const ImageCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { +export const ImageCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -26,7 +30,6 @@ export const ImageCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { shortDescription, } = item; - const onLike = () => {}; const isLiked = false; return ( @@ -80,7 +83,7 @@ export const ImageCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { { return parsedHistory; }; -export const MarketCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { +export const MarketCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const isTVL = item.type === EFeedItemType.TVL; const { @@ -37,7 +41,6 @@ export const MarketCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { data: coinData, } = item; - const onLike = () => {}; const isLiked = false; const isDown = shortDescription?.includes("down"); @@ -133,7 +136,7 @@ export const MarketCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { = ({ item }) => { +export const NewsCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -29,7 +33,6 @@ export const NewsCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { date, } = item; - const onLike = () => {}; const isLiked = false; return ( @@ -80,7 +83,7 @@ export const NewsCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { >; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; } const PlayPauseButton: FC<{ @@ -63,6 +65,8 @@ export const PodcastCard: FC = ({ item, selectedPodcast, setSelectedPodcast, + onLike, + onShare, }) => { const { title, @@ -112,8 +116,6 @@ export const PodcastCard: FC = ({ setSelectedPodcast(item); togglePlayPause(); }; - - const onLike = () => {}; const isLiked = false; return ( @@ -203,7 +205,7 @@ export const PodcastCard: FC = ({ = ({ = ({ item }) => { +export const SocialCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -30,7 +34,6 @@ export const SocialCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { date, } = item; - const onLike = () => {}; const isLiked = false; return ( @@ -91,7 +94,7 @@ export const SocialCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { ); -export const VideoCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { +export const VideoCard: FC<{ + item: TSuperfeedItem; + onLike: () => MaybeAsync; + onShare: () => MaybeAsync; +}> = ({ item, onLike, onShare }) => { const { title, tags, @@ -48,7 +52,6 @@ export const VideoCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { shortDescription, } = item; - const onLike = () => {}; const isLiked = false; /** @@ -98,7 +101,7 @@ export const VideoCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { {open ? null : ( {}} /> )} @@ -116,7 +119,7 @@ export const VideoCard: FC<{ item: TSuperfeedItem }> = ({ item }) => { = ({ item }) => { ([]); @@ -92,6 +97,28 @@ const SuperfeedContainer: FC<{ setfeedData((prevState) => [...prevState, ...feedDataForCurrentPage]); prevFeedDataResponseRef.current = feedDataResponse?.results; } + + const shareItem = useCallback( + async (item: TSuperfeedItem) => { + try { + await shareData({ + title: item.title, + text: item.shortDescription, + url: item.url, + }); + + // Log the share + logShareSuperfeedItem(item); + } catch (e) { + Logger.error( + "SuperfeedModule::FeedCard: error sharing item", + e + ); + toast("Error sharing item"); + } + }, + [logShareSuperfeedItem] + ); // set current page 350ms after next page is set. // RTK should cache requests, so we don't need to be too careful about rerenders. useEffect(() => { @@ -157,6 +184,7 @@ const SuperfeedContainer: FC<{ toggleShowFeedFilters={onToggleFeedFilters} selectedPodcast={selectedPodcast} setSelectedPodcast={setSelectedPodcast} + onShareItem={shareItem} /> ); diff --git a/packages/ui-kit/src/mobile-components/button/buttons.tsx b/packages/ui-kit/src/mobile-components/button/buttons.tsx index 00ea5fb6..25ba65ce 100644 --- a/packages/ui-kit/src/mobile-components/button/buttons.tsx +++ b/packages/ui-kit/src/mobile-components/button/buttons.tsx @@ -2,15 +2,16 @@ import { FC, ReactNode } from "react"; import { Link } from "react-router-dom"; import { twMerge } from "tailwind-merge"; -export const ActionButton: FC<{ children: ReactNode; onClick: () => void }> = ({ - onClick, - children, -}) => { +export const ActionButton: FC<{ + children: ReactNode; + onClick: () => MaybeAsync; +}> = ({ onClick, children }) => { const handleClick = ( e: React.MouseEvent ) => { e.preventDefault(); e.stopPropagation(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises onClick(); }; return (