Merge pull request #4816 from h3poteto/feat/account-added

Start/stop streamings when an account is added/deleted
This commit is contained in:
AkiraFukushima 2024-01-30 00:36:53 +09:00 committed by GitHub
commit fa573da337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 129 additions and 59 deletions

View File

@ -1,4 +1,4 @@
import { localeType } from '@/utils/i18n' import { localeType } from '@/provider/i18n'
import { Dialog, DialogBody, DialogHeader, Input, Option, Select, Typography } from '@material-tailwind/react' import { Dialog, DialogBody, DialogHeader, Input, Option, Select, Typography } from '@material-tailwind/react'
import { ChangeEvent, useEffect, useState } from 'react' import { ChangeEvent, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'

View File

@ -13,7 +13,7 @@ import {
FaXmark FaXmark
} from 'react-icons/fa6' } from 'react-icons/fa6'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { useToast } from '@/utils/toast' import { useToast } from '@/provider/toast'
import Picker from '@emoji-mart/react' import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData' import { data } from '@/utils/emojiData'
import EditMedia from './EditMedia' import EditMedia from './EditMedia'

View File

@ -5,9 +5,7 @@ import NewAccount from '@/components/accounts/New'
import Settings from '@/components/Settings' import Settings from '@/components/Settings'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import generateNotification from '@/utils/notification' import { Context } from '@/provider/i18n'
import generator, { Entity, WebSocketInterface } from 'megalodon'
import { Context } from '@/utils/i18n'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { import {
Avatar, Avatar,
@ -21,8 +19,8 @@ import {
PopoverHandler PopoverHandler
} from '@material-tailwind/react' } from '@material-tailwind/react'
import Thirdparty from '../Thirdparty' import Thirdparty from '../Thirdparty'
import { useUnreads } from '@/utils/unreads' import { useUnreads } from '@/provider/unreads'
import { unreadCount } from '@/entities/marker' import { useAccounts } from '@/provider/accounts'
type LayoutProps = { type LayoutProps = {
children: React.ReactNode children: React.ReactNode
@ -39,8 +37,8 @@ export default function Layout({ children }: LayoutProps) {
const { switchLang } = useContext(Context) const { switchLang } = useContext(Context)
const router = useRouter() const router = useRouter()
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const streamings = useRef<Array<WebSocketInterface>>([]) const { unreads } = useUnreads()
const { unreads, setUnreads } = useUnreads() const { addAccount, removeAccount, removeAll } = useAccounts()
for (let i = 1; i < 9; i++) { for (let i = 1; i < 9; i++) {
useHotkeys(`mod+${i}`, () => { useHotkeys(`mod+${i}`, () => {
@ -60,44 +58,13 @@ export default function Layout({ children }: LayoutProps) {
setOpenNewModal(true) setOpenNewModal(true)
} }
acct.forEach(async account => { acct.forEach(async account => {
// Start user streaming for notification addAccount(account)
const client = generator(account.sns, account.url, account.access_token, 'Whalebird')
const instance = await client.getInstance()
const notifications = (await client.getNotifications()).data
const res = await client.getMarkers(['notifications'])
const marker = res.data as Entity.Marker
if (marker.notifications) {
const count = unreadCount(marker.notifications, notifications)
setUnreads(current =>
Object.assign({}, current, {
[account.id?.toString()]: count
})
)
}
const ws = generator(account.sns, instance.data.urls.streaming_api, account.access_token, 'Whalebird')
const socket = ws.userSocket()
streamings.current = [...streamings.current, socket]
socket.on('connect', () => {
console.log(`connect to user streaming for ${account.domain}`)
})
socket.on('notification', (notification: Entity.Notification) => {
const [title, body] = generateNotification(notification, formatMessage)
if (title.length > 0) {
new window.Notification(title, { body: body })
}
})
}) })
} }
fn() fn()
return () => { return () => {
streamings.current.forEach(streaming => { removeAll()
streaming.removeAllListeners()
streaming.stop()
})
streamings.current = []
console.log('close user streamings')
} }
}, []) }, [])
@ -120,8 +87,9 @@ export default function Layout({ children }: LayoutProps) {
document.getElementById(`${id}`).click() document.getElementById(`${id}`).click()
} }
const removeAccount = async (id: number) => { const deleteAccount = async (account: Account) => {
await db.accounts.delete(id) removeAccount(account)
await db.accounts.delete(account.id)
const acct = await db.accounts.toArray() const acct = await db.accounts.toArray()
setAccounts(acct) setAccounts(acct)
if (acct.length === 0) { if (acct.length === 0) {
@ -164,7 +132,7 @@ export default function Layout({ children }: LayoutProps) {
</PopoverHandler> </PopoverHandler>
<PopoverContent> <PopoverContent>
<List className="py-2 px-0"> <List className="py-2 px-0">
<ListItem onClick={() => removeAccount(account.id)} className="py-2 px-4 rounded-none"> <ListItem onClick={() => deleteAccount(account)} className="py-2 px-4 rounded-none">
<ListItemPrefix> <ListItemPrefix>
<FaTrash /> <FaTrash />
</ListItemPrefix> </ListItemPrefix>

View File

@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { FaBell, FaGlobe, FaHouse, FaList, FaUsers } from 'react-icons/fa6' import { FaBell, FaGlobe, FaHouse, FaList, FaUsers } from 'react-icons/fa6'
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import Jump from '../Jump' import Jump from '../Jump'
import { useUnreads } from '@/utils/unreads' import { useUnreads } from '@/provider/unreads'
type LayoutProps = { type LayoutProps = {
children: React.ReactNode children: React.ReactNode

View File

@ -9,8 +9,8 @@ import { useRouter } from 'next/router'
import Detail from '../detail/Detail' import Detail from '../detail/Detail'
import { Marker, unreadCount } from '@/entities/marker' import { Marker, unreadCount } from '@/entities/marker'
import { FaCheck } from 'react-icons/fa6' import { FaCheck } from 'react-icons/fa6'
import { useToast } from '@/utils/toast' import { useToast } from '@/provider/toast'
import { useUnreads } from '@/utils/unreads' import { useUnreads } from '@/provider/unreads'
const TIMELINE_STATUSES_COUNT = 30 const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647 const TIMELINE_MAX_STATUSES = 2147483647

View File

@ -113,7 +113,11 @@ const actionIcon = (notification: Entity.Notification) => {
return <FaPenToSquare className="text-blue-600 w-4 mr-2 ml-auto" /> return <FaPenToSquare className="text-blue-600 w-4 mr-2 ml-auto" />
} }
case 'emoji_reaction': { case 'emoji_reaction': {
return <span dangerouslySetInnerHTML={{ __html: notification.emoji }} /> return (
<div className="w-5 mr-2 ml-auto">
<span dangerouslySetInnerHTML={{ __html: notification.emoji }} />
</div>
)
} }
default: default:
return null return null
@ -136,7 +140,7 @@ const actionId = (notification: Entity.Notification) => {
return 'notification.status.body' return 'notification.status.body'
case 'update': case 'update':
return 'notification.update.body' return 'notification.update.body'
case 'emoji_reqction': case 'emoji_reaction':
return 'notification.emoji_reaction.body' return 'notification.emoji_reaction.body'
default: default:

View File

@ -2,10 +2,11 @@ import type { AppProps } from 'next/app'
import '../app.css' import '../app.css'
import AccountLayout from '@/components/layouts/account' import AccountLayout from '@/components/layouts/account'
import TimelineLayout from '@/components/layouts/timelines' import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n' import { IntlProviderWrapper } from '@/provider/i18n'
import { ThemeProvider } from '@material-tailwind/react' import { ThemeProvider } from '@material-tailwind/react'
import { ToastProvider } from '@/utils/toast' import { ToastProvider } from '@/provider/toast'
import { UnreadsProvider } from '@/utils/unreads' import { UnreadsProvider } from '@/provider/unreads'
import { AccountsProvider } from '@/provider/accounts'
export default function MyApp({ Component, pageProps }: AppProps) { export default function MyApp({ Component, pageProps }: AppProps) {
const customTheme = { const customTheme = {
@ -106,11 +107,13 @@ export default function MyApp({ Component, pageProps }: AppProps) {
<IntlProviderWrapper> <IntlProviderWrapper>
<ToastProvider> <ToastProvider>
<UnreadsProvider> <UnreadsProvider>
<AccountLayout> <AccountsProvider>
<TimelineLayout> <AccountLayout>
<Component {...pageProps} /> <TimelineLayout>
</TimelineLayout> <Component {...pageProps} />
</AccountLayout> </TimelineLayout>
</AccountLayout>
</AccountsProvider>
</UnreadsProvider> </UnreadsProvider>
</ToastProvider> </ToastProvider>
</IntlProviderWrapper> </IntlProviderWrapper>

View File

@ -0,0 +1,95 @@
import { Account } from '@/db'
import generateNotification from '@/utils/notification'
import { unreadCount } from '@/entities/marker'
import generator, { Entity, WebSocketInterface } from 'megalodon'
import { createContext, useContext, useRef, useState } from 'react'
import { useUnreads } from './unreads'
import { useIntl } from 'react-intl'
type Context = {
addAccount: (account: Account) => void
removeAccount: (account: Account) => void
removeAll: () => void
}
const AccountsContext = createContext<Context>({
addAccount: (_: Account) => {},
removeAccount: (_: Account) => {},
removeAll: () => {}
})
AccountsContext.displayName = 'AccountsContext'
export const useAccounts = () => {
return useContext(AccountsContext)
}
type Props = {
children: React.ReactNode
}
export const AccountsProvider: React.FC<Props> = ({ children }) => {
const [_accounts, setAccounts] = useState<Array<Account>>([])
const streamings = useRef<{ [key: string]: WebSocketInterface }>({})
const { setUnreads } = useUnreads()
const { formatMessage } = useIntl()
const addAccount = (account: Account) => {
setAccounts(current => [...current, account])
startStreaming(account)
}
const removeAccount = (account: Account) => {
setAccounts(current => current.filter(a => a.id !== account.id))
if (streamings.current[account.id]) {
streamings.current[account.id].removeAllListeners()
streamings.current[account.id].stop()
console.log(`close user streaming for ${account.domain}`)
}
}
const removeAll = () => {
setAccounts([])
Object.keys(streamings.current).map(key => {
streamings.current[key].removeAllListeners()
streamings.current[key].stop()
})
console.log('close all user streamings')
streamings.current = {}
}
const startStreaming = async (account: Account) => {
if (!account.id) return
// Start user streaming for notification
const client = generator(account.sns, account.url, account.access_token, 'Whalebird')
const instance = await client.getInstance()
const notifications = (await client.getNotifications()).data
const res = await client.getMarkers(['notifications'])
const marker = res.data as Entity.Marker
if (marker.notifications) {
const count = unreadCount(marker.notifications, notifications)
setUnreads(current =>
Object.assign({}, current, {
[account.id?.toString()]: count
})
)
}
const ws = generator(account.sns, instance.data.urls.streaming_api, account.access_token, 'Whalebird')
const socket = ws.userSocket()
streamings.current = Object.assign({}, streamings.current, {
[account.id]: socket
})
socket.on('connect', () => {
console.log(`connect to user streaming for ${account.domain}`)
})
socket.on('notification', (notification: Entity.Notification) => {
const [title, body] = generateNotification(notification, formatMessage)
if (title.length > 0) {
new window.Notification(title, { body: body })
}
})
}
return <AccountsContext.Provider value={{ addAccount, removeAccount, removeAll }}>{children}</AccountsContext.Provider>
}

View File

@ -1,6 +1,6 @@
import en from '../../locales/en/translation.json' import en from '../../locales/en/translation.json'
import ja from '../../locales/ja/translation.json' import ja from '../../locales/ja/translation.json'
import { flattenMessages } from './flattenMessage' import { flattenMessages } from '../utils/flattenMessage'
import { createContext, useState } from 'react' import { createContext, useState } from 'react'
import { IntlProvider } from 'react-intl' import { IntlProvider } from 'react-intl'