feat: adding new friend session notification

This commit is contained in:
Chubby Granny Chaser
2025-05-10 17:43:09 +01:00
parent fee9cfb3e8
commit 216f813771
15 changed files with 286 additions and 414 deletions

2
proto

Submodule proto updated: b8c2db166d...7a23620f93

View File

@@ -370,10 +370,11 @@
"restart_to_install_update": "Restart Hydra to install the update",
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
"new_friend_request_description": "You have received a new friend request",
"new_friend_request_description": "{{displayName}} sent you a friend request",
"new_friend_request_title": "New friend request",
"extraction_complete": "Extraction complete",
"game_extracted": "{{title}} extracted successfully"
"game_extracted": "{{title}} extracted successfully",
"friend_started_playing_game": "{{displayName}} started playing a game"
},
"system_tray": {
"open": "Open Hydra",

View File

@@ -371,7 +371,8 @@
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
"new_friend_request_title": "Nueva solicitud de amistad",
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
"friend_started_playing_game": "{{displayName}} está jugando"
},
"system_tray": {
"open": "Abrir Hydra",

View File

@@ -356,9 +356,10 @@
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Você recebeu um novo pedido de amizade",
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
"extraction_complete": "Extração concluída",
"game_extracted": "{{title}} extraído com sucesso"
"game_extracted": "{{title}} extraído com sucesso",
"friend_started_playing_game": "{{displayName}} começou a jogar"
},
"system_tray": {
"open": "Abrir Hydra",

View File

@@ -341,7 +341,8 @@
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Recebeste um novo pedido de amizade"
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
"friend_started_playing_game": "{{displayName}} começou a jogar"
},
"system_tray": {
"open": "Abrir o Hydra",

View File

@@ -1,5 +1,10 @@
import { registerEvent } from "../register-event";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
import {
DownloadManager,
HydraApi,
WSClient,
gamesPlaytime,
} from "@main/services";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
@@ -30,6 +35,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),
]);
WSClient.close();
};
registerEvent("signOut", signOut);

View File

@@ -14,348 +14,227 @@ import { MessageType } from "@protobuf-ts/runtime";
* @generated from protobuf message FriendRequest
*/
export interface FriendRequest {
/**
* @generated from protobuf field: int32 friend_request_count = 1;
*/
friendRequestCount: number;
/**
* @generated from protobuf field: optional string sender_id = 2;
*/
senderId?: string;
/**
* @generated from protobuf field: int32 friend_request_count = 1;
*/
friendRequestCount: number;
/**
* @generated from protobuf field: optional string sender_id = 2;
*/
senderId?: string;
}
/**
* @generated from protobuf message UpdateGamePlaytime
* @generated from protobuf message FriendGameSession
*/
export interface UpdateGamePlaytime {
/**
* @generated from protobuf field: int64 playtime_delta_in_seconds = 1;
*/
playtimeDeltaInSeconds: bigint;
/**
* @generated from protobuf field: string last_time_played = 2;
*/
lastTimePlayed: string;
/**
* @generated from protobuf field: string game_id = 3;
*/
gameId: string;
export interface FriendGameSession {
/**
* @generated from protobuf field: string object_id = 1;
*/
objectId: string;
/**
* @generated from protobuf field: string shop = 2;
*/
shop: string;
/**
* @generated from protobuf field: string friend_id = 3;
*/
friendId: string;
}
/**
* @generated from protobuf message Envelope
*/
export interface Envelope {
/**
* @generated from protobuf oneof: payload
*/
payload:
| {
/**
* @generated from protobuf oneof: payload
*/
payload: {
oneofKind: "friendRequest";
/**
* @generated from protobuf field: FriendRequest friend_request = 1;
*/
friendRequest: FriendRequest;
}
| {
oneofKind: "updateGamePlaytime";
} | {
oneofKind: "friendGameSession";
/**
* @generated from protobuf field: UpdateGamePlaytime update_game_playtime = 2;
* @generated from protobuf field: FriendGameSession friend_game_session = 2;
*/
updateGamePlaytime: UpdateGamePlaytime;
}
| {
friendGameSession: FriendGameSession;
} | {
oneofKind: undefined;
};
};
}
// @generated message type with reflection information, may provide speed optimized methods
class FriendRequest$Type extends MessageType<FriendRequest> {
constructor() {
super("FriendRequest", [
{
no: 1,
name: "friend_request_count",
kind: "scalar",
T: 5 /*ScalarType.INT32*/,
},
{
no: 2,
name: "sender_id",
kind: "scalar",
opt: true,
T: 9 /*ScalarType.STRING*/,
},
]);
}
create(value?: PartialMessage<FriendRequest>): FriendRequest {
const message = globalThis.Object.create(this.messagePrototype!);
message.friendRequestCount = 0;
if (value !== undefined)
reflectionMergePartial<FriendRequest>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: FriendRequest
): FriendRequest {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 friend_request_count */ 1:
message.friendRequestCount = reader.int32();
break;
case /* optional string sender_id */ 2:
message.senderId = reader.string();
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
constructor() {
super("FriendRequest", [
{ no: 1, name: "friend_request_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 2, name: "sender_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FriendRequest>): FriendRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.friendRequestCount = 0;
if (value !== undefined)
reflectionMergePartial<FriendRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FriendRequest): FriendRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 friend_request_count */ 1:
message.friendRequestCount = reader.int32();
break;
case /* optional string sender_id */ 2:
message.senderId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FriendRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 friend_request_count = 1; */
if (message.friendRequestCount !== 0)
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
/* optional string sender_id = 2; */
if (message.senderId !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: FriendRequest,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int32 friend_request_count = 1; */
if (message.friendRequestCount !== 0)
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
/* optional string sender_id = 2; */
if (message.senderId !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message FriendRequest
*/
export const FriendRequest = new FriendRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class UpdateGamePlaytime$Type extends MessageType<UpdateGamePlaytime> {
constructor() {
super("UpdateGamePlaytime", [
{
no: 1,
name: "playtime_delta_in_seconds",
kind: "scalar",
T: 3 /*ScalarType.INT64*/,
L: 0 /*LongType.BIGINT*/,
},
{
no: 2,
name: "last_time_played",
kind: "scalar",
T: 9 /*ScalarType.STRING*/,
},
{ no: 3, name: "game_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
]);
}
create(value?: PartialMessage<UpdateGamePlaytime>): UpdateGamePlaytime {
const message = globalThis.Object.create(this.messagePrototype!);
message.playtimeDeltaInSeconds = 0n;
message.lastTimePlayed = "";
message.gameId = "";
if (value !== undefined)
reflectionMergePartial<UpdateGamePlaytime>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: UpdateGamePlaytime
): UpdateGamePlaytime {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int64 playtime_delta_in_seconds */ 1:
message.playtimeDeltaInSeconds = reader.int64().toBigInt();
break;
case /* string last_time_played */ 2:
message.lastTimePlayed = reader.string();
break;
case /* string game_id */ 3:
message.gameId = reader.string();
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
class FriendGameSession$Type extends MessageType<FriendGameSession> {
constructor() {
super("FriendGameSession", [
{ no: 1, name: "object_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "shop", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "friend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FriendGameSession>): FriendGameSession {
const message = globalThis.Object.create((this.messagePrototype!));
message.objectId = "";
message.shop = "";
message.friendId = "";
if (value !== undefined)
reflectionMergePartial<FriendGameSession>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FriendGameSession): FriendGameSession {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string object_id */ 1:
message.objectId = reader.string();
break;
case /* string shop */ 2:
message.shop = reader.string();
break;
case /* string friend_id */ 3:
message.friendId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FriendGameSession, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string object_id = 1; */
if (message.objectId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.objectId);
/* string shop = 2; */
if (message.shop !== "")
writer.tag(2, WireType.LengthDelimited).string(message.shop);
/* string friend_id = 3; */
if (message.friendId !== "")
writer.tag(3, WireType.LengthDelimited).string(message.friendId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: UpdateGamePlaytime,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int64 playtime_delta_in_seconds = 1; */
if (message.playtimeDeltaInSeconds !== 0n)
writer.tag(1, WireType.Varint).int64(message.playtimeDeltaInSeconds);
/* string last_time_played = 2; */
if (message.lastTimePlayed !== "")
writer.tag(2, WireType.LengthDelimited).string(message.lastTimePlayed);
/* string game_id = 3; */
if (message.gameId !== "")
writer.tag(3, WireType.LengthDelimited).string(message.gameId);
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message UpdateGamePlaytime
* @generated MessageType for protobuf message FriendGameSession
*/
export const UpdateGamePlaytime = new UpdateGamePlaytime$Type();
export const FriendGameSession = new FriendGameSession$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Envelope$Type extends MessageType<Envelope> {
constructor() {
super("Envelope", [
{
no: 1,
name: "friend_request",
kind: "message",
oneof: "payload",
T: () => FriendRequest,
},
{
no: 2,
name: "update_game_playtime",
kind: "message",
oneof: "payload",
T: () => UpdateGamePlaytime,
},
]);
}
create(value?: PartialMessage<Envelope>): Envelope {
const message = globalThis.Object.create(this.messagePrototype!);
message.payload = { oneofKind: undefined };
if (value !== undefined)
reflectionMergePartial<Envelope>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: Envelope
): Envelope {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* FriendRequest friend_request */ 1:
message.payload = {
oneofKind: "friendRequest",
friendRequest: FriendRequest.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).friendRequest
),
};
break;
case /* UpdateGamePlaytime update_game_playtime */ 2:
message.payload = {
oneofKind: "updateGamePlaytime",
updateGamePlaytime: UpdateGamePlaytime.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).updateGamePlaytime
),
};
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
constructor() {
super("Envelope", [
{ no: 1, name: "friend_request", kind: "message", oneof: "payload", T: () => FriendRequest },
{ no: 2, name: "friend_game_session", kind: "message", oneof: "payload", T: () => FriendGameSession }
]);
}
create(value?: PartialMessage<Envelope>): Envelope {
const message = globalThis.Object.create((this.messagePrototype!));
message.payload = { oneofKind: undefined };
if (value !== undefined)
reflectionMergePartial<Envelope>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Envelope): Envelope {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* FriendRequest friend_request */ 1:
message.payload = {
oneofKind: "friendRequest",
friendRequest: FriendRequest.internalBinaryRead(reader, reader.uint32(), options, (message.payload as any).friendRequest)
};
break;
case /* FriendGameSession friend_game_session */ 2:
message.payload = {
oneofKind: "friendGameSession",
friendGameSession: FriendGameSession.internalBinaryRead(reader, reader.uint32(), options, (message.payload as any).friendGameSession)
};
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Envelope, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* FriendRequest friend_request = 1; */
if (message.payload.oneofKind === "friendRequest")
FriendRequest.internalBinaryWrite(message.payload.friendRequest, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
/* FriendGameSession friend_game_session = 2; */
if (message.payload.oneofKind === "friendGameSession")
FriendGameSession.internalBinaryWrite(message.payload.friendGameSession, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: Envelope,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* FriendRequest friend_request = 1; */
if (message.payload.oneofKind === "friendRequest")
FriendRequest.internalBinaryWrite(
message.payload.friendRequest,
writer.tag(1, WireType.LengthDelimited).fork(),
options
).join();
/* UpdateGamePlaytime update_game_playtime = 2; */
if (message.payload.oneofKind === "updateGamePlaytime")
UpdateGamePlaytime.internalBinaryWrite(
message.payload.updateGamePlaytime,
writer.tag(2, WireType.LengthDelimited).fork(),
options
).join();
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message Envelope

View File

@@ -1,16 +1,21 @@
import { Aria2, DownloadManager, Ludusavi, startMainLoop } from "./services";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types";
import { TorBoxClient } from "./services/download/torbox";
import { CommonRedistManager } from "./services/common-redist-manager";
import { WSManager } from "./services/ws-manager";
import { SystemPath } from "./services/system-path";
import {
WSClient,
SystemPath,
CommonRedistManager,
TorBoxClient,
RealDebridClient,
Aria2,
DownloadManager,
Ludusavi,
HydraApi,
uploadGamesBatch,
startMainLoop,
} from "@main/services";
export const loadState = async () => {
SystemPath.checkIfPathsAreAvailable();
@@ -38,7 +43,7 @@ export const loadState = async () => {
await HydraApi.setupApi().then(() => {
uploadGamesBatch();
WSManager.connect();
WSClient.connect();
});
const downloads = await downloadsSublevel

View File

@@ -1 +1,3 @@
export * from "./download-manager";
export * from "./real-debrid";
export * from "./torbox";

View File

@@ -11,7 +11,7 @@ import { getUserData } from "./user/get-user-data";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
import { WSManager } from "./ws-manager";
import { WSClient } from "./ws/ws-client";
interface HydraApiOptions {
needsAuth?: boolean;
@@ -102,8 +102,8 @@ export class HydraApi {
WindowManager.mainWindow.webContents.send("on-signin");
await clearGamesRemoteIds();
uploadGamesBatch();
WSManager.close();
WSManager.connect();
WSClient.close();
WSClient.connect();
}
}

View File

@@ -12,3 +12,6 @@ export * from "./7zip";
export * from "./game-files-manager";
export * from "./common-redist-manager";
export * from "./aria2";
export * from "./ws";
export * from "./system-path";
export * from "./library-sync";

View File

@@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import type { Game, GameStats, UserPreferences, UserProfile } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
@@ -82,7 +82,7 @@ export const publishNotificationUpdateReadyToInstall = async (
};
export const publishNewFriendRequestNotification = async (
senderProfileImageUrl?: string
user: UserProfile
) => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
@@ -99,9 +99,26 @@ export const publishNewFriendRequestNotification = async (
}),
body: t("new_friend_request_description", {
ns: "notifications",
displayName: user.displayName,
}),
icon: senderProfileImageUrl
? await downloadImage(senderProfileImageUrl)
icon: user?.profileImageUrl
? await downloadImage(user.profileImageUrl)
: trayIcon,
}).show();
};
export const publishFriendStartedPlayingGameNotification = async (
friend: UserProfile,
game: GameStats
) => {
new Notification({
title: t("friend_started_playing_game", {
ns: "notifications",
displayName: friend.displayName,
}),
body: game.assets?.title,
icon: friend?.profileImageUrl
? await downloadImage(friend.profileImageUrl)
: trayIcon,
}).show();
};

View File

@@ -1,75 +0,0 @@
import { WebSocket } from "ws";
import { HydraApi } from "./hydra-api";
import { Envelope } from "@main/generated/envelope";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
export class WSManager {
private static ws: WebSocket | null = null;
private static reconnectInterval = 1000;
private static maxReconnectInterval = 30000;
private static reconnectAttempts = 0;
private static reconnecting = false;
static async connect() {
const { token } = await HydraApi.post<{ token: string }>("/auth/ws");
this.ws = new WebSocket(import.meta.env.MAIN_VITE_WS_URL, {
headers: {
Authorization: `Bearer ${token}`,
},
});
this.ws.on("open", () => {
logger.info("WS connected");
this.reconnectInterval = 1000;
this.reconnecting = false;
});
this.ws.on("message", (message) => {
const envelope = Envelope.fromBinary(
new Uint8Array(Buffer.from(message.toString()))
);
if (envelope.payload.oneofKind === "friendRequest") {
WindowManager.mainWindow?.webContents.send("on-sync-friend-requests", {
friendRequestCount: envelope.payload.friendRequest.friendRequestCount,
});
}
});
this.ws.on("close", () => {
logger.warn("WS closed. Attempting reconnect...");
this.tryReconnect();
});
this.ws.on("error", (err) => {
logger.error("WS error:", err);
this.tryReconnect();
});
}
private static async tryReconnect() {
if (this.reconnecting) return;
this.reconnecting = true;
this.reconnectAttempts++;
const waitTime = Math.min(
this.reconnectInterval * 2 ** this.reconnectAttempts,
this.maxReconnectInterval
);
logger.info(`Reconnecting in ${waitTime / 1000}s...`);
setTimeout(() => {
this.connect();
}, waitTime);
}
public static async close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}

View File

@@ -25,6 +25,21 @@
border-right: 1px solid globals.$border-color;
}
&__cover-placeholder {
display: flex;
align-items: center;
justify-content: center;
color: globals.$body-color;
width: 200px;
height: 103px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(255, 255, 255, 0.1) 100%
);
}
&__details {
display: flex;
flex-direction: column;

View File

@@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom";
import "./game-item.scss";
import { useTranslation } from "react-i18next";
import { CatalogueSearchResult } from "@types";
import { QuestionIcon } from "@primer/octicons-react";
export interface GameItemProps {
game: CatalogueSearchResult;
@@ -43,18 +44,32 @@ export function GameItem({ game }: GameItemProps) {
});
}, [game.genres, language, steamGenres]);
const libraryImage = useMemo(() => {
if (game.libraryImageUrl) {
return (
<img
className="game-item__cover"
src={game.libraryImageUrl}
alt={game.title}
loading="lazy"
/>
);
}
return (
<div className="game-item__cover-placeholder">
<QuestionIcon size={28} />
</div>
);
}, [game.libraryImageUrl, game.title]);
return (
<button
type="button"
className="game-item"
onClick={() => navigate(buildGameDetailsPath(game))}
>
<img
className="game-item__cover"
src={game.libraryImageUrl}
alt={game.title}
loading="lazy"
/>
{libraryImage}
<div className="game-item__details">
<span>{game.title}</span>