Skip to content

Commit 8a4015d

Browse files
authored
Merge pull request #332 from allending313/fix/persist-sort
Fix: persist listing sort order between page navigations
2 parents e7864d0 + da78e1b commit 8a4015d

File tree

1 file changed

+127
-39
lines changed

1 file changed

+127
-39
lines changed

src/lib/components/market/sort_listings.ts

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {getMarketInspectLink} from './helpers';
88
import {ItemInfo} from '../../bridge/handlers/fetch_inspect_info';
99
import {getFadeParams, getFadePercentage} from '../../utils/skin';
1010
import {AppId, ContextId} from '../../types/steam_constants';
11+
import {debounce} from 'lodash-decorators';
12+
import {DebouncedFunc} from 'lodash';
1113

1214
enum SortType {
1315
FLOAT = 'Float',
@@ -20,13 +22,31 @@ enum SortDirection {
2022
DESC,
2123
}
2224

25+
// Union type for fetched item info: successful and failed.
26+
type SortableItem =
27+
| {
28+
failed: false;
29+
listingId: string;
30+
info: ItemInfo;
31+
converted_price: number;
32+
fadePercentage: number;
33+
}
34+
| {
35+
failed: true;
36+
listingId: string;
37+
};
38+
39+
type SuccessfulSortableItem = Extract<SortableItem, {failed: false}>;
40+
2341
@CustomElement()
2442
export class SortListings extends FloatElement {
2543
@state()
2644
private type: SortType = SortType.FLOAT;
2745
@state()
2846
private direction: SortDirection = SortDirection.NONE;
2947

48+
private observer: MutationObserver | null = null;
49+
3050
@state()
3151
get isFadeSkin() {
3252
const firstRow = document.querySelector('#searchResultsRows .market_listing_row.market_recent_listing_row');
@@ -42,6 +62,113 @@ export class SortListings extends FloatElement {
4262
return getFadeParams(asset) !== undefined;
4363
}
4464

65+
connectedCallback() {
66+
super.connectedCallback();
67+
68+
// Find the container of listings that we need to watch.
69+
const targetNode = document.getElementById('searchResultsRows');
70+
if (!targetNode) return;
71+
72+
// Create a MutationObserver to detect when the page's items are dynamically replaced.
73+
this.observer = new MutationObserver(() => this.onMutation());
74+
75+
// Start observing the target node for additions or removals of child elements.
76+
this.observer.observe(targetNode, {childList: true});
77+
}
78+
79+
disconnectedCallback() {
80+
super.disconnectedCallback();
81+
if (this.observer) {
82+
this.observer.disconnect();
83+
}
84+
85+
// Workaround to avoid using @ts-ignore:
86+
// type assertion to inform ts about the .cancel() added from the lodash debounce
87+
(this.onMutation as DebouncedFunc<() => void>).cancel();
88+
}
89+
90+
/**
91+
* This decorated method is called when the item list changes.
92+
* The @debounce decorator ensures it only runs once after a series of rapid changes.
93+
*/
94+
@debounce(500)
95+
private onMutation() {
96+
// Only re-sort if a sort is currently active.
97+
if (this.direction === SortDirection.NONE) return;
98+
99+
const targetNode = document.getElementById('searchResultsRows');
100+
101+
// Disconnect the observer temporarily to prevent sortListings() from causing this mutation
102+
// handler to re-trigger, causing a loop.
103+
this.observer?.disconnect();
104+
105+
this.sortListings(this.type, this.direction)
106+
.catch((err) => console.error('CSFloat: Failed to re-sort list', err))
107+
.finally(() => {
108+
// Reconnect the observer to watch for the next page change.
109+
if (targetNode) {
110+
this.observer?.observe(targetNode, {childList: true});
111+
}
112+
});
113+
}
114+
115+
private async sortListings(sortType: SortType, direction: SortDirection) {
116+
const rows = document.querySelectorAll('#searchResultsRows .market_listing_row.market_recent_listing_row');
117+
if (rows.length === 0) return;
118+
119+
const infoPromises = [...rows]
120+
.map((e) => e.id.replace('listing_', ''))
121+
.map(async (listingId): Promise<SortableItem> => {
122+
// Catch error to prevent one failure from stopping the Promise.all() later
123+
try {
124+
const link = getMarketInspectLink(listingId);
125+
const info = await gFloatFetcher.fetch({link: link!});
126+
const listingInfo = g_rgListingInfo[listingId];
127+
const asset = g_rgAssets[AppId.CSGO][ContextId.PRIMARY][listingInfo.asset.id];
128+
return {
129+
failed: false,
130+
info,
131+
listingId: listingId!,
132+
converted_price: listingInfo?.converted_price || 0,
133+
fadePercentage: (asset && getFadePercentage(asset, info)?.percentage) || 0,
134+
};
135+
} catch (error) {
136+
console.error(`CSFloat: Failed to fetch float for listing ${listingId}:`, error);
137+
return {failed: true, listingId: listingId!};
138+
}
139+
});
140+
141+
const infos = await Promise.all(infoPromises);
142+
143+
// Type Guard that checks if an item was successfully fetched.
144+
function isSuccessfulItem(item: SortableItem): item is SuccessfulSortableItem {
145+
return !item.failed;
146+
}
147+
148+
const successfulItems = infos.filter(isSuccessfulItem);
149+
const failedItems = infos.filter((r) => r.failed);
150+
const sortedInfos = [...SortListings.sort(successfulItems, sortType, direction), ...failedItems];
151+
152+
let lastItem = document.querySelector('#searchResultsRows .market_listing_table_header');
153+
154+
for (const info of sortedInfos) {
155+
const itemElement = document.querySelector(`#listing_${info.listingId}`);
156+
if (itemElement && itemElement.parentNode && lastItem) {
157+
lastItem = itemElement.parentNode.insertBefore(itemElement, lastItem.nextSibling);
158+
}
159+
}
160+
}
161+
162+
async onClick(sortType: SortType) {
163+
const newDirection =
164+
sortType === this.type ? SortListings.getNextSortDirection(this.direction) : SortDirection.ASC;
165+
166+
await this.sortListings(sortType, newDirection);
167+
168+
this.type = sortType;
169+
this.direction = newDirection;
170+
}
171+
45172
computeButtonText(sortType: SortType): string {
46173
let txt = `Sort by ${sortType}`;
47174

@@ -107,43 +234,4 @@ export class SortListings extends FloatElement {
107234
);
108235
}
109236
}
110-
111-
async onClick(sortType: SortType) {
112-
const newDirection =
113-
sortType == this.type ? SortListings.getNextSortDirection(this.direction) : SortDirection.ASC;
114-
115-
const rows = document.querySelectorAll('#searchResultsRows .market_listing_row.market_recent_listing_row');
116-
117-
const infoPromises = [...rows]
118-
.map((e) => e.id.replace('listing_', ''))
119-
.map(async (listingId) => {
120-
const link = getMarketInspectLink(listingId);
121-
122-
const info = await gFloatFetcher.fetch({link: link!});
123-
124-
const listingInfo = g_rgListingInfo[listingId];
125-
126-
const asset = g_rgAssets[AppId.CSGO][ContextId.PRIMARY][listingInfo.asset.id];
127-
128-
return {
129-
info,
130-
listingId: listingId!,
131-
converted_price: listingInfo?.converted_price || 0,
132-
fadePercentage: (asset && getFadePercentage(asset, info)?.percentage) || 0,
133-
};
134-
});
135-
136-
const infos = await Promise.all(infoPromises);
137-
const sortedInfos = SortListings.sort(infos, sortType, newDirection);
138-
139-
let lastItem = document.querySelector('#searchResultsRows .market_listing_table_header');
140-
141-
for (const info of sortedInfos) {
142-
const itemElement = document.querySelector(`#listing_${info.listingId}`);
143-
lastItem = itemElement!.parentNode!.insertBefore(itemElement!, lastItem!.nextSibling);
144-
}
145-
146-
this.type = sortType;
147-
this.direction = newDirection;
148-
}
149237
}

0 commit comments

Comments
 (0)