feat: message reaction

This commit is contained in:
zerosoul
2022-03-05 13:26:06 +08:00
parent acd007fb71
commit 2579761c7a
28 changed files with 826 additions and 304 deletions
+1 -1
View File
@@ -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;
+45
View File
@@ -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;
+19
View File
@@ -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,
+19
View File
@@ -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,
+5
View File
@@ -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
View File
@@ -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
+77 -14
View File
@@ -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>
);
}
+8
View File
@@ -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>;
}
+52 -36
View File
@@ -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;
+29 -1
View File
@@ -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;
+14 -67
View File
@@ -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;
}
+20 -15
View File
@@ -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;
+2 -2
View File
@@ -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>
);
+48
View File
@@ -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>
);
}
+4
View File
@@ -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}
+2
View File
@@ -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}
+29
View File
@@ -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
View File
@@ -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} />}
</>