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
public
*.cjs
src/graphql/*.gen.ts
src/graphql/schema/*.gen.ts
dist/
.vercel/

3
.gitignore vendored
View File

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

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
schema: 'http://127.0.0.1:8080'
#schema: 'https://v2.discours.io'
generates:
src/graphql/introspec.gen.ts:
plugins:
- urql-introspection
config:
useTypeImports: true
includeScalars: true
includeEnums: true
src/graphql/types.gen.ts:
# Generate types for chat
src/graphql/schema/chat.gen.ts:
schema: 'https://chat.discours.io'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-urql'
config:
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:
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

12013
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "discoursio-webapp",
"version": "0.8.0",
"version": "0.9.0",
"private": true,
"license": "MIT",
"type": "module",
@ -12,6 +12,7 @@
"dev": "vite",
"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",
"postinstall": "npm run codegen",
"lint": "npm run lint:code && npm run lint:styles",
"lint:code": "eslint .",
"lint:code:fix": "eslint . --fix",
@ -30,6 +31,7 @@
"typecheck:watch": "tsc --noEmit --watch"
},
"dependencies": {
"@authorizerdev/authorizer-js": "1.2.11",
"form-data": "4.0.0",
"i18next": "22.4.15",
"i18next-icu": "2.3.0",

View File

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

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx'
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 { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'

View File

@ -7,7 +7,7 @@ import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session'
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 { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
@ -25,7 +25,7 @@ type Props = {
compact?: boolean
isArticleAuthor?: boolean
sortedComments?: Reaction[]
lastSeen?: Date
lastSeen?: number
class?: string
showArticleLink?: boolean
clickedReply?: (id: number) => void
@ -52,7 +52,7 @@ export const Comment = (props: Props) => {
actions: { showSnackbar },
} = 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 body = createMemo(() => (comment().body || '').trim())
@ -82,7 +82,7 @@ export const Comment = (props: Props) => {
setLoading(true)
await createReaction({
kind: ReactionKind.Comment,
replyTo: props.comment.id,
reply_to: props.comment.id,
body: value,
shout: props.comment.shout.id,
})
@ -114,13 +114,11 @@ export const Comment = (props: Props) => {
}
}
const createdAt = new Date(comment()?.createdAt)
return (
<li
id={`comment_${comment().id}`}
class={clsx(styles.comment, props.class, {
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen,
[styles.isNew]: !isCommentAuthor() && comment()?.created_at > props.lastSeen,
})}
>
<Show when={!!body()}>
@ -130,8 +128,8 @@ export const Comment = (props: Props) => {
fallback={
<div>
<Userpic
name={comment().createdBy.name}
userpic={comment().createdBy.userpic}
name={comment().created_by.name}
userpic={comment().created_by.pic}
class={clsx({
[styles.compactUserpic]: props.compact,
})}
@ -144,7 +142,7 @@ export const Comment = (props: Props) => {
>
<div class={styles.commentDetails}>
<div class={styles.commentAuthor}>
<AuthorLink author={comment()?.createdBy as Author} />
<AuthorLink author={comment()?.created_by as Author} />
</div>
<Show when={props.isArticleAuthor}>
@ -253,7 +251,7 @@ export const Comment = (props: Props) => {
</Show>
<Show when={props.sortedComments}>
<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) => (
<Comment
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 { Show } from 'solid-js'
@ -33,12 +33,12 @@ export const CommentDate = (props: Props) => {
[styles.showOnHover]: props.showOnHover,
})}
>
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>
<Show when={props.comment.updatedAt}>
<time class={styles.date}>{formattedDate(props.comment.created_at * 1000)}</time>
<Show when={props.comment.updated_at}>
<time class={styles.date}>
<Icon name="edit" class={styles.icon} />
<span class={styles.text}>
{t('Edited')} {formattedDate(props.comment.updatedAt)}
{t('Edited')} {formattedDate(props.comment.updated_at * 1000)}
</span>
</time>
</Show>

View File

@ -1,5 +1,3 @@
import type { Reaction } from '../../graphql/types.gen'
import { clsx } from 'clsx'
import { createMemo } from 'solid-js'
@ -7,7 +5,7 @@ import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
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 { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
@ -33,20 +31,20 @@ export const CommentRatingControl = (props: Props) => {
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.created_by.slug === user()?.slug &&
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 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(() =>
Object.values(reactionEntities).filter(
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
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(
(r) =>
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.created_by.slug === user()?.slug &&
r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id,
r.reply_to === props.comment.id,
)
return deleteReaction(reactionToDelete.id)
}
@ -71,7 +69,7 @@ export const CommentRatingControl = (props: Props) => {
await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id,
replyTo: props.comment.id,
reply_to: props.comment.id,
})
}
} catch {

View File

@ -4,7 +4,7 @@ import { Show, createMemo, createSignal, onMount, For, lazy } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
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 { Button } from '../_shared/Button'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
@ -18,7 +18,7 @@ const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
if (a.replyTo && b.replyTo) {
if (a.reply_to && b.reply_to) {
return 0
}
@ -76,19 +76,19 @@ export const CommentsTree = (props: Props) => {
return newSortedComments
})
const dateFromLocalStorage = new Date(localStorage.getItem(`${props.shoutSlug}`))
const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`))
const currentDate = new Date()
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
onMount(() => {
if (!dateFromLocalStorage) {
setCookie()
} else if (currentDate > dateFromLocalStorage) {
} else if (currentDate.getTime() > dateFromLocalStorage) {
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
}
const created = new Date(c.createdAt)
const created = c.created_at
return created > dateFromLocalStorage
})
setNewReactions(newComments)
@ -153,12 +153,12 @@ export const CommentsTree = (props: Props) => {
</Show>
</div>
<ul class={styles.comments}>
<For each={sortedComments().filter((r) => !r.replyTo)}>
<For each={sortedComments().filter((r) => !r.reply_to)}>
{(reaction) => (
<Comment
sortedComments={sortedComments()}
isArticleAuthor={Boolean(
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug),
props.articleAuthors.some((a) => a.slug === reaction.created_by.slug),
)}
comment={reaction}
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 { createPopper } from '@popperjs/core'
@ -68,13 +68,9 @@ export const FullArticle = (props: Props) => {
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(
() =>
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
props.article.topics[0],
)
const mainTopic = createMemo(() => (props.article.topics.length > 0 ? props.article.topics[0] : null))
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
@ -293,10 +289,10 @@ export const FullArticle = (props: Props) => {
onClick={handleArticleBodyClick}
>
{/*TODO: Check styles.shoutTopic*/}
<Show when={props.article.layout !== 'music'}>
<Show when={props.article.layout !== 'audio'}>
<div class={styles.shoutHeader}>
<Show when={mainTopic()}>
<CardTopic title={mainTopic().title} slug={props.article.mainTopic} />
<CardTopic title={mainTopic().title} slug={mainTopic().slug} />
</Show>
<h1>{props.article.title}</h1>
@ -328,7 +324,7 @@ export const FullArticle = (props: Props) => {
<Show when={props.article.lead}>
<section class={styles.lead} innerHTML={props.article.lead} />
</Show>
<Show when={props.article.layout === 'music'}>
<Show when={props.article.layout === 'audio'}>
<AudioHeader
title={props.article.title}
cover={props.article.cover}

View File

@ -4,7 +4,7 @@ import { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
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 { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
@ -33,9 +33,9 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.created_by.slug === user()?.slug &&
r.shout.id === props.shout.id &&
!r.replyTo,
!r.reply_to,
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
@ -47,7 +47,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
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(
(r) =>
r.kind === reactionKind &&
r.createdBy.slug === user()?.slug &&
r.created_by.slug === user()?.slug &&
r.shout.id === props.shout.id &&
!r.replyTo,
!r.reply_to,
)
return deleteReaction(reactionToDelete.id)
}

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx'
import { Author } from '../../../graphql/types.gen'
import { Author } from '../../../graphql/schema/core.gen'
import { Userpic } from '../Userpic'
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 { 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 { follow, unfollow } from '../../../stores/zine/common'
import { Button } from '../../_shared/Button'
@ -67,7 +67,7 @@ export const AuthorBadge = (props: Props) => {
hasLink={true}
size={'M'}
name={props.author.name}
userpic={props.author.userpic}
userpic={props.author.pic}
slug={props.author.slug}
/>
<a href={`/author/${props.author.slug}`} class={styles.info}>
@ -78,7 +78,9 @@ export const AuthorBadge = (props: Props) => {
<Switch
fallback={
<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>
}
>

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 { clsx } from 'clsx'
@ -6,7 +6,7 @@ import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
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 { router, useRouter } from '../../../stores/router'
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'

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 { clsx } from 'clsx'
@ -53,7 +53,7 @@ export const Draft = (props: Props) => {
<div class={clsx(props.class)}>
<div class={styles.created}>
<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 class={styles.titleContainer}>
<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 { 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 { clsx } from 'clsx'
@ -86,12 +86,10 @@ const getTitleAndSubtitle = (
export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize()
const { user } = useSession()
const mainTopic =
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
props.article.topics[0]
const mainTopic = props.article.topics[0]
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)

View File

@ -1,6 +1,6 @@
// 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 { 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 { 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'

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'

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 { 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'

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'

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'

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 { 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'

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 { 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'

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 { 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 { useSession } from '../../../context/session'
import { ApiError } from '../../../graphql/error'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import styles from './AuthModal.module.scss'

View File

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

View File

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

View File

@ -5,11 +5,11 @@ import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { ApiError } from '../../../graphql/error'
import { register } from '../../../stores/auth'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail'
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 { clsx } from 'clsx'
@ -6,9 +6,9 @@ import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
import { router, ROUTES, useRouter } from '../../../stores/router'
import { useModalStore } from '../../../stores/ui'
import { apiClient } from '../../../utils/apiClient'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
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 { getPagePath, openPage } from '@nanostores/router'
@ -7,12 +6,13 @@ import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
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 { GroupAvatar } from '../../_shared/GroupAvatar'
import { TimeAgo } from '../../_shared/TimeAgo'
import styles from './NotificationView.module.scss'
import { apiClient } from '../../../graphql/client/core'
import { Reaction, Shout } from '../../../graphql/schema/core.gen'
type Props = {
notification: Notification
@ -21,22 +21,6 @@ type Props = {
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) => {
const {
actions: { markNotificationAsRead, hideNotificationsPanel },
@ -46,19 +30,13 @@ export const NotificationView = (props: Props) => {
const { t, formatDate, formatTime } = useLocalize()
const [data, setData] = createSignal<NotificationData>(null)
const [data, setData] = createSignal<Reaction>(null) // NOTE: supports only SSMessage.entity == "reaction"
onMount(() => {
setTimeout(() => setData(JSON.parse(props.notification.data)))
setTimeout(() => setData(JSON.parse(props.notification.payload)))
})
const lastUser = createMemo(() => {
if (!data()) {
return null
}
return data().users[data().users.length - 1]
})
const lastUser = createMemo(() => data().created_by)
const handleLinkClick = (event: MouseEvent) => {
event.stopPropagation()
@ -87,45 +65,65 @@ export const NotificationView = (props: Props) => {
}
}
switch (props.notification.type) {
case NotificationType.NewComment: {
return (
<>
{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,
})}
</>
)
switch (props.notification.action) {
case 'create': {
if (data()?.reply_to) {
return (
<>
{t('NotificationNewReplyText1', {
commentsCount: 0, // FIXME: props.notification.occurrences,
})}{' '}
<a
href={getPagePath(router, 'article', { slug: data().shout.slug })}
onClick={handleLinkClick}
>
{shoutTitle}
</a>{' '}
{t('NotificationNewReplyText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{t('NotificationNewReplyText3', {
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 NotificationType.NewReply: {
return (
<>
{t('NotificationNewReplyText1', {
commentsCount: props.notification.occurrences,
})}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle}
</a>{' '}
{t('NotificationNewReplyText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{t('NotificationNewReplyText3', {
restUsersCount: data().users.length - 1,
})}
</>
)
case 'update': {
}
case 'delete': {
}
case 'follow': {
}
case 'unfollow': {
}
case 'invited': {
// TODO: invited for collaborative authoring
}
default:
return <></>
}
})
@ -137,22 +135,22 @@ export const NotificationView = (props: Props) => {
}
openPage(router, 'article', { slug: data().shout.slug })
if (data().reactionIds) {
changeSearchParam({ commentId: data().reactionIds[0].toString() })
}
// FIXME:
// if (data().reactionIds) {
// changeSearchParam({ commentId: data().reactionIds[0].toString() })
// }
}
const formattedDateTime = createMemo(() => {
switch (props.dateTimeFormat) {
case 'ago': {
return <TimeAgo date={props.notification.createdAt} />
return <TimeAgo date={props.notification.created_at} />
}
case 'time': {
return formatTime(new Date(props.notification.createdAt))
return formatTime(new Date(props.notification.created_at))
}
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}
>
<div class={styles.userpic}>
<GroupAvatar authors={data().users} />
<GroupAvatar authors={[] /*d FIXME: data().users */} />
</div>
<div>{content()}</div>
<div class={styles.timeContainer}>{formattedDateTime()}</div>

View File

@ -95,15 +95,19 @@ export const NotificationsPanel = (props: Props) => {
}
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(() => {
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
return sortedNotifications().filter((notification) =>
isYesterday(new Date(notification.created_at * 1000)),
)
})
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 }

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 { createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
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 { capitalize } from '../../utils/capitalize'
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 { 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 { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
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 { Button } from '../_shared/Button'

View File

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

View File

@ -4,9 +4,9 @@ import { createSignal, For, onMount, Show } from 'solid-js'
import { useEditorContext } from '../../../context/editor'
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 { apiClient } from '../../../utils/apiClient'
import { Draft } from '../../Draft'
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 deepEqual from 'fast-deep-equal'
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 { useLocalize } from '../../context/localize'
import { ShoutVisibility, type Shout, type Topic } from '../../graphql/schema/core.gen'
import { LayoutType, MediaItem } from '../../pages/types'
import { useRouter } from '../../stores/router'
import { clone } from '../../utils/clone'
@ -75,7 +74,7 @@ export const EditView = (props: Props) => {
description: props.shout.description,
subtitle: props.shout.subtitle,
selectedTopics: shoutTopics,
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
mainTopic: shoutTopics[0],
body: props.shout.body,
coverImageUrl: props.shout.cover,
media: props.shout.media,
@ -163,7 +162,7 @@ export const EditView = (props: Props) => {
const articleTitle = () => {
switch (props.shout.layout as LayoutType) {
case 'music': {
case 'audio': {
return t('Album name')
}
case 'image': {
@ -182,7 +181,7 @@ export const EditView = (props: Props) => {
const hasChanges = !deepEqual(form, prevForm)
if (hasChanges) {
setSaving(true)
if (props.shout.visibility === 'owner') {
if (props.shout.visibility === ShoutVisibility.Authors) {
await saveDraft(form)
} else {
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">
<Show when={page().route === 'edit'}>
<div class={styles.headingActions}>
<Show when={!isSubtitleVisible() && props.shout.layout !== 'music'}>
<Show when={!isSubtitleVisible() && props.shout.layout !== 'audio'}>
<div class={styles.action} onClick={showSubtitleInput}>
{t('Add subtitle')}
</div>
</Show>
<Show when={!isLeadVisible() && props.shout.layout !== 'music'}>
<Show when={!isLeadVisible() && props.shout.layout !== 'audio'}>
<div class={styles.action} onClick={showLeadInput}>
{t('Add intro')}
</div>
</Show>
</div>
<>
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'music' })}>
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}>
<div class={styles.inputContainer}>
<GrowingTextarea
allowEnterKey={true}
@ -270,7 +269,7 @@ export const EditView = (props: Props) => {
<div class={styles.validationError}>{formErrors.title}</div>
</Show>
<Show when={props.shout.layout === 'music'}>
<Show when={props.shout.layout === 'audio'}>
<div class={styles.additional}>
<input
type="text"
@ -298,7 +297,7 @@ export const EditView = (props: Props) => {
/>
</div>
</Show>
<Show when={props.shout.layout !== 'music'}>
<Show when={props.shout.layout !== 'audio'}>
<Show when={isSubtitleVisible()}>
<GrowingTextarea
textAreaRef={(el) => {
@ -324,7 +323,7 @@ export const EditView = (props: Props) => {
</Show>
</Show>
</div>
<Show when={props.shout.layout === 'music'}>
<Show when={props.shout.layout === 'audio'}>
<Show
when={form.coverImageUrl}
fallback={
@ -387,7 +386,7 @@ export const EditView = (props: Props) => {
/>
</Show>
<Show when={props.shout.layout === 'music'}>
<Show when={props.shout.layout === 'audio'}>
<AudioUploader
audio={mediaItems()}
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 { 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 { router, useRouter } from '../../../stores/router'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
@ -39,7 +39,9 @@ export const Expo = (props: Props) => {
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)
setIsLoadMoreButtonVisible(hasMore)
@ -107,11 +109,11 @@ export const Expo = (props: Props) => {
<span class={clsx('linkReplacement')}>{t('Literature')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'music' })}>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'audio' })}>
<ConditionalWrapper
condition={getLayout() !== 'music'}
condition={getLayout() !== 'audio'}
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>

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 { clsx } from 'clsx'
@ -209,7 +209,7 @@ export const FeedView = (props: Props) => {
/>
</div>
<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} />
</div>
<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'

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

View File

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

View File

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

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'

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 { 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 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 { Icon } from '../Icon'

View File

@ -1,9 +1,7 @@
import type { Reaction } from '../../../graphql/types.gen'
import { clsx } from 'clsx'
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 styles from './VotersList.module.scss'
@ -26,11 +24,11 @@ export const VotersList = (props: Props) => {
<li class={styles.item}>
<div class={styles.user}>
<Userpic
name={reaction.createdBy.name}
userpic={reaction.createdBy.userpic}
name={reaction.created_by.name}
userpic={reaction.created_by.pic}
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>
{reaction.kind === ReactionKind.Like ? (
<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 { 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 { apiClient } from '../utils/apiClient'
import { slugify } from '../utils/slugify'
import { useLocalize } from './localize'
@ -130,10 +130,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
shoutId: formToUpdate.shoutId,
shoutInput: {
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']>>>
// community?: InputMaybe<Scalars['Int']>
mainTopic: topic2topicInput(formToUpdate.mainTopic),
// mainTopic: topic2topicInput(formToUpdate.mainTopic),
slug: formToUpdate.slug,
subtitle: formToUpdate.subtitle,
title: formToUpdate.title,
@ -163,7 +163,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const shout = await updateShout(formToSave, { publish: false })
removeDraftFromLocalStorage(formToSave.shoutId)
if (shout.visibility === 'owner') {
if (shout.visibility === ShoutVisibility.Authors) {
openPage(router, 'drafts')
} else {
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 { createContext, createEffect, createSignal, onMount, useContext } from 'solid-js'
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
import { inboxClient } from '../utils/apiClient'
import { createContext, createSignal, useContext } from 'solid-js'
import { SSEMessage, useConnect } from './connect'
import { loadMessages } from '../stores/inbox'
import { getToken } from '../graphql/privateGraphQLClient'
import { fetchEventSource } from '@microsoft/fetch-event-source'
export interface SSEMessage {
id: string
entity: string
action: string
payload: any // Author | Shout | Reaction | Message
timestamp?: number
seen?: boolean
}
import { inboxClient } from '../graphql/client/chat'
type InboxContextType = {
chats: Accessor<Chat[]>
@ -35,47 +25,20 @@ export function useInbox() {
export const InboxProvider = (props: { children: JSX.Element }) => {
const [chats, setChats] = createSignal<Chat[]>([])
const [messages, setMessages] = createSignal<Message[]>([])
const handleMessage = (sseMessage) => {
const handleMessage = (sseMessage: SSEMessage) => {
console.log('[context.inbox]:', sseMessage)
// TODO: handle all action types: create update delete join left
if (sseMessage.entity == 'message') {
if (sseMessage.entity === 'message') {
const relivedMessage = sseMessage.payload
setMessages((prev) => [...prev, relivedMessage])
} else if (sseMessage.entity == 'chat') {
} else if (sseMessage.entity === 'chat') {
const relivedChat = sseMessage.payload
setChats((prev) => [...prev, relivedChat])
}
}
createEffect(async () => {
const token = getToken()
if (token) {
await fetchEventSource('https://chat.discours.io/connect', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
onmessage(event) {
const m: SSEMessage = JSON.parse(event.data)
console.log('[context.inbox] Received message:', m)
if (m.entity === 'chat' || m.entity == 'message') {
handleMessage(m)
} else {
console.debug(m)
}
},
onclose() {
console.log('[context.inbox] sse connection closed by server')
},
onerror(err) {
console.error('[context.inbox] sse connection closed by error', err)
throw new Error(err) // NOTE: simple hack to close the connection
},
})
}
})
const { addHandler } = useConnect()
addHandler(handleMessage)
const loadChats = async () => {
try {
@ -103,7 +66,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
const currentChat = chats().find((chat) => chat.id === args.chat_id)
setChats((prev) => [
...prev.filter((c) => c.id !== currentChat.id),
{ ...currentChat, updatedAt: message.createdAt },
{ ...currentChat, updated_at: message.created_at },
])
} catch (error) {
console.error('Error sending message:', error)

View File

@ -1,17 +1,14 @@
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 { Portal } from 'solid-js/web'
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
import { NotificationsPanel } from '../components/NotificationsPanel'
import { Notification } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient'
import { apiBaseUrl } from '../utils/config'
import SSEService, { EventData } from '../utils/sseService'
import { useSession } from './session'
import { notifierClient as apiClient } from '../graphql/client/notifier'
import { Notification } from '../graphql/schema/notifier.gen'
import { SSEMessage, useConnect } from './connect'
type NotificationsContextType = {
notificationEntities: Record<number, Notification>
@ -35,53 +32,45 @@ export function useNotifications() {
return useContext(NotificationsContext)
}
const sseService = new SSEService()
export const NotificationsProvider = (props: { children: JSX.Element }) => {
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
const { isAuthenticated, user } = useSession()
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
const { addHandler } = useConnect()
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) => {
acc[notification.id] = notification
return acc
}, {})
setTotalNotificationsCount(totalCount)
setUnreadNotificationsCount(totalUnreadCount)
setTotalNotificationsCount(total)
setUnreadNotificationsCount(unread)
setNotificationEntities(newNotificationEntities)
return notifications
}
const sortedNotifications = createMemo(() => {
return Object.values(notificationEntities).sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
return Object.values(notificationEntities).sort((a, b) => b.created_at - a.created_at)
})
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
createEffect(() => {
if (isAuthenticated()) {
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`)
sseService.subscribeToEvent('message', (data: EventData) => {
if (data.type === 'newNotifications') {
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
} else {
console.error(`[NotificationsProvider] unknown message type: ${JSON.stringify(data)}`)
}
})
} else {
sseService.disconnect()
}
onMount(() => {
addHandler((data: SSEMessage) => {
if (data.entity === 'reaction') {
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
} else {
console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`)
}
})
})
const markNotificationAsRead = async (notification: Notification) => {
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)
}
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 { createStore } from 'solid-js/store'
import { apiClient } from '../graphql/client/core'
import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
import { apiClient } from '../utils/apiClient'
import { useSession } from './session'
@ -16,9 +16,7 @@ const userpicUrl = (userpic: string) => {
}
const useProfileForm = () => {
const { session } = useSession()
const currentSlug = createMemo(() => session()?.user?.slug)
const { authorEntities } = useAuthorsStore({ authors: [] })
const currentAuthor = createMemo(() => authorEntities()[currentSlug()])
const currentAuthor = createMemo(() => session()?.author)
const [slugError, setSlugError] = createSignal<string>()
const submit = async (profile: ProfileInput) => {
@ -35,20 +33,20 @@ const useProfileForm = () => {
bio: '',
about: '',
slug: '',
userpic: '',
pic: '',
links: [],
})
createEffect(async () => {
if (!currentSlug()) return
if (!currentAuthor()) return
try {
await loadAuthor({ slug: currentSlug() })
await loadAuthor({ slug: currentAuthor().slug })
setForm({
name: currentAuthor()?.name,
slug: currentAuthor()?.slug,
bio: currentAuthor()?.bio,
about: currentAuthor()?.about,
userpic: userpicUrl(currentAuthor()?.userpic),
pic: userpicUrl(currentAuthor()?.pic),
links: currentAuthor()?.links,
})
} catch (error) {

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql`
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
}
}

View File

@ -2,7 +2,7 @@ import { gql } from '@urql/core'
export default gql`
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
message {
id

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import { ChatInput } from './../types.gen'
import { gql } from '@urql/core'
export default gql`
mutation UpdateChat($chat: ChatInput!) {
updateChat(chat: $chat) {
update_chat(chat: $chat) {
error
chat {
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`
mutation CreateShoutMutation($shout: ShoutInput!) {
createShout(inp: $shout) {
create_shout(inp: $shout) {
error
shout {
id

View File

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

View File

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

View File

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

View File

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

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