refactored.

This commit is contained in:
Untone 2023-11-28 16:18:25 +03:00
parent 9fb5e27906
commit 8cfbe8303c
167 changed files with 8561 additions and 6921 deletions

View File

@ -1,6 +1,6 @@
node_modules node_modules
public public
*.cjs *.cjs
src/graphql/*.gen.ts src/graphql/schema/*.gen.ts
dist/ dist/
.vercel/ .vercel/

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ pnpm-debug.log*
.eslint/.eslintcache .eslint/.eslintcache
public/upload/* public/upload/*
src/graphql/introspec.gen.ts src/graphql/introspec.gen.ts
src/graphql/schema/*.gen.ts
stats.html stats.html
*.scss.d.ts *.scss.d.ts
pnpm-lock.yaml pnpm-lock.yaml

View File

@ -1,179 +0,0 @@
[0.8.0]
[+] i18next for ,solid
[-] i18n
[+] custom snackbar
[+] editor lazy load
[+] hygen
[-] astro removed
[+] vite ssr plugin
[0.7.1]
[+] reactions CUDL
[+] api/upload with storj
[+] api/feedback
[+] bumped astro pkgs versions
[+] graphql ws subs
[0.7.0]
[+] inbox: context provider, chats
[+] comments: show
[+] session: context provider
[+] views tracker: counting for shouts
[0.6.1]
[+] auth ver. 0.9
[+] load-by interfaces for shouts, authors and messages
[+] inbox logix and markup
[-] old views counting
[0.6.0]
[+] hybrid routing ssr/spa
[+] 'expo' pages
[-] layout term usage with an exception
[-] less nanostores
[+] inbox
[+] css modules
[+] draft editor
[+] solid-driven storages
[0.5.1]
[+] nanostores-base global store
[-] Root.tsx components
[+] astro/solid basic hydration
[0.5.0]
[-] removed solid-primitives/i18n
[+] added custom dummy utils/intl
[-] solid-app-router
[+] astro build and routing
[-] solid-top-loading-bar
[+] lint, prettier
[-] context providers, _cache
[+] ssr PoW
[0.4.1]
[-] markdown-it
[+] remark, rehype, gfm
[+] api fixes
[0.4.0]
[+] upload, feedback, newsletter serverless
[-] ratings
[-] comments
[-] proposals
[+] universal reaction entity
[+] staged preload
[0.3.1]
[+] promisisified stores
[+] prerender based on mdx
[+] hybryd zine state manager
[0.3.0]
[+] markup is simpler
[+] really use mdx
[+] really use i18n
[+] refactored queries
[+] final routing
[0.2.1]
[+] custom store
[+] playwright
[+] mdx
[0.2.0]
[-] sveltekit
[-] graphql-request
[+] migrated to solid
[+] urql
[+] graphql caching results
[0.1.0]
[+] husky, lint-staged
[+] components refactoring
[+] 'static' pages fixes
[+] ShoutFeed's reusable components
[+] render order revised
[0.0.9]
[+] lots of visual changes for demo
[+] cookie-based subscriptions
[+] prerender fix
[+] refactor queries
[+] caching topics with localStorage
[+] added some 'static' routes
[0.0.8]
[+] isolated editor codebase
[+] sveo
[+] SSG first
[+] svelte-kit caching fixes
[+] isolated MD component
[-] code cleanup
[-] /auth route
[-] top nav changes
[0.0.7]
[+] nav refactoring /[what] /@[who]
[+] /reset/[code], /reset/password
[+] modal auth dialog
[+] Topic.pic field
[+] internal svelte prerender
[+] GET_SHOUTS, TOP_SHOUTS_BY_RATING, GET_TOPICS, GET_COMMUNITIES via caching json trick
[~] User.username -> User.name
[0.0.6]
[-] organization, org_id
[+] community entity
[+] mainpage markup
[+] topics filter navigation
[+] monor schema fixes
[-] gitea.js api
[-] postcss with plugins
[-] bootstrap
[+] windicss
[+] async sveltekit-styled queries
[+] login basic markup
[0.0.5]
[+] migrate to sveltekit
[-] removed apollo due bug
[-] removed custom prerender code
[+] stylelint enabled
[+] precompiler windows support
[+] precompiler separated
[0.0.4]
[+] precompiler generated static indexes
[-] puppeteer switched off
[+] topic entity added
[-] i18n switched off
[+] own signaling server connected
[-] store-based routing removed
[+] reset password page
[+] login/register form
[+] social auth fb, ggl, vk
[0.0.3]
[~] prerender with puppeteer
[+] precompiled data.json
[~] international content support
[+] auth graphql client
[-] removed ws yjs-server
[-] pathfinder replaced
[+] mdsvex support
[0.0.2]
[+] apollo client with codegen
[+] ci basics
[+] code organized
[0.0.1]
[+] 3rd party deps: tiptap, apollo,
[+] boiilerplate with esbuild
[+] simple structure

View File

@ -1,21 +1,53 @@
overwrite: true overwrite: true
schema: 'http://127.0.0.1:8080'
#schema: 'https://v2.discours.io'
generates: generates:
src/graphql/introspec.gen.ts: # Generate types for chat
plugins: src/graphql/schema/chat.gen.ts:
- urql-introspection schema: 'https://chat.discours.io'
config:
useTypeImports: true
includeScalars: true
includeEnums: true
src/graphql/types.gen.ts:
plugins: plugins:
- 'typescript' - 'typescript'
- 'typescript-operations' - 'typescript-operations'
- 'typescript-urql' - 'typescript-urql'
config: config:
skipTypename: true skipTypename: true
useTypeImports: true
outputPath: './src/graphql/types/chat.gen.ts'
# Generate types for core
src/graphql/schema/core.gen.ts:
schema: 'https://testapi.discours.io'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-urql'
config:
skipTypename: true
useTypeImports: true
outputPath: './src/graphql/types/core.gen.ts'
# Generate types for notifier
src/graphql/schema/notifier.gen.ts:
schema: 'http://notifier.discours.io' # FIXME: https
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-urql'
config:
skipTypename: true
useTypeImports: true
outputPath: './src/graphql/types/notifier.gen.ts'
# Generate types for auth
src/graphql/schema/auth.gen.ts:
schema: 'https://auth.discours.io/graphql'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-urql'
config:
skipTypename: true
useTypeImports: true
outputPath: './src/graphql/types/auth.gen.ts'
hooks: hooks:
afterAllFileWrite: afterAllFileWrite:
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/types.gen.ts - prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts

12009
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "discoursio-webapp", "name": "discoursio-webapp",
"version": "0.8.0", "version": "0.9.0",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
@ -12,6 +12,7 @@
"dev": "vite", "dev": "vite",
"fix": "npm run lint:code:fix && npm run lint:styles:fix", "fix": "npm run lint:code:fix && npm run lint:styles:fix",
"format": "npx prettier \"{,!(node_modules)/**/}*.{js,ts,tsx,json,scss,css}\" --write --ignore-path .gitignore", "format": "npx prettier \"{,!(node_modules)/**/}*.{js,ts,tsx,json,scss,css}\" --write --ignore-path .gitignore",
"postinstall": "npm run codegen",
"lint": "npm run lint:code && npm run lint:styles", "lint": "npm run lint:code && npm run lint:styles",
"lint:code": "eslint .", "lint:code": "eslint .",
"lint:code:fix": "eslint . --fix", "lint:code:fix": "eslint . --fix",
@ -30,6 +31,7 @@
"typecheck:watch": "tsc --noEmit --watch" "typecheck:watch": "tsc --noEmit --watch"
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "1.2.11",
"form-data": "4.0.0", "form-data": "4.0.0",
"i18next": "22.4.15", "i18next": "22.4.15",
"i18next-icu": "2.3.0", "i18next-icu": "2.3.0",

View File

@ -40,6 +40,7 @@ import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page' import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router' import { ROUTES, useRouter } from '../stores/router'
import { hideModal, MODALS, showModal } from '../stores/ui' import { hideModal, MODALS, showModal } from '../stores/ui'
import { ConnectProvider } from '../context/connect'
// TODO: lazy load // TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage')) // const SomePage = lazy(() => import('./Pages/SomePage'))
@ -119,11 +120,13 @@ export const App = (props: Props) => {
<SnackbarProvider> <SnackbarProvider>
<ConfirmProvider> <ConfirmProvider>
<SessionProvider> <SessionProvider>
<ConnectProvider>
<NotificationsProvider> <NotificationsProvider>
<EditorProvider> <EditorProvider>
<Dynamic component={pageComponent()} {...props} /> <Dynamic component={pageComponent()} {...props} />
</EditorProvider> </EditorProvider>
</NotificationsProvider> </NotificationsProvider>
</ConnectProvider>
</SessionProvider> </SessionProvider>
</ConfirmProvider> </ConfirmProvider>
</SnackbarProvider> </SnackbarProvider>

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { Topic } from '../../../graphql/types.gen' import { Topic } from '../../../graphql/schema/core.gen'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'

View File

@ -7,7 +7,7 @@ import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions' import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen' import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
@ -25,7 +25,7 @@ type Props = {
compact?: boolean compact?: boolean
isArticleAuthor?: boolean isArticleAuthor?: boolean
sortedComments?: Reaction[] sortedComments?: Reaction[]
lastSeen?: Date lastSeen?: number
class?: string class?: string
showArticleLink?: boolean showArticleLink?: boolean
clickedReply?: (id: number) => void clickedReply?: (id: number) => void
@ -52,7 +52,7 @@ export const Comment = (props: Props) => {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug) const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === session()?.author?.slug)
const comment = createMemo(() => props.comment) const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim()) const body = createMemo(() => (comment().body || '').trim())
@ -82,7 +82,7 @@ export const Comment = (props: Props) => {
setLoading(true) setLoading(true)
await createReaction({ await createReaction({
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
replyTo: props.comment.id, reply_to: props.comment.id,
body: value, body: value,
shout: props.comment.shout.id, shout: props.comment.shout.id,
}) })
@ -114,13 +114,11 @@ export const Comment = (props: Props) => {
} }
} }
const createdAt = new Date(comment()?.createdAt)
return ( return (
<li <li
id={`comment_${comment().id}`} id={`comment_${comment().id}`}
class={clsx(styles.comment, props.class, { class={clsx(styles.comment, props.class, {
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen, [styles.isNew]: !isCommentAuthor() && comment()?.created_at > props.lastSeen,
})} })}
> >
<Show when={!!body()}> <Show when={!!body()}>
@ -130,8 +128,8 @@ export const Comment = (props: Props) => {
fallback={ fallback={
<div> <div>
<Userpic <Userpic
name={comment().createdBy.name} name={comment().created_by.name}
userpic={comment().createdBy.userpic} userpic={comment().created_by.pic}
class={clsx({ class={clsx({
[styles.compactUserpic]: props.compact, [styles.compactUserpic]: props.compact,
})} })}
@ -144,7 +142,7 @@ export const Comment = (props: Props) => {
> >
<div class={styles.commentDetails}> <div class={styles.commentDetails}>
<div class={styles.commentAuthor}> <div class={styles.commentAuthor}>
<AuthorLink author={comment()?.createdBy as Author} /> <AuthorLink author={comment()?.created_by as Author} />
</div> </div>
<Show when={props.isArticleAuthor}> <Show when={props.isArticleAuthor}>
@ -253,7 +251,7 @@ export const Comment = (props: Props) => {
</Show> </Show>
<Show when={props.sortedComments}> <Show when={props.sortedComments}>
<ul> <ul>
<For each={props.sortedComments.filter((r) => r.replyTo === props.comment.id)}> <For each={props.sortedComments.filter((r) => r.reply_to === props.comment.id)}>
{(c) => ( {(c) => (
<Comment <Comment
sortedComments={props.sortedComments} sortedComments={props.sortedComments}

View File

@ -1,4 +1,4 @@
import type { Reaction } from '../../../graphql/types.gen' import type { Reaction } from '../../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show } from 'solid-js' import { Show } from 'solid-js'
@ -33,12 +33,12 @@ export const CommentDate = (props: Props) => {
[styles.showOnHover]: props.showOnHover, [styles.showOnHover]: props.showOnHover,
})} })}
> >
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time> <time class={styles.date}>{formattedDate(props.comment.created_at * 1000)}</time>
<Show when={props.comment.updatedAt}> <Show when={props.comment.updated_at}>
<time class={styles.date}> <time class={styles.date}>
<Icon name="edit" class={styles.icon} /> <Icon name="edit" class={styles.icon} />
<span class={styles.text}> <span class={styles.text}>
{t('Edited')} {formattedDate(props.comment.updatedAt)} {t('Edited')} {formattedDate(props.comment.updated_at * 1000)}
</span> </span>
</time> </time>
</Show> </Show>

View File

@ -1,5 +1,3 @@
import type { Reaction } from '../../graphql/types.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
@ -7,7 +5,7 @@ import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { ReactionKind } from '../../graphql/types.gen' import { Reaction, ReactionKind } from '../../graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles' import { loadShout } from '../../stores/zine/articles'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
@ -33,20 +31,20 @@ export const CommentRatingControl = (props: Props) => {
Object.values(reactionEntities).some( Object.values(reactionEntities).some(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.created_by.slug === user()?.slug &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id, r.reply_to === props.comment.id,
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const canVote = createMemo(() => user()?.slug !== props.comment.createdBy.slug) const canVote = createMemo(() => user()?.slug !== props.comment.created_by.slug)
const commentRatingReactions = createMemo(() => const commentRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter( Object.values(reactionEntities).filter(
(r) => (r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id, r.reply_to === props.comment.id,
), ),
) )
@ -54,9 +52,9 @@ export const CommentRatingControl = (props: Props) => {
const reactionToDelete = Object.values(reactionEntities).find( const reactionToDelete = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.created_by.slug === user()?.slug &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id, r.reply_to === props.comment.id,
) )
return deleteReaction(reactionToDelete.id) return deleteReaction(reactionToDelete.id)
} }
@ -71,7 +69,7 @@ export const CommentRatingControl = (props: Props) => {
await createReaction({ await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id, shout: props.comment.shout.id,
replyTo: props.comment.id, reply_to: props.comment.id,
}) })
} }
} catch { } catch {

View File

@ -4,7 +4,7 @@ import { Show, createMemo, createSignal, onMount, For, lazy } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen' import { Author, Reaction, ReactionKind } from '../../graphql/schema/core.gen'
import { byCreated } from '../../utils/sortby' import { byCreated } from '../../utils/sortby'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
@ -18,7 +18,7 @@ const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly' type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => { const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
if (a.replyTo && b.replyTo) { if (a.reply_to && b.reply_to) {
return 0 return 0
} }
@ -76,19 +76,19 @@ export const CommentsTree = (props: Props) => {
return newSortedComments return newSortedComments
}) })
const dateFromLocalStorage = new Date(localStorage.getItem(`${props.shoutSlug}`)) const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`))
const currentDate = new Date() const currentDate = new Date()
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`) const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
onMount(() => { onMount(() => {
if (!dateFromLocalStorage) { if (!dateFromLocalStorage) {
setCookie() setCookie()
} else if (currentDate > dateFromLocalStorage) { } else if (currentDate.getTime() > dateFromLocalStorage) {
const newComments = comments().filter((c) => { const newComments = comments().filter((c) => {
if (c.replyTo || c.createdBy.slug === session()?.user.slug) { if (c.reply_to || c.created_by.slug === session()?.user.slug) {
return return
} }
const created = new Date(c.createdAt) const created = c.created_at
return created > dateFromLocalStorage return created > dateFromLocalStorage
}) })
setNewReactions(newComments) setNewReactions(newComments)
@ -153,12 +153,12 @@ export const CommentsTree = (props: Props) => {
</Show> </Show>
</div> </div>
<ul class={styles.comments}> <ul class={styles.comments}>
<For each={sortedComments().filter((r) => !r.replyTo)}> <For each={sortedComments().filter((r) => !r.reply_to)}>
{(reaction) => ( {(reaction) => (
<Comment <Comment
sortedComments={sortedComments()} sortedComments={sortedComments()}
isArticleAuthor={Boolean( isArticleAuthor={Boolean(
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug), props.articleAuthors.some((a) => a.slug === reaction.created_by.slug),
)} )}
comment={reaction} comment={reaction}
clickedReply={(id) => setClickedReplyId(id)} clickedReply={(id) => setClickedReplyId(id)}

View File

@ -1,4 +1,4 @@
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core' import { createPopper } from '@popperjs/core'
@ -68,13 +68,9 @@ export const FullArticle = (props: Props) => {
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) const formattedDate = createMemo(() => formatDate(new Date(props.article.created_at * 1000)))
const mainTopic = createMemo( const mainTopic = createMemo(() => (props.article.topics.length > 0 ? props.article.topics[0] : null))
() =>
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
props.article.topics[0],
)
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
@ -293,10 +289,10 @@ export const FullArticle = (props: Props) => {
onClick={handleArticleBodyClick} onClick={handleArticleBodyClick}
> >
{/*TODO: Check styles.shoutTopic*/} {/*TODO: Check styles.shoutTopic*/}
<Show when={props.article.layout !== 'music'}> <Show when={props.article.layout !== 'audio'}>
<div class={styles.shoutHeader}> <div class={styles.shoutHeader}>
<Show when={mainTopic()}> <Show when={mainTopic()}>
<CardTopic title={mainTopic().title} slug={props.article.mainTopic} /> <CardTopic title={mainTopic().title} slug={mainTopic().slug} />
</Show> </Show>
<h1>{props.article.title}</h1> <h1>{props.article.title}</h1>
@ -328,7 +324,7 @@ export const FullArticle = (props: Props) => {
<Show when={props.article.lead}> <Show when={props.article.lead}>
<section class={styles.lead} innerHTML={props.article.lead} /> <section class={styles.lead} innerHTML={props.article.lead} />
</Show> </Show>
<Show when={props.article.layout === 'music'}> <Show when={props.article.layout === 'audio'}>
<AudioHeader <AudioHeader
title={props.article.title} title={props.article.title}
cover={props.article.cover} cover={props.article.cover}

View File

@ -4,7 +4,7 @@ import { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { ReactionKind, Shout } from '../../graphql/types.gen' import { ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles' import { loadShout } from '../../stores/zine/articles'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
@ -33,9 +33,9 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
Object.values(reactionEntities).some( Object.values(reactionEntities).some(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.created_by.slug === user()?.slug &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo, !r.reply_to,
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
@ -47,7 +47,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
(r) => (r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo, !r.reply_to,
), ),
) )
@ -55,9 +55,9 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const reactionToDelete = Object.values(reactionEntities).find( const reactionToDelete = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.created_by.slug === user()?.slug &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo, !r.reply_to,
) )
return deleteReaction(reactionToDelete.id) return deleteReaction(reactionToDelete.id)
} }

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Author } from '../../../graphql/types.gen' import { Author } from '../../../graphql/schema/core.gen'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss' import styles from './AhtorLink.module.scss'

View File

@ -4,7 +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 { Author, FollowingEntity } from '../../../graphql/types.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'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
@ -67,7 +67,7 @@ export const AuthorBadge = (props: Props) => {
hasLink={true} hasLink={true}
size={'M'} size={'M'}
name={props.author.name} name={props.author.name}
userpic={props.author.userpic} userpic={props.author.pic}
slug={props.author.slug} slug={props.author.slug}
/> />
<a href={`/author/${props.author.slug}`} class={styles.info}> <a href={`/author/${props.author.slug}`} class={styles.info}>
@ -78,7 +78,9 @@ export const AuthorBadge = (props: Props) => {
<Switch <Switch
fallback={ fallback={
<div class={styles.bio}> <div class={styles.bio}>
{t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })} {t('Registered since {date}', {
date: formatDate(new Date(props.author.created_at * 1000)),
})}
</div> </div>
} }
> >

View File

@ -1,4 +1,4 @@
import type { Author } from '../../../graphql/types.gen' import type { Author } from '../../../graphql/schema/core.gen'
import { openPage, redirectPage } from '@nanostores/router' import { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -6,7 +6,7 @@ import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/types.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
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'

View File

@ -1,4 +1,4 @@
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -53,7 +53,7 @@ export const Draft = (props: Props) => {
<div class={clsx(props.class)}> <div class={clsx(props.class)}>
<div class={styles.created}> <div class={styles.created}>
<Icon name="pencil-outline" class={styles.icon} />{' '} <Icon name="pencil-outline" class={styles.icon} />{' '}
{formatDate(new Date(props.shout.createdAt), { hour: '2-digit', minute: '2-digit' })} {formatDate(new Date(props.shout.created_at * 1000), { hour: '2-digit', minute: '2-digit' })}
</div> </div>
<div class={styles.titleContainer}> <div class={styles.titleContainer}>
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle} <span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}

View File

@ -1,4 +1,4 @@
import type { Topic } from '../../../graphql/types.gen' import type { Topic } from '../../../graphql/schema/core.gen'
import { createOptions, Select } from '@thisbeyond/solid-select' import { createOptions, Select } from '@thisbeyond/solid-select'
import { clsx } from 'clsx' import { clsx } from 'clsx'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../../graphql/types.gen' import type { Shout } from '../../../graphql/schema/core.gen'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -86,12 +86,10 @@ const getTitleAndSubtitle = (
export const ArticleCard = (props: ArticleCardProps) => { export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize() const { t, lang, formatDate } = useLocalize()
const { user } = useSession() const { user } = useSession()
const mainTopic = const mainTopic = props.article.topics[0]
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
props.article.topics[0]
const formattedDate = createMemo<string>(() => { const formattedDate = createMemo<string>(() => {
return formatDate(new Date(props.article.createdAt)) return formatDate(new Date(props.article.created_at * 1000))
}) })
const { title, subtitle } = getTitleAndSubtitle(props.article) const { title, subtitle } = getTitleAndSubtitle(props.article)

View File

@ -1,6 +1,6 @@
// TODO: additional entities list column + article // TODO: additional entities list column + article
import type { Author, Shout, Topic, User } from '../../graphql/types.gen' import type { Author, Shout, Topic, User } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { Show } from 'solid-js' import { Show } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { createComputed, createSignal, Show, For } from 'solid-js' import { createComputed, createSignal, Show, For } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { For } from 'solid-js' import { For } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/schema/core.gen'
import { createSignal, For, createEffect } from 'solid-js' import { createSignal, For, createEffect } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { ChatMember } from '../../graphql/types.gen' import type { ChatMember } from '../../graphql/schema/core.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'

View File

@ -1,4 +1,4 @@
import type { Chat } from '../../graphql/types.gen' import type { Chat } from '../../graphql/schema/core.gen'
import DialogCard from './DialogCard' import DialogCard from './DialogCard'

View File

@ -1,4 +1,4 @@
import type { ChatMember } from '../../graphql/types.gen' import type { ChatMember } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For } from 'solid-js' import { For } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/schema/core.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'

View File

@ -1,4 +1,4 @@
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen' import type { Message as MessageType, ChatMember } from '../../graphql/schema/chat.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'

View File

@ -5,9 +5,9 @@ import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { ApiError } from '../../../graphql/error'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'

View File

@ -4,9 +4,9 @@ import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js' import { createSignal, JSX, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { ApiError } from '../../../graphql/error'
import { signSendLink } from '../../../stores/auth' import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'

View File

@ -6,10 +6,10 @@ import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { ApiError } from '../../../graphql/error'
import { signSendLink } from '../../../stores/auth' import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'

View File

@ -5,11 +5,11 @@ import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { ApiError } from '../../../graphql/error'
import { register } from '../../../stores/auth' import { register } from '../../../stores/auth'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'

View File

@ -1,4 +1,4 @@
import type { Topic } from '../../../graphql/types.gen' import type { Topic } from '../../../graphql/schema/core.gen'
import { getPagePath, redirectPage } from '@nanostores/router' import { getPagePath, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -6,9 +6,9 @@ import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
import { router, ROUTES, useRouter } from '../../../stores/router' import { router, ROUTES, useRouter } from '../../../stores/router'
import { useModalStore } from '../../../stores/ui' import { useModalStore } from '../../../stores/ui'
import { apiClient } from '../../../utils/apiClient'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Subscribe } from '../../_shared/Subscribe' import { Subscribe } from '../../_shared/Subscribe'

View File

@ -1,4 +1,3 @@
import type { Notification } from '../../../graphql/types.gen'
import type { ArticlePageSearchParams } from '../../Article/FullArticle' import type { ArticlePageSearchParams } from '../../Article/FullArticle'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
@ -7,12 +6,13 @@ import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useNotifications } from '../../../context/notifications' import { useNotifications } from '../../../context/notifications'
import { NotificationType } from '../../../graphql/types.gen' import { Notification } from '../../../graphql/schema/notifier.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { GroupAvatar } from '../../_shared/GroupAvatar' import { GroupAvatar } from '../../_shared/GroupAvatar'
import { TimeAgo } from '../../_shared/TimeAgo' import { TimeAgo } from '../../_shared/TimeAgo'
import styles from './NotificationView.module.scss' import styles from './NotificationView.module.scss'
import { apiClient } from '../../../graphql/client/core'
import { Reaction, Shout } from '../../../graphql/schema/core.gen'
type Props = { type Props = {
notification: Notification notification: Notification
@ -21,22 +21,6 @@ type Props = {
class?: string class?: string
} }
export type NotificationUser = {
id: number
name: string
slug: string
userpic: string
}
type NotificationData = {
shout: {
slug: string
title: string
}
users: NotificationUser[]
reactionIds: number[]
}
export const NotificationView = (props: Props) => { export const NotificationView = (props: Props) => {
const { const {
actions: { markNotificationAsRead, hideNotificationsPanel }, actions: { markNotificationAsRead, hideNotificationsPanel },
@ -46,19 +30,13 @@ export const NotificationView = (props: Props) => {
const { t, formatDate, formatTime } = useLocalize() const { t, formatDate, formatTime } = useLocalize()
const [data, setData] = createSignal<NotificationData>(null) const [data, setData] = createSignal<Reaction>(null) // NOTE: supports only SSMessage.entity == "reaction"
onMount(() => { onMount(() => {
setTimeout(() => setData(JSON.parse(props.notification.data))) setTimeout(() => setData(JSON.parse(props.notification.payload)))
}) })
const lastUser = createMemo(() => { const lastUser = createMemo(() => data().created_by)
if (!data()) {
return null
}
return data().users[data().users.length - 1]
})
const handleLinkClick = (event: MouseEvent) => { const handleLinkClick = (event: MouseEvent) => {
event.stopPropagation() event.stopPropagation()
@ -87,33 +65,18 @@ export const NotificationView = (props: Props) => {
} }
} }
switch (props.notification.type) { switch (props.notification.action) {
case NotificationType.NewComment: { case 'create': {
return ( if (data()?.reply_to) {
<>
{t('NotificationNewCommentText1', {
commentsCount: props.notification.occurrences,
})}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle}
</a>{' '}
{t('NotificationNewCommentText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{t('NotificationNewCommentText3', {
restUsersCount: data().users.length - 1,
})}
</>
)
}
case NotificationType.NewReply: {
return ( return (
<> <>
{t('NotificationNewReplyText1', { {t('NotificationNewReplyText1', {
commentsCount: props.notification.occurrences, commentsCount: 0, // FIXME: props.notification.occurrences,
})}{' '} })}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}> <a
href={getPagePath(router, 'article', { slug: data().shout.slug })}
onClick={handleLinkClick}
>
{shoutTitle} {shoutTitle}
</a>{' '} </a>{' '}
{t('NotificationNewReplyText2')}{' '} {t('NotificationNewReplyText2')}{' '}
@ -121,12 +84,47 @@ export const NotificationView = (props: Props) => {
{lastUser().name} {lastUser().name}
</a>{' '} </a>{' '}
{t('NotificationNewReplyText3', { {t('NotificationNewReplyText3', {
restUsersCount: data().users.length - 1, restUsersCount: 0, // FIXME: data().users.length - 1,
})}
</>
)
} else {
return (
<>
{t('NotificationNewCommentText1', {
commentsCount: 0, // FIXME: props.notification.occurrences,
})}{' '}
<a
href={getPagePath(router, 'article', { slug: data().shout.slug })}
onClick={handleLinkClick}
>
{shoutTitle}
</a>{' '}
{t('NotificationNewCommentText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{t('NotificationNewCommentText3', {
restUsersCount: 0, // FIXME: data().users.length - 1,
})} })}
</> </>
) )
} }
} }
case 'update': {
}
case 'delete': {
}
case 'follow': {
}
case 'unfollow': {
}
case 'invited': {
// TODO: invited for collaborative authoring
}
default:
return <></>
}
}) })
const handleClick = () => { const handleClick = () => {
@ -137,22 +135,22 @@ export const NotificationView = (props: Props) => {
} }
openPage(router, 'article', { slug: data().shout.slug }) openPage(router, 'article', { slug: data().shout.slug })
// FIXME:
if (data().reactionIds) { // if (data().reactionIds) {
changeSearchParam({ commentId: data().reactionIds[0].toString() }) // changeSearchParam({ commentId: data().reactionIds[0].toString() })
} // }
} }
const formattedDateTime = createMemo(() => { const formattedDateTime = createMemo(() => {
switch (props.dateTimeFormat) { switch (props.dateTimeFormat) {
case 'ago': { case 'ago': {
return <TimeAgo date={props.notification.createdAt} /> return <TimeAgo date={props.notification.created_at} />
} }
case 'time': { case 'time': {
return formatTime(new Date(props.notification.createdAt)) return formatTime(new Date(props.notification.created_at))
} }
case 'date': { case 'date': {
return formatDate(new Date(props.notification.createdAt), { month: 'numeric', year: '2-digit' }) return formatDate(new Date(props.notification.created_at), { month: 'numeric', year: '2-digit' })
} }
} }
}) })
@ -166,7 +164,7 @@ export const NotificationView = (props: Props) => {
onClick={handleClick} onClick={handleClick}
> >
<div class={styles.userpic}> <div class={styles.userpic}>
<GroupAvatar authors={data().users} /> <GroupAvatar authors={[] /*d FIXME: data().users */} />
</div> </div>
<div>{content()}</div> <div>{content()}</div>
<div class={styles.timeContainer}>{formattedDateTime()}</div> <div class={styles.timeContainer}>{formattedDateTime()}</div>

View File

@ -95,15 +95,19 @@ export const NotificationsPanel = (props: Props) => {
} }
const todayNotifications = createMemo(() => { const todayNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt))) return sortedNotifications().filter((notification) => isToday(new Date(notification.created_at * 1000)))
}) })
const yesterdayNotifications = createMemo(() => { const yesterdayNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt))) return sortedNotifications().filter((notification) =>
isYesterday(new Date(notification.created_at * 1000)),
)
}) })
const earlierNotifications = createMemo(() => { const earlierNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt))) return sortedNotifications().filter((notification) =>
isEarlier(new Date(notification.created_at * 1000)),
)
}) })
const scrollContainerRef: { current: HTMLDivElement } = { current: null } const scrollContainerRef: { current: HTMLDivElement } = { current: null }

View File

@ -1,11 +1,11 @@
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/schema/core.gen'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'

View File

@ -1,4 +1,4 @@
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/schema/core.gen'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'

View File

@ -1,11 +1,11 @@
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js' import { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/schema/core.gen'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'

View File

@ -3,7 +3,7 @@ import { createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/types.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'

View File

@ -1,4 +1,4 @@
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'

View File

@ -1,14 +1,14 @@
import type { Author, Shout, Topic } from '../../../graphql/types.gen' import type { Author, Shout, Topic } from '../../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js' import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { apiClient } from '../../../graphql/client/core'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
import { useAuthorsStore } from '../../../stores/zine/authors' import { useAuthorsStore } from '../../../stores/zine/authors'
import { apiClient } from '../../../utils/apiClient'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { splitToPages } from '../../../utils/splitToPages' import { splitToPages } from '../../../utils/splitToPages'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
@ -118,7 +118,7 @@ export const AuthorView = (props: Props) => {
if (getPage().route === 'authorComments') { if (getPage().route === 'authorComments') {
try { try {
const data = await apiClient.getReactionsBy({ const data = await apiClient.getReactionsBy({
by: { comment: true, createdBy: props.authorSlug }, by: { comment: true, created_by: props.author.id },
}) })
setCommented(data) setCommented(data)
} catch (error) { } catch (error) {

View File

@ -4,9 +4,9 @@ import { createSignal, For, onMount, Show } from 'solid-js'
import { useEditorContext } from '../../../context/editor' import { useEditorContext } from '../../../context/editor'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Shout } from '../../../graphql/types.gen' import { apiClient } from '../../../graphql/client/core'
import { Shout } from '../../../graphql/schema/core.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { apiClient } from '../../../utils/apiClient'
import { Draft } from '../../Draft' import { Draft } from '../../Draft'
import styles from './DraftsView.module.scss' import styles from './DraftsView.module.scss'

View File

@ -1,5 +1,3 @@
import type { Shout, Topic } from '../../graphql/types.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show } from 'solid-js' import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
@ -7,6 +5,7 @@ import { createStore } from 'solid-js/store'
import { ShoutForm, useEditorContext } from '../../context/editor' import { ShoutForm, useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { ShoutVisibility, type Shout, type Topic } from '../../graphql/schema/core.gen'
import { LayoutType, MediaItem } from '../../pages/types' import { LayoutType, MediaItem } from '../../pages/types'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { clone } from '../../utils/clone' import { clone } from '../../utils/clone'
@ -75,7 +74,7 @@ export const EditView = (props: Props) => {
description: props.shout.description, description: props.shout.description,
subtitle: props.shout.subtitle, subtitle: props.shout.subtitle,
selectedTopics: shoutTopics, selectedTopics: shoutTopics,
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC, mainTopic: shoutTopics[0],
body: props.shout.body, body: props.shout.body,
coverImageUrl: props.shout.cover, coverImageUrl: props.shout.cover,
media: props.shout.media, media: props.shout.media,
@ -163,7 +162,7 @@ export const EditView = (props: Props) => {
const articleTitle = () => { const articleTitle = () => {
switch (props.shout.layout as LayoutType) { switch (props.shout.layout as LayoutType) {
case 'music': { case 'audio': {
return t('Album name') return t('Album name')
} }
case 'image': { case 'image': {
@ -182,7 +181,7 @@ export const EditView = (props: Props) => {
const hasChanges = !deepEqual(form, prevForm) const hasChanges = !deepEqual(form, prevForm)
if (hasChanges) { if (hasChanges) {
setSaving(true) setSaving(true)
if (props.shout.visibility === 'owner') { if (props.shout.visibility === ShoutVisibility.Authors) {
await saveDraft(form) await saveDraft(form)
} else { } else {
saveDraftToLocalStorage(form) saveDraftToLocalStorage(form)
@ -243,19 +242,19 @@ export const EditView = (props: Props) => {
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5"> <div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={page().route === 'edit'}> <Show when={page().route === 'edit'}>
<div class={styles.headingActions}> <div class={styles.headingActions}>
<Show when={!isSubtitleVisible() && props.shout.layout !== 'music'}> <Show when={!isSubtitleVisible() && props.shout.layout !== 'audio'}>
<div class={styles.action} onClick={showSubtitleInput}> <div class={styles.action} onClick={showSubtitleInput}>
{t('Add subtitle')} {t('Add subtitle')}
</div> </div>
</Show> </Show>
<Show when={!isLeadVisible() && props.shout.layout !== 'music'}> <Show when={!isLeadVisible() && props.shout.layout !== 'audio'}>
<div class={styles.action} onClick={showLeadInput}> <div class={styles.action} onClick={showLeadInput}>
{t('Add intro')} {t('Add intro')}
</div> </div>
</Show> </Show>
</div> </div>
<> <>
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'music' })}> <div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}>
<div class={styles.inputContainer}> <div class={styles.inputContainer}>
<GrowingTextarea <GrowingTextarea
allowEnterKey={true} allowEnterKey={true}
@ -270,7 +269,7 @@ export const EditView = (props: Props) => {
<div class={styles.validationError}>{formErrors.title}</div> <div class={styles.validationError}>{formErrors.title}</div>
</Show> </Show>
<Show when={props.shout.layout === 'music'}> <Show when={props.shout.layout === 'audio'}>
<div class={styles.additional}> <div class={styles.additional}>
<input <input
type="text" type="text"
@ -298,7 +297,7 @@ export const EditView = (props: Props) => {
/> />
</div> </div>
</Show> </Show>
<Show when={props.shout.layout !== 'music'}> <Show when={props.shout.layout !== 'audio'}>
<Show when={isSubtitleVisible()}> <Show when={isSubtitleVisible()}>
<GrowingTextarea <GrowingTextarea
textAreaRef={(el) => { textAreaRef={(el) => {
@ -324,7 +323,7 @@ export const EditView = (props: Props) => {
</Show> </Show>
</Show> </Show>
</div> </div>
<Show when={props.shout.layout === 'music'}> <Show when={props.shout.layout === 'audio'}>
<Show <Show
when={form.coverImageUrl} when={form.coverImageUrl}
fallback={ fallback={
@ -387,7 +386,7 @@ export const EditView = (props: Props) => {
/> />
</Show> </Show>
<Show when={props.shout.layout === 'music'}> <Show when={props.shout.layout === 'audio'}>
<AudioUploader <AudioUploader
audio={mediaItems()} audio={mediaItems()}
baseFields={baseAudioFields()} baseFields={baseAudioFields()}

View File

@ -3,7 +3,7 @@ import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { LoadShoutsOptions, Shout } from '../../../graphql/types.gen' import { LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen'
import { LayoutType } from '../../../pages/types' import { LayoutType } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
@ -39,7 +39,9 @@ export const Expo = (props: Props) => {
offset: sortedArticles().length, offset: sortedArticles().length,
} }
options.filters = getLayout() ? { layout: getLayout() } : { exclude_layout: 'article' } options.filters = getLayout()
? { layouts: [getLayout()] }
: { layouts: ['audio', 'video', 'image', 'literature'] }
const { hasMore } = await loadShouts(options) const { hasMore } = await loadShouts(options)
setIsLoadMoreButtonVisible(hasMore) setIsLoadMoreButtonVisible(hasMore)
@ -107,11 +109,11 @@ export const Expo = (props: Props) => {
<span class={clsx('linkReplacement')}>{t('Literature')}</span> <span class={clsx('linkReplacement')}>{t('Literature')}</span>
</ConditionalWrapper> </ConditionalWrapper>
</li> </li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'music' })}> <li class={clsx({ 'view-switcher__item--selected': getLayout() === 'audio' })}>
<ConditionalWrapper <ConditionalWrapper
condition={getLayout() !== 'music'} condition={getLayout() !== 'audio'}
wrapper={(children) => ( wrapper={(children) => (
<a href={getPagePath(router, 'expoLayout', { layout: 'music' })}>{children}</a> <a href={getPagePath(router, 'expoLayout', { layout: 'audio' })}>{children}</a>
)} )}
> >
<span class={clsx('linkReplacement')}>{t('Music')}</span> <span class={clsx('linkReplacement')}>{t('Music')}</span>

View File

@ -1,4 +1,4 @@
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/types.gen' import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -209,7 +209,7 @@ export const FeedView = (props: Props) => {
/> />
</div> </div>
<div class={styles.commentDetails}> <div class={styles.commentDetails}>
<AuthorLink author={comment.createdBy as Author} size={'XS'} /> <AuthorLink author={comment.created_by as Author} size={'XS'} />
<CommentDate comment={comment} isShort={true} isLastInRow={true} /> <CommentDate comment={comment} isShort={true} isLastInRow={true} />
</div> </div>
<div class={clsx('text-truncate', styles.commentArticleTitle)}> <div class={clsx('text-truncate', styles.commentArticleTitle)}>

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { createMemo, createSignal, For, onMount, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'

View File

@ -1,27 +1,28 @@
import type { Chat, Message as MessageType } from '../../graphql/schema/chat.gen'
import type { Author } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx'
import { For, createSignal, Show, onMount, createEffect, createMemo, on } from 'solid-js' import { For, createSignal, Show, onMount, createEffect, createMemo, on } from 'solid-js'
import type { Author, Chat, Message as MessageType } from '../../graphql/types.gen'
import DialogCard from '../Inbox/DialogCard' import { useInbox } from '../../context/inbox'
import Search from '../Inbox/Search' import { useLocalize } from '../../context/localize'
import { Message } from '../Inbox/Message'
import CreateModalContent from '../Inbox/CreateModalContent'
import DialogHeader from '../Inbox/DialogHeader'
import MessagesFallback from '../Inbox/MessagesFallback'
import QuotedMessage from '../Inbox/QuotedMessage'
import { Icon } from '../_shared/Icon'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { loadRecipients } from '../../stores/inbox' import { loadRecipients } from '../../stores/inbox'
import { Modal } from '../Nav/Modal'
import { showModal } from '../../stores/ui'
import { useInbox } from '../../context/inbox'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { clsx } from 'clsx' import { showModal } from '../../stores/ui'
import styles from '../../styles/Inbox.module.scss' import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize'
import SimplifiedEditor from '../Editor/SimplifiedEditor'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import SimplifiedEditor from '../Editor/SimplifiedEditor'
import CreateModalContent from '../Inbox/CreateModalContent'
import DialogCard from '../Inbox/DialogCard'
import DialogHeader from '../Inbox/DialogHeader'
import { Message } from '../Inbox/Message'
import MessagesFallback from '../Inbox/MessagesFallback'
import QuotedMessage from '../Inbox/QuotedMessage'
import Search from '../Inbox/Search'
import { Modal } from '../Nav/Modal'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) import styles from '../../styles/Inbox.module.scss'
type InboxSearchParams = { type InboxSearchParams = {
initChat: string initChat: string
@ -41,7 +42,7 @@ export const InboxView = () => {
const { const {
chats, chats,
messages, messages,
actions: { loadChats, getMessages, sendMessage, createChat } actions: { loadChats, getMessages, sendMessage, createChat },
} = useInbox() } = useInbox()
const [recipients, setRecipients] = createSignal<Author[]>([]) const [recipients, setRecipients] = createSignal<Author[]>([])
@ -51,12 +52,12 @@ export const InboxView = () => {
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null) const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
const [isClear, setClear] = createSignal(false) const [isClear, setClear] = createSignal(false)
const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false) const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false)
const { session } = useSession() const { author } = useSession()
const currentUserId = createMemo(() => session()?.user.id) const currentUserId = createMemo(() => author().id)
const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>() const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>()
const messagesContainerRef: { current: HTMLDivElement } = { const messagesContainerRef: { current: HTMLDivElement } = {
current: null current: null,
} }
// Поиск по диалогам // Поиск по диалогам
@ -72,7 +73,7 @@ export const InboxView = () => {
const handleOpenChat = async (chat: Chat) => { const handleOpenChat = async (chat: Chat) => {
setCurrentDialog(chat) setCurrentDialog(chat)
changeSearchParam({ changeSearchParam({
chat: chat.id chat: chat.id,
}) })
try { try {
await getMessages(chat.id) await getMessages(chat.id)
@ -81,14 +82,14 @@ export const InboxView = () => {
} finally { } finally {
messagesContainerRef.current.scroll({ messagesContainerRef.current.scroll({
top: messagesContainerRef.current.scrollHeight, top: messagesContainerRef.current.scrollHeight,
behavior: 'instant' behavior: 'instant',
}) })
} }
} }
onMount(async () => { onMount(async () => {
try { try {
const response = await loadRecipients({ days: 365 }) const response = await loadRecipients() // time ago in seconds
setRecipients(response as unknown as Author[]) setRecipients(response as unknown as Author[])
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@ -100,7 +101,7 @@ export const InboxView = () => {
await sendMessage({ await sendMessage({
body: message, body: message,
chat_id: currentDialog().id.toString(), chat_id: currentDialog().id.toString(),
reply_to: messageToReply()?.id reply_to: messageToReply()?.id,
}) })
setClear(true) setClear(true)
setMessageToReply(null) setMessageToReply(null)
@ -121,7 +122,7 @@ export const InboxView = () => {
await loadChats() await loadChats()
changeSearchParam({ changeSearchParam({
initChat: null, initChat: null,
chat: newChat.chat.id chat: newChat.chat.id,
}) })
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id) const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
await handleOpenChat(chatToOpen) await handleOpenChat(chatToOpen)
@ -160,11 +161,11 @@ export const InboxView = () => {
} }
messagesContainerRef.current.scroll({ messagesContainerRef.current.scroll({
top: messagesContainerRef.current.scrollHeight, top: messagesContainerRef.current.scrollHeight,
behavior: 'smooth' behavior: 'smooth',
}) })
} },
), ),
{ defer: true } { defer: true },
) )
const handleScrollMessageContainer = () => { const handleScrollMessageContainer = () => {
if ( if (
@ -179,7 +180,7 @@ export const InboxView = () => {
const handleScrollToNew = () => { const handleScrollToNew = () => {
messagesContainerRef.current.scroll({ messagesContainerRef.current.scroll({
top: messagesContainerRef.current.scrollHeight, top: messagesContainerRef.current.scrollHeight,
behavior: 'smooth' behavior: 'smooth',
}) })
setIsScrollToNewVisible(false) setIsScrollToNewVisible(false)
} }

View File

@ -3,9 +3,9 @@ import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Author, Topic } from '../../../graphql/types.gen' import { apiClient } from '../../../graphql/client/core'
import { Author, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { apiClient } from '../../../utils/apiClient'
import { dummyFilter } from '../../../utils/dummyFilter' import { dummyFilter } from '../../../utils/dummyFilter'
// TODO: refactor styles // TODO: refactor styles
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
@ -20,7 +20,7 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
export const ProfileSubscriptions = () => { export const ProfileSubscriptions = () => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { user } = useSession() const { session } = useSession()
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([]) const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
@ -29,8 +29,8 @@ export const ProfileSubscriptions = () => {
const fetchSubscriptions = async () => { const fetchSubscriptions = async () => {
try { try {
const [getAuthors, getTopics] = await Promise.all([ const [getAuthors, getTopics] = await Promise.all([
apiClient.getAuthorFollowingUsers({ slug: user().slug }), apiClient.getAuthorFollowingUsers({ slug: session()?.author.slug }),
apiClient.getAuthorFollowingTopics({ slug: user().slug }), apiClient.getAuthorFollowingTopics({ slug: session()?.author.slug }),
]) ])
setFollowing([...getAuthors, ...getTopics]) setFollowing([...getAuthors, ...getTopics])
setFiltered([...getAuthors, ...getTopics]) setFiltered([...getAuthors, ...getTopics])

View File

@ -6,11 +6,11 @@ import { createStore } from 'solid-js/store'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Topic } from '../../../graphql/types.gen' import { apiClient } from '../../../graphql/client/core'
import { Topic } from '../../../graphql/schema/core.gen'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { hideModal, showModal } from '../../../stores/ui' import { hideModal, showModal } from '../../../stores/ui'
import { apiClient } from '../../../utils/apiClient'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
@ -36,9 +36,9 @@ const shorten = (str: string, maxLen: number) => {
return `${result}...` return `${result}...`
} }
export const PublishSettings = (props: Props) => { export const PublishSettings = async (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const { session, author } = useSession()
const composeDescription = () => { const composeDescription = () => {
if (!props.form.description) { if (!props.form.description) {
@ -154,7 +154,7 @@ export const PublishSettings = (props: Props) => {
</Show> </Show>
<div class={styles.shoutCardTitle}>{settingsForm.title}</div> <div class={styles.shoutCardTitle}>{settingsForm.title}</div>
<div class={styles.shoutCardSubtitle}>{settingsForm.subtitle}</div> <div class={styles.shoutCardSubtitle}>{settingsForm.subtitle}</div>
<div class={styles.shoutAuthor}>{user().name}</div> <div class={styles.shoutAuthor}>{author()?.name}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { Show, For, createSignal } from 'solid-js' import { Show, For, createSignal } from 'solid-js'

View File

@ -1,4 +1,4 @@
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createMemo, onMount, createSignal } from 'solid-js' import { For, Show, createMemo, onMount, createSignal } from 'solid-js'

View File

@ -2,7 +2,7 @@ import { clsx } from 'clsx'
import { For, onMount, Show } from 'solid-js' import { For, onMount, Show } from 'solid-js'
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper' import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
import { Shout } from '../../../graphql/types.gen' import { Shout } from '../../../graphql/schema/core.gen'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import { Icon } from '../Icon' import { Icon } from '../Icon'

View File

@ -1,9 +1,7 @@
import type { Reaction } from '../../../graphql/types.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import { ReactionKind } from '../../../graphql/types.gen' import { Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import styles from './VotersList.module.scss' import styles from './VotersList.module.scss'
@ -26,11 +24,11 @@ export const VotersList = (props: Props) => {
<li class={styles.item}> <li class={styles.item}>
<div class={styles.user}> <div class={styles.user}>
<Userpic <Userpic
name={reaction.createdBy.name} name={reaction.created_by.name}
userpic={reaction.createdBy.userpic} userpic={reaction.created_by.pic}
class={styles.userpic} class={styles.userpic}
/> />
<a href={`/author/${reaction.createdBy.slug}`}>{reaction.createdBy.name || ''}</a> <a href={`/author/${reaction.created_by.slug}`}>{reaction.created_by.name || ''}</a>
</div> </div>
{reaction.kind === ReactionKind.Like ? ( {reaction.kind === ReactionKind.Like ? (
<div class={styles.commentRatingPositive}>+1</div> <div class={styles.commentRatingPositive}>+1</div>

242
src/context/authorizer.tsx Normal file
View File

@ -0,0 +1,242 @@
import { createContext, createEffect, createMemo, onCleanup, onMount, useContext } from 'solid-js'
import type { ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'
import { Authorizer, User, AuthToken } from '@authorizerdev/authorizer-js'
export enum AuthorizerProviderActionType {
SET_USER = 'SET_USER',
SET_TOKEN = 'SET_TOKEN',
SET_LOADING = 'SET_LOADING',
SET_AUTH_DATA = 'SET_AUTH_DATA',
SET_CONFIG = 'SET_CONFIG',
}
export type AuthorizerState = {
user: User | null
token: AuthToken | null
loading: boolean
config: {
authorizerURL: string
redirectURL: string
client_id: string
is_google_login_enabled: boolean
is_github_login_enabled: boolean
is_facebook_login_enabled: boolean
is_linkedin_login_enabled: boolean
is_apple_login_enabled: boolean
is_twitter_login_enabled: boolean
is_microsoft_login_enabled: boolean
is_email_verification_enabled: boolean
is_basic_authentication_enabled: boolean
is_magic_link_login_enabled: boolean
is_sign_up_enabled: boolean
is_strong_password_enabled: boolean
}
}
export type AuthorizerProviderAction = {
type: AuthorizerProviderActionType
payload: any
}
export type OtpDataType = {
isScreenVisible: boolean
email: string
}
type AuthorizerContextActions = {
setLoading: (loading: boolean) => void
setToken: (token: AuthToken | null) => void
setUser: (user: User | null) => void
setAuthData: (data: AuthorizerState) => void
authorizer: () => Authorizer
logout: () => Promise<void>
}
// TODO: fix types
const AuthorizerContext = createContext<[AuthorizerState, AuthorizerContextActions]>([
{
config: {
authorizerURL: '',
redirectURL: '/',
client_id: '',
is_google_login_enabled: true,
is_github_login_enabled: true,
is_facebook_login_enabled: true,
is_linkedin_login_enabled: false,
is_apple_login_enabled: false,
is_twitter_login_enabled: true,
is_microsoft_login_enabled: false,
is_email_verification_enabled: true,
is_basic_authentication_enabled: true,
is_magic_link_login_enabled: true,
is_sign_up_enabled: true,
is_strong_password_enabled: true,
},
user: null,
token: null,
loading: false,
},
{
setLoading: () => {},
setToken: () => {},
setUser: () => {},
setAuthData: () => {},
authorizer: () =>
new Authorizer({
authorizerURL: 'http://auth.discours.io',
redirectURL: 'http://auth.discours.io',
clientID: '', // FIXME: add real client id
}),
logout: async () => {},
},
])
type AuthorizerProviderProps = {
authorizerURL: string
redirectURL: string
clientID: string
onStateChangeCallback?: (stateData: AuthorizerState) => void
}
export const AuthorizerProvider: ParentComponent<AuthorizerProviderProps> = (props) => {
const [state, setState] = createStore<AuthorizerState>({
user: null,
token: null,
loading: true,
config: {
authorizerURL: props.authorizerURL,
is_apple_login_enabled: false,
is_microsoft_login_enabled: false,
redirectURL: props.redirectURL,
is_google_login_enabled: true,
is_github_login_enabled: true,
is_facebook_login_enabled: true,
is_linkedin_login_enabled: false,
is_twitter_login_enabled: true,
is_email_verification_enabled: true,
is_basic_authentication_enabled: true,
is_magic_link_login_enabled: true,
is_sign_up_enabled: false,
is_strong_password_enabled: true,
client_id: props.clientID,
},
})
const authorizer = createMemo(
() =>
new Authorizer({
authorizerURL: props.authorizerURL,
redirectURL: props.redirectURL,
clientID: props.clientID,
}),
)
createEffect(() => {
if (props.onStateChangeCallback) {
props.onStateChangeCallback(state)
}
})
// Actions
const setLoading = (loading: boolean) => {
setState('loading', loading)
}
const handleTokenChange = (token: AuthToken | null) => {
setState('token', token)
// If we have an access_token, then we clear the interval and create a new interval
// to the token expires_in, so we can retrieve the token again before it expires
if (token?.access_token) {
if (interval) {
clearInterval(interval)
}
interval = setInterval(() => {
getToken()
}, token.expires_in * 1000) as any
}
}
const setUser = (user: User | null) => {
setState('user', user)
}
const setAuthData = (data: AuthorizerState) => {
setState(data)
}
const logout = async () => {
setState('loading', true)
setState('user', null)
}
let interval: number | null = null
const getToken = async () => {
setState('loading', true)
const metaRes = await authorizer().getMetaData()
try {
const res = await authorizer().getSession()
if (res.access_token && res.user) {
setState((prev) => ({
...prev,
token: {
access_token: res.access_token,
expires_in: res.expires_in,
id_token: res.id_token,
refresh_token: res.refresh_token || '',
},
user: res.user,
}))
if (interval) {
clearInterval(interval)
}
interval = setInterval(() => {
getToken()
}, res.expires_in * 1000) as any
} else {
setState((prev) => ({ ...prev, user: null, token: null }))
}
} catch (e) {
setState((prev) => ({ ...prev, user: null, token: null }))
} finally {
setState('config', (config) => ({ ...config, ...metaRes }))
setState('loading', false)
}
}
onMount(() => {
!state.token && getToken()
})
onCleanup(() => {
if (interval) {
clearInterval(interval)
}
})
return (
<AuthorizerContext.Provider
value={[
state,
{
setUser,
setLoading,
setToken: handleTokenChange,
setAuthData,
authorizer,
logout,
},
]}
>
{props.children}
</AuthorizerContext.Provider>
)
}
export const useAuthorizer = () => useContext(AuthorizerContext)

76
src/context/connect.tsx Normal file
View File

@ -0,0 +1,76 @@
import type { Accessor, JSX } from 'solid-js'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { createContext, useContext, createSignal, createEffect } from 'solid-js'
import { getToken } from '../graphql/privateGraphQLClient'
import { useSession } from './session'
export interface SSEMessage {
id: string
entity: string
action: string
payload: any // Author | Shout | Reaction | Message
created_at?: number // unixtime x1000
seen?: boolean
}
type MessageHandler = (m: SSEMessage) => void
export interface ConnectContextType {
addHandler: (handler: MessageHandler) => void
connected: Accessor<boolean>
}
const ConnectContext = createContext<ConnectContextType>()
export const ConnectProvider = (props: { children: JSX.Element }) => {
const [messageHandlers, setHandlers] = createSignal<Array<MessageHandler>>([])
// const [messages, setMessages] = createSignal<Array<SSEMessage>>([]);
const [connected, setConnected] = createSignal(false)
const { isAuthenticated, author } = useSession()
const addHandler = (handler: MessageHandler) => {
setHandlers((hhh) => [...hhh, handler])
}
const listen = () => {
const token = getToken()
if (token) {
fetchEventSource('https://connect.discours.io', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
onmessage(event) {
const m: SSEMessage = JSON.parse(event.data)
console.log('[context.connect] Received message:', m)
// Iterate over all registered handlers and call them
messageHandlers().forEach((handler) => handler(m))
},
onclose() {
console.log('[context.connect] sse connection closed by server')
},
onerror(err) {
console.error('[context.connect] sse connection closed by error', err)
throw new Error(err) // NOTE: simple hack to close the connection
},
})
}
}
createEffect(() => {
if (isAuthenticated() && !connected()) {
listen()
setConnected(true)
}
})
const value: ConnectContextType = { addHandler, connected }
return <ConnectContext.Provider value={value}>{props.children}</ConnectContext.Provider>
}
export const useConnect = () => useContext(ConnectContext)

View File

@ -5,9 +5,9 @@ import { Editor } from '@tiptap/core'
import { Accessor, createContext, 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 { Topic, TopicInput } from '../graphql/types.gen' import { apiClient } from '../graphql/client/core'
import { ShoutVisibility, Topic, TopicInput } from '../graphql/schema/core.gen'
import { router, useRouter } from '../stores/router' import { router, useRouter } from '../stores/router'
import { apiClient } from '../utils/apiClient'
import { slugify } from '../utils/slugify' import { slugify } from '../utils/slugify'
import { useLocalize } from './localize' import { useLocalize } from './localize'
@ -130,10 +130,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
shoutId: formToUpdate.shoutId, shoutId: formToUpdate.shoutId,
shoutInput: { shoutInput: {
body: formToUpdate.body, body: formToUpdate.body,
topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)), topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)), // NOTE: first is main
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>> // authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
// community?: InputMaybe<Scalars['Int']> // community?: InputMaybe<Scalars['Int']>
mainTopic: topic2topicInput(formToUpdate.mainTopic), // mainTopic: topic2topicInput(formToUpdate.mainTopic),
slug: formToUpdate.slug, slug: formToUpdate.slug,
subtitle: formToUpdate.subtitle, subtitle: formToUpdate.subtitle,
title: formToUpdate.title, title: formToUpdate.title,
@ -163,7 +163,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const shout = await updateShout(formToSave, { publish: false }) const shout = await updateShout(formToSave, { publish: false })
removeDraftFromLocalStorage(formToSave.shoutId) removeDraftFromLocalStorage(formToSave.shoutId)
if (shout.visibility === 'owner') { if (shout.visibility === ShoutVisibility.Authors) {
openPage(router, 'drafts') openPage(router, 'drafts')
} else { } else {
openPage(router, 'article', { slug: shout.slug }) openPage(router, 'article', { slug: shout.slug })

View File

@ -1,19 +1,9 @@
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/schema/chat.gen'
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createEffect, createSignal, onMount, useContext } from 'solid-js' import { createContext, createSignal, useContext } from 'solid-js'
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen' import { SSEMessage, useConnect } from './connect'
import { inboxClient } from '../utils/apiClient'
import { loadMessages } from '../stores/inbox' import { loadMessages } from '../stores/inbox'
import { getToken } from '../graphql/privateGraphQLClient' import { inboxClient } from '../graphql/client/chat'
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 = { type InboxContextType = {
chats: Accessor<Chat[]> chats: Accessor<Chat[]>
@ -35,47 +25,20 @@ 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 handleMessage = (sseMessage: SSEMessage) => {
const handleMessage = (sseMessage) => {
console.log('[context.inbox]:', sseMessage) console.log('[context.inbox]:', sseMessage)
// TODO: handle all action types: create update delete join left // TODO: handle all action types: create update delete join left
if (sseMessage.entity == 'message') { if (sseMessage.entity === 'message') {
const relivedMessage = sseMessage.payload const relivedMessage = sseMessage.payload
setMessages((prev) => [...prev, relivedMessage]) setMessages((prev) => [...prev, relivedMessage])
} else if (sseMessage.entity == 'chat') { } else if (sseMessage.entity === 'chat') {
const relivedChat = sseMessage.payload const relivedChat = sseMessage.payload
setChats((prev) => [...prev, relivedChat]) setChats((prev) => [...prev, relivedChat])
} }
} }
createEffect(async () => { const { addHandler } = useConnect()
const token = getToken() addHandler(handleMessage)
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 () => { const loadChats = async () => {
try { try {
@ -103,7 +66,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
const currentChat = chats().find((chat) => chat.id === args.chat_id) const currentChat = chats().find((chat) => chat.id === args.chat_id)
setChats((prev) => [ setChats((prev) => [
...prev.filter((c) => c.id !== currentChat.id), ...prev.filter((c) => c.id !== currentChat.id),
{ ...currentChat, updatedAt: message.createdAt }, { ...currentChat, updated_at: message.created_at },
]) ])
} catch (error) { } catch (error) {
console.error('Error sending message:', error) console.error('Error sending message:', error)

View File

@ -1,17 +1,14 @@
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js' import { createContext, 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'
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
import { NotificationsPanel } from '../components/NotificationsPanel' import { NotificationsPanel } from '../components/NotificationsPanel'
import { Notification } from '../graphql/types.gen' import { notifierClient as apiClient } from '../graphql/client/notifier'
import { apiClient } from '../utils/apiClient' import { Notification } from '../graphql/schema/notifier.gen'
import { apiBaseUrl } from '../utils/config' import { SSEMessage, useConnect } from './connect'
import SSEService, { EventData } from '../utils/sseService'
import { useSession } from './session'
type NotificationsContextType = { type NotificationsContextType = {
notificationEntities: Record<number, Notification> notificationEntities: Record<number, Notification>
@ -35,53 +32,45 @@ export function useNotifications() {
return useContext(NotificationsContext) return useContext(NotificationsContext)
} }
const sseService = new SSEService()
export const NotificationsProvider = (props: { children: JSX.Element }) => { export const NotificationsProvider = (props: { children: JSX.Element }) => {
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false) const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
const { isAuthenticated, user } = useSession()
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({}) const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
const { addHandler } = useConnect()
const loadNotifications = async (options: { limit: number; offset?: number }) => { const loadNotifications = async (options: { limit: number; offset?: number }) => {
const { notifications, totalUnreadCount, totalCount } = await apiClient.getNotifications(options) const { notifications, unread, total } = await apiClient.getNotifications(options)
const newNotificationEntities = notifications.reduce((acc, notification) => { const newNotificationEntities = notifications.reduce((acc, notification) => {
acc[notification.id] = notification acc[notification.id] = notification
return acc return acc
}, {}) }, {})
setTotalNotificationsCount(totalCount) setTotalNotificationsCount(total)
setUnreadNotificationsCount(totalUnreadCount) setUnreadNotificationsCount(unread)
setNotificationEntities(newNotificationEntities) setNotificationEntities(newNotificationEntities)
return notifications return notifications
} }
const sortedNotifications = createMemo(() => { const sortedNotifications = createMemo(() => {
return Object.values(notificationEntities).sort( return Object.values(notificationEntities).sort((a, b) => b.created_at - a.created_at)
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
}) })
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length) const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
createEffect(() => {
if (isAuthenticated()) { onMount(() => {
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`) addHandler((data: SSEMessage) => {
sseService.subscribeToEvent('message', (data: EventData) => { if (data.entity === 'reaction') {
if (data.type === 'newNotifications') {
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
} else { } else {
console.error(`[NotificationsProvider] unknown message type: ${JSON.stringify(data)}`) console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`)
} }
}) })
} else {
sseService.disconnect()
}
}) })
const markNotificationAsRead = async (notification: Notification) => { const markNotificationAsRead = async (notification: Notification) => {
await apiClient.markNotificationAsRead(notification.id) await apiClient.markNotificationAsRead(notification.id)
setNotificationEntities(notification.id, 'seen', true) const nnn = new Set([...notification.seen, notification.id])
setNotificationEntities(notification.id, 'seen', Array.from(nnn))
setUnreadNotificationsCount((oldCount) => oldCount - 1) setUnreadNotificationsCount((oldCount) => oldCount - 1)
} }
const markAllNotificationsAsRead = async () => { const markAllNotificationsAsRead = async () => {

View File

@ -1,10 +1,10 @@
import type { ProfileInput } from '../graphql/types.gen' import type { ProfileInput } from '../graphql/schema/core.gen'
import { createEffect, createMemo, createSignal } from 'solid-js' import { createEffect, createMemo, createSignal } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { apiClient } from '../graphql/client/core'
import { loadAuthor, useAuthorsStore } from '../stores/zine/authors' import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
import { apiClient } from '../utils/apiClient'
import { useSession } from './session' import { useSession } from './session'
@ -16,9 +16,7 @@ const userpicUrl = (userpic: string) => {
} }
const useProfileForm = () => { const useProfileForm = () => {
const { session } = useSession() const { session } = useSession()
const currentSlug = createMemo(() => session()?.user?.slug) const currentAuthor = createMemo(() => session()?.author)
const { authorEntities } = useAuthorsStore({ authors: [] })
const currentAuthor = createMemo(() => authorEntities()[currentSlug()])
const [slugError, setSlugError] = createSignal<string>() const [slugError, setSlugError] = createSignal<string>()
const submit = async (profile: ProfileInput) => { const submit = async (profile: ProfileInput) => {
@ -35,20 +33,20 @@ const useProfileForm = () => {
bio: '', bio: '',
about: '', about: '',
slug: '', slug: '',
userpic: '', pic: '',
links: [], links: [],
}) })
createEffect(async () => { createEffect(async () => {
if (!currentSlug()) return if (!currentAuthor()) return
try { try {
await loadAuthor({ slug: currentSlug() }) await loadAuthor({ slug: currentAuthor().slug })
setForm({ setForm({
name: currentAuthor()?.name, name: currentAuthor()?.name,
slug: currentAuthor()?.slug, slug: currentAuthor()?.slug,
bio: currentAuthor()?.bio, bio: currentAuthor()?.bio,
about: currentAuthor()?.about, about: currentAuthor()?.about,
userpic: userpicUrl(currentAuthor()?.userpic), pic: userpicUrl(currentAuthor()?.pic),
links: currentAuthor()?.links, links: currentAuthor()?.links,
}) })
} catch (error) { } catch (error) {

View File

@ -1,11 +1,10 @@
import type { Reaction, ReactionBy, ReactionInput } from '../graphql/types.gen'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { createContext, onCleanup, useContext } from 'solid-js' import { createContext, onCleanup, useContext } from 'solid-js'
import { createStore, reconcile } from 'solid-js/store' import { createStore, reconcile } from 'solid-js/store'
import { ReactionKind } from '../graphql/types.gen' import { apiClient } from '../graphql/client/core'
import { apiClient } from '../utils/apiClient' import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
type ReactionsContextType = { type ReactionsContextType = {
reactionEntities: Record<number, Reaction> reactionEntities: Record<number, Reaction>
@ -56,9 +55,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const oppositeReaction = Object.values(reactionEntities).find( const oppositeReaction = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === oppositeReactionKind && r.kind === oppositeReactionKind &&
r.createdBy.slug === reaction.createdBy.slug && r.created_by.slug === reaction.created_by.slug &&
r.shout.id === reaction.shout.id && r.shout.id === reaction.shout.id &&
r.replyTo === reaction.replyTo, r.reply_to === reaction.reply_to,
) )
if (oppositeReaction) { if (oppositeReaction) {

View File

@ -1,5 +1,5 @@
import type { AuthModalSource } from '../components/Nav/AuthModal/types' import type { AuthModalSource } from '../components/Nav/AuthModal/types'
import type { AuthResult, MySubscriptionsQueryResult, User } from '../graphql/types.gen' import type { Author, Result } from '../graphql/schema/core.gen'
import type { Accessor, JSX, Resource } from 'solid-js' import type { Accessor, JSX, Resource } from 'solid-js'
import { import {
@ -12,29 +12,33 @@ import {
useContext, useContext,
} from 'solid-js' } from 'solid-js'
import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { authApiClient } from '../graphql/client/auth'
import { apiClient } from '../graphql/client/core'
import { getToken, resetToken, setToken } from '../graphql/privateGraphQLClient'
import { showModal } from '../stores/ui' import { showModal } from '../stores/ui'
import { apiClient } from '../utils/apiClient'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './snackbar' import { useSnackbar } from './snackbar'
import { useAuthorizer } from './authorizer'
import { VerifyEmailInput, LoginInput, AuthToken, User } from '@authorizerdev/authorizer-js'
type SessionContextType = { export type SessionContextType = {
session: Resource<AuthResult> session: Resource<AuthToken>
isSessionLoaded: Accessor<boolean> isSessionLoaded: Accessor<boolean>
subscriptions: Accessor<MySubscriptionsQueryResult> subscriptions: Accessor<Result>
user: Accessor<User> user: Accessor<User>
author: Resource<Author | null>
isAuthenticated: Accessor<boolean> isAuthenticated: Accessor<boolean>
actions: { actions: {
loadSession: () => AuthResult | Promise<AuthResult> loadSession: () => AuthToken | Promise<AuthToken>
loadSubscriptions: () => Promise<void> loadSubscriptions: () => Promise<void>
requireAuthentication: ( requireAuthentication: (
callback: (() => Promise<void>) | (() => void), callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource, modalSource: AuthModalSource,
) => void ) => void
signIn: ({ email, password }: { email: string; password: string }) => Promise<void> signIn: (params: LoginInput) => Promise<void>
signOut: () => Promise<void> signOut: () => Promise<void>
confirmEmail: (token: string) => Promise<void> confirmEmail: (input: VerifyEmailInput) => Promise<void>
} }
} }
@ -51,21 +55,37 @@ const EMPTY_SUBSCRIPTIONS = {
export const SessionProvider = (props: { children: JSX.Element }) => { export const SessionProvider = (props: { children: JSX.Element }) => {
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [subscriptions, setSubscriptions] = createSignal<MySubscriptionsQueryResult>(EMPTY_SUBSCRIPTIONS) const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const { t } = useLocalize() const { t } = useLocalize()
const { const {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const [, { authorizer }] = useAuthorizer()
const [authToken, setToken] = createSignal<string>('')
const getSession = async (): Promise<AuthResult> => { const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.getMySubscriptions()
if (result) {
setSubscriptions(result)
} else {
setSubscriptions(EMPTY_SUBSCRIPTIONS)
}
}
const getSession = async (): Promise<AuthToken> => {
try { try {
const authResult = await apiClient.getSession() const token = getToken() // FIXME: token in localStorage?
const authResult = await authorizer().getSession({
Authorization: token,
})
if (!authResult) { if (!authResult) {
return null return null
} } else {
setToken(authResult.token) console.log(authResult)
setToken(authResult.access_token || authResult.id_token)
loadSubscriptions() loadSubscriptions()
return authResult return authResult
}
} catch (error) { } catch (error) {
console.error('getSession error:', error) console.error('getSession error:', error)
resetToken() resetToken()
@ -77,28 +97,35 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
} }
} }
const loadSubscriptions = async (): Promise<void> => { const [session, { refetch: loadSession, mutate }] = createResource<AuthToken>(getSession, {
const result = await apiClient.getMySubscriptions()
if (result) {
setSubscriptions(result)
} else {
setSubscriptions(EMPTY_SUBSCRIPTIONS)
}
}
const [session, { refetch: loadSession, mutate }] = createResource<AuthResult>(getSession, {
ssrLoadFrom: 'initial', ssrLoadFrom: 'initial',
initialValue: null, initialValue: null,
}) })
const user = createMemo(() => session()?.user) const user = createMemo(() => session()?.user)
const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug)) const [author, { refetch: loadAuthor }] = createResource<Author | null>(
async () => {
const user = session()?.user
if (user) {
return (await apiClient.getAuthor({ user: user.id })) ?? null
}
return null
},
{
ssrLoadFrom: 'initial',
initialValue: null,
},
)
const signIn = async ({ email, password }: { email: string; password: string }) => { const isAuthenticated = createMemo(() => Boolean(session()?.user))
const authResult = await apiClient.authLogin({ email, password })
setToken(authResult.token) const signIn = async (params: LoginInput) => {
const authResult = await authorizer().login(params)
if (authResult) {
setToken(authResult.access_token || authResult.id_token)
mutate(authResult) mutate(authResult)
}
loadSubscriptions() loadSubscriptions()
// console.debug('signed in') // console.debug('signed in')
} }
@ -115,30 +142,26 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
} }
} }
createEffect(async () => { onMount(async () => {
if (isAuthWithCallback()) { // Load the session and author data on mount
const sessionProof = await session() await loadSession()
loadAuthor()
if (sessionProof) {
await isAuthWithCallback()()
setIsAuthWithCallback(null)
}
}
}) })
const signOut = async () => { const signOut = async () => {
// TODO: call backend to revoke token await authorizer().logout()
mutate(null) mutate(null)
resetToken() resetToken()
setSubscriptions(EMPTY_SUBSCRIPTIONS) setSubscriptions(EMPTY_SUBSCRIPTIONS)
showSnackbar({ body: t("You've successfully logged out") }) showSnackbar({ body: t("You've successfully logged out") })
} }
const confirmEmail = async (token: string) => { const confirmEmail = async (input: VerifyEmailInput) => {
const authResult = await apiClient.confirmEmail({ token }) const authToken: void | AuthToken = await authorizer().verifyEmail(input)
setToken(authResult.token) if (authToken) {
mutate(authResult) setToken(authToken.access_token)
mutate(authToken)
}
} }
const actions = { const actions = {
@ -149,11 +172,11 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
confirmEmail, confirmEmail,
loadSubscriptions, loadSubscriptions,
} }
const value: SessionContextType = { const value: SessionContextType = {
session, session,
subscriptions, subscriptions,
isSessionLoaded, isSessionLoaded,
author,
user, user,
isAuthenticated, isAuthenticated,
actions, actions,
@ -162,6 +185,5 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
onMount(() => { onMount(() => {
loadSession() loadSession()
}) })
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider> return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
} }

141
src/graphql/client/auth.ts Normal file
View File

@ -0,0 +1,141 @@
import { ApiError } from '../error'
import authConfirmEmailMutation from '../mutation/auth/auth-confirm-email'
import authLogoutQuery from '../mutation/auth/auth-logout'
import authRegisterMutation from '../mutation/auth/auth-register'
import authSendLinkMutation from '../mutation/auth/auth-send-link'
import mySession from '../mutation/auth/my-session'
import { getToken, getPrivateClient } from '../privateGraphQLClient'
import { getPublicClient } from '../publicGraphQLClient'
import authCheckEmailQuery from '../query/auth/auth-check-email'
import authLoginQuery from '../query/auth/auth-login'
import {
ResendVerifyEmailInput,
ResetPasswordInput,
CreateUserInput,
DeleteUserInput,
UpdateUserInput,
LoginInput,
SignUpInput,
MagicLinkLoginInput,
AuthResponse,
} from '../schema/auth.gen'
export const authPublicGraphQLClient = getPublicClient('auth')
export const authPrivateGraphqlClient = getPrivateClient('auth')
export const authApiClient = {
authLogin: async ({ email, password }: { email: string; password: string }): Promise<AuthResponse> => {
const response = await authPublicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
// console.debug('[api-client] authLogin', { response })
if (response.error) {
if (
response.error.message === '[GraphQL] User not found' ||
response.error.message === "[GraphQL] 'dict' object has no attribute 'id'"
) {
throw new ApiError('user_not_found')
}
throw new ApiError('unknown', response.error.message)
}
if (response.data.signIn.error) {
if (response.data.signIn.error === 'please, confirm email') {
throw new ApiError('email_not_confirmed')
}
throw new ApiError('unknown', response.data.signIn.error)
}
return response.data.signIn
},
authRegister: async ({
email,
password,
name,
}: {
email: string
password: string
name: string
}): Promise<void> => {
const response = await authPublicGraphQLClient
.mutation(authRegisterMutation, { email, password, name })
.toPromise()
if (response.error) {
if (response.error.message === '[GraphQL] User already exist') {
throw new ApiError('user_already_exists', response.error.message)
}
throw new ApiError('unknown', response.error.message)
}
},
authSignOut: async () => {
const response = await authPublicGraphQLClient.query(authLogoutQuery, {}).toPromise()
return response.data.signOut
},
authCheckEmail: async ({ email }) => {
// check if email is used
const response = await authPublicGraphQLClient.query(authCheckEmailQuery, { email }).toPromise()
return response.data.isEmailUsed
},
authSendLink: async ({ email, lang, template }) => {
// send link with code on email
const response = await authPublicGraphQLClient
.mutation(authSendLinkMutation, { email, lang, template })
.toPromise()
if (response.error) {
if (response.error.message === '[GraphQL] User not found') {
throw new ApiError('user_not_found', response.error.message)
}
throw new ApiError('unknown', response.error.message)
}
if (response.data.sendLink.error) {
throw new ApiError('unknown', response.data.sendLink.message)
}
return response.data.sendLink
},
confirmEmail: async ({ token }: { token: string }) => {
// confirm email with code from link
const response = await authPublicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise()
if (response.error) {
// TODO: better error communication
if (response.error.message === '[GraphQL] check token lifetime') {
throw new ApiError('token_expired', response.error.message)
}
if (response.error.message === '[GraphQL] token is not valid') {
throw new ApiError('token_invalid', response.error.message)
}
throw new ApiError('unknown', response.error.message)
}
if (response.data?.confirmEmail?.error) {
throw new ApiError('unknown', response.data?.confirmEmail?.error)
}
return response.data.confirmEmail
},
getSession: async (): Promise<AuthResponse> => {
if (!getToken()) {
return null
}
// renew session with auth token in header (!)
const response = await authPrivateGraphqlClient.mutation(mySession, {}).toPromise()
if (response.error) {
throw new ApiError('unknown', response.error.message)
}
if (response.data?.getSession?.error) {
throw new ApiError('unknown', response.data.getSession.error)
}
return response.data.getSession
},
}

View File

@ -0,0 +1,78 @@
// inbox
import createChat from '../mutation/chat/chat-create'
import deleteChat from '../mutation/chat/chat-delete'
import markAsRead from '../mutation/chat/chat-mark-as-read'
import createChatMessage from '../mutation/chat/chat-message-create'
import deleteChatMessage from '../mutation/chat/chat-message-delete'
import updateChatMessage from '../mutation/chat/chat-message-update'
import updateChat from '../mutation/chat/chat-update'
import { getPrivateClient } from '../privateGraphQLClient'
import chatMessagesLoadBy from '../query/chat/chat-messages-load-by'
import loadRecipients from '../query/chat/chat-recipients'
import myChats from '../query/chat/chats-load'
import {
QueryLoadChatsArgs,
QueryLoadMessagesByArgs,
MutationCreateChatArgs,
MutationCreateMessageArgs,
QueryLoadRecipientsArgs,
Chat,
MutationMarkAsReadArgs,
MutationDeleteChatArgs,
MutationUpdateChatArgs,
MutationUpdateMessageArgs,
MutationDeleteMessageArgs,
} from '../schema/chat.gen'
const privateInboxGraphQLClient = getPrivateClient('chat')
export const inboxClient = {
loadChats: async (options: QueryLoadChatsArgs): Promise<Chat[]> => {
const resp = await privateInboxGraphQLClient.query(myChats, options).toPromise()
return resp.data.load_chats.chats
},
createChat: async (options: MutationCreateChatArgs) => {
const resp = await privateInboxGraphQLClient.mutation(createChat, options).toPromise()
return resp.data.create_chat
},
markAsRead: async (options: MutationMarkAsReadArgs) => {
const resp = await privateInboxGraphQLClient.mutation(markAsRead, options).toPromise()
return resp.data.mark_as_read
},
updateChat: async (options: MutationUpdateChatArgs) => {
const resp = await privateInboxGraphQLClient.mutation(updateChat, options).toPromise()
return resp.data.update_chat
},
deleteChat: async (options: MutationDeleteChatArgs) => {
const resp = await privateInboxGraphQLClient.mutation(deleteChat, options).toPromise()
return resp.data.delete_chat
},
createMessage: async (options: MutationCreateMessageArgs) => {
const resp = await privateInboxGraphQLClient.mutation(createChatMessage, options).toPromise()
return resp.data.create_message.message
},
updateMessage: async (options: MutationUpdateMessageArgs) => {
const resp = await privateInboxGraphQLClient.mutation(updateChatMessage, options).toPromise()
return resp.data.update_message.message
},
deleteMessage: async (options: MutationDeleteMessageArgs) => {
const resp = await privateInboxGraphQLClient.mutation(deleteChatMessage, options).toPromise()
return resp.data.delete_message
},
loadChatMessages: async (options: QueryLoadMessagesByArgs) => {
const resp = await privateInboxGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
return resp.data.load_messages_by.messages
},
loadRecipients: async (options: QueryLoadRecipientsArgs) => {
const resp = await privateInboxGraphQLClient.query(loadRecipients, options).toPromise()
return resp.data.load_recipients.members
},
}

209
src/graphql/client/core.ts Normal file
View File

@ -0,0 +1,209 @@
import type {
FollowingEntity,
ShoutInput,
Topic,
Author,
LoadShoutsOptions,
QueryLoadAuthorsByArgs,
ProfileInput,
ReactionInput,
ReactionBy,
Shout,
Result,
} from '../schema/core.gen'
import createArticle from '../mutation/core/article-create'
import deleteShout from '../mutation/core/article-delete'
import updateArticle from '../mutation/core/article-update'
import followMutation from '../mutation/core/follow'
import reactionCreate from '../mutation/core/reaction-create'
import reactionDestroy from '../mutation/core/reaction-destroy'
import reactionUpdate from '../mutation/core/reaction-update'
import unfollowMutation from '../mutation/core/unfollow'
import updateProfile from '../mutation/core/update-profile'
import { getPrivateClient } from '../privateGraphQLClient'
import { getPublicClient } from '../publicGraphQLClient'
import shoutLoad from '../query/core/article-load'
import shoutsLoadBy from '../query/core/articles-load-by'
import authorBy from '../query/core/author-by'
import authorFollowers from '../query/core/author-followers'
import authorFollowed from '../query/core/authors-followed-by'
import userFollowedTopics from '../query/core/topics-by-author'
import authorsAll from '../query/core/authors-all'
import authorsLoadBy from '../query/core/authors-load-by'
import draftsLoad from '../query/core/articles-load-drafts'
import myFeed from '../query/core/articles-load-feed'
import mySubscriptions from '../query/core/my-followed'
import reactionsLoadBy from '../query/core/reactions-load-by'
import topicBySlug from '../query/core/topic-by-slug'
import topicsAll from '../query/core/topics-all'
import topicsRandomQuery from '../query/core/topics-random'
export const privateGraphQLClient = getPublicClient('core')
export const publicGraphQLClient = getPrivateClient('core')
export const apiClient = {
getRandomTopics: async ({ amount }: { amount: number }) => {
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
if (!response.data) {
console.error('[graphql.client.core] getRandomTopics', response.error)
}
return response.data.get_topics_random
},
follow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
const response = await privateGraphQLClient.mutation(followMutation, { what, slug }).toPromise()
return response.data.follow
},
unfollow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
const response = await privateGraphQLClient.mutation(unfollowMutation, { what, slug }).toPromise()
return response.data.unfollow
},
getAllTopics: async () => {
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
if (response.error) {
console.debug('[graphql.client.core] get_topicss_all', response.error)
}
return response.data.get_topics_all
},
getAllAuthors: async () => {
const response = await publicGraphQLClient.query(authorsAll, {}).toPromise()
if (response.error) {
console.debug('[graphql.client.core] get_authors_all', response.error)
}
return response.data.get_authors_all
},
getAuthor: async (params: { slug?: string; author_id?: number; user?: string }): Promise<Author> => {
const response = await publicGraphQLClient.query(authorBy, params).toPromise()
return response.data.get_author
},
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
return response.data.get_author_followers
},
getAuthorFollowingUsers: async ({ slug }: { slug: string }): Promise<Author[]> => {
const response = await publicGraphQLClient.query(authorFollowed, { slug }).toPromise()
return response.data.get_author_followed
},
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
const response = await publicGraphQLClient.query(userFollowedTopics, { slug }).toPromise()
return response.data.userFollowedTopics
},
updateProfile: async (input: ProfileInput) => {
const response = await privateGraphQLClient.mutation(updateProfile, { profile: input }).toPromise()
return response.data.update_profile
},
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
return response.data.get_topic
},
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
return response.data.create_shout.shout
},
updateArticle: async ({
shoutId,
shoutInput,
publish,
}: {
shoutId: number
shoutInput?: ShoutInput
publish: boolean
}): Promise<Shout> => {
const response = await privateGraphQLClient
.mutation(updateArticle, { shoutId, shoutInput, publish })
.toPromise()
console.debug('[graphql.client.core] updateArticle:', response.data)
return response.data.update_shout.shout
},
deleteShout: async ({ shoutId }: { shoutId: number }): Promise<void> => {
const response = await privateGraphQLClient.mutation(deleteShout, { shout_id: shoutId }).toPromise()
console.debug('[graphql.client.core] deleteShout:', response)
},
getDrafts: async (): Promise<Shout[]> => {
const response = await privateGraphQLClient.query(draftsLoad, {}).toPromise()
console.debug('[graphql.client.core] getDrafts:', response)
return response.data.load_shouts_drafts
},
createReaction: async (input: ReactionInput) => {
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction: input }).toPromise()
console.debug('[graphql.client.core] createReaction:', response)
return response.data.create_reaction.reaction
},
destroyReaction: async (id: number) => {
const response = await privateGraphQLClient.mutation(reactionDestroy, { id: id }).toPromise()
console.debug('[graphql.client.core] destroyReaction:', response)
return response.data.delete_reaction.reaction
},
updateReaction: async (id: number, input: ReactionInput) => {
const response = await privateGraphQLClient
.mutation(reactionUpdate, { id: id, reaction: input })
.toPromise()
console.debug('[graphql.client.core] updateReaction:', response)
return response.data.update_reaction.reaction
},
getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()
return resp.data.load_authors_by
},
getShoutBySlug: async (slug: string) => {
const resp = await publicGraphQLClient
.query(shoutLoad, {
slug,
})
.toPromise()
// if (resp.error) {
// console.error(resp)
// }
return resp.data.get_shout
},
getShoutById: async (shout_id: number) => {
const resp = await publicGraphQLClient
.query(shoutLoad, {
shout_id,
})
.toPromise()
if (resp.error) {
console.error(resp)
}
return resp.data.get_shout
},
getShouts: async (options: LoadShoutsOptions) => {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
if (resp.error) {
console.error(resp)
}
return resp.data.load_shouts_by
},
getMyFeed: async (options: LoadShoutsOptions) => {
const resp = await privateGraphQLClient.query(myFeed, { options }).toPromise()
if (resp.error) {
console.error(resp)
}
return resp.data.load_shouts_feed
},
getReactionsBy: async ({ by, limit }: { by: ReactionBy; limit?: number }) => {
const resp = await publicGraphQLClient
.query(reactionsLoadBy, { by, limit: limit ?? 1000, offset: 0 })
.toPromise()
return resp.data.load_reactions_by
},
getMySubscriptions: async (): Promise<Result> => {
const resp = await privateGraphQLClient.query(mySubscriptions, {}).toPromise()
// console.debug(resp.data)
return resp.data.get_my_followed
},
}

View File

@ -0,0 +1,25 @@
import markAllNotificationsAsRead from '../mutation/notifier/mark-all-notifications-as-read'
import markNotificationAsRead from '../mutation/notifier/mark-notification-as-read'
import { getPrivateClient } from '../privateGraphQLClient'
import loadNotifications from '../query/notifier/notifications-load'
import { Notification, NotificationsResult, QueryLoad_NotificationsArgs } from '../schema/notifier.gen'
export const notifierPrivateGraphqlClient = getPrivateClient('notifier')
export const notifierClient = {
getNotifications: async (params: QueryLoad_NotificationsArgs): Promise<NotificationsResult> => {
const resp = await notifierPrivateGraphqlClient.query(loadNotifications, { params }).toPromise()
return resp.data.load_notifications
},
markNotificationAsRead: async (notification_id: number): Promise<void> => {
await notifierPrivateGraphqlClient
.mutation(markNotificationAsRead, {
notification_id,
})
.toPromise()
},
markAllNotificationsAsRead: async (): Promise<void> => {
await notifierPrivateGraphqlClient.mutation(markAllNotificationsAsRead, {}).toPromise()
},
}

16
src/graphql/error.ts Normal file
View File

@ -0,0 +1,16 @@
type ApiErrorCode =
| 'unknown'
| 'email_not_confirmed'
| 'user_not_found'
| 'user_already_exists'
| 'token_expired'
| 'token_invalid'
export class ApiError extends Error {
code: ApiErrorCode
constructor(code: ApiErrorCode, message?: string) {
super(message)
this.code = code
}
}

View File

@ -1,16 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation PublishShoutMutation($shoutId: Int!, $shoutInput: ShoutInput) {
publishShout(shout_id: $shoutId, shout_input: $shoutInput) {
error
shout {
id
slug
title
subtitle
body
}
}
}
`

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation CreateChat($title: String, $members: [Int]!) { mutation CreateChat($title: String, $members: [Int]!) {
createChat(title: $title, members: $members) { create_chat(title: $title, members: $members) {
error error
chat { chat {
id id

View File

@ -1,9 +1,8 @@
import { ChatInput } from './../types.gen'
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteChat($chat_id: String!) { mutation DeleteChat($chat_id: String!) {
deleteChat(chat_id: $chat_id) { delete_chat(chat_id: $chat_id) {
error error
} }
} }

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation MarkAsReadMutation($message_id: Int!, $chat_id: String!) { mutation MarkAsReadMutation($message_id: Int!, $chat_id: String!) {
markAsRead(message_id: $message_id, chat_id: $chat_id) { marke_as_read(message_id: $message_id, chat_id: $chat_id) {
error error
} }
} }

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation createMessage($chat_id: String!, $body: String!, $reply_to: Int) { mutation createMessage($chat_id: String!, $body: String!, $reply_to: Int) {
createMessage(chat_id: $chat_id, body: $body, reply_to: $reply_to) { create_message(chat_id: $chat_id, body: $body, reply_to: $reply_to) {
error error
message { message {
id id

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteMessage($chat_id: String!) { mutation DeleteMessage($chat_id: String!) {
deleteMessage(chat_id: $chat_id) { delete_message(chat_id: $chat_id) {
error error
} }
} }

View File

@ -1,9 +1,8 @@
import { ChatInput } from './../types.gen'
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateMessage($message: MessageInput!) { mutation UpdateMessage($message: MessageInput!) {
createMessage(message: $message) { update_message(message: $message) {
error error
message { message {
id id

View File

@ -1,9 +1,8 @@
import { ChatInput } from './../types.gen'
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateChat($chat: ChatInput!) { mutation UpdateChat($chat: ChatInput!) {
updateChat(chat: $chat) { update_chat(chat: $chat) {
error error
chat { chat {
id id

View File

@ -1,9 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabInviteMutation($author: String!, $slug: String!) {
inviteAuthor(author: $author, shout: $slug) {
error
}
}
`

View File

@ -1,9 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabRemoveeMutation($author: String!, $slug: String!) {
removeAuthor(author: $author, shout: $slug) {
error
}
}
`

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation CreateShoutMutation($shout: ShoutInput!) { mutation CreateShoutMutation($shout: ShoutInput!) {
createShout(inp: $shout) { create_shout(inp: $shout) {
error error
shout { shout {
id id

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteShoutMutation($shoutId: Int!) { mutation DeleteShoutMutation($shoutId: Int!) {
deleteShout(shout_id: $shoutId) { delete_shout(shout_id: $shoutId) {
error error
} }
} }

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput, $publish: Boolean) { mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput, $publish: Boolean) {
updateShout(shout_id: $shoutId, shout_input: $shoutInput, publish: $publish) { update_shout(shout_id: $shoutId, shout_input: $shoutInput, publish: $publish) {
error error
shout { shout {
id id

View File

@ -0,0 +1,9 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabInviteMutation($invite_id: Int!) {
accept_invite(invite_id: $invite_id) {
error
}
}
`

View File

@ -0,0 +1,9 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabInviteMutation($author_id: Int!, $slug: String!) {
create_invite(author_id: $author_id, slug: $slug) {
error
}
}
`

View File

@ -0,0 +1,9 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabInviteMutation($invite_id: Int!) {
reject_invite(invite_id: $invite_id) {
error
}
}
`

View File

@ -0,0 +1,9 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabRemoveeMutation($author_id: Int!, $slug: String!) {
remove_author(author_id: $author_id, slug: $slug) {
error
}
}
`

View File

@ -0,0 +1,9 @@
import { gql } from '@urql/core'
export default gql`
mutation CollabRemoveeMutation($invite_id: Int!) {
remove_invite(invite_id: $invite_id) {
error
}
}
`

View File

@ -2,13 +2,13 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation CommunityCreateMutation($title: String!, $desc: String!) { mutation CommunityCreateMutation($title: String!, $desc: String!) {
createCommunity(title: $title, desc: $desc) { create_community(title: $title, desc: $desc) {
id id
desc desc
name name
pic pic
createdAt created_at
createdBy created_by
} }
} }
` `

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation CommunityDestroyMutation($slug: String!) { mutation CommunityDestroyMutation($slug: String!) {
deleteCommunity(slug: $slug) { delete_community(slug: $slug) {
error error
} }
} }

View File

@ -2,14 +2,14 @@ import { gql } from '@urql/core'
export default gql` export default gql`
mutation CommunityUpdateMutation($community: Community!) { mutation CommunityUpdateMutation($community: Community!) {
updateCommunity(community: $community) { update_community(community: $community) {
id id
slug slug
desc desc
name name
pic pic
createdAt created_at
createdBy created_by
} }
} }
` `

Some files were not shown because too many files have changed in this diff Show More