diff --git a/src/main/model/user.ts b/src/main/model/user.ts index 0ce8246c6..634a55aa7 100644 --- a/src/main/model/user.ts +++ b/src/main/model/user.ts @@ -2,17 +2,26 @@ // // SPDX-License-Identifier: MIT +export type ClanBaseData = { + clanId: string; + name: string; + tag: string; + language: string; +}; + export type User = { userId: string; username: string; displayName: string; - clanId: string | null; + clanBaseData?: ClanBaseData | null; partyId: string | null; countryCode: string; status: "offline" | "menu" | "playing" | "lobby"; + rating?: { value: number } | null; + roles?: ReadonlyArray<"contributor" | "admin" | "moderator" | "tournament_winner" | "tournament_caster">; // Is the user me? - isMe?: 0 | 1; + isMe: boolean; // When user is a contender in a battle battleRoomState: { diff --git a/src/renderer/assets/languages/en.json b/src/renderer/assets/languages/en.json index 8c8637ded..3a8ce47ee 100644 --- a/src/renderer/assets/languages/en.json +++ b/src/renderer/assets/languages/en.json @@ -1896,9 +1896,23 @@ }, "views": { "profile": { - "status": "Status: ", - "clan": "Clan: ", - "userNotFound": "User not found" + "status": "Status", + "userId": "User ID", + "clan": "Clan", + "userNotFound": "User not found", + "myProfile": "My profile", + "findAClan": "Find a clan", + "statusOffline": "Offline", + "statusMenu": "Menu", + "statusPlaying": "Playing", + "statusLobby": "Lobby", + "rating": "Rating", + "roles": "Roles", + "roleContributor": "Contributor", + "roleAdmin": "Admin", + "roleModerator": "Moderator", + "roleTournamentWinner": "Tournament Winner", + "roleTournamentCaster": "Tournament Caster" }, "play": { "comingSoon": "(Coming Soon)", diff --git a/src/renderer/components/battle/BattleMessage.vue b/src/renderer/components/battle/BattleMessage.vue new file mode 100644 index 000000000..dfa3e6440 --- /dev/null +++ b/src/renderer/components/battle/BattleMessage.vue @@ -0,0 +1,70 @@ + + + + + + + diff --git a/src/renderer/store/db.ts b/src/renderer/store/db.ts index 174d8df08..8dd55c308 100644 --- a/src/renderer/store/db.ts +++ b/src/renderer/store/db.ts @@ -72,6 +72,22 @@ db.version(1).stores({ users: "userId, username, countryCode, status, displayName, clanId, partyId, scopes, isMe", }); +db.version(2) + .stores({ + users: "userId, username, countryCode, status, displayName, partyId, scopes, isMe", + }) + .upgrade(async (tx) => { + await tx + .table("users") + .toCollection() + .modify((user) => { + // TODO: For backward compatibility, I've left it in for now to support older database entries if necessary. But it can be removed later! + if (user.isMe === "undefined") { + user.isMe = false; + } + }); + }); + db.on("ready", function () { console.debug("Database is ready"); }); diff --git a/src/renderer/store/me.store.ts b/src/renderer/store/me.store.ts index e6db7d787..1a05f66c6 100644 --- a/src/renderer/store/me.store.ts +++ b/src/renderer/store/me.store.ts @@ -17,11 +17,14 @@ export const me = reactive< >({ isInitialized: false, userId: "0", - clanId: null, + clanBaseData: null, partyId: null, countryCode: "", displayName: "", status: "offline", + rating: { value: 0 }, + roles: [], + isMe: true, isAuthenticated: false, username: "Player", battleRoomState: {}, @@ -74,11 +77,11 @@ async function changeAccount() { window.tachyon.onEvent("user/self", async (event) => { console.debug(`Received user/self event: ${JSON.stringify(event)}`); if (event && event.user) { - await db.users.where({ isMe: 1 }).modify({ isMe: 0 }); + await db.users.where({ isMe: true }).modify({ isMe: false }); Object.assign(me, event.user); db.users.put({ ...toRaw(me), - isMe: 1, + isMe: true, }); await processFriendData(event.user); @@ -211,7 +214,7 @@ export const friends = { export async function initMeStore() { await db.users - .where({ isMe: 1 }) + .where({ isMe: true }) .first() .then((user) => { if (user) { diff --git a/src/renderer/store/users.store.ts b/src/renderer/store/users.store.ts index b6601e87a..9729b44bf 100644 --- a/src/renderer/store/users.store.ts +++ b/src/renderer/store/users.store.ts @@ -5,6 +5,8 @@ import { db } from "@renderer/store/db"; import { reactive } from "vue"; import { SubsManager } from "@renderer/utils/subscriptions-manager"; +import { UserInfoOkResponseData } from "tachyon-protocol/types"; +import { apply as applyPatch } from "json8-merge-patch"; export const usersStore: { isInitialized: boolean; @@ -23,7 +25,19 @@ export function initUsersStore() { console.warn("Received user/updated event with no userId, skipping update."); return; } - const updated = await db.users.update(user.userId, { ...user }); + + const existingUser = await db.users.get(user.userId); + const updatedUser = applyPatch(existingUser || {}, { + ...user, + clanBaseData: user.clanBaseData + ? { + ...user.clanBaseData, + language: user.clanBaseData.language || "unknown", + } + : undefined, + }); + + const updated = await db.users.update(user.userId, updatedUser); if (updated === 0) { // No records updated, so user doesn't exist - create new user @@ -31,12 +45,20 @@ export function initUsersStore() { userId: user.userId, username: user.username ?? "Unknown User", displayName: user.displayName ?? "Unknown User", - clanId: null, + clanBaseData: user.clanBaseData + ? { + ...user.clanBaseData, + language: user.clanBaseData.language || "unknown", + } + : null, partyId: null, - countryCode: "??", - status: "offline", + countryCode: user.countryCode ?? "??", + status: user.status ?? "offline", + roles: user.roles ?? [], + rating: user.rating ?? null, battleRoomState: {}, - ...user, // Override defaults with actual data + isMe: false, + ...user, }); } }); @@ -44,3 +66,13 @@ export function initUsersStore() { usersStore.isInitialized = true; } + +export async function fetchUserInfo(userId: string): Promise { + try { + const response = await window.tachyon.request("user/info", { userId: userId }); + return response.data; + } catch (error) { + console.error("Error fetching user info:", error); + return null; + } +} diff --git a/src/renderer/views/profile/[userId].vue b/src/renderer/views/profile/[userId].vue index 9ae121537..209900664 100644 --- a/src/renderer/views/profile/[userId].vue +++ b/src/renderer/views/profile/[userId].vue @@ -11,14 +11,37 @@ SPDX-License-Identifier: MIT
-
@@ -36,12 +59,47 @@ import { db } from "@renderer/store/db"; import { useTypedI18n } from "@renderer/i18n"; const { t } = useTypedI18n(); +const roleTranslationKeys: Record = { + contributor: "lobby.views.profile.roleContributor", + admin: "lobby.views.profile.roleAdmin", + moderator: "lobby.views.profile.roleModerator", + tournament_winner: "lobby.views.profile.roleTournamentWinner", + tournament_caster: "lobby.views.profile.roleTournamentCaster", +}; + +function formatRoles(roles: ReadonlyArray | undefined) { + if (!roles?.length) return "—"; + + return roles + .map((role) => { + if (role in roleTranslationKeys) { + return t(roleTranslationKeys[role]); + } + return role; + }) + .join(", "); +} + +const statusTranslationKeys: Record = { + offline: "lobby.views.profile.statusOffline", + menu: "lobby.views.profile.statusMenu", + playing: "lobby.views.profile.statusPlaying", + lobby: "lobby.views.profile.statusLobby", +}; + +function formatStatusLabel(status: string) { + if (status in statusTranslationKeys) { + return t(statusTranslationKeys[status]); + } + return status; +} + const props = defineProps<{ userId: string; }>(); -const user = useDexieLiveQueryWithDeps([() => props.userId], () => { - return db.users.get(props.userId); +const user = useDexieLiveQueryWithDeps([() => props.userId], async () => { + return await db.users.get(props.userId); }); @@ -77,4 +135,79 @@ const user = useDexieLiveQueryWithDeps([() => props.userId], () => { flex-direction: column; gap: 3px; } + +.profile-info { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 12px; + row-gap: 4px; + margin: 0; + + dt { + color: rgba(255, 255, 255, 0.5); + white-space: nowrap; + } + + dd { + margin: 0; + display: flex; + align-items: center; + } +} +.my-profile-note { + color: rgba(255, 255, 255, 0.5); +} + +.status-dot { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 3px; + border-radius: 50%; + vertical-align: middle; + position: relative; + top: -1px; + box-sizing: border-box; +} + +.status-dot--offline { + background-color: #8d8d8d; +} + +.status-dot--menu { + border: 2px solid #48c774; + background-color: transparent; +} + +.status-dot--lobby { + background-color: #48c774; +} + +.status-dot--playing { + background-color: #f1c40f; +} +.btn-find-clan { + align-self: center; + font-family: Rajdhani; + font-weight: bold; + font-size: 1rem; + padding: 3px 20px; + border: none; + border-radius: 2px; + text-align: center; + cursor: pointer; + position: relative; + overflow: hidden; + transition: + transform 0.3s ease, + box-shadow 0.3s ease; +} +.btn-find-clan { + color: #fff; + background: linear-gradient(90deg, #22c55e, #16a34a); + box-shadow: 0 0 15px rgba(34, 197, 94, 0.4); +} +.btn-find-clan:hover { + box-shadow: 0 0 25px rgba(34, 197, 94, 0.6); +}