mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat(notification): in-app message support
This commit is contained in:
parent
e3caaee61c
commit
cd8141ca15
@ -7,10 +7,10 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent, css } from '@nocobase/client';
|
||||
import { useLocalTranslation } from '../../locale';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import React from 'react';
|
||||
import { useLocalTranslation } from '../../locale';
|
||||
|
||||
export const ContentConfigForm = ({ variableOptions }) => {
|
||||
const { t } = useLocalTranslation();
|
||||
@ -31,6 +31,17 @@ export const ContentConfigForm = ({ variableOptions }) => {
|
||||
useTypedConstant: ['string'],
|
||||
},
|
||||
},
|
||||
contentType: {
|
||||
type: 'string',
|
||||
title: `{{t("Content type")}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: [
|
||||
{ label: 'HTML', value: 'html' },
|
||||
{ label: `{{t("Plain text")}}`, value: 'text' },
|
||||
],
|
||||
default: 'html',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
|
@ -16,27 +16,26 @@
|
||||
* For more information, please rwefer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { reaction } from '@formily/reactive';
|
||||
import { Badge, Button, ConfigProvider, Drawer, Tooltip, notification } 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 { reaction } from '@formily/reactive';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import { useCurrentUserContext } from '@nocobase/client';
|
||||
import { Icon, useCurrentUserContext } from '@nocobase/client';
|
||||
import { Badge, Button, ConfigProvider, Drawer, Tooltip, notification } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useLocalTranslation } from '../../locale';
|
||||
import {
|
||||
updateUnreadMsgsCount,
|
||||
unreadMsgsCountObs,
|
||||
startMsgSSEStreamWithRetry,
|
||||
fetchChannels,
|
||||
inboxVisible,
|
||||
userIdObs,
|
||||
liveSSEObs,
|
||||
messageMapObs,
|
||||
selectedChannelNameObs,
|
||||
startMsgSSEStreamWithRetry,
|
||||
unreadMsgsCountObs,
|
||||
updateUnreadMsgsCount,
|
||||
userIdObs,
|
||||
} from '../observables';
|
||||
import { InboxContent } from './InboxContent';
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
button: {
|
||||
@ -94,6 +93,12 @@ const InnerInbox = (props) => {
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
);
|
||||
const renderContent = useCallback((content: string, contentType: string) => {
|
||||
if (contentType === 'HTML') {
|
||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
return content.slice(0, 100) + (content.length > 100 ? '...' : '');
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const dispose = reaction(
|
||||
() => liveSSEObs.value,
|
||||
@ -116,7 +121,7 @@ const InnerInbox = (props) => {
|
||||
{data.title}
|
||||
</div>
|
||||
),
|
||||
description: data.content.slice(0, 100) + (data.content.length > 100 ? '...' : ''),
|
||||
description: renderContent(data.content, data.contentType),
|
||||
onClick: () => {
|
||||
inboxVisible.value = true;
|
||||
selectedChannelNameObs.value = data.channelName;
|
||||
|
@ -7,12 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent, css } from '@nocobase/client';
|
||||
import { useLocalTranslation, NAMESPACE } from '../../locale';
|
||||
import { UsersSelect } from './UsersSelect';
|
||||
import { UsersAddition } from './UsersAddition';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import React from 'react';
|
||||
import { NAMESPACE, useLocalTranslation } from '../../locale';
|
||||
import { UsersAddition } from './UsersAddition';
|
||||
import { UsersSelect } from './UsersSelect';
|
||||
|
||||
export const MessageConfigForm = ({ variableOptions }) => {
|
||||
const { t } = useLocalTranslation();
|
||||
@ -84,6 +84,17 @@ export const MessageConfigForm = ({ variableOptions }) => {
|
||||
useTypedConstant: ['string'],
|
||||
},
|
||||
},
|
||||
contentType: {
|
||||
type: 'string',
|
||||
title: `{{t("Content type")}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: [
|
||||
{ label: `{{t("Plain text")}}`, value: 'text' },
|
||||
{ label: 'HTML', value: 'HTML' },
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
|
@ -90,7 +90,12 @@ const MessageList = observer(() => {
|
||||
}, [messages, selectedChannelName]);
|
||||
|
||||
const title = Schema.compile(channelMapObs.value[selectedChannelName].title, { t: app.i18n.t });
|
||||
|
||||
const renderContent = (content: string, contentType: string) => {
|
||||
if (contentType === 'HTML') {
|
||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
@ -156,10 +161,7 @@ const MessageList = observer(() => {
|
||||
>
|
||||
<Descriptions key={index} column={1}>
|
||||
<Descriptions.Item label={t('Content')}>
|
||||
{' '}
|
||||
<Tooltip title={message.content?.length > 100 ? message.content : ''} mouseEnterDelay={0.5}>
|
||||
{message.content?.slice(0, 100) + (message.content?.length > 100 ? '...' : '')}{' '}
|
||||
</Tooltip>
|
||||
{renderContent(message.content, message.contentType)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('Datetime')}>{dayjs(message.receiveTimestamp).fromNow()}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('Status')}>
|
||||
|
@ -7,33 +7,32 @@
|
||||
* 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 { useCurrentUserContext, css, useApp } from '@nocobase/client';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { css, useApp, useCurrentUserContext } from '@nocobase/client';
|
||||
import { dayjs } from '@nocobase/utils/client';
|
||||
import { Badge, DotLoading, 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 {
|
||||
MobilePageContentContainer,
|
||||
MobilePageHeader,
|
||||
MobilePageProvider,
|
||||
MobilePageContentContainer,
|
||||
useMobileTitle,
|
||||
} from '@nocobase/plugin-mobile/client';
|
||||
import { useLocalTranslation } from '../../../locale';
|
||||
import {
|
||||
userIdObs,
|
||||
fetchChannels,
|
||||
fetchMessages,
|
||||
selectedChannelNameObs,
|
||||
selectedChannelObs,
|
||||
selectedMessageListObs,
|
||||
fetchChannels,
|
||||
updateMessage,
|
||||
fetchMessages,
|
||||
showMsgLoadingMoreObs,
|
||||
updateMessage,
|
||||
userIdObs,
|
||||
} from '../../observables';
|
||||
import { useLocalTranslation } from '../../../locale';
|
||||
import InfiniteScrollContent from './InfiniteScrollContent';
|
||||
import { Schema } from '@formily/react';
|
||||
|
||||
const MobileMessagePageInner = () => {
|
||||
const app = useApp();
|
||||
@ -100,6 +99,12 @@ const MobileMessagePageInner = () => {
|
||||
}, [messages]);
|
||||
const title = Schema.compile(selectedChannelObs.value?.title, { t: app.i18n.t }) || t('Message');
|
||||
|
||||
const renderContent = useCallback((content: string, contentType: string) => {
|
||||
if (contentType === 'HTML') {
|
||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
return content;
|
||||
}, []);
|
||||
return (
|
||||
<MobilePageProvider>
|
||||
<MobilePageHeader>
|
||||
@ -131,7 +136,7 @@ const MobileMessagePageInner = () => {
|
||||
<Badge key={item.id} content={item.status === 'unread' ? Badge.dot : null} />
|
||||
</div>
|
||||
}
|
||||
description={item.content}
|
||||
description={renderContent(item.content, item.contentType)}
|
||||
extra={dayjs(item.receiveTimestamp).fromNow(true)}
|
||||
onClick={() => {
|
||||
onMessageClick(item);
|
||||
|
@ -11,13 +11,7 @@ import { autorun, observable } from '@formily/reactive';
|
||||
import { merge } from '@nocobase/utils/client';
|
||||
import { InAppMessagesDefinition, Message } from '../../types';
|
||||
import { getAPIClient } from '../utils';
|
||||
import {
|
||||
channelMapObs,
|
||||
channelStatusFilterObs,
|
||||
fetchChannels,
|
||||
InappChannelStatusEnum,
|
||||
selectedChannelNameObs,
|
||||
} from './channel';
|
||||
import { channelMapObs, channelStatusFilterObs, selectedChannelNameObs } from './channel';
|
||||
import { userIdObs } from './user';
|
||||
|
||||
export const messageMapObs = observable<{ value: Record<string, Message> }>({ value: {} });
|
||||
|
@ -7,14 +7,13 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { BaseNotificationChannel, SendFnType } from '@nocobase/plugin-notification-manager';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { SendFnType, BaseNotificationChannel } from '@nocobase/plugin-notification-manager';
|
||||
import { InAppMessageFormValues } from '../types';
|
||||
import { PassThrough } from 'stream';
|
||||
import { InAppMessagesDefinition as MessagesDefinition } from '../types';
|
||||
import { parseUserSelectionConf } from './parseUserSelectionConf';
|
||||
import defineMyInAppMessages from './defineMyInAppMessages';
|
||||
import { InAppMessageFormValues, InAppMessagesDefinition as MessagesDefinition } from '../types';
|
||||
import defineMyInAppChannels from './defineMyInAppChannels';
|
||||
import defineMyInAppMessages from './defineMyInAppMessages';
|
||||
import { parseUserSelectionConf } from './parseUserSelectionConf';
|
||||
|
||||
type UserID = string;
|
||||
type ClientID = string;
|
||||
@ -76,6 +75,7 @@ export default class InAppNotificationChannel extends BaseNotificationChannel {
|
||||
|
||||
saveMessageToDB = async ({
|
||||
content,
|
||||
contentType,
|
||||
status,
|
||||
userId,
|
||||
title,
|
||||
@ -85,6 +85,7 @@ export default class InAppNotificationChannel extends BaseNotificationChannel {
|
||||
}: {
|
||||
content: string;
|
||||
userId: number;
|
||||
contentType: 'text' | 'HTML';
|
||||
title: string;
|
||||
channelName: string;
|
||||
status: 'read' | 'unread';
|
||||
@ -97,6 +98,7 @@ export default class InAppNotificationChannel extends BaseNotificationChannel {
|
||||
content,
|
||||
title,
|
||||
channelName,
|
||||
contentType,
|
||||
status,
|
||||
userId,
|
||||
receiveTimestamp: receiveTimestamp ?? Date.now(),
|
||||
@ -109,7 +111,7 @@ export default class InAppNotificationChannel extends BaseNotificationChannel {
|
||||
send: SendFnType<InAppMessageFormValues> = async (params) => {
|
||||
const { channel, message, receivers } = params;
|
||||
let userIds: number[];
|
||||
const { content, title, options = {} } = message;
|
||||
const { content, contentType, title, options = {} } = message;
|
||||
const userRepo = this.app.db.getRepository('users');
|
||||
if (receivers?.type === 'userId') {
|
||||
userIds = receivers.value;
|
||||
@ -123,6 +125,7 @@ export default class InAppNotificationChannel extends BaseNotificationChannel {
|
||||
content,
|
||||
status: 'unread',
|
||||
userId,
|
||||
contentType,
|
||||
channelName: channel.name,
|
||||
options,
|
||||
});
|
||||
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Migration } from '@nocobase/server';
|
||||
import { InAppMessagesDefinition } from '../../types';
|
||||
export default class extends Migration {
|
||||
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
|
||||
appVersion = '<1.6.24';
|
||||
|
||||
async up() {
|
||||
const { db } = this.context;
|
||||
|
||||
const collection = db.getCollection(InAppMessagesDefinition.name);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
const Field = db.getRepository('fields');
|
||||
const existed = await Field.count({
|
||||
filter: {
|
||||
name: 'contentType',
|
||||
collectionName: InAppMessagesDefinition.name,
|
||||
},
|
||||
});
|
||||
if (existed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.getRepository('fields').create({
|
||||
values: {
|
||||
collectionName: InAppMessagesDefinition.name,
|
||||
name: 'contentType',
|
||||
type: 'string',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Content type")}}',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
options: [
|
||||
{ label: '{{t("Text")}}', value: 'text' },
|
||||
{ label: '{{t("HTML")}}', value: 'HTML' },
|
||||
],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
enum: ['HTML', 'text'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Update existing records to have contentType = 'text'
|
||||
// Update existing records to have 'map' as the default value
|
||||
await db.getRepository(InAppMessagesDefinition.name).update({
|
||||
filter: {
|
||||
contentType: null,
|
||||
},
|
||||
values: {
|
||||
contentType: 'text',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ export interface Message {
|
||||
title: string;
|
||||
userId: string;
|
||||
channelName: string;
|
||||
contentType: 'text' | 'HTML';
|
||||
content: string;
|
||||
receiveTimestamp: number;
|
||||
status: 'read' | 'unread';
|
||||
@ -35,6 +36,7 @@ export type SSEData = {
|
||||
export interface InAppMessageFormValues {
|
||||
receivers: string[];
|
||||
content: string;
|
||||
contentType: 'text' | 'HTML';
|
||||
senderName: string;
|
||||
senderId: string;
|
||||
url: string;
|
||||
@ -49,6 +51,7 @@ export const InAppMessagesDefinition = {
|
||||
channelName: 'channelName',
|
||||
userId: 'userId',
|
||||
content: 'content',
|
||||
contentType: 'contentType',
|
||||
status: 'status',
|
||||
title: 'title',
|
||||
receiveTimestamp: 'receiveTimestamp',
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { CollectionOptions } from '@nocobase/client';
|
||||
import { InAppMessagesDefinition, ChannelsDefinition } from './index';
|
||||
import { ChannelsDefinition, InAppMessagesDefinition } from './index';
|
||||
|
||||
export const messageCollection: CollectionOptions = {
|
||||
name: InAppMessagesDefinition.name,
|
||||
@ -61,6 +61,27 @@ export const messageCollection: CollectionOptions = {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: InAppMessagesDefinition.fieldNameMap.contentType,
|
||||
type: 'string',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Content type")}}',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Content type")}}',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
options: [
|
||||
{ label: '{{t("Text")}}', value: 'text' },
|
||||
{ label: '{{t("HTML")}}', value: 'HTML' },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: InAppMessagesDefinition.fieldNameMap.content,
|
||||
type: 'text',
|
||||
|
Loading…
x
Reference in New Issue
Block a user