Skip to content

Commit

Permalink
GH-67: Add image preview and lightbox
Browse files Browse the repository at this point in the history
  • Loading branch information
SetZero committed Jun 28, 2023
1 parent a52b2eb commit 7d27961
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 189 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@lexical/react": "^0.11.1",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.126",
"@mui/material": "^5.12.0",
Expand All @@ -27,7 +26,8 @@
"dompurify": "^3.0.2",
"fuse.js": "^6.6.2",
"highlight.js": "^11.8.0",
"lexical": "^0.11.1",
"html-react-parser": "^4.0.0",
"lru-cache": "^10.0.0",
"marked": "^5.0.2",
"marked-highlight": "^2.0.0",
"react": "^18.2.0",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ image = "0.24.6"
directories = "5.0.1"
num-traits = "0.2"
brotli = "3.3.4"
webbrowser = "0.8.10"

[dev-dependencies]
tempfile = "3.5.0"
Expand Down
14 changes: 14 additions & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use base64::{engine::general_purpose, Engine};
use tauri::State;
use tokio::sync::Mutex;
use tracing::{error, info, trace};
use webbrowser::{Browser, BrowserOptions};

pub struct ConnectionState {
pub connection: Mutex<Option<Connection>>,
Expand Down Expand Up @@ -316,3 +317,16 @@ pub fn unzip_data_from_utf8(data: &str) -> Result<String, String> {
let result = String::from_utf8(output).map_err(|e| e.to_string())?;
Ok(result)
}

#[tauri::command]
pub fn open_browser(url: &str) -> Result<(), String> {
if let Err(e) = webbrowser::open_browser_with_options(
Browser::Default,
url,
BrowserOptions::new().with_suppress_output(false),
) {
return Err(format!("{e:?}"));
}

Ok(())
}
6 changes: 4 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use tracing_subscriber::fmt;

use crate::commands::{
change_user_state, connect_to_server, get_audio_devices, get_server_list, like_message, logout,
save_server, send_message, set_user_image, unzip_data_from_utf8, zip_data_to_utf8,
open_browser, save_server, send_message, set_user_image, unzip_data_from_utf8,
zip_data_to_utf8,
};

fn init_logging() {
Expand Down Expand Up @@ -68,7 +69,8 @@ fn main() {
change_user_state,
get_audio_devices,
zip_data_to_utf8,
unzip_data_from_utf8
unzip_data_from_utf8,
open_browser
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
21 changes: 14 additions & 7 deletions src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { RootState } from "../store/store";
import { useSelector } from "react-redux";
import "./styles/ChatMessage.css";
import UserInfoPopover from "./UserInfoPopover";
import MessageUIHelper from "../helper/MessageUIHelper";
import { m } from "@tauri-apps/api/dialog-15855a2f";


interface ChatMessageProps {
Expand All @@ -33,13 +35,18 @@ const parseMessage = (message: string | undefined) => {
.parseForImages()
.parseForLinks()
)
.build();
.buildString();

return (
<div>
{messageParser}
</div>
)
return messageParser;
}

return message;
}
const parseUI = (message: string | undefined) => {
if (message && message.includes('<')) {
let messageParser = new MessageUIHelper(message);

return messageParser.build();
}

return message;
Expand All @@ -66,7 +73,7 @@ const ChatMessage: React.FC<ChatMessageProps> = React.memo(({ message, messageId
userList.users.find(e => e.id === message.sender.user_id)
, [userList, message.sender.user_id]);

const parsedMessage = React.useMemo(() => parseMessage(message.message), [message.message]);
const parsedMessage = React.useMemo(() => parseUI(parseMessage(message.message)), [message.message]);
const date = React.useMemo(() => generateDate(message.timestamp), [message.timestamp]);

const deleteMessageEvent = React.useCallback(() => {
Expand Down
41 changes: 41 additions & 0 deletions src/components/LightBoxImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Backdrop, Box, Container, Link, Popover, } from "@mui/material";
import { UsersState } from "../store/features/users/userSlice";
import "./styles/UserInfo.css";
import "./styles/common.css"
import UserInfo from "./UserInfo";
import { useState } from "react";
import { openInBrowser } from "../helper/BrowserUtils";

interface LightBoxImageProps {
src: string;
}

function LightBoxImage(props: LightBoxImageProps) {
const [open, setOpen]: any = useState(false);

const handleClose = () => {
setOpen(false);
};

return (
<Box>
<img src={props.src} onClick={() => setOpen(true)} style={{ maxWidth: '100%', maxHeight: '100%', cursor: 'pointer' }} />
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1, backdropFilter: 'blur(5px)', padding: '50px 10px 10px 10px' }}
open={open}
onClick={handleClose}
>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
<Box sx={{ flexShrink: 0, display: 'contents' }}>
<img src={props.src} style={{ height: 'auto', width: 'auto', maxWidth: '100%', maxHeight: 'calc(100% - 2em)', objectFit: 'contain' }} />
</Box>
<Box sx={{ flexShrink: 1, textAlign: 'center'}}>
<Link href="#" color="inherit" underline="hover" onClick={() => openInBrowser(props.src)}>Open In Browser</Link>
</Box>
</Box>
</Backdrop>
</Box>
);
}

export default LightBoxImage;
2 changes: 2 additions & 0 deletions src/components/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function UserInfo(props: UserInfoProps) {

let mutedText = props.userInfo?.mutedSince ? dayjs(props.userInfo?.mutedSince).fromNow() : '';
let deafenedText = props.userInfo?.deafenedSince ? dayjs(props.userInfo?.deafenedSince).fromNow() : '';
let joinedText = props.userInfo?.joinedSince ? dayjs(props.userInfo?.joinedSince).fromNow() : '';

function generateCardMedia() {
if (background) {
Expand Down Expand Up @@ -95,6 +96,7 @@ function UserInfo(props: UserInfoProps) {
</Box>
{showStatusBox("Muted", mutedText)}
{showStatusBox("Deafened", deafenedText)}
{showStatusBox("Joined", joinedText)}
</Box>
<Divider sx={{ margin: '10px 0' }} />
<Box className="user-info-text">
Expand Down
5 changes: 5 additions & 0 deletions src/helper/BrowserUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { invoke } from "@tauri-apps/api";

export function openInBrowser(url: string) {
invoke('open_browser', { url: url });
}
8 changes: 4 additions & 4 deletions src/helper/MessageParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class DOMMessageParser {
private replacementUrl: LinkReplacement[] = [
{ regex: /https:\/\/store\.steampowered\.com\/app\/([0-9]+)\/?.*/, replacement: 'steam://advertise/$1', inline: false },
{ regex: /https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/, replacement: 'https://www.youtube.com/embed/$1', inline: true },
{ regex: /https:\/\/www\.twitch\.tv\/videos\/([0-9]+)/, replacement: 'https://player.twitch.tv/?video=$1&parent='+(window.location.hostname), inline: true },
{ regex: /https:\/\/clips\.twitch\.tv\/([a-zA-Z0-9_-]+)/, replacement: 'https://clips.twitch.tv/embed?clip=$1&parent='+(window.location.hostname), inline: true },
{ regex: /https:\/\/www\.twitch\.tv\/([a-zA-Z0-9_-]+)/, replacement: 'https://player.twitch.tv/?channel=$1&parent='+(window.location.hostname), inline: true },
{ regex: /https:\/\/www\.twitch\.tv\/videos\/([0-9]+)/, replacement: 'https://player.twitch.tv/?video=$1&parent=' + (window.location.hostname), inline: true },
{ regex: /https:\/\/clips\.twitch\.tv\/([a-zA-Z0-9_-]+)/, replacement: 'https://clips.twitch.tv/embed?clip=$1&parent=' + (window.location.hostname), inline: true },
{ regex: /https:\/\/www\.twitch\.tv\/([a-zA-Z0-9_-]+)/, replacement: 'https://player.twitch.tv/?channel=$1&parent=' + (window.location.hostname), inline: true },
{ regex: /https:\/\/twitter\.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/, replacement: 'https://twitframe.com/show?url=https://twitter.com/$1/status/$2', inline: true },
{ regex: /https:\/\/twitter\.com\/([a-zA-Z0-9_]+)/, replacement: 'https://twitframe.com/show?url=https://twitter.com/$1', inline: true },
{ regex: /https:\/\/giphy.com\/gifs\/.*-([a-zA-Z0-9_-]+)/, replacement: 'https://giphy.com/embed/$1', inline: true },
Expand Down Expand Up @@ -58,7 +58,7 @@ class DOMMessageParser {
}

build() {
return this.document.documentElement.innerHTML;
return this.document.body.innerHTML;
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/helper/MessageUIHelper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Container } from '@mui/material';
import parse, { DOMNode, HTMLReactParserOptions, domToReact } from 'html-react-parser';
import LightBoxImage from '../components/LightBoxImage';

export default class MessageUIHelper {
private input: string;

constructor(input: string) {
this.input = input;
}

build() {
const options: HTMLReactParserOptions = {
replace: ({ name, attribs, children }: any) => {
switch (name) {
case 'img':
return (<Container >
<LightBoxImage src={attribs.src} />
</Container>);
}
},
};

return parse(this.input, options);
}
}
8 changes: 6 additions & 2 deletions src/store/features/users/userSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface UsersState {
talking: boolean,
mutedSince: number | undefined,
deafenedSince: number | undefined,
joinedSince: number | undefined,
commentData: UserCommentData
}

Expand Down Expand Up @@ -81,6 +82,7 @@ export function defaultInitializeUser(): UsersState {
talking: false,
mutedSince: undefined,
deafenedSince: undefined,
joinedSince: undefined,
commentData: {
comment: "",
background_picture: "",
Expand Down Expand Up @@ -190,21 +192,23 @@ export const userSlice = createSlice({
let deafened_since = action.payload.self_deaf && state.users[userIndex].self_deaf !== action.payload.self_deaf ? Date.now() : undefined;
let profilePicture = state.users[userIndex].profile_picture;
let comment = state.users[userIndex].comment;
let parsedComment = parseUserCommentForData(action.payload.comment);
let joined = state.users[userIndex].joinedSince;

state.users[userIndex] = action.payload;
state.users[userIndex].comment = comment;
state.users[userIndex].profile_picture = profilePicture;
state.users[userIndex].mutedSince = muted_since;
state.users[userIndex].deafenedSince = deafened_since;
state.users[userIndex].joinedSince = joined;
} else {
// new user
action.payload.talking = false;
action.payload.joinedSince = Date.now();
state.users.push(action.payload);
}

if (state.currentUser?.id === userId) {
state.currentUser = action.payload;
state.currentUser = state.users[userIndex];
}
});
}
Expand Down
Loading

0 comments on commit 7d27961

Please sign in to comment.