diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 38260fe..11e580e 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -213,6 +213,9 @@ block **overrides but does not delete** an existing friendship (so you may block they keep seeing you as one); active games are never interrupted — you can finish them, with the blocked opponent's chat composer hidden (only the log remains). Blocking from a game card mirrors the block in **Settings → Friends**; **unblock** and **unfriend** live there only. +On Settings → Friends each friend is a one-line row whose right-hand kebab (⋮) slides open +**block 🚫** and **remove ✖️** icon actions, and each action is gated by a confirmation +that names the friend (*Block this player?* / *Remove from friends?*). Blocking an **auto-match opponent who is secretly a robot** behaves the same in that game (struck name, hidden composer) and lists the blocked opponent under the name you saw, but is recorded only against that game — the disguise holds, the shared robot is never globally diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index fa1dcd6..3fc7c05 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -218,6 +218,9 @@ _Вход сейчас только через провайдера, поэто заблокированного соперника «подвал» чата скрыт (остаётся только лог). Блокировка с карточки в партии повторяет блокировку в **Настройках → Друзья**; **разблокировка** и **удаление из друзей** есть только там. +В **Настройках → Друзья** каждый друг — однострочник, чей правый кебаб (⋮) выдвигает +иконки-действия **заблокировать 🚫** и **удалить ✖️**, и каждое действие подтверждается +диалогом с именем друга (*Заблокировать?* / *Удалить из друзей?*). Блокировка **авто-матч соперника, который втайне робот**, в этой партии ведёт себя так же (зачёркнутое имя, скрытый «подвал») и в списке заблокированных показывается под тем именем, которое ты видел, но записывается только для этой партии — маскировка сохраняется, общий diff --git a/ui/e2e/social.spec.ts b/ui/e2e/social.spec.ts index ae01d0f..742044f 100644 --- a/ui/e2e/social.spec.ts +++ b/ui/e2e/social.spec.ts @@ -44,6 +44,60 @@ test('friends: issue a code, accept an incoming request, redeem a code', async ( await expect(page.locator('.who', { hasText: 'Friend 111111' })).toBeVisible(); }); +test('friends: the row kebab reveals block/remove and an outside tap closes it', async ({ page }) => { + await loginLobby(page); + await openFriends(page); + + // A friend row slides open on its kebab (like the lobby), exposing two icon actions. + const kaya = page.locator('.rowwrap', { hasText: 'Kaya' }); + await kaya.locator('.kebab').click(); + await expect(kaya).toHaveClass(/revealed/); + await expect(kaya.locator('.acts').getByRole('button', { name: 'Block' })).toBeVisible(); + await expect(kaya.locator('.acts').getByRole('button', { name: 'Remove' })).toBeVisible(); + + // A tap anywhere outside the action buttons collapses the row again. + await page.getByRole('heading', { name: 'Your friends' }).click(); + await expect(kaya).not.toHaveClass(/revealed/); +}); + +test('friends: blocking from the list confirms (naming the friend) and moves them to Blocked', async ({ page }) => { + await loginLobby(page); + await openFriends(page); + + const kaya = page.locator('.rowwrap', { hasText: 'Kaya' }); + await kaya.locator('.kebab').click(); + await kaya.locator('.acts').getByRole('button', { name: 'Block' }).click(); + + // The confirmation keeps a generic title and names the friend in the body. + const dialog = page.getByRole('dialog'); + await expect(dialog.getByText('Block this player?')).toBeVisible(); + await expect(dialog.locator('.confirm-name')).toHaveText('Kaya'); + await dialog.getByRole('button', { name: 'Block' }).click(); + + // The block applied: Kaya leaves the friends list and shows under Blocked players. + await expect(page.getByText('No friends yet.')).toBeVisible(); + const blocked = page.locator('.rowwrap', { hasText: 'Kaya' }); + await expect(blocked.getByRole('button', { name: 'Unblock' })).toBeVisible(); +}); + +test('friends: removing from the list confirms (naming the friend) and drops them', async ({ page }) => { + await loginLobby(page); + await openFriends(page); + + const kaya = page.locator('.rowwrap', { hasText: 'Kaya' }); + await kaya.locator('.kebab').click(); + await kaya.locator('.acts').getByRole('button', { name: 'Remove' }).click(); + + const dialog = page.getByRole('dialog'); + await expect(dialog.getByText('Remove from friends?')).toBeVisible(); + await expect(dialog.locator('.confirm-name')).toHaveText('Kaya'); + await dialog.getByRole('button', { name: 'Remove' }).click(); + + // Unfriending just drops the friendship — Kaya is gone and not blocked. + await expect(page.getByText('No friends yet.')).toBeVisible(); + await expect(page.locator('.rowwrap', { hasText: 'Kaya' })).toHaveCount(0); +}); + test('invitations: the lobby shows an invitation and accepting clears it', async ({ page }) => { await loginLobby(page); await expect(page.getByText('Invitations')).toBeVisible(); diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index db0cad9..6f7cb9e 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -227,6 +227,9 @@ export const en = { 'friends.decline': 'Decline', 'friends.unfriend': 'Remove', 'friends.block': 'Block', + 'friends.actions': 'Actions', + 'friends.blockConfirm': 'Block this player?', + 'friends.unfriendConfirm': 'Remove from friends?', 'friends.add': 'Add a friend', 'friends.addFromGame': 'Add to friends', 'friends.blockFromGame': 'Block player', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index 4321efd..bd5b5a6 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -228,6 +228,9 @@ export const ru: Record = { 'friends.decline': 'Отклонить', 'friends.unfriend': 'Удалить', 'friends.block': 'Заблокировать', + 'friends.actions': 'Действия', + 'friends.blockConfirm': 'Заблокировать?', + 'friends.unfriendConfirm': 'Удалить из друзей?', 'friends.add': 'Добавить друга', 'friends.addFromGame': 'В друзья', 'friends.blockFromGame': 'Заблокировать', diff --git a/ui/src/screens/Friends.svelte b/ui/src/screens/Friends.svelte index 4471ef4..b4cb81d 100644 --- a/ui/src/screens/Friends.svelte +++ b/ui/src/screens/Friends.svelte @@ -1,5 +1,6 @@