fix(pc-client): PC 端统一手机端设计 Token 并补提交 src/components(B-52 视觉阻塞修复)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -25,7 +25,7 @@ function App() {
|
||||
autoInsertSpaceInButton={false}
|
||||
locale={locale === "zh-CN" ? zhCN : enUS}
|
||||
theme={{
|
||||
token: { colorPrimary: "#0073D9" },
|
||||
token: { colorPrimary: "#3B87F5", colorError: "#F53F3F" },
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<span className="text-base font-medium">{t("placeholder.about")}</span>
|
||||
<CloseOutlined
|
||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
||||
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||
rev={undefined}
|
||||
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">
|
||||
<span className="text-base font-medium">{t("placeholder.blackList")}</span>
|
||||
<CloseOutlined
|
||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
||||
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||
rev={undefined}
|
||||
onClick={closeOverlay}
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const PersonalSettingsContent = ({
|
||||
<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>
|
||||
<CloseOutlined
|
||||
className="app-no-drag cursor-pointer text-[#8e9aaf]"
|
||||
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
|
||||
rev={undefined}
|
||||
onClick={closeOverlay}
|
||||
/>
|
||||
|
||||
@@ -128,7 +128,7 @@ const NavItem = ({ nav: { icon: Icon, icon_active: ActiveIcon, title, path } }:
|
||||
<NavIcon
|
||||
className={clsx("text-[22px]", {
|
||||
"text-[var(--primary)]": isActive,
|
||||
"text-[#8e9ab0]": !isActive,
|
||||
"text-[var(--sub-text)]": !isActive,
|
||||
})}
|
||||
rev={undefined}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import ConversationItemComp from "./ConversationItem";
|
||||
// 断网时顶部的深色提示横条
|
||||
const OfflineBar = () => (
|
||||
<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
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -153,7 +153,7 @@ const ConversationSider = () => {
|
||||
<div className="pb-3 text-base font-bold">{t("placeholder.chat")}</div>
|
||||
<Input
|
||||
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")}
|
||||
allowClear
|
||||
value={keyword}
|
||||
|
||||
@@ -141,7 +141,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
|
||||
};
|
||||
|
||||
const toolIconClass =
|
||||
"cursor-pointer text-xl text-[#8e9ab0] hover:text-[#515E70]";
|
||||
"cursor-pointer text-xl text-[var(--sub-text)] hover:text-[#515E70]";
|
||||
|
||||
return (
|
||||
<footer className="relative bg-white">
|
||||
@@ -186,7 +186,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
|
||||
{/* 录音条 */}
|
||||
{recording && (
|
||||
<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">
|
||||
{t("placeholder.microphone")} {seconds}″
|
||||
</span>
|
||||
|
||||
@@ -17,14 +17,14 @@ const FileIcon = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<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"
|
||||
stroke="#0073D9"
|
||||
stroke="var(--primary)"
|
||||
strokeWidth="1.6"
|
||||
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
|
||||
d="M8.5 12h7M8.5 15.5h7"
|
||||
stroke="#0073D9"
|
||||
stroke="var(--primary)"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@ const SoundMessageRender: FC<IMessageItemProps> = ({ message, isSender }) => {
|
||||
<span className={clsx(isSender ? "mr-2" : "ml-2")}>{duration}″</span>
|
||||
</div>
|
||||
{!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>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ const AlphabetIndex: ForwardRefRenderFunction<
|
||||
{indexList.map((letter, idx) => (
|
||||
<span
|
||||
className={clsx("my-0.5 cursor-pointer text-xs text-[var(--sub-text)]", {
|
||||
"!text-[#0073D9]": currentAlphabet === letter,
|
||||
"!text-[var(--primary)]": currentAlphabet === letter,
|
||||
})}
|
||||
key={letter}
|
||||
onClick={() => jumpToLetter(idx, letter)}
|
||||
|
||||
@@ -90,10 +90,10 @@ export const MyFriends = () => {
|
||||
groupCounts={sectionData.groupCounts}
|
||||
groupContent={(index) => (
|
||||
<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]}
|
||||
</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>
|
||||
)}
|
||||
itemContent={(index) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ const GroupListItem = ({
|
||||
<OIMAvatar size={48} src={source?.faceURL} isgroup />
|
||||
<div className="ml-3">
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
.ant-drawer-header {
|
||||
padding: 0 24px;
|
||||
background-color: #e8eaef;
|
||||
background-color: var(--gap-text);
|
||||
border: none;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
.anticon-close,
|
||||
.anticon-right {
|
||||
color: #8e9ab0;
|
||||
color: var(--sub-text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
:root {
|
||||
--full-height: 100%;
|
||||
--chat-bubble: #ffffff;
|
||||
--chat-bubble-sender: #cde6ff;
|
||||
--base-black: #0c1c33;
|
||||
--primary: #0073d9;
|
||||
--primary-active: #eaf4ff;
|
||||
--top-search-bar: #0073d9;
|
||||
--sub-text: #8e9ab0;
|
||||
--gap-text: #e8eaef;
|
||||
--warn-text: #ff381f;
|
||||
--chat-bubble-sender: #d6e7fc;
|
||||
--base-black: #1d2129;
|
||||
--primary: #3b87f5;
|
||||
--primary-active: #ebf3fe;
|
||||
--top-search-bar: #3b87f5;
|
||||
--sub-text: #86909c;
|
||||
--gap-text: #eeeeee;
|
||||
--warn-text: #f53f3f;
|
||||
--moment-text: #6085b1;
|
||||
--searchbar-height: 2.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user