feat(frontend): add feature flag for tribe multiplayer (@eikomaniac) (#7234)

### Description

Adds a tribeEnabled feature flag to gate tribe multiplayer
functionality.

# Note
Once this PR is approved and merged into `newtribemerge`,
`newtribemerge` can safely be merged into `master`

---------

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
eikomaniac 2025-12-14 14:30:38 +00:00 committed by GitHub
parent 5d229e712b
commit 7a17ba25ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 81 additions and 19 deletions

View file

@ -104,6 +104,7 @@ export const BASE_CONFIGURATION: Configuration = {
},
},
connections: { enabled: false, maxPerUser: 100 },
tribe: { enabled: false },
};
type BaseSchema = {
@ -615,5 +616,12 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema<Configuration> = {
},
},
},
tribe: {
type: "object",
label: "Tribe (Multiplayer)",
fields: {
enabled: { type: "boolean", label: "Enabled" },
},
},
},
};

View file

@ -79,9 +79,9 @@
</div>
</a>
<a
class="textButton view-tribe"
class="textButton view-tribe hidden"
href="/tribe"
onclick="this.blur();"
onclick="this.blur()"
router-link
title="tribe"
>
@ -122,7 +122,7 @@
<i class="fas fa-fw fa-cog"></i>
</div>
</a>
<div></div>
<div class="spacer"></div>
<button class="text showAlerts" onclick="this.blur()">
<div class="icon">
<i class="fas fa-fw fa-bell"></i>

View file

@ -9,7 +9,7 @@
<i class="fas icon fa-keyboard"></i>
input
</a>
<a class="textButton" href="#group_tribe">
<a class="textButton hidden" href="#group_tribe">
<i class="fas icon fa-satellite"></i>
tribe
</a>
@ -654,11 +654,11 @@
<div id="ad-settings-2-small"></div>
</div>
<button id="group_tribe" class="text sectionGroupTitle" group="tribe">
<button id="group_tribe" class="text sectionGroupTitle hidden" group="tribe">
<i class="fas fa-chevron-down"></i>
tribe
</button>
<div class="settingsGroup tribe1">
<div class="settingsGroup tribe hidden">
<div class="section" data-config-name="tribeDelta">
<div class="groupTitle">
<i class="fas fa-exchange-alt"></i>

View file

@ -2,14 +2,13 @@ nav {
font-size: 1rem;
line-height: 1rem;
color: var(--sub-color);
display: grid;
grid-auto-flow: column;
display: flex;
gap: 0.5rem;
// margin-bottom: -0.4rem;
width: -moz-fit-content;
width: fit-content;
width: 100%;
grid-template-columns: auto auto auto auto auto 1fr auto;
.spacer {
flex-grow: 1;
}
button.showAlerts {
position: relative;

View file

@ -1,5 +1,6 @@
import * as UpdateConfig from "../../config";
import { Command, CommandsSubgroup } from "../types";
import { isTribeEnabled } from "../../utils/misc";
const subgroup: CommandsSubgroup = {
title: "Tribe carets...",
@ -38,6 +39,7 @@ const commands: Command[] = [
display: "Tribe carets...",
icon: "fa-i-cursor",
subgroup,
available: (): boolean => isTribeEnabled(),
},
];

View file

@ -1,5 +1,6 @@
import * as UpdateConfig from "../../config";
import { Command, CommandsSubgroup } from "../types";
import { isTribeEnabled } from "../../utils/misc";
const subgroup: CommandsSubgroup = {
title: "Tribe delta...",
@ -38,6 +39,7 @@ const commands: Command[] = [
display: "Tribe delta...",
icon: "fa-exchange-alt",
subgroup,
available: (): boolean => isTribeEnabled(),
},
];

View file

@ -3,7 +3,7 @@ import * as DB from "./db";
import * as Notifications from "./elements/notifications";
import { isAuthenticated } from "./firebase";
import { canSetFunboxWithConfig } from "./test/funbox/funbox-validation";
import { isDevEnvironment, reloadAfter } from "./utils/misc";
import { isDevEnvironment, isTribeEnabled, reloadAfter } from "./utils/misc";
import * as ConfigSchemas from "@monkeytype/schemas/configs";
import { roundTo1 } from "@monkeytype/util/numbers";
import { capitalizeFirstLetter } from "./utils/strings";
@ -209,12 +209,18 @@ export const configMetadata: ConfigMetadataObject = {
displayString: "tribe delta",
changeRequiresRestart: false,
group: "tribe",
isBlocked: () => {
return !isTribeEnabled();
},
},
tribeCarets: {
icon: "fa-users",
displayString: "tribe carets",
changeRequiresRestart: false,
group: "tribe",
isBlocked: () => {
return !isTribeEnabled();
},
},
// behavior

View file

@ -9,6 +9,7 @@ import * as Notifications from "../elements/notifications";
import tribeSocket from "../tribe/tribe-socket";
import { setAutoJoin } from "../tribe/tribe-auto-join";
import * as NavigationEvent from "../observables/navigation-event";
import { isTribeEnabled } from "../utils/misc";
//source: https://www.youtube.com/watch?v=OstALBk-jTc
// https://www.youtube.com/watch?v=OstALBk-jTc
@ -59,7 +60,7 @@ const routes: Route[] = [
return;
}
if (TribeState.getState() >= 5) {
if (isTribeEnabled() && TribeState.getState() >= 5) {
if (TribeState.getState() === 22 && TribeState.getSelf()?.isLeader) {
tribeSocket.out.room.backToLobby();
} else {
@ -176,6 +177,11 @@ const routes: Route[] = [
{
path: "/tribe",
load: async (params, options): Promise<void> => {
if (!isTribeEnabled()) {
await navigate("/", options);
return;
}
if (options?.tribeOverride === true) {
await PageController.change("tribe", {
tribeOverride: options?.tribeOverride ?? false,
@ -198,7 +204,12 @@ const routes: Route[] = [
},
{
path: "/tribe/:roomId",
load: async (params): Promise<void> => {
load: async (params, options): Promise<void> => {
if (!isTribeEnabled()) {
await navigate("/", options);
return;
}
setAutoJoin(params["roomId"] as string);
await PageController.change("tribe", {
force: true,
@ -215,6 +226,7 @@ export async function navigate(
options = {} as NavigationEvent.NavigateOptions,
): Promise<void> {
if (
isTribeEnabled() &&
TribeState.getState() > 5 &&
TribeState.getState() < 22 &&
!options?.tribeOverride

View file

@ -251,8 +251,10 @@ async function initGroups(): Promise<void> {
"customBackgroundSize",
"button",
);
groups["tribeDelta"] = new SettingsGroup("tribeDelta", "button");
groups["tribeCarets"] = new SettingsGroup("tribeCarets", "button");
if (Misc.isTribeEnabled()) {
groups["tribeDelta"] = new SettingsGroup("tribeDelta", "button");
groups["tribeCarets"] = new SettingsGroup("tribeCarets", "button");
}
}
async function fillSettingsPage(): Promise<void> {

View file

@ -10,7 +10,7 @@ import { getActiveFunboxesWithFunction } from "./test/funbox/list";
import { configLoadPromise } from "./config";
import { authPromise } from "./firebase";
import { animate } from "animejs";
import { onDocumentReady, qs } from "./utils/dom";
import { onDocumentReady, qs, qsa } from "./utils/dom";
onDocumentReady(async () => {
await configLoadPromise;
@ -44,6 +44,18 @@ onDocumentReady(async () => {
if (!ServerConfiguration.get()?.connections.enabled) {
qs(".accountButtonAndMenu .goToFriends")?.addClass("hidden");
}
if (Misc.isTribeEnabled()) {
qs("header nav .textButton.view-tribe")?.removeClass("hidden");
for (const el of qsa(".pageSettings [group='tribe']")) {
el.removeClass("hidden");
}
for (const el of qsa(".pageSettings .settingsGroup.tribe")) {
el.removeClass("hidden");
}
qs(
".pageSettings .settingsGroup.quickNav button[href='#group_tribe']",
)?.removeClass("hidden");
}
});
}
MonkeyPower.init();

View file

@ -22,7 +22,12 @@ import * as Random from "../utils/random";
import TribeSocket from "./tribe-socket";
import * as ActivePage from "../states/active-page";
import * as TribeState from "./tribe-state";
import { escapeRegExp, escapeHTML, isDevEnvironment } from "../utils/misc";
import {
escapeRegExp,
escapeHTML,
isTribeEnabled,
isDevEnvironment,
} from "../utils/misc";
import * as Time from "../states/time";
import * as TestWords from "../test/test-words";
import * as TestStats from "../test/test-stats";
@ -119,6 +124,8 @@ function updateState(newState: number): void {
}
export async function init(): Promise<void> {
if (!isTribeEnabled()) return;
TribePagePreloader.updateIcon("circle-notch", true);
// TribePagePreloader.updateText("Waiting for login");
// await AccountController.authPromise;

View file

@ -5,6 +5,7 @@ export type EnvConfig = {
recaptchaSiteKey: string;
quickLoginEmail: string | undefined;
quickLoginPassword: string | undefined;
forceTribe: boolean;
};
declare module "virtual:env-config" {

View file

@ -1,6 +1,7 @@
import * as Loader from "../elements/loader";
import * as Random from "../utils/random";
import { envConfig } from "virtual:env-config";
import * as ServerConfiguration from "../ape/server-configuration";
import { lastElementFromArray } from "./arrays";
import { Config } from "@monkeytype/schemas/configs";
import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared";
@ -492,6 +493,11 @@ export function isDevEnvironment(): boolean {
return envConfig.isDevelopment;
}
export function isTribeEnabled(): boolean {
if (envConfig.forceTribe) return true;
return ServerConfiguration.get()?.tribe?.enabled ?? false;
}
export function zipfyRandomArrayIndex(dictLength: number): number {
/**
* get random index based on probability distribution of Zipf's law,

View file

@ -27,6 +27,7 @@ export function envConfig(options: {
recaptchaSiteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
quickLoginEmail: options.env["QUICK_LOGIN_EMAIL"],
quickLoginPassword: options.env["QUICK_LOGIN_PASSWORD"],
forceTribe: options.env["FORCE_TRIBE"] === "true",
};
const prodConfig: EnvConfig = {
@ -39,6 +40,7 @@ export function envConfig(options: {
quickLoginEmail: undefined,
quickLoginPassword: undefined,
clientVersion: options.clientVersion,
forceTribe: options.env["FORCE_TRIBE"] === "true",
};
const envConfig = options.isDevelopment ? devConfig : prodConfig;

View file

@ -127,5 +127,8 @@ export const ConfigurationSchema = z.object({
enabled: z.boolean(),
maxPerUser: z.number().int().nonnegative(),
}),
tribe: z.object({
enabled: z.boolean(),
}),
});
export type Configuration = z.infer<typeof ConfigurationSchema>;