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
34 changes: 20 additions & 14 deletions apps/backoffice/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ burger menu (see [The menu](#the-menu)), by group:
- **Map**: Reassign a country, Paint random tiles, Download a replay (see [The map tools](#the-map-tools)).
- **Ops**: Reconcile titles, the run after a deploy (see [Ops](#ops)).

What is left of the plan is a live chat view: `ListMessages` pages through the chat (the report page reads it),
and no admin method streams it.
What is left of the plan is a live chat view: `ListMessages` pages through the chat (the report and player pages
read it), and no admin method streams it.

It is a third app, not a page of the game: **it imports nothing from `apps/frontend`** and has no Three.js (see
the root `CLAUDE.md`, "Independence of the apps").
Expand Down Expand Up @@ -144,20 +144,21 @@ note and keeps its text).
the oldest or the newest message shown. A page shorter than asked is the end, and its button goes. The reported
message shows while they load, marked ("Reported message", `aria-current`), and stays shown if they fail.
`GetMessage` names no author, so its line takes the name the report gives its account.
- **What the reported account said, in its conversations** (`TheirMessages`, "What Ana said"), for every report: a
report from a player's card names no message, and one line cannot be judged alone. `ListMessages` with the
account's id reads its messages, 20 at a time (`THEIR_PAGE`), then `moreConversations` (`theirConversations.ts`)
reads the whole chat around each, `AROUND` (5) messages each side, and shows them at once, newest first, three at a
time (`CONVERSATIONS`), "Show more conversations" for the next. **One of their messages a conversation already holds
gets none of its own, and a conversation that reaches the next one joins it**, so an exchange of three insults is
one conversation, read with two calls. Their lines are marked (`line-theirs`) and tagged `reported`, the reporter's
`reporter`. Each conversation grows with "Show earlier" and "Show later". Nothing kept is "Nothing they said in the
chat is kept."
- **What the reported account said, in its conversations** (`chat/TheirMessages`, "What Ana said"), for every
report: a report from a player's card names no message, and one line cannot be judged alone. `ListMessages` with
the account's id reads its messages, 20 at a time (`THEIR_PAGE`), then `moreConversations`
(`theirConversations.ts`) reads the whole chat around each, `AROUND` (5) messages each side, and shows them at
once, newest first, three at a time (`CONVERSATIONS`), "Show more conversations" for the next. **One of their
messages a conversation already holds gets none of its own, and a conversation that reaches the next one joins
it**, so an exchange of three insults is one conversation, read with two calls. Their lines are marked
(`line-theirs`) and tagged `reported`, the reporter's `reporter`. Each conversation grows with "Show earlier" and
"Show later". Nothing kept is "Nothing they said in the chat is kept."
- **Each line** has its author's name as the chat names it now, its country and its time (with the date when it
is not the reported message's day), what it answers ("In reply to …"), and is struck through with when it was
removed. **What the reporter and the reported account said is tagged** `reporter` and `reported`, by account
id. An author's name opens it on the player page (`OpenPlayer`), so a third player in the conversation is a
click away. A page past the chat's retention is `not_found`: "That part of the chat is no longer kept."
click away; the player already open there stays open, with its row. A page past the chat's retention is
`not_found`: "That part of the chat is no longer kept."
- **The actions, all on the reported account id**, never its name: remove the message (`RemoveMessage`, only while
it is shown), mute (`Mute`, an hour by default), ban (`BanPlayer`, no duration by default, so the antibot's
ladder picks the next step) and inspect (`InspectPlayer`). Then close the report (`DecideReport`): Acted or
Expand Down Expand Up @@ -218,6 +219,10 @@ address over IPv4, the /64 over IPv6) and checks.
heading is its flag and its name: the name the profile answers, which is the name it holds now, over the one the
page that opened it knew; the flag of its row, or else the country it took the most tiles for. `not_found` is an
account the game keeps nothing about. An address has no profile, and is not asked.
- **An account's page shows what it said, in its conversations** (`chat/TheirMessages`, as a report does): its
messages by its account id, the chat around each, three conversations at a time. Its lines are marked
(`line-theirs`), and no line is tagged: there is no report. An address has no messages, since the chat keeps them
by account, and is not asked.
- **The page inspects on open** (`InspectPlayer`): the antibot's readings, its ban, its pardon and its clicks. The antibot off
reads as such, not as a failure. A ban or an unban reads it again.
- **The tools**: ban (`BanPlayer`, the ladder's next step by default), unban (`UnbanPlayer`, which forgets the
Expand Down Expand Up @@ -289,8 +294,9 @@ src/
components/ Menu, ConfirmAction, FailureNotice, AccountId and Copyable (with Copy), Country, CountryField,
Fact, Person, When
support/ Ideas and bugs: NoteList, Attachments, labels.ts (names, outcomes, device lines, sizes)
reports/ Reports: list, detail, the report's own actions, Conversation and TheirMessages over
chatPages.ts (a page of ListMessages, the parties a report names) and theirConversations.ts
chat/ Conversation and TheirMessages, which reports/ and players/ show, over chatPages.ts (a page of
ListMessages, the parties a report names) and theirConversations.ts
reports/ Reports: list, detail, the report's own actions
players/ Players: list, PlayerName, PlayerView, ProfilePanel, the tools on one player, actions.ts
map/ Reassign a country, Paint random tiles, Download a replay
ops/ Reconcile titles
Expand Down
5 changes: 3 additions & 2 deletions apps/backoffice/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ReassignPanel} from "./map/ReassignPanel.tsx"
import {ReplayPanel} from "./map/ReplayPanel.tsx"
import {OpenPlayer, Opened, PageId, titleOf} from "./navigation.ts"
import {TitlesPanel} from "./ops/TitlesPanel.tsx"
import {idOf} from "./players/caller.ts"
import {PlayersPage} from "./players/PlayersPage.tsx"
import {ReportsView} from "./reports/ReportsView.tsx"
import {Callback} from "./signIn/callback.ts"
Expand Down Expand Up @@ -47,9 +48,9 @@ function Console({operator, onSignedOut}: {operator: string, onSignedOut: () =>
const [page, setPage] = usePage()
const [player, setPlayer] = useState<Opened | undefined>(undefined)

// A report opens its player on the Players page, where every tool on it is.
// A report opens its player on the Players page, where every tool on it is. The player already open keeps its row.
const openPlayer = useCallback((opened: Opened) => {
setPlayer(opened)
setPlayer(shown => shown !== undefined && idOf(shown.caller) === idOf(opened.caller) ? shown : opened)
setPage("players")
}, [setPage])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type Props = {
chat: ChatAdmin
anchor: KeptMessage
removedAt: Date | undefined
parties: Parties
// What the anchor is to the operator: "Reported message", "Their message".
parties: Parties | undefined
// What the anchor is to the operator: "Reported message", "This message".
mark: string
}

Expand Down Expand Up @@ -55,7 +55,7 @@ type PagesProps = {
chat: ChatAdmin
first: {before: Page, after: Page}
reported: Reported
parties: Parties
parties: Parties | undefined
}

function Pages({chat, first, reported, parties}: PagesProps) {
Expand Down Expand Up @@ -118,7 +118,7 @@ type LinesProps = {
before: KeptMessage[]
reported: Reported
after: KeptMessage[]
parties: Parties
parties: Parties | undefined
}

function Lines({before, reported, after, parties}: LinesProps) {
Expand All @@ -134,7 +134,7 @@ function Lines({before, reported, after, parties}: LinesProps) {

type LineProps = {
line: KeptMessage
// Who the report names, to tag what each said; none when every line is one party's.
// Who the report names, to tag what each said; none off a report, or when every line is one party's.
parties: Parties | undefined
// The anchor's day: a line sent on another one shows its date too, as the anchor does.
day: Date | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Props = {
chat: ChatAdmin
accountId: string
name: string
parties: Parties
parties: Parties | undefined
}

type Built = {
Expand All @@ -34,8 +34,8 @@ async function theirPage(chat: ChatAdmin, accountId: string, before: string, sig
return {newestFirst: [...page.messages].reverse(), more: page.more, oldest: page.messages[0]?.messageId ?? before}
}

// What the reported account said in the chat, in the conversations it said it in, whatever the report names: a
// report from a player's card names no message.
// What one account said in the chat, in the conversations it said it in, by its account id: a report from a
// player's card names no message.
export function TheirMessages({chat, accountId, name, parties}: Props) {
const read = useCallback<Read>((ask, limit) => pageOf(chat, ask, limit), [chat])
const first = useCallback(async (signal: AbortSignal): Promise<Built> => {
Expand All @@ -57,7 +57,7 @@ export function TheirMessages({chat, accountId, name, parties}: Props) {
)
}

type ConversationsProps = {chat: ChatAdmin, accountId: string, first: Built, read: Read, parties: Parties}
type ConversationsProps = {chat: ChatAdmin, accountId: string, first: Built, read: Read, parties: Parties | undefined}

function Conversations({chat, accountId, first, read, parties}: ConversationsProps) {
const [built, setBuilt] = useState(first)
Expand Down Expand Up @@ -101,7 +101,7 @@ type WindowProps = {
chat: ChatAdmin
window: Window
accountId: string
parties: Parties
parties: Parties | undefined
onGrown: (window: Window) => void
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ChatAdmin} from "../clients.ts"
import {GetMessageResponse, KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"
import {KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"

// An account a report names, so the conversation marks what it said.
export type Party = {accountId: string, name: string}
Expand All @@ -13,17 +13,3 @@ export async function pageOf(chat: ChatAdmin, ask: Ask, limit: number, signal?:
const answer = await chat.listMessages({...ask, limit}, {signal})
return {messages: answer.messages, more: answer.messages.length === limit}
}

// GetMessage names no author: the report does, for the account it names.
export function keptOf(message: GetMessageResponse, parties: Parties): KeptMessage {
const party = [parties.reported, parties.reporter].find(each => each.accountId === message.accountId)
return new KeptMessage({
messageId: message.messageId,
accountId: message.accountId,
authorName: message.accountId === "" ? "" : party?.name ?? "",
text: message.text,
sentAt: message.sentAt,
countryId: message.countryId,
removedAt: message.removedAt,
})
}
5 changes: 5 additions & 0 deletions apps/backoffice/src/players/PlayerView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Code, ConnectError} from "@connectrpc/connect"
import {useCallback} from "react"
import {TheirMessages} from "../chat/TheirMessages.tsx"
import {Clients} from "../clients.ts"
import {Copyable} from "../components/AccountId.tsx"
import {FailureNotice} from "../components/FailureNotice.tsx"
Expand Down Expand Up @@ -88,6 +89,10 @@ export function PlayerView({clients, opened, onBack, onOpen}: Props) {

{isAccount(caller) && <ProfilePanel profile={profile} onReload={readProfileAgain}/>}

{isAccount(caller) && (
<TheirMessages chat={clients.chat} accountId={caller.accountId} name={name} parties={undefined}/>
)}

<section className="panel" aria-label="Antibot">
<div className="panel-head">
<h2>Antibot</h2>
Expand Down
39 changes: 39 additions & 0 deletions apps/backoffice/src/players/PlayersPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {Code} from "@connectrpc/connect"
import {cleanup, fireEvent, render, screen, within} from "@testing-library/react"
import {useState} from "react"
import {afterEach, describe, expect, it} from "vitest"
import {AROUND} from "../chat/theirConversations.ts"
import {ACCOUNT, AT, FakeAdmin} from "../fakeAdmin.ts"
import {KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"
import {InspectPlayerResponse} from "../gen/grpc/planet/v1/admin_pb.ts"
import {CountryTiles} from "../gen/grpc/player/v1/player_pb.ts"
import {Opened} from "../navigation.ts"
Expand All @@ -18,6 +20,13 @@ function Page({admin, first}: {admin: FakeAdmin, first?: Opened}) {
return <PlayersPage opened={opened} onOpen={setOpened} clients={admin.clients()}/>
}

function said(n: number, accountId = ACCOUNT, authorName = "BraveFox42") {
return new KeptMessage({
messageId: `${accountId}-${n}`, accountId, authorName, text: `${authorName} ${n}`,
sentAt: Timestamp.fromDate(new Date(AT.getTime() + n * 60_000)), countryId: "fr",
})
}

function confirm(label: string, yes: string) {
fireEvent.click(screen.getByRole("button", {name: label}))
fireEvent.click(screen.getByRole("button", {name: yes}))
Expand Down Expand Up @@ -138,6 +147,36 @@ describe("one player", () => {
expect(admin.askedFor("getPlayer")).toHaveLength(1)
})

it("shows what the account said in its conversations, read by its account id, no one tagged", async () => {
const admin = new FakeAdmin()
admin.chat = [said(1, "account-cleo", "Cleo"), said(2), said(3, "account-dan", "Dan")]
render(<Page admin={admin} first={account}/>)

const messages = within(await screen.findByRole("region", {name: "Their messages"}))
expect(await messages.findByRole("heading", {name: "What BraveFox42 said"})).toBeTruthy()
const list = await messages.findByRole("list", {name: "Their conversations, newest first"})
const shown = within(list).getAllByRole("list", {name: "A conversation"}).map(conversation =>
within(conversation).getAllByText(/^\w+ \d+$/, {selector: ".line-text"}).map(line => line.textContent))
expect(shown).toEqual([["Cleo 1", "BraveFox42 2", "Dan 3"]])
expect(messages.getByText("BraveFox42 2").closest("li")?.className).toContain("line-theirs")
expect(messages.queryByText("reported")).toBeNull()
expect(messages.queryByText("reporter")).toBeNull()
expect(admin.askedFor("listMessages")).toEqual([
{accountId: ACCOUNT, limit: 20},
{beforeMessageId: `${ACCOUNT}-2`, limit: AROUND},
{afterMessageId: `${ACCOUNT}-2`, limit: AROUND},
])
})

it("reads no messages for an address: the chat keeps them by account", async () => {
const admin = new FakeAdmin()
render(<Page admin={admin} first={address}/>)

await screen.findByRole("heading", {name: "Ban"})
expect(screen.queryByRole("region", {name: "Their messages"})).toBeNull()
expect(admin.askedFor("listMessages")).toEqual([])
})

it("says the antibot is off rather than failing", async () => {
const admin = new FakeAdmin()
admin.refuse("inspectPlayer", Code.FailedPrecondition)
Expand Down
20 changes: 17 additions & 3 deletions apps/backoffice/src/reports/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import {Code, ConnectError} from "@connectrpc/connect"
import {useCallback, useState} from "react"
import {FailureNotice} from "../components/FailureNotice.tsx"
import {timeOf} from "../components/time.ts"
import {GetMessageResponse} from "../gen/grpc/chat/v1/admin_pb.ts"
import {GetMessageResponse, KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"
import {ChatAdmin} from "../clients.ts"
import {Parties} from "../chat/chatPages.ts"
import {Conversation} from "../chat/Conversation.tsx"
import {ConfirmAction} from "../components/ConfirmAction.tsx"
import {useCall} from "../useCall.ts"
import {MESSAGE_GONE, refusalOf, removeMessage} from "./actions.ts"
import {keptOf, Parties} from "./chatPages.ts"
import {Conversation} from "./Conversation.tsx"

type Read = {kept: true, message: GetMessageResponse} | {kept: false}

Expand Down Expand Up @@ -87,3 +87,17 @@ function Message({chat, message, parties, removedAt, remove, onRemoved}: Message
</>
)
}

// GetMessage names no author: the report does, for the account it names.
function keptOf(message: GetMessageResponse, parties: Parties): KeptMessage {
const party = [parties.reported, parties.reporter].find(each => each.accountId === message.accountId)
return new KeptMessage({
messageId: message.messageId,
accountId: message.accountId,
authorName: message.accountId === "" ? "" : party?.name ?? "",
text: message.text,
sentAt: message.sentAt,
countryId: message.countryId,
removedAt: message.removedAt,
})
}
2 changes: 1 addition & 1 deletion apps/backoffice/src/reports/ReportDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {Report, ReportOutcome} from "../gen/grpc/support/v1/admin_pb.ts"
import {ReportReason} from "../gen/grpc/support/v1/support_pb.ts"
import {OpenPlayer} from "../navigation.ts"
import {ReportDetail} from "./ReportDetail.tsx"
import {AROUND} from "./theirConversations.ts"
import {AROUND} from "../chat/theirConversations.ts"

afterEach(cleanup)

Expand Down
2 changes: 1 addition & 1 deletion apps/backoffice/src/reports/ReportDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Timestamp} from "@bufbuild/protobuf"
import {useState} from "react"
import {TheirMessages} from "../chat/TheirMessages.tsx"
import {Clients, CLIENTS} from "../clients.ts"
import {Person} from "../components/Person.tsx"
import {When} from "../components/When.tsx"
Expand All @@ -10,7 +11,6 @@ import {ActPanel} from "./ActPanel.tsx"
import {DecideForm} from "./DecideForm.tsx"
import {DecisionLine} from "./DecisionLine.tsx"
import {MessagePanel} from "./MessagePanel.tsx"
import {TheirMessages} from "./TheirMessages.tsx"

type Props = {
report: Report
Expand Down
2 changes: 1 addition & 1 deletion apps/backoffice/src/reports/TheirMessages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"
import {Report} from "../gen/grpc/support/v1/admin_pb.ts"
import {ReportReason} from "../gen/grpc/support/v1/support_pb.ts"
import {ReportDetail} from "./ReportDetail.tsx"
import {AROUND} from "./theirConversations.ts"
import {AROUND} from "../chat/theirConversations.ts"

afterEach(cleanup)

Expand Down
Loading