Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c39a97f3
|
||
|
|
72b86fdc5c
|
||
|
|
53287f9c80
|
||
|
|
8979970a90
|
||
|
|
75c5979478
|
||
|
|
5778d865e2
|
||
|
|
0ac18faf5c |
@@ -24,6 +24,24 @@ cp .env.sample .env
|
||||
yarn && yarn dev
|
||||
```
|
||||
|
||||
## Commentaires fédérés (Mastodon / bokante.o-k-i.net)
|
||||
|
||||
Les commentaires sous une parole peuvent provenir d'un compte pawol.nu ou du Fediverse
|
||||
(réponses reçues sur le statut miroir publié sur bokante.o-k-i.net par le backend). Un
|
||||
bouton permet de répondre directement depuis son propre compte Mastodon, quelle que soit
|
||||
son instance, via `authorize_interaction` — aucun compte pawol.nu requis.
|
||||
|
||||
**Variables d'environnement :**
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `NEXT_PUBLIC_BOKANTE_URL` | Instance Mastodon de l'organisation (défaut : `https://bokante.o-k-i.net`) |
|
||||
| `NEXT_PUBLIC_BOKANTE_ACCOUNT` | Compte bot dont les statuts servent de miroir (défaut : `pawol_nu`) |
|
||||
|
||||
Voir le RFC dédié pour le détail de la conception
|
||||
(`RFC-commentaires-activitypub-2026-07-04.md`, à la racine du dossier `PAWOL.NU`) et le
|
||||
`README.md` de `api.pawol.nu` pour la configuration côté backend.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020 - 2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
||||
|
||||
@@ -34,4 +34,18 @@ describe('Komante', () => {
|
||||
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')
|
||||
})
|
||||
|
||||
it('met en valeur les mentions @ dans le contenu', () => {
|
||||
render(<Komante commentaires={[{
|
||||
id: 3,
|
||||
contenu: '@pawol_nu oh oh ye ye, avec @cybermawonaj@bokante.o-k-i.net aussi',
|
||||
origine: 'activitypub',
|
||||
auteurNom: 'Quelqu\'un'
|
||||
}]} />)
|
||||
|
||||
const mansyon = screen.getByText('@pawol_nu')
|
||||
expect(mansyon).toBeInTheDocument()
|
||||
expect(screen.getByText('@cybermawonaj@bokante.o-k-i.net')).toBeInTheDocument()
|
||||
expect(screen.getByText(/oh oh ye ye/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
+75
-25
@@ -2,10 +2,12 @@
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import Box from '@mui/material/Box'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Link from 'next/link'
|
||||
import {Mastodon} from '@icons-pack/react-simple-icons'
|
||||
|
||||
function formatAuteur(commentaire) {
|
||||
if (commentaire.origine === 'activitypub') {
|
||||
@@ -15,6 +17,29 @@ function formatAuteur(commentaire) {
|
||||
return commentaire.user?.username || 'Anonim'
|
||||
}
|
||||
|
||||
function formatDat(commentaire) {
|
||||
if (!commentaire.datePublication) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {dateStyle: 'short', timeStyle: 'short'})
|
||||
.format(new Date(commentaire.datePublication))
|
||||
}
|
||||
|
||||
const MENTION_REGEX = /(?:@[\w.-]+){1,2}/g
|
||||
|
||||
function metanValèMansyon(contenu) {
|
||||
const mansyon = contenu.match(MENTION_REGEX) || []
|
||||
return contenu.split(MENTION_REGEX).flatMap((moso, index) => (
|
||||
mansyon[index] ? [
|
||||
moso,
|
||||
<Box key={index} component='span' sx={{color: 'primary.main', fontWeight: 700}}>
|
||||
{mansyon[index]}
|
||||
</Box>
|
||||
] : [moso]
|
||||
))
|
||||
}
|
||||
|
||||
export default function Komante({commentaires}) {
|
||||
if (!commentaires || commentaires.length === 0) {
|
||||
return null
|
||||
@@ -23,36 +48,61 @@ export default function Komante({commentaires}) {
|
||||
return (
|
||||
<Box sx={{maxWidth: 700, margin: '2em auto 0'}}>
|
||||
<Typography gutterBottom variant='h6'>
|
||||
Komantè
|
||||
Komantè ({commentaires.length})
|
||||
</Typography>
|
||||
{commentaires.map(commentaire => {
|
||||
const auteur = formatAuteur(commentaire)
|
||||
const estFedere = commentaire.origine === 'activitypub'
|
||||
<Stack spacing={1.5}>
|
||||
{commentaires.map(commentaire => {
|
||||
const auteur = formatAuteur(commentaire)
|
||||
const estFedere = commentaire.origine === 'activitypub'
|
||||
const dat = formatDat(commentaire)
|
||||
|
||||
return (
|
||||
<Box key={commentaire.id} sx={{display: 'flex', gap: 1.5, mb: 2}}>
|
||||
<Avatar src={commentaire.auteurAvatarUrl} alt={auteur} />
|
||||
<Box sx={{flex: 1}}>
|
||||
<Typography variant='subtitle2' component='div'>
|
||||
{estFedere && commentaire.auteurProfilUrl ? (
|
||||
<Link href={commentaire.auteurProfilUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{auteur}
|
||||
return (
|
||||
<Box
|
||||
key={commentaire.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1.5,
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
bgcolor: 'action.hover'
|
||||
}}
|
||||
>
|
||||
<Avatar src={commentaire.auteurAvatarUrl} alt={auteur} />
|
||||
<Box sx={{flex: 1, minWidth: 0}}>
|
||||
<Stack direction='row' alignItems='center' flexWrap='wrap' rowGap={0.5}>
|
||||
<Stack direction='row' alignItems='center' spacing={1}>
|
||||
<Typography variant='subtitle2' component='div'>
|
||||
{estFedere && commentaire.auteurProfilUrl ? (
|
||||
<Link href={commentaire.auteurProfilUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{auteur}
|
||||
</Link>
|
||||
) : auteur}
|
||||
</Typography>
|
||||
{estFedere && (
|
||||
<Chip
|
||||
label='Mastodon'
|
||||
size='small'
|
||||
icon={<Mastodon size={14} color='#6364FF' title='' />}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{dat && (
|
||||
<Typography variant='caption' color='text.secondary' sx={{ml: 'auto', pl: 2}}>
|
||||
{dat}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography variant='body2' sx={{mt: 0.5}}>{metanValèMansyon(commentaire.contenu)}</Typography>
|
||||
{estFedere && commentaire.remoteUrl && (
|
||||
<Link href={commentaire.remoteUrl} target='_blank' rel='noopener noreferrer'>
|
||||
<Typography variant='caption' color='text.secondary'>Voir sur Mastodon</Typography>
|
||||
</Link>
|
||||
) : auteur}
|
||||
{estFedere && (
|
||||
<Chip label='Mastodon' size='small' sx={{ml: 1}} />
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{commentaire.contenu}</Typography>
|
||||
{estFedere && commentaire.remoteUrl && (
|
||||
<Link href={commentaire.remoteUrl} target='_blank' rel='noopener noreferrer'>
|
||||
<Typography variant='caption' color='text.secondary'>Voir sur Mastodon</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
+14
-36
@@ -3,8 +3,7 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Box from '@mui/material/Box'
|
||||
import Tabs from '@mui/material/Tabs'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import {useMediaQuery} from '@mui/material'
|
||||
@@ -13,8 +12,6 @@ import slugify from 'slugify'
|
||||
|
||||
import {styled} from '@mui/material/styles'
|
||||
import ExplicitIcon from '@mui/icons-material/Explicit'
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
@@ -121,6 +118,7 @@ export default function Teks({parole}) {
|
||||
const coverFmt = formatKuveti(parole.couverture)
|
||||
const [tab, setTab] = useState(0)
|
||||
const [, setCurrentTrack] = useCurrentTrack()
|
||||
const options = ['Transcription', ...langArray.map(({title}) => title)]
|
||||
|
||||
useEffect(() => {
|
||||
const isBrowser = () => typeof window !== 'undefined'
|
||||
@@ -230,39 +228,19 @@ export default function Teks({parole}) {
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
|
||||
<Tabs
|
||||
scrollButtons
|
||||
allowScrollButtonsMobile
|
||||
value={tab}
|
||||
variant='scrollable'
|
||||
centered={langArray.length === 0}
|
||||
slots={{
|
||||
startScrollButtonIcon: ArrowBackIosNewIcon,
|
||||
endScrollButtonIcon: ArrowForwardIosIcon
|
||||
}}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
mb: 2,
|
||||
'& .MuiTabs-scrollButtons': {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: '50%',
|
||||
color: 'primary.main',
|
||||
backgroundColor: 'action.hover',
|
||||
mx: 0.5,
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
onChange={(event, value) => setTab(value)}
|
||||
>
|
||||
<Tab label='Transcription' />
|
||||
{langArray.map(({title}) => (
|
||||
<Tab key={title} label={title} />
|
||||
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1, borderBottom: 1, borderColor: 'divider', mb: 2, pb: 2}}>
|
||||
{options.map((label, index) => (
|
||||
<Chip
|
||||
key={label}
|
||||
clickable
|
||||
color='primary'
|
||||
label={label}
|
||||
sx={{fontWeight: tab === index ? 600 : 400}}
|
||||
variant={tab === index ? 'filled' : 'outlined'}
|
||||
onClick={() => setTab(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
{tab === 0 && (
|
||||
<>
|
||||
{parole.langueSource && parole.langueSource !== 'ka' && (
|
||||
|
||||
Reference in New Issue
Block a user