Skip to content

Commit

Permalink
GH-73 GH-72: Update chat message log and fix link preview
Browse files Browse the repository at this point in the history
- Refactor code for event log, move out to own component
- Split event log in seperat functions
- Use HTML parser for Link preview
  • Loading branch information
SetZero committed Jul 18, 2023
1 parent ed5813f commit 52cd5e8
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 183 deletions.
28 changes: 13 additions & 15 deletions src-tauri/src/commands/helper.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use reqwest::header::{HeaderMap, COOKIE};
use reqwest::redirect::Policy;
use scraper::{Html, Selector};
use serde::Serialize;
use tauri::regex::Regex;
use tracing_subscriber::prelude::*;

#[derive(Debug, Serialize)]
pub struct OpenGraphMetadata {
Expand All @@ -18,6 +16,8 @@ pub struct OpenGraphCrawler {
impl OpenGraphCrawler {
pub fn new() -> Self {
let client = reqwest::Client::builder()
.redirect(Policy::limited(10))
.user_agent("FancyMumbleClient/0.1.0")
//.cookie_store(true)
.build()
.unwrap();
Expand All @@ -29,10 +29,14 @@ impl OpenGraphCrawler {
let body = response.text().await.ok()?;
let document = Html::parse_document(&body);

let title = self.extract_metadata(&document, "og:title");
let mut title = self.extract_metadata(&document, "og:title");
let description = self.extract_metadata(&document, "og:description");
let image = self.extract_metadata(&document, "og:image");

if title.is_none() {
title = self.extract_property(&document, "title");
}

Some(OpenGraphMetadata {
title,
description,
Expand All @@ -45,16 +49,10 @@ impl OpenGraphCrawler {
let element = document.select(&selector).next()?;
element.value().attr("content").map(String::from)
}
}

pub(crate) fn extract_og_property(body: &str, pattern: &str) -> Result<String, String> {
let re = Regex::new(pattern).map_err(|e| format!("{e:?}"))?;
let property = re
.captures(body)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str())
.map(String::from)
.ok_or("regex not found")?;

Ok(property)
fn extract_property(&self, document: &Html, property: &str) -> Option<String> {
let selector = Selector::parse(property).unwrap();
let element = document.select(&selector).next()?;
Some(element.children().next()?.value().as_text()?.to_string())
}
}
61 changes: 19 additions & 42 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ use crate::{
},
};
use base64::{engine::general_purpose, Engine};
use reqwest::redirect::Policy;
use serde_json::json;
use tauri::State;
use tokio::sync::Mutex;
use tracing::{error, info, trace};
use webbrowser::{Browser, BrowserOptions};

use self::helper::extract_og_property;
use self::helper::OpenGraphCrawler;

pub struct ConnectionState {
pub connection: Mutex<Option<Connection>>,
Expand Down Expand Up @@ -337,50 +336,28 @@ pub fn open_browser(url: &str) -> Result<(), String> {
Ok(())
}

#[tauri::command]
pub async fn get_open_graph_data_from_website(url: &str) -> Result<String, String> {
let client = reqwest::Client::builder()
.redirect(Policy::limited(10))
.user_agent("FancyMumbleClient/0.1.0")
.build()
.map_err(|e| format!("{e:?}"))?;

let request = client
.get(url)
.build()
.map_err(|_| "Failed to build request".to_string())?;
let res = client
.execute(request)
.await
.map_err(|_| "Failed to fetch website".to_string())?;

let body = res
.text()
.await
.map_err(|_| "Failed to read website body".to_string())?;

trace!("Got body: {:?}", body);
pub struct CrawlerState {
pub crawler: Mutex<Option<OpenGraphCrawler>>,
}

let title = extract_og_property(
&body,
r#"<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["']"#,
)?;
#[tauri::command]
pub async fn get_open_graph_data_from_website(
state: State<'_, CrawlerState>,
url: &str,
) -> Result<String, String> {
// setup crawler if not already done
let mut client = state.crawler.lock().await;
if client.is_none() {
*client = Some(OpenGraphCrawler::new());
}

let description = extract_og_property(
&body,
r#"<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:description["']"#,
)?;
let client = client
.as_ref()
.ok_or("Failed to read website body".to_string())?;

let image = extract_og_property(
&body,
r#"<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']"#,
)?;
let result = client.crawl(url).await;

let result = json!({
"title": title,
"description": description,
"image": image,
});
let result = json!(result);

Ok(result.to_string())
}
5 changes: 4 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ mod tests;

use std::collections::HashMap;

use commands::ConnectionState;
use commands::{ConnectionState, CrawlerState};
use tokio::sync::Mutex;

use tauri::Manager;
Expand Down Expand Up @@ -60,6 +60,9 @@ fn main() {
message_handler: Mutex::new(HashMap::new()),
device_manager: Mutex::new(None),
});
app.manage(CrawlerState {
crawler: Mutex::new(None),
});

Ok(())
})
Expand Down
53 changes: 53 additions & 0 deletions src/components/ChatInfoBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Box, IconButton, Paper, Tooltip } from '@mui/material';


import { RootState } from '../store/store';
import { useSelector } from 'react-redux';
import { useEffect, useMemo, useState } from 'react';
import AutoStoriesIcon from '@mui/icons-material/AutoStories';
import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight';
import React from 'react';

interface ChatInfoBarProps {
onShowLog: (showLog: boolean) => void;
}

const ChatInfoBar: React.FC<ChatInfoBarProps> = React.memo(({ onShowLog }) => {
const [showLog, setShowLog] = useState(false);

const currentChannelId = useSelector((state: RootState) => state.reducer.userInfo.currentUser?.channel_id);
const channelInfo = useSelector((state: RootState) => state.reducer.channel.find(e => e.channel_id === currentChannelId));

const eventLogIcon = useMemo(() => {
if (!showLog) return (<AutoStoriesIcon sx={{ fontSize: 20 }} />);
else return (<KeyboardDoubleArrowRightIcon sx={{ fontSize: 20 }} />);
}, [showLog]);

useEffect(() => {
onShowLog(showLog);
}, [showLog, onShowLog]);

return (
<Box sx={{ flexShrink: 1 }}>
<Paper elevation={0} sx={{ backgroundImage: `url(${channelInfo?.channelImage})`, backgroundSize: 'contain' }}>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', backdropFilter: 'blur(20px)', textShadow: '1px 1px #000' }}>
<Box sx={{ flexGrow: 1, paddingLeft: 1 }}>
{channelInfo?.name}
</Box>
<Box sx={{ flexGrow: 0 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Tooltip title={showLog ? 'Hide Log' : 'Show Log'}>
<IconButton size="small" onClick={() => setShowLog(!showLog)}>
{eventLogIcon}
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
</Paper>
</Box>

)
});

export default ChatInfoBar;
20 changes: 8 additions & 12 deletions src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { Avatar, Box, Grid, IconButton, Link, Tooltip, Typography } from "@mui/material"
import { makeStyles } from '@mui/styles';
import { Box, Grid, IconButton, Link, Tooltip, Typography } from "@mui/material"
import dayjs from "dayjs";
import 'dayjs/locale/en';
import 'dayjs/plugin/isToday';
import 'dayjs/plugin/isYesterday';
import { grey } from "@mui/material/colors";
import MessageParser from "../helper/MessageParser";
import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt';
import { invoke } from "@tauri-apps/api";
import { getProfileImage } from "../helper/UserInfoHelper";
import { TextMessage, deleteChatMessage } from "../store/features/users/chatMessageSlice";
import ClearIcon from '@mui/icons-material/Clear';
import { useDispatch } from "react-redux";
import UserInfo from "./UserInfo";
import React, { useEffect } from "react";
import React, { } from "react";
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 {
message: TextMessage,
messageId: number,
onLoaded: () => void,
}

const parseMessage = (message: string | undefined) => {
Expand All @@ -44,9 +39,9 @@ const parseMessage = (message: string | undefined) => {

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

return messageParser.build();
}
Expand All @@ -67,15 +62,16 @@ const generateDate = (timestamp: number) => {
}
}

const ChatMessage: React.FC<ChatMessageProps> = React.memo(({ message, messageId }) => {
const ChatMessage: React.FC<ChatMessageProps> = React.memo(({ message, messageId, onLoaded }) => {
const userList = useSelector((state: RootState) => state.reducer.userInfo);
const dispatch = useDispatch();
const [loaded, setLoaded] = React.useState(false);

const user = React.useMemo(() =>
userList.users.find(e => e.id === message.sender.user_id)
, [userList, message.sender.user_id]);

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

const deleteMessageEvent = React.useCallback(() => {
Expand Down
5 changes: 4 additions & 1 deletion src/components/ChatMessageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ const ChatMessageContainer = (props: ChatMessageContainerProps) => {
const prevPropsRef = useRef(props);

const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
new Promise(r => setTimeout(r, 100)).then(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
});
}

useEffect(() => {
Expand Down Expand Up @@ -101,6 +103,7 @@ const ChatMessageContainer = (props: ChatMessageContainerProps) => {
messageId={el.timestamp}
key={el.timestamp}
message={el}
onLoaded={() => { scrollToBottom(); }}
/>
);

Expand Down
73 changes: 73 additions & 0 deletions src/components/EventLog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Box, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip } from '@mui/material';


import { RootState } from '../store/store';
import { useSelector } from 'react-redux';
import React, { useEffect, useRef } from 'react';
import dayjs from 'dayjs';

interface EventLogProps {
showLog: boolean;
}

const EventLog: React.FC<EventLogProps> = React.memo(({ showLog }) => {
const eventLog = useSelector((state: RootState) => state.eventLog);
const eventLogRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (showLog && eventLogRef.current) {
eventLogRef.current.scrollTo({ top: eventLogRef.current.scrollHeight, behavior: 'smooth' });
}
}, [showLog, eventLog, eventLogRef]);

if (!showLog) {
return null;
}

return (
<Box sx={{
maxWidth: '300px',
display: 'flex',
flexDirection: 'column',
flex: 1,
overflowY: 'auto',
}} >
<Paper
elevation={0}
sx={{ flexGrow: 1, overflowX: 'hidden' }}
ref={eventLogRef}
>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start', height: '100%' }}>
<Box sx={{ flexGrow: 1, paddingLeft: 0 }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 300 }} aria-label="messaeg log" size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Timestamp</TableCell>
<TableCell align="left">Message</TableCell>
</TableRow>
</TableHead>
<TableBody>
{eventLog.map((row, i) => {
const timestamp = dayjs(row.timestamp).format('HH:mm:ss');

return (
<TableRow key={'' + i + row.timestamp} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">
{timestamp}
</TableCell>
<TableCell align="left">{row.logMessage}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</Box>
</Box>
</Paper>
</Box>
)
});

export default EventLog;
Loading

0 comments on commit 52cd5e8

Please sign in to comment.