Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions composables/useChatBlocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { watch } from "vue";
import { useChatTabs } from "~/composables/useChatTabs";
import { directRoomId, directTabId } from "~/composables/useDirectMessages";
import { usePlayerBlocks } from "~/composables/usePlayerBlocks";
import { useAuthStore } from "~/stores/AuthStore";
import socket from "~/web-sockets/Socket";

// The api sends no event on a block or an unblock, only changes what it sends
// from then on, so what this client already holds is its own to hide. A block
// made on another device arrives through the same subscription.
export function useChatBlocks() {
const { blocks, loaded } = usePlayerBlocks();
const { closeTab } = useChatTabs();
const authStore = useAuthStore();

watch(
() => (loaded.value ? blocks.value : null),
(rows) => {
if (!rows) {
return;
}

const { added } = socket.setHiddenAuthors(
rows.map((row) => String(row.blocked_steam_id)),
);

const mySteamId = authStore.me?.steam_id;
if (!mySteamId) {
return;
}

for (const steamId of added) {
closeTab(directTabId(directRoomId(mySteamId, steamId)));
}
},
{ immediate: true },
);
}
9 changes: 9 additions & 0 deletions composables/useChatTabSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,16 @@ export function useChatTabSetup() {
// /chat/direct/conversations, which counts the whole thread rather than
// the last 200 messages of it. Only the ids behind it are filled in, so a
// deleted message can come off the badge.
//
// A room on screen counts nothing, as the live handler above doesn't. The
// cursor only moves when the tab is opened, so a rejoin (an unblock
// rejoins every room) would otherwise badge lines read while it was open.
lobby.on("lobby:messages", (messages: any[]) => {
if (isChatTabOnScreen(tab.id)) {
setUnread(tab.id, 0);
return;
}

if (tab.type === "direct") {
const count = unreadCounts.value[tab.id] ?? 0;

Expand Down
16 changes: 14 additions & 2 deletions composables/useIncomingDirectMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export function useIncomingDirectMessages() {
unread = 0,
position?: number,
) {
if (socket.hidesAuthor(peer?.steam_id)) {
return;
}

openTab({
id: directTabId(roomId),
label: peer?.name ?? peer?.steam_id ?? roomId,
Expand Down Expand Up @@ -114,15 +118,23 @@ export function useIncomingDirectMessages() {
return;
}

const peerId =
data.from?.steam_id ?? peerSteamId(data.roomId, steamId) ?? "";

// A blocked player's message can still be on its way when the block
// lands, and it must neither flash nor badge.
if (socket.hidesAuthor(peerId)) {
return;
}

// The room's own `lobby:chat` carries the same id when its tab is open.
useTabFlash().signalChat("direct", data.message);

// Deliberately does not inject the message: opening the tab makes
// useChatTabSetup join the room, and the join's history snapshot delivers
// it (deduped by chatMessageKey either way).
ensureTab(data.roomId, {
steam_id:
data.from?.steam_id ?? peerSteamId(data.roomId, steamId) ?? "",
steam_id: peerId,
name: data.from?.name,
avatar_url: data.from?.avatar_url,
});
Expand Down
2 changes: 2 additions & 0 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { e_player_roles_enum } from "~/generated/zeus";
import { useGtm } from "@/layouts/composables/useGtm";
import { useChatTabSetup } from "~/composables/useChatTabSetup";
import { useChatPresence } from "~/composables/useChatPresence";
import { useChatBlocks } from "~/composables/useChatBlocks";

const AppSidebar = defineAsyncComponent(
() => import("@/components/AppSidebar.vue"),
Expand Down Expand Up @@ -53,6 +54,7 @@ useChatTabSetup();
useChatTabPersistence();
useIncomingDirectMessages();
useChatPresence();
useChatBlocks();

const route = useRoute();
const authStore = useAuthStore();
Expand Down
23 changes: 22 additions & 1 deletion pages/chat/[tabId].vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
setPageChatFocus,
useChatPresence,
} from "~/composables/useChatPresence";
import { useChatBlocks } from "~/composables/useChatBlocks";
import { peerSteamId } from "~/composables/useDirectMessages";
import { usePlayerBlocks } from "~/composables/usePlayerBlocks";
import { chatThreadKey } from "~/utilities/chatThread";
import { useMatchLobbyStore } from "~/stores/MatchLobbyStore";
import { matchTeamLobbyId } from "~/utilities/matchTeamLobby";
Expand Down Expand Up @@ -80,7 +83,22 @@ const currentTab = computed<ChatTab | null>(() => {
return tabFromSession.value ?? tabFromQuery.value ?? tabFromId.value;
});

const hasTab = computed(() => currentTab.value !== null);
const { isBlocked } = usePlayerBlocks();

// The sidebar closes its tab for a blocked player, but this window has no tab
// to close.
const blockedPeer = computed(() => {
const tab = currentTab.value;
const mySteamId = useAuthStore().me?.steam_id;

if (tab?.type !== "direct" || !mySteamId) {
return false;
}

return isBlocked(peerSteamId(tab.lobbyId, String(mySteamId)));
});

const hasTab = computed(() => currentTab.value !== null && !blockedPeer.value);

// Same second room the sidebar and the match page offer. This window can be
// opened cold from a bare URL, so the match is looked up in the store rather
Expand Down Expand Up @@ -131,6 +149,9 @@ const thread = computed(() =>
);

useChatPresence();
// Nor does its block watcher, so this window would keep showing the lines of
// a player blocked while it was open.
useChatBlocks();

watch(
thread,
Expand Down
99 changes: 99 additions & 0 deletions tests/components/ChatLobby.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import ChatMessage from "~/components/chat/ChatMessage.vue";
import { useAuthStore } from "~/stores/AuthStore";
import socket, { type LobbyMessage } from "~/web-sockets/Socket";

const { playNotificationSound } = vi.hoisted(() => ({
playNotificationSound: vi.fn(),
}));

vi.mock("~/composables/useSound", () => ({
useSound: () => ({ playNotificationSound }),
}));

vi.mock("~/graphql/getGraphqlClient", () => ({
default: () => ({
query: vi.fn().mockResolvedValue({ data: {} }),
Expand Down Expand Up @@ -310,3 +318,94 @@ describe("ChatLobby moderation", () => {
expect(lobby.lastReadMessageCount).toBe(1);
});
});

describe("ChatLobby blocked authors", () => {
const THIRD = "76561198000000003";

afterEach(() => {
socket.setHiddenAuthors([]);
playNotificationSound.mockClear();
});

it("stays silent for a blocked player's live line", async () => {
signIn({ role: "user" });

const wrapper = await mountLobby({
lobbyId: "blocked-live",
playNotificationSound: true,
});
socket.setHiddenAuthors([OTHER]);

socket.emit("lobby:match:blocked-live:chat", line("hidden", 0));
await flushPromises();

expect(playNotificationSound).not.toHaveBeenCalled();
expect(wrapper.emitted("message-received")).toBeUndefined();
expect(wrapper.text()).not.toContain("line hidden");

socket.emit("lobby:match:blocked-live:chat", line("shown", 1, THIRD));
await flushPromises();

expect(playNotificationSound).toHaveBeenCalledTimes(1);
expect(wrapper.emitted("message-received")).toHaveLength(1);
expect(wrapper.text()).toContain("line shown");
});

it("keeps the New line on the same message when a blocked player's lines go", async () => {
signIn({ role: "user" });

const wrapper = await mountLobby({
lobbyId: "blocked-divider",
isGlobalContext: true,
});
socket.emit("lobby:match:blocked-divider:messages", {
messages: [
line("a", 0),
line("b", 1, THIRD),
line("c", 2),
line("d", 3, THIRD),
line("e", 4),
],
});
await flushPromises();

const lobby = wrapper.vm as any;
lobby.lastReadMessageCount = 4;

socket.setHiddenAuthors([OTHER]);
await flushPromises();

expect(lobby.messages.map((m: LobbyMessage) => m.id)).toEqual(["b", "d"]);
expect(lobby.lastReadMessageCount).toBe(2);
expect(wrapper.text()).not.toContain("line a");
});

it("keeps the New line in place when both merged rooms lose their lines", async () => {
signIn({ role: "user" });

const wrapper = await mountLobby({
lobbyId: "blocked-merged",
teamLobbyId: "blocked-merged:lineup-1",
});
socket.emit("lobby:match:blocked-merged:messages", {
messages: [line("e1", 0), line("e2", 2, THIRD), line("e3", 4)],
});
socket.emit("lobby:match_team:blocked-merged:lineup-1:messages", {
messages: [line("t1", 1, THIRD), line("t2", 3), line("t3", 5, THIRD)],
});
await flushPromises();

const lobby = wrapper.vm as any;
lobby.lastReadMessageCount = 4;

socket.setHiddenAuthors([OTHER]);
await flushPromises();

expect(lobby.messages.map((m: LobbyMessage) => m.id)).toEqual([
"t1",
"e2",
"t3",
]);
expect(lobby.lastReadMessageCount).toBe(2);
});
});
1 change: 1 addition & 0 deletions tests/components/ChatPanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ vi.mock("~/web-sockets/Socket", async (importOriginal) => ({
};
},
joinLobby: () => ({ messages: [], on() {}, leave() {} }),
hidesAuthor: () => false,
markLobbyRead() {},
},
}));
Expand Down
Loading
Loading