mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 22:49:26 +08:00
feat(notification): adapt the style of the notification pop-up window for mobile (#6557)
* refactor: improve message detail navigation * refactor: extract InboxPopup component for better readability * refactor: restructure ActionDrawer to use MobilePopup for improved modularity * feat: enhance MobilePopup and related components for better mobile experience * feat: add onClick handler to PinnedPluginList for better user interaction * fix: update InboxPopup visibility logic and enhance message navigation handling
This commit is contained in:
parent
d429835215
commit
4ee9ccebfb
@ -88,7 +88,7 @@ const dividerTheme = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PinnedPluginList = React.memo(() => {
|
export const PinnedPluginList = React.memo((props: { onClick?: () => void }) => {
|
||||||
const { allowAll, snippets } = useACLRoleContext();
|
const { allowAll, snippets } = useACLRoleContext();
|
||||||
const getSnippetsAllow = (aclKey) => {
|
const getSnippetsAllow = (aclKey) => {
|
||||||
return allowAll || aclKey === '*' || snippets?.includes(aclKey);
|
return allowAll || aclKey === '*' || snippets?.includes(aclKey);
|
||||||
@ -98,13 +98,15 @@ export const PinnedPluginList = React.memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pinnedPluginListClassName}>
|
<div className={pinnedPluginListClassName}>
|
||||||
{Object.keys(ctx.items)
|
<div onClick={props.onClick}>
|
||||||
.sort((a, b) => ctx.items[a].order - ctx.items[b].order)
|
{Object.keys(ctx.items)
|
||||||
.filter((key) => getSnippetsAllow(ctx.items[key].snippet))
|
.sort((a, b) => ctx.items[a].order - ctx.items[b].order)
|
||||||
.map((key) => {
|
.filter((key) => getSnippetsAllow(ctx.items[key].snippet))
|
||||||
const Action = get(components, ctx.items[key].component);
|
.map((key) => {
|
||||||
return Action ? <Action key={key} /> : null;
|
const Action = get(components, ctx.items[key].component);
|
||||||
})}
|
return Action ? <Action key={key} /> : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<ConfigProvider theme={dividerTheme}>
|
<ConfigProvider theme={dividerTheme}>
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
@ -12,6 +12,7 @@ import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layou
|
|||||||
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
|
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
@ -52,7 +53,6 @@ import { KeepAlive } from './KeepAlive';
|
|||||||
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
|
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
|
||||||
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
|
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
|
||||||
import { userCenterSettings } from './userCenterSettings';
|
import { userCenterSettings } from './userCenterSettings';
|
||||||
import { createGlobalStyle, createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
export { KeepAlive, NocoBaseDesktopRouteType };
|
export { KeepAlive, NocoBaseDesktopRouteType };
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ const AllAccessDesktopRoutesContext = createContext<{
|
|||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}>({
|
}>({
|
||||||
allAccessRoutes: emptyArray,
|
allAccessRoutes: emptyArray,
|
||||||
refresh: () => {},
|
refresh: () => { },
|
||||||
});
|
});
|
||||||
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
||||||
|
|
||||||
@ -418,9 +418,22 @@ const popoverStyle = css`
|
|||||||
|
|
||||||
const MobileActions: FC = (props) => {
|
const MobileActions: FC = (props) => {
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// 点击时立即关闭 Popover,避免影响用户操作
|
||||||
|
const handleContentClick = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover rootClassName={popoverStyle} content={<PinnedPluginList />} color={token.colorBgHeader}>
|
<Popover
|
||||||
|
rootClassName={popoverStyle}
|
||||||
|
content={<PinnedPluginList onClick={handleContentClick} />}
|
||||||
|
color={token.colorBgHeader}
|
||||||
|
trigger="click"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
<div style={{ padding: '0 16px', display: 'flex', alignItems: 'center', height: '100%', marginRight: -16 }}>
|
<div style={{ padding: '0 16px', display: 'flex', alignItems: 'center', height: '100%', marginRight: -16 }}>
|
||||||
<EllipsisOutlined
|
<EllipsisOutlined
|
||||||
style={{
|
style={{
|
||||||
@ -535,7 +548,7 @@ const IsMobileLayoutContext = React.createContext<{
|
|||||||
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}>({
|
}>({
|
||||||
isMobileLayout: false,
|
isMobileLayout: false,
|
||||||
setIsMobileLayout: () => {},
|
setIsMobileLayout: () => { },
|
||||||
});
|
});
|
||||||
|
|
||||||
const MobileLayoutProvider: FC = (props) => {
|
const MobileLayoutProvider: FC = (props) => {
|
||||||
|
@ -20,55 +20,54 @@ import {
|
|||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import { Popup } from 'antd-mobile';
|
import { Popup } from 'antd-mobile';
|
||||||
import { CloseOutline } from 'antd-mobile-icons';
|
import { CloseOutline } from 'antd-mobile-icons';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMobileActionDrawerStyle } from './ActionDrawer.style';
|
import { useMobileActionDrawerStyle } from './ActionDrawer.style';
|
||||||
import { usePopupContainer } from './FilterAction';
|
import { usePopupContainer } from './FilterAction';
|
||||||
import { MIN_Z_INDEX_INCREMENT } from './zIndex';
|
import { MIN_Z_INDEX_INCREMENT } from './zIndex';
|
||||||
|
|
||||||
export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?: string }) => {
|
export interface MobilePopupProps {
|
||||||
const fieldSchema = useFieldSchema();
|
title?: string;
|
||||||
const field = useField();
|
visible: boolean;
|
||||||
const { visible, setVisible } = useActionContext();
|
minHeight?: number | string;
|
||||||
const { popupContainerRef, visiblePopup } = usePopupContainer(visible);
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
visible,
|
||||||
|
onClose: closePopup,
|
||||||
|
children,
|
||||||
|
minHeight,
|
||||||
|
} = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { popupContainerRef } = usePopupContainer(visible);
|
||||||
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
||||||
const parentZIndex = useZIndexContext();
|
const parentZIndex = useZIndexContext();
|
||||||
const { theme: globalTheme } = useGlobalTheme();
|
const { theme: globalTheme } = useGlobalTheme();
|
||||||
|
|
||||||
// this schema need to add padding in the content area of the popup
|
|
||||||
const isSpecialSchema = isChangePasswordSchema(fieldSchema) || isEditProfileSchema(fieldSchema);
|
|
||||||
|
|
||||||
const footerNodeName = isSpecialSchema ? 'Action.Drawer.Footer' : props.footerNodeName;
|
|
||||||
|
|
||||||
const specialStyle = isSpecialSchema ? { backgroundColor: 'white' } : {};
|
|
||||||
|
|
||||||
const newZIndex = parentZIndex + MIN_Z_INDEX_INCREMENT;
|
const newZIndex = parentZIndex + MIN_Z_INDEX_INCREMENT;
|
||||||
|
|
||||||
const zIndexStyle = useMemo(() => {
|
const zIndexStyle = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
zIndex: newZIndex,
|
zIndex: newZIndex,
|
||||||
|
minHeight,
|
||||||
};
|
};
|
||||||
}, [newZIndex]);
|
}, [newZIndex, minHeight]);
|
||||||
|
|
||||||
const footerSchema = fieldSchema.reduceProperties((buf, s) => {
|
|
||||||
if (s['x-component'] === footerNodeName) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = field.title || '';
|
|
||||||
|
|
||||||
const closePopup = useCallback(() => {
|
|
||||||
setVisible(false);
|
|
||||||
}, [setVisible]);
|
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...globalTheme,
|
...globalTheme,
|
||||||
token: {
|
token: {
|
||||||
...globalTheme.token,
|
...globalTheme.token,
|
||||||
marginBlock: 12,
|
|
||||||
zIndexPopupBase: newZIndex,
|
zIndexPopupBase: newZIndex,
|
||||||
|
paddingPageHorizontal: 8,
|
||||||
|
paddingPageVertical: 8,
|
||||||
|
marginBlock: 12,
|
||||||
|
borderRadiusBlock: 8,
|
||||||
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [globalTheme, newZIndex]);
|
}, [globalTheme, newZIndex]);
|
||||||
@ -78,7 +77,7 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
|||||||
<ConfigProvider theme={theme}>
|
<ConfigProvider theme={theme}>
|
||||||
<Popup
|
<Popup
|
||||||
className={`${componentCls} ${hashId}`}
|
className={`${componentCls} ${hashId}`}
|
||||||
visible={visiblePopup}
|
visible={visible}
|
||||||
onClose={closePopup}
|
onClose={closePopup}
|
||||||
onMaskClick={closePopup}
|
onMaskClick={closePopup}
|
||||||
getContainer={() => popupContainerRef.current}
|
getContainer={() => popupContainerRef.current}
|
||||||
@ -93,43 +92,90 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
|||||||
<CloseOutline />
|
<CloseOutline />
|
||||||
</span>
|
</span>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
<span className="nb-mobile-action-drawer-close-icon" onClick={closePopup}>
|
<span
|
||||||
|
className="nb-mobile-action-drawer-close-icon"
|
||||||
|
onClick={closePopup}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={t("Close")}
|
||||||
|
>
|
||||||
<CloseOutline />
|
<CloseOutline />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isSpecialSchema ? (
|
{children}
|
||||||
<div style={{ padding: 12, ...specialStyle }}>
|
|
||||||
<SchemaComponent
|
|
||||||
schema={fieldSchema}
|
|
||||||
filterProperties={(s) => {
|
|
||||||
return s['x-component'] !== footerNodeName;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<SchemaComponent
|
|
||||||
schema={fieldSchema}
|
|
||||||
onlyRenderProperties
|
|
||||||
filterProperties={(s) => {
|
|
||||||
return s['x-component'] !== footerNodeName;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{footerSchema ? (
|
|
||||||
<div className="nb-mobile-action-drawer-footer" style={isSpecialSchema ? specialStyle : null}>
|
|
||||||
<NocoBaseRecursionField
|
|
||||||
basePath={field.address}
|
|
||||||
schema={fieldSchema}
|
|
||||||
onlyRenderProperties
|
|
||||||
filterProperties={(s) => {
|
|
||||||
return s['x-component'] === footerNodeName;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Popup>
|
</Popup>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</zIndexContext.Provider>
|
</zIndexContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?: string }) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const { visible, setVisible } = useActionContext();
|
||||||
|
const { visiblePopup } = usePopupContainer(visible);
|
||||||
|
|
||||||
|
// this schema need to add padding in the content area of the popup
|
||||||
|
const isSpecialSchema = isChangePasswordSchema(fieldSchema) || isEditProfileSchema(fieldSchema);
|
||||||
|
|
||||||
|
const footerNodeName = isSpecialSchema ? 'Action.Drawer.Footer' : props.footerNodeName;
|
||||||
|
|
||||||
|
const specialStyle = isSpecialSchema ? { backgroundColor: 'white' } : {};
|
||||||
|
|
||||||
|
const footerSchema = fieldSchema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === footerNodeName) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = field.title || '';
|
||||||
|
|
||||||
|
const closePopup = useCallback(() => {
|
||||||
|
setVisible(false);
|
||||||
|
}, [setVisible]);
|
||||||
|
|
||||||
|
const popupContent = isSpecialSchema ? (
|
||||||
|
<div style={{ padding: 12, ...specialStyle }}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={fieldSchema}
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] !== footerNodeName;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={fieldSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] !== footerNodeName;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const footerContent = footerSchema ? (
|
||||||
|
<div className="nb-mobile-action-drawer-footer" style={isSpecialSchema ? specialStyle : null}>
|
||||||
|
<NocoBaseRecursionField
|
||||||
|
basePath={field.address}
|
||||||
|
schema={fieldSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] === footerNodeName;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobilePopup
|
||||||
|
title={title}
|
||||||
|
visible={visiblePopup}
|
||||||
|
onClose={closePopup}
|
||||||
|
>
|
||||||
|
{popupContent}
|
||||||
|
{footerContent}
|
||||||
|
</MobilePopup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ import { MobileSettingsBlockSchemaSettings } from './mobile-blocks/settings-bloc
|
|||||||
import pkg from './../../package.json';
|
import pkg from './../../package.json';
|
||||||
import { MobileComponentsProvider } from './MobileComponentsProvider';
|
import { MobileComponentsProvider } from './MobileComponentsProvider';
|
||||||
|
|
||||||
|
export { MobilePopup } from './adaptor-of-desktop/ActionDrawer';
|
||||||
export * from './desktop-mode';
|
export * from './desktop-mode';
|
||||||
export * from './mobile';
|
export * from './mobile';
|
||||||
export * from './mobile-layout';
|
export * from './mobile-layout';
|
||||||
|
@ -12,7 +12,7 @@ import _ from 'lodash';
|
|||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { PageBackgroundColor } from '../../../constants';
|
import { PageBackgroundColor } from '../../../constants';
|
||||||
|
|
||||||
export const MobilePageContentContainer: FC<{ hideTabBar?: boolean }> = ({ children, hideTabBar }) => {
|
export const MobilePageContentContainer: FC<{ hideTabBar?: boolean; displayPageHeader?: boolean }> = ({ children, hideTabBar, displayPageHeader = true }) => {
|
||||||
const [mobileTabBarHeight, setMobileTabBarHeight] = React.useState(0);
|
const [mobileTabBarHeight, setMobileTabBarHeight] = React.useState(0);
|
||||||
const [mobilePageHeader, setMobilePageHeader] = React.useState(0);
|
const [mobilePageHeader, setMobilePageHeader] = React.useState(0);
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
@ -29,7 +29,7 @@ export const MobilePageContentContainer: FC<{ hideTabBar?: boolean }> = ({ child
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{mobilePageHeader ? <div style={{ height: mobilePageHeader }}></div> : null}
|
{(mobilePageHeader && displayPageHeader) ? <div style={{ height: mobilePageHeader }}></div> : null}
|
||||||
<div
|
<div
|
||||||
className="mobile-page-content"
|
className="mobile-page-content"
|
||||||
data-testid="mobile-page-content"
|
data-testid="mobile-page-content"
|
||||||
|
@ -12,13 +12,13 @@ import { cx, NocoBaseRecursionField, SchemaToolbarProvider } from '@nocobase/cli
|
|||||||
import { NavBar } from 'antd-mobile';
|
import { NavBar } from 'antd-mobile';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { useRouteTranslation } from '../../../../locale';
|
||||||
import { useMobileTitle } from '../../../../mobile-providers';
|
import { useMobileTitle } from '../../../../mobile-providers';
|
||||||
import { useMobilePage } from '../../context';
|
import { useMobilePage } from '../../context';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import { useRouteTranslation } from '../../../../locale';
|
|
||||||
|
|
||||||
export const MobilePageNavigationBar: FC = () => {
|
export const MobilePageNavigationBar: FC = () => {
|
||||||
const { title } = useMobileTitle();
|
const { title } = useMobileTitle() || {};
|
||||||
const { displayNavigationBar = true, displayPageTitle = true } = useMobilePage();
|
const { displayNavigationBar = true, displayPageTitle = true } = useMobilePage();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { componentCls, hashId } = useStyles();
|
const { componentCls, hashId } = useStyles();
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
export const mobilePageHeaderStyle: any = {
|
export const mobilePageHeaderStyle: any = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
borderBottom: '1px solid var(--adm-color-border)',
|
borderBottom: '1px solid var(--adm-color-border)',
|
||||||
|
@ -16,27 +16,23 @@
|
|||||||
* For more information, please rwefer to: https://www.nocobase.com/agreement.
|
* For more information, please rwefer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useCallback } from 'react';
|
|
||||||
import { reaction } from '@formily/reactive';
|
import { reaction } from '@formily/reactive';
|
||||||
import { Badge, Button, ConfigProvider, Drawer, Tooltip, notification, theme } from 'antd';
|
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import { Icon } from '@nocobase/client';
|
|
||||||
import { InboxContent } from './InboxContent';
|
|
||||||
import { useLocalTranslation } from '../../locale';
|
|
||||||
import { fetchChannels } from '../observables';
|
|
||||||
import { observer } from '@formily/reactive-react';
|
import { observer } from '@formily/reactive-react';
|
||||||
import { useCurrentUserContext } from '@nocobase/client';
|
import { Icon, useCurrentUserContext, useMobileLayout } from '@nocobase/client';
|
||||||
|
import { MobilePopup } from '@nocobase/plugin-mobile/client';
|
||||||
|
import { Badge, Button, ConfigProvider, Drawer, notification, theme, Tooltip } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useLocalTranslation } from '../../locale';
|
||||||
|
import { Channel } from '../../types';
|
||||||
import {
|
import {
|
||||||
updateUnreadMsgsCount,
|
fetchChannels, inboxVisible, liveSSEObs,
|
||||||
unreadMsgsCountObs,
|
|
||||||
startMsgSSEStreamWithRetry,
|
|
||||||
inboxVisible,
|
|
||||||
userIdObs,
|
|
||||||
liveSSEObs,
|
|
||||||
messageMapObs,
|
messageMapObs,
|
||||||
selectedChannelNameObs,
|
selectedChannelNameObs, startMsgSSEStreamWithRetry, unreadMsgsCountObs, updateUnreadMsgsCount, userIdObs
|
||||||
} from '../observables';
|
} from '../observables';
|
||||||
|
import { InboxContent } from './InboxContent';
|
||||||
|
import { MobileChannelPage } from './mobile/ChannelPage';
|
||||||
|
import { MobileMessagePage } from './mobile/MessagePage';
|
||||||
const useStyles = createStyles(({ token }) => {
|
const useStyles = createStyles(({ token }) => {
|
||||||
return {
|
return {
|
||||||
button: {
|
button: {
|
||||||
@ -46,6 +42,42 @@ const useStyles = createStyles(({ token }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const InboxPopup: FC<{ title: string; visible: boolean; onClose: () => void }> = (props) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null);
|
||||||
|
|
||||||
|
if (isMobileLayout) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MobilePopup title={props.title} visible={props.visible} onClose={props.onClose} minHeight={'60vh'}>
|
||||||
|
<MobileChannelPage displayNavigationBar={false} onClickItem={setSelectedChannel} />
|
||||||
|
</MobilePopup>
|
||||||
|
<MobilePopup title={selectedChannel?.title} visible={props.visible && !!selectedChannel} onClose={() => setSelectedChannel(null)} minHeight={'60vh'}>
|
||||||
|
<MobileMessagePage displayPageHeader={false} />
|
||||||
|
</MobilePopup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={<div style={{ padding: '0', paddingLeft: token.padding }}>{props.title}</div>}
|
||||||
|
open={props.visible}
|
||||||
|
width={900}
|
||||||
|
onClose={props.onClose}
|
||||||
|
styles={{
|
||||||
|
header: {
|
||||||
|
paddingLeft: token.paddingMD,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InboxContent />
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const InnerInbox = (props) => {
|
const InnerInbox = (props) => {
|
||||||
const { t } = useLocalTranslation();
|
const { t } = useLocalTranslation();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
@ -89,7 +121,6 @@ const InnerInbox = (props) => {
|
|||||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const DrawerTitle = <div style={{ padding: '0', paddingLeft: token.padding }}>{t('Message')}</div>;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dispose = reaction(
|
const dispose = reaction(
|
||||||
() => liveSSEObs.value,
|
() => liveSSEObs.value,
|
||||||
@ -138,21 +169,7 @@ const InnerInbox = (props) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Drawer
|
<InboxPopup title={t('Message')} visible={inboxVisible.value} onClose={() => { inboxVisible.value = false; }} />
|
||||||
title={DrawerTitle}
|
|
||||||
open={inboxVisible.value}
|
|
||||||
width={900}
|
|
||||||
onClose={() => {
|
|
||||||
inboxVisible.value = false;
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
header: {
|
|
||||||
paddingLeft: token.paddingMD,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InboxContent />
|
|
||||||
</Drawer>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,18 +7,18 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
|
||||||
import { observer } from '@formily/reactive-react';
|
|
||||||
import { reaction } from '@formily/reactive';
|
|
||||||
import { List, Badge, InfiniteScroll, ListRef } from 'antd-mobile';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { dayjs } from '@nocobase/utils/client';
|
|
||||||
import InfiniteScrollContent from './InfiniteScrollContent';
|
|
||||||
import { channelListObs, channelStatusFilterObs, showChannelLoadingMoreObs, fetchChannels } from '../../observables';
|
|
||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
|
import { reaction } from '@formily/reactive';
|
||||||
|
import { observer } from '@formily/reactive-react';
|
||||||
import { useApp } from '@nocobase/client';
|
import { useApp } from '@nocobase/client';
|
||||||
|
import { dayjs } from '@nocobase/utils/client';
|
||||||
|
import { Badge, InfiniteScroll, List, ListRef } from 'antd-mobile';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { channelListObs, channelStatusFilterObs, fetchChannels, showChannelLoadingMoreObs } from '../../observables';
|
||||||
|
import InfiniteScrollContent from './InfiniteScrollContent';
|
||||||
|
|
||||||
const InternalChannelList = () => {
|
const InternalChannelList = (props: { onClickItem?: (item: any) => void }) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const channels = channelListObs.value;
|
const channels = channelListObs.value;
|
||||||
@ -66,7 +66,11 @@ const InternalChannelList = () => {
|
|||||||
<List.Item
|
<List.Item
|
||||||
key={item.name}
|
key={item.name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/page/in-app-message/messages?channel=${item.name}`);
|
if (props.onClickItem) {
|
||||||
|
props.onClickItem(item);
|
||||||
|
} else {
|
||||||
|
navigate(`/page/in-app-message/messages?channel=${item.name}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
description={
|
description={
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
@ -7,20 +7,20 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useCallback } from 'react';
|
|
||||||
import { Tabs } from 'antd-mobile';
|
|
||||||
import { observer } from '@formily/reactive-react';
|
import { observer } from '@formily/reactive-react';
|
||||||
import { useCurrentUserContext, css } from '@nocobase/client';
|
import { css, useCurrentUserContext } from '@nocobase/client';
|
||||||
import {
|
import {
|
||||||
|
MobilePageContentContainer,
|
||||||
MobilePageHeader,
|
MobilePageHeader,
|
||||||
MobilePageNavigationBar,
|
MobilePageNavigationBar,
|
||||||
MobilePageProvider,
|
MobilePageProvider,
|
||||||
MobilePageContentContainer,
|
|
||||||
} from '@nocobase/plugin-mobile/client';
|
} from '@nocobase/plugin-mobile/client';
|
||||||
import { userIdObs, fetchChannels, ChannelStatus, channelStatusFilterObs } from '../../observables';
|
import { Tabs } from 'antd-mobile';
|
||||||
import { ChannelList } from './ChannelList';
|
import React, { useEffect } from 'react';
|
||||||
import { useLocalTranslation } from '../../../locale';
|
import { useLocalTranslation } from '../../../locale';
|
||||||
const MobileMessageBoxInner = () => {
|
import { ChannelStatus, channelStatusFilterObs, fetchChannels, userIdObs } from '../../observables';
|
||||||
|
import { ChannelList } from './ChannelList';
|
||||||
|
const MobileMessageBoxInner = (props: { displayNavigationBar?: boolean; onClickItem?: (item: any) => void; }) => {
|
||||||
const { t } = useLocalTranslation();
|
const { t } = useLocalTranslation();
|
||||||
const ctx = useCurrentUserContext();
|
const ctx = useCurrentUserContext();
|
||||||
const currUserId = ctx.data?.data?.id;
|
const currUserId = ctx.data?.data?.id;
|
||||||
@ -31,7 +31,7 @@ const MobileMessageBoxInner = () => {
|
|||||||
fetchChannels({});
|
fetchChannels({});
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<MobilePageProvider>
|
<MobilePageProvider displayNavigationBar={props.displayNavigationBar}>
|
||||||
<MobilePageHeader>
|
<MobilePageHeader>
|
||||||
<MobilePageNavigationBar />
|
<MobilePageNavigationBar />
|
||||||
<Tabs
|
<Tabs
|
||||||
@ -57,7 +57,7 @@ const MobileMessageBoxInner = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</MobilePageHeader>
|
</MobilePageHeader>
|
||||||
<MobilePageContentContainer>
|
<MobilePageContentContainer>
|
||||||
<ChannelList />
|
<ChannelList onClickItem={props.onClickItem} />
|
||||||
</MobilePageContentContainer>
|
</MobilePageContentContainer>
|
||||||
</MobilePageProvider>
|
</MobilePageProvider>
|
||||||
);
|
);
|
||||||
|
@ -7,35 +7,34 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useCallback, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { List, Badge, InfiniteScroll, NavBar, DotLoading } from 'antd-mobile';
|
|
||||||
import { observer } from '@formily/reactive-react';
|
import { observer } from '@formily/reactive-react';
|
||||||
import { useCurrentUserContext, css, useApp } from '@nocobase/client';
|
import { css, useApp, useCurrentUserContext } from '@nocobase/client';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import { dayjs } from '@nocobase/utils/client';
|
import { dayjs } from '@nocobase/utils/client';
|
||||||
|
import { Badge, InfiniteScroll, List, NavBar } from 'antd-mobile';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Schema } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
MobilePageHeader,
|
|
||||||
MobilePageProvider,
|
|
||||||
MobilePageContentContainer,
|
MobilePageContentContainer,
|
||||||
useMobileTitle,
|
MobilePageHeader,
|
||||||
|
MobilePageProvider
|
||||||
} from '@nocobase/plugin-mobile/client';
|
} from '@nocobase/plugin-mobile/client';
|
||||||
|
import { useLocalTranslation } from '../../../locale';
|
||||||
import {
|
import {
|
||||||
userIdObs,
|
fetchChannels,
|
||||||
|
fetchMessages,
|
||||||
|
inboxVisible,
|
||||||
selectedChannelNameObs,
|
selectedChannelNameObs,
|
||||||
selectedChannelObs,
|
selectedChannelObs,
|
||||||
selectedMessageListObs,
|
selectedMessageListObs,
|
||||||
fetchChannels,
|
|
||||||
updateMessage,
|
|
||||||
fetchMessages,
|
|
||||||
showMsgLoadingMoreObs,
|
showMsgLoadingMoreObs,
|
||||||
|
updateMessage,
|
||||||
|
userIdObs,
|
||||||
} from '../../observables';
|
} from '../../observables';
|
||||||
import { useLocalTranslation } from '../../../locale';
|
|
||||||
import InfiniteScrollContent from './InfiniteScrollContent';
|
import InfiniteScrollContent from './InfiniteScrollContent';
|
||||||
import { Schema } from '@formily/react';
|
|
||||||
|
|
||||||
const MobileMessagePageInner = () => {
|
const MobileMessagePageInner = (props: { displayPageHeader?: boolean }) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const { t } = useLocalTranslation();
|
const { t } = useLocalTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -58,11 +57,13 @@ const MobileMessagePageInner = () => {
|
|||||||
}, [currUserId]);
|
}, [currUserId]);
|
||||||
const messages = selectedMessageListObs.value;
|
const messages = selectedMessageListObs.value;
|
||||||
const viewMessageDetail = (message) => {
|
const viewMessageDetail = (message) => {
|
||||||
const url = message.options?.mobileUrl;
|
const url = message.options?.mobileUrl || message.options?.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
if (url.startsWith('/m/')) navigate(url.substring(2));
|
if (url.startsWith('/m/')) navigate(url.substring(2));
|
||||||
else if (url.startsWith('/')) navigate(url);
|
else if (url.startsWith('/')) {
|
||||||
else {
|
navigate(url);
|
||||||
|
inboxVisible.value = false;
|
||||||
|
} else {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,13 +102,13 @@ const MobileMessagePageInner = () => {
|
|||||||
const title = Schema.compile(selectedChannelObs.value?.title, { t: app.i18n.t }) || t('Message');
|
const title = Schema.compile(selectedChannelObs.value?.title, { t: app.i18n.t }) || t('Message');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobilePageProvider>
|
<MobilePageProvider displayPageHeader={props.displayPageHeader}>
|
||||||
<MobilePageHeader>
|
<MobilePageHeader>
|
||||||
<NavBar className="nb-message-back-action" onBack={() => navigate('/page/in-app-message')}>
|
<NavBar className="nb-message-back-action" onBack={() => navigate('/page/in-app-message')}>
|
||||||
{title}
|
{title}
|
||||||
</NavBar>
|
</NavBar>
|
||||||
</MobilePageHeader>
|
</MobilePageHeader>
|
||||||
<MobilePageContentContainer>
|
<MobilePageContentContainer displayPageHeader={props.displayPageHeader}>
|
||||||
<div
|
<div
|
||||||
style={{ height: '100%', overflowY: 'auto' }}
|
style={{ height: '100%', overflowY: 'auto' }}
|
||||||
className={css({
|
className={css({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user