refactored.
This commit is contained in:
parent
9fb5e27906
commit
8cfbe8303c
|
@ -1,6 +1,6 @@
|
|||
node_modules
|
||||
public
|
||||
*.cjs
|
||||
src/graphql/*.gen.ts
|
||||
src/graphql/schema/*.gen.ts
|
||||
dist/
|
||||
.vercel/
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||
|
|
179
CHANGELOG.txt
179
CHANGELOG.txt
|
@ -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
|
54
codegen.yml
54
codegen.yml
|
@ -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
12013
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import type { Author } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Shout } from '../../graphql/types.gen'
|
||||
import type { Shout } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Shout } from '../../graphql/types.gen'
|
||||
import type { Shout } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Shout } from '../../graphql/types.gen'
|
||||
import type { Shout } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { For } from 'solid-js'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Chat } from '../../graphql/types.gen'
|
||||
import type { Chat } from '../../graphql/schema/core.gen'
|
||||
|
||||
import DialogCard from './DialogCard'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import type { Author } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
242
src/context/authorizer.tsx
Normal 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
76
src/context/connect.tsx
Normal 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)
|
|
@ -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 })
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
141
src/graphql/client/auth.ts
Normal 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
|
||||
},
|
||||
}
|
78
src/graphql/client/chat.ts
Normal file
78
src/graphql/client/chat.ts
Normal 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
209
src/graphql/client/core.ts
Normal 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
|
||||
},
|
||||
}
|
25
src/graphql/client/notifier.ts
Normal file
25
src/graphql/client/notifier.ts
Normal 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
16
src/graphql/error.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -1,9 +0,0 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
mutation CollabInviteMutation($author: String!, $slug: String!) {
|
||||
inviteAuthor(author: $author, shout: $slug) {
|
||||
error
|
||||
}
|
||||
}
|
||||
`
|
|
@ -1,9 +0,0 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
mutation CollabRemoveeMutation($author: String!, $slug: String!) {
|
||||
removeAuthor(author: $author, shout: $slug) {
|
||||
error
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
9
src/graphql/mutation/core/collab-accept.ts
Normal file
9
src/graphql/mutation/core/collab-accept.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
9
src/graphql/mutation/core/collab-invite.ts
Normal file
9
src/graphql/mutation/core/collab-invite.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
9
src/graphql/mutation/core/collab-reject.ts
Normal file
9
src/graphql/mutation/core/collab-reject.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
9
src/graphql/mutation/core/collab-remove-author.ts
Normal file
9
src/graphql/mutation/core/collab-remove-author.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
9
src/graphql/mutation/core/collab-remove-invite.ts
Normal file
9
src/graphql/mutation/core/collab-remove-invite.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
|||
|
||||
export default gql`
|
||||
mutation CommunityDestroyMutation($slug: String!) {
|
||||
deleteCommunity(slug: $slug) {
|
||||
delete_community(slug: $slug) {
|
||||
error
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue
Block a user