postmerge

This commit is contained in:
Untone 2023-11-17 19:22:54 +03:00
parent 8d0a6269e1
commit b6134cc04a
9 changed files with 17530 additions and 123 deletions

17442
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@ import { MediaItem } from '../../../pages/types'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { CardTopic } from '../../Feed/CardTopic'
import { Image } from '../../_shared/Image'
import styles from './AudioHeader.module.scss'

View File

@ -17,7 +17,6 @@ import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss'
import { AuthorLink } from '../../Author/AhtorLink'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))

View File

@ -1,9 +1,19 @@
import type { Accessor, JSX } from 'solid-js'
import { createContext, createSignal, onMount, useContext } from 'solid-js'
import { createContext, createEffect, createSignal, onMount, useContext } from 'solid-js'
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
import { inboxClient } from '../utils/apiClient'
import { loadMessages } from '../stores/inbox'
import { MessageHandler, SSEMessage, useNotifications } from './notifications'
import { getToken } from '../graphql/privateGraphQLClient'
import { fetchEventSource } from '@microsoft/fetch-event-source'
export interface SSEMessage {
id: string
entity: string
action: string
payload: any // Author | Shout | Reaction | Message
timestamp?: number
seen?: boolean
}
type InboxContextType = {
chats: Accessor<Chat[]>
@ -25,9 +35,6 @@ export function useInbox() {
export const InboxProvider = (props: { children: JSX.Element }) => {
const [chats, setChats] = createSignal<Chat[]>([])
const [messages, setMessages] = createSignal<Message[]>([])
const {
actions: { setMessageHandler }
} = useNotifications()
const handleMessage = (sseMessage) => {
console.log('[context.inbox]:', sseMessage)
@ -41,11 +48,34 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
}
}
onMount(() =>
setMessageHandler((_h) => {
return handleMessage
})
)
createEffect(async () => {
const token = getToken()
if (token) {
await fetchEventSource('https://chat.discours.io/connect', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
onmessage(event) {
const m: SSEMessage = JSON.parse(event.data)
console.log('[context.inbox] Received message:', m)
if (m.entity === 'chat' || m.entity == 'message') {
handleMessage(m)
} else {
console.debug(m)
}
},
onclose() {
console.log('[context.inbox] sse connection closed by server')
},
onerror(err) {
console.error('[context.inbox] sse connection closed by error', err)
throw new Error(err) // NOTE: simple hack to close the connection
},
})
}
})
const loadChats = async () => {
try {
@ -73,7 +103,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
const currentChat = chats().find((chat) => chat.id === args.chat_id)
setChats((prev) => [
...prev.filter((c) => c.id !== currentChat.id),
{ ...currentChat, updatedAt: message.createdAt }
{ ...currentChat, updatedAt: message.createdAt },
])
} catch (error) {
console.error('Error sending message:', error)
@ -94,7 +124,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
createChat,
loadChats,
getMessages,
sendMessage
sendMessage,
}
const value: InboxContextType = { chats, messages, actions }

View File

@ -12,27 +12,9 @@ import { apiBaseUrl } from '../utils/config'
import SSEService, { EventData } from '../utils/sseService'
import { useSession } from './session'
import { Portal } from 'solid-js/web'
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
import { IDBPDatabase, openDB } from 'idb'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getToken } from '../graphql/privateGraphQLClient'
import { Author, Message, Reaction, Shout } from '../graphql/types.gen'
export const PAGE_SIZE = 20
export interface SSEMessage {
id: string
entity: string
action: string
payload: any // Author | Shout | Reaction | Message
timestamp?: number
seen?: boolean
}
export type MessageHandler = (m: SSEMessage) => void
type NotificationsContextType = {
notificationEntities: Record<number, SSEMessage>
notificationEntities: Record<number, Notification>
unreadNotificationsCount: Accessor<number>
sortedNotifications: Accessor<Notification[]>
loadedNotificationsCount: Accessor<number>
@ -43,7 +25,6 @@ type NotificationsContextType = {
markNotificationAsRead: (notification: Notification) => Promise<void>
markAllNotificationsAsRead: () => Promise<void>
loadNotifications: (options: { limit: number; offset: number }) => Promise<Notification[]>
setMessageHandler: (h: MessageHandler) => void
}
}
@ -61,17 +42,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
const { isAuthenticated, user } = useSession()
const [notificationEntities, setNotificationEntities] = createStore<Record<number, SSEMessage>>({})
const [db, setDb] = createSignal<Promise<IDBPDatabase<unknown>>>()
onMount(() => {
const dbx = openDB('notifications-db', 1, {
upgrade(indexedDb) {
indexedDb.createObjectStore('notifications')
},
})
setDb(dbx)
})
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
const loadNotifications = async (options: { limit: number; offset?: number }) => {
const { notifications, totalUnreadCount, totalCount } = await apiClient.getNotifications(options)
@ -82,34 +53,17 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
setTotalNotificationsCount(totalCount)
setUnreadNotificationsCount(totalUnreadCount)
setNotificationEntities(
notifications.reduce((acc, notification) => {
acc[notification.id] = notification
return acc
}, {}),
)
setNotificationEntities(newNotificationEntities)
return notifications
}
const sortedNotifications = createMemo(() => {
return Object.values(notificationEntities).sort((a, b) => b.timestamp - a.timestamp)
return Object.values(notificationEntities).sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
})
const storeNotification = async (notification: SSEMessage) => {
console.log('[context.notifications] Storing notification:', notification)
const storage = await db()
const tx = storage.transaction('notifications', 'readwrite')
const store = tx.objectStore('notifications')
await store.put(notification, 'id')
await tx.done
loadNotifications()
}
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
createEffect(() => {
if (isAuthenticated()) {
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`)
@ -124,42 +78,6 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
sseService.disconnect()
}
})
const [messageHandler, setMessageHandler] = createSignal<MessageHandler>(console.warn)
createEffect(async () => {
if (isAuthenticated()) {
loadNotifications()
await fetchEventSource('https://chat.discours.io/connect', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: getToken(),
},
onmessage(event) {
const m: SSEMessage = JSON.parse(event.data)
console.log('[context.notifications] Received message:', m)
if (m.entity === 'chat' || m.entity == 'message') {
messageHandler()(m)
} else {
storeNotification({
...m,
id: event.id,
timestamp: Date.now(),
seen: false,
})
}
},
onclose() {
console.log('[context.notifications] sse connection closed by server')
},
onerror(err) {
console.error('[context.notifications] sse connection closed by error', err)
throw new Error(err) // NOTE: simple hack to close the connection
},
})
}
})
const markNotificationAsRead = async (notification: Notification) => {
await apiClient.markNotificationAsRead(notification.id)
@ -180,7 +98,6 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
}
const actions = {
setMessageHandler,
showNotificationsPanel,
hideNotificationsPanel,
markNotificationAsRead,

View File

@ -115,7 +115,7 @@ main {
position: relative;
.scrollToNew {
osition: absolute;
position: absolute;
z-index: 2;
bottom: 8px;
overflow: hidden;

View File

@ -28,12 +28,11 @@ import type {
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { getToken, privateGraphQLClient, privateInboxGraphQLClient } from '../graphql/privateGraphQLClient'
import topicsAll from '../graphql/query/topics-all'
import mySession from '../graphql/mutation/my-session'
import topicsRandomQuery from '../graphql/query/topics-random'
import authLogoutQuery from '../graphql/mutation/auth-logout'
import authRegisterMutation from '../graphql/mutation/auth-register'
import authSendLinkMutation from '../graphql/mutation/auth-send-link'
import createChat from '../graphql/mutation/create-chat'
import createMessage from '../graphql/mutation/create-chat-message'
import authConfirmEmailMutation from '../graphql/mutation/auth-confirm-email'
import followMutation from '../graphql/mutation/follow'
import markAllNotificationsAsRead from '../graphql/mutation/mark-all-notifications-as-read'
import markNotificationAsRead from '../graphql/mutation/mark-notification-as-read'
@ -43,8 +42,6 @@ import reactionDestroy from '../graphql/mutation/reaction-destroy'
import reactionUpdate from '../graphql/mutation/reaction-update'
import unfollowMutation from '../graphql/mutation/unfollow'
import updateProfile from '../graphql/mutation/update-profile'
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import shoutLoad from '../graphql/query/article-load'
import shoutsLoadBy from '../graphql/query/articles-load-by'
import authCheckEmailQuery from '../graphql/query/auth-check-email'
@ -58,17 +55,15 @@ import topicBySlug from '../graphql/query/topic-by-slug'
import reactionsLoadBy from '../graphql/query/reactions-load-by'
import authorsLoadBy from '../graphql/query/authors-load-by'
import chatMessagesLoadBy from '../graphql/query/chat-messages-load-by'
import loadRecipients from '../graphql/query/chat-recipients'
import myChats from '../graphql/query/chats-load'
import draftsLoad from '../graphql/query/drafts-load'
import myFeed from '../graphql/query/my-feed'
import loadRecipients from '../graphql/query/chat-recipients'
import updateProfile from '../graphql/mutation/update-profile'
import createArticle from '../graphql/mutation/article-create'
import updateArticle from '../graphql/mutation/article-update'
import deleteShout from '../graphql/mutation/article-delete'
// import notifications from '../graphql/query/notifications'
// import markNotificationAsRead from '../graphql/mutation/mark-notification-as-read'
import markAllNotificationsAsRead from '../graphql/mutation/mark-all-notifications-as-read'
import notifications from '../graphql/query/notifications'
import markAsRead from '../graphql/mutation/chat-mark-as-read'
import createChat from '../graphql/mutation/chat-create'
import updateChat from '../graphql/mutation/chat-update'
@ -371,8 +366,7 @@ export const apiClient = {
.toPromise()
return resp.data.loadReactionsBy
},
// TODO: store notifications in browser storage
/*
getNotifications: async (params: NotificationsQueryParams): Promise<NotificationsQueryResult> => {
const resp = await privateGraphQLClient.query(notifications, { params }).toPromise()
return resp.data.loadNotifications
@ -384,10 +378,6 @@ export const apiClient = {
})
.toPromise()
},
*/
markAllNotificationsAsRead: async (): Promise<void> => {
await privateGraphQLClient.mutation(markAllNotificationsAsRead, {}).toPromise()
},
markAllNotificationsAsRead: async (): Promise<void> => {
await privateGraphQLClient.mutation(markAllNotificationsAsRead, {}).toPromise()

View File

@ -7,7 +7,4 @@ export const apiBaseUrl = 'https://v2.discours.io'
const defaultThumborUrl = 'https://images.discours.io'
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
const defaultThumborUrl = 'https://images.discours.io'
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''

33
src/utils/sseService.ts Normal file
View File

@ -0,0 +1,33 @@
export type EventData = {
type: string
}
class SSEService {
private eventSource: EventSource | null
constructor() {
this.eventSource = null
}
public connect(url: string): void {
this.eventSource = new EventSource(url)
}
public disconnect(): void {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
}
}
public subscribeToEvent(eventName: string, callback: (eventData: EventData) => void): void {
if (this.eventSource) {
this.eventSource.addEventListener(eventName, (event: MessageEvent) => {
const data = JSON.parse(event.data)
callback(data)
})
}
}
}
export default SSEService