@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
247 lines (221 loc) • 7.74 kB
text/typescript
import isEmpty from 'lodash/isEmpty';
import DOMPurify from 'dompurify';
import { Link, toTextList, getLink as getLinkUtil } from '@abtnode/util/lib/notification-preview/highlight';
import { isSameAddr } from '@abtnode/util/lib/notification-preview/func';
import { getBlockletSDK } from '@blocklet/js-sdk';
/**
* 通知对象的活动目标接口
*/
interface ActivityTarget {
type?: string;
id?: string;
}
/**
* 通知对象的活动元数据接口
*/
interface ActivityMeta {
id?: string;
}
/**
* 活动对象接口
*/
interface Activity {
type?: string;
actor?: string;
target?: ActivityTarget;
meta?: ActivityMeta;
}
/**
* 通知对象接口
*/
interface Notification {
activity?: Activity;
actorInfo?: any;
severity?: string;
title?: string;
description?: string;
items?: Notification[];
}
/**
* 合并相邻的通知数据
* 合并条件:
* 1. 数据必须是相邻的
* 2. activity.type 必须相同且不为 null 或 undefined
* 3. 如果存在target对象,activity.target.type和activity.target.id都必须相同
* 4. 如果相邻数据的 activity.type 相同但没有 activity.target 对象,则需要合并
*
* @param {Notification[]} notifications - 需要处理的通知数据
* @returns {Notification[]} - 合并后的通知数组
*/
export const mergeAdjacentNotifications = (notifications: Notification[]): Notification[] => {
if (!notifications || !notifications.length) {
return [];
}
const result: Notification[] = [];
let currentGroup: Notification | null = null;
let groupItems: Notification[] = [];
notifications.forEach((notification: Notification) => {
// 如果没有activity或activity.type为null/undefined或者这是第一条记录
if (!notification.activity || !notification.activity.type || !currentGroup) {
// 处理前一个分组(如果存在)
if (currentGroup) {
// 如果只有一个通知,直接添加原通知
if (groupItems.length === 1) {
result.push(groupItems[0]);
} else {
// 将items添加到第一个通知对象,并将其添加到结果中
currentGroup.items = groupItems;
result.push(currentGroup);
}
}
// 处理当前通知
if (notification.activity && notification.activity.type) {
// 将第一个通知对象作为组的基础,不创建新对象
currentGroup = notification;
groupItems = [notification];
} else {
// 无activity或activity.type的记录直接添加到结果中
result.push(notification);
// 重置当前组
currentGroup = null;
groupItems = [];
}
return;
}
// 获取当前组和当前通知的activity信息
const currentActivity = groupItems[0].activity;
const currentType = currentActivity?.type;
const currentTargetType = currentActivity?.target?.type;
const currentTargetId = currentActivity?.target?.id;
const newType = notification.activity.type;
const newTargetType = notification.activity.target?.type;
const newTargetId = notification.activity.target?.id;
// 判断是否需要合并
const shouldMerge =
// activity.type 必须相同且不为null或undefined
currentType &&
newType &&
currentType === newType &&
// 如果都没有target,可以合并
((!currentActivity?.target && !notification.activity.target) ||
// 如果都有target,则target.type和target.id都必须相同
(currentActivity?.target &&
notification.activity.target &&
currentTargetType === newTargetType &&
currentTargetId === newTargetId));
if (shouldMerge) {
// 合并到当前组
groupItems.push(notification);
} else {
// 不满足合并条件,处理当前组
// 如果只有一个通知,直接添加原通知
if (groupItems.length === 1) {
result.push(groupItems[0]);
} else {
// 将items添加到第一个通知对象,并将其添加到结果中
// 使用Object.assign创建浅拷贝,避免修改原对象
const groupToAdd = Object.assign({}, currentGroup, { items: groupItems });
result.push(groupToAdd);
}
// 开始新的分组
currentGroup = notification;
groupItems = [notification];
}
});
// 处理最后一个分组(如果有)
if (currentGroup) {
// 如果只有一个通知,直接添加原通知
if (groupItems.length === 1) {
result.push(groupItems[0]);
} else {
// 将items添加到第一个通知对象,并将其添加到结果中
// 使用Object.assign创建浅拷贝,避免修改原对象
const groupToAdd = Object.assign({}, currentGroup, { items: groupItems });
result.push(groupToAdd);
}
}
return result;
};
/**
* 判断通知是否包含activity
* @param {Notification} notification - 通知对象
* @returns {boolean} - 是否包含activity
*/
export const isIncludeActivity = (notification: Notification): boolean => {
return !isEmpty(notification.activity) && !!notification.activity?.type && !!notification.activity?.actor;
};
/**
* 是否可以自动已读
*/
export const canAutoRead = (notification: Notification | null | undefined): boolean => {
const { severity } = notification || {};
return !!severity && ['normal', 'success', 'info'].includes(severity);
};
/**
* 获取 activity 的链接
* 链接的来源有两种
* 1. activity.meta.id
* 2. activity.target.id
* @param {Activity} activity - 活动对象
* @returns {Object | null} - 活动的链接
*/
export const getActivityLink = (
activity: Activity | null | undefined
): { metaLink?: string | null; targetLink?: string | null } | null => {
if (!activity) {
return null;
}
const { meta, target } = activity;
return {
metaLink: meta?.id,
targetLink: target?.id,
};
};
const getUserProfileUrl = async (did: string, locale: string) => {
try {
const client = getBlockletSDK();
const profileUrl = await client.user.getProfileUrl({ did, locale });
return profileUrl;
} catch (error) {
console.error(error);
return undefined;
}
};
const getLink = async (item: any, locale: string) => {
const { type, did } = item;
if (isSameAddr(type, 'did')) {
const profileUrl = await getUserProfileUrl(did, locale);
return profileUrl || getLinkUtil(item);
}
return getLinkUtil(item);
};
export const toClickableSpan = async (str: string, locale: string, isHighLight = true) => {
const textList = toTextList(str);
const result = await Promise.all(
textList.map(async (item: any) => {
if (item instanceof Link) {
if (isHighLight) {
const url = await getLink(item, locale);
const { type, chainId, did } = item;
// HACK: 邮件中无法支持 dapp 的展示,缺少 dapp 链接,只能作为加粗展示
if (isSameAddr(type, 'dapp')) {
return `<em class="dapp" data-type="${type}" data-chain-id="${chainId}" data-did="${did}">${item.text}</em>`;
}
if (url) {
return `<a target="_blank" rel="noopener noreferrer" class="link" href="${url}">${item.text}</a>`;
}
// 默认展示为加粗
return `<em class="common" data-type="${type}" data-chain-id="${chainId}" data-did="${did}">${item.text}</em>`;
}
return item.text;
}
return item;
})
).then((results) => results.join(''));
return result;
};
export const sanitize = (innerHtml: string) => {
return DOMPurify.sanitize(innerHtml, {
ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'id', 'style'],
});
};