feat: message reaction
This commit is contained in:
@@ -36,7 +36,7 @@ const baseQueryWithReauth = async (args, api, extraOptions) => {
|
||||
case 401:
|
||||
{
|
||||
if (api.endpoint === "renew") {
|
||||
toast.error("token expired, please login again");
|
||||
// toast.error("token expired, please login again");
|
||||
api.dispatch(clearAuthData());
|
||||
location.href = "/#/login";
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
// import BASE_URL from "../config";
|
||||
|
||||
import baseQuery from "./base.query";
|
||||
|
||||
export const messageApi = createApi({
|
||||
reducerPath: "message",
|
||||
baseQuery,
|
||||
endpoints: (builder) => ({
|
||||
editMessage: builder.mutation({
|
||||
query: ({ mid, data }) => ({
|
||||
url: `/message/${mid}/edit`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
likeMessage: builder.mutation({
|
||||
query: ({ mid, action }) => ({
|
||||
url: `/message/${mid}/like`,
|
||||
method: "PUT",
|
||||
body: { action },
|
||||
}),
|
||||
}),
|
||||
deleteMessage: builder.query({
|
||||
query: (mid) => ({
|
||||
url: `/message/${mid}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
}),
|
||||
replyMessage: builder.mutation({
|
||||
query: (mid, data) => ({
|
||||
url: `/message/${mid}/reply`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useEditMessageMutation,
|
||||
useLikeMessageMutation,
|
||||
useReplyMessageMutation,
|
||||
useLazyDeleteMessageQuery,
|
||||
} = messageApi;
|
||||
@@ -39,6 +39,23 @@ const channelMsgSlice = createSlice({
|
||||
state[id] = { [mid]: newMsg };
|
||||
}
|
||||
},
|
||||
deleteChannelMsg(state, action) {
|
||||
const { id, mid } = action.payload;
|
||||
console.log("delete channel message", id, mid, state[id][mid]);
|
||||
if (state[id][mid]) {
|
||||
// 添加removed标识
|
||||
// state[id][mid]["removed"] = true;
|
||||
delete state[id][mid];
|
||||
}
|
||||
},
|
||||
likeChannelMsg(state, action) {
|
||||
const { id, mid, action: emoji } = action.payload;
|
||||
console.log("add user likes message", id, mid, state[id][mid]);
|
||||
if (state[id][mid]) {
|
||||
const currLikes = state[id][mid].likes;
|
||||
state[id][mid].likes = currLikes ? [...currLikes, emoji] : [emoji];
|
||||
}
|
||||
},
|
||||
setChannelMsgRead(state, action) {
|
||||
const { id, mid } = action.payload;
|
||||
console.log("set unread", id, mid);
|
||||
@@ -54,6 +71,8 @@ const channelMsgSlice = createSlice({
|
||||
},
|
||||
});
|
||||
export const {
|
||||
deleteChannelMsg,
|
||||
likeChannelMsg,
|
||||
clearChannelMsg,
|
||||
initChannelMsg,
|
||||
clearChannelMsgUnread,
|
||||
|
||||
@@ -38,6 +38,23 @@ const userMsgSlice = createSlice({
|
||||
state[id] = { [mid]: newMsg };
|
||||
}
|
||||
},
|
||||
likeUserMsg(state, action) {
|
||||
const { id, mid, action: emoji } = action.payload;
|
||||
console.log("add user likes", id, mid, state[id][mid]);
|
||||
if (state[id][mid]) {
|
||||
const currLikes = state[id][mid].likes;
|
||||
state[id][mid].likes = currLikes ? [...currLikes, emoji] : [emoji];
|
||||
}
|
||||
},
|
||||
deleteUserMsg(state, action) {
|
||||
const { id, mid } = action.payload;
|
||||
console.log("delete user message", id, mid);
|
||||
if (state[id][mid]) {
|
||||
// 添加removed标识
|
||||
// state[id][mid].removed = true;
|
||||
delete state[id][mid];
|
||||
}
|
||||
},
|
||||
setUserMsgRead(state, action) {
|
||||
const { id, mid } = action.payload;
|
||||
console.log("set unread", id, mid);
|
||||
@@ -51,6 +68,8 @@ const userMsgSlice = createSlice({
|
||||
},
|
||||
});
|
||||
export const {
|
||||
likeUserMsg,
|
||||
deleteUserMsg,
|
||||
clearUserMsg,
|
||||
initUserMsg,
|
||||
addUserMsg,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
ready: false,
|
||||
menuExpand: true,
|
||||
setting: false,
|
||||
channelSetting: null,
|
||||
@@ -9,6 +10,9 @@ const uiSlice = createSlice({
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setReady(state) {
|
||||
state.ready = true;
|
||||
},
|
||||
toggleMenuExpand(state) {
|
||||
state.menuExpand = !state.menuExpand;
|
||||
},
|
||||
@@ -23,6 +27,7 @@ const uiSlice = createSlice({
|
||||
},
|
||||
});
|
||||
export const {
|
||||
setReady,
|
||||
toggleSetting,
|
||||
toggleMenuExpand,
|
||||
toggleChannelSetting,
|
||||
|
||||
+4
-1
@@ -15,6 +15,7 @@ import userMsgReducer from "./slices/message.user";
|
||||
import { authApi } from "./services/auth";
|
||||
import { contactApi } from "./services/contact";
|
||||
import { channelApi } from "./services/channel";
|
||||
import { messageApi } from "./services/message";
|
||||
import { serverApi } from "./services/server";
|
||||
const getCacheKey = (action = "") => {
|
||||
const [type] = action.split("/");
|
||||
@@ -39,6 +40,7 @@ const reducer = combineReducers({
|
||||
channelMessage: channelMsgReducer,
|
||||
authData: authDataReducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
[messageApi.reducerPath]: messageApi.reducer,
|
||||
[contactApi.reducerPath]: contactApi.reducer,
|
||||
[channelApi.reducerPath]: channelApi.reducer,
|
||||
[serverApi.reducerPath]: serverApi.reducer,
|
||||
@@ -73,7 +75,8 @@ const store = configureStore({
|
||||
authApi.middleware,
|
||||
contactApi.middleware,
|
||||
channelApi.middleware,
|
||||
serverApi.middleware
|
||||
serverApi.middleware,
|
||||
messageApi.middleware
|
||||
)
|
||||
.prepend(listenerMiddleware.middleware),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate, useMatch } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
@@ -9,33 +8,8 @@ import { deleteChannel } from "../../../app/slices/channels";
|
||||
import Modal from "../Modal";
|
||||
// import BASE_URL from "../../app/config";
|
||||
import { useLazyRemoveChannelQuery } from "../../../app/services/channel";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
const StyledConfirm = styled.div`
|
||||
padding: 32px;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.desc {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function DeleteConfirmModal({ id, closeModal }) {
|
||||
const navigateTo = useNavigate();
|
||||
@@ -58,16 +32,20 @@ export default function DeleteConfirmModal({ id, closeModal }) {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledConfirm className="animate__animated animate__fadeInDown animate__faster">
|
||||
<h3 className="title">Delete Channel</h3>
|
||||
<p className="desc">Are you sure want to delete this channel?</p>
|
||||
<div className="btns">
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleDelete} className="danger">
|
||||
{isLoading ? "Deleting" : `Delete`}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledConfirm>
|
||||
<StyledModal
|
||||
title="Delete Channel"
|
||||
description="Are you sure want to delete this channel?"
|
||||
buttons={
|
||||
<>
|
||||
{" "}
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleDelete} className="danger">
|
||||
{isLoading ? "Deleting" : `Delete`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
className="animate__animated animate__fadeInDown animate__faster"
|
||||
></StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import { useEffect } from "react";
|
||||
import { getMessaging, getToken } from "firebase/messaging";
|
||||
|
||||
// // Import the functions you need from the SDKs you need
|
||||
// import { initializeApp } from "firebase/app";
|
||||
// import { getAnalytics } from "firebase/analytics";
|
||||
// // TODO: Add SDKs for Firebase products that you want to use
|
||||
// // https://firebase.google.com/docs/web/setup#available-libraries
|
||||
|
||||
// // Your web app's Firebase configuration
|
||||
// // For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||
// const firebaseConfig = {
|
||||
// apiKey: "AIzaSyDyJ6B1Ouenoha_gdGkBwIkBNStlwhlbO0",
|
||||
// authDomain: "rustchat-develop.firebaseapp.com",
|
||||
// projectId: "rustchat-develop",
|
||||
// storageBucket: "rustchat-develop.appspot.com",
|
||||
// messagingSenderId: "418687074928",
|
||||
// appId: "1:418687074928:web:7ad57a83f756285cf9eab5",
|
||||
// measurementId: "G-61T6TLQ27T"
|
||||
// };
|
||||
|
||||
// // Initialize Firebase
|
||||
// const app = initializeApp(firebaseConfig);
|
||||
// const analytics = getAnalytics(app);
|
||||
|
||||
export default function Notification() {
|
||||
useEffect(() => {
|
||||
// Get registration token. Initially this makes a network call, once retrieved
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// import React from 'react'
|
||||
import { useState, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { useOutsideClick } from "rooks";
|
||||
import StyledMenu from "../StyledMenu";
|
||||
import DeleteMessageConfirm from "./DeleteMessageConfirm";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
const StyledCmds = styled.ul`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
@@ -13,41 +17,100 @@ const StyledCmds = styled.ul`
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
visibility: hidden;
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
.cmd {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 36px;
|
||||
}
|
||||
`;
|
||||
// https://static.nicegoodthings.com/project/rustchat/icon.forward.svg
|
||||
// https://static.nicegoodthings.com/project/rustchat/icon.edit.svg
|
||||
export default function Commands() {
|
||||
export default function Commands({
|
||||
message = null,
|
||||
mid = 0,
|
||||
uid = 0,
|
||||
menuVisible,
|
||||
toggleMenu,
|
||||
emojiPopVisible,
|
||||
toggleEmojiPopover,
|
||||
}) {
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
|
||||
const currUid = useSelector((store) => store.authData.user.uid);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
const handleClick = () => {
|
||||
toast.success("cooming soon");
|
||||
};
|
||||
|
||||
useOutsideClick(menuRef, toggleMenu);
|
||||
const toggleDeleteModal = () => {
|
||||
setDeleteModalVisible((prev) => !prev);
|
||||
};
|
||||
const alwaysVisible = menuVisible || emojiPopVisible;
|
||||
return (
|
||||
<StyledCmds className="cmds">
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
<StyledCmds className={`cmds ${alwaysVisible ? "visible" : ""}`}>
|
||||
<li className="cmd" onClick={toggleEmojiPopover}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.reply.svg"
|
||||
alt="icon emoji"
|
||||
/>
|
||||
</li>
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.edit.svg"
|
||||
alt="icon emoji"
|
||||
/>
|
||||
</li>
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
{emojiPopVisible && (
|
||||
<EmojiPicker mid={mid} hidePicker={toggleEmojiPopover} />
|
||||
)}
|
||||
{currUid == uid ? (
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.edit.svg"
|
||||
alt="icon edit"
|
||||
/>
|
||||
</li>
|
||||
) : (
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.forward.svg"
|
||||
alt="icon reply"
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
<li className="cmd" onClick={toggleMenu}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.dots.svg"
|
||||
alt="icon emoji"
|
||||
/>
|
||||
</li>
|
||||
{menuVisible && (
|
||||
<StyledMenu className="menu" ref={menuRef}>
|
||||
<li className="item">Edit Message</li>
|
||||
<li className="item underline">Pin Message</li>
|
||||
<li className="item">Reply</li>
|
||||
{currUid == uid && (
|
||||
<li className="item danger" onClick={toggleDeleteModal}>
|
||||
Delete Message
|
||||
</li>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)}
|
||||
{deleteModalVisible && (
|
||||
<DeleteMessageConfirm
|
||||
closeModal={toggleDeleteModal}
|
||||
message={{ mid, ...message }}
|
||||
/>
|
||||
)}
|
||||
</StyledCmds>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
// import { useDispatch } from "react-redux";
|
||||
// import BASE_URL from "../../app/config";
|
||||
import { useLazyDeleteMessageQuery } from "../../../app/services/message";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
import Modal from "../Modal";
|
||||
import PreviewMessage from "./PreviewMessage";
|
||||
export default function LogoutConfirmModal({ closeModal, message = null }) {
|
||||
// const dispatch = useDispatch();
|
||||
const [deleteMessage, { isLoading, isSuccess }] = useLazyDeleteMessageQuery();
|
||||
const handleDelete = (evt) => {
|
||||
const { mid } = evt.target.dataset;
|
||||
if (!mid) return;
|
||||
deleteMessage(mid);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
closeModal();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
if (!message) return;
|
||||
const { mid, ...previewContent } = message;
|
||||
return (
|
||||
<Modal>
|
||||
<StyledModal
|
||||
className="animate__animated animate__fadeInDown animate__faster"
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button data-mid={mid} onClick={handleDelete} className="danger">
|
||||
{isLoading ? "Deleting" : `Delete`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
title="Delete Message"
|
||||
description="Are you sure want to delete this message?"
|
||||
>
|
||||
<PreviewMessage data={previewContent} />
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// import { Picker } from "emoji-mart";
|
||||
// import "emoji-mart/css/emoji-mart.css";
|
||||
import { useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useOutsideClick } from "rooks";
|
||||
import { useLikeMessageMutation } from "../../../app/services/message";
|
||||
const StyledPicker = styled.div`
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 0;
|
||||
transform: translateX(-100%);
|
||||
background-color: #fff;
|
||||
padding: 5px;
|
||||
.emojis {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
.emoji {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
font-size: 30px;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const emojis = {
|
||||
thumb_up: "👍",
|
||||
ok: "👌",
|
||||
like: "❤️",
|
||||
};
|
||||
export default function EmojiPicker({ mid, hidePicker }) {
|
||||
const wrapperRef = useRef(null);
|
||||
const [reactMessage, { isLoading }] = useLikeMessageMutation();
|
||||
useOutsideClick(wrapperRef, hidePicker);
|
||||
const handleReact = (action) => {
|
||||
console.log("react", action);
|
||||
reactMessage({ mid, action });
|
||||
};
|
||||
return (
|
||||
<StyledPicker ref={wrapperRef}>
|
||||
<ul className="emojis">
|
||||
{Object.entries(emojis).map(([key, emoji]) => {
|
||||
return (
|
||||
<li
|
||||
className="emoji"
|
||||
key={key}
|
||||
onClick={handleReact.bind(null, key)}
|
||||
>
|
||||
{emoji}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</StyledPicker>
|
||||
);
|
||||
}
|
||||
export { emojis };
|
||||
@@ -0,0 +1,23 @@
|
||||
// import { useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
// import { useSelector } from "react-redux";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledWrapper from "./styled";
|
||||
export default function PreviewMessage({ data = null }) {
|
||||
if (!data) return null;
|
||||
const { avatar, name, time, content } = data;
|
||||
return (
|
||||
<StyledWrapper className={`preview`}>
|
||||
<div className="avatar">
|
||||
<Avatar url={avatar} name={name} />
|
||||
</div>
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{name}</span>
|
||||
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
</div>
|
||||
<div className={`down`}>{content}</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
export default function Removed() {
|
||||
return <Styled>Removed</Styled>;
|
||||
}
|
||||
@@ -1,46 +1,16 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useInViewRef } from "rooks";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import Linkify from "react-linkify";
|
||||
import Profile from "../Profile";
|
||||
import Avatar from "../Avatar";
|
||||
import BASE_URL from "../../../app/config";
|
||||
import { setChannelMsgRead } from "../../../app/slices/message.channel";
|
||||
import { setUserMsgRead } from "../../../app/slices/message.user";
|
||||
import StyledWrapper from "./styled";
|
||||
import Commands from "./Commands";
|
||||
const renderContent = (type, content) => {
|
||||
let ctn = null;
|
||||
switch (type) {
|
||||
case "text/plain":
|
||||
ctn = (
|
||||
<Linkify
|
||||
componentDecorator={(decoratedHref, decoratedText, key) => (
|
||||
<a target="blank" href={decoratedHref} key={key}>
|
||||
{decoratedText}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Linkify>
|
||||
);
|
||||
break;
|
||||
case "image/jpeg":
|
||||
ctn = (
|
||||
<img
|
||||
className="img"
|
||||
src={`${BASE_URL}/resource/image?id=${encodeURIComponent(content)}`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ctn;
|
||||
};
|
||||
import { emojis } from "./EmojiPicker";
|
||||
import renderContent from "./renderContent";
|
||||
export default function Message({
|
||||
gid = "",
|
||||
mid = "",
|
||||
@@ -51,11 +21,21 @@ export default function Message({
|
||||
content_type = "text/plain",
|
||||
unread = false,
|
||||
pending,
|
||||
removed = false,
|
||||
likes,
|
||||
}) {
|
||||
const [myRef, inView] = useInViewRef();
|
||||
const [emojiPopVisible, setEmojiPopVisible] = useState(false);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const disptach = useDispatch();
|
||||
const avatarRef = useRef(null);
|
||||
const contacts = useSelector((store) => store.contacts);
|
||||
const toggleMenu = () => {
|
||||
setMenuVisible((prev) => !prev);
|
||||
};
|
||||
const toggleEmojiPopover = () => {
|
||||
setEmojiPopVisible((prev) => !prev);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!unread) {
|
||||
avatarRef.current?.scrollIntoView();
|
||||
@@ -73,8 +53,10 @@ export default function Message({
|
||||
|
||||
if (!contacts) return null;
|
||||
const currUser = contacts.find((c) => c.uid == fromUid) || {};
|
||||
return (
|
||||
<StyledWrapper ref={myRef}>
|
||||
return removed ? (
|
||||
"removed"
|
||||
) : (
|
||||
<StyledWrapper ref={myRef} className={`${menuVisible ? "menu" : ""}`}>
|
||||
<Tippy
|
||||
interactive
|
||||
placement="left"
|
||||
@@ -89,12 +71,46 @@ export default function Message({
|
||||
<div className="up">
|
||||
<span className="name">{currUser.name}</span>
|
||||
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
{likes && (
|
||||
<span className="likes">
|
||||
{Object.entries(
|
||||
likes.reduce((acc, val) => {
|
||||
return { ...acc, [val]: (acc[val] || 0) + 1 };
|
||||
}, {})
|
||||
).map(([l, count]) => {
|
||||
return (
|
||||
<i
|
||||
className="like"
|
||||
// data-count={count > 1 ? count : ""}
|
||||
key={l}
|
||||
>
|
||||
{emojis[l]}
|
||||
|
||||
{count > 1 ? <em>{`+${count}`} </em> : null}
|
||||
</i>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`down ${pending ? "pending" : ""}`}>
|
||||
{renderContent(content_type, content)}
|
||||
</div>
|
||||
</div>
|
||||
<Commands />
|
||||
<Commands
|
||||
message={{
|
||||
name: currUser.name,
|
||||
avatar: currUser.avatar,
|
||||
time,
|
||||
content: renderContent(content_type, content),
|
||||
}}
|
||||
mid={mid}
|
||||
uid={fromUid}
|
||||
toggleMenu={toggleMenu}
|
||||
menuVisible={menuVisible}
|
||||
emojiPopVisible={emojiPopVisible}
|
||||
toggleEmojiPopover={toggleEmojiPopover}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import Linkify from "react-linkify";
|
||||
import BASE_URL from "../../../app/config";
|
||||
const renderContent = (type, content) => {
|
||||
let ctn = null;
|
||||
switch (type) {
|
||||
case "text/plain":
|
||||
ctn = (
|
||||
<Linkify
|
||||
componentDecorator={(decoratedHref, decoratedText, key) => (
|
||||
<a target="blank" href={decoratedHref} key={key}>
|
||||
{decoratedText}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Linkify>
|
||||
);
|
||||
break;
|
||||
case "image/jpeg":
|
||||
ctn = (
|
||||
<img
|
||||
className="img"
|
||||
src={`${BASE_URL}/resource/image?id=${encodeURIComponent(content)}`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ctn;
|
||||
};
|
||||
|
||||
export default renderContent;
|
||||
@@ -7,12 +7,16 @@ const StyledMsg = styled.div`
|
||||
padding: 4px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.preview {
|
||||
background: #f5f6f7;
|
||||
.cmds {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.menu {
|
||||
z-index: 9;
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
img {
|
||||
@@ -41,6 +45,30 @@ const StyledMsg = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.likes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
/* align-items: center; */
|
||||
.like {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
em {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
/* &:after {
|
||||
content: attr(data-count);
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
.down {
|
||||
user-select: text;
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
} from "../../../app/slices/auth.data";
|
||||
import { setUsersVersion, setAfterMid } from "../../../app/slices/visit.mark";
|
||||
|
||||
import { addChannelMsg } from "../../../app/slices/message.channel";
|
||||
import { addUserMsg } from "../../../app/slices/message.user";
|
||||
// import { addChannelMsg } from "../../../app/slices/message.channel";
|
||||
// import { addUserMsg } from "../../../app/slices/message.user";
|
||||
import { setReady } from "../../../app/slices/ui";
|
||||
import useMessageHandler from "./useMessageHandler";
|
||||
const getQueryString = (params = {}) => {
|
||||
const sp = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, val]) => {
|
||||
@@ -33,12 +35,13 @@ const getQueryString = (params = {}) => {
|
||||
return sp.toString();
|
||||
};
|
||||
const NotificationHub = ({ usersVersion = 0, afterMid = 0 }) => {
|
||||
const { enableNotification, showNotification } = useNotification();
|
||||
const { enableNotification } = useNotification();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { token, refreshToken, user: currUser } = useSelector(
|
||||
(store) => store.authData
|
||||
);
|
||||
const handleMessage = useMessageHandler(currUser);
|
||||
const [
|
||||
renewToken,
|
||||
{ data, isSuccess: refreshTokenSuccess },
|
||||
@@ -99,6 +102,10 @@ const NotificationHub = ({ usersVersion = 0, afterMid = 0 }) => {
|
||||
case "heartbeat":
|
||||
console.log("heartbeat");
|
||||
break;
|
||||
case "ready":
|
||||
console.log("sse ready");
|
||||
dispatch(setReady());
|
||||
break;
|
||||
case "users_snapshot":
|
||||
{
|
||||
console.log("users snapshot");
|
||||
@@ -161,72 +168,12 @@ const NotificationHub = ({ usersVersion = 0, afterMid = 0 }) => {
|
||||
break;
|
||||
case "chat":
|
||||
{
|
||||
// console.log("chat data", data);
|
||||
const {
|
||||
detail: { target },
|
||||
} = data;
|
||||
const {
|
||||
created_at,
|
||||
mid,
|
||||
from_uid,
|
||||
detail: { content, content_type, expires_in, type },
|
||||
} = data;
|
||||
if (typeof target.gid !== "undefined") {
|
||||
// channel msg
|
||||
const gid = target.gid;
|
||||
const isSelf = from_uid == currUser.uid;
|
||||
dispatch(
|
||||
addChannelMsg({
|
||||
id: gid,
|
||||
from_uid,
|
||||
// 自己发的 就不用标记未读
|
||||
unread: !isSelf,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
// group message notification
|
||||
if (!isSelf) {
|
||||
showNotification({
|
||||
body: content,
|
||||
data: {
|
||||
path: `/chat/channel/${gid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// user msg
|
||||
const isSelf = data.from_uid == currUser.uid;
|
||||
handleMessage(data);
|
||||
|
||||
dispatch(
|
||||
addUserMsg({
|
||||
// 此处需要特别注意
|
||||
id: isSelf ? target.uid : from_uid,
|
||||
from_uid: from_uid,
|
||||
unread: !isSelf,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
if (!isSelf) {
|
||||
showNotification({
|
||||
body: data.content,
|
||||
data: {
|
||||
path: `/chat/dm/${data.from_uid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// 更新after_mid
|
||||
dispatch(setAfterMid({ mid: data.mid }));
|
||||
if (data.detail.type == "normal") {
|
||||
dispatch(setAfterMid({ mid: data.mid }));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// import React from "react";
|
||||
import {
|
||||
addChannelMsg,
|
||||
deleteChannelMsg,
|
||||
likeChannelMsg,
|
||||
} from "../../../app/slices/message.channel";
|
||||
import {
|
||||
addUserMsg,
|
||||
deleteUserMsg,
|
||||
likeUserMsg,
|
||||
} from "../../../app/slices/message.user";
|
||||
import useNotification from "./useNotification";
|
||||
import { useDispatch } from "react-redux";
|
||||
export default function useMessageHandler(currUser) {
|
||||
const { showNotification } = useNotification();
|
||||
const dispatch = useDispatch();
|
||||
const handleReaction = ({ from = "user", id, mid, detail = {} }) => {
|
||||
const { type = "" } = detail;
|
||||
const deleteMsg = from == "user" ? deleteUserMsg : deleteChannelMsg;
|
||||
const likeMsg = from == "user" ? likeUserMsg : likeChannelMsg;
|
||||
switch (type) {
|
||||
case "edit":
|
||||
break;
|
||||
case "like":
|
||||
dispatch(likeMsg({ id, mid, action: detail.action }));
|
||||
break;
|
||||
case "delete":
|
||||
dispatch(deleteMsg({ id, mid }));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const handleUserMessage = ({
|
||||
self = false,
|
||||
uid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
}) => {
|
||||
switch (type) {
|
||||
case "normal":
|
||||
dispatch(
|
||||
addUserMsg({
|
||||
// 此处需要特别注意
|
||||
id: self ? uid : from_uid,
|
||||
from_uid: from_uid,
|
||||
unread: !self,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "reaction":
|
||||
handleReaction({
|
||||
from: "user",
|
||||
id: self ? uid : from_uid,
|
||||
mid: detailMid,
|
||||
detail,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!self && type == "normal") {
|
||||
showNotification({
|
||||
body: content,
|
||||
data: {
|
||||
path: `/chat/dm/${from_uid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleChannelMessage = ({
|
||||
gid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
}) => {
|
||||
const isSelf = from_uid == currUser.uid;
|
||||
switch (type) {
|
||||
case "normal":
|
||||
dispatch(
|
||||
addChannelMsg({
|
||||
id: gid,
|
||||
from_uid,
|
||||
// 自己发的 就不用标记未读
|
||||
unread: !isSelf,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "reaction":
|
||||
handleReaction({ from: "channel", id: gid, mid: detailMid, detail });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// group message notification
|
||||
if (!isSelf && type == "normal") {
|
||||
showNotification({
|
||||
body: content,
|
||||
data: {
|
||||
path: `/chat/channel/${gid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleMessage = (data) => {
|
||||
const { target } = data;
|
||||
const {
|
||||
created_at,
|
||||
mid,
|
||||
from_uid,
|
||||
detail: {
|
||||
mid: detailMid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail = {},
|
||||
},
|
||||
} = data;
|
||||
if (typeof target.gid !== "undefined") {
|
||||
// channel message
|
||||
handleChannelMessage({
|
||||
gid: target.gid,
|
||||
created_at,
|
||||
mid,
|
||||
detailMid,
|
||||
from_uid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
});
|
||||
} else {
|
||||
const isSelf = data.from_uid == currUser.uid;
|
||||
handleUserMessage({
|
||||
self: isSelf,
|
||||
uid: target.uid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
};
|
||||
return handleMessage;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { addChannelMsg } from "../../../../app/slices/message.channel";
|
||||
import { addUserMsg } from "../../../../app/slices/message.user";
|
||||
import Modal from "../../Modal";
|
||||
import Button from "../../styled/Button";
|
||||
|
||||
import StyledWrapper from "./styled";
|
||||
|
||||
export default function UploadModal({
|
||||
@@ -75,9 +76,25 @@ export default function UploadModal({
|
||||
if (!sendTo) return null;
|
||||
return (
|
||||
<Modal>
|
||||
<StyledWrapper className="animate__animated animate__fadeInDown animate__faster">
|
||||
<h3 className="head">Upload a file</h3>
|
||||
<p className="intro">Photos accept jpg, png, max size limit to 10M.</p>
|
||||
<StyledWrapper
|
||||
title={"Upload a file"}
|
||||
description="Photos accept jpg, png, max size limit to 10M."
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="upload main"
|
||||
disabled={channelSending || userSending}
|
||||
onClick={handleUpload}
|
||||
>
|
||||
{channelSending || userSending ? "Uploading" : `Upload`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
className="animate__animated animate__fadeInDown animate__faster"
|
||||
>
|
||||
<ul className="list">
|
||||
{blobs.map((b, idx) => {
|
||||
console.log({ b });
|
||||
@@ -102,18 +119,6 @@ export default function UploadModal({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="btns">
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="upload main"
|
||||
disabled={channelSending || userSending}
|
||||
onClick={handleUpload}
|
||||
>
|
||||
{channelSending || userSending ? "Uploading" : `Upload`}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 8px;
|
||||
.head {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.intro {
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
import StyledModal from "../../styled/Modal";
|
||||
const StyledWrapper = styled(StyledModal)`
|
||||
.list {
|
||||
padding-top: 32px;
|
||||
display: flex;
|
||||
@@ -69,14 +48,5 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
padding-top: 32px;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -66,8 +66,8 @@ const StyledSend = styled.div`
|
||||
}
|
||||
.picker {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
right: 0;
|
||||
top: -20px;
|
||||
right: -15px;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,15 @@ import { clearUserMsg } from "../../../app/slices/message.user";
|
||||
import { clearPendingMsg } from "../../../app/slices/message.pending";
|
||||
// import BASE_URL from "../../app/config";
|
||||
import { useLazyLogoutQuery } from "../../../app/services/auth";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
import Checkbox from "../styled/Checkbox";
|
||||
const StyledConfirm = styled.div`
|
||||
padding: 32px;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.desc,
|
||||
const StyledConfirm = styled(StyledModal)`
|
||||
.clear {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.clear {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
@@ -47,13 +34,6 @@ const StyledConfirm = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
import Modal from "../Modal";
|
||||
export default function LogoutConfirmModal({ closeModal }) {
|
||||
@@ -86,9 +66,20 @@ export default function LogoutConfirmModal({ closeModal }) {
|
||||
}, [isSuccess, clearLocal]);
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledConfirm className="animate__animated animate__fadeInDown animate__faster">
|
||||
<h3 className="title">Log Out</h3>
|
||||
<p className="desc">Are you sure want to log out this account?</p>
|
||||
<StyledConfirm
|
||||
title="Log Out"
|
||||
description="Are you sure want to log out this account?"
|
||||
buttons={
|
||||
<>
|
||||
{" "}
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleLogout} className="danger">
|
||||
{isLoading ? "Logging out" : `Log Out`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
className="animate__animated animate__fadeInDown animate__faster"
|
||||
>
|
||||
<div className="clear">
|
||||
<label htmlFor="clear_cb" className="txt">
|
||||
Clear local data
|
||||
@@ -99,12 +90,6 @@ export default function LogoutConfirmModal({ closeModal }) {
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
</div>
|
||||
<div className="btns">
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleLogout} className="danger">
|
||||
{isLoading ? "Logging out" : `Log Out`}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledConfirm>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -5,28 +5,9 @@ import styled from "styled-components";
|
||||
import Input from "../styled/Input";
|
||||
// import BASE_URL from "../../app/config";
|
||||
import { useUpdateInfoMutation } from "../../../app/services/contact";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
const StyledEdit = styled.div`
|
||||
width: 440px;
|
||||
padding: 32px;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.desc {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
const StyledEdit = styled(StyledModal)`
|
||||
.input {
|
||||
margin: 48px 0;
|
||||
width: 100%;
|
||||
@@ -40,13 +21,6 @@ const StyledEdit = styled.div`
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
import Modal from "../Modal";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -76,19 +50,23 @@ export default function ProfileBasicEditModal({
|
||||
}, [isSuccess]);
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledEdit className="animate__animated animate__fadeInDown animate__faster">
|
||||
<h3 className="title">{title}</h3>
|
||||
<p className="desc">{intro}</p>
|
||||
<StyledEdit
|
||||
title={title}
|
||||
description={intro}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleUpdate} className="main">
|
||||
{isLoading ? "Updating" : `Done`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
className="animate__animated animate__fadeInDown animate__faster"
|
||||
>
|
||||
<div className="input">
|
||||
<label htmlFor={valueKey}>{label}</label>
|
||||
<Input name={valueKey} value={input} onChange={handleChange}></Input>
|
||||
</div>
|
||||
<div className="btns">
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={handleUpdate} className="main">
|
||||
{isLoading ? "Updating" : `Done`}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledEdit>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// import React from 'react'
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
padding: 32px;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
min-width: 440px;
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.desc {
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.btns {
|
||||
padding-top: 32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
export default function StyledModal({
|
||||
title = "",
|
||||
description = "",
|
||||
buttons = null,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Styled {...props}>
|
||||
{title && <h3 className="title">{title}</h3>}
|
||||
{description && <p className="desc">{description}</p>}
|
||||
{children}
|
||||
{buttons && <div className="btns">{buttons}</div>}
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
@@ -104,15 +104,19 @@ export default function ChannelChat({
|
||||
.map(([mid, msg]) => {
|
||||
if (!msg) return null;
|
||||
const {
|
||||
likes = null,
|
||||
pending = false,
|
||||
from_uid,
|
||||
content,
|
||||
content_type,
|
||||
created_at,
|
||||
unread,
|
||||
removed = false,
|
||||
} = msg;
|
||||
return (
|
||||
<Message
|
||||
likes={likes}
|
||||
removed={removed}
|
||||
pending={pending}
|
||||
content_type={content_type}
|
||||
unread={unread}
|
||||
|
||||
@@ -83,9 +83,11 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
|
||||
created_at,
|
||||
unread,
|
||||
pending = false,
|
||||
removed = false,
|
||||
} = msg;
|
||||
return (
|
||||
<Message
|
||||
removed={removed}
|
||||
pending={pending}
|
||||
content_type={content_type}
|
||||
unread={unread}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// import React from 'react'
|
||||
import styled from "styled-components";
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
border: 4px solid #999;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
export default function Loading() {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="loading">Loading...</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
+37
-30
@@ -6,6 +6,7 @@ import { toggleMenuExpand } from "../../app/slices/ui";
|
||||
import StyledWrapper from "./styled";
|
||||
import ServerDropList from "./ServerDropList";
|
||||
import Tools from "./Tools";
|
||||
import Loading from "./Loading";
|
||||
import Menu from "./Menu";
|
||||
import usePreload from "./usePreload";
|
||||
import SettingModal from "../../common/component/Setting";
|
||||
@@ -18,7 +19,7 @@ import NotificationHub from "../../common/component/NotificationHub";
|
||||
export default function HomePage() {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
ui: { menuExpand, setting, channelSetting },
|
||||
ui: { ready, menuExpand, setting, channelSetting },
|
||||
visitMark: { usersVersion, afterMid },
|
||||
} = useSelector((store) => {
|
||||
return {
|
||||
@@ -32,39 +33,45 @@ export default function HomePage() {
|
||||
};
|
||||
console.log({ data, error, success });
|
||||
if (loading) {
|
||||
return "loading";
|
||||
return <Loading />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<NotificationHub usersVersion={usersVersion} afterMid={afterMid} />
|
||||
<StyledWrapper>
|
||||
<div className={`col left ${menuExpand ? "expand" : ""}`}>
|
||||
<ServerDropList data={data?.server} expand={menuExpand} />
|
||||
<nav className="nav">
|
||||
<NavLink className="link" to={"/chat"}>
|
||||
<img src={ChatIcon} alt="chat icon" />{" "}
|
||||
{menuExpand && (
|
||||
<span className="animate__animated animate__fadeIn">Chat</span>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink className="link" to={"/contacts"}>
|
||||
<img src={ContactIcon} alt="contact icon" />{" "}
|
||||
{menuExpand && (
|
||||
<span className="animate__animated animate__fadeIn">
|
||||
Contacts
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div className="divider"></div>
|
||||
<Tools expand={menuExpand} />
|
||||
<Menu toggle={toggleExpand} expand={menuExpand} />
|
||||
{/* <CurrentUser expand={menuExpand} /> */}
|
||||
</div>
|
||||
<div className="col right">
|
||||
<Outlet />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
{ready ? (
|
||||
<StyledWrapper>
|
||||
<div className={`col left ${menuExpand ? "expand" : ""}`}>
|
||||
<ServerDropList data={data?.server} expand={menuExpand} />
|
||||
<nav className="nav">
|
||||
<NavLink className="link" to={"/chat"}>
|
||||
<img src={ChatIcon} alt="chat icon" />{" "}
|
||||
{menuExpand && (
|
||||
<span className="animate__animated animate__fadeIn">
|
||||
Chat
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink className="link" to={"/contacts"}>
|
||||
<img src={ContactIcon} alt="contact icon" />{" "}
|
||||
{menuExpand && (
|
||||
<span className="animate__animated animate__fadeIn">
|
||||
Contacts
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div className="divider"></div>
|
||||
<Tools expand={menuExpand} />
|
||||
<Menu toggle={toggleExpand} expand={menuExpand} />
|
||||
{/* <CurrentUser expand={menuExpand} /> */}
|
||||
</div>
|
||||
<div className="col right">
|
||||
<Outlet />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
{setting && <SettingModal />}
|
||||
{channelSetting && <ChannelSettingModal id={channelSetting} />}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user