From eda30345c64370c41ff49f947cf13ea516609d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 23:45:27 +0400 Subject: [PATCH 1/2] feat: afficher les commentaires (locaux et Mastodon) --- components/teks/__tests__/komante.test.jsx | 37 ++++++++++++ components/teks/an-teks.js | 1 - components/teks/komante.jsx | 62 +++++++++++++++++++ components/teks/teks.js | 2 + lib/oki-api.js | 11 +++- package.json | 1 + yarn.lock | 70 ++++++++++++++++++++-- 7 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 components/teks/__tests__/komante.test.jsx create mode 100644 components/teks/komante.jsx diff --git a/components/teks/__tests__/komante.test.jsx b/components/teks/__tests__/komante.test.jsx new file mode 100644 index 0000000..bcc2088 --- /dev/null +++ b/components/teks/__tests__/komante.test.jsx @@ -0,0 +1,37 @@ +import {describe, it, expect} from 'vitest' +import {render, screen} from '@testing-library/react' +import Komante from '../komante' + +describe('Komante', () => { + it('ne rend rien quand il n\'y a aucun commentaire', () => { + const {container} = render() + expect(container).toBeEmptyDOMElement() + }) + + it('affiche un commentaire local avec le pseudo pawol.nu', () => { + render() + + expect(screen.getByText('foo')).toBeInTheDocument() + expect(screen.getByText('Bèl tèks')).toBeInTheDocument() + expect(screen.queryByText('Mastodon')).not.toBeInTheDocument() + }) + + it('affiche un commentaire fédéré avec le badge et le lien Mastodon', () => { + render() + + expect(screen.getByText('Quelqu\'un')).toBeInTheDocument() + expect(screen.getByText('Mastodon')).toBeInTheDocument() + expect(screen.getByRole('link', {name: 'Quelqu\'un'})).toHaveAttribute('href', 'https://mastodon.social/@quelqun') + expect(screen.getByRole('link', {name: /voir sur mastodon/i})).toHaveAttribute('href', 'https://bokante.o-k-i.net/@quelqun/1') + }) +}) diff --git a/components/teks/an-teks.js b/components/teks/an-teks.js index 43bbea5..1fcb9cd 100644 --- a/components/teks/an-teks.js +++ b/components/teks/an-teks.js @@ -8,7 +8,6 @@ export default function AnTeks({parole, paroleId}) { ) } diff --git a/components/teks/komante.jsx b/components/teks/komante.jsx new file mode 100644 index 0000000..3890360 --- /dev/null +++ b/components/teks/komante.jsx @@ -0,0 +1,62 @@ +'use client' + +import PropTypes from 'prop-types' +import Box from '@mui/material/Box' +import Avatar from '@mui/material/Avatar' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Link from 'next/link' + +function formatAuteur(commentaire) { + if (commentaire.origine === 'activitypub') { + return commentaire.auteurNom || commentaire.auteurHandle || 'Yon moun' + } + + return commentaire.user?.username || 'Anonim' +} + +export default function Komante({commentaires}) { + if (!commentaires || commentaires.length === 0) { + return null + } + + return ( + + + Komantè + + {commentaires.map(commentaire => { + const auteur = formatAuteur(commentaire) + const estFedere = commentaire.origine === 'activitypub' + + return ( + + + + + {estFedere && commentaire.auteurProfilUrl ? ( + + {auteur} + + ) : auteur} + {estFedere && ( + + )} + + {commentaire.contenu} + {estFedere && commentaire.remoteUrl && ( + + Voir sur Mastodon + + )} + + + ) + })} + + ) +} + +Komante.propTypes = { + commentaires: PropTypes.array +} diff --git a/components/teks/teks.js b/components/teks/teks.js index fa40b1d..2c15136 100644 --- a/components/teks/teks.js +++ b/components/teks/teks.js @@ -29,6 +29,7 @@ import {StreamButton} from '../streaming-buttons' import EntegreMizik from './entegre-mizik' import OkiMizik from './oki-mizik' import DiferansDialog from './diferans-dialog' +import Komante from './komante' import {useCurrentTrack} from './current-track-context' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' @@ -281,6 +282,7 @@ export default function Teks({parole}) { ) ))} + ) } diff --git a/lib/oki-api.js b/lib/oki-api.js index 32043eb..7cfb76d 100644 --- a/lib/oki-api.js +++ b/lib/oki-api.js @@ -38,7 +38,16 @@ export async function jwennTeksEpiSlug(slug) { fields: ['username', 'firstname'] }, commentaires: { - fields: ['contenu', 'datePublication'], + fields: [ + 'contenu', + 'datePublication', + 'origine', + 'auteurNom', + 'auteurHandle', + 'auteurAvatarUrl', + 'auteurProfilUrl', + 'remoteUrl' + ], populate: { user: { fields: ['username'] diff --git a/package.json b/package.json index d79b45f..045e014 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/yarn.lock b/yarn.lock index 5de779e..bcfbf93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2356,6 +2356,20 @@ dependencies: tslib "^2.8.0" +"@testing-library/dom@^10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + "@testing-library/jest-dom@^6.9.1": version "6.9.1" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" @@ -2392,6 +2406,11 @@ dependencies: tslib "^2.4.0" +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/chai@^5.2.2": version "5.2.3" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" @@ -2776,6 +2795,11 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2794,6 +2818,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" @@ -3525,6 +3556,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -3556,6 +3592,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-accessibility-api@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" @@ -5581,6 +5622,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -6213,16 +6259,16 @@ peek-readable@^4.1.0: resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== +picocolors@1.1.1, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - picomatch@^2.2.2: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -6350,6 +6396,15 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -6448,6 +6503,11 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^19.0.0, react-is@^19.2.4: version "19.2.5" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.5.tgz#7e7b54143e9313fed787b23fd4295d5a23872ad9" -- 2.39.5 From 21248b71383c8a903b2da4630fb1c490c706cee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sun, 5 Jul 2026 01:15:55 +0400 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20bouton=20R=C3=A9pondre=20via=20Mast?= =?UTF-8?q?odon=20(authorize=5Finteraction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 5 ++ .../teks/__tests__/repons-mastodon.test.jsx | 55 ++++++++++++ components/teks/repons-mastodon.jsx | 83 +++++++++++++++++++ components/teks/teks.js | 2 + 4 files changed, 145 insertions(+) create mode 100644 components/teks/__tests__/repons-mastodon.test.jsx create mode 100644 components/teks/repons-mastodon.jsx diff --git a/.env.sample b/.env.sample index 114d967..65f689a 100644 --- a/.env.sample +++ b/.env.sample @@ -81,6 +81,11 @@ NEXT_PUBLIC_GIT= NEXT_PUBLIC_CODEBERG= NEXT_PUBLIC_BLUESKY_URL= +# Commentaires fédérés (bokante.o-k-i.net) + +NEXT_PUBLIC_BOKANTE_URL=https://bokante.o-k-i.net +NEXT_PUBLIC_BOKANTE_ACCOUNT=pawol_nu + # DOMAIN IMAGE NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com" diff --git a/components/teks/__tests__/repons-mastodon.test.jsx b/components/teks/__tests__/repons-mastodon.test.jsx new file mode 100644 index 0000000..afe6b19 --- /dev/null +++ b/components/teks/__tests__/repons-mastodon.test.jsx @@ -0,0 +1,55 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' +import {render, screen} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import ReponsMastodon from '../repons-mastodon' + +describe('ReponsMastodon', () => { + const originalOpen = window.open + + afterEach(() => { + window.open = originalOpen + }) + + it('ne rend rien sans bokanteStatusId', () => { + const {container} = render() + expect(container).toBeEmptyDOMElement() + }) + + it('affiche un lien direct vers le fil bokante', () => { + render() + const lien = screen.getByRole('link', {name: 'Mastodon'}) + expect(lien).toHaveAttribute('href', 'https://bokante.o-k-i.net/@pawol_nu/112233') + }) + + it('redirige vers authorize_interaction avec le domaine extrait d\'un identifiant complet', async () => { + window.open = vi.fn() + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button', {name: /répondre via mastodon/i})) + await user.type(screen.getByLabelText(/@vous@votre-instance/i), '@quelqun@mastodon.social') + await user.click(screen.getByRole('button', {name: /continuer/i})) + + expect(window.open).toHaveBeenCalledWith( + 'https://mastodon.social/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'), + '_blank', + 'noopener,noreferrer' + ) + }) + + it('accepte aussi juste un nom d\'instance sans @', async () => { + window.open = vi.fn() + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button', {name: /répondre via mastodon/i})) + await user.type(screen.getByLabelText(/@vous@votre-instance/i), 'mastodon.social') + await user.click(screen.getByRole('button', {name: /continuer/i})) + + expect(window.open).toHaveBeenCalledWith( + expect.stringContaining('https://mastodon.social/authorize_interaction'), + '_blank', + 'noopener,noreferrer' + ) + }) +}) diff --git a/components/teks/repons-mastodon.jsx b/components/teks/repons-mastodon.jsx new file mode 100644 index 0000000..fd63b63 --- /dev/null +++ b/components/teks/repons-mastodon.jsx @@ -0,0 +1,83 @@ +'use client' + +import {useState} from 'react' +import PropTypes from 'prop-types' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import Dialog from '@mui/material/Dialog' +import DialogTitle from '@mui/material/DialogTitle' +import DialogContent from '@mui/material/DialogContent' +import DialogActions from '@mui/material/DialogActions' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import Link from 'next/link' + +const BOKANTE_URL = process.env.NEXT_PUBLIC_BOKANTE_URL || 'https://bokante.o-k-i.net' +const BOKANTE_ACCOUNT = process.env.NEXT_PUBLIC_BOKANTE_ACCOUNT || 'pawol_nu' + +function extraireInstance(saisie) { + const valeur = saisie.trim().replace(/^@/, '').replace(/^https?:\/\//, '').replace(/\/$/, '') + return valeur.includes('@') ? valeur.split('@').pop() : valeur +} + +export default function ReponsMastodon({bokanteStatusId}) { + const [ouvert, setOuvert] = useState(false) + const [saisie, setSaisie] = useState('') + + if (!bokanteStatusId) { + return null + } + + const tootUrl = `${BOKANTE_URL}/@${BOKANTE_ACCOUNT}/${bokanteStatusId}` + + const repondre = () => { + const instance = extraireInstance(saisie) + if (!instance) { + return + } + + window.open(`https://${instance}/authorize_interaction?uri=${encodeURIComponent(tootUrl)}`, '_blank', 'noopener,noreferrer') + setOuvert(false) + } + + return ( + + + Rejoignez la discussion sur{' '} + Mastodon + + + setOuvert(false)}> + Répondre depuis votre compte Mastodon + + + Indiquez votre identifiant (@vous@votre-instance) ou juste le nom de votre + instance, vous serez redirigé·e vers votre compte pour répondre. + + setSaisie(event.target.value)} + onKeyDown={event => { + if (event.key === 'Enter') { + repondre() + } + }} + /> + + + + + + + + ) +} + +ReponsMastodon.propTypes = { + bokanteStatusId: PropTypes.string +} diff --git a/components/teks/teks.js b/components/teks/teks.js index 2c15136..d3a9d00 100644 --- a/components/teks/teks.js +++ b/components/teks/teks.js @@ -30,6 +30,7 @@ import EntegreMizik from './entegre-mizik' import OkiMizik from './oki-mizik' import DiferansDialog from './diferans-dialog' import Komante from './komante' +import ReponsMastodon from './repons-mastodon' import {useCurrentTrack} from './current-track-context' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' @@ -282,6 +283,7 @@ export default function Teks({parole}) { ) ))} + ) -- 2.39.5