diff --git a/app/features/sendouq/core/likes.server.ts b/app/features/sendouq/core/likes.server.ts index 392d9ee56..dca24be6c 100644 --- a/app/features/sendouq/core/likes.server.ts +++ b/app/features/sendouq/core/likes.server.ts @@ -8,19 +8,19 @@ import { } from "../q-constants"; import { refreshSendouQInstance, SendouQ } from "./SendouQ.server"; -/** Cancels every pending challenge involving the user's active full group. Partial groups merge rather than start a match on accept, so their preferences are not locked in yet. */ +/** Cancels every pending challenge involving the user's active full group, returning whether any were, which also means the instance was refreshed. Partial groups merge rather than start a match on accept, so their preferences are not locked in yet. */ export async function cancelActiveGroupLikes(userId: number) { const ownGroup = SendouQ.findOwnGroup(userId); - if (!ownGroup) return; - if (ownGroup.status !== "ACTIVE" || ownGroup.matchId) return; - if (ownGroup.members.length !== FULL_GROUP_SIZE) return; + if (!ownGroup) return false; + if (ownGroup.status !== "ACTIVE" || ownGroup.matchId) return false; + if (ownGroup.members.length !== FULL_GROUP_SIZE) return false; const likes = await SQGroupRepository.findAllLikesByGroupId(ownGroup.id); const affectedGroupIds = R.unique([ ...likes.given.map((like) => like.groupId), ...likes.received.map((like) => like.groupId), ]); - if (affectedGroupIds.length === 0) return; + if (affectedGroupIds.length === 0) return false; await SQGroupRepository.deleteAllLikesByGroupId(ownGroup.id); @@ -40,4 +40,6 @@ export async function cancelActiveGroupLikes(userId: number) { ) ?? [], ), ); + + return true; } diff --git a/app/features/settings/actions/settings.server.test.ts b/app/features/settings/actions/settings.server.test.ts new file mode 100644 index 000000000..fe9f7ff3a --- /dev/null +++ b/app/features/settings/actions/settings.server.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory"; +import * as UserFactory from "~/db/seed/factories/UserFactory"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; +import { + refreshSendouQInstance, + SendouQ, +} from "~/features/sendouq/core/SendouQ.server"; +import type { MainWeaponId } from "~/modules/in-game-lists/types"; +import { wrappedAction } from "~/utils/Test"; +import type { settingsActionSchema } from "../settings-schemas.server"; +import { action } from "./settings.server"; + +vi.mock("~/features/chat/ChatSystemMessage.server", () => ({ + send: vi.fn(), + notifyStatusChanged: vi.fn(), + notifyNotificationsChanged: vi.fn(), +})); + +const OLD_WEAPON_ID: MainWeaponId = 0; +const NEW_WEAPON_ID: MainWeaponId = 40; + +const settingsAction = wrappedAction({ + action, + isJsonSubmission: true, +}); + +const users = UserFactory.pool(); + +const actorId = () => users.id(1); + +const updateWeaponPool = (weaponId: MainWeaponId) => + settingsAction( + { + _action: "UPDATE_MATCH_PROFILE", + mapModePreferences: { modes: [], pool: [] }, + weaponPool: [{ id: weaponId, isFavorite: false }], + vc: "NO", + languages: [], + noScreen: false, + }, + { user: actorId() }, + ); + +const ownWeaponsInQueue = () => + SendouQ.findOwnGroup(actorId()) + ?.members.find((member) => member.id === actorId()) + ?.weapons?.map((weapon) => weapon.weaponSplId); + +describe("settings action: UPDATE_MATCH_PROFILE", () => { + beforeEach(async () => { + await users.create(1, null, { + matchProfile: { weaponPool: [{ id: OLD_WEAPON_ID, isFavorite: false }] }, + }); + }); + + test("a new weapon pool is shown to the queue right away", async () => { + await SQGroupFactory.create({ memberUserIds: [actorId()] }); + await refreshSendouQInstance(); + + await updateWeaponPool(NEW_WEAPON_ID); + + expect(ownWeaponsInQueue()).toEqual([NEW_WEAPON_ID]); + }); + + test("updates the weapon pool of a user who is not queueing", async () => { + await updateWeaponPool(NEW_WEAPON_ID); + + const weaponPool = await MatchProfileRepository.findWeaponPoolByUserId( + actorId(), + ); + expect(weaponPool.map((weapon) => weapon.weaponSplId)).toEqual([ + NEW_WEAPON_ID, + ]); + }); +}); diff --git a/app/features/settings/actions/settings.server.ts b/app/features/settings/actions/settings.server.ts index 55e7834c1..051542e81 100644 --- a/app/features/settings/actions/settings.server.ts +++ b/app/features/settings/actions/settings.server.ts @@ -1,7 +1,16 @@ import type { ActionFunctionArgs } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; +import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; import { cancelActiveGroupLikes } from "~/features/sendouq/core/likes.server"; +import { + refreshSendouQInstance, + SendouQ, +} from "~/features/sendouq/core/SendouQ.server"; +import { + SENDOUQ_LOOKING_CHANNEL, + sqGroupChannel, +} from "~/features/sendouq/q-constants"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { parseFormData } from "~/form/parse.server"; import { isSupporter } from "~/modules/permissions/utils"; @@ -78,8 +87,13 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); // challenges are based on the preferences shown at the time, so changing them undoes pending ones - if (mapModePreferencesChanged || noScreenChanged) { - await cancelActiveGroupLikes(user.id); + const likesCancelled = + mapModePreferencesChanged || noScreenChanged + ? await cancelActiveGroupLikes(user.id) + : false; + + if (!likesCancelled) { + await showMatchProfileToOthers(user.id); } break; } @@ -90,3 +104,16 @@ export const action = async ({ request }: ActionFunctionArgs) => { return null; }; + +/** SendouQ pages serve group members from the in-memory instance, so a match profile change stays invisible to everyone else till it is rebuilt. */ +async function showMatchProfileToOthers(userId: number) { + const ownGroup = SendouQ.findOwnGroup(userId); + if (!ownGroup) return; + + await refreshSendouQInstance(); + + ChatSystemMessage.send([ + { channel: sqGroupChannel(ownGroup.id) }, + { channel: SENDOUQ_LOOKING_CHANNEL }, + ]); +} diff --git a/changelog/2026-09-20-sendouq-match-profile-visible-right-away.md b/changelog/2026-09-20-sendouq-match-profile-visible-right-away.md new file mode 100644 index 000000000..3247266a0 --- /dev/null +++ b/changelog/2026-09-20-sendouq-match-profile-visible-right-away.md @@ -0,0 +1,5 @@ +--- +navItem: sendouq +type: bug +--- +Changes to your SendouQ info (weapon pool, languages, voice chat) now show up to other players right away instead of after a delay