feat: merge alex version

This commit is contained in:
Tristan Yang
2023-05-15 08:53:12 +08:00
parent 83023423ab
commit 67e3538588
55 changed files with 911 additions and 217 deletions
+51
View File
@@ -0,0 +1,51 @@
import { useTranslation } from 'react-i18next';
// import React from 'react';
import IconAdd from '../../../assets/icons/add.person.svg';
import IconBlock from '../../../assets/icons/block.svg';
import { useAppSelector } from '../../../app/store';
import { useUpdateContactStatusMutation } from '../../../app/services/user';
import { ContactAction } from '../../../types/user';
import ServerVersionChecker from '../../../common/component/ServerVersionChecker';
// import toast from 'react-hot-toast';
type Props = {
uid: number
}
const AddContactTip = (props: Props) => {
const { t } = useTranslation("chat");
const [updateContactStatus] = useUpdateContactStatusMutation();
const targetUser = useAppSelector(store => store.users.byId[props.uid]);
const handleContactStatus = (action: ContactAction) => {
updateContactStatus({ target_uid: props.uid, action });
};
// useEffect(() => {
// if(isSuccess){
// toast.success(t("tip"))
// }
// }, [isSuccess])
const itemClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-50 dark:bg-gray-800 text-sm pt-3.5 pb-3`;
if (!targetUser) return null;
if (targetUser.status == "added") return null;
const blocked = targetUser.status == "blocked";
return (
<ServerVersionChecker version='0.3.7' empty={true}>
<div className="py-4 px-10 flex flex-col items-center gap-3 bg-slate-100 dark:bg-slate-600">
<h3 className='text-gray-700 dark:text-gray-300 text-sm font-semibold'>{blocked ? t("contact_block_tip") : t("contact_tip")}</h3>
<ul className='flex gap-4'>
{!blocked && <li className={itemClass} onClick={handleContactStatus.bind(null, "add")}>
<IconAdd className="fill-primary-400" />
<span>{t("add_contact")}</span>
</li>}
<li className={itemClass} onClick={blocked ? handleContactStatus.bind(null, "unblock") : handleContactStatus.bind(null, "block")}>
<IconBlock className="stroke-primary-400" />
<span>{blocked ? t("unblock") : t("block")}</span>
</li>
</ul>
</div>
</ServerVersionChecker>
);
};
export default AddContactTip;
+2
View File
@@ -18,6 +18,7 @@ import ImagePreview from "../../../common/component/ImagePreview";
import VirtualMessageFeed from "./VirtualMessageFeed";
import DMVoice from "./DMVoicing";
import AddContactTip from "./AddContactTip";
interface Props {
readonly?: boolean;
@@ -100,6 +101,7 @@ const Layout: FC<Props> = ({
<div className="w-full h-full flex items-start justify-between relative" >
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
{context == "user" && <DMVoice uid={to} />}
{context == "user" && <AddContactTip uid={to} />}
{/* 消息流 */}
<VirtualMessageFeed key={`${context}_${to}`} context={context} id={to} />
{/* 发送框 */}
+26 -2
View File
@@ -2,7 +2,7 @@ import { FC, ReactElement } from "react";
import Tippy from "@tippyjs/react";
import { useDispatch } from "react-redux";
import { useNavigate, useLocation, useMatch } from "react-router-dom";
import { useUpdateMuteSettingMutation } from "../../../app/services/user";
import { usePinChatMutation, useUnpinChatMutation, useUpdateMuteSettingMutation } from "../../../app/services/user";
import { useReadMessageMutation } from "../../../app/services/message";
import { removeUserSession } from "../../../app/slices/message.user";
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
@@ -10,8 +10,10 @@ import useUserOperation from "../../../common/hook/useUserOperation";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
import { ChatContext } from "../../../types/common";
import { compareVersion } from "../../../common/utils";
type Props = {
context: ChatContext;
pinned: boolean;
id: number;
visible: boolean;
mid: number;
@@ -21,6 +23,7 @@ type Props = {
children: ReactElement;
};
const SessionContextMenu: FC<Props> = ({
pinned,
context = "dm",
id,
visible,
@@ -36,13 +39,16 @@ const SessionContextMenu: FC<Props> = ({
cid: context == "channel" ? id : undefined
});
const [muteChannel] = useUpdateMuteSettingMutation();
const [pinChat] = usePinChatMutation();
const [unpinChat] = useUnpinChatMutation();
const [updateReadIndex] = useReadMessageMutation();
const pathMatched = useMatch(`/chat/dm/${id}`);
const dispatch = useDispatch();
const navigateTo = useNavigate();
const { pathname } = useLocation();
const { channelMuted } = useAppSelector((store) => {
const { channelMuted, serverVersion } = useAppSelector((store) => {
return {
serverVersion: store.server.version,
channelMuted: context == "channel" ? store.footprint.muteChannels[id] : false
};
});
@@ -70,13 +76,27 @@ const SessionContextMenu: FC<Props> = ({
const data = channelMuted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
muteChannel(data);
};
const handlePinChat = () => {
const data = context == "dm" ? { uid: id } : { gid: id };
if (pinned) {
unpinChat(data);
} else {
pinChat(data);
}
};
const handleDMSetting = () => {
navigateTo(`/setting/dm/${id}/overview?f=${pathname}`);
};
const pinTxt = t(pinned ? "unpin_chat" : "pin_chat", { ns: "chat" });
const pinVisible = compareVersion(serverVersion, "0.3.7") >= 0;
const items =
context == "dm"
? [
pinVisible && {
title: pinTxt,
handler: handlePinChat
},
{
title: t("action.mark_read"),
handler: handleReadAll
@@ -96,6 +116,10 @@ const SessionContextMenu: FC<Props> = ({
}
]
: [
pinVisible && {
title: pinTxt,
handler: handlePinChat
},
{
title: t("setting"),
underline: true,
+5 -2
View File
@@ -21,11 +21,13 @@ interface IProps {
type?: ChatContext;
id: number;
mid: number;
pinned?: boolean;
setDeleteChannelId: (param: number) => void;
setInviteChannelId: (param: number) => void;
}
const Session: FC<IProps> = ({
type = "dm",
pinned = false,
id,
mid,
setDeleteChannelId,
@@ -85,7 +87,7 @@ const Session: FC<IProps> = ({
useEffect(() => {
const tmp = type == "dm" ? userData[id] : channelData[id];
if (!tmp) return;
if ("avatar" in tmp) {
if (type == "dm") {
// user
const { name, avatar } = tmp;
setData({ name, icon: avatar, mid, is_public: true });
@@ -110,13 +112,14 @@ const Session: FC<IProps> = ({
console.log("unreads", unreads, isCurrentPath);
return (
<li className="session">
<li className={clsx("session")}>
<ContextMenu
visible={contextMenuVisible}
hide={hideContextMenu}
context={type}
id={id}
mid={mid}
pinned={pinned}
setInviteChannelId={setInviteChannelId}
deleteChannel={setDeleteChannelId}
>
+62 -15
View File
@@ -6,6 +6,7 @@ import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
import InviteModal from "../../../common/component/InviteModal";
import { useAppSelector } from "../../../app/store";
import { ChatContext } from "../../../types/common";
import { PinChatTargetChannel, PinChatTargetUser } from "../../../types/sse";
export interface ChatSession {
type: ChatContext;
id: number;
@@ -20,9 +21,11 @@ const SessionList: FC<Props> = ({ tempSession }) => {
const [deleteId, setDeleteId] = useState<number>();
const [inviteChannelId, setInviteChannelId] = useState<number>();
const [sessions, setSessions] = useState<ChatSession[]>([]);
const { channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
const [pinSessions, setPinSessions] = useState<ChatSession[]>([]);
const { pins, channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
useAppSelector((store) => {
return {
pins: store.footprint.pinChats,
loginUid: store.authData.user?.uid,
channelIDs: store.channels.ids,
DMs: store.userMessage.ids,
@@ -34,24 +37,49 @@ const SessionList: FC<Props> = ({ tempSession }) => {
});
useEffect(() => {
const cSessions = channelIDs.map((id) => {
const mids = channelMessage[id];
// const pinDMs=
const getSessionObj = (id: number, type: "dm" | "channel") => {
const mids = type == "dm" ? userMessage[id] : channelMessage[id];
if (!mids || mids.length == 0) {
return { unreads: 0, id, type: "channel" };
return { unread: 0, id, type } as ChatSession;
}
// 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop();
return { id, mid, type: "channel" };
return { id, mid, type } as ChatSession;
};
const pinTmps = pins.map((p) => {
const { target } = p;
if ("uid" in target) {
return getSessionObj((target as PinChatTargetUser).uid, "dm");
}
if ("gid" in target) {
return getSessionObj((target as PinChatTargetChannel).gid, "channel");
}
return null;
}).filter((p) => !!p);
const channelPinIds = pins.map(p => {
if (p.target.gid) {
return p.target.gid;
}
return null;
}).filter(id => !!id);
const dmPinIds = pins.map(p => {
if (p.target.uid) {
return p.target.uid;
}
return null;
}).filter(id => !!id);
const cSessions = channelIDs.filter(id => {
return !channelPinIds.includes(id);
}).map((id) => {
return getSessionObj(id, "channel");
});
const uSessions = DMs.map((id) => {
// console.log("adddd", id);
const mids = userMessage[id];
if (!mids || mids.length == 0) {
return { unreads: 0, id, type: "dm" };
}
// 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop();
return { type: "dm", id, mid };
const uSessions = DMs.filter(id => {
return !dmPinIds.includes(id);
}).map((id) => {
return getSessionObj(id, "dm");
});
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
const { mid: aMid = 0 } = a;
@@ -62,6 +90,7 @@ const SessionList: FC<Props> = ({ tempSession }) => {
const newSessions = tempSession ? [tempSession, ...temps] : temps;
// console.log("qqqq", newSessions);
setSessions(newSessions);
setPinSessions(pinTmps);
}, [
channelIDs,
DMs,
@@ -70,11 +99,29 @@ const SessionList: FC<Props> = ({ tempSession }) => {
readUsers,
loginUid,
userMessage,
tempSession
tempSession,
pins
]);
return (
<>
{pinSessions.length ? <ul className="flex flex-col gap-0.5 p-2 bg-primary-500/10">
{pinSessions.map((p) => {
const { type, id, mid = 0 } = p;
const key = `${type}_${id}`;
return (
<Session
key={key}
type={type}
pinned={true}
id={id}
mid={mid}
setInviteChannelId={setInviteChannelId}
setDeleteChannelId={setDeleteId}
/>
);
})}
</ul> : null}
<ul ref={ref} className="flex flex-1 flex-col gap-0.5 p-2 overflow-auto">
<ViewportList
initialPrerender={10}
+1 -4
View File
@@ -72,10 +72,7 @@ function ChatPage() {
)}>
<Server readonly={isGuest} />
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
{isGuest ? <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
</footer> : <RTCWidget id={+contextId} context={context} />}
<RTCWidget id={+contextId} context={context} />
</div>
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
{voiceFullscreenVisible && <VoiceFullscreen id={contextId} context={context} />}
+9 -6
View File
@@ -32,11 +32,10 @@ import LazyIt from './lazy';
import store, { useAppSelector } from "../app/store";
import useDeviceToken from "../common/component/Notification/useDeviceToken";
import { vapidKey } from "../app/config";
import useTabBroadcast from "../common/hook/useTabBroadcast";
import InactiveScreen from "../common/component/InactiveScreen";
import RequireSingleTab from "../common/component/RequireSingleTab";
import InvitePrivate from "./invitePrivate";
let toastId: string;
const PageRoutes = () => {
const {
ui: { online },
fileMessages
@@ -59,6 +58,7 @@ const PageRoutes = () => {
<HashRouter>
<Routes>
<Route path="/guest_login" element={<LazyIt><GuestLogin /></LazyIt>} />
<Route path="/invite_private/:channel_id" element={<LazyIt><RequireAuth><InvitePrivate /></RequireAuth></LazyIt>} />
<Route path="/cb/:type/:payload" element={<LazyIt><CallbackPage /></LazyIt>} />
<Route path="/oauth/:token" element={<LazyIt><OAuthPage /></LazyIt>} />
<Route
@@ -112,7 +112,10 @@ const PageRoutes = () => {
element={
<LazyIt>
<RequireAuth>
<HomePage />
{/* 只允许活跃一个tab标签 */}
<RequireSingleTab>
<HomePage />
</RequireSingleTab>
</RequireAuth>
</LazyIt>
}
@@ -199,11 +202,11 @@ const PageRoutes = () => {
};
export default function ReduxRoutes() {
const { tabActive } = useTabBroadcast();
return (
<Provider store={store}>
<Meta />
{tabActive ? <PageRoutes /> : <InactiveScreen />}
<PageRoutes />
</Provider>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { useEffect } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useJoinPrivateChannelMutation, useLazyGetChannelQuery } from '../../app/services/channel';
import StyledButton from '../../common/component/styled/Button';
import { useCheckMagicTokenValidMutation } from '../../app/services/auth';
import { useAppSelector } from '../../app/store';
const InvitePrivate = () => {
const { channel_id } = useParams();
const server = useAppSelector(store => store.server);
const navigateTo = useNavigate();
const [joinChannel, { isLoading, data, isSuccess }] = useJoinPrivateChannelMutation();
const [fetchChannelInfo, { data: channel, isSuccess: fetchChannelSuccess }] = useLazyGetChannelQuery();
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
useCheckMagicTokenValidMutation();
let [searchParams] = useSearchParams(new URLSearchParams(location.search));
const magic_token = searchParams.get("magic_token") ?? "";
useEffect(() => {
if (channel_id) {
fetchChannelInfo(+channel_id);
}
}, [channel_id]);
useEffect(() => {
if (magic_token) {
checkTokenInvalid(magic_token);
}
}, [magic_token]);
useEffect(() => {
if (data && isSuccess) {
// joinChannel(data)
navigateTo(`/chat/channel/${data.gid}`);
}
}, [isSuccess, data]);
const handleJoin = async () => {
const resp = await joinChannel({ magic_token });
if ("error" in resp) {
if (resp.error.originalStatus === 409) {
// alert("The invite link is invalid or expired");
}
}
};
if (!fetchChannelSuccess) return null;
return (
<div className="flex-center flex-col gap-4 h-screen overflow-x-hidden overflow-y-auto dark:bg-gray-700 dark:text-slate-100">
<div className="flex flex-col gap-4 items-center py-8 px-10 rounded-lg shadow-md bg-slate-100/30 dark:bg-gray-800 text-center">
<div className="flex flex-col items-center gap-4">
<img src={server.logo} className='w-20 h-20' alt="server logo" />
<h2 className="text-2xl font-bold">
{server.name}
</h2>
</div>
<span>
{checkingToken ? "Checking..." : isTokenValid ? <>You are invited to join private channel <strong className='text-primary-400'>#{channel?.name}</strong></> : "The invite link is invalid or expired"}
</span>
<StyledButton disabled={isLoading || checkingToken || !isTokenValid} onClick={handleJoin}>Join</StyledButton>
</div>
</div>
);
};
export default InvitePrivate;
-8
View File
@@ -88,14 +88,6 @@ const navs = [
name: "version",
component: <Version />
},
{
name: "faq",
link: "https://doc.voce.chat/faq",
},
{
name: "feedback",
link: `https://privoce.voce.chat/widget?from=${encodeURIComponent(location.host)}`,
},
]
}
];
+13 -7
View File
@@ -1,30 +1,36 @@
import { FC, ChangeEvent } from "react";
import Tippy from "@tippyjs/react";
import IconSearch from "../../assets/icons/search.svg";
import IconAdd from "../../assets/icons/add.svg";
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
import Tooltip from "../../common/component/Tooltip";
import { FC, ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../../app/store";
import { compareVersion } from "../../common/utils";
import Tooltip from "../../common/component/Tooltip";
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
type Props = {
input: string,
openModal: () => void,
updateInput: (input: string) => void
};
const Search: FC<Props> = ({ input, updateInput }) => {
const Search: FC<Props> = ({ input, updateInput, openModal }) => {
const serverVersion = useAppSelector(store => store.server.version);
const { t } = useTranslation();
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
updateInput(evt.target.value);
};
const enableContact = compareVersion(serverVersion, "0.3.7") >= 0;
return (
<div className="hidden md:flex relative min-h-[56px] px-2 py-3 items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
<div className="flex items-center gap-1">
<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50" onChange={handleInput} />
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50 outline-none" onChange={handleInput} />
</div>
<Tooltip tip={t("more")} placement="bottom">
{enableContact ? <IconAdd onClick={openModal} role="button" className="dark:fill-gray-400" /> : <Tooltip tip={t("more")} placement="bottom">
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
<IconAdd role="button" className="dark:fill-gray-400" />
</Tippy>
</Tooltip>
</Tooltip>}
</div>
);
};
+8 -3
View File
@@ -1,4 +1,4 @@
import { memo, useEffect, useRef } from "react";
import { memo, useEffect, useRef, useState } from "react";
import { NavLink, useParams, useLocation } from "react-router-dom";
import { useDispatch } from "react-redux";
import { ViewportList } from 'react-viewport-list';
@@ -12,6 +12,7 @@ import BlankPlaceholder from "../../common/component/BlankPlaceholder";
import useFilteredUsers from "../../common/hook/useFilteredUsers";
import clsx from "clsx";
import GoBackNav from "../../common/component/GoBackNav";
import SearchUser from "../../common/component/SearchUser";
function UsersPage() {
const ref = useRef<HTMLDivElement | null>(
@@ -19,6 +20,7 @@ function UsersPage() {
);
const dispatch = useDispatch();
const { pathname } = useLocation();
const [modalVisible, setModalVisible] = useState(false);
const { input, updateInput, users } = useFilteredUsers();
const { user_id } = useParams();
// 记住路由
@@ -28,7 +30,9 @@ function UsersPage() {
dispatch(updateRememberedNavs({ key: "user", path: pathname }));
};
}, [pathname]);
const toggleModal = () => {
setModalVisible(prev => !prev);
};
if (!users) return null;
const isUserDetail = !!user_id;
return (
@@ -36,7 +40,8 @@ function UsersPage() {
<div className={clsx("md:rounded-l-2xl bg-white dark:bg-gray-800 relative flex flex-col w-full md:w-auto md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]",
isUserDetail && "hidden md:flex"
)}>
<Search input={input} updateInput={updateInput} />
<Search input={input} updateInput={updateInput} openModal={toggleModal} />
{modalVisible && <SearchUser closeModal={toggleModal} />}
<div className="flex flex-col md:gap-1 px-2 pt-3 pb-20 md:py-3 overflow-scroll" ref={ref}>
<ViewportList
viewportRef={ref}