feat(schedule): 待办提醒推送、行事历仅展示提醒与管理 API
单次提醒 worker 投递、local 时间写入校验、未来 7 天提醒列表与忽略/批量删除;行事历去掉事项重复展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import type {
|
||||
MindSpacePageDeletePreview,
|
||||
MindSpacePageDeleteResult,
|
||||
MindSpaceQuota,
|
||||
MindSpaceScheduleReminder,
|
||||
MindSpaceSaveCategory,
|
||||
MindSpacePublication,
|
||||
MindSpacePublicationStats,
|
||||
@@ -580,6 +581,25 @@ export async function getMindSpace(): Promise<MindSpace> {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function ignoreMindSpaceScheduleReminder(reminderId: string) {
|
||||
const result = await apiFetch<{ data: MindSpaceScheduleReminder }>(
|
||||
`/mindspace/v1/schedule/reminders/${encodeURIComponent(reminderId)}/ignore`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function deleteMindSpaceScheduleReminders(ids: string[]) {
|
||||
const result = await apiFetch<{ data: { deleted: number } }>(
|
||||
'/mindspace/v1/schedule/reminders/bulk-delete',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids }),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function listMindSpaceCleanupItems(): Promise<{
|
||||
items: MindSpaceCleanupItem[];
|
||||
totalBytes: number;
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
createMindSpacePageFromAsset,
|
||||
deleteMindSpaceAsset,
|
||||
deleteMindSpacePage,
|
||||
deleteMindSpaceScheduleReminders,
|
||||
bindMindSpacePageLiveEdit,
|
||||
getMindSpaceAgentJob,
|
||||
getMindSpace,
|
||||
getMindSpacePage,
|
||||
getMindSpacePageDeletePreview,
|
||||
ignoreMindSpaceScheduleReminder,
|
||||
listMindSpaceAgentJobs,
|
||||
listMindSpaceAssets,
|
||||
listMindSpaceCleanupItems,
|
||||
@@ -34,6 +36,7 @@ import type {
|
||||
MindSpacePage,
|
||||
MindSpacePageDeletePreview,
|
||||
MindSpaceQuota,
|
||||
MindSpaceScheduleReminder,
|
||||
MindSpaceSaveCategory,
|
||||
PortalUser,
|
||||
} from '../types';
|
||||
@@ -119,33 +122,40 @@ const JOB_STATUS_LABELS: Record<MindSpaceAgentJob['status'], string> = {
|
||||
timed_out: '超时',
|
||||
};
|
||||
|
||||
const PREVIEW_SCHEDULE_ITEMS = [
|
||||
const PREVIEW_SCHEDULE_REMINDERS = [
|
||||
{
|
||||
id: 'schedule-preview-1',
|
||||
time: '09:30',
|
||||
kind: '待办',
|
||||
title: '跟进客户报价单',
|
||||
meta: 'OA 工作区资料',
|
||||
tone: 'task',
|
||||
},
|
||||
{
|
||||
id: 'schedule-preview-2',
|
||||
time: '14:00',
|
||||
kind: '日程',
|
||||
title: '产品评审会',
|
||||
meta: '提前 30 分钟提醒',
|
||||
tone: 'event',
|
||||
},
|
||||
{
|
||||
id: 'schedule-preview-3',
|
||||
time: '明天',
|
||||
kind: '提醒',
|
||||
id: 'schedule-preview-rem-1',
|
||||
time: '明天 09:00',
|
||||
title: '带项目材料去会议',
|
||||
meta: '提前 30 分钟 · 服务号推送',
|
||||
notifyLabel: '待通知',
|
||||
canIgnore: true,
|
||||
},
|
||||
{
|
||||
id: 'schedule-preview-rem-2',
|
||||
time: '今天 14:00',
|
||||
title: '产品评审会',
|
||||
meta: '服务号推送',
|
||||
tone: 'reminder',
|
||||
notifyLabel: '已通知',
|
||||
canIgnore: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function scheduleReminderNotifyLabel(status: MindSpaceScheduleReminder['status']) {
|
||||
return status === 'sent' ? '已通知' : '待通知';
|
||||
}
|
||||
|
||||
function formatReminderEventWindow(reminder: MindSpaceScheduleReminder) {
|
||||
const tz = reminder.itemTimezone || 'Asia/Shanghai';
|
||||
const start = reminder.itemStartAt ?? null;
|
||||
const end = reminder.itemEndAt ?? null;
|
||||
if (!start) return null;
|
||||
const startLabel = formatScheduleWhen(start, tz);
|
||||
if (!end || end === start) return `事项 ${startLabel}`;
|
||||
const endClock = formatScheduleClock(end, tz);
|
||||
return `事项 ${startLabel} ~ ${endClock}`;
|
||||
}
|
||||
|
||||
function formatScheduleClock(value: number | null | undefined, timezone?: string | null) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
|
||||
try {
|
||||
@@ -175,6 +185,48 @@ function formatDigestSchedule(digest: {
|
||||
return nextRun === '—' ? time : `${time} · 下次 ${nextRun}`;
|
||||
}
|
||||
|
||||
function scheduleLocalDateParts(epochMs: number, timezone?: string | null) {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: timezone || 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
}).formatToParts(new Date(epochMs));
|
||||
const pick = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
||||
return { year: pick('year'), month: pick('month'), day: pick('day') };
|
||||
}
|
||||
|
||||
function isSameScheduleDay(aMs: number, bMs: number, timezone?: string | null) {
|
||||
const a = scheduleLocalDateParts(aMs, timezone);
|
||||
const b = scheduleLocalDateParts(bMs, timezone);
|
||||
return a.year === b.year && a.month === b.month && a.day === b.day;
|
||||
}
|
||||
|
||||
function formatScheduleDayLabel(epochMs: number, timezone?: string | null) {
|
||||
const tz = timezone || 'Asia/Shanghai';
|
||||
const now = Date.now();
|
||||
if (isSameScheduleDay(epochMs, now, tz)) return '今天';
|
||||
if (isSameScheduleDay(epochMs, now + 24 * 60 * 60 * 1000, tz)) return '明天';
|
||||
try {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: tz,
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
}).format(new Date(epochMs));
|
||||
} catch {
|
||||
return formatScheduleDayLabel(epochMs, 'Asia/Shanghai');
|
||||
}
|
||||
}
|
||||
|
||||
function formatScheduleWhen(epochMs: number | null | undefined, timezone?: string | null, allDay = false) {
|
||||
if (typeof epochMs !== 'number' || Number.isNaN(epochMs)) return '—';
|
||||
const day = formatScheduleDayLabel(epochMs, timezone);
|
||||
if (allDay) return day;
|
||||
const clock = formatScheduleClock(epochMs, timezone);
|
||||
return clock === '—' ? day : `${day} ${clock}`;
|
||||
}
|
||||
|
||||
const PREVIEW_PAGE_READERS: Record<string, { kicker: string; body: string[] }> = {
|
||||
'page-1': {
|
||||
kicker: '市场趋势',
|
||||
@@ -534,6 +586,8 @@ export function MindSpaceView({
|
||||
const [allPagesLoading, setAllPagesLoading] = useState(false);
|
||||
const [selectedAllPageIds, setSelectedAllPageIds] = useState<string[]>([]);
|
||||
const [bulkDeletingAllPages, setBulkDeletingAllPages] = useState(false);
|
||||
const [selectedScheduleReminderIds, setSelectedScheduleReminderIds] = useState<string[]>([]);
|
||||
const [scheduleReminderActionLoading, setScheduleReminderActionLoading] = useState(false);
|
||||
const [recentPagesTotal, setRecentPagesTotal] = useState(0);
|
||||
const [exportingPageId, setExportingPageId] = useState<string | null>(null);
|
||||
const [sharePayload, setSharePayload] = useState<SharePayload | null>(null);
|
||||
@@ -1040,6 +1094,27 @@ export function MindSpaceView({
|
||||
void load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewMode) return;
|
||||
const refreshSchedule = () => {
|
||||
if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen) {
|
||||
void refreshSpaceQuietly();
|
||||
}
|
||||
};
|
||||
window.addEventListener('focus', refreshSchedule);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') refreshSchedule();
|
||||
});
|
||||
return () => window.removeEventListener('focus', refreshSchedule);
|
||||
}, [
|
||||
previewMode,
|
||||
selectedCategory,
|
||||
selectedPageId,
|
||||
newPageOpen,
|
||||
agentJobsPanelOpen,
|
||||
allPagesOpen,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPageId) setSelectedPageId(initialPageId);
|
||||
}, [initialPageId]);
|
||||
@@ -1813,19 +1888,74 @@ export function MindSpaceView({
|
||||
|
||||
const showHomeModules =
|
||||
space && !selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen;
|
||||
const scheduleItems = previewMode
|
||||
? PREVIEW_SCHEDULE_ITEMS
|
||||
: (space?.schedule?.todayTodoItems ?? []).map((item) => ({
|
||||
id: item.id,
|
||||
time: item.allDay ? '全天' : formatScheduleClock(item.dueAt ?? item.startAt, item.timezone),
|
||||
kind: item.kind === 'event' ? '日程' : '待办',
|
||||
title: item.title,
|
||||
meta:
|
||||
item.description?.trim() ||
|
||||
(item.sourceChannel === 'wechat' ? '来自服务号' : '来自空间同步'),
|
||||
tone: item.kind === 'event' ? 'event' : 'task',
|
||||
}));
|
||||
const rawUpcomingReminders = previewMode ? [] : space?.schedule?.upcomingReminders ?? [];
|
||||
const scheduleReminders = previewMode
|
||||
? PREVIEW_SCHEDULE_REMINDERS.map((item) => ({ ...item }))
|
||||
: rawUpcomingReminders.map((reminder) => {
|
||||
const metaParts = [
|
||||
formatReminderEventWindow(reminder),
|
||||
reminder.offsetMinutes != null && reminder.offsetMinutes > 0
|
||||
? `提前 ${reminder.offsetMinutes} 分钟`
|
||||
: null,
|
||||
reminder.channel === 'wechat' ? '服务号推送' : '站内提醒',
|
||||
].filter(Boolean);
|
||||
return {
|
||||
id: reminder.id,
|
||||
time: formatScheduleWhen(reminder.remindAt, reminder.itemTimezone),
|
||||
title: reminder.itemTitle || '待办提醒',
|
||||
meta: metaParts.join(' · '),
|
||||
notifyLabel: scheduleReminderNotifyLabel(reminder.status),
|
||||
canIgnore: reminder.status === 'pending' || reminder.status === 'locked',
|
||||
};
|
||||
});
|
||||
const scheduleDigests = previewMode ? [] : space?.schedule?.digestSubscriptions ?? [];
|
||||
const pendingReminderCount = scheduleReminders.filter((item) => item.notifyLabel === '待通知').length;
|
||||
const hasScheduleContent = scheduleReminders.length > 0 || scheduleDigests.length > 0;
|
||||
|
||||
const toggleScheduleReminderSelection = (reminderId: string) => {
|
||||
setSelectedScheduleReminderIds((prev) =>
|
||||
prev.includes(reminderId) ? prev.filter((id) => id !== reminderId) : [...prev, reminderId],
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAllScheduleReminders = () => {
|
||||
if (selectedScheduleReminderIds.length === scheduleReminders.length) {
|
||||
setSelectedScheduleReminderIds([]);
|
||||
} else {
|
||||
setSelectedScheduleReminderIds(scheduleReminders.map((item) => item.id));
|
||||
}
|
||||
};
|
||||
|
||||
const ignoreScheduleReminder = async (reminderId: string) => {
|
||||
if (previewMode) return;
|
||||
setScheduleReminderActionLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await ignoreMindSpaceScheduleReminder(reminderId);
|
||||
setSelectedScheduleReminderIds((prev) => prev.filter((id) => id !== reminderId));
|
||||
await refreshSpaceQuietly();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '忽略提醒失败');
|
||||
} finally {
|
||||
setScheduleReminderActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSelectedScheduleReminders = async () => {
|
||||
if (previewMode || selectedScheduleReminderIds.length === 0) return;
|
||||
setScheduleReminderActionLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await deleteMindSpaceScheduleReminders(selectedScheduleReminderIds);
|
||||
setSelectedScheduleReminderIds([]);
|
||||
await refreshSpaceQuietly();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '删除提醒失败');
|
||||
} finally {
|
||||
setScheduleReminderActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedPreviewPage = selectedPageId
|
||||
? pages.find((page) => page.id === selectedPageId) ?? null
|
||||
: null;
|
||||
@@ -2082,24 +2212,83 @@ export function MindSpaceView({
|
||||
</button>
|
||||
</div>
|
||||
<div className="mindspace-schedule-summary">
|
||||
<strong>今天 {scheduleItems.length || 0} 项</strong>
|
||||
<span>
|
||||
{scheduleItems.length > 0
|
||||
? '已有待办记录'
|
||||
: scheduleDigests.length > 0
|
||||
? `${scheduleDigests.length} 个服务号待办推送`
|
||||
: '还没有安排'}
|
||||
</span>
|
||||
<strong>
|
||||
未来 7 天 {scheduleReminders.length} 条提醒 · {pendingReminderCount} 待通知
|
||||
</strong>
|
||||
<span>{hasScheduleContent ? '仅展示提醒通知' : '还没有安排'}</span>
|
||||
</div>
|
||||
{scheduleReminders.length > 0 && (
|
||||
<div className="mindspace-schedule-toolbar">
|
||||
<label className="mindspace-schedule-select-all">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
scheduleReminders.length > 0 &&
|
||||
selectedScheduleReminderIds.length === scheduleReminders.length
|
||||
}
|
||||
onChange={() => toggleAllScheduleReminders()}
|
||||
disabled={scheduleReminderActionLoading || previewMode}
|
||||
/>
|
||||
<span>全选</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-schedule-toolbar-btn danger"
|
||||
disabled={
|
||||
previewMode ||
|
||||
scheduleReminderActionLoading ||
|
||||
selectedScheduleReminderIds.length === 0
|
||||
}
|
||||
onClick={() => void deleteSelectedScheduleReminders()}
|
||||
>
|
||||
{scheduleReminderActionLoading ? '处理中…' : '删除提醒'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{scheduleReminders.length > 0 && (
|
||||
<div className="mindspace-schedule-list">
|
||||
{scheduleReminders.map((item) => (
|
||||
<article className="mindspace-schedule-item has-checkbox" key={item.id}>
|
||||
<label className="mindspace-schedule-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedScheduleReminderIds.includes(item.id)}
|
||||
onChange={() => toggleScheduleReminderSelection(item.id)}
|
||||
disabled={scheduleReminderActionLoading || previewMode}
|
||||
/>
|
||||
</label>
|
||||
<time>{item.time}</time>
|
||||
<span
|
||||
className={`mindspace-schedule-notify is-${item.notifyLabel === '已通知' ? 'sent' : 'pending'}`}
|
||||
>
|
||||
{item.notifyLabel}
|
||||
</span>
|
||||
<div className="mindspace-schedule-item-body">
|
||||
<strong>{item.title}</strong>
|
||||
<small>{item.meta}</small>
|
||||
</div>
|
||||
{item.canIgnore && !previewMode && (
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-schedule-ignore"
|
||||
disabled={scheduleReminderActionLoading}
|
||||
onClick={() => void ignoreScheduleReminder(item.id)}
|
||||
>
|
||||
忽略
|
||||
</button>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{scheduleDigests.length > 0 && (
|
||||
<div className="mindspace-schedule-list">
|
||||
<p className="mindspace-schedule-subhead">订阅</p>
|
||||
{scheduleDigests.map((digest) => (
|
||||
<article className="mindspace-schedule-item" key={digest.id}>
|
||||
<article className="mindspace-schedule-item mindspace-schedule-item-digest" key={digest.id}>
|
||||
<time>{String(digest.hour).padStart(2, '0')}:{String(digest.minute).padStart(2, '0')}</time>
|
||||
<span className={`mindspace-schedule-kind is-${digest.status === 'active' ? 'event' : 'reminder'}`}>
|
||||
订阅
|
||||
</span>
|
||||
<div>
|
||||
<span className="mindspace-schedule-notify is-pending">订阅</span>
|
||||
<div className="mindspace-schedule-item-body">
|
||||
<strong>每天待办摘要</strong>
|
||||
<small>{formatDigestSchedule(digest)}</small>
|
||||
</div>
|
||||
@@ -2107,22 +2296,9 @@ export function MindSpaceView({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{scheduleItems.length > 0 ? (
|
||||
<div className="mindspace-schedule-list">
|
||||
{scheduleItems.map((item) => (
|
||||
<article className="mindspace-schedule-item" key={item.id}>
|
||||
<time>{item.time}</time>
|
||||
<span className={`mindspace-schedule-kind is-${item.tone}`}>{item.kind}</span>
|
||||
<div>
|
||||
<strong>{item.title}</strong>
|
||||
<small>{item.meta}</small>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
{!hasScheduleContent && (
|
||||
<p className="mindspace-schedule-empty">
|
||||
今天还没有待办,可以在聊天里说“明天上午提醒我带材料”。
|
||||
未来 7 天还没有提醒,可以在聊天里说「明天早上 6 点跑步,5 点 55 提醒我」。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -5416,6 +5416,15 @@ body,
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mindspace-schedule-subhead {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #7a857f;
|
||||
}
|
||||
|
||||
.mindspace-schedule-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
@@ -5433,6 +5442,94 @@ body,
|
||||
background: rgba(24, 33, 29, 0.045);
|
||||
}
|
||||
|
||||
.mindspace-schedule-item.has-checkbox {
|
||||
grid-template-columns: 22px 72px 52px minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.mindspace-schedule-item.mindspace-schedule-item-digest {
|
||||
grid-template-columns: 72px 52px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mindspace-schedule-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mindspace-schedule-select-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #68716c;
|
||||
}
|
||||
|
||||
.mindspace-schedule-toolbar-btn {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
background: rgba(24, 33, 29, 0.08);
|
||||
color: #18211d;
|
||||
}
|
||||
|
||||
.mindspace-schedule-toolbar-btn.danger {
|
||||
background: rgba(196, 74, 58, 0.12);
|
||||
color: #a33f32;
|
||||
}
|
||||
|
||||
.mindspace-schedule-toolbar-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mindspace-schedule-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mindspace-schedule-item-body strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mindspace-schedule-notify {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
padding: 3px 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mindspace-schedule-notify.is-pending {
|
||||
color: #8a5b16;
|
||||
background: rgba(238, 176, 78, 0.22);
|
||||
}
|
||||
|
||||
.mindspace-schedule-notify.is-sent {
|
||||
color: #2f6f57;
|
||||
background: rgba(47, 111, 87, 0.16);
|
||||
}
|
||||
|
||||
.mindspace-schedule-ignore {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: #68716c;
|
||||
background: rgba(24, 33, 29, 0.08);
|
||||
}
|
||||
|
||||
.mindspace-schedule-ignore:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mindspace-schedule-item time {
|
||||
color: #2f6f57;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -359,8 +359,25 @@ export type MindSpaceDigestSubscription = {
|
||||
attempts?: number;
|
||||
};
|
||||
|
||||
export type MindSpaceScheduleReminder = {
|
||||
id: string;
|
||||
itemId: string;
|
||||
itemTitle: string;
|
||||
itemKind: 'task' | 'event';
|
||||
itemTimezone: string;
|
||||
itemStartAt?: number | null;
|
||||
itemEndAt?: number | null;
|
||||
remindAt: number;
|
||||
offsetMinutes?: number | null;
|
||||
channel: 'wechat' | 'in_app';
|
||||
status: 'pending' | 'locked' | 'sent' | 'failed' | 'cancelled';
|
||||
sentAt?: number | null;
|
||||
};
|
||||
|
||||
export type MindSpaceSchedule = {
|
||||
todayTodoItems: MindSpaceScheduleItem[];
|
||||
upcomingItems: MindSpaceScheduleItem[];
|
||||
upcomingReminders: MindSpaceScheduleReminder[];
|
||||
digestSubscriptions: MindSpaceDigestSubscription[];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user