diff --git a/src/app/ItemTradesPage.js b/src/app/ItemTradesPage.js index c55807d..2499490 100644 --- a/src/app/ItemTradesPage.js +++ b/src/app/ItemTradesPage.js @@ -1,18 +1,13 @@ import React from "react"; import { css } from "emotion"; -import { - Box, - Skeleton, - Tooltip, - useColorModeValue, - useToken, -} from "@chakra-ui/core"; +import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/core"; import gql from "graphql-tag"; import { useQuery } from "@apollo/client"; import { Link, useHistory, useParams } from "react-router-dom"; import { Heading2, usePageTitle } from "./util"; import ItemPageLayout from "./ItemPageLayout"; +import useCurrentUser from "./components/useCurrentUser"; export function ItemTradesOfferingPage() { return ( @@ -30,6 +25,10 @@ export function ItemTradesOfferingPage() { id username lastTradeActivity + matchingItems: itemsTheyWantThatCurrentUserOwns { + id + name + } } closetList { id @@ -59,6 +58,10 @@ export function ItemTradesSeekingPage() { id username lastTradeActivity + matchingItems: itemsTheyOwnThatCurrentUserWants { + id + name + } } closetList { id @@ -124,20 +127,12 @@ function ItemTradesTable({ compareListHeading, tradesQuery, }) { + const { isLoggedIn } = useCurrentUser(); const { loading, error, data } = useQuery(tradesQuery, { variables: { itemId }, }); - // HACK: I'm pretty much hiding this for now, because it's not ready. But - /// it's visible at #show-compare-column! - const shouldShowCompareColumn = window.location.href.includes( - "show-compare-column" - ); - - const minorColumnWidth = { - base: shouldShowCompareColumn ? "23%" : "30%", - md: "18ex", - }; + const shouldShowCompareColumn = isLoggedIn; // We partially randomize trade sorting, but we want it to stay stable across // re-renders. To do this, we can use `getTradeSortKey`, which will either @@ -148,7 +143,10 @@ function ItemTradesTable({ if (!tradeSortKeys.has(trade.id)) { tradeSortKeys.set( trade.id, - getVaguelyRandomizedSortKeyForDate(trade.user.lastTradeActivity) + getVaguelyRandomizedTradeSortKey( + trade.user.lastTradeActivity, + trade.user.matchingItems.length + ) ); } return tradeSortKeys.get(trade.id); @@ -161,6 +159,11 @@ function ItemTradesTable({ return {error.message}; } + const minorColumnWidth = { + base: shouldShowCompareColumn ? "23%" : "30%", + md: "20ex", + }; + return ( Last active Last edit + {shouldShowCompareColumn && ( + + + Potential trades + + Matches + + )} {userHeading} - {shouldShowCompareColumn && ( - - Compare - - )} List @@ -221,6 +227,7 @@ function ItemTradesTable({ username={trade.user.username} listName={trade.closetList.name} lastTradeActivity={trade.user.lastTradeActivity} + matchingItems={trade.user.matchingItems} shouldShowCompareColumn={shouldShowCompareColumn} /> ))} @@ -246,12 +253,17 @@ function ItemTradesTableRow({ username, listName, lastTradeActivity, + matchingItems, shouldShowCompareColumn, }) { const history = useHistory(); const onClick = React.useCallback(() => history.push(href), [history, href]); const focusBackground = useColorModeValue("gray.100", "gray.600"); + const sortedMatchingItems = [...matchingItems].sort((a, b) => + a.name.localeCompare(b.name) + ); + return ( {formatVagueDate(lastTradeActivity)} - - {username} - {shouldShowCompareColumn && ( - - {compareListHeading}: - - - Adorable Freckles - - - Constellation Dress + {matchingItems.length > 0 ? ( + + {sortedMatchingItems.slice(0, 4).map((item) => ( + + + {item.name} - (WIP: This is placeholder data!) - - } - > - - 2 match - 2 matches + ))} + {matchingItems.length > 4 && ( + + {matchingItems.length - 4} more + )} - + ) : ( + <> + No matches + None + + )} )} - + {username} + = 1; // "This week" sorts after all other dates, but with a random factor! I don't // want people worrying about gaming themselves up to the very top, just be // active and trust the system 😅 (I figure that, if you care enough to "game" // the system by faking activity every week, you probably also care enough to // be... making real trades every week lmao) + // + // We also prioritize having matches, but we don't bother to sort _how many_ + // matches, to decrease the power of gaming with large honeypot lists, and + // because it's hard to judge how good matches are anyway. if (isThisWeek(date)) { - return `ZZZthisweekZZZ-${Math.random()}`; + const matchingItemsKey = hasMatchingItems + ? "ZZmatchingZZ" + : "AAnotmatchingAA"; + return `ZZZthisweekZZZ-${matchingItemsKey}-${Math.random()}`; } return dateString; diff --git a/src/server/loaders.js b/src/server/loaders.js index 67878c3..afcc117 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -116,6 +116,85 @@ const buildSpeciesTranslationLoader = (db) => ); }); +const buildTradeMatchesLoader = (db) => + new DataLoader( + async (userPairs) => { + const conditions = userPairs + .map( + (_) => + `(public_user_hangers.user_id = ? AND current_user_hangers.user_id = ? AND public_user_hangers.owned = ? AND current_user_hangers.owned = ?)` + ) + .join(" OR "); + const conditionValues = userPairs + .map(({ publicUserId, currentUserId, direction }) => { + if (direction === "public-owns-current-wants") { + return [publicUserId, currentUserId, true, false]; + } else if (direction === "public-wants-current-owns") { + return [publicUserId, currentUserId, false, true]; + } else { + throw new Error( + `unexpected user pair direction: ${JSON.stringify(direction)}` + ); + } + }) + .flat(); + + const [rows, _] = await db.execute( + ` + SELECT + public_user_hangers.user_id AS public_user_id, + current_user_hangers.user_id AS current_user_id, + IF( + public_user_hangers.owned, + "public-owns-current-wants", + "public-wants-current-owns" + ) AS direction, + GROUP_CONCAT(public_user_hangers.item_id) AS item_ids + FROM closet_hangers AS public_user_hangers + INNER JOIN users AS public_users ON public_users.id = public_user_hangers.user_id + LEFT JOIN closet_lists AS public_user_lists + ON public_user_lists.id = public_user_hangers.list_id + INNER JOIN closet_hangers AS current_user_hangers + ON public_user_hangers.item_id = current_user_hangers.item_id + WHERE ( + (${conditions}) + AND ( + -- For the public user (but not the current), the hanger must be + -- marked Trading. + (public_user_hangers.list_id IS NOT NULL AND public_user_lists.visibility >= 2) + OR ( + public_user_hangers.list_id IS NULL AND public_user_hangers.owned = 1 + AND public_users.owned_closet_hangers_visibility >= 2 + ) + OR ( + public_user_hangers.list_id IS NULL AND public_user_hangers.owned = 0 + AND public_users.wanted_closet_hangers_visibility >= 2 + ) + ) + ) + GROUP BY public_user_id, current_user_id; + `, + conditionValues + ); + + const entities = rows.map(normalizeRow); + + return userPairs.map(({ publicUserId, currentUserId, direction }) => { + const entity = entities.find( + (e) => + e.publicUserId === publicUserId && + e.currentUserId === currentUserId && + e.direction === direction + ); + return entity ? entity.itemIds.split(",") : []; + }); + }, + { + cacheKeyFn: ({ publicUserId, currentUserId, direction }) => + `${publicUserId}-${currentUserId}-${direction}`, + } + ); + const loadAllPetTypes = (db) => async () => { const [rows, _] = await db.execute( `SELECT species_id, color_id FROM pet_types` @@ -1071,6 +1150,7 @@ function buildLoaders(db) { ); loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); + loaders.tradeMatchesLoader = buildTradeMatchesLoader(db); loaders.userLoader = buildUserLoader(db); loaders.userByNameLoader = buildUserByNameLoader(db); loaders.userByEmailLoader = buildUserByEmailLoader(db); diff --git a/src/server/types/User.js b/src/server/types/User.js index 9372cda..da761cc 100644 --- a/src/server/types/User.js +++ b/src/server/types/User.js @@ -11,6 +11,11 @@ const typeDefs = gql` itemsTheyOwn: [Item!]! itemsTheyWant: [Item!]! + # These items represent potential trade matches. We use this as a preview + # in the trade list table. + itemsTheyOwnThatCurrentUserWants: [Item!]! + itemsTheyWantThatCurrentUserOwns: [Item!]! + # When this user last updated any of their trade lists, as an ISO 8601 # timestamp. lastTradeActivity: String! @@ -119,6 +124,39 @@ const resolvers = { return items; }, + itemsTheyOwnThatCurrentUserWants: async ( + { id: publicUserId }, + { currentUserId }, + { tradeMatchesLoader } + ) => { + if (!currentUserId) { + return []; + } + + const itemIds = await tradeMatchesLoader.load({ + publicUserId, + currentUserId, + direction: "public-owns-current-wants", + }); + return itemIds.map((id) => ({ id })); + }, + itemsTheyWantThatCurrentUserOwns: async ( + { id: publicUserId }, + _, + { currentUserId, tradeMatchesLoader } + ) => { + if (!currentUserId) { + return []; + } + + const itemIds = await tradeMatchesLoader.load({ + publicUserId, + currentUserId, + direction: "public-wants-current-owns", + }); + return itemIds.map((id) => ({ id })); + }, + closetLists: async ( { id }, _,