refactored.
This commit is contained in:
parent
9fb5e27906
commit
8cfbe8303c
|
@ -1,6 +1,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
public
|
public
|
||||||
*.cjs
|
*.cjs
|
||||||
src/graphql/*.gen.ts
|
src/graphql/schema/*.gen.ts
|
||||||
dist/
|
dist/
|
||||||
.vercel/
|
.vercel/
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,6 +11,7 @@ pnpm-debug.log*
|
||||||
.eslint/.eslintcache
|
.eslint/.eslintcache
|
||||||
public/upload/*
|
public/upload/*
|
||||||
src/graphql/introspec.gen.ts
|
src/graphql/introspec.gen.ts
|
||||||
|
src/graphql/schema/*.gen.ts
|
||||||
stats.html
|
stats.html
|
||||||
*.scss.d.ts
|
*.scss.d.ts
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
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
|
overwrite: true
|
||||||
schema: 'http://127.0.0.1:8080'
|
|
||||||
#schema: 'https://v2.discours.io'
|
|
||||||
generates:
|
generates:
|
||||||
src/graphql/introspec.gen.ts:
|
# Generate types for chat
|
||||||
plugins:
|
src/graphql/schema/chat.gen.ts:
|
||||||
- urql-introspection
|
schema: 'https://chat.discours.io'
|
||||||
config:
|
|
||||||
useTypeImports: true
|
|
||||||
includeScalars: true
|
|
||||||
includeEnums: true
|
|
||||||
src/graphql/types.gen.ts:
|
|
||||||
plugins:
|
plugins:
|
||||||
- 'typescript'
|
- 'typescript'
|
||||||
- 'typescript-operations'
|
- 'typescript-operations'
|
||||||
- 'typescript-urql'
|
- 'typescript-urql'
|
||||||
config:
|
config:
|
||||||
skipTypename: true
|
skipTypename: true
|
||||||
|
useTypeImports: true
|
||||||
|
outputPath: './src/graphql/types/chat.gen.ts'
|
||||||
|
|
||||||
|
# Generate types for core
|
||||||
|
src/graphql/schema/core.gen.ts:
|
||||||
|
schema: 'https://testapi.discours.io'
|
||||||
|
plugins:
|
||||||
|
- 'typescript'
|
||||||
|
- 'typescript-operations'
|
||||||
|
- 'typescript-urql'
|
||||||
|
config:
|
||||||
|
skipTypename: true
|
||||||
|
useTypeImports: true
|
||||||
|
outputPath: './src/graphql/types/core.gen.ts'
|
||||||
|
|
||||||
|
# Generate types for notifier
|
||||||
|
src/graphql/schema/notifier.gen.ts:
|
||||||
|
schema: 'http://notifier.discours.io' # FIXME: https
|
||||||
|
plugins:
|
||||||
|
- 'typescript'
|
||||||
|
- 'typescript-operations'
|
||||||
|
- 'typescript-urql'
|
||||||
|
config:
|
||||||
|
skipTypename: true
|
||||||
|
useTypeImports: true
|
||||||
|
outputPath: './src/graphql/types/notifier.gen.ts'
|
||||||
|
|
||||||
|
# Generate types for auth
|
||||||
|
src/graphql/schema/auth.gen.ts:
|
||||||
|
schema: 'https://auth.discours.io/graphql'
|
||||||
|
plugins:
|
||||||
|
- 'typescript'
|
||||||
|
- 'typescript-operations'
|
||||||
|
- 'typescript-urql'
|
||||||
|
config:
|
||||||
|
skipTypename: true
|
||||||
|
useTypeImports: true
|
||||||
|
outputPath: './src/graphql/types/auth.gen.ts'
|
||||||
|
|
||||||
hooks:
|
hooks:
|
||||||
afterAllFileWrite:
|
afterAllFileWrite:
|
||||||
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/types.gen.ts
|
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts
|
||||||
|
|
12009
package-lock.json
generated
12009
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "discoursio-webapp",
|
"name": "discoursio-webapp",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"fix": "npm run lint:code:fix && npm run lint:styles:fix",
|
"fix": "npm run lint:code:fix && npm run lint:styles:fix",
|
||||||
"format": "npx prettier \"{,!(node_modules)/**/}*.{js,ts,tsx,json,scss,css}\" --write --ignore-path .gitignore",
|
"format": "npx prettier \"{,!(node_modules)/**/}*.{js,ts,tsx,json,scss,css}\" --write --ignore-path .gitignore",
|
||||||
|
"postinstall": "npm run codegen",
|
||||||
"lint": "npm run lint:code && npm run lint:styles",
|
"lint": "npm run lint:code && npm run lint:styles",
|
||||||
"lint:code": "eslint .",
|
"lint:code": "eslint .",
|
||||||
"lint:code:fix": "eslint . --fix",
|
"lint:code:fix": "eslint . --fix",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"typecheck:watch": "tsc --noEmit --watch"
|
"typecheck:watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@authorizerdev/authorizer-js": "1.2.11",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { SearchPage } from '../pages/search.page'
|
||||||
import { TopicPage } from '../pages/topic.page'
|
import { TopicPage } from '../pages/topic.page'
|
||||||
import { ROUTES, useRouter } from '../stores/router'
|
import { ROUTES, useRouter } from '../stores/router'
|
||||||
import { hideModal, MODALS, showModal } from '../stores/ui'
|
import { hideModal, MODALS, showModal } from '../stores/ui'
|
||||||
|
import { ConnectProvider } from '../context/connect'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
||||||
|
@ -119,11 +120,13 @@ export const App = (props: Props) => {
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
|
<ConnectProvider>
|
||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<EditorProvider>
|
<EditorProvider>
|
||||||
<Dynamic component={pageComponent()} {...props} />
|
<Dynamic component={pageComponent()} {...props} />
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
|
</ConnectProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</ConfirmProvider>
|
</ConfirmProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
|
|
||||||
import { Topic } from '../../../graphql/types.gen'
|
import { Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Image } from '../../_shared/Image'
|
import { Image } from '../../_shared/Image'
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useLocalize } from '../../../context/localize'
|
||||||
import { useReactions } from '../../../context/reactions'
|
import { useReactions } from '../../../context/reactions'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { useSnackbar } from '../../../context/snackbar'
|
import { useSnackbar } from '../../../context/snackbar'
|
||||||
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen'
|
import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
||||||
|
@ -25,7 +25,7 @@ type Props = {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
isArticleAuthor?: boolean
|
isArticleAuthor?: boolean
|
||||||
sortedComments?: Reaction[]
|
sortedComments?: Reaction[]
|
||||||
lastSeen?: Date
|
lastSeen?: number
|
||||||
class?: string
|
class?: string
|
||||||
showArticleLink?: boolean
|
showArticleLink?: boolean
|
||||||
clickedReply?: (id: number) => void
|
clickedReply?: (id: number) => void
|
||||||
|
@ -52,7 +52,7 @@ export const Comment = (props: Props) => {
|
||||||
actions: { showSnackbar },
|
actions: { showSnackbar },
|
||||||
} = useSnackbar()
|
} = useSnackbar()
|
||||||
|
|
||||||
const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
|
const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === session()?.author?.slug)
|
||||||
|
|
||||||
const comment = createMemo(() => props.comment)
|
const comment = createMemo(() => props.comment)
|
||||||
const body = createMemo(() => (comment().body || '').trim())
|
const body = createMemo(() => (comment().body || '').trim())
|
||||||
|
@ -82,7 +82,7 @@ export const Comment = (props: Props) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
await createReaction({
|
await createReaction({
|
||||||
kind: ReactionKind.Comment,
|
kind: ReactionKind.Comment,
|
||||||
replyTo: props.comment.id,
|
reply_to: props.comment.id,
|
||||||
body: value,
|
body: value,
|
||||||
shout: props.comment.shout.id,
|
shout: props.comment.shout.id,
|
||||||
})
|
})
|
||||||
|
@ -114,13 +114,11 @@ export const Comment = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = new Date(comment()?.createdAt)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
id={`comment_${comment().id}`}
|
id={`comment_${comment().id}`}
|
||||||
class={clsx(styles.comment, props.class, {
|
class={clsx(styles.comment, props.class, {
|
||||||
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen,
|
[styles.isNew]: !isCommentAuthor() && comment()?.created_at > props.lastSeen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Show when={!!body()}>
|
<Show when={!!body()}>
|
||||||
|
@ -130,8 +128,8 @@ export const Comment = (props: Props) => {
|
||||||
fallback={
|
fallback={
|
||||||
<div>
|
<div>
|
||||||
<Userpic
|
<Userpic
|
||||||
name={comment().createdBy.name}
|
name={comment().created_by.name}
|
||||||
userpic={comment().createdBy.userpic}
|
userpic={comment().created_by.pic}
|
||||||
class={clsx({
|
class={clsx({
|
||||||
[styles.compactUserpic]: props.compact,
|
[styles.compactUserpic]: props.compact,
|
||||||
})}
|
})}
|
||||||
|
@ -144,7 +142,7 @@ export const Comment = (props: Props) => {
|
||||||
>
|
>
|
||||||
<div class={styles.commentDetails}>
|
<div class={styles.commentDetails}>
|
||||||
<div class={styles.commentAuthor}>
|
<div class={styles.commentAuthor}>
|
||||||
<AuthorLink author={comment()?.createdBy as Author} />
|
<AuthorLink author={comment()?.created_by as Author} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.isArticleAuthor}>
|
<Show when={props.isArticleAuthor}>
|
||||||
|
@ -253,7 +251,7 @@ export const Comment = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.sortedComments}>
|
<Show when={props.sortedComments}>
|
||||||
<ul>
|
<ul>
|
||||||
<For each={props.sortedComments.filter((r) => r.replyTo === props.comment.id)}>
|
<For each={props.sortedComments.filter((r) => r.reply_to === props.comment.id)}>
|
||||||
{(c) => (
|
{(c) => (
|
||||||
<Comment
|
<Comment
|
||||||
sortedComments={props.sortedComments}
|
sortedComments={props.sortedComments}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Reaction } from '../../../graphql/types.gen'
|
import type { Reaction } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
@ -33,12 +33,12 @@ export const CommentDate = (props: Props) => {
|
||||||
[styles.showOnHover]: props.showOnHover,
|
[styles.showOnHover]: props.showOnHover,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>
|
<time class={styles.date}>{formattedDate(props.comment.created_at * 1000)}</time>
|
||||||
<Show when={props.comment.updatedAt}>
|
<Show when={props.comment.updated_at}>
|
||||||
<time class={styles.date}>
|
<time class={styles.date}>
|
||||||
<Icon name="edit" class={styles.icon} />
|
<Icon name="edit" class={styles.icon} />
|
||||||
<span class={styles.text}>
|
<span class={styles.text}>
|
||||||
{t('Edited')} {formattedDate(props.comment.updatedAt)}
|
{t('Edited')} {formattedDate(props.comment.updated_at * 1000)}
|
||||||
</span>
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import type { Reaction } from '../../graphql/types.gen'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
|
@ -7,7 +5,7 @@ import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
import { ReactionKind } from '../../graphql/types.gen'
|
import { Reaction, ReactionKind } from '../../graphql/schema/core.gen'
|
||||||
import { loadShout } from '../../stores/zine/articles'
|
import { loadShout } from '../../stores/zine/articles'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
import { VotersList } from '../_shared/VotersList'
|
import { VotersList } from '../_shared/VotersList'
|
||||||
|
@ -33,20 +31,20 @@ export const CommentRatingControl = (props: Props) => {
|
||||||
Object.values(reactionEntities).some(
|
Object.values(reactionEntities).some(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.createdBy.slug === user()?.slug &&
|
r.created_by.slug === user()?.slug &&
|
||||||
r.shout.id === props.comment.shout.id &&
|
r.shout.id === props.comment.shout.id &&
|
||||||
r.replyTo === props.comment.id,
|
r.reply_to === props.comment.id,
|
||||||
)
|
)
|
||||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||||
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||||
const canVote = createMemo(() => user()?.slug !== props.comment.createdBy.slug)
|
const canVote = createMemo(() => user()?.slug !== props.comment.created_by.slug)
|
||||||
|
|
||||||
const commentRatingReactions = createMemo(() =>
|
const commentRatingReactions = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter(
|
Object.values(reactionEntities).filter(
|
||||||
(r) =>
|
(r) =>
|
||||||
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
||||||
r.shout.id === props.comment.shout.id &&
|
r.shout.id === props.comment.shout.id &&
|
||||||
r.replyTo === props.comment.id,
|
r.reply_to === props.comment.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,9 +52,9 @@ export const CommentRatingControl = (props: Props) => {
|
||||||
const reactionToDelete = Object.values(reactionEntities).find(
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.createdBy.slug === user()?.slug &&
|
r.created_by.slug === user()?.slug &&
|
||||||
r.shout.id === props.comment.shout.id &&
|
r.shout.id === props.comment.shout.id &&
|
||||||
r.replyTo === props.comment.id,
|
r.reply_to === props.comment.id,
|
||||||
)
|
)
|
||||||
return deleteReaction(reactionToDelete.id)
|
return deleteReaction(reactionToDelete.id)
|
||||||
}
|
}
|
||||||
|
@ -71,7 +69,7 @@ export const CommentRatingControl = (props: Props) => {
|
||||||
await createReaction({
|
await createReaction({
|
||||||
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||||
shout: props.comment.shout.id,
|
shout: props.comment.shout.id,
|
||||||
replyTo: props.comment.id,
|
reply_to: props.comment.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Show, createMemo, createSignal, onMount, For, lazy } from 'solid-js'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
import { Author, Reaction, ReactionKind } from '../../graphql/schema/core.gen'
|
||||||
import { byCreated } from '../../utils/sortby'
|
import { byCreated } from '../../utils/sortby'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||||
|
@ -18,7 +18,7 @@ const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
|
||||||
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
|
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
|
||||||
|
|
||||||
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
||||||
if (a.replyTo && b.replyTo) {
|
if (a.reply_to && b.reply_to) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,19 +76,19 @@ export const CommentsTree = (props: Props) => {
|
||||||
return newSortedComments
|
return newSortedComments
|
||||||
})
|
})
|
||||||
|
|
||||||
const dateFromLocalStorage = new Date(localStorage.getItem(`${props.shoutSlug}`))
|
const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`))
|
||||||
const currentDate = new Date()
|
const currentDate = new Date()
|
||||||
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
|
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!dateFromLocalStorage) {
|
if (!dateFromLocalStorage) {
|
||||||
setCookie()
|
setCookie()
|
||||||
} else if (currentDate > dateFromLocalStorage) {
|
} else if (currentDate.getTime() > dateFromLocalStorage) {
|
||||||
const newComments = comments().filter((c) => {
|
const newComments = comments().filter((c) => {
|
||||||
if (c.replyTo || c.createdBy.slug === session()?.user.slug) {
|
if (c.reply_to || c.created_by.slug === session()?.user.slug) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const created = new Date(c.createdAt)
|
const created = c.created_at
|
||||||
return created > dateFromLocalStorage
|
return created > dateFromLocalStorage
|
||||||
})
|
})
|
||||||
setNewReactions(newComments)
|
setNewReactions(newComments)
|
||||||
|
@ -153,12 +153,12 @@ export const CommentsTree = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ul class={styles.comments}>
|
<ul class={styles.comments}>
|
||||||
<For each={sortedComments().filter((r) => !r.replyTo)}>
|
<For each={sortedComments().filter((r) => !r.reply_to)}>
|
||||||
{(reaction) => (
|
{(reaction) => (
|
||||||
<Comment
|
<Comment
|
||||||
sortedComments={sortedComments()}
|
sortedComments={sortedComments()}
|
||||||
isArticleAuthor={Boolean(
|
isArticleAuthor={Boolean(
|
||||||
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug),
|
props.articleAuthors.some((a) => a.slug === reaction.created_by.slug),
|
||||||
)}
|
)}
|
||||||
comment={reaction}
|
comment={reaction}
|
||||||
clickedReply={(id) => setClickedReplyId(id)}
|
clickedReply={(id) => setClickedReplyId(id)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { createPopper } from '@popperjs/core'
|
import { createPopper } from '@popperjs/core'
|
||||||
|
@ -68,13 +68,9 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.created_at * 1000)))
|
||||||
|
|
||||||
const mainTopic = createMemo(
|
const mainTopic = createMemo(() => (props.article.topics.length > 0 ? props.article.topics[0] : null))
|
||||||
() =>
|
|
||||||
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
|
|
||||||
props.article.topics[0],
|
|
||||||
)
|
|
||||||
|
|
||||||
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
|
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
|
||||||
|
|
||||||
|
@ -293,10 +289,10 @@ export const FullArticle = (props: Props) => {
|
||||||
onClick={handleArticleBodyClick}
|
onClick={handleArticleBodyClick}
|
||||||
>
|
>
|
||||||
{/*TODO: Check styles.shoutTopic*/}
|
{/*TODO: Check styles.shoutTopic*/}
|
||||||
<Show when={props.article.layout !== 'music'}>
|
<Show when={props.article.layout !== 'audio'}>
|
||||||
<div class={styles.shoutHeader}>
|
<div class={styles.shoutHeader}>
|
||||||
<Show when={mainTopic()}>
|
<Show when={mainTopic()}>
|
||||||
<CardTopic title={mainTopic().title} slug={props.article.mainTopic} />
|
<CardTopic title={mainTopic().title} slug={mainTopic().slug} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<h1>{props.article.title}</h1>
|
<h1>{props.article.title}</h1>
|
||||||
|
@ -328,7 +324,7 @@ export const FullArticle = (props: Props) => {
|
||||||
<Show when={props.article.lead}>
|
<Show when={props.article.lead}>
|
||||||
<section class={styles.lead} innerHTML={props.article.lead} />
|
<section class={styles.lead} innerHTML={props.article.lead} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.article.layout === 'music'}>
|
<Show when={props.article.layout === 'audio'}>
|
||||||
<AudioHeader
|
<AudioHeader
|
||||||
title={props.article.title}
|
title={props.article.title}
|
||||||
cover={props.article.cover}
|
cover={props.article.cover}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { createMemo, Show } from 'solid-js'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { ReactionKind, Shout } from '../../graphql/types.gen'
|
import { ReactionKind, Shout } from '../../graphql/schema/core.gen'
|
||||||
import { loadShout } from '../../stores/zine/articles'
|
import { loadShout } from '../../stores/zine/articles'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
|
@ -33,9 +33,9 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
Object.values(reactionEntities).some(
|
Object.values(reactionEntities).some(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.createdBy.slug === user()?.slug &&
|
r.created_by.slug === user()?.slug &&
|
||||||
r.shout.id === props.shout.id &&
|
r.shout.id === props.shout.id &&
|
||||||
!r.replyTo,
|
!r.reply_to,
|
||||||
)
|
)
|
||||||
|
|
||||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||||
|
@ -47,7 +47,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
(r) =>
|
(r) =>
|
||||||
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
||||||
r.shout.id === props.shout.id &&
|
r.shout.id === props.shout.id &&
|
||||||
!r.replyTo,
|
!r.reply_to,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,9 +55,9 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
const reactionToDelete = Object.values(reactionEntities).find(
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.createdBy.slug === user()?.slug &&
|
r.created_by.slug === user()?.slug &&
|
||||||
r.shout.id === props.shout.id &&
|
r.shout.id === props.shout.id &&
|
||||||
!r.replyTo,
|
!r.reply_to,
|
||||||
)
|
)
|
||||||
return deleteReaction(reactionToDelete.id)
|
return deleteReaction(reactionToDelete.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
import { Author } from '../../../graphql/types.gen'
|
import { Author } from '../../../graphql/schema/core.gen'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
|
|
||||||
import styles from './AhtorLink.module.scss'
|
import styles from './AhtorLink.module.scss'
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Author, FollowingEntity } from '../../../graphql/types.gen'
|
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
|
@ -67,7 +67,7 @@ export const AuthorBadge = (props: Props) => {
|
||||||
hasLink={true}
|
hasLink={true}
|
||||||
size={'M'}
|
size={'M'}
|
||||||
name={props.author.name}
|
name={props.author.name}
|
||||||
userpic={props.author.userpic}
|
userpic={props.author.pic}
|
||||||
slug={props.author.slug}
|
slug={props.author.slug}
|
||||||
/>
|
/>
|
||||||
<a href={`/author/${props.author.slug}`} class={styles.info}>
|
<a href={`/author/${props.author.slug}`} class={styles.info}>
|
||||||
|
@ -78,7 +78,9 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<Switch
|
<Switch
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.bio}>
|
<div class={styles.bio}>
|
||||||
{t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })}
|
{t('Registered since {date}', {
|
||||||
|
date: formatDate(new Date(props.author.created_at * 1000)),
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author } from '../../../graphql/types.gen'
|
import type { Author } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { openPage, redirectPage } from '@nanostores/router'
|
import { openPage, redirectPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -6,7 +6,7 @@ import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { SubscriptionFilter } from '../../../pages/types'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -53,7 +53,7 @@ export const Draft = (props: Props) => {
|
||||||
<div class={clsx(props.class)}>
|
<div class={clsx(props.class)}>
|
||||||
<div class={styles.created}>
|
<div class={styles.created}>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />{' '}
|
<Icon name="pencil-outline" class={styles.icon} />{' '}
|
||||||
{formatDate(new Date(props.shout.createdAt), { hour: '2-digit', minute: '2-digit' })}
|
{formatDate(new Date(props.shout.created_at * 1000), { hour: '2-digit', minute: '2-digit' })}
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.titleContainer}>
|
<div class={styles.titleContainer}>
|
||||||
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}
|
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Topic } from '../../../graphql/types.gen'
|
import type { Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createOptions, Select } from '@thisbeyond/solid-select'
|
import { createOptions, Select } from '@thisbeyond/solid-select'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../../graphql/types.gen'
|
import type { Shout } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath, openPage } from '@nanostores/router'
|
import { getPagePath, openPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -86,12 +86,10 @@ const getTitleAndSubtitle = (
|
||||||
export const ArticleCard = (props: ArticleCardProps) => {
|
export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
const { t, lang, formatDate } = useLocalize()
|
const { t, lang, formatDate } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
const mainTopic =
|
const mainTopic = props.article.topics[0]
|
||||||
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
|
|
||||||
props.article.topics[0]
|
|
||||||
|
|
||||||
const formattedDate = createMemo<string>(() => {
|
const formattedDate = createMemo<string>(() => {
|
||||||
return formatDate(new Date(props.article.createdAt))
|
return formatDate(new Date(props.article.created_at * 1000))
|
||||||
})
|
})
|
||||||
|
|
||||||
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// TODO: additional entities list column + article
|
// TODO: additional entities list column + article
|
||||||
|
|
||||||
import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
|
import type { Author, Shout, Topic, User } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
import { For, Show } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createComputed, createSignal, Show, For } from 'solid-js'
|
import { createComputed, createSignal, Show, For } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
import { For, Show } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { ArticleCard } from './ArticleCard'
|
import { ArticleCard } from './ArticleCard'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createSignal, For, createEffect } from 'solid-js'
|
import { createSignal, For, createEffect } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ChatMember } from '../../graphql/types.gen'
|
import type { ChatMember } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, Switch, Match, createMemo } from 'solid-js'
|
import { Show, Switch, Match, createMemo } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Chat } from '../../graphql/types.gen'
|
import type { Chat } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import DialogCard from './DialogCard'
|
import DialogCard from './DialogCard'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ChatMember } from '../../graphql/types.gen'
|
import type { ChatMember } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
|
import type { Message as MessageType, ChatMember } from '../../graphql/schema/chat.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
import { ApiError } from '../../../graphql/error'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
|
||||||
|
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { clsx } from 'clsx'
|
||||||
import { createSignal, JSX, Show } from 'solid-js'
|
import { createSignal, JSX, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { ApiError } from '../../../graphql/error'
|
||||||
import { signSendLink } from '../../../stores/auth'
|
import { signSendLink } from '../../../stores/auth'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
|
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { createSignal, Show } from 'solid-js'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { useSnackbar } from '../../../context/snackbar'
|
import { useSnackbar } from '../../../context/snackbar'
|
||||||
|
import { ApiError } from '../../../graphql/error'
|
||||||
import { signSendLink } from '../../../stores/auth'
|
import { signSendLink } from '../../../stores/auth'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { clsx } from 'clsx'
|
||||||
import { Show, createSignal } from 'solid-js'
|
import { Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { ApiError } from '../../../graphql/error'
|
||||||
import { register } from '../../../stores/auth'
|
import { register } from '../../../stores/auth'
|
||||||
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Topic } from '../../../graphql/types.gen'
|
import type { Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath, redirectPage } from '@nanostores/router'
|
import { getPagePath, redirectPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -6,9 +6,9 @@ import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { router, ROUTES, useRouter } from '../../../stores/router'
|
import { router, ROUTES, useRouter } from '../../../stores/router'
|
||||||
import { useModalStore } from '../../../stores/ui'
|
import { useModalStore } from '../../../stores/ui'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Subscribe } from '../../_shared/Subscribe'
|
import { Subscribe } from '../../_shared/Subscribe'
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Notification } from '../../../graphql/types.gen'
|
|
||||||
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||||
|
|
||||||
import { getPagePath, openPage } from '@nanostores/router'
|
import { getPagePath, openPage } from '@nanostores/router'
|
||||||
|
@ -7,12 +6,13 @@ import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useNotifications } from '../../../context/notifications'
|
import { useNotifications } from '../../../context/notifications'
|
||||||
import { NotificationType } from '../../../graphql/types.gen'
|
import { Notification } from '../../../graphql/schema/notifier.gen'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { GroupAvatar } from '../../_shared/GroupAvatar'
|
import { GroupAvatar } from '../../_shared/GroupAvatar'
|
||||||
import { TimeAgo } from '../../_shared/TimeAgo'
|
import { TimeAgo } from '../../_shared/TimeAgo'
|
||||||
|
|
||||||
import styles from './NotificationView.module.scss'
|
import styles from './NotificationView.module.scss'
|
||||||
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
import { Reaction, Shout } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notification: Notification
|
notification: Notification
|
||||||
|
@ -21,22 +21,6 @@ type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationUser = {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
slug: string
|
|
||||||
userpic: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationData = {
|
|
||||||
shout: {
|
|
||||||
slug: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
users: NotificationUser[]
|
|
||||||
reactionIds: number[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationView = (props: Props) => {
|
export const NotificationView = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
actions: { markNotificationAsRead, hideNotificationsPanel },
|
actions: { markNotificationAsRead, hideNotificationsPanel },
|
||||||
|
@ -46,19 +30,13 @@ export const NotificationView = (props: Props) => {
|
||||||
|
|
||||||
const { t, formatDate, formatTime } = useLocalize()
|
const { t, formatDate, formatTime } = useLocalize()
|
||||||
|
|
||||||
const [data, setData] = createSignal<NotificationData>(null)
|
const [data, setData] = createSignal<Reaction>(null) // NOTE: supports only SSMessage.entity == "reaction"
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setTimeout(() => setData(JSON.parse(props.notification.data)))
|
setTimeout(() => setData(JSON.parse(props.notification.payload)))
|
||||||
})
|
})
|
||||||
|
|
||||||
const lastUser = createMemo(() => {
|
const lastUser = createMemo(() => data().created_by)
|
||||||
if (!data()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return data().users[data().users.length - 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleLinkClick = (event: MouseEvent) => {
|
const handleLinkClick = (event: MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -87,33 +65,18 @@ export const NotificationView = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.notification.type) {
|
switch (props.notification.action) {
|
||||||
case NotificationType.NewComment: {
|
case 'create': {
|
||||||
return (
|
if (data()?.reply_to) {
|
||||||
<>
|
|
||||||
{t('NotificationNewCommentText1', {
|
|
||||||
commentsCount: props.notification.occurrences,
|
|
||||||
})}{' '}
|
|
||||||
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
|
|
||||||
{shoutTitle}
|
|
||||||
</a>{' '}
|
|
||||||
{t('NotificationNewCommentText2')}{' '}
|
|
||||||
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
|
|
||||||
{lastUser().name}
|
|
||||||
</a>{' '}
|
|
||||||
{t('NotificationNewCommentText3', {
|
|
||||||
restUsersCount: data().users.length - 1,
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case NotificationType.NewReply: {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{t('NotificationNewReplyText1', {
|
{t('NotificationNewReplyText1', {
|
||||||
commentsCount: props.notification.occurrences,
|
commentsCount: 0, // FIXME: props.notification.occurrences,
|
||||||
})}{' '}
|
})}{' '}
|
||||||
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
|
<a
|
||||||
|
href={getPagePath(router, 'article', { slug: data().shout.slug })}
|
||||||
|
onClick={handleLinkClick}
|
||||||
|
>
|
||||||
{shoutTitle}
|
{shoutTitle}
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
{t('NotificationNewReplyText2')}{' '}
|
{t('NotificationNewReplyText2')}{' '}
|
||||||
|
@ -121,12 +84,47 @@ export const NotificationView = (props: Props) => {
|
||||||
{lastUser().name}
|
{lastUser().name}
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
{t('NotificationNewReplyText3', {
|
{t('NotificationNewReplyText3', {
|
||||||
restUsersCount: data().users.length - 1,
|
restUsersCount: 0, // FIXME: data().users.length - 1,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{t('NotificationNewCommentText1', {
|
||||||
|
commentsCount: 0, // FIXME: props.notification.occurrences,
|
||||||
|
})}{' '}
|
||||||
|
<a
|
||||||
|
href={getPagePath(router, 'article', { slug: data().shout.slug })}
|
||||||
|
onClick={handleLinkClick}
|
||||||
|
>
|
||||||
|
{shoutTitle}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewCommentText2')}{' '}
|
||||||
|
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
|
||||||
|
{lastUser().name}
|
||||||
|
</a>{' '}
|
||||||
|
{t('NotificationNewCommentText3', {
|
||||||
|
restUsersCount: 0, // FIXME: data().users.length - 1,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'update': {
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
}
|
||||||
|
case 'follow': {
|
||||||
|
}
|
||||||
|
case 'unfollow': {
|
||||||
|
}
|
||||||
|
case 'invited': {
|
||||||
|
// TODO: invited for collaborative authoring
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
|
@ -137,22 +135,22 @@ export const NotificationView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
openPage(router, 'article', { slug: data().shout.slug })
|
openPage(router, 'article', { slug: data().shout.slug })
|
||||||
|
// FIXME:
|
||||||
if (data().reactionIds) {
|
// if (data().reactionIds) {
|
||||||
changeSearchParam({ commentId: data().reactionIds[0].toString() })
|
// changeSearchParam({ commentId: data().reactionIds[0].toString() })
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedDateTime = createMemo(() => {
|
const formattedDateTime = createMemo(() => {
|
||||||
switch (props.dateTimeFormat) {
|
switch (props.dateTimeFormat) {
|
||||||
case 'ago': {
|
case 'ago': {
|
||||||
return <TimeAgo date={props.notification.createdAt} />
|
return <TimeAgo date={props.notification.created_at} />
|
||||||
}
|
}
|
||||||
case 'time': {
|
case 'time': {
|
||||||
return formatTime(new Date(props.notification.createdAt))
|
return formatTime(new Date(props.notification.created_at))
|
||||||
}
|
}
|
||||||
case 'date': {
|
case 'date': {
|
||||||
return formatDate(new Date(props.notification.createdAt), { month: 'numeric', year: '2-digit' })
|
return formatDate(new Date(props.notification.created_at), { month: 'numeric', year: '2-digit' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -166,7 +164,7 @@ export const NotificationView = (props: Props) => {
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div class={styles.userpic}>
|
<div class={styles.userpic}>
|
||||||
<GroupAvatar authors={data().users} />
|
<GroupAvatar authors={[] /*d FIXME: data().users */} />
|
||||||
</div>
|
</div>
|
||||||
<div>{content()}</div>
|
<div>{content()}</div>
|
||||||
<div class={styles.timeContainer}>{formattedDateTime()}</div>
|
<div class={styles.timeContainer}>{formattedDateTime()}</div>
|
||||||
|
|
|
@ -95,15 +95,19 @@ export const NotificationsPanel = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayNotifications = createMemo(() => {
|
const todayNotifications = createMemo(() => {
|
||||||
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt)))
|
return sortedNotifications().filter((notification) => isToday(new Date(notification.created_at * 1000)))
|
||||||
})
|
})
|
||||||
|
|
||||||
const yesterdayNotifications = createMemo(() => {
|
const yesterdayNotifications = createMemo(() => {
|
||||||
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
|
return sortedNotifications().filter((notification) =>
|
||||||
|
isYesterday(new Date(notification.created_at * 1000)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const earlierNotifications = createMemo(() => {
|
const earlierNotifications = createMemo(() => {
|
||||||
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt)))
|
return sortedNotifications().filter((notification) =>
|
||||||
|
isEarlier(new Date(notification.created_at * 1000)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
|
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo, createSignal, Show } from 'solid-js'
|
import { createMemo, createSignal, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo, Show } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createMemo, createSignal, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import type { Author, Shout, Topic } from '../../../graphql/types.gen'
|
import type { Author, Shout, Topic } from '../../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
|
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { useAuthorsStore } from '../../../stores/zine/authors'
|
import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
|
||||||
import { splitToPages } from '../../../utils/splitToPages'
|
import { splitToPages } from '../../../utils/splitToPages'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
|
@ -118,7 +118,7 @@ export const AuthorView = (props: Props) => {
|
||||||
if (getPage().route === 'authorComments') {
|
if (getPage().route === 'authorComments') {
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
by: { comment: true, createdBy: props.authorSlug },
|
by: { comment: true, created_by: props.author.id },
|
||||||
})
|
})
|
||||||
setCommented(data)
|
setCommented(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { createSignal, For, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useEditorContext } from '../../../context/editor'
|
import { useEditorContext } from '../../../context/editor'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Shout } from '../../../graphql/types.gen'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
import { Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
|
||||||
import { Draft } from '../../Draft'
|
import { Draft } from '../../Draft'
|
||||||
|
|
||||||
import styles from './DraftsView.module.scss'
|
import styles from './DraftsView.module.scss'
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import deepEqual from 'fast-deep-equal'
|
import deepEqual from 'fast-deep-equal'
|
||||||
import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
|
import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
@ -7,6 +5,7 @@ import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { ShoutForm, useEditorContext } from '../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../context/editor'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { ShoutVisibility, type Shout, type Topic } from '../../graphql/schema/core.gen'
|
||||||
import { LayoutType, MediaItem } from '../../pages/types'
|
import { LayoutType, MediaItem } from '../../pages/types'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { clone } from '../../utils/clone'
|
import { clone } from '../../utils/clone'
|
||||||
|
@ -75,7 +74,7 @@ export const EditView = (props: Props) => {
|
||||||
description: props.shout.description,
|
description: props.shout.description,
|
||||||
subtitle: props.shout.subtitle,
|
subtitle: props.shout.subtitle,
|
||||||
selectedTopics: shoutTopics,
|
selectedTopics: shoutTopics,
|
||||||
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
|
mainTopic: shoutTopics[0],
|
||||||
body: props.shout.body,
|
body: props.shout.body,
|
||||||
coverImageUrl: props.shout.cover,
|
coverImageUrl: props.shout.cover,
|
||||||
media: props.shout.media,
|
media: props.shout.media,
|
||||||
|
@ -163,7 +162,7 @@ export const EditView = (props: Props) => {
|
||||||
|
|
||||||
const articleTitle = () => {
|
const articleTitle = () => {
|
||||||
switch (props.shout.layout as LayoutType) {
|
switch (props.shout.layout as LayoutType) {
|
||||||
case 'music': {
|
case 'audio': {
|
||||||
return t('Album name')
|
return t('Album name')
|
||||||
}
|
}
|
||||||
case 'image': {
|
case 'image': {
|
||||||
|
@ -182,7 +181,7 @@ export const EditView = (props: Props) => {
|
||||||
const hasChanges = !deepEqual(form, prevForm)
|
const hasChanges = !deepEqual(form, prevForm)
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
if (props.shout.visibility === 'owner') {
|
if (props.shout.visibility === ShoutVisibility.Authors) {
|
||||||
await saveDraft(form)
|
await saveDraft(form)
|
||||||
} else {
|
} else {
|
||||||
saveDraftToLocalStorage(form)
|
saveDraftToLocalStorage(form)
|
||||||
|
@ -243,19 +242,19 @@ export const EditView = (props: Props) => {
|
||||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||||
<Show when={page().route === 'edit'}>
|
<Show when={page().route === 'edit'}>
|
||||||
<div class={styles.headingActions}>
|
<div class={styles.headingActions}>
|
||||||
<Show when={!isSubtitleVisible() && props.shout.layout !== 'music'}>
|
<Show when={!isSubtitleVisible() && props.shout.layout !== 'audio'}>
|
||||||
<div class={styles.action} onClick={showSubtitleInput}>
|
<div class={styles.action} onClick={showSubtitleInput}>
|
||||||
{t('Add subtitle')}
|
{t('Add subtitle')}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!isLeadVisible() && props.shout.layout !== 'music'}>
|
<Show when={!isLeadVisible() && props.shout.layout !== 'audio'}>
|
||||||
<div class={styles.action} onClick={showLeadInput}>
|
<div class={styles.action} onClick={showLeadInput}>
|
||||||
{t('Add intro')}
|
{t('Add intro')}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<>
|
<>
|
||||||
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'music' })}>
|
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}>
|
||||||
<div class={styles.inputContainer}>
|
<div class={styles.inputContainer}>
|
||||||
<GrowingTextarea
|
<GrowingTextarea
|
||||||
allowEnterKey={true}
|
allowEnterKey={true}
|
||||||
|
@ -270,7 +269,7 @@ export const EditView = (props: Props) => {
|
||||||
<div class={styles.validationError}>{formErrors.title}</div>
|
<div class={styles.validationError}>{formErrors.title}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.shout.layout === 'music'}>
|
<Show when={props.shout.layout === 'audio'}>
|
||||||
<div class={styles.additional}>
|
<div class={styles.additional}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -298,7 +297,7 @@ export const EditView = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.shout.layout !== 'music'}>
|
<Show when={props.shout.layout !== 'audio'}>
|
||||||
<Show when={isSubtitleVisible()}>
|
<Show when={isSubtitleVisible()}>
|
||||||
<GrowingTextarea
|
<GrowingTextarea
|
||||||
textAreaRef={(el) => {
|
textAreaRef={(el) => {
|
||||||
|
@ -324,7 +323,7 @@ export const EditView = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.shout.layout === 'music'}>
|
<Show when={props.shout.layout === 'audio'}>
|
||||||
<Show
|
<Show
|
||||||
when={form.coverImageUrl}
|
when={form.coverImageUrl}
|
||||||
fallback={
|
fallback={
|
||||||
|
@ -387,7 +386,7 @@ export const EditView = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.shout.layout === 'music'}>
|
<Show when={props.shout.layout === 'audio'}>
|
||||||
<AudioUploader
|
<AudioUploader
|
||||||
audio={mediaItems()}
|
audio={mediaItems()}
|
||||||
baseFields={baseAudioFields()}
|
baseFields={baseAudioFields()}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { clsx } from 'clsx'
|
||||||
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { LoadShoutsOptions, Shout } from '../../../graphql/types.gen'
|
import { LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { LayoutType } from '../../../pages/types'
|
import { LayoutType } from '../../../pages/types'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
|
@ -39,7 +39,9 @@ export const Expo = (props: Props) => {
|
||||||
offset: sortedArticles().length,
|
offset: sortedArticles().length,
|
||||||
}
|
}
|
||||||
|
|
||||||
options.filters = getLayout() ? { layout: getLayout() } : { exclude_layout: 'article' }
|
options.filters = getLayout()
|
||||||
|
? { layouts: [getLayout()] }
|
||||||
|
: { layouts: ['audio', 'video', 'image', 'literature'] }
|
||||||
|
|
||||||
const { hasMore } = await loadShouts(options)
|
const { hasMore } = await loadShouts(options)
|
||||||
setIsLoadMoreButtonVisible(hasMore)
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
|
@ -107,11 +109,11 @@ export const Expo = (props: Props) => {
|
||||||
<span class={clsx('linkReplacement')}>{t('Literature')}</span>
|
<span class={clsx('linkReplacement')}>{t('Literature')}</span>
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
</li>
|
</li>
|
||||||
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'music' })}>
|
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'audio' })}>
|
||||||
<ConditionalWrapper
|
<ConditionalWrapper
|
||||||
condition={getLayout() !== 'music'}
|
condition={getLayout() !== 'audio'}
|
||||||
wrapper={(children) => (
|
wrapper={(children) => (
|
||||||
<a href={getPagePath(router, 'expoLayout', { layout: 'music' })}>{children}</a>
|
<a href={getPagePath(router, 'expoLayout', { layout: 'audio' })}>{children}</a>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span class={clsx('linkReplacement')}>{t('Music')}</span>
|
<span class={clsx('linkReplacement')}>{t('Music')}</span>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/types.gen'
|
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -209,7 +209,7 @@ export const FeedView = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.commentDetails}>
|
<div class={styles.commentDetails}>
|
||||||
<AuthorLink author={comment.createdBy as Author} size={'XS'} />
|
<AuthorLink author={comment.created_by as Author} size={'XS'} />
|
||||||
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
|
import type { Chat, Message as MessageType } from '../../graphql/schema/chat.gen'
|
||||||
|
import type { Author } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
|
import { clsx } from 'clsx'
|
||||||
import { For, createSignal, Show, onMount, createEffect, createMemo, on } from 'solid-js'
|
import { For, createSignal, Show, onMount, createEffect, createMemo, on } from 'solid-js'
|
||||||
import type { Author, Chat, Message as MessageType } from '../../graphql/types.gen'
|
|
||||||
import DialogCard from '../Inbox/DialogCard'
|
import { useInbox } from '../../context/inbox'
|
||||||
import Search from '../Inbox/Search'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Message } from '../Inbox/Message'
|
|
||||||
import CreateModalContent from '../Inbox/CreateModalContent'
|
|
||||||
import DialogHeader from '../Inbox/DialogHeader'
|
|
||||||
import MessagesFallback from '../Inbox/MessagesFallback'
|
|
||||||
import QuotedMessage from '../Inbox/QuotedMessage'
|
|
||||||
import { Icon } from '../_shared/Icon'
|
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { loadRecipients } from '../../stores/inbox'
|
import { loadRecipients } from '../../stores/inbox'
|
||||||
|
|
||||||
import { Modal } from '../Nav/Modal'
|
|
||||||
import { showModal } from '../../stores/ui'
|
|
||||||
import { useInbox } from '../../context/inbox'
|
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { clsx } from 'clsx'
|
import { showModal } from '../../stores/ui'
|
||||||
import styles from '../../styles/Inbox.module.scss'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { useLocalize } from '../../context/localize'
|
|
||||||
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
|
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
||||||
|
import CreateModalContent from '../Inbox/CreateModalContent'
|
||||||
|
import DialogCard from '../Inbox/DialogCard'
|
||||||
|
import DialogHeader from '../Inbox/DialogHeader'
|
||||||
|
import { Message } from '../Inbox/Message'
|
||||||
|
import MessagesFallback from '../Inbox/MessagesFallback'
|
||||||
|
import QuotedMessage from '../Inbox/QuotedMessage'
|
||||||
|
import Search from '../Inbox/Search'
|
||||||
|
import { Modal } from '../Nav/Modal'
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
|
import styles from '../../styles/Inbox.module.scss'
|
||||||
|
|
||||||
type InboxSearchParams = {
|
type InboxSearchParams = {
|
||||||
initChat: string
|
initChat: string
|
||||||
|
@ -41,7 +42,7 @@ export const InboxView = () => {
|
||||||
const {
|
const {
|
||||||
chats,
|
chats,
|
||||||
messages,
|
messages,
|
||||||
actions: { loadChats, getMessages, sendMessage, createChat }
|
actions: { loadChats, getMessages, sendMessage, createChat },
|
||||||
} = useInbox()
|
} = useInbox()
|
||||||
|
|
||||||
const [recipients, setRecipients] = createSignal<Author[]>([])
|
const [recipients, setRecipients] = createSignal<Author[]>([])
|
||||||
|
@ -51,12 +52,12 @@ export const InboxView = () => {
|
||||||
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
|
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
|
||||||
const [isClear, setClear] = createSignal(false)
|
const [isClear, setClear] = createSignal(false)
|
||||||
const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false)
|
const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false)
|
||||||
const { session } = useSession()
|
const { author } = useSession()
|
||||||
const currentUserId = createMemo(() => session()?.user.id)
|
const currentUserId = createMemo(() => author().id)
|
||||||
const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>()
|
const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>()
|
||||||
|
|
||||||
const messagesContainerRef: { current: HTMLDivElement } = {
|
const messagesContainerRef: { current: HTMLDivElement } = {
|
||||||
current: null
|
current: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Поиск по диалогам
|
// Поиск по диалогам
|
||||||
|
@ -72,7 +73,7 @@ export const InboxView = () => {
|
||||||
const handleOpenChat = async (chat: Chat) => {
|
const handleOpenChat = async (chat: Chat) => {
|
||||||
setCurrentDialog(chat)
|
setCurrentDialog(chat)
|
||||||
changeSearchParam({
|
changeSearchParam({
|
||||||
chat: chat.id
|
chat: chat.id,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await getMessages(chat.id)
|
await getMessages(chat.id)
|
||||||
|
@ -81,14 +82,14 @@ export const InboxView = () => {
|
||||||
} finally {
|
} finally {
|
||||||
messagesContainerRef.current.scroll({
|
messagesContainerRef.current.scroll({
|
||||||
top: messagesContainerRef.current.scrollHeight,
|
top: messagesContainerRef.current.scrollHeight,
|
||||||
behavior: 'instant'
|
behavior: 'instant',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await loadRecipients({ days: 365 })
|
const response = await loadRecipients() // time ago in seconds
|
||||||
setRecipients(response as unknown as Author[])
|
setRecipients(response as unknown as Author[])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
@ -100,7 +101,7 @@ export const InboxView = () => {
|
||||||
await sendMessage({
|
await sendMessage({
|
||||||
body: message,
|
body: message,
|
||||||
chat_id: currentDialog().id.toString(),
|
chat_id: currentDialog().id.toString(),
|
||||||
reply_to: messageToReply()?.id
|
reply_to: messageToReply()?.id,
|
||||||
})
|
})
|
||||||
setClear(true)
|
setClear(true)
|
||||||
setMessageToReply(null)
|
setMessageToReply(null)
|
||||||
|
@ -121,7 +122,7 @@ export const InboxView = () => {
|
||||||
await loadChats()
|
await loadChats()
|
||||||
changeSearchParam({
|
changeSearchParam({
|
||||||
initChat: null,
|
initChat: null,
|
||||||
chat: newChat.chat.id
|
chat: newChat.chat.id,
|
||||||
})
|
})
|
||||||
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
|
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
|
||||||
await handleOpenChat(chatToOpen)
|
await handleOpenChat(chatToOpen)
|
||||||
|
@ -160,11 +161,11 @@ export const InboxView = () => {
|
||||||
}
|
}
|
||||||
messagesContainerRef.current.scroll({
|
messagesContainerRef.current.scroll({
|
||||||
top: messagesContainerRef.current.scrollHeight,
|
top: messagesContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
{ defer: true }
|
{ defer: true },
|
||||||
)
|
)
|
||||||
const handleScrollMessageContainer = () => {
|
const handleScrollMessageContainer = () => {
|
||||||
if (
|
if (
|
||||||
|
@ -179,7 +180,7 @@ export const InboxView = () => {
|
||||||
const handleScrollToNew = () => {
|
const handleScrollToNew = () => {
|
||||||
messagesContainerRef.current.scroll({
|
messagesContainerRef.current.scroll({
|
||||||
top: messagesContainerRef.current.scrollHeight,
|
top: messagesContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
setIsScrollToNewVisible(false)
|
setIsScrollToNewVisible(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Author, Topic } from '../../../graphql/types.gen'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
import { Author, Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { SubscriptionFilter } from '../../../pages/types'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
|
||||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||||
// TODO: refactor styles
|
// TODO: refactor styles
|
||||||
import { isAuthor } from '../../../utils/isAuthor'
|
import { isAuthor } from '../../../utils/isAuthor'
|
||||||
|
@ -20,7 +20,7 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
||||||
|
|
||||||
export const ProfileSubscriptions = () => {
|
export const ProfileSubscriptions = () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { user } = useSession()
|
const { session } = useSession()
|
||||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
|
||||||
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
|
||||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||||
|
@ -29,8 +29,8 @@ export const ProfileSubscriptions = () => {
|
||||||
const fetchSubscriptions = async () => {
|
const fetchSubscriptions = async () => {
|
||||||
try {
|
try {
|
||||||
const [getAuthors, getTopics] = await Promise.all([
|
const [getAuthors, getTopics] = await Promise.all([
|
||||||
apiClient.getAuthorFollowingUsers({ slug: user().slug }),
|
apiClient.getAuthorFollowingUsers({ slug: session()?.author.slug }),
|
||||||
apiClient.getAuthorFollowingTopics({ slug: user().slug }),
|
apiClient.getAuthorFollowingTopics({ slug: session()?.author.slug }),
|
||||||
])
|
])
|
||||||
setFollowing([...getAuthors, ...getTopics])
|
setFollowing([...getAuthors, ...getTopics])
|
||||||
setFiltered([...getAuthors, ...getTopics])
|
setFiltered([...getAuthors, ...getTopics])
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { createStore } from 'solid-js/store'
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Topic } from '../../../graphql/types.gen'
|
import { apiClient } from '../../../graphql/client/core'
|
||||||
|
import { Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { hideModal, showModal } from '../../../stores/ui'
|
import { hideModal, showModal } from '../../../stores/ui'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Image } from '../../_shared/Image'
|
import { Image } from '../../_shared/Image'
|
||||||
|
@ -36,9 +36,9 @@ const shorten = (str: string, maxLen: number) => {
|
||||||
return `${result}...`
|
return `${result}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PublishSettings = (props: Props) => {
|
export const PublishSettings = async (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { user } = useSession()
|
const { session, author } = useSession()
|
||||||
|
|
||||||
const composeDescription = () => {
|
const composeDescription = () => {
|
||||||
if (!props.form.description) {
|
if (!props.form.description) {
|
||||||
|
@ -154,7 +154,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.shoutCardTitle}>{settingsForm.title}</div>
|
<div class={styles.shoutCardTitle}>{settingsForm.title}</div>
|
||||||
<div class={styles.shoutCardSubtitle}>{settingsForm.subtitle}</div>
|
<div class={styles.shoutCardSubtitle}>{settingsForm.subtitle}</div>
|
||||||
<div class={styles.shoutAuthor}>{user().name}</div>
|
<div class={styles.shoutAuthor}>{author()?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { Show, For, createSignal } from 'solid-js'
|
import { Show, For, createSignal } from 'solid-js'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
import type { Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
|
import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { clsx } from 'clsx'
|
||||||
import { For, onMount, Show } from 'solid-js'
|
import { For, onMount, Show } from 'solid-js'
|
||||||
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
||||||
|
|
||||||
import { Shout } from '../../../graphql/types.gen'
|
import { Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { ArticleCard } from '../../Feed/ArticleCard'
|
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||||
import { Icon } from '../Icon'
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { Reaction } from '../../../graphql/types.gen'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
||||||
import { ReactionKind } from '../../../graphql/types.gen'
|
import { Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
|
||||||
import { Userpic } from '../../Author/Userpic'
|
import { Userpic } from '../../Author/Userpic'
|
||||||
|
|
||||||
import styles from './VotersList.module.scss'
|
import styles from './VotersList.module.scss'
|
||||||
|
@ -26,11 +24,11 @@ export const VotersList = (props: Props) => {
|
||||||
<li class={styles.item}>
|
<li class={styles.item}>
|
||||||
<div class={styles.user}>
|
<div class={styles.user}>
|
||||||
<Userpic
|
<Userpic
|
||||||
name={reaction.createdBy.name}
|
name={reaction.created_by.name}
|
||||||
userpic={reaction.createdBy.userpic}
|
userpic={reaction.created_by.pic}
|
||||||
class={styles.userpic}
|
class={styles.userpic}
|
||||||
/>
|
/>
|
||||||
<a href={`/author/${reaction.createdBy.slug}`}>{reaction.createdBy.name || ''}</a>
|
<a href={`/author/${reaction.created_by.slug}`}>{reaction.created_by.name || ''}</a>
|
||||||
</div>
|
</div>
|
||||||
{reaction.kind === ReactionKind.Like ? (
|
{reaction.kind === ReactionKind.Like ? (
|
||||||
<div class={styles.commentRatingPositive}>+1</div>
|
<div class={styles.commentRatingPositive}>+1</div>
|
||||||
|
|
242
src/context/authorizer.tsx
Normal file
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 { Accessor, createContext, createSignal, useContext } from 'solid-js'
|
||||||
import { createStore, SetStoreFunction } from 'solid-js/store'
|
import { createStore, SetStoreFunction } from 'solid-js/store'
|
||||||
|
|
||||||
import { Topic, TopicInput } from '../graphql/types.gen'
|
import { apiClient } from '../graphql/client/core'
|
||||||
|
import { ShoutVisibility, Topic, TopicInput } from '../graphql/schema/core.gen'
|
||||||
import { router, useRouter } from '../stores/router'
|
import { router, useRouter } from '../stores/router'
|
||||||
import { apiClient } from '../utils/apiClient'
|
|
||||||
import { slugify } from '../utils/slugify'
|
import { slugify } from '../utils/slugify'
|
||||||
|
|
||||||
import { useLocalize } from './localize'
|
import { useLocalize } from './localize'
|
||||||
|
@ -130,10 +130,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
shoutId: formToUpdate.shoutId,
|
shoutId: formToUpdate.shoutId,
|
||||||
shoutInput: {
|
shoutInput: {
|
||||||
body: formToUpdate.body,
|
body: formToUpdate.body,
|
||||||
topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)),
|
topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)), // NOTE: first is main
|
||||||
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
// community?: InputMaybe<Scalars['Int']>
|
// community?: InputMaybe<Scalars['Int']>
|
||||||
mainTopic: topic2topicInput(formToUpdate.mainTopic),
|
// mainTopic: topic2topicInput(formToUpdate.mainTopic),
|
||||||
slug: formToUpdate.slug,
|
slug: formToUpdate.slug,
|
||||||
subtitle: formToUpdate.subtitle,
|
subtitle: formToUpdate.subtitle,
|
||||||
title: formToUpdate.title,
|
title: formToUpdate.title,
|
||||||
|
@ -163,7 +163,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
const shout = await updateShout(formToSave, { publish: false })
|
const shout = await updateShout(formToSave, { publish: false })
|
||||||
removeDraftFromLocalStorage(formToSave.shoutId)
|
removeDraftFromLocalStorage(formToSave.shoutId)
|
||||||
|
|
||||||
if (shout.visibility === 'owner') {
|
if (shout.visibility === ShoutVisibility.Authors) {
|
||||||
openPage(router, 'drafts')
|
openPage(router, 'drafts')
|
||||||
} else {
|
} else {
|
||||||
openPage(router, 'article', { slug: shout.slug })
|
openPage(router, 'article', { slug: shout.slug })
|
||||||
|
|
|
@ -1,19 +1,9 @@
|
||||||
|
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/schema/chat.gen'
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
import { createContext, createEffect, createSignal, onMount, useContext } from 'solid-js'
|
import { createContext, createSignal, useContext } from 'solid-js'
|
||||||
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
import { SSEMessage, useConnect } from './connect'
|
||||||
import { inboxClient } from '../utils/apiClient'
|
|
||||||
import { loadMessages } from '../stores/inbox'
|
import { loadMessages } from '../stores/inbox'
|
||||||
import { getToken } from '../graphql/privateGraphQLClient'
|
import { inboxClient } from '../graphql/client/chat'
|
||||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
|
||||||
|
|
||||||
export interface SSEMessage {
|
|
||||||
id: string
|
|
||||||
entity: string
|
|
||||||
action: string
|
|
||||||
payload: any // Author | Shout | Reaction | Message
|
|
||||||
timestamp?: number
|
|
||||||
seen?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type InboxContextType = {
|
type InboxContextType = {
|
||||||
chats: Accessor<Chat[]>
|
chats: Accessor<Chat[]>
|
||||||
|
@ -35,47 +25,20 @@ export function useInbox() {
|
||||||
export const InboxProvider = (props: { children: JSX.Element }) => {
|
export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const [chats, setChats] = createSignal<Chat[]>([])
|
const [chats, setChats] = createSignal<Chat[]>([])
|
||||||
const [messages, setMessages] = createSignal<Message[]>([])
|
const [messages, setMessages] = createSignal<Message[]>([])
|
||||||
|
const handleMessage = (sseMessage: SSEMessage) => {
|
||||||
const handleMessage = (sseMessage) => {
|
|
||||||
console.log('[context.inbox]:', sseMessage)
|
console.log('[context.inbox]:', sseMessage)
|
||||||
// TODO: handle all action types: create update delete join left
|
// TODO: handle all action types: create update delete join left
|
||||||
if (sseMessage.entity == 'message') {
|
if (sseMessage.entity === 'message') {
|
||||||
const relivedMessage = sseMessage.payload
|
const relivedMessage = sseMessage.payload
|
||||||
setMessages((prev) => [...prev, relivedMessage])
|
setMessages((prev) => [...prev, relivedMessage])
|
||||||
} else if (sseMessage.entity == 'chat') {
|
} else if (sseMessage.entity === 'chat') {
|
||||||
const relivedChat = sseMessage.payload
|
const relivedChat = sseMessage.payload
|
||||||
setChats((prev) => [...prev, relivedChat])
|
setChats((prev) => [...prev, relivedChat])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(async () => {
|
const { addHandler } = useConnect()
|
||||||
const token = getToken()
|
addHandler(handleMessage)
|
||||||
if (token) {
|
|
||||||
await fetchEventSource('https://chat.discours.io/connect', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: token,
|
|
||||||
},
|
|
||||||
onmessage(event) {
|
|
||||||
const m: SSEMessage = JSON.parse(event.data)
|
|
||||||
console.log('[context.inbox] Received message:', m)
|
|
||||||
if (m.entity === 'chat' || m.entity == 'message') {
|
|
||||||
handleMessage(m)
|
|
||||||
} else {
|
|
||||||
console.debug(m)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
console.log('[context.inbox] sse connection closed by server')
|
|
||||||
},
|
|
||||||
onerror(err) {
|
|
||||||
console.error('[context.inbox] sse connection closed by error', err)
|
|
||||||
throw new Error(err) // NOTE: simple hack to close the connection
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadChats = async () => {
|
const loadChats = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -103,7 +66,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const currentChat = chats().find((chat) => chat.id === args.chat_id)
|
const currentChat = chats().find((chat) => chat.id === args.chat_id)
|
||||||
setChats((prev) => [
|
setChats((prev) => [
|
||||||
...prev.filter((c) => c.id !== currentChat.id),
|
...prev.filter((c) => c.id !== currentChat.id),
|
||||||
{ ...currentChat, updatedAt: message.createdAt },
|
{ ...currentChat, updated_at: message.created_at },
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message:', error)
|
console.error('Error sending message:', error)
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
|
|
||||||
import { createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
|
import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { Portal } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
|
|
||||||
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
||||||
import { NotificationsPanel } from '../components/NotificationsPanel'
|
import { NotificationsPanel } from '../components/NotificationsPanel'
|
||||||
import { Notification } from '../graphql/types.gen'
|
import { notifierClient as apiClient } from '../graphql/client/notifier'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { Notification } from '../graphql/schema/notifier.gen'
|
||||||
import { apiBaseUrl } from '../utils/config'
|
import { SSEMessage, useConnect } from './connect'
|
||||||
import SSEService, { EventData } from '../utils/sseService'
|
|
||||||
|
|
||||||
import { useSession } from './session'
|
|
||||||
|
|
||||||
type NotificationsContextType = {
|
type NotificationsContextType = {
|
||||||
notificationEntities: Record<number, Notification>
|
notificationEntities: Record<number, Notification>
|
||||||
|
@ -35,53 +32,45 @@ export function useNotifications() {
|
||||||
return useContext(NotificationsContext)
|
return useContext(NotificationsContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sseService = new SSEService()
|
|
||||||
|
|
||||||
export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
|
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
|
||||||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||||
const { isAuthenticated, user } = useSession()
|
|
||||||
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
||||||
|
const { addHandler } = useConnect()
|
||||||
const loadNotifications = async (options: { limit: number; offset?: number }) => {
|
const loadNotifications = async (options: { limit: number; offset?: number }) => {
|
||||||
const { notifications, totalUnreadCount, totalCount } = await apiClient.getNotifications(options)
|
const { notifications, unread, total } = await apiClient.getNotifications(options)
|
||||||
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
||||||
acc[notification.id] = notification
|
acc[notification.id] = notification
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
setTotalNotificationsCount(totalCount)
|
setTotalNotificationsCount(total)
|
||||||
setUnreadNotificationsCount(totalUnreadCount)
|
setUnreadNotificationsCount(unread)
|
||||||
setNotificationEntities(newNotificationEntities)
|
setNotificationEntities(newNotificationEntities)
|
||||||
return notifications
|
return notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedNotifications = createMemo(() => {
|
const sortedNotifications = createMemo(() => {
|
||||||
return Object.values(notificationEntities).sort(
|
return Object.values(notificationEntities).sort((a, b) => b.created_at - a.created_at)
|
||||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
|
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
|
||||||
createEffect(() => {
|
|
||||||
if (isAuthenticated()) {
|
onMount(() => {
|
||||||
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`)
|
addHandler((data: SSEMessage) => {
|
||||||
sseService.subscribeToEvent('message', (data: EventData) => {
|
if (data.entity === 'reaction') {
|
||||||
if (data.type === 'newNotifications') {
|
|
||||||
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
||||||
} else {
|
} else {
|
||||||
console.error(`[NotificationsProvider] unknown message type: ${JSON.stringify(data)}`)
|
console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
sseService.disconnect()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const markNotificationAsRead = async (notification: Notification) => {
|
const markNotificationAsRead = async (notification: Notification) => {
|
||||||
await apiClient.markNotificationAsRead(notification.id)
|
await apiClient.markNotificationAsRead(notification.id)
|
||||||
setNotificationEntities(notification.id, 'seen', true)
|
const nnn = new Set([...notification.seen, notification.id])
|
||||||
|
setNotificationEntities(notification.id, 'seen', Array.from(nnn))
|
||||||
setUnreadNotificationsCount((oldCount) => oldCount - 1)
|
setUnreadNotificationsCount((oldCount) => oldCount - 1)
|
||||||
}
|
}
|
||||||
const markAllNotificationsAsRead = async () => {
|
const markAllNotificationsAsRead = async () => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { ProfileInput } from '../graphql/types.gen'
|
import type { ProfileInput } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { createEffect, createMemo, createSignal } from 'solid-js'
|
import { createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
|
import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
|
||||||
import { apiClient } from '../utils/apiClient'
|
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
|
@ -16,9 +16,7 @@ const userpicUrl = (userpic: string) => {
|
||||||
}
|
}
|
||||||
const useProfileForm = () => {
|
const useProfileForm = () => {
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
const currentSlug = createMemo(() => session()?.user?.slug)
|
const currentAuthor = createMemo(() => session()?.author)
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [] })
|
|
||||||
const currentAuthor = createMemo(() => authorEntities()[currentSlug()])
|
|
||||||
const [slugError, setSlugError] = createSignal<string>()
|
const [slugError, setSlugError] = createSignal<string>()
|
||||||
|
|
||||||
const submit = async (profile: ProfileInput) => {
|
const submit = async (profile: ProfileInput) => {
|
||||||
|
@ -35,20 +33,20 @@ const useProfileForm = () => {
|
||||||
bio: '',
|
bio: '',
|
||||||
about: '',
|
about: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
userpic: '',
|
pic: '',
|
||||||
links: [],
|
links: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (!currentSlug()) return
|
if (!currentAuthor()) return
|
||||||
try {
|
try {
|
||||||
await loadAuthor({ slug: currentSlug() })
|
await loadAuthor({ slug: currentAuthor().slug })
|
||||||
setForm({
|
setForm({
|
||||||
name: currentAuthor()?.name,
|
name: currentAuthor()?.name,
|
||||||
slug: currentAuthor()?.slug,
|
slug: currentAuthor()?.slug,
|
||||||
bio: currentAuthor()?.bio,
|
bio: currentAuthor()?.bio,
|
||||||
about: currentAuthor()?.about,
|
about: currentAuthor()?.about,
|
||||||
userpic: userpicUrl(currentAuthor()?.userpic),
|
pic: userpicUrl(currentAuthor()?.pic),
|
||||||
links: currentAuthor()?.links,
|
links: currentAuthor()?.links,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import type { Reaction, ReactionBy, ReactionInput } from '../graphql/types.gen'
|
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
|
||||||
import { createContext, onCleanup, useContext } from 'solid-js'
|
import { createContext, onCleanup, useContext } from 'solid-js'
|
||||||
import { createStore, reconcile } from 'solid-js/store'
|
import { createStore, reconcile } from 'solid-js/store'
|
||||||
|
|
||||||
import { ReactionKind } from '../graphql/types.gen'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
type ReactionsContextType = {
|
type ReactionsContextType = {
|
||||||
reactionEntities: Record<number, Reaction>
|
reactionEntities: Record<number, Reaction>
|
||||||
|
@ -56,9 +55,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
const oppositeReaction = Object.values(reactionEntities).find(
|
const oppositeReaction = Object.values(reactionEntities).find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === oppositeReactionKind &&
|
r.kind === oppositeReactionKind &&
|
||||||
r.createdBy.slug === reaction.createdBy.slug &&
|
r.created_by.slug === reaction.created_by.slug &&
|
||||||
r.shout.id === reaction.shout.id &&
|
r.shout.id === reaction.shout.id &&
|
||||||
r.replyTo === reaction.replyTo,
|
r.reply_to === reaction.reply_to,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (oppositeReaction) {
|
if (oppositeReaction) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
import type { AuthModalSource } from '../components/Nav/AuthModal/types'
|
||||||
import type { AuthResult, MySubscriptionsQueryResult, User } from '../graphql/types.gen'
|
import type { Author, Result } from '../graphql/schema/core.gen'
|
||||||
import type { Accessor, JSX, Resource } from 'solid-js'
|
import type { Accessor, JSX, Resource } from 'solid-js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -12,29 +12,33 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
} from 'solid-js'
|
} from 'solid-js'
|
||||||
|
|
||||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
import { authApiClient } from '../graphql/client/auth'
|
||||||
|
import { apiClient } from '../graphql/client/core'
|
||||||
|
import { getToken, resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||||
import { showModal } from '../stores/ui'
|
import { showModal } from '../stores/ui'
|
||||||
import { apiClient } from '../utils/apiClient'
|
|
||||||
|
|
||||||
import { useLocalize } from './localize'
|
import { useLocalize } from './localize'
|
||||||
import { useSnackbar } from './snackbar'
|
import { useSnackbar } from './snackbar'
|
||||||
|
import { useAuthorizer } from './authorizer'
|
||||||
|
import { VerifyEmailInput, LoginInput, AuthToken, User } from '@authorizerdev/authorizer-js'
|
||||||
|
|
||||||
type SessionContextType = {
|
export type SessionContextType = {
|
||||||
session: Resource<AuthResult>
|
session: Resource<AuthToken>
|
||||||
isSessionLoaded: Accessor<boolean>
|
isSessionLoaded: Accessor<boolean>
|
||||||
subscriptions: Accessor<MySubscriptionsQueryResult>
|
subscriptions: Accessor<Result>
|
||||||
user: Accessor<User>
|
user: Accessor<User>
|
||||||
|
author: Resource<Author | null>
|
||||||
isAuthenticated: Accessor<boolean>
|
isAuthenticated: Accessor<boolean>
|
||||||
actions: {
|
actions: {
|
||||||
loadSession: () => AuthResult | Promise<AuthResult>
|
loadSession: () => AuthToken | Promise<AuthToken>
|
||||||
loadSubscriptions: () => Promise<void>
|
loadSubscriptions: () => Promise<void>
|
||||||
requireAuthentication: (
|
requireAuthentication: (
|
||||||
callback: (() => Promise<void>) | (() => void),
|
callback: (() => Promise<void>) | (() => void),
|
||||||
modalSource: AuthModalSource,
|
modalSource: AuthModalSource,
|
||||||
) => void
|
) => void
|
||||||
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
|
signIn: (params: LoginInput) => Promise<void>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
confirmEmail: (token: string) => Promise<void>
|
confirmEmail: (input: VerifyEmailInput) => Promise<void>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,21 +55,37 @@ const EMPTY_SUBSCRIPTIONS = {
|
||||||
|
|
||||||
export const SessionProvider = (props: { children: JSX.Element }) => {
|
export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
|
||||||
const [subscriptions, setSubscriptions] = createSignal<MySubscriptionsQueryResult>(EMPTY_SUBSCRIPTIONS)
|
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const {
|
const {
|
||||||
actions: { showSnackbar },
|
actions: { showSnackbar },
|
||||||
} = useSnackbar()
|
} = useSnackbar()
|
||||||
|
const [, { authorizer }] = useAuthorizer()
|
||||||
|
const [authToken, setToken] = createSignal<string>('')
|
||||||
|
|
||||||
const getSession = async (): Promise<AuthResult> => {
|
const loadSubscriptions = async (): Promise<void> => {
|
||||||
|
const result = await apiClient.getMySubscriptions()
|
||||||
|
if (result) {
|
||||||
|
setSubscriptions(result)
|
||||||
|
} else {
|
||||||
|
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSession = async (): Promise<AuthToken> => {
|
||||||
try {
|
try {
|
||||||
const authResult = await apiClient.getSession()
|
const token = getToken() // FIXME: token in localStorage?
|
||||||
|
const authResult = await authorizer().getSession({
|
||||||
|
Authorization: token,
|
||||||
|
})
|
||||||
if (!authResult) {
|
if (!authResult) {
|
||||||
return null
|
return null
|
||||||
}
|
} else {
|
||||||
setToken(authResult.token)
|
console.log(authResult)
|
||||||
|
setToken(authResult.access_token || authResult.id_token)
|
||||||
loadSubscriptions()
|
loadSubscriptions()
|
||||||
return authResult
|
return authResult
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getSession error:', error)
|
console.error('getSession error:', error)
|
||||||
resetToken()
|
resetToken()
|
||||||
|
@ -77,28 +97,35 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSubscriptions = async (): Promise<void> => {
|
const [session, { refetch: loadSession, mutate }] = createResource<AuthToken>(getSession, {
|
||||||
const result = await apiClient.getMySubscriptions()
|
|
||||||
if (result) {
|
|
||||||
setSubscriptions(result)
|
|
||||||
} else {
|
|
||||||
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [session, { refetch: loadSession, mutate }] = createResource<AuthResult>(getSession, {
|
|
||||||
ssrLoadFrom: 'initial',
|
ssrLoadFrom: 'initial',
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = createMemo(() => session()?.user)
|
const user = createMemo(() => session()?.user)
|
||||||
|
|
||||||
const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug))
|
const [author, { refetch: loadAuthor }] = createResource<Author | null>(
|
||||||
|
async () => {
|
||||||
|
const user = session()?.user
|
||||||
|
if (user) {
|
||||||
|
return (await apiClient.getAuthor({ user: user.id })) ?? null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ssrLoadFrom: 'initial',
|
||||||
|
initialValue: null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const signIn = async ({ email, password }: { email: string; password: string }) => {
|
const isAuthenticated = createMemo(() => Boolean(session()?.user))
|
||||||
const authResult = await apiClient.authLogin({ email, password })
|
|
||||||
setToken(authResult.token)
|
const signIn = async (params: LoginInput) => {
|
||||||
|
const authResult = await authorizer().login(params)
|
||||||
|
if (authResult) {
|
||||||
|
setToken(authResult.access_token || authResult.id_token)
|
||||||
mutate(authResult)
|
mutate(authResult)
|
||||||
|
}
|
||||||
loadSubscriptions()
|
loadSubscriptions()
|
||||||
// console.debug('signed in')
|
// console.debug('signed in')
|
||||||
}
|
}
|
||||||
|
@ -115,30 +142,26 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(async () => {
|
onMount(async () => {
|
||||||
if (isAuthWithCallback()) {
|
// Load the session and author data on mount
|
||||||
const sessionProof = await session()
|
await loadSession()
|
||||||
|
loadAuthor()
|
||||||
if (sessionProof) {
|
|
||||||
await isAuthWithCallback()()
|
|
||||||
|
|
||||||
setIsAuthWithCallback(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
// TODO: call backend to revoke token
|
await authorizer().logout()
|
||||||
mutate(null)
|
mutate(null)
|
||||||
resetToken()
|
resetToken()
|
||||||
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
||||||
showSnackbar({ body: t("You've successfully logged out") })
|
showSnackbar({ body: t("You've successfully logged out") })
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmEmail = async (token: string) => {
|
const confirmEmail = async (input: VerifyEmailInput) => {
|
||||||
const authResult = await apiClient.confirmEmail({ token })
|
const authToken: void | AuthToken = await authorizer().verifyEmail(input)
|
||||||
setToken(authResult.token)
|
if (authToken) {
|
||||||
mutate(authResult)
|
setToken(authToken.access_token)
|
||||||
|
mutate(authToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
@ -149,11 +172,11 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
confirmEmail,
|
confirmEmail,
|
||||||
loadSubscriptions,
|
loadSubscriptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
const value: SessionContextType = {
|
const value: SessionContextType = {
|
||||||
session,
|
session,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
|
author,
|
||||||
user,
|
user,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
actions,
|
actions,
|
||||||
|
@ -162,6 +185,5 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadSession()
|
loadSession()
|
||||||
})
|
})
|
||||||
|
|
||||||
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
|
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
141
src/graphql/client/auth.ts
Normal file
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`
|
export default gql`
|
||||||
mutation CreateChat($title: String, $members: [Int]!) {
|
mutation CreateChat($title: String, $members: [Int]!) {
|
||||||
createChat(title: $title, members: $members) {
|
create_chat(title: $title, members: $members) {
|
||||||
error
|
error
|
||||||
chat {
|
chat {
|
||||||
id
|
id
|
|
@ -1,9 +1,8 @@
|
||||||
import { ChatInput } from './../types.gen'
|
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteChat($chat_id: String!) {
|
mutation DeleteChat($chat_id: String!) {
|
||||||
deleteChat(chat_id: $chat_id) {
|
delete_chat(chat_id: $chat_id) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation MarkAsReadMutation($message_id: Int!, $chat_id: String!) {
|
mutation MarkAsReadMutation($message_id: Int!, $chat_id: String!) {
|
||||||
markAsRead(message_id: $message_id, chat_id: $chat_id) {
|
marke_as_read(message_id: $message_id, chat_id: $chat_id) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation createMessage($chat_id: String!, $body: String!, $reply_to: Int) {
|
mutation createMessage($chat_id: String!, $body: String!, $reply_to: Int) {
|
||||||
createMessage(chat_id: $chat_id, body: $body, reply_to: $reply_to) {
|
create_message(chat_id: $chat_id, body: $body, reply_to: $reply_to) {
|
||||||
error
|
error
|
||||||
message {
|
message {
|
||||||
id
|
id
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteMessage($chat_id: String!) {
|
mutation DeleteMessage($chat_id: String!) {
|
||||||
deleteMessage(chat_id: $chat_id) {
|
delete_message(chat_id: $chat_id) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
import { ChatInput } from './../types.gen'
|
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UpdateMessage($message: MessageInput!) {
|
mutation UpdateMessage($message: MessageInput!) {
|
||||||
createMessage(message: $message) {
|
update_message(message: $message) {
|
||||||
error
|
error
|
||||||
message {
|
message {
|
||||||
id
|
id
|
|
@ -1,9 +1,8 @@
|
||||||
import { ChatInput } from './../types.gen'
|
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UpdateChat($chat: ChatInput!) {
|
mutation UpdateChat($chat: ChatInput!) {
|
||||||
updateChat(chat: $chat) {
|
update_chat(chat: $chat) {
|
||||||
error
|
error
|
||||||
chat {
|
chat {
|
||||||
id
|
id
|
|
@ -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`
|
export default gql`
|
||||||
mutation CreateShoutMutation($shout: ShoutInput!) {
|
mutation CreateShoutMutation($shout: ShoutInput!) {
|
||||||
createShout(inp: $shout) {
|
create_shout(inp: $shout) {
|
||||||
error
|
error
|
||||||
shout {
|
shout {
|
||||||
id
|
id
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteShoutMutation($shoutId: Int!) {
|
mutation DeleteShoutMutation($shoutId: Int!) {
|
||||||
deleteShout(shout_id: $shoutId) {
|
delete_shout(shout_id: $shoutId) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput, $publish: Boolean) {
|
mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput, $publish: Boolean) {
|
||||||
updateShout(shout_id: $shoutId, shout_input: $shoutInput, publish: $publish) {
|
update_shout(shout_id: $shoutId, shout_input: $shoutInput, publish: $publish) {
|
||||||
error
|
error
|
||||||
shout {
|
shout {
|
||||||
id
|
id
|
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`
|
export default gql`
|
||||||
mutation CommunityCreateMutation($title: String!, $desc: String!) {
|
mutation CommunityCreateMutation($title: String!, $desc: String!) {
|
||||||
createCommunity(title: $title, desc: $desc) {
|
create_community(title: $title, desc: $desc) {
|
||||||
id
|
id
|
||||||
desc
|
desc
|
||||||
name
|
name
|
||||||
pic
|
pic
|
||||||
createdAt
|
created_at
|
||||||
createdBy
|
created_by
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation CommunityDestroyMutation($slug: String!) {
|
mutation CommunityDestroyMutation($slug: String!) {
|
||||||
deleteCommunity(slug: $slug) {
|
delete_community(slug: $slug) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,14 +2,14 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation CommunityUpdateMutation($community: Community!) {
|
mutation CommunityUpdateMutation($community: Community!) {
|
||||||
updateCommunity(community: $community) {
|
update_community(community: $community) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
desc
|
desc
|
||||||
name
|
name
|
||||||
pic
|
pic
|
||||||
createdAt
|
created_at
|
||||||
createdBy
|
created_by
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user