inbox-reuse-authors
This commit is contained in:
parent
a405172b76
commit
eb1be9652c
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -126,7 +126,7 @@
|
||||||
"typograf": "7.1.0",
|
"typograf": "7.1.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "4.5.0",
|
"vite": "4.5.1",
|
||||||
"vite-plugin-mkcert": "1.16.0",
|
"vite-plugin-mkcert": "1.16.0",
|
||||||
"vite-plugin-sass-dts": "1.3.11",
|
"vite-plugin-sass-dts": "1.3.11",
|
||||||
"vite-plugin-solid": "2.7.2",
|
"vite-plugin-solid": "2.7.2",
|
||||||
|
@ -18707,9 +18707,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
||||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
"typograf": "7.1.0",
|
"typograf": "7.1.0",
|
||||||
"uniqolor": "1.1.0",
|
"uniqolor": "1.1.0",
|
||||||
"vike": "0.4.148",
|
"vike": "0.4.148",
|
||||||
"vite": "4.5.0",
|
"vite": "4.5.1",
|
||||||
"vite-plugin-mkcert": "1.16.0",
|
"vite-plugin-mkcert": "1.16.0",
|
||||||
"vite-plugin-sass-dts": "1.3.11",
|
"vite-plugin-sass-dts": "1.3.11",
|
||||||
"vite-plugin-solid": "2.7.2",
|
"vite-plugin-solid": "2.7.2",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { useSession } from '../../context/session'
|
||||||
import { MediaItem } from '../../pages/types'
|
import { MediaItem } from '../../pages/types'
|
||||||
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
|
import { isCyrillic } from '../../utils/cyrillic'
|
||||||
import { getImageUrl } from '../../utils/getImageUrl'
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
import { getDescription, getKeywords } from '../../utils/meta'
|
import { getDescription, getKeywords } from '../../utils/meta'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
@ -295,7 +296,9 @@ export const FullArticle = (props: Props) => {
|
||||||
const description = getDescription(props.article.description || body())
|
const description = getDescription(props.article.description || body())
|
||||||
const ogTitle = props.article.title
|
const ogTitle = props.article.title
|
||||||
const keywords = getKeywords(props.article)
|
const keywords = getKeywords(props.article)
|
||||||
|
const getAuthorName = (a: Author) => {
|
||||||
|
return lang() == 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta name="descprition" content={description} />
|
<Meta name="descprition" content={description} />
|
||||||
|
@ -333,7 +336,7 @@ export const FullArticle = (props: Props) => {
|
||||||
{(a: Author, index) => (
|
{(a: Author, index) => (
|
||||||
<>
|
<>
|
||||||
<Show when={index() > 0}>, </Show>
|
<Show when={index() > 0}>, </Show>
|
||||||
<a href={getPagePath(router, 'author', { slug: a.slug })}>{a.name}</a>
|
<a href={getPagePath(router, 'author', { slug: a.slug })}>{getAuthorName(a)}</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Author } from '../../../graphql/schema/core.gen'
|
import { Author } from '../../../graphql/schema/core.gen'
|
||||||
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
|
import { isCyrillic } from '../../../utils/cyrillic'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
|
|
||||||
import styles from './AhtorLink.module.scss'
|
import styles from './AhtorLink.module.scss'
|
||||||
|
@ -13,6 +17,12 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorLink = (props: Props) => {
|
export const AuthorLink = (props: Props) => {
|
||||||
|
const { lang } = useLocalize()
|
||||||
|
const name = createMemo(() => {
|
||||||
|
return lang() === 'en' && isCyrillic(props.author.name)
|
||||||
|
? capitalize(props.author.slug.replace(/-/, ' '))
|
||||||
|
: props.author.name
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.AuthorLink, props.class, styles[props.size ?? 'M'], {
|
class={clsx(styles.AuthorLink, props.class, styles[props.size ?? 'M'], {
|
||||||
|
@ -20,8 +30,8 @@ export const AuthorLink = (props: Props) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<a class={styles.link} href={`/author/${props.author.slug}`}>
|
<a class={styles.link} href={`/author/${props.author.slug}`}>
|
||||||
<Userpic size={props.size ?? 'M'} name={props.author.name} userpic={props.author.userpic} />
|
<Userpic size={props.size ?? 'M'} name={props.author.name} userpic={props.author.pic} />
|
||||||
<div class={styles.name}>{props.author.name}</div>
|
<div class={styles.name}>{name()}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
import { ChatMember } from '../../../graphql/schema/chat.gen'
|
||||||
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { ChatMember } from '../../graphql/schema/core.gen'
|
import type { ChatMember } from '../../graphql/schema/chat.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, Switch, Match, createMemo } from 'solid-js'
|
import { Show, Switch, Match, createMemo } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { Author } from '../../graphql/schema/core.gen'
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
|
|
||||||
import DialogAvatar from './DialogAvatar'
|
import DialogAvatar from './DialogAvatar'
|
||||||
|
@ -26,7 +27,7 @@ type DialogProps = {
|
||||||
const DialogCard = (props: DialogProps) => {
|
const DialogCard = (props: DialogProps) => {
|
||||||
const { t, formatTime } = useLocalize()
|
const { t, formatTime } = useLocalize()
|
||||||
const companions = createMemo(
|
const companions = createMemo(
|
||||||
() => props.members && props.members.filter((member) => member.id !== props.ownId),
|
() => props.members && props.members.filter((member: ChatMember) => member.id !== props.ownId),
|
||||||
)
|
)
|
||||||
|
|
||||||
const names = createMemo(
|
const names = createMemo(
|
||||||
|
@ -51,11 +52,11 @@ const DialogCard = (props: DialogProps) => {
|
||||||
when={props.isChatHeader}
|
when={props.isChatHeader}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.avatar}>
|
<div class={styles.avatar}>
|
||||||
<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />
|
<DialogAvatar name={props.members[0].slug} url={props.members[0].pic} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AuthorBadge nameOnly={true} author={props.members[0]} />
|
<AuthorBadge nameOnly={true} author={props.members[0] as Author} />
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
const { isAuthenticated } = useSession()
|
const { isAuthenticated } = useSession()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
const {
|
||||||
|
after,
|
||||||
sortedNotifications,
|
sortedNotifications,
|
||||||
unreadNotificationsCount,
|
unreadNotificationsCount,
|
||||||
loadedNotificationsCount,
|
loadedNotificationsCount,
|
||||||
|
@ -112,7 +113,7 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
|
|
||||||
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
|
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
|
||||||
const loadNextPage = async () => {
|
const loadNextPage = async () => {
|
||||||
await loadNotifications({ limit: PAGE_SIZE, offset: loadedNotificationsCount() })
|
await loadNotifications({ after: after(), limit: PAGE_SIZE, offset: loadedNotificationsCount() })
|
||||||
if (loadedNotificationsCount() < totalNotificationsCount()) {
|
if (loadedNotificationsCount() < totalNotificationsCount()) {
|
||||||
const hasMore = scrollContainerRef.current.scrollHeight <= scrollContainerRef.current.offsetHeight
|
const hasMore = scrollContainerRef.current.scrollHeight <= scrollContainerRef.current.offsetHeight
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,15 @@ import type { JSX } from 'solid-js'
|
||||||
|
|
||||||
import { openPage } from '@nanostores/router'
|
import { openPage } from '@nanostores/router'
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js'
|
import { Accessor, createContext, createSignal, useContext } from 'solid-js'
|
||||||
import { createStore, SetStoreFunction } from 'solid-js/store'
|
import { createStore, SetStoreFunction } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient as coreClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { ShoutVisibility, Topic, TopicInput } from '../graphql/schema/core.gen'
|
import { ShoutVisibility, Topic, TopicInput } from '../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../stores/router'
|
import { router, useRouter } from '../stores/router'
|
||||||
import { slugify } from '../utils/slugify'
|
import { slugify } from '../utils/slugify'
|
||||||
|
|
||||||
import { useLocalize } from './localize'
|
import { useLocalize } from './localize'
|
||||||
import { useSession } from './session'
|
|
||||||
import { useSnackbar } from './snackbar'
|
import { useSnackbar } from './snackbar'
|
||||||
|
|
||||||
type WordCounter = {
|
type WordCounter = {
|
||||||
|
@ -83,23 +82,12 @@ const removeDraftFromLocalStorage = (shoutId: number) => {
|
||||||
|
|
||||||
export const EditorProvider = (props: { children: JSX.Element }) => {
|
export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
|
||||||
actions: { getToken },
|
|
||||||
} = useSession()
|
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const apiClient = createMemo(() => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!coreClient.private) coreClient.connect(token)
|
|
||||||
return coreClient
|
|
||||||
})
|
|
||||||
const {
|
const {
|
||||||
actions: { showSnackbar },
|
actions: { showSnackbar },
|
||||||
} = useSnackbar()
|
} = useSnackbar()
|
||||||
|
|
||||||
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
||||||
|
|
||||||
const editorRef: { current: () => Editor } = { current: null }
|
const editorRef: { current: () => Editor } = { current: null }
|
||||||
|
|
||||||
const [form, setForm] = createStore<ShoutForm>(null)
|
const [form, setForm] = createStore<ShoutForm>(null)
|
||||||
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
||||||
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
||||||
|
@ -133,7 +121,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
|
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
|
||||||
return apiClient().updateArticle({
|
return await apiClient.updateArticle({
|
||||||
shoutId: formToUpdate.shoutId,
|
shoutId: formToUpdate.shoutId,
|
||||||
shoutInput: {
|
shoutInput: {
|
||||||
body: formToUpdate.body,
|
body: formToUpdate.body,
|
||||||
|
@ -218,7 +206,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const publishShoutById = async (shoutId: number) => {
|
const publishShoutById = async (shoutId: number) => {
|
||||||
try {
|
try {
|
||||||
await apiClient().updateArticle({
|
await apiClient.updateArticle({
|
||||||
shoutId,
|
shoutId,
|
||||||
publish: true,
|
publish: true,
|
||||||
})
|
})
|
||||||
|
@ -232,7 +220,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const deleteShout = async (shoutId: number) => {
|
const deleteShout = async (shoutId: number) => {
|
||||||
try {
|
try {
|
||||||
await apiClient().deleteShout({
|
await apiClient.deleteShout({
|
||||||
shoutId,
|
shoutId,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||||
|
|
||||||
import { inboxClient } from '../graphql/client/chat'
|
import { inboxClient } from '../graphql/client/chat'
|
||||||
import { Author } from '../graphql/schema/core.gen'
|
import { Author } from '../graphql/schema/core.gen'
|
||||||
|
import { useAuthorsStore } from '../stores/zine/authors'
|
||||||
|
|
||||||
import { SSEMessage, useConnect } from './connect'
|
import { SSEMessage, useConnect } from './connect'
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
@ -15,7 +16,7 @@ type InboxContextType = {
|
||||||
actions: {
|
actions: {
|
||||||
createChat: (members: number[], title: string) => Promise<{ chat: Chat }>
|
createChat: (members: number[], title: string) => Promise<{ chat: Chat }>
|
||||||
loadChats: () => Promise<Array<Chat>>
|
loadChats: () => Promise<Array<Chat>>
|
||||||
loadRecipients: () => Promise<Array<Author>>
|
loadRecipients: () => Array<Author>
|
||||||
loadMessages: (by: MessagesBy, limit: number, offset: number) => Promise<Array<Message>>
|
loadMessages: (by: MessagesBy, limit: number, offset: number) => Promise<Array<Message>>
|
||||||
getMessages?: (chatId: string) => Promise<Array<Message>>
|
getMessages?: (chatId: string) => Promise<Array<Message>>
|
||||||
sendMessage?: (args: MutationCreate_MessageArgs) => void
|
sendMessage?: (args: MutationCreate_MessageArgs) => void
|
||||||
|
@ -31,6 +32,7 @@ export function useInbox() {
|
||||||
export const InboxProvider = (props: { children: JSX.Element }) => {
|
export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const [chats, setChats] = createSignal<Chat[]>([])
|
const [chats, setChats] = createSignal<Chat[]>([])
|
||||||
const [messages, setMessages] = createSignal<Message[]>([])
|
const [messages, setMessages] = createSignal<Message[]>([])
|
||||||
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
|
|
||||||
const handleMessage = (sseMessage: SSEMessage) => {
|
const handleMessage = (sseMessage: SSEMessage) => {
|
||||||
console.log('[context.inbox]:', sseMessage)
|
console.log('[context.inbox]:', sseMessage)
|
||||||
|
@ -48,25 +50,6 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const { addHandler } = useConnect()
|
const { addHandler } = useConnect()
|
||||||
addHandler(handleMessage)
|
addHandler(handleMessage)
|
||||||
|
|
||||||
const {
|
|
||||||
actions: { getToken },
|
|
||||||
} = useSession()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!inboxClient.private && token) {
|
|
||||||
inboxClient.connect(token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadRecipients = async (limit = 50, offset = 0): Promise<Array<Author>> => {
|
|
||||||
if (inboxClient.private) {
|
|
||||||
// TODO: perhaps setMembers(authors) ?
|
|
||||||
return await inboxClient.loadRecipients({ limit, offset })
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMessages = async (
|
const loadMessages = async (
|
||||||
by: MessagesBy,
|
by: MessagesBy,
|
||||||
limit: number = 50,
|
limit: number = 50,
|
||||||
|
@ -136,7 +119,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
createChat,
|
createChat,
|
||||||
loadChats,
|
loadChats,
|
||||||
loadMessages,
|
loadMessages,
|
||||||
loadRecipients,
|
loadRecipients: sortedAuthors,
|
||||||
getMessages,
|
getMessages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
|
|
||||||
|
import { createStorageSignal } from '@solid-primitives/storage'
|
||||||
import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { Portal } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
|
@ -7,7 +8,7 @@ import { Portal } from 'solid-js/web'
|
||||||
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
||||||
import { NotificationsPanel } from '../components/NotificationsPanel'
|
import { NotificationsPanel } from '../components/NotificationsPanel'
|
||||||
import { notifierClient } from '../graphql/client/notifier'
|
import { notifierClient } from '../graphql/client/notifier'
|
||||||
import { Notification } from '../graphql/schema/notifier.gen'
|
import { Notification, QueryLoad_NotificationsArgs } from '../graphql/schema/notifier.gen'
|
||||||
|
|
||||||
import { SSEMessage, useConnect } from './connect'
|
import { SSEMessage, useConnect } from './connect'
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
@ -15,6 +16,7 @@ import { useSession } from './session'
|
||||||
type NotificationsContextType = {
|
type NotificationsContextType = {
|
||||||
notificationEntities: Record<number, Notification>
|
notificationEntities: Record<number, Notification>
|
||||||
unreadNotificationsCount: Accessor<number>
|
unreadNotificationsCount: Accessor<number>
|
||||||
|
after: Accessor<number>
|
||||||
sortedNotifications: Accessor<Notification[]>
|
sortedNotifications: Accessor<Notification[]>
|
||||||
loadedNotificationsCount: Accessor<number>
|
loadedNotificationsCount: Accessor<number>
|
||||||
totalNotificationsCount: Accessor<number>
|
totalNotificationsCount: Accessor<number>
|
||||||
|
@ -23,7 +25,7 @@ type NotificationsContextType = {
|
||||||
hideNotificationsPanel: () => void
|
hideNotificationsPanel: () => void
|
||||||
markNotificationAsRead: (notification: Notification) => Promise<void>
|
markNotificationAsRead: (notification: Notification) => Promise<void>
|
||||||
markAllNotificationsAsRead: () => Promise<void>
|
markAllNotificationsAsRead: () => Promise<void>
|
||||||
loadNotifications: (options: { limit: number; offset: number }) => Promise<Notification[]>
|
loadNotifications: (options: QueryLoad_NotificationsArgs) => Promise<Notification[]>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,21 +41,10 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||||
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
||||||
const {
|
const { isAuthenticated } = useSession()
|
||||||
isAuthenticated,
|
|
||||||
actions: { getToken },
|
|
||||||
} = useSession()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!notifierClient.private && token) {
|
|
||||||
notifierClient.connect(token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { addHandler } = useConnect()
|
const { addHandler } = useConnect()
|
||||||
|
|
||||||
const loadNotifications = async (options: { limit?: number; offset?: number }) => {
|
const loadNotifications = async (options: { after: number; limit?: number; offset?: number }) => {
|
||||||
if (isAuthenticated() && notifierClient?.private) {
|
if (isAuthenticated() && notifierClient?.private) {
|
||||||
const { notifications, unread, total } = await notifierClient.getNotifications(options)
|
const { notifications, unread, total } = await notifierClient.getNotifications(options)
|
||||||
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
||||||
|
@ -75,16 +66,18 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
return Object.values(notificationEntities).sort((a, b) => b.created_at - a.created_at)
|
return Object.values(notificationEntities).sort((a, b) => b.created_at - a.created_at)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
|
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
|
||||||
|
const [after, setAfter] = createStorageSignal('notifier_timestamp', now)
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
addHandler((data: SSEMessage) => {
|
addHandler((data: SSEMessage) => {
|
||||||
if (data.entity === 'reaction' && isAuthenticated()) {
|
if (data.entity === 'reaction' && isAuthenticated()) {
|
||||||
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
loadNotifications({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
||||||
} else {
|
} else {
|
||||||
console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`)
|
console.info(`[NotificationsProvider] bypassed:`, data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
setAfter(now)
|
||||||
})
|
})
|
||||||
|
|
||||||
const markNotificationAsRead = async (notification: Notification) => {
|
const markNotificationAsRead = async (notification: Notification) => {
|
||||||
|
@ -96,7 +89,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
const markAllNotificationsAsRead = async () => {
|
const markAllNotificationsAsRead = async () => {
|
||||||
if (isAuthenticated() && notifierClient.private) {
|
if (isAuthenticated() && notifierClient.private) {
|
||||||
await notifierClient.markAllNotificationsAsRead()
|
await notifierClient.markAllNotificationsAsRead()
|
||||||
await loadNotifications({ limit: loadedNotificationsCount() })
|
await loadNotifications({ after: after(), limit: loadedNotificationsCount() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +110,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const value: NotificationsContextType = {
|
const value: NotificationsContextType = {
|
||||||
|
after,
|
||||||
notificationEntities,
|
notificationEntities,
|
||||||
sortedNotifications,
|
sortedNotifications,
|
||||||
unreadNotificationsCount,
|
unreadNotificationsCount,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { ProfileInput } from '../graphql/schema/core.gen'
|
||||||
import { createContext, createEffect, createMemo, JSX, useContext } from 'solid-js'
|
import { createContext, createEffect, createMemo, JSX, useContext } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient as coreClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { loadAuthor } from '../stores/zine/authors'
|
import { loadAuthor } from '../stores/zine/authors'
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
@ -38,14 +38,8 @@ export const ProfileFormProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const currentSlug = createMemo(() => author()?.slug)
|
const currentSlug = createMemo(() => author()?.slug)
|
||||||
|
|
||||||
const apiClient = createMemo(() => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!coreClient.private) coreClient.connect(token)
|
|
||||||
return coreClient
|
|
||||||
})
|
|
||||||
|
|
||||||
const submit = async (profile: ProfileInput) => {
|
const submit = async (profile: ProfileInput) => {
|
||||||
const response = await apiClient().updateProfile(profile)
|
const response = await apiClient.updateProfile(profile)
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error(response.error)
|
console.error(response.error)
|
||||||
throw response.error
|
throw response.error
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { JSX } from 'solid-js'
|
||||||
import { createContext, createMemo, onCleanup, useContext } from 'solid-js'
|
import { createContext, createMemo, onCleanup, useContext } from 'solid-js'
|
||||||
import { createStore, reconcile } from 'solid-js/store'
|
import { createStore, reconcile } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient as coreClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
@ -38,12 +38,6 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
actions: { getToken },
|
actions: { getToken },
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
|
||||||
const apiClient = createMemo(() => {
|
|
||||||
const token = getToken()
|
|
||||||
if (!coreClient.private) coreClient.connect(token)
|
|
||||||
return coreClient
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadReactionsBy = async ({
|
const loadReactionsBy = async ({
|
||||||
by,
|
by,
|
||||||
limit,
|
limit,
|
||||||
|
@ -53,7 +47,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<Reaction[]> => {
|
}): Promise<Reaction[]> => {
|
||||||
const reactions = await coreClient.getReactionsBy({ by, limit, offset })
|
const reactions = await apiClient.getReactionsBy({ by, limit, offset })
|
||||||
const newReactionEntities = reactions.reduce((acc, reaction) => {
|
const newReactionEntities = reactions.reduce((acc, reaction) => {
|
||||||
acc[reaction.id] = reaction
|
acc[reaction.id] = reaction
|
||||||
return acc
|
return acc
|
||||||
|
@ -63,7 +57,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createReaction = async (input: ReactionInput): Promise<void> => {
|
const createReaction = async (input: ReactionInput): Promise<void> => {
|
||||||
const reaction = await apiClient().createReaction(input)
|
const reaction = await apiClient.createReaction(input)
|
||||||
|
|
||||||
const changes = {
|
const changes = {
|
||||||
[reaction.id]: reaction,
|
[reaction.id]: reaction,
|
||||||
|
@ -90,14 +84,14 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteReaction = async (id: number): Promise<void> => {
|
const deleteReaction = async (id: number): Promise<void> => {
|
||||||
const reaction = await apiClient().destroyReaction(id)
|
const reaction = await apiClient.destroyReaction(id)
|
||||||
setReactionEntities({
|
setReactionEntities({
|
||||||
[reaction.id]: undefined,
|
[reaction.id]: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
||||||
const reaction = await apiClient().updateReaction(id, input)
|
const reaction = await apiClient.updateReaction(id, input)
|
||||||
setReactionEntities(reaction.id, reaction)
|
setReactionEntities(reaction.id, reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,9 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
} from 'solid-js'
|
} from 'solid-js'
|
||||||
|
|
||||||
|
import { inboxClient } from '../graphql/client/chat'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
|
import { notifierClient } from '../graphql/client/notifier'
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
import { showModal } from '../stores/ui'
|
import { showModal } from '../stores/ui'
|
||||||
|
|
||||||
|
@ -121,9 +123,12 @@ export const SessionProvider = (props: {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// authorized graphql client
|
const token = getToken()
|
||||||
const tkn = getToken()
|
if (!inboxClient.private && token) {
|
||||||
if (tkn) apiClient.connect(tkn)
|
apiClient.connect(token)
|
||||||
|
notifierClient.connect(token)
|
||||||
|
inboxClient.connect(token)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadSubscriptions = async (): Promise<void> => {
|
const loadSubscriptions = async (): Promise<void> => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadUnratedShoutsQuery($limit: Int!) {
|
query LoadUnratedShoutsQuery($limit: Int, $offset: Int) {
|
||||||
loadUnratedShouts(limit: $limit) {
|
load_shouts_unrated(limit: $limit, offset: $offset) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
# lead
|
# lead
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadNotificationsQuery($limit: Int, $offset: Int) {
|
query LoadNotificationsQuery($after: Int!, $limit: Int, $offset: Int) {
|
||||||
load_notifications(limit: $limit, offset: $offset) {
|
load_notifications(after: $after, limit: $limit, offset: $offset) {
|
||||||
notifications {
|
notifications {
|
||||||
id
|
id
|
||||||
entity
|
entity
|
||||||
|
|
5
src/utils/cyrillic.ts
Normal file
5
src/utils/cyrillic.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const isCyrillic = (s: string): boolean => {
|
||||||
|
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
|
||||||
|
|
||||||
|
return cyrillicRegex.test(s)
|
||||||
|
}
|
|
@ -13,6 +13,9 @@ const getSizeUrlPart = (options: { width?: number; height?: number } = {}) => {
|
||||||
|
|
||||||
export const getImageUrl = (src: string, options: { width?: number; height?: number } = {}) => {
|
export const getImageUrl = (src: string, options: { width?: number; height?: number } = {}) => {
|
||||||
const sizeUrlPart = getSizeUrlPart(options)
|
const sizeUrlPart = getSizeUrlPart(options)
|
||||||
|
const sourceUrl = src.replace(thumborUrl, '').replace('/unsafe', '')
|
||||||
return `${thumborUrl}/unsafe/${sizeUrlPart}${src.replace(thumborUrl, '').replace('/unsafe', '')}`
|
return (
|
||||||
|
'https://' +
|
||||||
|
`${thumborUrl}/unsafe/${sizeUrlPart}${sourceUrl}`.replace('https://', '').replace('//', '/')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user