fix(pc-client): PC 端统一手机端设计 Token 并补提交 src/components(B-52 视觉阻塞修复)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
components/
|
# 仅忽略仓库根目录的 components(如 OpenIM 样例遗留目录),
|
||||||
|
# 不匹配 pc-client/src/components 等源码子目录,保证其可入库
|
||||||
|
/components/
|
||||||
.selftest/
|
.selftest/
|
||||||
data/
|
data/
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function App() {
|
|||||||
autoInsertSpaceInButton={false}
|
autoInsertSpaceInButton={false}
|
||||||
locale={locale === "zh-CN" ? zhCN : enUS}
|
locale={locale === "zh-CN" ? zhCN : enUS}
|
||||||
theme={{
|
theme={{
|
||||||
token: { colorPrimary: "#0073D9" },
|
token: { colorPrimary: "#3B87F5", colorError: "#F53F3F" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { ApplicationHandleResult } from "@openim/wasm-client-sdk";
|
||||||
|
import {
|
||||||
|
FriendApplicationItem,
|
||||||
|
GroupApplicationItem,
|
||||||
|
} from "@openim/wasm-client-sdk/lib/types/entity";
|
||||||
|
import { Button, Spin } from "antd";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { memo, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import arrow from "@/assets/images/contact/arrowTopRight.png";
|
||||||
|
import OIMAvatar from "@/components/OIMAvatar";
|
||||||
|
import { IMSDK } from "@/layout/MainContentWrap";
|
||||||
|
import { emit } from "@/utils/events";
|
||||||
|
|
||||||
|
type ApplicationItemSource = FriendApplicationItem & GroupApplicationItem;
|
||||||
|
|
||||||
|
export type AccessFunction = (
|
||||||
|
source: Partial<ApplicationItemSource>,
|
||||||
|
isRecv: boolean,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
const ApplicationItem = ({
|
||||||
|
currentUserID,
|
||||||
|
source,
|
||||||
|
onAccept,
|
||||||
|
onReject,
|
||||||
|
}: {
|
||||||
|
source: Partial<ApplicationItemSource>;
|
||||||
|
currentUserID: string;
|
||||||
|
onAccept: AccessFunction;
|
||||||
|
onReject: AccessFunction;
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const isRecv = source.userID !== currentUserID && source.fromUserID !== currentUserID;
|
||||||
|
const isGroup = Boolean(source.groupID);
|
||||||
|
const showActionBtn = source.handleResult === 0 && isRecv;
|
||||||
|
|
||||||
|
const getApplicationDesc = () => {
|
||||||
|
if (isGroup) {
|
||||||
|
return t("application.applyToJoin");
|
||||||
|
}
|
||||||
|
return isRecv ? t("application.applyToFriend") : t("application.applyToAdd");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
if (isGroup) {
|
||||||
|
return isRecv ? source.nickname : source.groupName;
|
||||||
|
}
|
||||||
|
return isRecv ? source.fromNickname : source.toNickname;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusStr = () => {
|
||||||
|
if (source.handleResult === ApplicationHandleResult.Agree) {
|
||||||
|
return t("application.agreed");
|
||||||
|
}
|
||||||
|
if (source.handleResult === ApplicationHandleResult.Reject) {
|
||||||
|
return t("application.refused");
|
||||||
|
}
|
||||||
|
return t("application.pending");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvatarUrl = () => {
|
||||||
|
if (isGroup) {
|
||||||
|
return isRecv ? source.userFaceURL : source.groupFaceURL;
|
||||||
|
}
|
||||||
|
return isRecv ? source.fromFaceURL : source.toFaceURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadingWrap = async (isAgree: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
await (isAgree ? onAccept(source, isRecv) : onReject(source, isRecv));
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryShowCard = useCallback(async () => {
|
||||||
|
if (isGroup) {
|
||||||
|
const { data } = await IMSDK.getSpecifiedGroupsInfo([source.groupID!]);
|
||||||
|
emit("OPEN_GROUP_CARD", data[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.userClick(isRecv ? source.fromUserID : source.toUserID);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className="flex flex-row items-center justify-between p-3.5 transition-colors hover:bg-[var(--primary-active)]">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<OIMAvatar
|
||||||
|
src={getAvatarUrl()}
|
||||||
|
text={getTitle()}
|
||||||
|
isgroup={isGroup && !isRecv}
|
||||||
|
onClick={tryShowCard}
|
||||||
|
/>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm">{getTitle()}</p>
|
||||||
|
<p className="pb-2.5 pt-[5px] text-xs ">
|
||||||
|
{getApplicationDesc()}
|
||||||
|
{(isGroup || (!isGroup && !isRecv)) && (
|
||||||
|
<span className="ml-1 text-xs text-[var(--primary)]">
|
||||||
|
{source.groupName || source.toNickname}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--sub-text)]">
|
||||||
|
{t("application.information")}:
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--sub-text)]">{source.reqMsg}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showActionBtn && (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="mr-5.5 h-8 w-[60px]">
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
size="small"
|
||||||
|
onClick={() => loadingWrap(false)}
|
||||||
|
className="!h-full !rounded-md border-2 border-[var(--primary)] text-[var(--primary)]"
|
||||||
|
>
|
||||||
|
{t("application.refuse")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-[60px]">
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
className="!h-full !rounded-md bg-[var(--primary)]"
|
||||||
|
onClick={() => loadingWrap(true)}
|
||||||
|
>
|
||||||
|
{t("application.agree")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showActionBtn && (
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
{!isRecv && <img className="mr-2 h-4 w-4" src={arrow} alt="" />}
|
||||||
|
<p className="text-sm text-[var(--sub-text)]">{getStatusStr()}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ApplicationItem);
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
.image-inline {
|
||||||
|
margin: 2px;
|
||||||
|
img {
|
||||||
|
aspect-ratio: auto !important;
|
||||||
|
max-width: 30vw !important;
|
||||||
|
max-height: 15vh !important;
|
||||||
|
width: auto !important;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content {
|
||||||
|
padding: 0 18px !important;
|
||||||
|
border: none !important;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content [draggable="true"] {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content > p {
|
||||||
|
margin: 10px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-toolbar {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor__editable {
|
||||||
|
--ck-inner-shadow: none !important;
|
||||||
|
--ck-drop-shadow: none !important;
|
||||||
|
--ck-drop-shadow-active: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor__editable:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-editor__editable[role="textbox"]:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-powered-by {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-widget__type-around__button,
|
||||||
|
.ck-widget__type-around__button_after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor__main,
|
||||||
|
.ck-content {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-image-upload-complete-icon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-image-upload-complete-icon:after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-balloon-panel_visible {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-list__item .ck-on {
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck .ck-widget {
|
||||||
|
--ck-widget-outline-thickness: 2px !important;
|
||||||
|
--ck-color-widget-hover-border: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck .ck-widget.ck-widget_selected {
|
||||||
|
--ck-widget-outline-thickness: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-clipboard-drop-target-line {
|
||||||
|
--ck-clipboard-drop-target-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-powered-by-balloon {
|
||||||
|
z-index: -99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor__top {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import "./index.scss";
|
||||||
|
import "ckeditor5/ckeditor5.css";
|
||||||
|
|
||||||
|
import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
|
||||||
|
import { Essentials } from "@ckeditor/ckeditor5-essentials";
|
||||||
|
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
|
||||||
|
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
ForwardRefRenderFunction,
|
||||||
|
memo,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export type CKEditorRef = {
|
||||||
|
focus: (moveToEnd?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CKEditorProps {
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onEnter?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiData {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyCodes = {
|
||||||
|
delete: 46,
|
||||||
|
backspace: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Index: ForwardRefRenderFunction<CKEditorRef, CKEditorProps> = (
|
||||||
|
{ value, placeholder, onChange, onEnter },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const ckEditor = useRef<ClassicEditor | null>(null);
|
||||||
|
|
||||||
|
const focus = (moveToEnd = false) => {
|
||||||
|
const editor = ckEditor.current;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
const model = editor.model;
|
||||||
|
const view = editor.editing.view;
|
||||||
|
const root = model.document.getRoot();
|
||||||
|
if (moveToEnd && root) {
|
||||||
|
const range = model.createRange(model.createPositionAt(root, "end"));
|
||||||
|
|
||||||
|
model.change((writer) => {
|
||||||
|
writer.setSelection(range);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenKeydown = (editor: ClassicEditor) => {
|
||||||
|
editor.editing.view.document.on(
|
||||||
|
"keydown",
|
||||||
|
(evt, data) => {
|
||||||
|
if (data.keyCode === 13 && !data.shiftKey) {
|
||||||
|
data.preventDefault();
|
||||||
|
evt.stop();
|
||||||
|
onEnter?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.keyCode === keyCodes.backspace || data.keyCode === keyCodes.delete) {
|
||||||
|
const selection = editor.model.document.selection;
|
||||||
|
const hasSelectContent = !editor.model.getSelectedContent(selection).isEmpty;
|
||||||
|
const hasEditorContent = Boolean(editor.getData());
|
||||||
|
|
||||||
|
if (!hasEditorContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSelectContent) return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ priority: "high" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
focus,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CKEditor
|
||||||
|
editor={ClassicEditor}
|
||||||
|
data={value}
|
||||||
|
config={{
|
||||||
|
placeholder,
|
||||||
|
toolbar: [],
|
||||||
|
image: {
|
||||||
|
toolbar: [],
|
||||||
|
insert: {
|
||||||
|
type: "inline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [Essentials, Paragraph],
|
||||||
|
}}
|
||||||
|
onReady={(editor) => {
|
||||||
|
ckEditor.current = editor;
|
||||||
|
listenKeydown(editor);
|
||||||
|
focus(true);
|
||||||
|
}}
|
||||||
|
onChange={(event, editor) => {
|
||||||
|
const data = editor.getData();
|
||||||
|
onChange?.(data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(forwardRef(Index));
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export const replaceEmoji2Str = (text: string) => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(text, "text/html");
|
||||||
|
|
||||||
|
const emojiEls: HTMLImageElement[] = Array.from(doc.querySelectorAll(".emojione"));
|
||||||
|
emojiEls.map((face) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const escapedOut = face.outerHTML.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||||
|
text = text.replace(new RegExp(escapedOut, "g"), face.alt);
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCleanText = (html: string) => {
|
||||||
|
let text = replaceEmoji2Str(html);
|
||||||
|
text = text.replace(/<\/p><p>/g, "\n");
|
||||||
|
text = text.replace(/<br\s*[/]?>/gi, "\n");
|
||||||
|
text = text.replace(/<[^>]+>/g, "");
|
||||||
|
text = convertChar(text);
|
||||||
|
text = decodeHtmlEntities(text);
|
||||||
|
return text.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
let textAreaDom: HTMLTextAreaElement | null = null;
|
||||||
|
const decodeHtmlEntities = (text: string) => {
|
||||||
|
if (!textAreaDom) {
|
||||||
|
textAreaDom = document.createElement("textarea");
|
||||||
|
}
|
||||||
|
textAreaDom.innerHTML = text;
|
||||||
|
return textAreaDom.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertChar = (text: string) => text.replace(/ /gi, " ");
|
||||||
|
|
||||||
|
export const getCleanTextExceptImg = (html: string) => {
|
||||||
|
html = replaceEmoji2Str(html);
|
||||||
|
|
||||||
|
const regP = /<\/p><p>/g;
|
||||||
|
html = html.replace(regP, "</p><br><p>");
|
||||||
|
|
||||||
|
const regBr = /<br\s*\/?>/gi;
|
||||||
|
html = html.replace(regBr, "\n");
|
||||||
|
|
||||||
|
const regWithoutHtmlExceptImg = /<(?!img\s*\/?)[^>]+>/gi;
|
||||||
|
return html.replace(regWithoutHtmlExceptImg, "");
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Modal, ModalProps } from "antd";
|
||||||
|
import { FC, memo, useRef, useState } from "react";
|
||||||
|
import type { DraggableData, DraggableEvent } from "react-draggable";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
|
||||||
|
interface IDraggableModalWrapProps extends ModalProps {
|
||||||
|
ignoreClasses?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraggableModalWrap: FC<IDraggableModalWrapProps> = (props) => {
|
||||||
|
const [bounds, setBounds] = useState({ left: 0, top: 0, bottom: 0, right: 0 });
|
||||||
|
const draggleRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
|
||||||
|
const { clientWidth, clientHeight } = window.document.documentElement;
|
||||||
|
const targetRect = draggleRef.current?.getBoundingClientRect();
|
||||||
|
if (!targetRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBounds({
|
||||||
|
left: -targetRect.left + uiData.x,
|
||||||
|
right: clientWidth - (targetRect.right - uiData.x),
|
||||||
|
top: -targetRect.top + uiData.y,
|
||||||
|
bottom: clientHeight - (targetRect.bottom - uiData.y),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...props}
|
||||||
|
modalRender={(modal) => (
|
||||||
|
<Draggable
|
||||||
|
allowAnyClick
|
||||||
|
cancel={props.ignoreClasses}
|
||||||
|
bounds={bounds}
|
||||||
|
onStart={(event, uiData) => onStart(event, uiData)}
|
||||||
|
>
|
||||||
|
<div ref={draggleRef}>{modal}</div>
|
||||||
|
</Draggable>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DraggableModalWrap);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { EnterOutlined } from "@ant-design/icons";
|
||||||
|
import { useClickAway } from "ahooks";
|
||||||
|
import { Input, InputRef } from "antd";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import edit_name from "@/assets/images/chatSetting/edit_name.png";
|
||||||
|
|
||||||
|
interface IEditableContentProps {
|
||||||
|
editable?: boolean;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
textClassName?: string;
|
||||||
|
onChange?: (value: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableContent: FC<IEditableContentProps> = ({
|
||||||
|
editable,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
textClassName,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
const [editState, setEditState] = useState({
|
||||||
|
isEdit: false,
|
||||||
|
loading: false,
|
||||||
|
innerValue: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
useClickAway(() => {
|
||||||
|
if (editState.isEdit) {
|
||||||
|
setEditState({
|
||||||
|
isEdit: false,
|
||||||
|
loading: false,
|
||||||
|
innerValue: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [wrapRef]);
|
||||||
|
|
||||||
|
const toggleEdit = (e: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditState({
|
||||||
|
isEdit: true,
|
||||||
|
loading: false,
|
||||||
|
innerValue: value === "-" ? "" : value,
|
||||||
|
});
|
||||||
|
setTimeout(() => inputRef.current?.focus());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPressEnter = async (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement> & { target: { value: string } },
|
||||||
|
) => {
|
||||||
|
setEditState((state) => ({ ...state, loading: true }));
|
||||||
|
await onChange?.(e.target.value);
|
||||||
|
setEditState({
|
||||||
|
isEdit: false,
|
||||||
|
loading: false,
|
||||||
|
innerValue: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapRef} className={clsx("ml-3 flex items-center", className)}>
|
||||||
|
{editState.isEdit ? (
|
||||||
|
<Input
|
||||||
|
spellCheck={false}
|
||||||
|
value={editState.innerValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={20}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditState((state) => ({ ...state, innerValue: e.target.value }))
|
||||||
|
}
|
||||||
|
ref={inputRef}
|
||||||
|
onPressEnter={onPressEnter}
|
||||||
|
suffix={<EnterOutlined rev={undefined} />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={clsx("mr-1 max-w-[240px] truncate", textClassName)}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
{editable && (
|
||||||
|
<img
|
||||||
|
className="cursor-pointer"
|
||||||
|
width={14}
|
||||||
|
src={edit_name}
|
||||||
|
alt="edit name"
|
||||||
|
onClick={toggleEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableContent;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component, ErrorInfo, ReactNode } from "react";
|
||||||
|
|
||||||
|
type ErrorBoundaryProps = {
|
||||||
|
logTips?: string;
|
||||||
|
placeholder: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrorBoundaryState = {
|
||||||
|
hasError: boolean;
|
||||||
|
logTips?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, logTips: props.logTips };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error(this.state.logTips ?? "ErrorBoundary");
|
||||||
|
console.error(errorInfo);
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
this.setState({ hasError: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return this.props.placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.sider_resize {
|
||||||
|
width: 300px;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 45vw;
|
||||||
|
resize: horizontal;
|
||||||
|
overflow: scroll;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider_bar {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-left: 1px solid var(--gap-text);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import styles from "./flexible-sider.module.scss";
|
||||||
|
|
||||||
|
const FlexibleSider = ({
|
||||||
|
needHidden,
|
||||||
|
children,
|
||||||
|
wrapClassName,
|
||||||
|
}: {
|
||||||
|
needHidden: boolean;
|
||||||
|
wrapClassName?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<aside
|
||||||
|
className={clsx(
|
||||||
|
"relative bg-white dark:text-white",
|
||||||
|
{ "max-[600px]:hidden": needHidden },
|
||||||
|
{ "max-[600px]:!max-w-none max-[600px]:!basis-full": !needHidden },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-0 left-0 right-1 top-0 z-10 overflow-hidden ${
|
||||||
|
wrapClassName ?? ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className={styles.sider_resize}></div>
|
||||||
|
<div className={styles.sider_bar}></div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FlexibleSider;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Avatar as AntdAvatar, AvatarProps } from "antd";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import default_group from "@/assets/images/contact/group.png";
|
||||||
|
import { avatarList, getDefaultAvatar } from "@/utils/avatar";
|
||||||
|
|
||||||
|
const default_avatars = avatarList.map((item) => item.name);
|
||||||
|
|
||||||
|
// 头像文字:两个字以内原样显示;中文名取最后一个字,其他取首字母
|
||||||
|
const getDisplayText = (text?: string) => {
|
||||||
|
if (!text) return text;
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (trimmed.length <= 2) return trimmed;
|
||||||
|
if (/^[一-龥]/.test(trimmed)) return trimmed.slice(-1);
|
||||||
|
return trimmed[0].toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IOIMAvatarProps extends AvatarProps {
|
||||||
|
text?: string;
|
||||||
|
color?: string;
|
||||||
|
bgColor?: string;
|
||||||
|
isgroup?: boolean;
|
||||||
|
isnotification?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OIMAvatar: React.FC<IOIMAvatarProps> = (props) => {
|
||||||
|
const {
|
||||||
|
src,
|
||||||
|
text,
|
||||||
|
size = 42,
|
||||||
|
color = "#fff",
|
||||||
|
bgColor = "#3B87F5",
|
||||||
|
isgroup = false,
|
||||||
|
isnotification,
|
||||||
|
} = props;
|
||||||
|
const [errorHolder, setErrorHolder] = React.useState<string>();
|
||||||
|
|
||||||
|
const getAvatarUrl = useMemo(() => {
|
||||||
|
if (src) {
|
||||||
|
if (default_avatars.includes(src as string))
|
||||||
|
return getDefaultAvatar(src as string);
|
||||||
|
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
return isgroup ? default_group : undefined;
|
||||||
|
}, [src, isgroup, isnotification]);
|
||||||
|
|
||||||
|
const avatarProps = { ...props, isgroup: undefined, isnotification: undefined };
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isgroup) {
|
||||||
|
setErrorHolder(undefined);
|
||||||
|
}
|
||||||
|
}, [isgroup]);
|
||||||
|
|
||||||
|
const errorHandler = () => {
|
||||||
|
if (isgroup) {
|
||||||
|
setErrorHolder(default_group);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AntdAvatar
|
||||||
|
style={{
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
minWidth: `${size}px`,
|
||||||
|
minHeight: `${size}px`,
|
||||||
|
lineHeight: `${size - 2}px`,
|
||||||
|
borderRadius: `${Math.round(size * 0.18)}px`,
|
||||||
|
color,
|
||||||
|
}}
|
||||||
|
shape="square"
|
||||||
|
{...avatarProps}
|
||||||
|
className={clsx(
|
||||||
|
{
|
||||||
|
"cursor-pointer": Boolean(props.onClick),
|
||||||
|
},
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
src={errorHolder ?? getAvatarUrl}
|
||||||
|
onError={errorHandler as any}
|
||||||
|
>
|
||||||
|
{getDisplayText(text)}
|
||||||
|
</AntdAvatar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OIMAvatar;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Switch } from "antd";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
interface ISettingRowProps {
|
||||||
|
title: string;
|
||||||
|
value?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
tryChange?: (checked: boolean) => Promise<void>;
|
||||||
|
rowClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingRow: FC<ISettingRowProps> = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
hidden,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
tryChange,
|
||||||
|
rowClick,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const onClick = async (checked: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
await tryChange?.(checked);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("flex items-center justify-between p-4", className)}
|
||||||
|
onClick={rowClick}
|
||||||
|
>
|
||||||
|
<div className="font-medium">{title}</div>
|
||||||
|
{children ?? (
|
||||||
|
<Switch
|
||||||
|
className="bg-[var(--sub-text)]"
|
||||||
|
loading={loading}
|
||||||
|
checked={value}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingRow;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Platform } from "@openim/wasm-client-sdk";
|
||||||
|
import { useKeyPress } from "ahooks";
|
||||||
|
|
||||||
|
import win_close from "@/assets/images/topSearchBar/win_close.png";
|
||||||
|
import win_max from "@/assets/images/topSearchBar/win_max.png";
|
||||||
|
import win_min from "@/assets/images/topSearchBar/win_min.png";
|
||||||
|
|
||||||
|
const WindowControlBar = () => {
|
||||||
|
useKeyPress("esc", () => {
|
||||||
|
window.electronAPI?.ipcInvoke("minimizeWindow");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!window.electronAPI || window.electronAPI?.getPlatform() === Platform.MacOSX) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="absolute right-3 top-3.5 z-[99999999] flex h-fit items-center">
|
||||||
|
<div
|
||||||
|
className="app-no-drag flex h-[14px] cursor-pointer items-center"
|
||||||
|
onClick={() => window.electronAPI?.ipcInvoke("minimizeWindow")}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="app-no-drag cursor-pointer"
|
||||||
|
width={14}
|
||||||
|
src={win_min}
|
||||||
|
alt="win_min"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
className="app-no-drag mx-3 cursor-pointer"
|
||||||
|
width={13}
|
||||||
|
src={win_max}
|
||||||
|
alt="win_max"
|
||||||
|
onClick={() => window.electronAPI?.ipcInvoke("maxmizeWindow")}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="app-no-drag cursor-pointer"
|
||||||
|
width={12}
|
||||||
|
src={win_close}
|
||||||
|
alt="win_close"
|
||||||
|
onClick={() => window.electronAPI?.ipcInvoke("closeWindow")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WindowControlBar;
|
||||||
@@ -136,7 +136,7 @@ export const AboutContent = ({ closeOverlay }: { closeOverlay?: () => void }) =>
|
|||||||
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
|
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
|
||||||
<span className="text-base font-medium">{t("placeholder.about")}</span>
|
<span className="text-base font-medium">{t("placeholder.about")}</span>
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||||
rev={undefined}
|
rev={undefined}
|
||||||
onClick={closeOverlay}
|
onClick={closeOverlay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const BlackListContent = ({ closeOverlay }: { closeOverlay?: () => void }
|
|||||||
<div className="flex items-center justify-between bg-[var(--gap-text)] p-5">
|
<div className="flex items-center justify-between bg-[var(--gap-text)] p-5">
|
||||||
<span className="text-base font-medium">{t("placeholder.blackList")}</span>
|
<span className="text-base font-medium">{t("placeholder.blackList")}</span>
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||||
rev={undefined}
|
rev={undefined}
|
||||||
onClick={closeOverlay}
|
onClick={closeOverlay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const PersonalSettingsContent = ({
|
|||||||
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
|
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
|
||||||
<span className="text-base font-medium">{t("placeholder.accountSetting")}</span>
|
<span className="text-base font-medium">{t("placeholder.accountSetting")}</span>
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||||
rev={undefined}
|
rev={undefined}
|
||||||
onClick={closeOverlay}
|
onClick={closeOverlay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const NavItem = ({ nav: { icon: Icon, icon_active: ActiveIcon, title, path } }:
|
|||||||
<NavIcon
|
<NavIcon
|
||||||
className={clsx("text-[22px]", {
|
className={clsx("text-[22px]", {
|
||||||
"text-[var(--primary)]": isActive,
|
"text-[var(--primary)]": isActive,
|
||||||
"text-[#8e9ab0]": !isActive,
|
"text-[var(--sub-text)]": !isActive,
|
||||||
})}
|
})}
|
||||||
rev={undefined}
|
rev={undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import ConversationItemComp from "./ConversationItem";
|
|||||||
// 断网时顶部的深色提示横条
|
// 断网时顶部的深色提示横条
|
||||||
const OfflineBar = () => (
|
const OfflineBar = () => (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex h-9 items-center bg-[#4a4a4a] px-3 text-xs text-white">
|
<div className="flex h-9 items-center bg-[#3a3f47] px-3 text-xs text-white">
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@@ -153,7 +153,7 @@ const ConversationSider = () => {
|
|||||||
<div className="pb-3 text-base font-bold">{t("placeholder.chat")}</div>
|
<div className="pb-3 text-base font-bold">{t("placeholder.chat")}</div>
|
||||||
<Input
|
<Input
|
||||||
className="app-no-drag mb-2 rounded-md border-none !bg-[#F3F5F7]"
|
className="app-no-drag mb-2 rounded-md border-none !bg-[#F3F5F7]"
|
||||||
prefix={<SearchOutlined className="text-[#8e9ab0]" rev={undefined} />}
|
prefix={<SearchOutlined className="text-[var(--sub-text)]" rev={undefined} />}
|
||||||
placeholder={t("placeholder.search")}
|
placeholder={t("placeholder.search")}
|
||||||
allowClear
|
allowClear
|
||||||
value={keyword}
|
value={keyword}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toolIconClass =
|
const toolIconClass =
|
||||||
"cursor-pointer text-xl text-[#8e9ab0] hover:text-[#515E70]";
|
"cursor-pointer text-xl text-[var(--sub-text)] hover:text-[#515E70]";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-white">
|
<footer className="relative bg-white">
|
||||||
@@ -186,7 +186,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
|
|||||||
{/* 录音条 */}
|
{/* 录音条 */}
|
||||||
{recording && (
|
{recording && (
|
||||||
<div className="mx-4 mt-2 flex items-center rounded-md bg-[#F3F5F7] px-3 py-2">
|
<div className="mx-4 mt-2 flex items-center rounded-md bg-[#F3F5F7] px-3 py-2">
|
||||||
<span className="mr-2 h-2 w-2 animate-pulse rounded-full bg-[#F4493C]" />
|
<span className="mr-2 h-2 w-2 animate-pulse rounded-full bg-[var(--warn-text)]" />
|
||||||
<span className="flex-1 text-sm">
|
<span className="flex-1 text-sm">
|
||||||
{t("placeholder.microphone")} {seconds}″
|
{t("placeholder.microphone")} {seconds}″
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ const FileIcon = () => (
|
|||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||||
<path
|
<path
|
||||||
d="M6 2.5h8L19 7.5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-17a1 1 0 0 1 1-1Z"
|
d="M6 2.5h8L19 7.5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-17a1 1 0 0 1 1-1Z"
|
||||||
stroke="#0073D9"
|
stroke="var(--primary)"
|
||||||
strokeWidth="1.6"
|
strokeWidth="1.6"
|
||||||
fill="#fff"
|
fill="#fff"
|
||||||
/>
|
/>
|
||||||
<path d="M13.5 2.5v5.5H19" stroke="#0073D9" strokeWidth="1.6" fill="none" />
|
<path d="M13.5 2.5v5.5H19" stroke="var(--primary)" strokeWidth="1.6" fill="none" />
|
||||||
<path
|
<path
|
||||||
d="M8.5 12h7M8.5 15.5h7"
|
d="M8.5 12h7M8.5 15.5h7"
|
||||||
stroke="#0073D9"
|
stroke="var(--primary)"
|
||||||
strokeWidth="1.4"
|
strokeWidth="1.4"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const SoundMessageRender: FC<IMessageItemProps> = ({ message, isSender }) => {
|
|||||||
<span className={clsx(isSender ? "mr-2" : "ml-2")}>{duration}″</span>
|
<span className={clsx(isSender ? "mr-2" : "ml-2")}>{duration}″</span>
|
||||||
</div>
|
</div>
|
||||||
{!isSender && !played && (
|
{!isSender && !played && (
|
||||||
<span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-[#F4493C]" />
|
<span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-[var(--warn-text)]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const AlphabetIndex: ForwardRefRenderFunction<
|
|||||||
{indexList.map((letter, idx) => (
|
{indexList.map((letter, idx) => (
|
||||||
<span
|
<span
|
||||||
className={clsx("my-0.5 cursor-pointer text-xs text-[var(--sub-text)]", {
|
className={clsx("my-0.5 cursor-pointer text-xs text-[var(--sub-text)]", {
|
||||||
"!text-[#0073D9]": currentAlphabet === letter,
|
"!text-[var(--primary)]": currentAlphabet === letter,
|
||||||
})}
|
})}
|
||||||
key={letter}
|
key={letter}
|
||||||
onClick={() => jumpToLetter(idx, letter)}
|
onClick={() => jumpToLetter(idx, letter)}
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ export const MyFriends = () => {
|
|||||||
groupCounts={sectionData.groupCounts}
|
groupCounts={sectionData.groupCounts}
|
||||||
groupContent={(index) => (
|
groupContent={(index) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-white px-3.5 pb-1 text-[13px] text-[#8E9AB0FF]">
|
<div className="bg-white px-3.5 pb-1 text-[13px] text-[var(--sub-text)]">
|
||||||
{sectionData.indexList[index]}
|
{sectionData.indexList[index]}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-3.5 mb-3 h-px w-full bg-[#E8EAEFFF] bg-white" />
|
<div className="mx-3.5 mb-3 h-px w-full bg-[var(--gap-text)] bg-white" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
itemContent={(index) => {
|
itemContent={(index) => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const GroupListItem = ({
|
|||||||
<OIMAvatar size={48} src={source?.faceURL} isgroup />
|
<OIMAvatar size={48} src={source?.faceURL} isgroup />
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm">{source.groupName}</p>
|
<p className="text-sm">{source.groupName}</p>
|
||||||
<p className="text-xs text-[#8E9AB0FF]">{source.memberCount}</p>
|
<p className="text-xs text-[var(--sub-text)]">{source.memberCount}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
.ant-drawer-header {
|
.ant-drawer-header {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
background-color: #e8eaef;
|
background-color: var(--gap-text);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
.ant-drawer-header-title {
|
.ant-drawer-header-title {
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
.anticon-close,
|
.anticon-close,
|
||||||
.anticon-right {
|
.anticon-right {
|
||||||
color: #8e9ab0;
|
color: var(--sub-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
:root {
|
:root {
|
||||||
--full-height: 100%;
|
--full-height: 100%;
|
||||||
--chat-bubble: #ffffff;
|
--chat-bubble: #ffffff;
|
||||||
--chat-bubble-sender: #cde6ff;
|
--chat-bubble-sender: #d6e7fc;
|
||||||
--base-black: #0c1c33;
|
--base-black: #1d2129;
|
||||||
--primary: #0073d9;
|
--primary: #3b87f5;
|
||||||
--primary-active: #eaf4ff;
|
--primary-active: #ebf3fe;
|
||||||
--top-search-bar: #0073d9;
|
--top-search-bar: #3b87f5;
|
||||||
--sub-text: #8e9ab0;
|
--sub-text: #86909c;
|
||||||
--gap-text: #e8eaef;
|
--gap-text: #eeeeee;
|
||||||
--warn-text: #ff381f;
|
--warn-text: #f53f3f;
|
||||||
--moment-text: #6085b1;
|
--moment-text: #6085b1;
|
||||||
--searchbar-height: 2.5rem;
|
--searchbar-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user