diff --git a/src/actions/index.js b/src/actions/index.js index d46ab22..e58d53c 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -752,6 +752,13 @@ export function closeWindow(windowId): Action { }; } +export function removeWindow(windowId): Action { + return { + type: 'REMOVE_WINDOW', + windowId, + }; +} + export function focusWindow(windowId): Action { return { type: 'FOCUS_WINDOW', @@ -797,6 +804,25 @@ export function resizeWindow(windowId, xDiff, yDiff): Action { }; } +export function closeAllWindowTypes(windowType: string): Action { + return { + type: 'CLOSE_ALL_WINDOW_TYPE', + windowType, + }; +} + +export function hideAllWindowTypes( + windowType: string, + hide: boolean, +): Action { + console.log(`hideAllWindowTypes`, hide); + return { + type: 'HIDE_ALL_WINDOW_TYPE', + windowType, + hide, + }; +} + export function openChatWindow(): Action { return openWindow('CHAT', t`Chat`, false, true, { chatChannel: 1, inputMessage: '' }); diff --git a/src/actions/types.js b/src/actions/types.js index efb9b93..6c066de 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -85,6 +85,9 @@ export type Action = args: Object, } | { type: 'CLOSE_WINDOW', windowId: number } + | { type: 'REMOVE_WINDOW', windowId: number } + | { type: 'HIDE_ALL_WINDOW_TYPE', windowType: string, hide: boolean } + | { type: 'CLOSE_ALL_WINDOW_TYPE', windowType: string} | { type: 'FOCUS_WINDOW', windowId: number } | { type: 'CLONE_WINDOW', windowId: number } | { type: 'MAXIMIZE_WINDOW', windowId: number } diff --git a/src/client.js b/src/client.js index a380699..eab7b36 100644 --- a/src/client.js +++ b/src/client.js @@ -63,7 +63,14 @@ function init() { ) => { const state = store.getState(); const { nameRegExp } = state.user; - const isRead = Object.values(state.windows.args).find(((args) => args.chatChannel === channelId)); + + // assume that if one chat window is not hidden, all are + let isRead = state.windows.showWindows + && state.windows.windows.find((win) => win.windowType === 'CHAT' && win.hidden === false) + && Object.values(state.windows.args).find((args) => args.chatChannel === channelId); + isRead = isRead || state.windows.modal.open + && state.windows.args[0].chatChannel === channelId; + const isPing = (nameRegExp && text.match(nameRegExp)); store.dispatch(receiveChatMessage( name, diff --git a/src/components/ChatButton.jsx b/src/components/ChatButton.jsx index 0971b8a..dda5356 100644 --- a/src/components/ChatButton.jsx +++ b/src/components/ChatButton.jsx @@ -6,31 +6,51 @@ import React, { useState, useEffect, } from 'react'; -import { connect } from 'react-redux'; +import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { MdForum } from 'react-icons/md'; import { t } from 'ttag'; -import { openChatWindow } from '../actions'; +import { + hideAllWindowTypes, + openChatWindow, +} from '../actions'; +/* + * return [ showWindows, chatOpen, chatHiden ] + * showWindows: if windows are enabled (false on small screens) + * chatOpen: if any chat window or modal is open + * chatHidden: if any chat windows are hidden + */ +const selectChatWindowStatus = (state) => [ + state.windows.showWindows, + state.windows.windows.find((win) => win.windowType === 'CHAT' && win.hidden === false) || ( + state.windows.modal.open + && state.windows.modal.windowType === 'CHAT' + ), + state.windows.windows.find((win) => win.windowType === 'CHAT' && win.hidden === true), +]; -const ChatButton = ({ - chatNotify, - channels, - unread, - mute, - open, -}) => { +const ChatButton = () => { const [unreadAny, setUnreadAny] = useState(false); - // TODO what do here? - const chatOpen = false; - const modalOpen = false; + + const dispatch = useDispatch(); + + const [showWindows, chatOpen, chatHidden] = useSelector( + selectChatWindowStatus, + shallowEqual, + ); + + const chatNotify = useSelector((state) => state.audio.chatNotify); + const channels = useSelector((state) => state.chat.channels); + const [unread, mute] = useSelector((state) => [state.chatRead.unread, state.chatRead.mute], + shallowEqual); /* * almost the same as in ChannelDropDown * just cares about chatNotify too */ useEffect(() => { - if (!chatNotify || modalOpen || chatOpen) { + if (!chatNotify || chatOpen) { setUnreadAny(false); return; } @@ -57,7 +77,15 @@ const ChatButton = ({
{ + if (chatOpen) { + dispatch(hideAllWindowTypes('CHAT', true)); + } else if (chatHidden && showWindows) { + dispatch(hideAllWindowTypes('CHAT', false)); + } else { + dispatch(openChatWindow()); + } + }} role="button" title={(chatOpen) ? t`Close Chat` : t`Open Chat`} tabIndex={0} @@ -78,31 +106,4 @@ const ChatButton = ({ ); }; -function mapDispatchToProps(dispatch) { - return { - open() { - // dispatch(showChatModal(false)); - dispatch(openChatWindow()); - }, - }; -} - -function mapStateToProps(state) { - const { - chatNotify, - } = state.audio; - const { - channels, - } = state.chat; - const { - unread, - mute, - } = state.chatRead; - return { - channels, - unread, - mute, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(ChatButton); +export default React.memo(ChatButton); diff --git a/src/components/ModalRoot.jsx b/src/components/ModalRoot.jsx index 49e70f1..9fc1f31 100644 --- a/src/components/ModalRoot.jsx +++ b/src/components/ModalRoot.jsx @@ -13,6 +13,7 @@ import { t } from 'ttag'; import { closeWindow, restoreWindow, + removeWindow, } from '../actions'; import COMPONENTS from './windows'; @@ -22,11 +23,15 @@ const ModalRoot = () => { const { windowType, open, title } = useSelector( (state) => state.windows.modal, ); + const showWindows = useSelector((state) => state.windows.showWindows); const dispatch = useDispatch(); const onTransitionEnd = () => { - if (!open) setRender(false); + if (!open) { + setRender(false); + dispatch(removeWindow(0)); + } }; useEffect(() => { @@ -64,6 +69,7 @@ const ModalRoot = () => { title={t`Close`} tabIndex={-1} >
+ {(showWindows) && (
dispatch(restoreWindow())} className="ModalRestore" @@ -72,6 +78,7 @@ const ModalRoot = () => { title={t`Restore`} tabIndex={-1} >↓
+ )}
diff --git a/src/components/Window.jsx b/src/components/Window.jsx index 552e677..729b34f 100644 --- a/src/components/Window.jsx +++ b/src/components/Window.jsx @@ -3,11 +3,14 @@ * @flow */ -import React, { useCallback, useRef } from 'react'; +import React, { + useState, useCallback, useRef, useEffect, +} from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { moveWindow, + removeWindow, resizeWindow, closeWindow, maximizeWindow, @@ -20,6 +23,8 @@ import COMPONENTS from './windows'; const selectWindowById = (state, windowId) => state.windows.windows.find((win) => win.windowId === windowId); const Window = ({ id }) => { + const [render, setRender] = useState(false); + const titleBarRef = useRef(null); const resizeRef = useRef(null); @@ -28,6 +33,18 @@ const Window = ({ id }) => { const dispatch = useDispatch(); const focus = useCallback(() => dispatch(focusWindow(id)), []); + const clone = (evt) => { + evt.stopPropagation(); + dispatch(cloneWindow(id)); + }; + const maximize = (evt) => { + evt.stopPropagation(); + dispatch(maximizeWindow(id)); + }; + const close = (evt) => { + evt.stopPropagation(); + dispatch(closeWindow(id)); + }; useDrag( titleBarRef, @@ -41,39 +58,34 @@ const Window = ({ id }) => { useCallback((xDiff, yDiff) => dispatch(resizeWindow(id, xDiff, yDiff)), []), ); - const clone = (evt) => { - evt.stopPropagation(); - dispatch(cloneWindow(id)); - }; - - const maximize = (evt) => { - evt.stopPropagation(); - dispatch(maximizeWindow(id)); - }; - - const close = (evt) => { - evt.stopPropagation(); - dispatch(closeWindow(id)); - }; - - if (!win) { - return null; - } - const { width, height, xPos, yPos, windowType, title, + open, } = win; + const onTransitionEnd = () => { + if (!open) { + dispatch(removeWindow(id)); + } + }; + + useEffect(() => { + window.setTimeout(() => { + if (open) setRender(true); + }, 10); + }, [open]); + const Content = COMPONENTS[windowType]; console.log(`render window ${id}`); return (
state.windows.windows.map((win) => win.window const WindowsRoot = () => { const windowIds = useSelector(selectWindowIds, shallowEqual); + const showWindows = useSelector((state) => state.windows.showWindows); + + if (!showWindows) return null; return windowIds.map((id) => ( diff --git a/src/reducers/windows.js b/src/reducers/windows.js index 3bd2dc0..78adc2c 100644 --- a/src/reducers/windows.js +++ b/src/reducers/windows.js @@ -12,6 +12,9 @@ const SCREEN_MARGIN_S = 30; const SCREEN_MARGIN_EW = 70; const MIN_WIDTH = 70; const MIN_HEIGHT = 50; +// if screen smaller than this, hide all windows and just +// allow Modals +const SCREEN_WIDTH_THRESHOLD = 604; function generateWindowId(state) { let windowId = Math.floor(Math.random() * 99999) + 1; @@ -24,6 +27,7 @@ function generateWindowId(state) { export type WindowsState = { // modal is considerd as "fullscreen window" // its windowId is considered 0 and args are under args[0] + showWindows: boolean, modal: { windowType: ?string, title: ?string, @@ -32,7 +36,8 @@ export type WindowsState = { // [ // { // windowId: number, - // windowOpen: boolean, + // open: boolean, + // hidden: boolean, // windowType: string, // title: string, // width: number, @@ -52,6 +57,7 @@ export type WindowsState = { } const initialState: WindowsState = { + showWindows: true, modal: { windowType: null, title: null, @@ -70,10 +76,10 @@ export default function windows( const { windowType, title, - fullscreen, cloneable, args, } = action; + const fullscreen = !state.showWindows || action.fullscreen; if (fullscreen) { return { ...state, @@ -98,7 +104,8 @@ export default function windows( { windowId, windowType, - windowOpen: true, + open: true, + hidden: false, title, width: 600, height: 300, @@ -152,25 +159,69 @@ export default function windows( }, }; } - /* - const newWindows = state.windows.map((win) => { - if (win.windowId !== windowId) return win; - return { - ...win, - windowOpen: false, - } - }); + + const newWindows = state.windows.map((win) => { + if (win.windowId !== windowId) return win; return { - ...state, - windows: newWindows, + ...win, + open: false, }; - */ - const args = { ...state.args }; - delete args[windowId]; + }); return { ...state, - windows: state.windows.filter((win) => win.windowId !== windowId), - args, + windows: newWindows, + }; + } + + case 'CLOSE_ALL_WINDOW_TYPE': { + const { + windowType, + } = action; + const newWindows = state.windows.map((win) => { + if (win.windowType !== windowType) return win; + return { + ...win, + open: false, + }; + }); + let { modal } = state; + if (modal.open && modal.windowType === windowType) { + modal = { + ...modal, + open: false, + }; + } + return { + ...state, + modal, + windows: newWindows, + }; + } + + case 'HIDE_ALL_WINDOW_TYPE': { + const { + windowType, + hide, + } = action; + console.log(`hideAllWindowTypes`, windowType, hide); + const newWindows = state.windows.map((win) => { + if (win.windowType !== windowType) return win; + return { + ...win, + hidden: hide, + }; + }); + let { modal } = state; + if (hide && modal.open && modal.windowType === windowType) { + modal = { + ...modal, + open: false, + }; + } + return { + ...state, + modal, + windows: newWindows, }; } @@ -266,7 +317,8 @@ export default function windows( { windowType, windowId, - windowOpen: true, + open: true, + hidden: false, title, width: 600, height: 300, @@ -301,7 +353,7 @@ export default function windows( xPos: clamp( win.xPos + xDiff, -win.width + SCREEN_MARGIN_EW, - width - SCREEN_MARGIN_S, + width - SCREEN_MARGIN_EW, ), yPos: clamp(win.yPos + yDiff, 0, height - SCREEN_MARGIN_S), }; @@ -340,11 +392,20 @@ export default function windows( }; } + case 'RECEIVE_ME': case 'WINDOW_RESIZE': { const { width, height, } = action; + + if (width <= SCREEN_WIDTH_THRESHOLD) { + return { + ...state, + showWindows: false, + }; + } + const xMax = width - SCREEN_MARGIN_EW; const yMax = height - SCREEN_MARGIN_S; let modified = false; @@ -373,18 +434,13 @@ export default function windows( } } - if (!modified) return state; - return { ...state, - windows: newWindows, + showWindows: true, + windows: (modified) ? newWindows : state.windows, }; } - case 'CLOSE_ALL_WINDOWS': { - return initialState; - } - /* * args specific actions */ diff --git a/src/styles/default.css b/src/styles/default.css index 2a6d1c2..d4f58eb 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -127,20 +127,6 @@ tr:nth-child(even) { background-color: #dddddd; } -.chatbox { - position: fixed; - background-color: rgba(226, 226, 226, 0.92); - border: solid black; - border-width: thin; - width: 0px; - height: 36px; - bottom: 16px; - right: 98px; - visibility: hidden; - overflow: hidden; - transition: 0.3s; -} - .window { position: fixed; background-color: rgba(226, 226, 226, 0.92); @@ -149,6 +135,8 @@ tr:nth-child(even) { overflow: hidden; padding: 3px; z-index: 3; + transition: opacity 200ms ease-in-out; + opacity: 0; } .win-topbar { @@ -182,7 +170,7 @@ tr:nth-child(even) { .win-resize { position: absolute; - bottom: -3px; + bottom: -4px; right: -3px; font-size: 22px; cursor: se-resize; @@ -227,14 +215,6 @@ tr:nth-child(even) { cursor: pointer; } -.chatbox.show { - height: 200px; - width: 350px; - bottom: 16px; - right: 98px; - visibility: visible; -} - .channelbtn { position: relative; background-color: #ebebeb; @@ -397,6 +377,7 @@ tr:nth-child(even) { top: 50%; left: 50%; right: auto; + padding: 0px 5px 5px 5px; bottom: auto; border: 1px solid rgb(204, 204, 204); background: rgb(255, 255, 255) none repeat scroll 0% 0%; @@ -802,6 +783,6 @@ tr:nth-child(even) { visibility: hidden; } -.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show { +.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show, .window.show { opacity: 1; }