Merge pull request #4816 from h3poteto/feat/account-added
Start/stop streamings when an account is added/deleted
This commit is contained in:
commit
fa573da337
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in New Issue