diff --git a/package.json b/package.json
index 0b879dd8..d94394cd 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,13 @@
},
"scripts": {
"clean": "rm -rf public/assets public/index.html",
+ "clean-w": "rmdir /s /q public\\assets && del /f /q public\\index.html",
"build": "npm run clean && npm run build:bundle",
+ "build-w": "npm run clean-w && npm run build:bundle",
"build:bundle": "webpack --progress",
"build:production": "npm run --prod build",
"dev-server": "npm run build && webpack-dev-server --inline --hot",
+ "dev-server-w": "npm run build-w && webpack-dev-server --inline --hot",
"flow": "flow check",
"lint": "eslint webpack.config.js --ext .js --ext .jsx src"
},
diff --git a/src/INITIAL_STATE.js b/src/INITIAL_STATE.js
index be3541bd..e0ee34f8 100644
--- a/src/INITIAL_STATE.js
+++ b/src/INITIAL_STATE.js
@@ -13,6 +13,7 @@ export default {
chatSize: localStorage ? Number(localStorage.getItem('chatSize')) || 400 : 400,
showChat: localStorage ? !(localStorage.getItem('showChat') === 'false') : true,
showHeader: true,
+ imageModalSrc: null,
showFooter: true,
},
self: {
diff --git a/src/actions/index.js b/src/actions/index.js
index 26c45c36..7803a371 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -175,6 +175,14 @@ export const showHeader = value => dispatch => {
});
};
+export const IMAGE_MODAL_SRC = Symbol('IMAGE_MODAL_SRC');
+export const imageModalSrc = value => dispatch => {
+ dispatch({
+ type: IMAGE_MODAL_SRC,
+ payload: value,
+ });
+};
+
export const SHOW_FOOTER = Symbol('SHOW_FOOTER');
export const showFooter = value => dispatch => {
dispatch({
diff --git a/src/client.jsx b/src/client.jsx
index 279e6a8d..aab3ac2a 100644
--- a/src/client.jsx
+++ b/src/client.jsx
@@ -4,8 +4,20 @@ import { render } from 'react-dom';
import App from './components/App';
import store from './store';
-
const mountPoint = document.getElementById('main');
+
+if (process.env.NODE_ENV !== 'production') {
+ // used for testing redux dispatch events in console, since we're using symbols
+ // they need to be attached to the window for them to match
+ // otherwise it goes into a catchall(default)
+ // you can now dispatch events using similar structure to line below
+ // window.store.dispatch({type:window.types.IMAGE_MODAL_SRC, payload:"url"})
+ import("./actions/index").then((types) => {
+ window.store = store;
+ window.types = types;
+ });
+}
+
render(
,
mountPoint,
diff --git a/src/components/ImagePreviewModal.jsx b/src/components/ImagePreviewModal.jsx
new file mode 100644
index 00000000..7d5961d7
--- /dev/null
+++ b/src/components/ImagePreviewModal.jsx
@@ -0,0 +1,82 @@
+import React from "react";
+import cs from "classnames";
+
+import "../css/ImageModal";
+import { connect } from "react-redux";
+import { imageModalSrc } from "../actions";
+
+const ImageModal = ({ dispatch, src, onClose }) => {
+ const modalCopyButton = React.useRef();
+
+ const copyToClipboard = () => {
+ modalCopyButton.current.classList.remove("copySuccessful");
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ modalCopyButton.current.classList.add("copySuccessful");
+ });
+ });
+
+ const url = src;
+ navigator.clipboard.writeText(url);
+ };
+
+ return (
+
{
+ if (e.key === "Escape") dispatch(imageModalSrc(null));
+ }}
+ >
+
{
+ dispatch(imageModalSrc(null));
+ }}
+ >
+
+
e.stopPropagation()}
+ >
+
+
+
+
+
+
+
+

+
+
+ );
+};
+
+export default connect()(ImageModal);
diff --git a/src/components/RoutesWithChat.jsx b/src/components/RoutesWithChat.jsx
index 107458f7..b0d44bd1 100644
--- a/src/components/RoutesWithChat.jsx
+++ b/src/components/RoutesWithChat.jsx
@@ -13,6 +13,7 @@ import Header from './Header';
import Footer from './Footer';
import CustomScrollbar from './CustomScrollbar';
+import ImageModal from './ImagePreviewModal';
import '../css/Stream';
@@ -22,7 +23,7 @@ import {
showHeader as headerFunc
} from '../actions';
-export const RoutesWithChat = ({showHeader, showFooter, setChatSize, showChat, showLeftChat=false, chatClosed, chatSize, headerFunc}) =>
+export const RoutesWithChat = ({showHeader, showFooter, setChatSize, showChat, showLeftChat=false, chatClosed, chatSize, headerFunc, imageModalSrc}) =>
{
let left = (
@@ -53,6 +54,11 @@ export const RoutesWithChat = ({showHeader, showFooter, setChatSize, showChat, s
showHeader ? headerFunc(false) : headerFunc(true)}>›
+ {imageModalSrc && (
+
+ )}
{
@@ -80,6 +86,8 @@ RoutesWithChat.propTypes = {
showLeftChat: PropTypes.bool,
chatClosed: PropTypes.bool,
+ imageModalSrc: PropTypes.string,
+
chatSize: PropTypes.number.isRequired,
setChatSize: PropTypes.func.isRequired,
@@ -96,6 +104,7 @@ export default compose(
showLeftChat: idx(state, _ => _.self.profile.data.left_chat),
chatClosed: !state.ui.showChat || !state.self.isLoggedIn,
headerClosed: !state.ui.showHeader,
+ imageModalSrc: state.ui.imageModalSrc,
}),
{
setChatSize,
diff --git a/src/css/ImageModal.scss b/src/css/ImageModal.scss
new file mode 100644
index 00000000..6620deae
--- /dev/null
+++ b/src/css/ImageModal.scss
@@ -0,0 +1,66 @@
+// modal when image is opened
+.image-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+}
+
+.image-modal__backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.8);
+}
+
+.image-modal__content {
+ height: 100%;
+ place-items: end;
+ margin-left: auto;
+ margin-right: auto;
+ display: flex;
+ justify-content: center;
+ flex-flow: column;
+ width: max-content;
+ z-index: 100;
+ position: relative;
+}
+
+.image-modal__btn {
+ margin-left: 15px;
+ cursor: pointer;
+ &.copySuccessful {
+ animation: greenFade 1000ms 1;
+ &:hover {
+ color: darken(#d17134, 20%);
+ }
+ }
+}
+
+@keyframes greenFade {
+ 0% {
+ color: green;
+ &:hover {
+ color: green;
+ }
+ }
+ 100% {
+ color: #d17134;
+ &:hover {
+ color: darken(#d17134, 20%);
+ }
+ }
+}
+
+.image-modal__toolbar {
+ margin-bottom: 10px;
+ button {
+ span {
+ font-size: 25px;
+ }
+ }
+}
+
+.image-modal img {
+ max-width: 90vw;
+ max-height: 90vh;
+ user-select: none;
+}
diff --git a/src/css/main.scss b/src/css/main.scss
index 52a1df9b..4b8562f5 100644
--- a/src/css/main.scss
+++ b/src/css/main.scss
@@ -60,6 +60,15 @@ a {
}
}
+.button-orange{
+ border: none;
+ background-color: transparent;
+ color: $rustle-orange;
+ &:hover, &:focus {
+ color: darken($rustle-orange, 20%);
+ }
+}
+
.form-control, .form-control, .btn-group > .navbar-btn, .input-group-btn > .btn {
background-color: #222;
color: #fff;
diff --git a/src/reducers/ui.js b/src/reducers/ui.js
index 92ba5569..93330e95 100644
--- a/src/reducers/ui.js
+++ b/src/reducers/ui.js
@@ -6,6 +6,7 @@ import {
SHOW_CHAT,
SHOW_HEADER,
SHOW_FOOTER,
+ IMAGE_MODAL_SRC,
} from '../actions';
import { actions } from '../actions/websocket';
@@ -38,6 +39,11 @@ function uiReducer(state = INITIAL_STATE.ui, action) {
...state,
showHeader: action.payload,
};
+ case IMAGE_MODAL_SRC:
+ return {
+ ...state,
+ imageModalSrc: action.payload,
+ };
case SHOW_FOOTER:
return {
...state,
diff --git a/src/redux/types.js b/src/redux/types.js
index a973a1f4..795b743a 100644
--- a/src/redux/types.js
+++ b/src/redux/types.js
@@ -51,5 +51,6 @@ export type State = {|
+showChat: boolean,
+showHeader: boolean,
+showFooter: boolean,
+ +imageModalSrc: string,
|}
|};