Merge remote-tracking branch 'hub/main' into feature/sse-connect

This commit is contained in:
Untone 2024-01-22 15:52:07 +03:00
commit cecf712cc3
13 changed files with 214 additions and 36 deletions

View File

@ -4,7 +4,7 @@ import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core'
import { Link, Meta } from '@solidjs/meta'
import { clsx } from 'clsx'
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup, on } from 'solid-js'
import { isServer } from 'solid-js/web'
import { useLocalize } from '../../context/localize'
@ -44,6 +44,11 @@ type Props = {
scrollToComments?: boolean
}
type IframeSize = {
width: number
height: number
}
export type ArticlePageSearchParams = {
scrollTo: 'comments'
commentId: string
@ -182,18 +187,6 @@ export const FullArticle = (props: Props) => {
actions: { loadReactionsBy },
} = useReactions()
onMount(async () => {
await loadReactionsBy({
by: { shout: props.article.slug },
})
setIsReactionsLoaded(true)
})
onMount(() => {
document.title = props.article.title
})
const clickHandlers = []
const documentClickHandlers = []
@ -295,8 +288,50 @@ export const FullArticle = (props: Props) => {
}
}
const cover = props.article.cover ?? 'production/image/logo_image.png'
// Check iframes size
const articleContainer: { current: HTMLElement } = { current: null }
const updateIframeSizes = () => {
if (!articleContainer?.current || !props.article.body) return
const iframes = articleContainer?.current?.querySelectorAll('iframe')
if (!iframes) return
const containerWidth = articleContainer.current?.offsetWidth
iframes.forEach((iframe) => {
const style = window.getComputedStyle(iframe)
const originalWidth = iframe.getAttribute('width') || style.width.replace('px', '')
const originalHeight = iframe.getAttribute('height') || style.height.replace('px', '')
const width = Number(originalWidth)
const height = Number(originalHeight)
if (containerWidth < width) {
const aspectRatio = width / height
iframe.style.width = `${containerWidth}px`
iframe.style.height = `${Math.round(containerWidth / aspectRatio) + 40}px`
}
})
}
createEffect(
on(
() => props.article,
() => {
updateIframeSizes()
},
),
)
onMount(async () => {
await loadReactionsBy({
by: { shout: props.article.slug },
})
setIsReactionsLoaded(true)
document.title = props.article.title
window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
})
const cover = props.article.cover ?? 'production/image/logo_image.png'
const ogImage = getOpenGraphImageUrl(cover, {
title: props.article.title,
topic: mainTopic().title,
@ -328,6 +363,7 @@ export const FullArticle = (props: Props) => {
<div class="wide-container">
<div class="row position-relative">
<article
ref={(el) => (articleContainer.current = el)}
class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)}
onClick={handleArticleBodyClick}
>

View File

@ -46,6 +46,8 @@ import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { Footnote } from './extensions/Footnote'
import { Iframe } from './extensions/Iframe'
import { Span } from './extensions/Span'
import { ToggleTextWrap } from './extensions/ToggleTextWrap'
import { TrailingNode } from './extensions/TrailingNode'
import { TextBubbleMenu } from './TextBubbleMenu'
@ -201,6 +203,8 @@ export const Editor = (props: Props) => {
CustomBlockquote,
Bold,
Italic,
Span,
ToggleTextWrap,
Strike,
HorizontalRule.configure({
HTMLAttributes: {
@ -208,7 +212,10 @@ export const Editor = (props: Props) => {
},
}),
Underline,
Link.configure({
Link.extend({
inclusive: false,
}).configure({
autolink: true,
openOnClick: false,
}),
Heading.configure({
@ -244,6 +251,7 @@ export const Editor = (props: Props) => {
Figure,
Figcaption,
Footnote,
ToggleTextWrap,
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
BubbleMenu.configure({
pluginKey: 'textBubbleMenu',
@ -252,6 +260,9 @@ export const Editor = (props: Props) => {
const { doc, selection } = state
const { empty } = selection
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
if (isEmptyTextBlock) {
e.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
}
setIsCommonMarkup(e.isActive('figcaption'))
const result =
(view.hasFocus() &&

View File

@ -311,3 +311,10 @@ footnote {
background-color: unset;
}
}
.highlight-fake-selection {
background: var(--selection-background);
color: var(--selection-color);
border: solid var(--selection-background);
border-width: 5px 0;
}

View File

@ -117,7 +117,10 @@ const SimplifiedEditor = (props: Props) => {
Paragraph,
Bold,
Italic,
Link.configure({
Link.extend({
inclusive: false,
}).configure({
autolink: true,
openOnClick: false,
}),
CharacterCount.configure({

View File

@ -129,11 +129,21 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
})
})
const handleOpenLinkForm = () => {
props.editor.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
setLinkEditorOpen(true)
}
const handleCloseLinkForm = () => {
setLinkEditorOpen(false)
props.editor.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
}
return (
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
<Switch>
<Match when={linkEditorOpen()}>
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
</Match>
<Match when={footnoteEditorOpen()}>
<SimplifiedEditor
@ -329,7 +339,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<button
ref={triggerRef}
type="button"
onClick={() => setLinkEditorOpen(true)}
onClick={handleOpenLinkForm}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink(),
})}

View File

@ -41,6 +41,8 @@ export const Iframe = Node.create<IframeOptions>({
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
width: { default: null },
height: { default: null },
}
},

View File

@ -0,0 +1,31 @@
import { Mark, mergeAttributes } from '@tiptap/core'
export const Span = Mark.create({
name: 'span',
parseHTML() {
return [
{
tag: 'span[class]',
getAttrs: (dom) => {
if (dom instanceof HTMLElement) {
return { class: dom.getAttribute('class') }
}
return false
},
},
]
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(HTMLAttributes), 0]
},
addAttributes() {
return {
class: {
default: null,
},
}
},
})

View File

@ -0,0 +1,50 @@
import { Extension } from '@tiptap/core'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
toggleSpanWrap: {
addTextWrap: (attributes: { class: string }) => ReturnType
removeTextWrap: (attributes: { class: string }) => ReturnType
}
}
}
export const ToggleTextWrap = Extension.create({
name: 'toggleTextWrap',
addCommands() {
return {
addTextWrap:
(attributes) =>
({ commands, state }) => {
return commands.setMark('span', attributes)
},
removeTextWrap:
(attributes) =>
({ state, dispatch }) => {
let tr = state.tr
let changesApplied = false
state.doc.descendants((node, pos) => {
if (node.isInline) {
node.marks.forEach((mark) => {
if (mark.type.name === 'span' && mark.attrs.class === attributes.class) {
const end = pos + node.nodeSize
tr = tr.removeMark(pos, end, mark.type)
changesApplied = true
}
})
}
})
if (changesApplied) {
dispatch(tr)
return true
} else {
return false
}
},
}
},
})

View File

@ -7,12 +7,10 @@ import { createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { showModal } from '../../../stores/ui'
import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { InviteCoAuthorsModal } from '../../_shared/InviteCoAuthorsModal'
import { Popover } from '../../_shared/Popover'
import { CoverImage } from '../../Article/CoverImage'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'

View File

@ -36,8 +36,12 @@ export const Expo = (props: Props) => {
const { t } = useLocalize()
// const { sortedArticles } = useArticlesStore({
// shouts: isLoaded() ? props.shouts : [],
// })
const { sortedArticles } = useArticlesStore({
shouts: isLoaded() ? props.shouts : [],
shouts: props.shouts || [],
layout: props.layout,
})
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {

View File

@ -8,7 +8,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
z-index: 99999;
animation: 300ms fadeIn;
animation-fill-mode: forwards;

View File

@ -30,6 +30,12 @@ export const Lightbox = (props: Props) => {
current: null,
}
const handleSmoothAction = (action: () => void) => {
setTransitionEnabled(true)
action()
setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED)
}
const closeLightbox = () => {
lightboxRef.current?.classList.add(styles.fadeOut)
@ -40,34 +46,45 @@ export const Lightbox = (props: Props) => {
const zoomIn = (event) => {
event.stopPropagation()
setTransitionEnabled(true)
handleSmoothAction(() => {
setZoomLevel(zoomLevel() * ZOOM_STEP)
setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED)
})
}
const zoomOut = (event) => {
event.stopPropagation()
setTransitionEnabled(true)
handleSmoothAction(() => {
setZoomLevel(zoomLevel() / ZOOM_STEP)
setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED)
})
}
const positionReset = () => {
setTranslateX(0)
setTranslateY(0)
}
const zoomReset = (event) => {
event.stopPropagation()
handleSmoothAction(() => {
setZoomLevel(1)
positionReset()
})
}
const handleWheelZoom = (event) => {
const handleMouseWheelZoom = (event) => {
event.preventDefault()
event.stopPropagation()
let scale = zoomLevel()
scale += event.deltaY * -0.01
scale = Math.min(Math.max(0.125, scale), 4)
setTransitionEnabled(true)
handleSmoothAction(() => {
setZoomLevel(scale * ZOOM_STEP)
})
}
useEscKeyDownHandler(closeLightbox)
@ -130,6 +147,7 @@ export const Lightbox = (props: Props) => {
<div
class={clsx(styles.Lightbox, props.class)}
onClick={closeLightbox}
onWheel={(e) => e.preventDefault()}
ref={(el) => (lightboxRef.current = el)}
>
<Show when={pictureScalePercentage()}>
@ -154,7 +172,7 @@ export const Lightbox = (props: Props) => {
src={getImageUrl(props.image, { noSizeUrlPart: true })}
alt={props.imageAlt || ''}
onClick={(event) => event.stopPropagation()}
onWheel={handleWheelZoom}
onWheel={handleMouseWheelZoom}
style={lightboxStyle()}
onMouseDown={onMouseDown}
/>

View File

@ -183,6 +183,7 @@ export const resetSortedArticles = () => {
type InitialState = {
shouts?: Shout[]
layout?: string
}
const TOP_MONTH_ARTICLES_COUNT = 10
@ -219,7 +220,14 @@ export const loadTopArticles = async (): Promise<void> => {
export const useArticlesStore = (initialState: InitialState = {}) => {
addArticles([...(initialState.shouts || [])])
if (initialState.shouts) {
if (initialState.layout) {
// eslint-disable-next-line promise/catch-or-return
loadShouts({ filters: { layout: initialState.layout }, limit: 10 }).then(({ newShouts }) => {
addArticles(newShouts)
setSortedArticles(newShouts)
})
} else if (initialState.shouts) {
addArticles([...initialState.shouts])
setSortedArticles([...initialState.shouts])
}