Skip to content

Commit

Permalink
✨Add Support for Bluesky
Browse files Browse the repository at this point in the history
  • Loading branch information
0x46616c6b committed Apr 28, 2024
1 parent 9e65a34 commit 49b0d42
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/api/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Message {
createdAt: Date
telegramUrl?: string
mastodonUrl?: string
blueskyUrl?: string
geoInformation: string
attachments?: Attachment[]
}
Expand Down
31 changes: 31 additions & 0 deletions src/api/Ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Ticker {
information: TickerInformation
mastodon: TickerMastodon
telegram: TickerTelegram
bluesky: TickerBluesky
location: TickerLocation
}

Expand Down Expand Up @@ -67,6 +68,19 @@ export interface TickerMastodonFormData {
accessToken?: string
}

export interface TickerBluesky {
active: boolean
connected: boolean
handle: string
appKey: string
}

export interface TickerBlueskyFormData {
active: boolean
handle?: string
appKey?: string
}

export interface TickerLocation {
lat: number
lon: number
Expand Down Expand Up @@ -168,6 +182,21 @@ export function useTickerApi(token: string) {
}).then(response => response.json())
}

const putTickerBluesky = (data: TickerBlueskyFormData, ticker: Ticker): Promise<Response<TickerResponseData>> => {
return fetch(`${ApiUrl}/admin/tickers/${ticker.id}/bluesky`, {
headers: headers,
body: JSON.stringify(data),
method: 'put',
}).then(response => response.json())
}

const deleteTickerBluesky = (ticker: Ticker): Promise<Response<TickerResponseData>> => {
return fetch(`${ApiUrl}/admin/tickers/${ticker.id}/bluesky`, {
headers: headers,
method: 'delete',
}).then(response => response.json())
}

return {
deleteTicker,
deleteTickerUser,
Expand All @@ -182,5 +211,7 @@ export function useTickerApi(token: string) {
deleteTickerMastodon,
putTickerTelegram,
deleteTickerTelegram,
putTickerBluesky,
deleteTickerBluesky,
}
}
3 changes: 2 additions & 1 deletion src/components/message/MessageFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC } from 'react'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import { faMastodon, faTelegram } from '@fortawesome/free-brands-svg-icons'
import { faBluesky, faMastodon, faTelegram } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Box, Link, Stack, Typography, useTheme } from '@mui/material'
import { Message } from '../../api/Message'
Expand All @@ -19,6 +19,7 @@ const MessageFooter: FC<Props> = ({ message }) => {
<Box>
<Icon icon={faTelegram} url={message.telegramUrl} />
<Icon icon={faMastodon} url={message.mastodonUrl} />
<Icon icon={faBluesky} url={message.blueskyUrl} />
</Box>
</Stack>
)
Expand Down
90 changes: 90 additions & 0 deletions src/components/ticker/BlueskyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import { FC, useCallback, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBluesky } from '@fortawesome/free-brands-svg-icons'
import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-icons'
import BlueskyModalForm from './BlueskyModalForm'

interface Props {
ticker: Ticker
}

const BlueskyCard: FC<Props> = ({ ticker }) => {
const { token } = useAuth()
const { deleteTickerBluesky, putTickerBluesky } = useTickerApi(token)
const [open, setOpen] = useState<boolean>(false)

const queryClient = useQueryClient()

const bluesky = ticker.bluesky

const handleDisconnect = useCallback(() => {
deleteTickerBluesky(ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
})
}, [deleteTickerBluesky, queryClient, ticker])

const handleToggle = useCallback(() => {
putTickerBluesky({ active: !bluesky.active }, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
})
}, [bluesky.active, putTickerBluesky, queryClient, ticker])

const profileLink = (
<Link href={'https://bsky.app/profile/' + bluesky.handle} rel="noreferrer" target="_blank">
{bluesky.handle}
</Link>
)

return (
<Card>
<CardContent>
<Stack alignItems="center" direction="row" justifyContent="space-between">
<Typography component="h5" variant="h5">
<FontAwesomeIcon icon={faBluesky} /> Bluesky
</Typography>
<Button onClick={() => setOpen(true)} size="small" startIcon={<FontAwesomeIcon icon={faGear} />}>
Configure
</Button>
</Stack>
</CardContent>
<Divider variant="middle" />
<CardContent>
{bluesky.connected ? (
<Box>
<Typography variant="body2">You are connected with Bluesky.</Typography>
<Typography variant="body2">Your Profile: {profileLink}</Typography>
</Box>
) : (
<Box>
<Typography variant="body2">
You are not connected with Bluesky. New messages will not be published to your account and old messages can not be deleted anymore.
</Typography>
</Box>
)}
</CardContent>
{bluesky.connected ? (
<CardActions>
{bluesky.active ? (
<Button onClick={handleToggle} size="small" startIcon={<FontAwesomeIcon icon={faPause} />}>
Disable
</Button>
) : (
<Button onClick={handleToggle} size="small" startIcon={<FontAwesomeIcon icon={faPlay} />}>
Enable
</Button>
)}
<Button onClick={handleDisconnect} size="small" startIcon={<FontAwesomeIcon icon={faTrash} />}>
Disconnect
</Button>
</CardActions>
) : null}
<BlueskyModalForm open={open} onClose={() => setOpen(false)} ticker={ticker} />
</Card>
)
}

export default BlueskyCard
73 changes: 73 additions & 0 deletions src/components/ticker/BlueskyForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FC } from 'react'
import { Ticker, TickerBlueskyFormData, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import { useForm } from 'react-hook-form'
import { useQueryClient } from '@tanstack/react-query'
import { Alert, Checkbox, FormControlLabel, FormGroup, Grid, TextField, Typography } from '@mui/material'

interface Props {
callback: () => void
ticker: Ticker
}

const BlueskyForm: FC<Props> = ({ callback, ticker }) => {
const bluesky = ticker.bluesky
const { token } = useAuth()
const { putTickerBluesky } = useTickerApi(token)
const {
formState: { errors },
handleSubmit,
register,
setError,
} = useForm<TickerBlueskyFormData>({
defaultValues: {
active: bluesky.active,
handle: bluesky.handle,
appKey: bluesky.appKey,
},
})
const queryClient = useQueryClient()

const onSubmit = handleSubmit(data => {
putTickerBluesky(data, ticker).then(response => {
if (response.status == 'error') {
setError('root.authenticationFailed', { message: 'Authentication failed' })
} else {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
callback()
}
})
})

return (
<form id="configureBluesky" onSubmit={onSubmit}>
<Grid columnSpacing={{ xs: 1, sm: 2, md: 3 }} container rowSpacing={1}>
<Grid item xs={12}>
<Typography>You need to create a application password in Bluesky.</Typography>
</Grid>
{errors.root?.authenticationFailed && (
<Grid item xs={12}>
<Alert severity="error">{errors.root.authenticationFailed.message}</Alert>
</Grid>
)}
<Grid item xs={12}>
<FormGroup>
<FormControlLabel control={<Checkbox {...register('active')} defaultChecked={ticker.bluesky.active} />} label="Active" />
</FormGroup>
</Grid>
<Grid item xs={12}>
<FormGroup>
<TextField {...register('handle')} defaultValue={ticker.bluesky.handle} label="Handle" placeholder="handle.bsky.social" required />
</FormGroup>
</Grid>
<Grid item xs={12}>
<FormGroup>
<TextField {...register('appKey')} defaultValue={ticker.bluesky.appKey} label="Application Password" required type="password" />
</FormGroup>
</Grid>
</Grid>
</form>
)
}

export default BlueskyForm
20 changes: 20 additions & 0 deletions src/components/ticker/BlueskyModalForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FC } from 'react'
import { Ticker } from '../../api/Ticker'
import Modal from '../common/Modal'
import BlueskyForm from './BlueskyForm'

interface Props {
onClose: () => void
open: boolean
ticker: Ticker
}

const BlueskyModalForm: FC<Props> = ({ onClose, open, ticker }) => {
return (
<Modal maxWidth="sm" onClose={onClose} open={open} submitForm="configureBluesky" title="Configure Bluesky">
<BlueskyForm callback={onClose} ticker={ticker} />
</Modal>
)
}

export default BlueskyModalForm
1 change: 1 addition & 0 deletions src/components/ticker/TickerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const TickerCard: FC<Props> = ({ ticker }) => {
<Box>
<SocialConnectionChip active={ticker.mastodon.active} label="Mastodon" />
<SocialConnectionChip active={ticker.telegram.active} label="Telegram" />
<SocialConnectionChip active={ticker.bluesky.active} label="Bluesky" />
</Box>
</NamedListItem>
</CardContent>
Expand Down
4 changes: 4 additions & 0 deletions src/components/ticker/TickerSocialConnections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FC } from 'react'
import { Ticker } from '../../api/Ticker'
import MastodonCard from './MastodonCard'
import TelegramCard from './TelegramCard'
import BlueskyCard from './BlueskyCard'

interface Props {
ticker: Ticker
Expand All @@ -17,6 +18,9 @@ const TickerSocialConnections: FC<Props> = ({ ticker }) => {
<Grid item md={6} xs={12}>
<TelegramCard ticker={ticker} />
</Grid>
<Grid item md={6} xs={12}>
<BlueskyCard ticker={ticker} />
</Grid>
</Grid>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/views/TickerView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthProvider } from '../components/useAuth'
import TickerView from './TickerView'
import ProtectedRoute from '../components/ProtectedRoute'
import { vi } from 'vitest'
import { blue } from '@mui/material/colors'

describe('TickerView', function () {
const jwt = sign(
Expand All @@ -31,6 +32,7 @@ describe('TickerView', function () {
mastodon: {},
twitter: {},
telegram: {},
bluesky: {},
location: {},
},
},
Expand Down

0 comments on commit 49b0d42

Please sign in to comment.