mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge pull request #1204 from hydralauncher/feat/adding-intercom
Feat/adding intercom
This commit is contained in:
commit
df0e124c3a
@ -36,6 +36,7 @@
|
|||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.0.22",
|
"@fontsource/noto-sans": "^5.0.22",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"queued": "{{title}} (Queued)",
|
"queued": "{{title}} (Queued)",
|
||||||
"game_has_no_executable": "Game has no executable selected",
|
"game_has_no_executable": "Game has no executable selected",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"friends": "Friends"
|
"friends": "Friends",
|
||||||
|
"need_help": "Need help?"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"queued": "{{title}} (En cola)",
|
"queued": "{{title}} (En cola)",
|
||||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar sesión",
|
||||||
"friends": "Amigos"
|
"friends": "Amigos",
|
||||||
|
"need_help": "¿Necesitas ayuda?"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"queued": "{{title}} (Na fila)",
|
"queued": "{{title}} (Na fila)",
|
||||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||||
"sign_in": "Login",
|
"sign_in": "Login",
|
||||||
"friends": "Amigos"
|
"friends": "Amigos",
|
||||||
|
"need_help": "Precisa de ajuda?"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
|
@ -24,7 +24,8 @@
|
|||||||
"queued": "{{title}} (В очереди)",
|
"queued": "{{title}} (В очереди)",
|
||||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||||
"sign_in": "Войти",
|
"sign_in": "Войти",
|
||||||
"friends": "Друзья"
|
"friends": "Друзья",
|
||||||
|
"need_help": "Нужна помощь?"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
@ -56,6 +56,7 @@ export const getUserData = () => {
|
|||||||
id: loggedUser.userId,
|
id: loggedUser.userId,
|
||||||
username: "",
|
username: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
|
email: null,
|
||||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||||
subscription: loggedUser.subscription
|
subscription: loggedUser.subscription
|
||||||
? {
|
? {
|
||||||
|
@ -85,6 +85,10 @@ export class WindowManager {
|
|||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (details.url.includes("intercom.io")) {
|
||||||
|
return callback(details);
|
||||||
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"access-control-allow-origin": ["*"],
|
"access-control-allow-origin": ["*"],
|
||||||
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
content="default-src 'self' *.intercom.io wss://nexus-websocket-a.intercom.io; script-src 'self' *.intercom.io *.intercomcdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: *; media-src 'self' local: data: *; font-src 'self' https://fonts.intercomcdn.com"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -126,3 +126,9 @@ export const titleBar = style({
|
|||||||
zIndex: "4",
|
zIndex: "4",
|
||||||
borderBottom: `1px solid ${vars.color.border}`,
|
borderBottom: `1px solid ${vars.color.border}`,
|
||||||
} as ComplexStyleRule);
|
} as ComplexStyleRule);
|
||||||
|
|
||||||
|
export const cloudText = style({
|
||||||
|
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
||||||
|
backgroundClip: "text",
|
||||||
|
color: "transparent",
|
||||||
|
});
|
||||||
|
@ -2,6 +2,8 @@ import { useCallback, useContext, useEffect, useRef } from "react";
|
|||||||
|
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
|
import Intercom from "@intercom/messenger-js-sdk";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
@ -34,6 +36,10 @@ export interface AppProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Intercom({
|
||||||
|
app_id: "pq96v8fh",
|
||||||
|
});
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const { updateLibrary, library } = useLibrary();
|
const { updateLibrary, library } = useLibrary();
|
||||||
@ -54,8 +60,13 @@ export function App() {
|
|||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
|
|
||||||
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
|
const {
|
||||||
useUserDetails();
|
userDetails,
|
||||||
|
hasActiveSubscription,
|
||||||
|
fetchUserDetails,
|
||||||
|
updateUserDetails,
|
||||||
|
clearUserDetails,
|
||||||
|
} = useUserDetails();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -204,7 +215,9 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new MutationObserver(() => {
|
new MutationObserver(() => {
|
||||||
const modal = document.body.querySelector("[role=dialog]");
|
const modal = document.body.querySelector(
|
||||||
|
"[role=dialog]:not([data-intercom-frame='true'])"
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
||||||
}).observe(document.body, {
|
}).observe(document.body, {
|
||||||
@ -270,7 +283,12 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
{window.electron.platform === "win32" && (
|
{window.electron.platform === "win32" && (
|
||||||
<div className={styles.titleBar}>
|
<div className={styles.titleBar}>
|
||||||
<h4>Hydra</h4>
|
<h4>
|
||||||
|
Hydra
|
||||||
|
{hasActiveSubscription && (
|
||||||
|
<span className={styles.cloudText}> Cloud</span>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -124,3 +124,29 @@ export const section = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
paddingBottom: `${SPACING_UNIT}px`,
|
paddingBottom: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const helpButton = style({
|
||||||
|
color: vars.color.muted,
|
||||||
|
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||||
|
gap: "9px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.6)",
|
||||||
|
borderTop: `solid 1px ${vars.color.border}`,
|
||||||
|
transition: "background-color ease 0.1s",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const helpButtonIcon = style({
|
||||||
|
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: "50%",
|
||||||
|
});
|
||||||
|
@ -5,7 +5,12 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
import { TextField } from "@renderer/components";
|
import { TextField } from "@renderer/components";
|
||||||
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
import {
|
||||||
|
useDownload,
|
||||||
|
useLibrary,
|
||||||
|
useToast,
|
||||||
|
useUserDetails,
|
||||||
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
@ -15,6 +20,9 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
|||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import { show, update } from "@intercom/messenger-js-sdk";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@ -42,6 +50,20 @@ export function Sidebar() {
|
|||||||
return sortBy(library, (game) => game.title);
|
return sortBy(library, (game) => game.title);
|
||||||
}, [library]);
|
}, [library]);
|
||||||
|
|
||||||
|
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userDetails) {
|
||||||
|
update({
|
||||||
|
name: userDetails.displayName,
|
||||||
|
Username: userDetails.username,
|
||||||
|
Email: userDetails.email,
|
||||||
|
"Subscription expiration date": userDetails?.subscription?.expiresAt,
|
||||||
|
"Payment status": userDetails?.subscription?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userDetails, hasActiveSubscription]);
|
||||||
|
|
||||||
const { lastPacket, progress } = useDownload();
|
const { lastPacket, progress } = useDownload();
|
||||||
|
|
||||||
const { showWarningToast } = useToast();
|
const { showWarningToast } = useToast();
|
||||||
@ -237,6 +259,15 @@ export function Sidebar() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasActiveSubscription && (
|
||||||
|
<button type="button" className={styles.helpButton} onClick={show}>
|
||||||
|
<div className={styles.helpButtonIcon}>
|
||||||
|
<CommentDiscussionIcon size={14} />
|
||||||
|
</div>
|
||||||
|
<span>{t("need_help")}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.handle}
|
className={styles.handle}
|
||||||
|
@ -10,12 +10,22 @@ export interface HowLongToBeatEntry {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CatalogueCache {
|
||||||
|
id?: number;
|
||||||
|
category: string;
|
||||||
|
games: { objectId: string; shop: GameShop }[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export const db = new Dexie("Hydra");
|
export const db = new Dexie("Hydra");
|
||||||
|
|
||||||
db.version(4).stores({
|
db.version(5).stores({
|
||||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||||
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||||
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
||||||
|
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadSourcesTable = db.table("downloadSources");
|
export const downloadSourcesTable = db.table("downloadSources");
|
||||||
@ -24,4 +34,6 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
|
|||||||
"howLongToBeatEntries"
|
"howLongToBeatEntries"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
|
||||||
|
|
||||||
db.open();
|
db.open();
|
||||||
|
@ -15,6 +15,14 @@ import * as styles from "./home.css";
|
|||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
import { catalogueCacheTable, db } from "@renderer/dexie";
|
||||||
|
import { add } from "date-fns";
|
||||||
|
|
||||||
|
const categoryCacheDurationInSeconds = {
|
||||||
|
[CatalogueCategory.Hot]: 60 * 60 * 2,
|
||||||
|
[CatalogueCategory.Weekly]: 60 * 60 * 24,
|
||||||
|
[CatalogueCategory.Achievements]: 60 * 60 * 24,
|
||||||
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
@ -36,19 +44,43 @@ export default function Home() {
|
|||||||
[CatalogueCategory.Achievements]: [],
|
[CatalogueCategory.Achievements]: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
|
||||||
|
try {
|
||||||
|
const catalogueCache = await catalogueCacheTable
|
||||||
|
.where("expiresAt")
|
||||||
|
.above(new Date())
|
||||||
|
.and((cache) => cache.category === category)
|
||||||
|
.first();
|
||||||
|
|
||||||
setCurrentCatalogueCategory(category);
|
setCurrentCatalogueCategory(category);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
window.electron
|
if (catalogueCache)
|
||||||
.getCatalogue(category)
|
return setCatalogue((prev) => ({
|
||||||
.then((catalogue) => {
|
...prev,
|
||||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
[category]: catalogueCache.games,
|
||||||
})
|
}));
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => {
|
const catalogue = await window.electron.getCatalogue(category);
|
||||||
setIsLoading(false);
|
|
||||||
|
db.transaction("rw", catalogueCacheTable, async () => {
|
||||||
|
await catalogueCacheTable.where("category").equals(category).delete();
|
||||||
|
|
||||||
|
await catalogueCacheTable.add({
|
||||||
|
category,
|
||||||
|
games: catalogue,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
expiresAt: add(new Date(), {
|
||||||
|
seconds: categoryCacheDurationInSeconds[category],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getRandomGame = useCallback(() => {
|
const getRandomGame = useCallback(() => {
|
||||||
|
@ -245,6 +245,7 @@ export interface Subscription {
|
|||||||
export interface UserDetails {
|
export interface UserDetails {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
email: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
@ -257,6 +258,7 @@ export interface UserProfile {
|
|||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
|
email: string | null;
|
||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
profileVisibility: ProfileVisibility;
|
profileVisibility: ProfileVisibility;
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
@ -373,4 +375,4 @@ export interface ComparedAchievements {
|
|||||||
export * from "./steam.types";
|
export * from "./steam.types";
|
||||||
export * from "./real-debrid.types";
|
export * from "./real-debrid.types";
|
||||||
export * from "./ludusavi.types";
|
export * from "./ludusavi.types";
|
||||||
export * from "./howlongtobeat.types";
|
export * from "./how-long-to-beat.types";
|
||||||
|
@ -1066,6 +1066,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||||
|
|
||||||
|
"@intercom/messenger-js-sdk@^0.0.14":
|
||||||
|
version "0.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.14.tgz#a27999370cc0a82a2a57a779426df25a57891863"
|
||||||
|
integrity sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==
|
||||||
|
|
||||||
"@isaacs/cliui@^8.0.2":
|
"@isaacs/cliui@^8.0.2":
|
||||||
version "8.0.2"
|
version "8.0.2"
|
||||||
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user