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 { ChangeEvent, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl'

View File

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

View File

@ -5,9 +5,7 @@ import NewAccount from '@/components/accounts/New'
import Settings from '@/components/Settings'
import { useRouter } from 'next/router'
import { FormattedMessage, useIntl } from 'react-intl'
import generateNotification from '@/utils/notification'
import generator, { Entity, WebSocketInterface } from 'megalodon'
import { Context } from '@/utils/i18n'
import { Context } from '@/provider/i18n'
import { useHotkeys } from 'react-hotkeys-hook'
import {
Avatar,
@ -21,8 +19,8 @@ import {
PopoverHandler
} from '@material-tailwind/react'
import Thirdparty from '../Thirdparty'
import { useUnreads } from '@/utils/unreads'
import { unreadCount } from '@/entities/marker'
import { useUnreads } from '@/provider/unreads'
import { useAccounts } from '@/provider/accounts'
type LayoutProps = {
children: React.ReactNode
@ -39,8 +37,8 @@ export default function Layout({ children }: LayoutProps) {
const { switchLang } = useContext(Context)
const router = useRouter()
const { formatMessage } = useIntl()
const streamings = useRef<Array<WebSocketInterface>>([])
const { unreads, setUnreads } = useUnreads()
const { unreads } = useUnreads()
const { addAccount, removeAccount, removeAll } = useAccounts()
for (let i = 1; i < 9; i++) {
useHotkeys(`mod+${i}`, () => {
@ -60,44 +58,13 @@ export default function Layout({ children }: LayoutProps) {
setOpenNewModal(true)
}
acct.forEach(async account => {
// 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 = [...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 })
}
})
addAccount(account)
})
}
fn()
return () => {
streamings.current.forEach(streaming => {
streaming.removeAllListeners()
streaming.stop()
})
streamings.current = []
console.log('close user streamings')
removeAll()
}
}, [])
@ -120,8 +87,9 @@ export default function Layout({ children }: LayoutProps) {
document.getElementById(`${id}`).click()
}
const removeAccount = async (id: number) => {
await db.accounts.delete(id)
const deleteAccount = async (account: Account) => {
removeAccount(account)
await db.accounts.delete(account.id)
const acct = await db.accounts.toArray()
setAccounts(acct)
if (acct.length === 0) {
@ -164,7 +132,7 @@ export default function Layout({ children }: LayoutProps) {
</PopoverHandler>
<PopoverContent>
<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>
<FaTrash />
</ListItemPrefix>

View File

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

View File

@ -9,8 +9,8 @@ import { useRouter } from 'next/router'
import Detail from '../detail/Detail'
import { Marker, unreadCount } from '@/entities/marker'
import { FaCheck } from 'react-icons/fa6'
import { useToast } from '@/utils/toast'
import { useUnreads } from '@/utils/unreads'
import { useToast } from '@/provider/toast'
import { useUnreads } from '@/provider/unreads'
const TIMELINE_STATUSES_COUNT = 30
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" />
}
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:
return null
@ -136,7 +140,7 @@ const actionId = (notification: Entity.Notification) => {
return 'notification.status.body'
case 'update':
return 'notification.update.body'
case 'emoji_reqction':
case 'emoji_reaction':
return 'notification.emoji_reaction.body'
default:

View File

@ -2,10 +2,11 @@ import type { AppProps } from 'next/app'
import '../app.css'
import AccountLayout from '@/components/layouts/account'
import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n'
import { IntlProviderWrapper } from '@/provider/i18n'
import { ThemeProvider } from '@material-tailwind/react'
import { ToastProvider } from '@/utils/toast'
import { UnreadsProvider } from '@/utils/unreads'
import { ToastProvider } from '@/provider/toast'
import { UnreadsProvider } from '@/provider/unreads'
import { AccountsProvider } from '@/provider/accounts'
export default function MyApp({ Component, pageProps }: AppProps) {
const customTheme = {
@ -106,11 +107,13 @@ export default function MyApp({ Component, pageProps }: AppProps) {
<IntlProviderWrapper>
<ToastProvider>
<UnreadsProvider>
<AccountLayout>
<TimelineLayout>
<Component {...pageProps} />
</TimelineLayout>
</AccountLayout>
<AccountsProvider>
<AccountLayout>
<TimelineLayout>
<Component {...pageProps} />
</TimelineLayout>
</AccountLayout>
</AccountsProvider>
</UnreadsProvider>
</ToastProvider>
</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 ja from '../../locales/ja/translation.json'
import { flattenMessages } from './flattenMessage'
import { flattenMessages } from '../utils/flattenMessage'
import { createContext, useState } from 'react'
import { IntlProvider } from 'react-intl'