diff --git a/assets/close.svg b/assets/close.svg new file mode 100644 index 000000000..fcb517711 --- /dev/null +++ b/assets/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/minus.svg b/assets/minus.svg new file mode 100644 index 000000000..6d2858a45 --- /dev/null +++ b/assets/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/plus.svg b/assets/plus.svg new file mode 100644 index 000000000..c13a81c3f --- /dev/null +++ b/assets/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/zoom-in.svg b/assets/zoom-in.svg new file mode 100644 index 000000000..2e18b4ced --- /dev/null +++ b/assets/zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/zoom-out.svg b/assets/zoom-out.svg new file mode 100644 index 000000000..b37084ded --- /dev/null +++ b/assets/zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev/App.tsx b/dev/App.tsx index 1976c8c8c..7e0d2d6d5 100644 --- a/dev/App.tsx +++ b/dev/App.tsx @@ -7,6 +7,8 @@ export default class App extends Component { componentDidMount() { addResponseMessage('Welcome to this awesome chat!'); addLinkSnippet({ link: 'https://google.com', title: 'Google' }); + addResponseMessage('![](https://raw.githubusercontent.com/Wolox/press-kit/master/logos/logo_banner.png)'); + addResponseMessage('![vertical](https://d2sofvawe08yqg.cloudfront.net/reintroducing-react/hero2x?1556470143)'); } handleNewUserMessage = (newMessage: any) => { @@ -36,14 +38,18 @@ export default class App extends Component { render() { return ( - +
+ + +
); } } diff --git a/dev/index.html b/dev/index.html index 8ec2a8359..580ad4157 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,9 +1,18 @@ - - + + Dev Widget +
diff --git a/package-lock.json b/package-lock.json index 87f47fa7f..0344a3fed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1893,6 +1893,12 @@ "type-detect": "4.0.8" } }, + "@toycode/markdown-it-class": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@toycode/markdown-it-class/-/markdown-it-class-1.2.3.tgz", + "integrity": "sha512-9N7iI5iGzjmYpeEFE93/76SkiSZkywkIiKUxahoU4h5WDUJyruXO+Ce4o/nFkFdKQnmsTmyxhdCtXVikl67XGw==", + "dev": true + }, "@types/babel__core": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz", diff --git a/package.json b/package.json index 27fd70805..1aade613a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@babel/preset-env": "^7.8.7", "@babel/preset-react": "^7.8.3", "@babel/preset-typescript": "^7.8.3", + "@toycode/markdown-it-class": "^1.2.3", "@types/classnames": "^2.2.10", "@types/enzyme": "^3.10.5", "@types/jest": "^25.1.4", diff --git a/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx b/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx index d6c0e24e1..b7ccdc08f 100644 --- a/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx +++ b/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx @@ -3,6 +3,7 @@ import format from 'date-fns/format'; import markdownIt from 'markdown-it'; import markdownItSup from 'markdown-it-sup'; import markdownItSanitizer from 'markdown-it-sanitizer'; +import markdownItClass from '@toycode/markdown-it-class'; import markdownItLinkAttributes from 'markdown-it-link-attributes'; import { Message } from 'src/store/types'; @@ -15,7 +16,11 @@ type Props = { } function Message({ message, showTimeStamp }: Props) { - const sanitizedHTML = markdownIt().use(markdownItSup) + const sanitizedHTML = markdownIt() + .use(markdownItClass, { + img: ['rcw-message-img'] + }) + .use(markdownItSup) .use(markdownItSanitizer) .use(markdownItLinkAttributes, { attrs: { target: '_blank', rel: 'noopener' } }) .render(message.text); diff --git a/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss b/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss index 533296076..c286b6289 100644 --- a/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss +++ b/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss @@ -16,7 +16,7 @@ display: flex; flex-direction: column; margin-left: auto; - + .rcw-message-text { @include message-bubble($turqois-2); } @@ -38,10 +38,14 @@ /* For markdown elements created with default styles */ .rcw-message-text { - p { margin: 0; } + + img { + width: 100%; + object-fit: contain; + } } .rcw-avatar { diff --git a/src/components/Widget/components/Conversation/components/Messages/index.tsx b/src/components/Widget/components/Conversation/components/Messages/index.tsx index ac15f6dfa..62db5cfd2 100644 --- a/src/components/Widget/components/Conversation/components/Messages/index.tsx +++ b/src/components/Widget/components/Conversation/components/Messages/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState, ElementRef, ImgHTMLAttributes, MouseEvent } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import format from 'date-fns/format'; @@ -23,7 +23,7 @@ function Messages({ profileAvatar, showTimeStamp }: Props) { showChat: state.behavior.showChat })); - const messageRef = useRef(null); + const messageRef = useRef(null); useEffect(() => { // @ts-ignore scrollToBottom(messageRef.current); diff --git a/src/components/Widget/components/Conversation/style.scss b/src/components/Widget/components/Conversation/style.scss index 2d65a85b8..a6b6fa82b 100644 --- a/src/components/Widget/components/Conversation/style.scss +++ b/src/components/Widget/components/Conversation/style.scss @@ -5,19 +5,19 @@ .rcw-conversation-container { border-radius: 10px; box-shadow: 0px 2px 10px 1px $grey-3; - &.active { opacity: 1; transform: translateY(0px); - transition: opacity 0.3s ease, transform 0.3s ease; + transition: opacity 0.3s ease, transform 0.3s ease; } &.hidden { + z-index: -1; pointer-events: none; opacity: 0; transform: translateY(10px); - transition: opacity 0.3s ease, transform 0.3s ease; + transition: opacity 0.3s ease, transform 0.3s ease; } } diff --git a/src/components/Widget/components/FullScreenPreview/index.tsx b/src/components/Widget/components/FullScreenPreview/index.tsx new file mode 100644 index 000000000..7ce438b31 --- /dev/null +++ b/src/components/Widget/components/FullScreenPreview/index.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, ReactNode } from 'react'; +import ReactDOM from 'react-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import usePreview from './usePreview'; +import usePortal from './usePortal'; +import './styles.scss'; +import { GlobalState } from '../../../../store/types'; +import { closeFullscreenPreview } from '../../../../store/actions'; + +const close = require('../../../../../assets/close.svg') as string; +const plus = require('../../../../../assets/plus.svg') as string; +const minus = require('../../../../../assets/minus.svg') as string; +const zoomIn = require('../../../../../assets/zoom-in.svg') as string; +const zoomOut = require('../../../../../assets/zoom-out.svg') as string; + +type Props = { + fullScreenMode?: boolean; + zoomStep?: number +} + +export default function FullScreenPreview({ fullScreenMode, zoomStep }:Props) { + const { + state, + initFileSize, + onZoomIn, + onZoomOut, + onResizePageZoom + } = usePreview(zoomStep); + + const dispatch = useDispatch(); + const { src, alt, width, height, visible } = useSelector((state: GlobalState) => ({ + src: state.preview.src, + alt: state.preview.alt, + width: state.preview.width, + height: state.preview.height, + visible: state.preview.visible + })); + + useEffect(() => { + if(src) { + initFileSize(width, height); + } + }, [src]) + + const pDom = usePortal() + + const onClosePreview = () => { + dispatch(closeFullscreenPreview()) + } + + const childNode: ReactNode = ( +
+
+ {alt} +
+ +
+ + + + +
+
+ ) + + return visible ? ReactDOM.createPortal(childNode, pDom) : null; +} diff --git a/src/components/Widget/components/FullScreenPreview/styles.scss b/src/components/Widget/components/FullScreenPreview/styles.scss new file mode 100644 index 000000000..6e61c135f --- /dev/null +++ b/src/components/Widget/components/FullScreenPreview/styles.scss @@ -0,0 +1,60 @@ +@import 'variables/colors'; + +.rcw-previewer-container { + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.75); + overflow: hidden; + position: fixed; + z-index: 9999; + left: 0; + top: 0; + + .rcw-previewer-image { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + transition: all 0.3s ease; + } + + .rcw-previewer-tools { + position: fixed; + right: 16px; + bottom: 16px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .rcw-previewer-button { + padding: 0; + margin: 16px; + box-shadow: 0 3px 8px 0px rgba(0, 0, 0, 0.3); + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + outline: none; + background-color: $white; + border: none; + } + + .rcw-previewer-close-button { + position: absolute; + right: 0; + top: 0; + } + + .rcw-previewer-veil { + width: 100%; + height: 100%; + overflow: scroll; + position: relative; + } +} diff --git a/src/components/Widget/components/FullScreenPreview/usePortal.ts b/src/components/Widget/components/FullScreenPreview/usePortal.ts new file mode 100644 index 000000000..57f3e836a --- /dev/null +++ b/src/components/Widget/components/FullScreenPreview/usePortal.ts @@ -0,0 +1,52 @@ +import { useRef, useEffect } from 'react'; + +function createRootElement(id: string):HTMLDivElement { + const rootContainer = document.createElement('div'); + rootContainer.setAttribute('id', id); + return rootContainer; +} + +function addRootElement(rootElem: HTMLDivElement):void { + document.body.appendChild(rootElem); +} + +function usePortal():HTMLDivElement { + const rootElemRef = useRef(null); + + useEffect(() => { + // Look for existing target dom element to append to + const existingParent: HTMLDivElement | null = document.querySelector('#rcw-image-preview'); + // Parent is either a new root or the existing dom element + const parentElem: HTMLDivElement = existingParent || createRootElement('#rcw-image-preview'); + + // If there is no existing DOM element, add a new one. + if (!existingParent) { + addRootElement(parentElem); + } + + // Add the detached element to the parent + if(rootElemRef.current) { + parentElem.appendChild(rootElemRef.current); + } + + return function removeElement() { + if(rootElemRef.current) { + rootElemRef.current.remove(); + } + if (parentElem.childNodes.length === -1) { + parentElem.remove(); + } + }; + }, []); + + function getRootElem():HTMLDivElement { + if (!rootElemRef.current) { + rootElemRef.current = document.createElement('div'); + } + return rootElemRef.current as HTMLDivElement; + } + + return getRootElem(); +} + +export default usePortal; diff --git a/src/components/Widget/components/FullScreenPreview/usePreview.ts b/src/components/Widget/components/FullScreenPreview/usePreview.ts new file mode 100644 index 000000000..11ad6daf7 --- /dev/null +++ b/src/components/Widget/components/FullScreenPreview/usePreview.ts @@ -0,0 +1,132 @@ +import { useState, useReducer } from 'react'; + +type Layout = { + width?: number; + height?: number; +} + +interface STATE { + layout: Layout; + zoom?: boolean + direction: 'vertical' | 'horizontal' +} + +const initState: STATE = { + layout: { width: 800 }, + zoom: false, + direction: 'vertical' +}; + +const usePreview = (zoomStep) => { + const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); + const [fileSize, setFileSize] = useState({ width: 0, height: 0 }); + + const reducer = (state, action) => { + switch (action.type) { + case 'initLayout': + return { + ...state, + layout: action.payload.layout, + direction: action.payload.direction, + zoom: false + }; + case 'zoomIn': + return { + ...state, + layout: action.layout, + zoom: true + }; + case 'zoomOut': + return { + ...state, + layout: action.layout, + zoom: true + }; + case 'resetZoom': + return { ...state, layout: action.layout, direction: action.direction }; + default: + throw new Error('Unexpected action'); + } + }; + + const [state, dispatch] = useReducer(reducer, { ...initState }); + + const initFileSize = (width: number, height: number):void => { + const { innerWidth, innerHeight } = window; + setWindowSize({ width: innerWidth, height: innerHeight }); + // default size + setFileSize({ width, height }); + + const payload: STATE = { layout: {}, direction: 'horizontal' }; + + /** + * Calculate the display ratio of screen to picture + */ + if(innerWidth / innerHeight <= width / height) { + payload.layout.width = innerWidth * 0.8 + payload.direction = 'horizontal' + } else { + payload.layout.height = innerHeight * 0.8 + payload.direction = 'vertical' + } + + dispatch({ + type: 'initLayout', + payload + }); + }; + + const getLayout = (step: number): Layout => { + let layout; + if(state.direction === 'vertical') { + layout = { + height: state.layout.height + step + } + } else { + layout = { + width: state.layout.width + step + } + } + return layout + } + + const isMinSize = (): Boolean => { + if(state.direction === 'vertical') { + return state.layout.height > (windowSize.height / 3) + } + return state.layout.width > windowSize.width / 3 + } + + const onZoomIn = ():void => { + dispatch({ + type: 'zoomIn', + layout: getLayout(zoomStep) + }); + }; + + + const onZoomOut = ():void => { + if (isMinSize()) { + dispatch({ + type: 'zoomOut', + layout: getLayout(-zoomStep) + }); + } + }; + + const onResizePageZoom = ():void => { + if (state.zoom) { + initFileSize(fileSize.width, fileSize.height) + } + }; + + return { + state, + initFileSize, + onZoomIn, + onZoomOut, + onResizePageZoom + }; +}; + +export default usePreview; diff --git a/src/components/Widget/index.tsx b/src/components/Widget/index.tsx index c290bc6a5..3ac70890c 100644 --- a/src/components/Widget/index.tsx +++ b/src/components/Widget/index.tsx @@ -24,6 +24,8 @@ type Props = { launcherCloseLabel: string; sendButtonAlt: string; showTimeStamp: boolean; + imagePreview?: boolean; + zoomStep?: number; handleSubmit?: AnyFunction; } @@ -45,6 +47,8 @@ function Widget({ launcherCloseLabel, sendButtonAlt, showTimeStamp, + imagePreview, + zoomStep, handleSubmit }: Props) { const dispatch = useDispatch(); @@ -93,6 +97,8 @@ function Widget({ launcherCloseLabel={launcherCloseLabel} sendButtonAlt={sendButtonAlt} showTimeStamp={showTimeStamp} + imagePreview={imagePreview} + zoomStep={zoomStep} /> ); } diff --git a/src/components/Widget/layout.tsx b/src/components/Widget/layout.tsx index 8718387de..804e8e835 100644 --- a/src/components/Widget/layout.tsx +++ b/src/components/Widget/layout.tsx @@ -1,12 +1,14 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; +import React,{ useEffect, useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import cn from 'classnames'; import { GlobalState } from 'src/store/types'; import { AnyFunction } from 'src/utils/types'; +import { openFullscreenPreview } from '@actions'; import Conversation from './components/Conversation'; import Launcher from './components/Launcher'; +import FullScreenPreview from './components/FullScreenPreview'; import './style.scss'; @@ -29,6 +31,8 @@ type Props = { launcherCloseLabel: string; sendButtonAlt: string; showTimeStamp: boolean; + imagePreview?: boolean; + zoomStep?: number; } function WidgetLayout({ @@ -49,32 +53,86 @@ function WidgetLayout({ launcherOpenLabel, launcherCloseLabel, sendButtonAlt, - showTimeStamp + showTimeStamp, + imagePreview, + zoomStep, }: Props) { - const { dissableInput, showChat } = useSelector((state: GlobalState) => ({ + const dispatch = useDispatch(); + const { dissableInput, showChat, visible } = useSelector((state: GlobalState) => ({ showChat: state.behavior.showChat, - dissableInput: state.behavior.disabledInput + dissableInput: state.behavior.disabledInput, + visible: state.preview.visible, })); + const messageRef = useRef(null); + + useEffect(() => { + if(showChat) { + messageRef.current = document.getElementById('messages') as HTMLDivElement; + } + return () => { + messageRef.current = null; + } + }, [showChat]) + + const eventHandle = evt => { + if(evt.target && evt.target.className === 'rcw-message-img') { + const { src, alt, naturalWidth, naturalHeight } = (evt.target as HTMLImageElement); + const obj = { + src: src, + alt: alt, + width: naturalWidth, + height: naturalHeight, + }; + dispatch(openFullscreenPreview(obj)) + } + } + + /** + * Previewer needs to prevent body scroll behavior when fullScreenMode is true + */ + useEffect(() => { + const target = messageRef?.current; + if(imagePreview && showChat) { + target?.addEventListener('click', eventHandle, false); + } + + return () => { + target?.removeEventListener('click', eventHandle); + } + }, [imagePreview, showChat]); + + useEffect(() => { + document.body.setAttribute('style', `overflow: ${visible || fullScreenMode ? 'hidden' : 'auto'}`) + }, [fullScreenMode, visible]) + return ( -
- +
+ {showChat && + + } {customLauncher ? customLauncher(onToggleConversation) : !fullScreenMode && @@ -85,6 +143,9 @@ function WidgetLayout({ closeLabel={launcherCloseLabel} /> } + { + imagePreview && + }
); } diff --git a/src/components/Widget/style.scss b/src/components/Widget/style.scss index e36ec2c96..565fea397 100644 --- a/src/components/Widget/style.scss +++ b/src/components/Widget/style.scss @@ -23,3 +23,7 @@ @include widget-container-fs; } } + +.rcw-previewer .rcw-message-img { + cursor: pointer; +} diff --git a/src/index.tsx b/src/index.tsx index 2a54d2b64..4d742b506 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,8 @@ type Props = { launcherCloseLabel?: string, sendButtonAlt?: string; showTimeStamp?: boolean; + imagePreview?: boolean; + zoomStep?: number; handleSubmit?: AnyFunction; } & typeof defaultProps; @@ -46,6 +48,8 @@ function ConnectedWidget({ launcherCloseLabel, sendButtonAlt, showTimeStamp, + imagePreview, + zoomStep, handleSubmit }: Props) { return ( @@ -68,6 +72,8 @@ function ConnectedWidget({ launcherCloseLabel={launcherCloseLabel} sendButtonAlt={sendButtonAlt} showTimeStamp={showTimeStamp} + imagePreview={imagePreview} + zoomStep={zoomStep} handleSubmit={handleSubmit} /> @@ -85,7 +91,9 @@ const defaultProps = { launcherOpenLabel: 'Open chat', launcherCloseLabel: 'Close chat', sendButtonAlt: 'Send', - showTimeStamp: true + showTimeStamp: true, + imagePreview: false, + zoomStep: 80, }; ConnectedWidget.defaultProps = defaultProps; diff --git a/src/store/actions/index.ts b/src/store/actions/index.ts index e56d72dfe..8a12a646f 100644 --- a/src/store/actions/index.ts +++ b/src/store/actions/index.ts @@ -1,7 +1,7 @@ import { ElementType } from 'react'; import * as actionsTypes from './types'; -import { LinkParams } from '../types'; +import { LinkParams, ImageState } from '../types'; export function toggleChat(): actionsTypes.ToggleChat { return { @@ -100,3 +100,16 @@ export function markAllMessagesRead(): actionsTypes.MarkAllMessagesRead { type: actionsTypes.MARK_ALL_READ } } + +export function openFullscreenPreview(payload: ImageState): actionsTypes.FullscreenPreviewActions { + return { + type: actionsTypes.OPEN_FULLSCREEN_PREVIEW, + payload + }; +} + +export function closeFullscreenPreview(): actionsTypes.FullscreenPreviewActions { + return { + type: actionsTypes.CLOSE_FULLSCREEN_PREVIEW + }; +} diff --git a/src/store/actions/types.ts b/src/store/actions/types.ts index 3358350f9..a3acc0a32 100644 --- a/src/store/actions/types.ts +++ b/src/store/actions/types.ts @@ -1,6 +1,6 @@ import { ElementType } from 'react'; -import { LinkParams } from '../types'; +import { LinkParams, FullscreenPreviewState } from '../types'; export const TOGGLE_CHAT = 'BEHAVIOR/TOGGLE_CHAT'; export const TOGGLE_INPUT_DISABLED = 'BEHAVIOR/TOGGLE_INPUT_DISABLED'; @@ -15,6 +15,8 @@ export const HIDE_AVATAR = 'MESSAGES/HIDE_AVATAR'; export const DELETE_MESSAGES = 'MESSAGES/DELETE_MESSAGES'; export const MARK_ALL_READ = 'MESSAGES/MARK_ALL_READ'; export const SET_QUICK_BUTTONS = 'SET_QUICK_BUTTONS'; +export const OPEN_FULLSCREEN_PREVIEW = 'FULLSCREEN/OPEN_PREVIEW'; +export const CLOSE_FULLSCREEN_PREVIEW = 'FULLSCREEN/CLOSE_PREVIEW'; export interface ToggleChat { type: typeof TOGGLE_CHAT; @@ -89,3 +91,14 @@ export type MessagesActions = AddUserMessage | AddResponseMessage | AddLinkSnipp | DropMessages | HideAvatar | DeleteMessages | MarkAllMessagesRead | SetBadgeCount; export type QuickButtonsActions = SetQuickButtons; + +export interface openFullscreenPreview { + type: typeof OPEN_FULLSCREEN_PREVIEW; + payload: FullscreenPreviewState +} + +export interface closeFullscreenPreview { + type: typeof CLOSE_FULLSCREEN_PREVIEW; +} + +export type FullscreenPreviewActions = openFullscreenPreview | closeFullscreenPreview; \ No newline at end of file diff --git a/src/store/dispatcher.ts b/src/store/dispatcher.ts index 0d2ee5386..ca362adad 100644 --- a/src/store/dispatcher.ts +++ b/src/store/dispatcher.ts @@ -2,7 +2,7 @@ import { ElementType } from 'react'; import store from '.'; import * as actions from './actions'; -import { LinkParams } from './types'; +import { LinkParams, ImageState } from './types'; export function addUserMessage(text: string, id?: string) { store.dispatch(actions.addUserMessage(text, id)); @@ -55,3 +55,11 @@ export function markAllAsRead() { export function setBadgeCount(count: number) { store.dispatch(actions.setBadgeCount(count)); } + +export function openFullscreenPreview(payload: ImageState) { + store.dispatch(actions.openFullscreenPreview(payload)); +} + +export function closeFullscreenPreview() { + store.dispatch(actions.closeFullscreenPreview()); +} diff --git a/src/store/index.ts b/src/store/index.ts index dca1fdd06..a70a40e90 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -3,6 +3,7 @@ import { createStore, combineReducers, compose } from 'redux'; import behavior from './reducers/behaviorReducer'; import messages from './reducers/messagesReducer'; import quickButtons from './reducers/quickButtonsReducer'; +import preview from './reducers/fullscreenPreviewReducer'; declare global { interface Window { @@ -11,6 +12,6 @@ declare global { } const composeEnhancers = (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; -const reducer = combineReducers({ behavior, messages, quickButtons }); +const reducer = combineReducers({ behavior, messages, quickButtons, preview }); export default createStore(reducer, composeEnhancers()); diff --git a/src/store/reducers/fullscreenPreviewReducer.ts b/src/store/reducers/fullscreenPreviewReducer.ts new file mode 100644 index 000000000..df8a50548 --- /dev/null +++ b/src/store/reducers/fullscreenPreviewReducer.ts @@ -0,0 +1,27 @@ +import { createReducer } from '../../utils/createReducer'; +import { FullscreenPreviewState, ImageState } from '../types'; + +import { + OPEN_FULLSCREEN_PREVIEW, + CLOSE_FULLSCREEN_PREVIEW, + FullscreenPreviewActions +} from '../actions/types'; + +const initialState = { + src: '', + alt: '', + width: 0, + height: 0, + visible: false +}; + +const fullscreenPreviewReducer = { + [OPEN_FULLSCREEN_PREVIEW]: (state: FullscreenPreviewState, { payload }) => { + const { src, width, height } = payload + return { ...state, src, width, height, visible: true } + }, + + [CLOSE_FULLSCREEN_PREVIEW]: (state: FullscreenPreviewState) => ({ ...initialState }), +}; + +export default (state: FullscreenPreviewState = initialState, action: FullscreenPreviewActions) => createReducer(fullscreenPreviewReducer, state, action); diff --git a/src/store/types.ts b/src/store/types.ts index 71daf09ff..89ca058af 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -52,8 +52,20 @@ export interface QuickButtonsState { quickButtons: QuickButton[]; } +export interface ImageState { + src: string; + alt?: string; + width: number; + height: number; +} + +export interface FullscreenPreviewState extends ImageState { + visible?: boolean; +}; + export interface GlobalState { messages: MessagesState; behavior: BehaviorState; quickButtons: QuickButtonsState; + preview: FullscreenPreviewState; } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index f4f896bb3..65997180d 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -86,7 +86,7 @@ function scrollWithSlowMotion(target: any, scrollStart: any, scroll: number) { raf(step); } -export function scrollToBottom(messagesDiv: HTMLDivElement) { +export function scrollToBottom(messagesDiv: HTMLDivElement | null) { if (!messagesDiv) return; const screenHeight = messagesDiv.clientHeight; const scrollTop = messagesDiv.scrollTop;