diff --git a/public/tooldb-worker/index.js b/public/tooldb-worker/index.js index e4e478a..986faa1 100644 --- a/public/tooldb-worker/index.js +++ b/public/tooldb-worker/index.js @@ -333,7 +333,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.convertDbMatchToData = void 0; +exports.getMatchesData = exports.getMatchesDataLocal = exports.convertDbMatchToData = void 0; const getRankFilterVal_1 = __importDefault(require("./getRankFilterVal")); function convertDbMatchToData(match) { const { internalMatch } = match; @@ -375,7 +375,7 @@ function getLocalData(key) { }); }); } -function getMatchesData(matchesIds, uuid) { +function getMatchesDataLocal(msgId, matchesIds, uuid) { const promises = matchesIds.map((id) => { return getLocalData(id); }); @@ -383,12 +383,53 @@ function getMatchesData(matchesIds, uuid) { .then((matches) => matches.filter((m) => m).map((m) => convertDbMatchToData(m))) .then((data) => { self.postMessage({ - type: "MATCHES_DATA", - value: data.filter((m) => m.uuid === uuid), + type: `${msgId}_OK`, + value: data.filter((m) => (uuid ? m.uuid === uuid : true)), }); }); } -exports.default = getMatchesData; +exports.getMatchesDataLocal = getMatchesDataLocal; +function getMatchesData(msgId, matchesIds, uuid, updateCallback) { + const matchesIndex = [...new Set([...(matchesIds || [])])]; + let saved = 0; + let timeout = null; + let lastUpdate = new Date().getTime(); + function updateState() { + timeout = null; + if (updateCallback) { + updateCallback(matchesIndex.length, saved); + } + if (saved === matchesIndex.length && matchesIndex.length > 0) { + getMatchesDataLocal(msgId, matchesIndex, uuid); + } + } + function debounceUpdateState() { + if (timeout) + clearTimeout(timeout); + if (new Date().getTime() - lastUpdate > 1000) { + lastUpdate = new Date().getTime(); + updateState(); + } + timeout = setTimeout(updateState, 100); + } + // Fetch any match we dont have locally + matchesIndex.forEach((id) => { + self.toolDb.store.get(id, (err) => { + if (!err) { + saved += 1; + debounceUpdateState(); + } + else { + self.toolDb.getData(id, false, 2000).finally(() => { + saved += 1; + debounceUpdateState(); + }); + } + }); + }); + updateState(); +} +exports.getMatchesData = getMatchesData; },{"./getRankFilterVal":10}],10:[function(require,module,exports){ "use strict"; @@ -537,52 +578,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable no-restricted-globals */ -const getMatchesData_1 = __importDefault(require("./getMatchesData")); +const getMatchesData_1 = require("./getMatchesData"); const reduxAction_1 = __importDefault(require("./reduxAction")); -function handleMatchesIndex(matchesIndex) { - self.globalData.matchesIndex = [ - ...new Set([...self.globalData.matchesIndex, ...(matchesIndex || [])]), - ]; - let saved = 0; - let timeout = null; - let lastUpdate = new Date().getTime(); - function updateState() { - timeout = null; - (0, reduxAction_1.default)("SET_MATCHES_FETCH_STATE", { - total: self.globalData.matchesIndex.length, - saved, +function handleMatchesIndex(matchesIds) { + if (matchesIds) { + (0, reduxAction_1.default)("SET_MATCHES_INDEX", matchesIds); + (0, getMatchesData_1.getMatchesData)("MATCHES_DATA", matchesIds, self.globalData.currentUUID, (total, saved) => { + (0, reduxAction_1.default)("SET_MATCHES_FETCH_STATE", { + total, + saved, + }); }); - if (saved === self.globalData.matchesIndex.length && - self.globalData.matchesIndex.length > 0) { - (0, reduxAction_1.default)("SET_MATCHES_INDEX", self.globalData.matchesIndex); - (0, getMatchesData_1.default)(self.globalData.matchesIndex, self.globalData.currentUUID); - } } - function debounceUpdateState() { - if (timeout) - clearTimeout(timeout); - if (new Date().getTime() - lastUpdate > 1000) { - lastUpdate = new Date().getTime(); - updateState(); - } - timeout = setTimeout(updateState, 100); - } - // Fetch any match we dont have locally - self.globalData.matchesIndex.forEach((id) => { - self.toolDb.store.get(id, (err) => { - if (!err) { - saved += 1; - debounceUpdateState(); - } - else { - self.toolDb.getData(id, false, 2000).finally(() => { - saved += 1; - debounceUpdateState(); - }); - } - }); - }); - updateState(); } exports.default = handleMatchesIndex; @@ -800,7 +807,7 @@ const getConnectionData_1 = __importDefault(require("./getConnectionData")); const getCrdt_1 = __importDefault(require("./getCrdt")); const getData_1 = __importDefault(require("./getData")); const getDataLocal_1 = __importDefault(require("./getDataLocal")); -const getMatchesData_1 = __importDefault(require("./getMatchesData")); +const getMatchesData_1 = require("./getMatchesData"); const getSaveKeysJson_1 = __importDefault(require("./getSaveKeysJson")); const handleMatchesIndex_1 = __importDefault(require("./handleMatchesIndex")); const keysLogin_1 = __importDefault(require("./keysLogin")); @@ -815,13 +822,35 @@ const toolDb = new mtgatool_db_1.ToolDb({ server: false, }); toolDb.on("init", (key) => console.warn("ToolDb initialized!", key)); -constants_1.DEFAULT_PEERS.forEach((peer) => { - const networkModule = toolDb.network; - networkModule.findServer(peer); +toolDb.store.get("servers", (err, data) => { + let serversData = {}; + if (err) { + console.error("Error getting servers from cache:", err); + } + else if (data) { + try { + serversData = JSON.parse(data); + } + catch (_e) { + console.error("Error parsing servers from cache:", _e); + } + } + console.log("Got servers from cache:", serversData); + constants_1.DEFAULT_PEERS.forEach((peer) => { + const networkModule = toolDb.network; + if (serversData[peer]) { + networkModule.connectTo(serversData[peer]); + } + else { + networkModule.findServer(peer); + } + }); }); toolDb.onConnect = () => { + const networkModule = toolDb.network; console.warn("ToolDb connected!"); self.postMessage({ type: "CONNECTED" }); + toolDb.store.put("servers", JSON.stringify(networkModule.serverPeerData), () => console.log("Saved servers to cache", networkModule.serverPeerData)); }; self.toolDb = toolDb; self.globalData = { @@ -881,7 +910,7 @@ self.onmessage = (e) => { (0, exploreAggregation_1.beginDataQuery)(e.data.days, e.data.event); break; case "GET_MATCHES_DATA": - (0, getMatchesData_1.default)(e.data.matchesIndex, e.data.uuid); + (0, getMatchesData_1.getMatchesData)(e.data.id, e.data.matchesIndex, e.data.uuid); break; case "REFRESH_MATCHES": if (self.toolDb.user) { diff --git a/src/components/App.tsx b/src/components/App.tsx index e0f5b9d..6367a8c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -52,7 +52,7 @@ function App(props: AppProps) { const toolDbWorkerRef = useRef(null); useEffect(() => { - toolDbWorkerRef.current = new Worker("tooldb-worker/index.js", { + toolDbWorkerRef.current = new Worker("../tooldb-worker/index.js", { type: "module", }); window.toolDbWorker = toolDbWorkerRef.current; diff --git a/src/components/ContentWrapper.tsx b/src/components/ContentWrapper.tsx index c63697e..a3265e9 100644 --- a/src/components/ContentWrapper.tsx +++ b/src/components/ContentWrapper.tsx @@ -12,6 +12,7 @@ import { setDateOption, } from "../redux/slices/FilterSlice"; import { AppState } from "../redux/stores/rendererStore"; +import { getMatchesData } from "../toolDb/worker-wrapper"; import { CardsData } from "../types/collectionTypes"; import { defaultCardsData } from "../types/dbTypes"; import aggregateStats from "../utils/aggregateStats"; @@ -33,6 +34,7 @@ import HistoryStats from "./views/history/HistoryStats"; import ViewHistory from "./views/history/ViewHistory"; import ViewHome from "./views/home/ViewHome"; import ViewLiveMatch from "./views/livematch/ViewLiveMatch"; +import ViewUser from "./views/user/ViewUser"; import ViewWip from "./views/wip/ViewWip"; const views = { @@ -45,6 +47,7 @@ const views = { explore: ViewExplore, collection: ViewCollection, aggregator: ViewExploreAggregator, + user: ViewUser, }; function delay(transition: any, timeout: number): any { @@ -97,19 +100,21 @@ const ContentWrapper = (mainProps: ContentWrapperProps) => { const [matchesData, setMatchesData] = useState([]); useEffect(() => { + getMatchesData(matchesIndex, currentUUID).then((d) => { + if (d) { + setMatchesData(d); + } + }); + + // hacky hack to listen for the after login matches data message const listener = (e: any) => { const { type, value } = e.data; - if (type === `MATCHES_DATA`) { + if (type === `MATCHES_DATA_OK`) { setMatchesData(value); } }; if (window.toolDbWorker) { - window.toolDbWorker.postMessage({ - type: "GET_MATCHES_DATA", - matchesIndex, - uuid: currentUUID, - }); window.toolDbWorker.addEventListener("message", listener); } diff --git a/src/components/views/explore/ExploreEvent.tsx b/src/components/views/explore/ExploreEvent.tsx index 2b5a1e3..9b5c73a 100644 --- a/src/components/views/explore/ExploreEvent.tsx +++ b/src/components/views/explore/ExploreEvent.tsx @@ -45,6 +45,8 @@ export default function ExploreEvent(props: ExploreEventProps) { } }, [eventId]); + const username = usernames[eventData?.aggregator || ""]; + return (
-
{`${ - usernames[eventData.aggregator || ""] - }`}
+
{ + e.preventDefault(); + e.stopPropagation(); + history.push( + `/user/${encodeURIComponent(eventData?.aggregator || "")}` + ); + }} + >{`${username}`}
- - {`Pushed by ${usernames[data.aggregator]}`} + Pushed by + { + e.preventDefault(); + e.stopPropagation(); + history.push( + `/user/${encodeURIComponent(data.aggregator || "")}` + ); + }} + > + {usernames[data.aggregator]}
-
{cleanUsername(name || "-")}
+
history.push(`/user/${encodeURIComponent(pubKey)}`)} + > + {cleanUsername(name || "-")} +
{timeAgo(updated)}
@@ -63,6 +72,7 @@ function DrawLimitedRank(props: DbRankInfo) { updated, name, avatar, + pubKey, limitedClass, limitedLevel, limitedStep, @@ -70,6 +80,8 @@ function DrawLimitedRank(props: DbRankInfo) { limitedLeaderboardPlace, } = props; + const history = useHistory(); + const mythicRankTitle = limitedLeaderboardPlace == 0 ? ` ${(limitedPercentile || 0).toFixed(2)}%` @@ -84,7 +96,12 @@ function DrawLimitedRank(props: DbRankInfo) { }} />
-
{name || "-"}
+
history.push(`/user/${encodeURIComponent(pubKey)}`)} + > + {name || "-"} +
{timeAgo(updated)}
@@ -146,6 +163,7 @@ export default function BestRanksFeed() { finallyThen(fetchUsername(rankInfo.pubKey)).then((name) => { return { ...rankInfo, + pubKey: rankInfo.pubKey, avatar: avatar || DEFAULT_AVATAR, name: name || "", }; diff --git a/src/components/views/home/DbRankInfo.ts b/src/components/views/home/DbRankInfo.ts index 3c7e048..02ea49b 100644 --- a/src/components/views/home/DbRankInfo.ts +++ b/src/components/views/home/DbRankInfo.ts @@ -2,6 +2,7 @@ import { CombinedRankInfo } from "../../../background/onLabel/InEventGetCombined export default interface DbRankInfo extends CombinedRankInfo { uuid: string; + pubKey: string; name: string; avatar: string; updated: number; diff --git a/src/components/views/home/LiveFeedMatch.tsx b/src/components/views/home/LiveFeedMatch.tsx index 79409ba..326dea2 100644 --- a/src/components/views/home/LiveFeedMatch.tsx +++ b/src/components/views/home/LiveFeedMatch.tsx @@ -2,6 +2,7 @@ import _ from "lodash"; import { Colors, constants } from "mtgatool-shared"; import { useEffect } from "react"; import { useSelector } from "react-redux"; +import { useHistory } from "react-router-dom"; import useFetchAvatar from "../../../hooks/useFetchAvatar"; import useFetchUsername from "../../../hooks/useFetchUsername"; @@ -34,6 +35,7 @@ export default function LiveFeedMatch({ pubKey, }: ListItemMatchProps): JSX.Element { const { internalMatch } = match; + const history = useHistory(); const avatars = useSelector((state: AppState) => state.avatars.avatars); const usernames = useSelector((state: AppState) => state.usernames.usernames); @@ -74,6 +76,11 @@ export default function LiveFeedMatch({
{ + e.preventDefault(); + e.stopPropagation(); + history.push(`/user/${encodeURIComponent(pubKey)}`); + }} style={{ backgroundImage: `url(${avatar})`, }} diff --git a/src/components/views/home/ViewHome.tsx b/src/components/views/home/ViewHome.tsx index e75d88e..8c2c003 100644 --- a/src/components/views/home/ViewHome.tsx +++ b/src/components/views/home/ViewHome.tsx @@ -25,13 +25,14 @@ export default function ViewHome() {

Live Feed

{liveFeed.map((matchId) => { + const pubKey = matchId.slice(1, matchId.indexOf(".matches-")); const match = liveFeedMatches[matchId] || undefined; if (match) { const data = convertDbMatchToData(match); return ( ); diff --git a/src/components/views/user/UserHistoryList.tsx b/src/components/views/user/UserHistoryList.tsx new file mode 100644 index 0000000..7e0c3f9 --- /dev/null +++ b/src/components/views/user/UserHistoryList.tsx @@ -0,0 +1,103 @@ +/* eslint-disable react/jsx-props-no-spreading */ + +import { useCallback, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useHistory } from "react-router-dom"; + +import usePagingControls from "../../../hooks/usePagingControls"; +import { selectCurrentFilterDate } from "../../../redux/slices/FilterSlice"; +import { AppState } from "../../../redux/stores/rendererStore"; +import doHistoryFilter from "../../../utils/tables/doHistoryFilter"; +import PagingControls from "../../PagingControls"; +import SortControls, { Sort } from "../../SortControls"; +import { MatchData } from "../history/convertDbMatchData"; +import ListItemMatch from "../history/ListItemMatch"; + +interface UserHistoryListProps { + matchesData: MatchData[]; + pubKey: string; +} + +export default function UserHistoryList(props: UserHistoryListProps) { + const filters = useSelector( + (state: AppState) => state.filter.matchDataFilters + ); + const filterDate = useSelector(selectCurrentFilterDate); + const history = useHistory(); + const dispatch = useDispatch(); + const { matchesData, pubKey } = props; + + const [sortValue, setSortValue] = useState>({ + key: "timestamp", + sort: -1, + }); + + const filteredData = useMemo(() => { + if (filters) { + const filtered = doHistoryFilter(matchesData, filters, sortValue); + + return filtered; + } + return []; + }, [dispatch, matchesData, filters, sortValue, filterDate]); + + const pagingControlProps = usePagingControls(filteredData.length, 25); + + const openMatch = useCallback( + (match: MatchData) => { + history.push( + `/history/${encodeURIComponent(`:${pubKey}.matches-${match.matchId}`)}` + ); + }, + [pubKey, history] + ); + + return ( +
+ + setSortCallback={setSortValue} + defaultSort={sortValue} + columnKeys={[ + "timestamp", + "rank", + "duration", + "playerWins", + "playerLosses", + "eventId", + "oppDeckColors", + "playerDeckColors", + ]} + columnNames={[ + "Date", + "Rank", + "Duration", + "Wins", + "Losses", + "Event", + "Opponent Colors", + "Player Colors", + ]} + /> + {filteredData + .slice( + pagingControlProps.pageIndex * pagingControlProps.pageSize, + (pagingControlProps.pageIndex + 1) * pagingControlProps.pageSize + ) + .map((match) => { + return ( + + ); + })} +
+ +
+
+ ); +} diff --git a/src/components/views/user/UserRank.tsx b/src/components/views/user/UserRank.tsx new file mode 100644 index 0000000..a70fb7f --- /dev/null +++ b/src/components/views/user/UserRank.tsx @@ -0,0 +1,76 @@ +import { getRankIndex, InternalRankData } from "mtgatool-shared"; + +import { CombinedRankInfo } from "../../../background/onLabel/InEventGetCombinedRankInfo"; +import formatRank from "../../../utils/formatRank"; + +interface TopRankProps { + rank: CombinedRankInfo | null; + rankClass: string; + type: "constructed" | "limited"; +} + +export default function UserRank(props: TopRankProps): JSX.Element { + const { rank, rankClass, type } = props; + + if (rank == null) { + // No rank badge, default to beginner and remove interactions + const rankStyle = { + backgroundPosition: "0px 0px", + }; + return ( +
+
+
+ ); + } + + let internalRank: InternalRankData = { + rank: rank.constructedClass, + tier: rank.constructedLevel, + step: rank.constructedStep, + won: rank.constructedMatchesWon, + lost: rank.constructedMatchesLost, + drawn: rank.constructedMatchesDrawn, + percentile: rank.constructedPercentile, + leaderboardPlace: rank.constructedLeaderboardPlace, + seasonOrdinal: rank.constructedSeasonOrdinal, + }; + + if (type === "limited") { + internalRank = { + rank: rank.limitedClass, + tier: rank.limitedLevel, + step: rank.limitedStep, + won: rank.limitedMatchesWon, + lost: rank.limitedMatchesLost, + drawn: rank.limitedMatchesDrawn, + percentile: rank.limitedPercentile, + leaderboardPlace: rank.limitedLeaderboardPlace, + seasonOrdinal: rank.limitedSeasonOrdinal, + }; + } + + const propTitle = formatRank(internalRank); + const rankStyle = { + backgroundPosition: `${ + getRankIndex(internalRank.rank, internalRank.tier) * -48 + }px 0px`, + }; + + return ( +
+
+
+
+
+ {propTitle} +
+
+ ); +} diff --git a/src/components/views/user/UserView.tsx b/src/components/views/user/UserView.tsx new file mode 100644 index 0000000..9378d7c --- /dev/null +++ b/src/components/views/user/UserView.tsx @@ -0,0 +1,123 @@ +import { useEffect, useRef, useState } from "react"; +import { useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; + +import { DEFAULT_AVATAR } from "../../../constants"; +import useDebounce from "../../../hooks/useDebounce"; +import useFetchAvatar from "../../../hooks/useFetchAvatar"; +import useFetchUsername from "../../../hooks/useFetchUsername"; +import { AppState } from "../../../redux/stores/rendererStore"; +import { getData } from "../../../toolDb/worker-wrapper"; +import { DbRankData } from "../../../types/dbTypes"; +import Section from "../../ui/Section"; +import { MatchData } from "../history/convertDbMatchData"; +import UserHistoryList from "./UserHistoryList"; +import UserRank from "./UserRank"; + +export default function UserView() { + const { key } = useParams<{ key: string }>(); + + const [pubKey, setPubKey] = useState(decodeURIComponent(key)); + const [rankData, setRankData] = useState(null); + + useEffect(() => { + const decoded = decodeURIComponent(key); + if (!decoded.startsWith("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE")) { + getData(`==${decoded}`).then((userData) => { + if (userData) { + setPubKey(userData.keys.skpub); + } + }); + } + }, [key]); + + const avatars = useSelector((state: AppState) => state.avatars.avatars); + const usernames = useSelector((state: AppState) => state.usernames.usernames); + + const [matchesList, setMatchesList] = useState([]); + const matches = useRef>({}); + + const deboucer = useDebounce(200); + + const fetchAvatar = useFetchAvatar(); + const fetchUsername = useFetchUsername(); + + useEffect(() => { + if (pubKey) { + fetchAvatar(pubKey); + fetchUsername(pubKey); + + getData(`:${pubKey}.userids`).then((data) => { + if (data) { + let newest = ""; + let newestDate = 0; + Object.keys(data).forEach((uuid) => { + if (uuid && uuid !== "undefined" && data[uuid] > newestDate) { + newestDate = data[uuid]; + newest = uuid; + } + }); + + window.toolDb + .doFunction("getLatestMatches", { + pubKey: pubKey, + items: 10, + }) + .then((d) => { + if (d.return) { + setMatchesList(d.return as any); + } + }); + + getData(`:${pubKey}.${newest}-rank`).then((rd) => { + if (rd) { + setRankData(rd); + } + }); + } + }); + } + }, [pubKey, matches, deboucer]); + + const avatar = avatars[pubKey || DEFAULT_AVATAR]; + const username = usernames[pubKey || ""]; + + return ( + <> +
+
+
+
+
+

{decodeURIComponent(username)}

+
+ {rankData && ( +
+ + +
+ )} +
+
+
+ +
+ +
+ + ); +} diff --git a/src/components/views/user/ViewUser.tsx b/src/components/views/user/ViewUser.tsx new file mode 100644 index 0000000..532e164 --- /dev/null +++ b/src/components/views/user/ViewUser.tsx @@ -0,0 +1,13 @@ +import { Route, Switch, useRouteMatch } from "react-router-dom"; + +import UserView from "./UserView"; + +export default function ViewUser() { + const { url } = useRouteMatch(); + + return ( + + + + ); +} diff --git a/src/hooks/useFetchPubKey.tsx b/src/hooks/useFetchPubKey.tsx new file mode 100644 index 0000000..7c4152a --- /dev/null +++ b/src/hooks/useFetchPubKey.tsx @@ -0,0 +1,17 @@ +import { UserRootData } from "mtgatool-db"; +import { useEffect, useState } from "react"; + +export default function useFetchPubKey(username: string) { + const [pubKey, setPubkey] = useState(null); + + useEffect(() => { + window.toolDb.getData(`==${username}`).then((userRoot) => { + console.log(userRoot); + if (userRoot) { + setPubkey(userRoot.keys.skpub); + } + }); + }, [username]); + + return pubKey; +} diff --git a/src/index.scss b/src/index.scss index ac21e9b..61ffa83 100644 --- a/src/index.scss +++ b/src/index.scss @@ -35,6 +35,7 @@ @import "scss/decksView.scss"; @import "scss/matchView.scss"; @import "scss/homeView.scss"; +@import "scss/userView.scss"; @import "scss/exploreView.scss"; @import "scss/settings.scss"; @import "scss/profile.scss"; diff --git a/src/info.json b/src/info.json index 040dc04..361a697 100644 --- a/src/info.json +++ b/src/info.json @@ -1 +1 @@ -{"version":"6.3.8","branch":"dev","timestamp":1696202802802} \ No newline at end of file +{"version":"6.3.8","branch":"user-profiles","timestamp":1696789241474} \ No newline at end of file diff --git a/src/redux/actions.ts b/src/redux/actions.ts index ada5638..6bf5dfe 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -29,7 +29,6 @@ export const actions = { SET_MATCHES_INDEX: MainDataSlice.setMatchesIndex, SET_DRAFTS_INDEX: MainDataSlice.setDraftsIndex, SET_HIDDEN_DECKS: MainDataSlice.setHiddenDecks, - SET_PEERS: RendererSlice.setPeers, SET_PUBKEY: RendererSlice.setPubKey, SET_READING_LOG: RendererSlice.setReadingLog, SHOW_POST_SIGNUP: RendererSlice.showPostSignup, diff --git a/src/redux/slices/rendererSlice.ts b/src/redux/slices/rendererSlice.ts index 83451de..5cc3d62 100644 --- a/src/redux/slices/rendererSlice.ts +++ b/src/redux/slices/rendererSlice.ts @@ -3,7 +3,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Format, InternalDraftv2 } from "mtgatool-shared"; import { - DEFAULT_PEERS, LOGIN_AUTH, LOGIN_FAILED, LOGIN_OK, @@ -24,7 +23,6 @@ export interface Popup { } export const initialRendererState = { - peers: DEFAULT_PEERS, pubKey: "", archivedCache: {} as Record, backgroundGrpid: null as number | null, @@ -61,9 +59,6 @@ const rendererSlice = createSlice({ name: "renderer", initialState: initialRendererState, reducers: { - setPeers: (state: RendererState, action: PayloadAction): void => { - state.peers = action.payload; - }, setPubKey: (state: RendererState, action: PayloadAction): void => { setLocalSetting("pubkey", action.payload); state.pubKey = action.payload; @@ -205,7 +200,6 @@ const rendererSlice = createSlice({ }); export const { - setPeers, setPubKey, setReadingLog, showPostSignup, diff --git a/src/scss/homeView.scss b/src/scss/homeView.scss index da41ad7..6d40e35 100644 --- a/src/scss/homeView.scss +++ b/src/scss/homeView.scss @@ -64,6 +64,7 @@ line-height: 48px; margin-bottom: 4px; padding: 4px 12px; + cursor: normal; &.loading { animation-duration: 2s; @@ -100,6 +101,11 @@ text-overflow: ellipsis; white-space: pre; overflow: hidden; + + &:hover { + cursor: pointer; + text-decoration: underline; + } } .rank-time { diff --git a/src/scss/userView.scss b/src/scss/userView.scss new file mode 100644 index 0000000..a0c5210 --- /dev/null +++ b/src/scss/userView.scss @@ -0,0 +1,57 @@ +.user-view { + width: 100%; + + margin-top: 16px; + overflow: hidden; + + .top-container { + display: flex; + margin-bottom: 24px; + justify-content: space-between; + + .top-userdata { + display: flex; + + .avatar { + min-width: 128px; + min-height: 128px; + width: 128px; + height: 128px; + border-radius: 128px; + background-size: contain; + margin: 0; + } + + .username { + margin: auto 24px; + } + } + + .top-ranks { + display: flex; + flex-direction: column; + } + } + + .top-constructed-rank, + .top-limited-rank { + /* Align leaderboard indicator to center and add text border*/ + display: flex; + margin: auto; + align-items: center; + justify-content: center; + -webkit-app-region: no-drag; + align-self: center; + width: 48px; + height: 48px; + background-position: 0px 0px; + text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; + } +} + +.user-rank { + width: 240px; + height: 48px; + display: flex; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/toolDb/worker-wrapper.ts b/src/toolDb/worker-wrapper.ts index 8786881..e04fa5c 100644 --- a/src/toolDb/worker-wrapper.ts +++ b/src/toolDb/worker-wrapper.ts @@ -1,5 +1,7 @@ import { FunctionReturn, ParsedKeys } from "mtgatool-db"; +import { MatchData } from "../components/views/history/convertDbMatchData"; + const login = (username: string, password: string) => { return new Promise((resolve, reject) => { if (window.toolDbWorker) { @@ -237,10 +239,53 @@ const queryKeys = ( }); }; +const getMatchesData = ( + matchesIndex: string[], + uuid?: string +): Promise => { + return new Promise((resolve, reject) => { + if (window.toolDbWorker) { + const id = Math.random().toString(36).substring(7); + window.toolDbWorker.postMessage({ + type: "GET_MATCHES_DATA", + matchesIndex, + uuid, + id, + }); + + const listener = (e: any) => { + const { type, value } = e.data; + + if (type === `${id}_OK`) { + resolve(value); + window.toolDbWorker.removeEventListener("message", listener); + } + if (type === `${id}_ERR`) { + reject(e.data.err); + window.toolDbWorker.removeEventListener("message", listener); + } + }; + window.toolDbWorker.addEventListener("message", listener); + } else { + reject(new Error("toolDbWorker not available")); + } + }); +}; + +window.toolDb = { + doFunction, + getData, + getLocalData, + getMatchesData, + putData, + queryKeys, +} as any; + export { doFunction, getData, getLocalData, + getMatchesData, keysLogin, login, putData, diff --git a/src/utils/arrayBufferToHex.ts b/src/utils/arrayBufferToHex.ts new file mode 100644 index 0000000..8e7c54f --- /dev/null +++ b/src/utils/arrayBufferToHex.ts @@ -0,0 +1,18 @@ +export default function arrayBufferToHex(buffer: ArrayBuffer) { + const dataView = new DataView(buffer); + let iii; + let len; + let hex = ""; + let c; + + for (iii = 0, len = dataView.byteLength; iii < len; iii += 1) { + c = dataView.getUint8(iii).toString(16); + if (c.length < 2) { + c = `0${c}`; + } + + hex += c; + } + + return hex.toLowerCase(); +} diff --git a/src/utils/formatRank.ts b/src/utils/formatRank.ts index 8109c17..004ef96 100644 --- a/src/utils/formatRank.ts +++ b/src/utils/formatRank.ts @@ -2,11 +2,16 @@ import { InternalRankData } from "mtgatool-shared"; // pass in playerData.constructed / limited / historic objects export default function formatRank(rank: InternalRankData): string { - if (rank.leaderboardPlace) { - return `Mythic #${rank.leaderboardPlace}`; - } - if (rank.percentile) { - return `Mythic ${rank.percentile}%`; + if (rank.rank === "Mythic") { + if (rank.leaderboardPlace) { + return `Mythic #${rank.leaderboardPlace}`; + } + if (rank.percentile) { + return `Mythic ${rank.percentile.toFixed(2)}%`; + } } + + if (!rank.rank) return "-"; + return `${rank.rank} ${rank.tier}`; } diff --git a/tooldb-worker/getMatchesData.ts b/tooldb-worker/getMatchesData.ts index ab630f7..a6d1dfb 100644 --- a/tooldb-worker/getMatchesData.ts +++ b/tooldb-worker/getMatchesData.ts @@ -60,7 +60,11 @@ function getLocalData(key: string): Promise { }); } -export default function getMatchesData(matchesIds: string[], uuid: string) { +export function getMatchesDataLocal( + msgId: string, + matchesIds: string[], + uuid?: string +) { const promises = matchesIds.map((id) => { return getLocalData(id); }); @@ -71,8 +75,59 @@ export default function getMatchesData(matchesIds: string[], uuid: string) { ) .then((data) => { self.postMessage({ - type: "MATCHES_DATA", - value: data.filter((m) => m.uuid === uuid), + type: `${msgId}_OK`, + value: data.filter((m) => (uuid ? m.uuid === uuid : true)), }); }); } + +export function getMatchesData( + msgId: string, + matchesIds: string[] | null, + uuid?: string, + updateCallback?: (total: number, saved: number) => void +) { + const matchesIndex = [...new Set([...(matchesIds || [])])]; + + let saved = 0; + let timeout: NodeJS.Timeout | null = null; + let lastUpdate = new Date().getTime(); + + function updateState() { + timeout = null; + + if (updateCallback) { + updateCallback(matchesIndex.length, saved); + } + + if (saved === matchesIndex.length && matchesIndex.length > 0) { + getMatchesDataLocal(msgId, matchesIndex, uuid); + } + } + + function debounceUpdateState() { + if (timeout) clearTimeout(timeout); + if (new Date().getTime() - lastUpdate > 1000) { + lastUpdate = new Date().getTime(); + updateState(); + } + timeout = setTimeout(updateState, 100); + } + + // Fetch any match we dont have locally + matchesIndex.forEach((id: string) => { + self.toolDb.store.get(id, (err) => { + if (!err) { + saved += 1; + debounceUpdateState(); + } else { + self.toolDb.getData(id, false, 2000).finally(() => { + saved += 1; + debounceUpdateState(); + }); + } + }); + }); + + updateState(); +} diff --git a/tooldb-worker/handleMatchesIndex.ts b/tooldb-worker/handleMatchesIndex.ts index 1859081..1b9083c 100644 --- a/tooldb-worker/handleMatchesIndex.ts +++ b/tooldb-worker/handleMatchesIndex.ts @@ -1,56 +1,21 @@ /* eslint-disable no-restricted-globals */ -import getMatchesData from "./getMatchesData"; +import { getMatchesData } from "./getMatchesData"; import reduxAction from "./reduxAction"; -export default function handleMatchesIndex(matchesIndex: string[] | null) { - self.globalData.matchesIndex = [ - ...new Set([...self.globalData.matchesIndex, ...(matchesIndex || [])]), - ]; - - let saved = 0; - let timeout: NodeJS.Timeout | null = null; - let lastUpdate = new Date().getTime(); - - function updateState() { - timeout = null; - reduxAction("SET_MATCHES_FETCH_STATE", { - total: self.globalData.matchesIndex.length, - saved, - }); - - if ( - saved === self.globalData.matchesIndex.length && - self.globalData.matchesIndex.length > 0 - ) { - reduxAction("SET_MATCHES_INDEX", self.globalData.matchesIndex); - - getMatchesData(self.globalData.matchesIndex, self.globalData.currentUUID); - } - } - - function debounceUpdateState() { - if (timeout) clearTimeout(timeout); - if (new Date().getTime() - lastUpdate > 1000) { - lastUpdate = new Date().getTime(); - updateState(); - } - timeout = setTimeout(updateState, 100); - } - - // Fetch any match we dont have locally - self.globalData.matchesIndex.forEach((id: string) => { - self.toolDb.store.get(id, (err) => { - if (!err) { - saved += 1; - debounceUpdateState(); - } else { - self.toolDb.getData(id, false, 2000).finally(() => { - saved += 1; - debounceUpdateState(); +export default function handleMatchesIndex(matchesIds: string[] | null) { + if (matchesIds) { + reduxAction("SET_MATCHES_INDEX", matchesIds); + + getMatchesData( + "MATCHES_DATA", + matchesIds, + self.globalData.currentUUID, + (total, saved) => { + reduxAction("SET_MATCHES_FETCH_STATE", { + total, + saved, }); } - }); - }); - - updateState(); + ); + } } diff --git a/tooldb-worker/toolDbWorker.ts b/tooldb-worker/toolDbWorker.ts index 15da181..a6e8ad1 100644 --- a/tooldb-worker/toolDbWorker.ts +++ b/tooldb-worker/toolDbWorker.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-globals */ -import { ToolDb, ToolDbNetwork } from "mtgatool-db"; +import { ServerPeerData, ToolDb, ToolDbNetwork } from "mtgatool-db"; import { DEFAULT_PEERS } from "./constants"; import doFunction from "./doFunction"; @@ -9,7 +9,7 @@ import getConnectionData from "./getConnectionData"; import getCrdt from "./getCrdt"; import getData from "./getData"; import getDataLocal from "./getDataLocal"; -import getMatchesData from "./getMatchesData"; +import { getMatchesData } from "./getMatchesData"; import getSaveKeysJson from "./getSaveKeysJson"; import handleMatchesIndex from "./handleMatchesIndex"; import keysLogin from "./keysLogin"; @@ -27,14 +27,40 @@ const toolDb = new ToolDb({ toolDb.on("init", (key) => console.warn("ToolDb initialized!", key)); -DEFAULT_PEERS.forEach((peer) => { - const networkModule = toolDb.network as ToolDbNetwork; - networkModule.findServer(peer); +// Try to conenct to servers from cache +toolDb.store.get("servers", (err, data) => { + let serversData: Record = {}; + if (err) { + console.error("Error getting servers from cache:", err); + } else if (data) { + try { + serversData = JSON.parse(data); + } catch (_e) { + console.error("Error parsing servers from cache:", _e); + } + } + + console.log("Got servers from cache:", serversData); + + DEFAULT_PEERS.forEach((peer) => { + const networkModule = toolDb.network as ToolDbNetwork; + if (serversData[peer]) { + networkModule.connectTo(serversData[peer]); + } else { + networkModule.findServer(peer); + } + }); }); toolDb.onConnect = () => { + const networkModule = toolDb.network as ToolDbNetwork; console.warn("ToolDb connected!"); self.postMessage({ type: "CONNECTED" }); + toolDb.store.put( + "servers", + JSON.stringify(networkModule.serverPeerData), + () => console.log("Saved servers to cache", networkModule.serverPeerData) + ); }; self.toolDb = toolDb; @@ -114,7 +140,7 @@ self.onmessage = (e: any) => { break; case "GET_MATCHES_DATA": - getMatchesData(e.data.matchesIndex, e.data.uuid); + getMatchesData(e.data.id, e.data.matchesIndex, e.data.uuid); break; case "REFRESH_MATCHES":