feat(ui): lobby site-style sidebar + profile screen (#47)
- Wrap lobby and profile in a shared `lobby-shell.svelte` chrome: page-list sidebar (Overview/Profile) and a top "Player-xxxx" identity strip mirroring the project site's monospace look. - Strip the legacy `lobby.title`, device-session-id `<code>`, and `lobby.greeting` paragraph; the identity strip both names the user and opens the profile editor. - Add a top-level `profile` AppScreen with a three-field form (`display_name`, `preferred_language`, `time_zone`) backed by a new `src/api/account.ts` wrapper around `user.account.get`, `user.profile.update`, and `user.settings.update`. Saving switches the active i18n locale in-place when the new preferred language is one the UI ships translations for. - Update e2e fixture + auth-flow / lobby-flow specs to use the new `lobby-account-name` testid and wait for the loaded identity before releasing pending `SubscribeEvents` (webkit revocation race). New `profile-screen.spec.ts` covers navigation, edit-save, and cancel. - Sync `ui/docs/lobby.md` and `ui/docs/navigation.md` to the new layout. Closes #47
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// Typed wrappers around `GalaxyClient.executeCommand` for the user-
|
||||
// account command catalog. Each wrapper builds a FlatBuffers request
|
||||
// payload via the generated TS bindings, calls `executeCommand`, then
|
||||
// decodes the `AccountResponse` reply. Errors with a non-`ok`
|
||||
// `result_code` surface as a thrown `AccountError` carrying the
|
||||
// canonical backend code (`invalid_request`, `subject_not_found`,
|
||||
// `forbidden`, `conflict`, `internal_error`).
|
||||
|
||||
import { Builder, ByteBuffer } from "flatbuffers";
|
||||
|
||||
import type { GalaxyClient } from "./galaxy-client";
|
||||
import {
|
||||
AccountResponse,
|
||||
AccountView,
|
||||
ErrorResponse as FbsErrorResponse,
|
||||
GetMyAccountRequest,
|
||||
UpdateMyProfileRequest,
|
||||
UpdateMySettingsRequest,
|
||||
} from "../proto/galaxy/fbs/user";
|
||||
|
||||
const RESULT_CODE_OK = "ok";
|
||||
|
||||
export class AccountError extends Error {
|
||||
readonly code: string;
|
||||
readonly resultCode: string;
|
||||
|
||||
constructor(resultCode: string, code: string, message: string) {
|
||||
super(message);
|
||||
this.name = "AccountError";
|
||||
this.resultCode = resultCode;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
userId: string;
|
||||
email: string;
|
||||
userName: string;
|
||||
displayName: string;
|
||||
preferredLanguage: string;
|
||||
timeZone: string;
|
||||
declaredCountry: string;
|
||||
}
|
||||
|
||||
export async function getMyAccount(client: GalaxyClient): Promise<Account> {
|
||||
const builder = new Builder(32);
|
||||
GetMyAccountRequest.startGetMyAccountRequest(builder);
|
||||
builder.finish(GetMyAccountRequest.endGetMyAccountRequest(builder));
|
||||
const payload = await execute(client, "user.account.get", builder.asUint8Array());
|
||||
return decodeAccountResponse(payload);
|
||||
}
|
||||
|
||||
export async function updateMyProfile(
|
||||
client: GalaxyClient,
|
||||
displayName: string,
|
||||
): Promise<Account> {
|
||||
const builder = new Builder(128);
|
||||
const displayNameOff = builder.createString(displayName);
|
||||
UpdateMyProfileRequest.startUpdateMyProfileRequest(builder);
|
||||
UpdateMyProfileRequest.addDisplayName(builder, displayNameOff);
|
||||
builder.finish(UpdateMyProfileRequest.endUpdateMyProfileRequest(builder));
|
||||
const payload = await execute(client, "user.profile.update", builder.asUint8Array());
|
||||
return decodeAccountResponse(payload);
|
||||
}
|
||||
|
||||
export async function updateMySettings(
|
||||
client: GalaxyClient,
|
||||
preferredLanguage: string,
|
||||
timeZone: string,
|
||||
): Promise<Account> {
|
||||
const builder = new Builder(128);
|
||||
const preferredLanguageOff = builder.createString(preferredLanguage);
|
||||
const timeZoneOff = builder.createString(timeZone);
|
||||
UpdateMySettingsRequest.startUpdateMySettingsRequest(builder);
|
||||
UpdateMySettingsRequest.addPreferredLanguage(builder, preferredLanguageOff);
|
||||
UpdateMySettingsRequest.addTimeZone(builder, timeZoneOff);
|
||||
builder.finish(UpdateMySettingsRequest.endUpdateMySettingsRequest(builder));
|
||||
const payload = await execute(client, "user.settings.update", builder.asUint8Array());
|
||||
return decodeAccountResponse(payload);
|
||||
}
|
||||
|
||||
async function execute(
|
||||
client: GalaxyClient,
|
||||
messageType: string,
|
||||
payloadBytes: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const result = await client.executeCommand(messageType, payloadBytes);
|
||||
if (result.resultCode !== RESULT_CODE_OK) {
|
||||
throw decodeAccountError(result.resultCode, result.payloadBytes);
|
||||
}
|
||||
return result.payloadBytes;
|
||||
}
|
||||
|
||||
function decodeAccountError(resultCode: string, payload: Uint8Array): AccountError {
|
||||
let code = resultCode;
|
||||
let message = resultCode;
|
||||
try {
|
||||
const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload));
|
||||
const body = errorResponse.error();
|
||||
if (body) {
|
||||
code = body.code() ?? resultCode;
|
||||
message = body.message() ?? resultCode;
|
||||
}
|
||||
} catch (_err) {
|
||||
// fall through with the raw result code
|
||||
}
|
||||
return new AccountError(resultCode, code, message);
|
||||
}
|
||||
|
||||
function decodeAccountResponse(payload: Uint8Array): Account {
|
||||
if (payload.length === 0) {
|
||||
throw new AccountError("internal_error", "internal_error", "empty account response");
|
||||
}
|
||||
const response = AccountResponse.getRootAsAccountResponse(new ByteBuffer(payload));
|
||||
const view = response.account();
|
||||
if (view === null) {
|
||||
throw new AccountError("internal_error", "internal_error", "account missing in response");
|
||||
}
|
||||
return decodeAccountView(view);
|
||||
}
|
||||
|
||||
function decodeAccountView(view: AccountView): Account {
|
||||
return {
|
||||
userId: view.userId() ?? "",
|
||||
email: view.email() ?? "",
|
||||
userName: view.userName() ?? "",
|
||||
displayName: view.displayName() ?? "",
|
||||
preferredLanguage: view.preferredLanguage() ?? "",
|
||||
timeZone: view.timeZone() ?? "",
|
||||
declaredCountry: view.declaredCountry() ?? "",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user