diff --git a/package-lock.json b/package-lock.json index e8a84240..5c76496f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "form-data": "4.0.0", + "idb": "8.0.0", "mailgun.js": "10.1.0" }, "devDependencies": { @@ -7231,6 +7232,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index 9a277dbc..5e79d11f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "form-data": "4.0.0", + "idb": "8.0.0", "mailgun.js": "10.1.0" }, "devDependencies": { diff --git a/src/components/App.tsx b/src/components/App.tsx index 7ad43757..726dd596 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -14,6 +14,7 @@ import { MediaQueryProvider } from '../context/mediaQuery' import { NotificationsProvider } from '../context/notifications' import { SessionProvider } from '../context/session' import { SnackbarProvider } from '../context/snackbar' +import { TopicsProvider } from '../context/topics' import { DiscussionRulesPage } from '../pages/about/discussionRules.page' import { DogmaPage } from '../pages/about/dogma.page' import { GuidePage } from '../pages/about/guide.page' @@ -117,21 +118,23 @@ export const App = (props: Props) => { - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx index ff0661c9..81e69d58 100644 --- a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx +++ b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx @@ -39,6 +39,7 @@ export const PlayerPlaylist = (props: Props) => { } const play = (index: number) => { + props.onPlayMedia(index) const mi = props.media[index] gtag('event', 'select_item', { item_list_id: props.articleSlug, diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 1c4c774d..98c9ec05 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -160,7 +160,7 @@ export const LoginForm = () => { /> -
{submitError()}
+
{submitError()}
diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx index 5c11b8ad..a478225d 100644 --- a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -41,8 +41,9 @@ export const PasswordField = (props: Props) => { } const handleInputBlur = (value: string) => { - if (props.variant === 'login') { - return props.onBlur(value) + if (props.variant === 'login' && props.onBlur) { + props.onBlur(value) + return } if (value.length < 1) { return diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index dfbb2e71..7c0258e1 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -6,12 +6,10 @@ import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' -import { apiClient } from '../../../graphql/client/core' import { ROUTES, router, useRouter } from '../../../stores/router' import { useModalStore } from '../../../stores/ui' import { getDescription } from '../../../utils/meta' import { SharePopup, getShareUrl } from '../../Article/SharePopup' -import { RANDOM_TOPICS_COUNT } from '../../Views/Home' import { Icon } from '../../_shared/Icon' import { Subscribe } from '../../_shared/Subscribe' import { AuthModal } from '../AuthModal' @@ -23,6 +21,8 @@ import { Snackbar } from '../Snackbar' import { Link } from './Link' +import { useTopics } from '../../../context/topics' +import { getRandomTopicsFromArray } from '../../../utils/getRandomTopicsFromArray' import styles from './Header.module.scss' type Props = { @@ -48,6 +48,7 @@ export const Header = (props: Props) => { const { page } = useRouter() const { requireAuthentication } = useSession() const { searchParams } = useRouter() + const { topics } = useTopics() const [randomTopics, setRandomTopics] = createSignal([]) const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false) @@ -58,6 +59,7 @@ export const Header = (props: Props) => { const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false) + const toggleFixed = () => setFixed(!fixed()) const tag = (topic: Topic) => @@ -65,6 +67,10 @@ export const Header = (props: Props) => { let windowScrollTop = 0 + createEffect(() => { + setRandomTopics(getRandomTopicsFromArray(topics())) + }) + createEffect(() => { const mainContent = document.querySelector('.main-content') @@ -141,11 +147,6 @@ export const Header = (props: Props) => { }, time) } - onMount(async () => { - const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT }) - setRandomTopics(topics) - }) - const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => { if (!fixed()) { return diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 1c2afd98..70389ccb 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -123,7 +123,7 @@ export const ProfileSettings = () => { setIsUserpicUpdating(true) const result = await handleImageUpload(uploadFile) - updateFormField('userpic', result.url) + updateFormField('pic', result.url) setUserpicFile(null) setIsUserpicUpdating(false) diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 6ca91c66..a592f433 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -26,6 +26,7 @@ import { Loading } from '../../_shared/Loading' import { byCreated } from '../../../utils/sortby' import stylesArticle from '../../Article/Article.module.scss' import styles from './Author.module.scss' +import { hideModal, MODALS } from "../../../stores/ui"; type Props = { shouts: Shout[] @@ -39,13 +40,14 @@ export const AuthorView = (props: Props) => { const { t } = useLocalize() const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { authorEntities } = useAuthorsStore({ authors: [props.author] }) - const { page: getPage } = useRouter() + const { page: getPage, searchParams } = useRouter() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [followers, setFollowers] = createSignal([]) const [following, setFollowing] = createSignal>([]) const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [commented, setCommented] = createSignal() + const modal = MODALS[searchParams().m] // current author const [author, setAuthor] = createSignal() @@ -102,6 +104,9 @@ export const AuthorView = (props: Props) => { } onMount(() => { + if (!modal) { + hideModal() + } checkBioHeight() // pagination if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 6dbe995b..eddd12a4 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -24,8 +24,8 @@ type Props = { layout: LayoutType } -export const PRERENDERED_ARTICLES_COUNT = 24 -const LOAD_MORE_PAGE_SIZE = 16 +export const PRERENDERED_ARTICLES_COUNT = 37 +const LOAD_MORE_PAGE_SIZE = 11 export const Expo = (props: Props) => { const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts)) diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 4e4dbce3..25e15a31 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -64,18 +64,8 @@ export const HomeView = (props: Props) => { limit: CLIENT_LOAD_ARTICLES_COUNT, offset: sortedArticles().length, }) - setIsLoadMoreButtonVisible(hasMore) } - - const result = await apiClient.getRandomTopicShouts(RANDOM_TOPIC_SHOUTS_COUNT) - if (!result || result.error) console.warn('[apiClient.getRandomTopicShouts] failed') - batch(() => { - if (!result?.error) { - if (result?.topic) setRandomTopic(result.topic) - if (result?.shouts) setRandomTopicArticles(result.shouts) - } - }) }) const loadMore = async () => { diff --git a/src/context/profile.tsx b/src/context/profile.tsx index 1b88f149..775b55c0 100644 --- a/src/context/profile.tsx +++ b/src/context/profile.tsx @@ -54,18 +54,12 @@ export const ProfileFormProvider = (props: { children: JSX.Element }) => { const updateFormField = (fieldName: string, value: string, remove?: boolean) => { if (fieldName === 'links') { - if (remove) { - setForm( - 'links', - form.links.filter((item) => item !== value), - ) - } else { - setForm((prev) => ({ ...prev, links: [...prev.links, value] })) - } - } else { - setForm({ - [fieldName]: value, + setForm((prev) => { + const updatedLinks = remove ? prev.links.filter((item) => item !== value) : [...prev.links, value] + return { ...prev, links: updatedLinks } }) + } else { + setForm((prev) => ({ ...prev, [fieldName]: value })) } } diff --git a/src/context/topics.tsx b/src/context/topics.tsx new file mode 100644 index 00000000..4fe8d5f6 --- /dev/null +++ b/src/context/topics.tsx @@ -0,0 +1,59 @@ +import { openDB } from 'idb' +import { Accessor, JSX, createContext, createSignal, onMount, useContext } from 'solid-js' +import { apiClient } from '../graphql/client/core' +import { Topic } from '../graphql/schema/core.gen' + +type TopicsContextType = { + topics: Accessor +} + +const TopicsContext = createContext() +export function useTopics() { + return useContext(TopicsContext) +} + +const DB_NAME = 'discourseAppDB' +const DB_VERSION = 1 +const STORE_NAME = 'topics' +const setupIndexedDB = async () => { + return await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }) + } + }, + }) +} + +const getTopicsFromIndexedDB = async (db) => { + const tx = db.transaction(STORE_NAME, 'readonly') + const store = tx.objectStore(STORE_NAME) + return store.getAll() +} +const saveTopicsToIndexedDB = async (db, topics) => { + const tx = db.transaction(STORE_NAME, 'readwrite') + const store = tx.objectStore(STORE_NAME) + for (const topic of topics) { + await store.put(topic) + } + await tx.done +} + +export const TopicsProvider = (props: { children: JSX.Element }) => { + const [randomTopics, setRandomTopics] = createSignal([]) + + onMount(async () => { + const db = await setupIndexedDB() + let topics = await getTopicsFromIndexedDB(db) + + if (topics.length === 0) { + topics = await apiClient.getAllTopics() + await saveTopicsToIndexedDB(db, topics) + } + setRandomTopics(topics) + }) + + const value: TopicsContextType = { topics: randomTopics } + + return {props.children} +} diff --git a/src/graphql/client/chat.ts b/src/graphql/client/chat.ts index 42c5be97..c00fd705 100644 --- a/src/graphql/client/chat.ts +++ b/src/graphql/client/chat.ts @@ -1,3 +1,4 @@ +import { chatApiUrl } from '../../utils/config' // inbox import { createGraphQLClient } from '../createGraphQLClient' import createChat from '../mutation/chat/chat-create' @@ -24,7 +25,7 @@ import { export const inboxClient = { private: null, - connect: (token: string) => (inboxClient.private = createGraphQLClient('chat', token)), + connect: (token: string) => (inboxClient.private = createGraphQLClient(chatApiUrl, token)), loadChats: async (options: QueryLoad_ChatsArgs): Promise => { const resp = await inboxClient.private.query(myChats, options).toPromise() diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index 4cbe81c2..e7d8708e 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -16,6 +16,7 @@ import type { Topic, } from '../schema/core.gen' +import { coreApiUrl } from '../../utils/config' import { createGraphQLClient } from '../createGraphQLClient' import createArticle from '../mutation/core/article-create' import deleteShout from '../mutation/core/article-delete' @@ -47,13 +48,13 @@ import topicBySlug from '../query/core/topic-by-slug' import topicsAll from '../query/core/topics-all' import topicsRandomQuery from '../query/core/topics-random' -const publicGraphQLClient = createGraphQLClient('core') +const publicGraphQLClient = createGraphQLClient(coreApiUrl) export const apiClient = { private: null, connect: (token: string) => { // NOTE: use it after token appears - apiClient.private = createGraphQLClient('core', token) + apiClient.private = createGraphQLClient(coreApiUrl, token) }, getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => { @@ -182,7 +183,7 @@ export const apiClient = { createReaction: async (input: ReactionInput) => { const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise() console.debug('[graphql.client.core] createReaction:', response) - return response.data.create_reaction.reaction + return response.data.create_reaction }, destroyReaction: async (reaction_id: number) => { const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise() @@ -192,7 +193,7 @@ export const apiClient = { updateReaction: async (reaction: ReactionInput) => { const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise() console.debug('[graphql.client.core] updateReaction:', response) - return response.data.update_reaction.reaction + return response.data.update_reaction }, loadAuthorsBy: async (args: QueryLoad_Authors_ByArgs) => { const resp = await publicGraphQLClient.query(authorsLoadBy, args).toPromise() diff --git a/src/graphql/createGraphQLClient.ts b/src/graphql/createGraphQLClient.ts index 62755e9a..89b0d620 100644 --- a/src/graphql/createGraphQLClient.ts +++ b/src/graphql/createGraphQLClient.ts @@ -1,18 +1,17 @@ -import { ClientOptions, Exchange, createClient, dedupExchange, fetchExchange } from '@urql/core' +import { ClientOptions, Exchange, createClient, fetchExchange } from '@urql/core' import { devtoolsExchange } from '@urql/devtools' import { isDev } from '../utils/config' -const exchanges: Exchange[] = [dedupExchange, fetchExchange] +const exchanges: Exchange[] = [fetchExchange] if (isDev) { exchanges.unshift(devtoolsExchange) } -export const createGraphQLClient = (serviceName: string, token = '') => { +export const createGraphQLClient = (url: string, token = '') => { const options: ClientOptions = { - url: `https://${serviceName}.discours.io`, - maskTypename: true, + url, requestPolicy: 'cache-and-network', fetchOptions: () => (token ? { headers: { Authorization: token } } : {}), exchanges, diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index e517378b..a52abf65 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -131,9 +131,8 @@ export const loadShout = async (slug: string): Promise => { export const loadShouts = async ( options: LoadShoutsOptions, ): Promise<{ hasMore: boolean; newShouts: Shout[] }> => { - options.limit += 1 const newShouts = await apiClient.getShouts(options) - const hasMore = newShouts?.length === options.limit + 1 + const hasMore = newShouts?.length !== options.limit + 1 && newShouts?.length !== 0 if (hasMore) { newShouts.splice(-1) diff --git a/src/utils/config.ts b/src/utils/config.ts index 1cfd89a5..cfc2f7fa 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -8,3 +8,12 @@ export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || '' const defaultSearchUrl = 'https://search.discours.io' export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl + +const defaultCoreUrl = 'https://core.discours.io' +export const coreApiUrl = import.meta.env.PUBLIC_CORE_API || defaultCoreUrl + +const defaultChatUrl = 'https://chat.discours.io' +export const chatApiUrl = import.meta.env.PUBLIC_CHAT_API || defaultChatUrl + +const defaultAuthUrl = 'https://auth.discours.io' +export const authApiUrl = import.meta.env.PUBLIC_AUTH_API || defaultAuthUrl diff --git a/src/utils/getRandomTopicsFromArray.ts b/src/utils/getRandomTopicsFromArray.ts new file mode 100644 index 00000000..ced616c0 --- /dev/null +++ b/src/utils/getRandomTopicsFromArray.ts @@ -0,0 +1,7 @@ +import { RANDOM_TOPICS_COUNT } from '../components/Views/Home' +import { Topic } from '../graphql/schema/core.gen' + +export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => { + const shuffledTopics = [...topics].sort(() => 0.5 - Math.random()) + return shuffledTopics.slice(0, count) +}