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 },
_,