From 3068dfe63755d21c35c99cf3826dd8ca89f73136 Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Wed, 19 May 2021 20:48:19 +0300 Subject: [PATCH] post messages from iframe - fix user info opening - remove iframe height checks by interval - update height by mutation observer events - rename events --- frontend/app/common/user-info-settings.ts | 9 --- .../app/components/auth-panel/auth-panel.tsx | 6 +- frontend/app/components/auth/auth.hooks.ts | 35 ++++------ frontend/app/components/comment/comment.tsx | 37 +++++----- frontend/app/components/dropdown/dropdown.tsx | 34 ++++------ frontend/app/components/root/root.tsx | 22 +++--- .../app/components/user-info/user-info.tsx | 13 ++-- frontend/app/embed.ts | 68 +++++++++---------- frontend/app/remark.tsx | 2 +- frontend/app/store/user-info/actions.ts | 11 ++- frontend/app/utils/parseQuery.ts | 2 +- frontend/app/utils/postMessage.ts | 47 +++++++++---- frontend/templates/iframe.ejs | 17 +---- 13 files changed, 139 insertions(+), 164 deletions(-) delete mode 100644 frontend/app/common/user-info-settings.ts diff --git a/frontend/app/common/user-info-settings.ts b/frontend/app/common/user-info-settings.ts deleted file mode 100644 index 357a8810..00000000 --- a/frontend/app/common/user-info-settings.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { parseQuery } from 'utils/parseQuery'; - -import type { UserInfo } from './types'; - -export const userInfo: UserInfo = parseQuery(); - -export const id = userInfo.id; -export const name = userInfo.name; -export const picture = userInfo.picture; diff --git a/frontend/app/components/auth-panel/auth-panel.tsx b/frontend/app/components/auth-panel/auth-panel.tsx index c36bbcb6..2a558045 100644 --- a/frontend/app/components/auth-panel/auth-panel.tsx +++ b/frontend/app/components/auth-panel/auth-panel.tsx @@ -6,7 +6,7 @@ import b from 'bem-react-helper'; import { User, Sorting, Theme, PostInfo } from 'common/types'; import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants'; import { requestDeletion } from 'utils/email'; -import { postMessage } from 'utils/postMessage'; +import { postMessageToParent } from 'utils/postMessage'; import { getHandleClickProps } from 'common/accessibility'; import { StoreState } from 'store'; import { Dropdown, DropdownItem } from 'components/dropdown'; @@ -75,11 +75,11 @@ class AuthPanelComponent extends Component { toggleUserInfoVisibility = () => { const { user } = this.props; - if (user === null) { + if (!user) { return; } - postMessage({ isUserInfoShown: true, user }); + postMessageToParent({ profile: user }); }; renderAuthorized = (user: User) => { diff --git a/frontend/app/components/auth/auth.hooks.ts b/frontend/app/components/auth/auth.hooks.ts index 3e9d389e..38fb8042 100644 --- a/frontend/app/components/auth/auth.hooks.ts +++ b/frontend/app/components/auth/auth.hooks.ts @@ -1,14 +1,11 @@ import { useEffect, useRef, useState } from 'preact/hooks'; +import { parseMessage, postMessageToParent } from 'utils/postMessage'; function handleChangeIframeSize(element: HTMLElement) { const { top } = element.getBoundingClientRect(); - const height = window.scrollY + Math.abs(top) + element.scrollHeight + 20; + const height = Math.max(window.scrollY + Math.abs(top) + element.scrollHeight + 20, document.body.offsetHeight); - if (window.innerHeight > height) { - return; - } - - document.body.style.setProperty('min-height', `${height}px`); + postMessageToParent({ height }); } export function useDropdown(disableClosing?: boolean) { @@ -19,28 +16,20 @@ export function useDropdown(disableClosing?: boolean) { }; useEffect(() => { - if (!showDropdown) { + const dropdownElement = rootRef.current; + + if (!showDropdown || !dropdownElement) { return; } - const dropdownElement = rootRef.current; - - handleChangeIframeSize(dropdownElement); - function handleMessageFromParent(evt: MessageEvent) { - if (typeof evt.data !== 'string' || disableClosing) { + const data = parseMessage(evt); + + if (disableClosing && data.clickOutside) { return; } - try { - const data = JSON.parse(evt.data); - - if (!data.clickOutside) { - return; - } - - setShowDropdown(false); - } catch (e) {} + setShowDropdown(false); } function handleClickOutside(evt: MouseEvent) { @@ -64,9 +53,13 @@ export function useDropdown(disableClosing?: boolean) { const dropdownElement = rootRef.current; if (!dropdownElement || !showDropdown) { + handleChangeIframeSize(document.body); + return; } + handleChangeIframeSize(dropdownElement); + const observer = new MutationObserver(() => { handleChangeIframeSize(dropdownElement); }); diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index 69ed9de9..44746c89 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -16,7 +16,7 @@ import { Avatar } from 'components/avatar'; import { Button } from 'components/button'; import { Countdown } from 'components/countdown'; import { getPreview, uploadImage } from 'common/api'; -import { postMessage } from 'utils/postMessage'; +import { postMessageToParent } from 'utils/postMessage'; import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage'; import { getBlockingDurations } from './getBlockingDurations'; @@ -211,14 +211,7 @@ export class Comment extends Component { }; toggleUserInfoVisibility = () => { - if (!window.parent) { - return; - } - - const { user } = this.props.data; - const data = JSON.stringify({ isUserInfoShown: true, user }); - - window.parent.postMessage(data, '*'); + postMessageToParent({ profile: this.props.data.user }); }; togglePin = () => { @@ -357,21 +350,23 @@ export class Comment extends Component { this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None }); }; - scrollToParent = (e: Event) => { - const { - data: { pid }, - } = this.props; - - e.preventDefault(); - + scrollToParent = (evt: Event) => { + const { pid } = this.props.data; const parentCommentNode = document.getElementById(`${COMMENT_NODE_CLASSNAME_PREFIX}${pid}`); - if (parentCommentNode) { - const top = parentCommentNode.getBoundingClientRect().top; - if (!postMessage({ scrollTo: top })) { - parentCommentNode.scrollIntoView(); - } + evt.preventDefault(); + + if (!parentCommentNode) { + return; } + + const top = parentCommentNode.getBoundingClientRect().top; + + if (postMessageToParent({ scrollTo: top })) { + return; + } + + parentCommentNode.scrollIntoView(); }; copyComment = () => { diff --git a/frontend/app/components/dropdown/dropdown.tsx b/frontend/app/components/dropdown/dropdown.tsx index 70af3682..aa3b90b6 100644 --- a/frontend/app/components/dropdown/dropdown.tsx +++ b/frontend/app/components/dropdown/dropdown.tsx @@ -134,25 +134,23 @@ export class Dropdown extends Component { }); } - receiveMessage(event: MessageEvent<{ clickOutside?: boolean }>) { - try { - const data = parseMessage(event); + receiveMessage(evt: MessageEvent) { + const data = parseMessage(evt); - if (!data || !data.clickOutside || !this.state.isActive) { - return; + if (!data.clickOutside || !this.state.isActive) { + return; + } + + this.setState( + { + contentTranslateX: 0, + isActive: false, + }, + () => { + this.__onClose(); + this.props.onClose?.(this.rootNode.current!); } - - this.setState( - { - contentTranslateX: 0, - isActive: false, - }, - () => { - this.__onClose(); - this.props.onClose && this.props.onClose(this.rootNode.current!); - } - ); - } catch (e) {} + ); } onOutsideClick(e: MouseEvent) { @@ -171,13 +169,11 @@ export class Dropdown extends Component { componentDidMount() { document.addEventListener('click', this.onOutsideClick); - window.addEventListener('message', this.receiveMessage); } componentWillUnmount() { document.removeEventListener('click', this.onOutsideClick); - window.removeEventListener('message', this.receiveMessage); } diff --git a/frontend/app/components/root/root.tsx b/frontend/app/components/root/root.tsx index 2ea15eed..d2c9dbed 100644 --- a/frontend/app/components/root/root.tsx +++ b/frontend/app/components/root/root.tsx @@ -4,7 +4,7 @@ import b from 'bem-react-helper'; import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl'; import clsx from 'clsx'; -import type { Sorting, Theme } from 'common/types'; +import type { Sorting } from 'common/types'; import type { StoreState } from 'store'; import { COMMENT_NODE_CLASSNAME_PREFIX, @@ -39,7 +39,7 @@ import { ConnectedComment as Comment } from 'components/comment/connected-commen import { uploadImage, getPreview } from 'common/api'; import { isUserAnonymous } from 'utils/isUserAnonymous'; import { bindActions } from 'utils/actionBinder'; -import { postMessage, parseMessage } from 'utils/postMessage'; +import { postMessageToParent, parseMessage } from 'utils/postMessage'; import { useActions } from 'hooks/useAction'; import { setCollapse } from 'store/thread/actions'; import { logout } from 'components/auth/auth.api'; @@ -122,7 +122,7 @@ export class Root extends Component { const userloading = this.props.fetchUser().finally(() => this.setState({ isUserLoading: false })); Promise.all([userloading, this.props.fetchComments()]).finally(() => { - postMessage({ remarkIframeHeight: document.body.offsetHeight }); + postMessageToParent({ height: document.body.offsetHeight }); setTimeout(this.checkUrlHash); window.addEventListener('hashchange', this.checkUrlHash); }); @@ -169,7 +169,7 @@ export class Root extends Component { toMessage = (hash: string) => { const comment = document.querySelector(hash); if (comment) { - postMessage({ scrollTo: comment.getBoundingClientRect().top }); + postMessageToParent({ scrollTo: comment.getBoundingClientRect().top }); comment.classList.add('comment_highlighting'); setTimeout(() => { comment.classList.remove('comment_highlighting'); @@ -177,14 +177,14 @@ export class Root extends Component { } }; - onMessage(event: MessageEvent<{ theme?: Theme }>) { - try { - const data = parseMessage(event); + onMessage(event: MessageEvent) { + const data = parseMessage(event); - if (data.theme && THEMES.includes(data.theme)) { - this.props.setTheme(data.theme); - } - } catch (e) {} + if (!data.theme || !THEMES.includes(data.theme)) { + return; + } + + this.props.setTheme(data.theme); } onBlockedUsersShow = async () => { diff --git a/frontend/app/components/user-info/user-info.tsx b/frontend/app/components/user-info/user-info.tsx index 65b5c683..7dd3f62b 100644 --- a/frontend/app/components/user-info/user-info.tsx +++ b/frontend/app/components/user-info/user-info.tsx @@ -6,9 +6,9 @@ import { useIntl, defineMessages, FormattedMessage, IntlShape } from 'react-intl import { StoreState } from 'store'; import { Comment } from 'common/types'; import { fetchInfo } from 'store/user-info/actions'; -import { userInfo } from 'common/user-info-settings'; -import { postMessage } from 'utils/postMessage'; +import { parseQuery } from 'utils/parseQuery'; import { bindActions } from 'utils/actionBinder'; +import { postMessageToParent } from 'utils/postMessage'; import { useActions } from 'hooks/useAction'; import { Avatar } from 'components/avatar'; @@ -23,6 +23,8 @@ const messages = defineMessages({ }, }); +const user = parseQuery(); + type Props = { comments: Comment[] | null; } & typeof boundActions & { intl: IntlShape }; @@ -38,7 +40,7 @@ class UserInfo extends Component { componentWillMount(): void { if (!this.props.comments && this.state.isLoading) { this.props - .fetchInfo() + .fetchInfo(user.id) .then(() => { this.setState({ isLoading: false }); }) @@ -55,7 +57,6 @@ class UserInfo extends Component { } render(): JSX.Element | null { - const user = userInfo; const { comments = [] } = this.props; const { isLoading } = this.state; @@ -87,12 +88,12 @@ class UserInfo extends Component { static onKeyDown(e: KeyboardEvent): void { // ESCAPE key pressed if (e.keyCode === 27) { - postMessage({ isUserInfoShown: false }); + postMessageToParent({ profile: null }); } } } -const commentsSelector = (state: StoreState) => state.userComments[userInfo.id]; +const commentsSelector = (state: StoreState) => state.userComments[user.id]; export const ConnectedUserInfo: FunctionComponent = () => { const comments = useSelector(commentsSelector); diff --git a/frontend/app/embed.ts b/frontend/app/embed.ts index 4cd970fc..1ea47532 100644 --- a/frontend/app/embed.ts +++ b/frontend/app/embed.ts @@ -1,6 +1,6 @@ import type { UserInfo, Theme } from 'common/types'; import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config'; -import { parseMessage, ParentMessage } from 'utils/postMessage'; +import { parseMessage, postMessageToIframe } from 'utils/postMessage'; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); @@ -264,59 +264,55 @@ function createInstance(config: typeof window.remark_config) { }, }; - function receiveMessages(event: MessageEvent): void { - try { - const data = parseMessage(event); + function receiveMessages(event: MessageEvent): void { + const data = parseMessage(event); - if (data.remarkIframeHeight) { - iframe.style.height = `${data.remarkIframeHeight}px`; - } + if (data.height) { + iframe.style.height = `${data.height}px`; + } - if (data.scrollTo) { - window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset); - } + if (data.scrollTo) { + window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset); + } - if (Object.prototype.hasOwnProperty.call(data, 'isUserInfoShown')) { - if (data.isUserInfoShown) { - userInfo.init(data.user); - } else { - userInfo.close(); - } + if (typeof data.profile === 'object') { + if (data.profile === null) { + userInfo.close(); + } else { + userInfo.init(data.profile); } + } - if (data.inited) { - postHashToIframe(); - postTitleToIframe(document.title); - } - } catch (e) {} + if (data.inited) { + postHashToIframe(); + postTitleToIframe(document.title); + } } - function postHashToIframe(e?: Event & { newURL: string }) { - const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash; + function postHashToIframe(evt?: Event & { newURL: string }) { + const hash = evt ? `#${evt.newURL.split('#')[1]}` : window.location.hash; - if (hash.indexOf(`#${COMMENT_NODE_CLASSNAME_PREFIX}`) === 0) { - if (e) e.preventDefault(); - - iframe.contentWindow!.postMessage({ hash }, '*'); + if (!hash.startsWith(`#${COMMENT_NODE_CLASSNAME_PREFIX}`)) { + return; } + + evt?.preventDefault(); + postMessageToIframe(iframe, { hash }); } function postTitleToIframe(title: string) { - if (iframe.contentWindow) { - iframe.contentWindow.postMessage({ title }, '*'); - } + postMessageToIframe(iframe, { title }); } - function postClickOutsideToIframe(e: MouseEvent) { - if (iframe.contentWindow && !iframe.contains(e.target as Node)) { - iframe.contentWindow.postMessage({ clickOutside: true }, '*'); + function postClickOutsideToIframe(evt: MouseEvent) { + if (iframe.contains(evt.target as Node)) { + return; } + postMessageToIframe(iframe, { clickOutside: true }); } function changeTheme(theme: Theme) { - if (iframe.contentWindow) { - iframe.contentWindow.postMessage({ theme }, '*'); - } + postMessageToIframe(iframe, { theme }); } function destroy() { diff --git a/frontend/app/remark.tsx b/frontend/app/remark.tsx index b3aa2b80..b7b77838 100644 --- a/frontend/app/remark.tsx +++ b/frontend/app/remark.tsx @@ -31,7 +31,7 @@ async function init(): Promise { throw new Error("Remark42: Can't find root node."); } - const params = parseQuery<{ page?: string; locale?: string; simple_view?: boolean }>(); + const params = parseQuery(); const locale = getLocale(params); const messages = await loadLocale(locale).catch(() => ({})); const boundActions = bindActionCreators({ fetchHiddenUsers, restoreCollapsedThreads }, store.dispatch); diff --git a/frontend/app/store/user-info/actions.ts b/frontend/app/store/user-info/actions.ts index 030571d9..77c35640 100644 --- a/frontend/app/store/user-info/actions.ts +++ b/frontend/app/store/user-info/actions.ts @@ -1,19 +1,16 @@ import { getUserComments } from 'common/api'; import { Comment } from 'common/types'; -import { userInfo } from 'common/user-info-settings'; import { StoreAction } from '../index'; import { USER_INFO_SET } from './types'; -export const fetchInfo = (): StoreAction> => async (dispatch) => { - if (!userInfo.id) { - return null; - } +export const fetchInfo = (id: string): StoreAction> => async (dispatch) => { // TODO: limit - const info = await getUserComments(userInfo.id, 10); + const info = await getUserComments(id, 10); + dispatch({ type: USER_INFO_SET, - id: userInfo.id, + id, comments: info.comments, }); return info.comments; diff --git a/frontend/app/utils/parseQuery.ts b/frontend/app/utils/parseQuery.ts index 97b4e158..7be355a7 100644 --- a/frontend/app/utils/parseQuery.ts +++ b/frontend/app/utils/parseQuery.ts @@ -1,6 +1,6 @@ /** converts window.location.search into object */ -export function parseQuery(search: string = window.location.search): T { +export function parseQuery>(search: string = window.location.search): T { const params: { [key: string]: string } = {}; new URLSearchParams(search).forEach((value: string, key: string) => { params[key] = value; diff --git a/frontend/app/utils/postMessage.ts b/frontend/app/utils/postMessage.ts index da58dde1..776b487c 100644 --- a/frontend/app/utils/postMessage.ts +++ b/frontend/app/utils/postMessage.ts @@ -1,42 +1,61 @@ import type { Theme, UserInfo } from 'common/types'; -export type ParentMessage = { +type ParentMessage = { inited?: true; scrollTo?: number; - remarkIframeHeight?: number; -} & ( - | { isUserInfoShown: true; user: UserInfo } - | { isUserInfoShown: false; user?: never } - | { isUserInfoShown?: never; user?: never } -); + height?: number; + profile?: UserInfo | null; +}; -export type ChildMessage = { +type ChildMessage = { + clickOutside?: true; + hash?: string; + title?: string; theme?: Theme; }; -type AllMessages = ParentMessage & ChildMessage; +type AllMessages = ChildMessage & ParentMessage; /** * Sends message to parent window * * @returns request success of fail */ -export function postMessage(data: AllMessages): boolean { - if (!window.parent || window.parent === window) return false; +export function postMessageToParent(data: ParentMessage): boolean { + if (!window.parent || window.parent === window) { + return false; + } + window.parent.postMessage(data, '*'); return true; } +/** + * Sends message to target iframe + * + * @param target iframe to send data + * @param data that will be send to iframe + * @returns request success of fail + */ +export function postMessageToIframe(target: HTMLIFrameElement, data: ChildMessage): boolean { + if (!target?.contentWindow) { + return false; + } + + target.contentWindow.postMessage(data, '*'); + return true; +} + /** * Parses data from post message that was received in iframe * * @param evt post message event * @returns */ -export function parseMessage({ data }: MessageEvent): T { +export function parseMessage({ data }: MessageEvent): AllMessages { if (typeof data !== 'object' || data === null || Array.isArray(data)) { - return {} as T; + return {} as AllMessages; } - return data as T; + return data as AllMessages; } diff --git a/frontend/templates/iframe.ejs b/frontend/templates/iframe.ejs index 4ff54472..84df3008 100644 --- a/frontend/templates/iframe.ejs +++ b/frontend/templates/iframe.ejs @@ -127,20 +127,7 @@ <% } %>