From eb1be9652ca746f52305e847474dcd11e26c0782 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 19 Dec 2023 12:34:24 +0300 Subject: [PATCH] inbox-reuse-authors --- package-lock.json | 8 ++--- package.json | 2 +- src/components/Article/FullArticle.tsx | 7 ++-- .../Author/AhtorLink/AuthorLink.tsx | 14 ++++++-- .../Author/AuthorBadge/AuthorBadge.tsx | 1 + src/components/Inbox/DialogCard.tsx | 9 +++--- .../NotificationsPanel/NotificationsPanel.tsx | 3 +- src/context/editor.tsx | 22 +++---------- src/context/inbox.tsx | 25 +++------------ src/context/notifications.tsx | 32 ++++++++----------- src/context/profile.tsx | 10 ++---- src/context/reactions.tsx | 16 +++------- src/context/session.tsx | 11 +++++-- .../query/core/articles-load-unrated.ts | 4 +-- .../query/notifier/notifications-load.ts | 4 +-- src/utils/cyrillic.ts | 5 +++ src/utils/getImageUrl.ts | 7 ++-- 17 files changed, 81 insertions(+), 99 deletions(-) create mode 100644 src/utils/cyrillic.ts diff --git a/package-lock.json b/package-lock.json index d0ee57c0..eb3fc3ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,7 @@ "typograf": "7.1.0", "uniqolor": "1.1.0", "vike": "0.4.148", - "vite": "4.5.0", + "vite": "4.5.1", "vite-plugin-mkcert": "1.16.0", "vite-plugin-sass-dts": "1.3.11", "vite-plugin-solid": "2.7.2", @@ -18707,9 +18707,9 @@ } }, "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/package.json b/package.json index 617c8561..ba7d22ec 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "typograf": "7.1.0", "uniqolor": "1.1.0", "vike": "0.4.148", - "vite": "4.5.0", + "vite": "4.5.1", "vite-plugin-mkcert": "1.16.0", "vite-plugin-sass-dts": "1.3.11", "vite-plugin-solid": "2.7.2", diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 4731b28d..9a4b7370 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -13,6 +13,7 @@ import { useSession } from '../../context/session' import { MediaItem } from '../../pages/types' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { capitalize } from '../../utils/capitalize' +import { isCyrillic } from '../../utils/cyrillic' import { getImageUrl } from '../../utils/getImageUrl' import { getDescription, getKeywords } from '../../utils/meta' import { Icon } from '../_shared/Icon' @@ -295,7 +296,9 @@ export const FullArticle = (props: Props) => { const description = getDescription(props.article.description || body()) const ogTitle = props.article.title const keywords = getKeywords(props.article) - + const getAuthorName = (a: Author) => { + return lang() == 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name + } return ( <> @@ -333,7 +336,7 @@ export const FullArticle = (props: Props) => { {(a: Author, index) => ( <> 0}>, - {a.name} + {getAuthorName(a)} )} diff --git a/src/components/Author/AhtorLink/AuthorLink.tsx b/src/components/Author/AhtorLink/AuthorLink.tsx index 16a6111d..262e36ab 100644 --- a/src/components/Author/AhtorLink/AuthorLink.tsx +++ b/src/components/Author/AhtorLink/AuthorLink.tsx @@ -1,6 +1,10 @@ import { clsx } from 'clsx' +import { createMemo } from 'solid-js' +import { useLocalize } from '../../../context/localize' import { Author } from '../../../graphql/schema/core.gen' +import { capitalize } from '../../../utils/capitalize' +import { isCyrillic } from '../../../utils/cyrillic' import { Userpic } from '../Userpic' import styles from './AhtorLink.module.scss' @@ -13,6 +17,12 @@ type 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 (
{ })} > - -
{props.author.name}
+ +
{name()}
) diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 9f648ff0..6c1400e5 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -4,6 +4,7 @@ import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' +import { ChatMember } from '../../../graphql/schema/chat.gen' import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { router, useRouter } from '../../../stores/router' import { follow, unfollow } from '../../../stores/zine/common' diff --git a/src/components/Inbox/DialogCard.tsx b/src/components/Inbox/DialogCard.tsx index afd7f24c..e4cb941e 100644 --- a/src/components/Inbox/DialogCard.tsx +++ b/src/components/Inbox/DialogCard.tsx @@ -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 { Show, Switch, Match, createMemo } from 'solid-js' import { useLocalize } from '../../context/localize' +import { Author } from '../../graphql/schema/core.gen' import { AuthorBadge } from '../Author/AuthorBadge' import DialogAvatar from './DialogAvatar' @@ -26,7 +27,7 @@ type DialogProps = { const DialogCard = (props: DialogProps) => { const { t, formatTime } = useLocalize() 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( @@ -51,11 +52,11 @@ const DialogCard = (props: DialogProps) => { when={props.isChatHeader} fallback={
- +
} > - + } > diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx index d35bf57e..10ab22fd 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.tsx +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -50,6 +50,7 @@ export const NotificationsPanel = (props: Props) => { const { isAuthenticated } = useSession() const { t } = useLocalize() const { + after, sortedNotifications, unreadNotificationsCount, loadedNotificationsCount, @@ -112,7 +113,7 @@ export const NotificationsPanel = (props: Props) => { const scrollContainerRef: { current: HTMLDivElement } = { current: null } const loadNextPage = async () => { - await loadNotifications({ limit: PAGE_SIZE, offset: loadedNotificationsCount() }) + await loadNotifications({ after: after(), limit: PAGE_SIZE, offset: loadedNotificationsCount() }) if (loadedNotificationsCount() < totalNotificationsCount()) { const hasMore = scrollContainerRef.current.scrollHeight <= scrollContainerRef.current.offsetHeight diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 737026da..cdb4022d 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -2,16 +2,15 @@ import type { JSX } from 'solid-js' import { openPage } from '@nanostores/router' 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 { apiClient as coreClient } from '../graphql/client/core' +import { apiClient } from '../graphql/client/core' import { ShoutVisibility, Topic, TopicInput } from '../graphql/schema/core.gen' import { router, useRouter } from '../stores/router' import { slugify } from '../utils/slugify' import { useLocalize } from './localize' -import { useSession } from './session' import { useSnackbar } from './snackbar' type WordCounter = { @@ -83,23 +82,12 @@ const removeDraftFromLocalStorage = (shoutId: number) => { export const EditorProvider = (props: { children: JSX.Element }) => { const { t } = useLocalize() - const { - actions: { getToken }, - } = useSession() const { page } = useRouter() - const apiClient = createMemo(() => { - const token = getToken() - if (!coreClient.private) coreClient.connect(token) - return coreClient - }) const { actions: { showSnackbar }, } = useSnackbar() - const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal(false) - const editorRef: { current: () => Editor } = { current: null } - const [form, setForm] = createStore(null) const [formErrors, setFormErrors] = createStore>(null) const [wordCounter, setWordCounter] = createSignal({ @@ -133,7 +121,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => { - return apiClient().updateArticle({ + return await apiClient.updateArticle({ shoutId: formToUpdate.shoutId, shoutInput: { body: formToUpdate.body, @@ -218,7 +206,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const publishShoutById = async (shoutId: number) => { try { - await apiClient().updateArticle({ + await apiClient.updateArticle({ shoutId, publish: true, }) @@ -232,7 +220,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const deleteShout = async (shoutId: number) => { try { - await apiClient().deleteShout({ + await apiClient.deleteShout({ shoutId, }) return true diff --git a/src/context/inbox.tsx b/src/context/inbox.tsx index 5188e473..7bd2a087 100644 --- a/src/context/inbox.tsx +++ b/src/context/inbox.tsx @@ -5,6 +5,7 @@ import { createContext, createEffect, createSignal, useContext } from 'solid-js' import { inboxClient } from '../graphql/client/chat' import { Author } from '../graphql/schema/core.gen' +import { useAuthorsStore } from '../stores/zine/authors' import { SSEMessage, useConnect } from './connect' import { useSession } from './session' @@ -15,7 +16,7 @@ type InboxContextType = { actions: { createChat: (members: number[], title: string) => Promise<{ chat: Chat }> loadChats: () => Promise> - loadRecipients: () => Promise> + loadRecipients: () => Array loadMessages: (by: MessagesBy, limit: number, offset: number) => Promise> getMessages?: (chatId: string) => Promise> sendMessage?: (args: MutationCreate_MessageArgs) => void @@ -31,6 +32,7 @@ export function useInbox() { export const InboxProvider = (props: { children: JSX.Element }) => { const [chats, setChats] = createSignal([]) const [messages, setMessages] = createSignal([]) + const { sortedAuthors } = useAuthorsStore() const handleMessage = (sseMessage: SSEMessage) => { console.log('[context.inbox]:', sseMessage) @@ -48,25 +50,6 @@ export const InboxProvider = (props: { children: JSX.Element }) => { const { addHandler } = useConnect() 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> => { - if (inboxClient.private) { - // TODO: perhaps setMembers(authors) ? - return await inboxClient.loadRecipients({ limit, offset }) - } - return [] - } - const loadMessages = async ( by: MessagesBy, limit: number = 50, @@ -136,7 +119,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => { createChat, loadChats, loadMessages, - loadRecipients, + loadRecipients: sortedAuthors, getMessages, sendMessage, } diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index 754006d9..5d9721dc 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -1,5 +1,6 @@ import type { Accessor, JSX } from 'solid-js' +import { createStorageSignal } from '@solid-primitives/storage' import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js' import { createStore } from 'solid-js/store' import { Portal } from 'solid-js/web' @@ -7,7 +8,7 @@ import { Portal } from 'solid-js/web' import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated' import { NotificationsPanel } from '../components/NotificationsPanel' 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 { useSession } from './session' @@ -15,6 +16,7 @@ import { useSession } from './session' type NotificationsContextType = { notificationEntities: Record unreadNotificationsCount: Accessor + after: Accessor sortedNotifications: Accessor loadedNotificationsCount: Accessor totalNotificationsCount: Accessor @@ -23,7 +25,7 @@ type NotificationsContextType = { hideNotificationsPanel: () => void markNotificationAsRead: (notification: Notification) => Promise markAllNotificationsAsRead: () => Promise - loadNotifications: (options: { limit: number; offset: number }) => Promise + loadNotifications: (options: QueryLoad_NotificationsArgs) => Promise } } @@ -39,21 +41,10 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [notificationEntities, setNotificationEntities] = createStore>({}) - const { - isAuthenticated, - actions: { getToken }, - } = useSession() - - createEffect(() => { - const token = getToken() - if (!notifierClient.private && token) { - notifierClient.connect(token) - } - }) - + const { isAuthenticated } = useSession() 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) { const { notifications, unread, total } = await notifierClient.getNotifications(options) 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) }) + const now = Math.floor(Date.now() / 1000) const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length) - + const [after, setAfter] = createStorageSignal('notifier_timestamp', now) onMount(() => { addHandler((data: SSEMessage) => { if (data.entity === 'reaction' && isAuthenticated()) { - loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) + loadNotifications({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) } else { - console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`) + console.info(`[NotificationsProvider] bypassed:`, data) } }) + setAfter(now) }) const markNotificationAsRead = async (notification: Notification) => { @@ -96,7 +89,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { const markAllNotificationsAsRead = async () => { if (isAuthenticated() && notifierClient.private) { 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 = { + after, notificationEntities, sortedNotifications, unreadNotificationsCount, diff --git a/src/context/profile.tsx b/src/context/profile.tsx index 10b50a0f..3688f60b 100644 --- a/src/context/profile.tsx +++ b/src/context/profile.tsx @@ -3,7 +3,7 @@ import type { ProfileInput } from '../graphql/schema/core.gen' import { createContext, createEffect, createMemo, JSX, useContext } from 'solid-js' 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 { useSession } from './session' @@ -38,14 +38,8 @@ export const ProfileFormProvider = (props: { children: JSX.Element }) => { 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 response = await apiClient().updateProfile(profile) + const response = await apiClient.updateProfile(profile) if (response.error) { console.error(response.error) throw response.error diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index d37ff466..c9893b6c 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -3,7 +3,7 @@ import type { JSX } from 'solid-js' import { createContext, createMemo, onCleanup, useContext } from 'solid-js' 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 { useSession } from './session' @@ -38,12 +38,6 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { actions: { getToken }, } = useSession() - const apiClient = createMemo(() => { - const token = getToken() - if (!coreClient.private) coreClient.connect(token) - return coreClient - }) - const loadReactionsBy = async ({ by, limit, @@ -53,7 +47,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { limit?: number offset?: number }): Promise => { - const reactions = await coreClient.getReactionsBy({ by, limit, offset }) + const reactions = await apiClient.getReactionsBy({ by, limit, offset }) const newReactionEntities = reactions.reduce((acc, reaction) => { acc[reaction.id] = reaction return acc @@ -63,7 +57,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } const createReaction = async (input: ReactionInput): Promise => { - const reaction = await apiClient().createReaction(input) + const reaction = await apiClient.createReaction(input) const changes = { [reaction.id]: reaction, @@ -90,14 +84,14 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } const deleteReaction = async (id: number): Promise => { - const reaction = await apiClient().destroyReaction(id) + const reaction = await apiClient.destroyReaction(id) setReactionEntities({ [reaction.id]: undefined, }) } const updateReaction = async (id: number, input: ReactionInput): Promise => { - const reaction = await apiClient().updateReaction(id, input) + const reaction = await apiClient.updateReaction(id, input) setReactionEntities(reaction.id, reaction) } diff --git a/src/context/session.tsx b/src/context/session.tsx index e8ce4ad8..a8314bad 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -20,7 +20,9 @@ import { useContext, } from 'solid-js' +import { inboxClient } from '../graphql/client/chat' import { apiClient } from '../graphql/client/core' +import { notifierClient } from '../graphql/client/notifier' import { useRouter } from '../stores/router' import { showModal } from '../stores/ui' @@ -121,9 +123,12 @@ export const SessionProvider = (props: { }) createEffect(() => { - // authorized graphql client - const tkn = getToken() - if (tkn) apiClient.connect(tkn) + const token = getToken() + if (!inboxClient.private && token) { + apiClient.connect(token) + notifierClient.connect(token) + inboxClient.connect(token) + } }) const loadSubscriptions = async (): Promise => { diff --git a/src/graphql/query/core/articles-load-unrated.ts b/src/graphql/query/core/articles-load-unrated.ts index a4d01b66..29b647de 100644 --- a/src/graphql/query/core/articles-load-unrated.ts +++ b/src/graphql/query/core/articles-load-unrated.ts @@ -1,8 +1,8 @@ import { gql } from '@urql/core' export default gql` - query LoadUnratedShoutsQuery($limit: Int!) { - loadUnratedShouts(limit: $limit) { + query LoadUnratedShoutsQuery($limit: Int, $offset: Int) { + load_shouts_unrated(limit: $limit, offset: $offset) { id title # lead diff --git a/src/graphql/query/notifier/notifications-load.ts b/src/graphql/query/notifier/notifications-load.ts index d119048d..666081ca 100644 --- a/src/graphql/query/notifier/notifications-load.ts +++ b/src/graphql/query/notifier/notifications-load.ts @@ -1,8 +1,8 @@ import { gql } from '@urql/core' export default gql` - query LoadNotificationsQuery($limit: Int, $offset: Int) { - load_notifications(limit: $limit, offset: $offset) { + query LoadNotificationsQuery($after: Int!, $limit: Int, $offset: Int) { + load_notifications(after: $after, limit: $limit, offset: $offset) { notifications { id entity diff --git a/src/utils/cyrillic.ts b/src/utils/cyrillic.ts new file mode 100644 index 00000000..09b6420e --- /dev/null +++ b/src/utils/cyrillic.ts @@ -0,0 +1,5 @@ +export const isCyrillic = (s: string): boolean => { + const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters + + return cyrillicRegex.test(s) +} diff --git a/src/utils/getImageUrl.ts b/src/utils/getImageUrl.ts index 1d7123d7..d91e23db 100644 --- a/src/utils/getImageUrl.ts +++ b/src/utils/getImageUrl.ts @@ -13,6 +13,9 @@ const getSizeUrlPart = (options: { width?: number; height?: number } = {}) => { export const getImageUrl = (src: string, options: { width?: number; height?: number } = {}) => { const sizeUrlPart = getSizeUrlPart(options) - - return `${thumborUrl}/unsafe/${sizeUrlPart}${src.replace(thumborUrl, '').replace('/unsafe', '')}` + const sourceUrl = src.replace(thumborUrl, '').replace('/unsafe', '') + return ( + 'https://' + + `${thumborUrl}/unsafe/${sizeUrlPart}${sourceUrl}`.replace('https://', '').replace('//', '/') + ) }