-
+
);
}
- return
;
+ return props.emptyFallback === undefined ? (
+
+ ) : (
+ props.emptyFallback
+ );
};
const NoRecordValue = () => {
@@ -46,6 +59,7 @@ const LocalUserValue = () => {
};
export const CreatedByValue = () => {
+ const doc = useService(DocService).doc.record;
const workspaceService = useService(WorkspaceService);
const isCloud = workspaceService.workspace.flavour !== 'local';
@@ -59,12 +73,13 @@ export const CreatedByValue = () => {
return (
-
+
);
};
export const UpdatedByValue = () => {
+ const doc = useService(DocService).doc.record;
const workspaceService = useService(WorkspaceService);
const isCloud = workspaceService.workspace.flavour !== 'local';
@@ -78,7 +93,7 @@ export const UpdatedByValue = () => {
return (
-
+
);
};
@@ -119,3 +134,45 @@ export const CreatedByUpdatedByFilterValue = ({
/>
);
};
+
+export const CreatedByDocListInlineProperty = ({
+ doc,
+}: DocListPropertyProps) => {
+ return (
+
+ );
+};
+
+export const UpdatedByDocListInlineProperty = ({
+ doc,
+}: DocListPropertyProps) => {
+ return (
+
+ );
+};
+
+export const ModifiedByGroupHeader = ({
+ groupId,
+ docCount,
+}: GroupHeaderProps) => {
+ const userId = groupId;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/date.css.ts b/packages/frontend/core/src/components/workspace-property-types/date.css.ts
index 8d1ec3d37e..748939ec84 100644
--- a/packages/frontend/core/src/components/workspace-property-types/date.css.ts
+++ b/packages/frontend/core/src/components/workspace-property-types/date.css.ts
@@ -1,6 +1,28 @@
import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const empty = style({
color: cssVar('placeholderColor'),
});
+
+export const dateDocListInlineProperty = style({
+ width: 60,
+ textAlign: 'center',
+ fontSize: 12,
+ lineHeight: '20px',
+ color: cssVarV2.text.secondary,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ flexShrink: 0,
+});
+
+export const tooltip = style({
+ display: 'inline-block',
+ selectors: {
+ '&::first-letter': {
+ textTransform: 'uppercase',
+ },
+ },
+});
diff --git a/packages/frontend/core/src/components/workspace-property-types/date.tsx b/packages/frontend/core/src/components/workspace-property-types/date.tsx
index 66e2d9655b..ec45a299bb 100644
--- a/packages/frontend/core/src/components/workspace-property-types/date.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/date.tsx
@@ -2,10 +2,14 @@ import { DatePicker, Menu, PropertyValue, Tooltip } from '@affine/component';
import type { FilterParams } from '@affine/core/modules/collection-rules';
import { DocService } from '@affine/core/modules/doc';
import { i18nTime, useI18n } from '@affine/i18n';
+import { DateTimeIcon } from '@blocksuite/icons/rc';
import { useLiveData, useServices } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useCallback } from 'react';
+import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import * as styles from './date.css';
@@ -184,3 +188,71 @@ export const DateFilterValue = ({
>
) : undefined;
};
+
+export const DateDocListProperty = ({ value }: DocListPropertyProps) => {
+ if (!value) return null;
+
+ return (
+
}>
+ {i18nTime(value, { absolute: { accuracy: 'day' } })}
+
+ );
+};
+
+export const CreateDateDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const t = useI18n();
+ const docMeta = useLiveData(doc.meta$);
+ const createDate = docMeta?.createDate;
+
+ if (!createDate) return null;
+
+ return (
+
+ {t.t('created at', { time: i18nTime(createDate) })}
+
+ }
+ >
+
+ {i18nTime(createDate, { relative: true })}
+
+
+ );
+};
+
+export const UpdatedDateDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const t = useI18n();
+ const docMeta = useLiveData(doc.meta$);
+ const updatedDate = docMeta?.updatedDate;
+
+ if (!updatedDate) return null;
+
+ return (
+
+ {t.t('updated at', { time: i18nTime(updatedDate) })}
+
+ }
+ >
+
+ {i18nTime(updatedDate, { relative: true })}
+
+
+ );
+};
+
+export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
+ const date = groupId || 'No Date';
+
+ return (
+
+ {date}
+
+ );
+};
+
+export const CreatedGroupHeader = (props: GroupHeaderProps) => {
+ return
;
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx b/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx
index 4d782d53e7..6bba38df11 100644
--- a/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx
@@ -9,9 +9,13 @@ import type { FilterParams } from '@affine/core/modules/collection-rules';
import { DocService } from '@affine/core/modules/doc';
import { useI18n } from '@affine/i18n';
import type { DocMode } from '@blocksuite/affine/model';
+import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
+import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
import * as styles from './doc-primary-mode.css';
@@ -114,3 +118,37 @@ export const DocPrimaryModeFilterValue = ({
);
};
+
+export const DocPrimaryModeDocListProperty = ({
+ doc,
+}: DocListPropertyProps) => {
+ const t = useI18n();
+ const primaryMode = useLiveData(doc.primaryMode$);
+
+ return (
+
:
}
+ >
+ {primaryMode === 'edgeless' ? t['Edgeless']() : t['Page']()}
+
+ );
+};
+
+export const DocPrimaryModeGroupHeader = ({
+ groupId,
+ docCount,
+}: GroupHeaderProps) => {
+ const t = useI18n();
+ const text =
+ groupId === 'edgeless'
+ ? t['com.affine.edgelessMode']()
+ : groupId === 'page'
+ ? t['com.affine.pageMode']()
+ : 'Default';
+
+ return (
+
+ {text}
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/edgeless-theme.tsx b/packages/frontend/core/src/components/workspace-property-types/edgeless-theme.tsx
index 1fdfb14ef5..21e5e635e9 100644
--- a/packages/frontend/core/src/components/workspace-property-types/edgeless-theme.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/edgeless-theme.tsx
@@ -1,9 +1,12 @@
import { PropertyValue, type RadioItem } from '@affine/component';
import { DocService } from '@affine/core/modules/doc';
import { useI18n } from '@affine/i18n';
+import { EdgelessIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
import * as styles from './edgeless-theme.css';
@@ -56,3 +59,20 @@ export const EdgelessThemeValue = ({
);
};
+
+export const EdgelessThemeDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const t = useI18n();
+ const edgelessTheme = useLiveData(
+ doc.properties$.selector(p => p.edgelessColorTheme)
+ );
+
+ return (
+
}>
+ {edgelessTheme === 'system' || !edgelessTheme
+ ? t['com.affine.themeSettings.auto']()
+ : edgelessTheme === 'light'
+ ? t['com.affine.themeSettings.light']()
+ : t['com.affine.themeSettings.dark']()}
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/index.ts b/packages/frontend/core/src/components/workspace-property-types/index.ts
index 9433af1627..ce634c30bf 100644
--- a/packages/frontend/core/src/components/workspace-property-types/index.ts
+++ b/packages/frontend/core/src/components/workspace-property-types/index.ts
@@ -20,30 +20,64 @@ import {
TodayIcon,
} from '@blocksuite/icons/rc';
+import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
-import { CheckboxFilterValue, CheckboxValue } from './checkbox';
import {
+ CheckboxDocListProperty,
+ CheckboxFilterValue,
+ CheckboxGroupHeader,
+ CheckboxValue,
+} from './checkbox';
+import {
+ CreatedByDocListInlineProperty,
CreatedByUpdatedByFilterValue,
CreatedByValue,
+ ModifiedByGroupHeader,
+ UpdatedByDocListInlineProperty,
UpdatedByValue,
} from './created-updated-by';
import {
+ CreateDateDocListProperty,
CreateDateValue,
+ CreatedGroupHeader,
+ DateDocListProperty,
DateFilterValue,
+ DateGroupHeader,
DateValue,
+ UpdatedDateDocListProperty,
UpdatedDateValue,
} from './date';
import {
+ DocPrimaryModeDocListProperty,
DocPrimaryModeFilterValue,
+ DocPrimaryModeGroupHeader,
DocPrimaryModeValue,
} from './doc-primary-mode';
-import { EdgelessThemeValue } from './edgeless-theme';
-import { JournalFilterValue, JournalValue } from './journal';
-import { NumberValue } from './number';
-import { PageWidthValue } from './page-width';
-import { TagsFilterValue, TagsValue } from './tags';
-import { TemplateValue } from './template';
-import { TextFilterValue, TextValue } from './text';
+import {
+ EdgelessThemeDocListProperty,
+ EdgelessThemeValue,
+} from './edgeless-theme';
+import {
+ JournalDocListProperty,
+ JournalFilterValue,
+ JournalGroupHeader,
+ JournalValue,
+} from './journal';
+import { NumberDocListProperty, NumberValue } from './number';
+import { PageWidthDocListProperty, PageWidthValue } from './page-width';
+import {
+ TagsDocListProperty,
+ TagsFilterValue,
+ TagsGroupHeader,
+ TagsValue,
+} from './tags';
+import { TemplateDocListProperty, TemplateValue } from './template';
+import {
+ TextDocListProperty,
+ TextFilterValue,
+ TextGroupHeader,
+ TextValue,
+} from './text';
const DateFilterMethod = {
after: 'com.affine.filter.after',
@@ -76,6 +110,9 @@ export const WorkspacePropertyTypes = {
allowInOrderBy: true,
defaultFilter: { method: 'is-not-empty' },
filterValue: TagsFilterValue,
+ showInDocList: 'inline',
+ docListProperty: TagsDocListProperty,
+ groupHeader: TagsGroupHeader,
},
text: {
icon: TextIcon,
@@ -92,12 +129,17 @@ export const WorkspacePropertyTypes = {
allowInOrderBy: true,
filterValue: TextFilterValue,
defaultFilter: { method: 'is-not-empty' },
+ showInDocList: 'stack',
+ docListProperty: TextDocListProperty,
+ groupHeader: TextGroupHeader,
},
number: {
icon: NumberIcon,
value: NumberValue,
name: 'com.affine.page-properties.property.number',
description: 'com.affine.page-properties.property.number.tooltips',
+ showInDocList: 'stack',
+ docListProperty: NumberDocListProperty,
},
checkbox: {
icon: CheckBoxCheckLinearIcon,
@@ -112,6 +154,9 @@ export const WorkspacePropertyTypes = {
allowInOrderBy: true,
filterValue: CheckboxFilterValue,
defaultFilter: { method: 'is', value: 'true' },
+ showInDocList: 'stack',
+ docListProperty: CheckboxDocListProperty,
+ groupHeader: CheckboxGroupHeader,
},
date: {
icon: DateTimeIcon,
@@ -127,6 +172,9 @@ export const WorkspacePropertyTypes = {
allowInOrderBy: true,
filterValue: DateFilterValue,
defaultFilter: { method: 'is-not-empty' },
+ showInDocList: 'stack',
+ docListProperty: DateDocListProperty,
+ groupHeader: DateGroupHeader,
},
createdBy: {
icon: MemberIcon,
@@ -140,6 +188,9 @@ export const WorkspacePropertyTypes = {
},
filterValue: CreatedByUpdatedByFilterValue,
defaultFilter: { method: 'include', value: '' },
+ showInDocList: 'inline',
+ docListProperty: CreatedByDocListInlineProperty,
+ groupHeader: ModifiedByGroupHeader,
},
updatedBy: {
icon: MemberIcon,
@@ -153,6 +204,9 @@ export const WorkspacePropertyTypes = {
},
filterValue: CreatedByUpdatedByFilterValue,
defaultFilter: { method: 'include', value: '' },
+ showInDocList: 'inline',
+ docListProperty: UpdatedByDocListInlineProperty,
+ groupHeader: ModifiedByGroupHeader,
},
updatedAt: {
icon: DateTimeIcon,
@@ -167,6 +221,8 @@ export const WorkspacePropertyTypes = {
},
filterValue: DateFilterValue,
defaultFilter: { method: 'this-week' },
+ showInDocList: 'inline',
+ docListProperty: UpdatedDateDocListProperty,
},
createdAt: {
icon: HistoryIcon,
@@ -181,6 +237,9 @@ export const WorkspacePropertyTypes = {
},
filterValue: DateFilterValue,
defaultFilter: { method: 'this-week' },
+ showInDocList: 'inline',
+ docListProperty: CreateDateDocListProperty,
+ groupHeader: CreatedGroupHeader,
},
docPrimaryMode: {
icon: FileIcon,
@@ -195,6 +254,9 @@ export const WorkspacePropertyTypes = {
},
filterValue: DocPrimaryModeFilterValue,
defaultFilter: { method: 'is', value: 'page' },
+ showInDocList: 'stack',
+ docListProperty: DocPrimaryModeDocListProperty,
+ groupHeader: DocPrimaryModeGroupHeader,
},
journal: {
icon: TodayIcon,
@@ -209,18 +271,25 @@ export const WorkspacePropertyTypes = {
},
filterValue: JournalFilterValue,
defaultFilter: { method: 'is', value: 'true' },
+ showInDocList: 'stack',
+ docListProperty: JournalDocListProperty,
+ groupHeader: JournalGroupHeader,
},
edgelessTheme: {
icon: EdgelessIcon,
value: EdgelessThemeValue,
name: 'com.affine.page-properties.property.edgelessTheme',
description: 'com.affine.page-properties.property.edgelessTheme.tooltips',
+ showInDocList: 'stack',
+ docListProperty: EdgelessThemeDocListProperty,
},
pageWidth: {
icon: LongerIcon,
value: PageWidthValue,
name: 'com.affine.page-properties.property.pageWidth',
description: 'com.affine.page-properties.property.pageWidth.tooltips',
+ showInDocList: 'stack',
+ docListProperty: PageWidthDocListProperty,
},
template: {
icon: TemplateIcon,
@@ -228,6 +297,8 @@ export const WorkspacePropertyTypes = {
name: 'com.affine.page-properties.property.template',
renameable: true,
description: 'com.affine.page-properties.property.template.tooltips',
+ showInDocList: 'stack',
+ docListProperty: TemplateDocListProperty,
},
unknown: {
icon: PropertyIcon,
@@ -254,6 +325,14 @@ export const WorkspacePropertyTypes = {
name: I18nString;
renameable?: boolean;
description?: I18nString;
+ /**
+ * Whether to show the property in the doc list,
+ * - `inline`: show the property in the doc list inline
+ * - `stack`: show as tags
+ */
+ showInDocList?: 'inline' | 'stack';
+ docListProperty?: React.FC
;
+ groupHeader?: React.FC;
};
};
diff --git a/packages/frontend/core/src/components/workspace-property-types/journal.tsx b/packages/frontend/core/src/components/workspace-property-types/journal.tsx
index 53bf453980..2112d52440 100644
--- a/packages/frontend/core/src/components/workspace-property-types/journal.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/journal.tsx
@@ -12,6 +12,7 @@ import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import { i18nTime, useI18n } from '@affine/i18n';
+import { TodayIcon } from '@blocksuite/icons/rc';
import {
useLiveData,
useService,
@@ -20,6 +21,9 @@ import {
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import * as styles from './journal.css';
@@ -216,3 +220,27 @@ export const JournalFilterValue = ({
);
};
+
+export const JournalDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const journalService = useService(JournalService);
+ const journalDate = useLiveData(journalService.journalDate$(doc.id));
+
+ if (!journalDate) {
+ return null;
+ }
+
+ return (
+ }>
+ {i18nTime(journalDate, { absolute: { accuracy: 'day' } })}
+
+ );
+};
+
+export const JournalGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
+ const text = groupId === 'true' ? 'Journal' : 'Not Journal';
+ return (
+
+ {text}
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/number.tsx b/packages/frontend/core/src/components/workspace-property-types/number.tsx
index 8dc4367482..4c6a1cd9e8 100644
--- a/packages/frontend/core/src/components/workspace-property-types/number.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/number.tsx
@@ -1,5 +1,6 @@
import { PropertyValue } from '@affine/component';
import { useI18n } from '@affine/i18n';
+import { NumberIcon } from '@blocksuite/icons/rc';
import {
type ChangeEventHandler,
useCallback,
@@ -7,6 +8,7 @@ import {
useState,
} from 'react';
+import { StackProperty } from '../explorer/docs-view/stack-property';
import type { PropertyValueProps } from '../properties/types';
import * as styles from './number.css';
@@ -55,3 +57,11 @@ export const NumberValue = ({
);
};
+
+export const NumberDocListProperty = ({ value }: { value: number }) => {
+ if (value !== 0 && !value) {
+ return null;
+ }
+
+ return }>{value};
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/page-width.tsx b/packages/frontend/core/src/components/workspace-property-types/page-width.tsx
index 7f688d05a1..a92b8da012 100644
--- a/packages/frontend/core/src/components/workspace-property-types/page-width.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/page-width.tsx
@@ -2,9 +2,12 @@ import { PropertyValue, type RadioItem } from '@affine/component';
import { DocService } from '@affine/core/modules/doc';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
+import { LongerIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
import { container } from './page-width.css';
@@ -58,3 +61,20 @@ export const PageWidthValue = ({ readonly }: PropertyValueProps) => {
);
};
+
+export const PageWidthDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const t = useI18n();
+ const pageWidth = useLiveData(doc.properties$.selector(p => p.pageWidth));
+
+ return (
+ }>
+ {pageWidth === 'standard' || !pageWidth
+ ? t[
+ 'com.affine.settings.editorSettings.page.default-page-width.standard'
+ ]()
+ : t[
+ 'com.affine.settings.editorSettings.page.default-page-width.full-width'
+ ]()}
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/tags.css.ts b/packages/frontend/core/src/components/workspace-property-types/tags.css.ts
index 855113d628..780e36b94b 100644
--- a/packages/frontend/core/src/components/workspace-property-types/tags.css.ts
+++ b/packages/frontend/core/src/components/workspace-property-types/tags.css.ts
@@ -5,3 +5,22 @@ export const tagInlineEditor = style({
});
export const container = style({});
+
+export const groupHeader = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+});
+export const groupHeaderIcon = style({
+ width: 24,
+ height: 24,
+ fontSize: 20,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+});
+export const groupHeaderLabel = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 4,
+});
diff --git a/packages/frontend/core/src/components/workspace-property-types/tags.tsx b/packages/frontend/core/src/components/workspace-property-types/tags.tsx
index 1210494bae..4db7d6e101 100644
--- a/packages/frontend/core/src/components/workspace-property-types/tags.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/tags.tsx
@@ -1,7 +1,7 @@
import { PropertyValue } from '@affine/component';
import type { FilterParams } from '@affine/core/modules/collection-rules';
import { DocService } from '@affine/core/modules/doc';
-import { TagService } from '@affine/core/modules/tag';
+import { type Tag, TagService } from '@affine/core/modules/tag';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { TagsIcon } from '@blocksuite/icons/rc';
@@ -9,6 +9,9 @@ import { useLiveData, useService } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useCallback, useMemo } from 'react';
+import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import type { PropertyValueProps } from '../properties/types';
import {
@@ -164,3 +167,73 @@ const TagsInlineEditor = ({
/>
);
};
+
+const TagName = ({ tag }: { tag: Tag }) => {
+ const name = useLiveData(tag.value$);
+ return name;
+};
+const TagIcon = ({ tag, size = 8 }: { tag: Tag; size?: number }) => {
+ const color = useLiveData(tag.color$);
+ return (
+
+ );
+};
+export const TagsDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const tagList = useService(TagService).tagList;
+ const tags = useLiveData(tagList.tagsByPageId$(doc.id));
+
+ return (
+ <>
+ {tags.map(tag => {
+ return (
+ } key={tag.id}>
+
+
+ );
+ })}
+ >
+ );
+};
+
+export const TagsGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
+ const t = useI18n();
+ const tagService = useService(TagService);
+ const tag = useLiveData(tagService.tagList.tagByTagId$(groupId));
+
+ if (!tag) {
+ return (
+
+ }
+ >
+ {t['com.affine.page.display.grouping.group-by-tag.untagged']()}
+
+ );
+ }
+ return (
+ }
+ >
+
+
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/template.tsx b/packages/frontend/core/src/components/workspace-property-types/template.tsx
index 3d9eb64f43..75ed3d16b0 100644
--- a/packages/frontend/core/src/components/workspace-property-types/template.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/template.tsx
@@ -1,8 +1,12 @@
import { Checkbox, PropertyValue } from '@affine/component';
import { DocService } from '@affine/core/modules/doc';
+import { useI18n } from '@affine/i18n';
+import { TemplateIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { type ChangeEvent, useCallback } from 'react';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { DocListPropertyProps } from '../explorer/types';
import type { PropertyValueProps } from '../properties/types';
import * as styles from './template.css';
@@ -39,3 +43,16 @@ export const TemplateValue = ({ readonly }: PropertyValueProps) => {
);
};
+
+export const TemplateDocListProperty = ({ doc }: DocListPropertyProps) => {
+ const t = useI18n();
+ const isTemplate = useLiveData(doc.properties$.selector(p => p.isTemplate));
+
+ if (!isTemplate) {
+ return null;
+ }
+
+ return (
+ }>{t['Template']()}
+ );
+};
diff --git a/packages/frontend/core/src/components/workspace-property-types/text.tsx b/packages/frontend/core/src/components/workspace-property-types/text.tsx
index 0693fcd22d..364e3a474c 100644
--- a/packages/frontend/core/src/components/workspace-property-types/text.tsx
+++ b/packages/frontend/core/src/components/workspace-property-types/text.tsx
@@ -1,7 +1,7 @@
import { Input, Menu, PropertyValue } from '@affine/component';
import type { FilterParams } from '@affine/core/modules/collection-rules';
import { useI18n } from '@affine/i18n';
-import { TextIcon } from '@blocksuite/icons/rc';
+import { TextIcon, TextTypeIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import {
@@ -12,6 +12,9 @@ import {
useState,
} from 'react';
+import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
+import { StackProperty } from '../explorer/docs-view/stack-property';
+import type { GroupHeaderProps } from '../explorer/types';
import { ConfigModal } from '../mobile';
import type { PropertyValueProps } from '../properties/types';
import * as styles from './text.css';
@@ -248,3 +251,20 @@ export const TextFilterValue = ({
) : null;
};
+
+export const TextDocListProperty = ({ value }: { value: string }) => {
+ if (!value) {
+ return null;
+ }
+
+ return }>{value};
+};
+
+export const TextGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
+ const text = groupId || 'No Text';
+ return (
+
+ {text}
+
+ );
+};
diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.css.ts b/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.css.ts
index c9d274c76a..366c5522de 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.css.ts
+++ b/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.css.ts
@@ -1,3 +1,4 @@
+import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const scrollContainer = style({
flex: 1,
@@ -27,3 +28,17 @@ export const body = style({
height: '100%',
width: '100%',
});
+
+export const scrollArea = style({
+ height: 0,
+ flex: 1,
+});
+
+// group
+export const groupHeader = style({
+ background: cssVarV2.layer.background.primary,
+});
+
+export const docItem = style({
+ transition: 'width 0.2s ease-in-out',
+});
diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.tsx
index c33f41380b..8718315f0b 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/all-page-new/all-page.tsx
@@ -1,11 +1,37 @@
+import {
+ Masonry,
+ type MasonryGroup,
+ RadioGroup,
+ useConfirmModal,
+} from '@affine/component';
+import {
+ DocExplorerContext,
+ type DocExplorerContextType,
+} from '@affine/core/components/explorer/context';
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
+import {
+ DocListItem,
+ type DocListItemView,
+} from '@affine/core/components/explorer/docs-view/doc-list-item';
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
import { Filters } from '@affine/core/components/filter';
+import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
+import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
-import { useI18n } from '@affine/i18n';
-import { useService } from '@toeverything/infra';
-import { useCallback, useEffect, useState } from 'react';
+import { DocsService } from '@affine/core/modules/doc';
+import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
+import { Trans, useI18n } from '@affine/i18n';
+import { useLiveData, useService } from '@toeverything/infra';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import {
+ memo,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
import {
ViewBody,
@@ -16,14 +42,123 @@ import {
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
import * as styles from './all-page.css';
import { MigrationAllDocsDataNotification } from './migration-data';
+
+const GroupHeader = memo(function GroupHeader({
+ groupId,
+ collapsed,
+ itemCount,
+}: {
+ groupId: string;
+ collapsed?: boolean;
+ itemCount: number;
+}) {
+ const { groupBy } = useContext(DocExplorerContext);
+ const propertyService = useService(WorkspacePropertyService);
+ const allProperties = useLiveData(propertyService.sortedProperties$);
+
+ const groupType = groupBy?.type;
+ const groupKey = groupBy?.key;
+
+ const header = useMemo(() => {
+ if (groupType === 'property') {
+ const property = allProperties.find(p => p.id === groupKey);
+ if (!property) return null;
+
+ const config = WorkspacePropertyTypes[property.type];
+ if (!config?.groupHeader) return null;
+ return (
+
+ );
+ } else {
+ return '// TODO: ' + groupType;
+ }
+ }, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
+
+ if (!groupType) {
+ return null;
+ }
+
+ return header;
+});
+
+const calcCardHeightById = (id: string) => {
+ const max = 5;
+ const min = 1;
+ const code = id.charCodeAt(0);
+ const value = Math.floor((code % (max - min)) + min);
+ return 250 + value * 10;
+};
+
+const DocListItemComponent = memo(function DocListItemComponent({
+ itemId,
+ groupId,
+}: {
+ groupId: string;
+ itemId: string;
+}) {
+ return ;
+});
+
export const AllPage = () => {
const t = useI18n();
+ const docsService = useService(DocsService);
+ const [view, setView] = useState('masonry');
+ const [collapsedGroups, setCollapsedGroups] = useState([]);
+ const [selectMode, setSelectMode] = useState(false);
+ const [selectedDocIds, setSelectedDocIds] = useState([]);
+ const [prevCheckAnchorId, setPrevCheckAnchorId] = useState(
+ null
+ );
const [explorerPreference, setExplorerPreference] =
- useState({});
+ useState({
+ filters: [
+ {
+ type: 'system',
+ key: 'trash',
+ value: 'false',
+ method: 'is',
+ },
+ ],
+ displayProperties: [],
+ showDocIcon: true,
+ showDocPreview: true,
+ });
const [groups, setGroups] = useState([]);
+ const { openConfirmModal } = useConfirmModal();
+
+ const masonryItems = useMemo(() => {
+ const items = groups.map((group: any) => {
+ return {
+ id: group.key,
+ Component: groups.length > 1 ? GroupHeader : undefined,
+ height: groups.length > 1 ? 24 : 0,
+ className: styles.groupHeader,
+ items: group.items.map((docId: string) => {
+ return {
+ id: docId,
+ Component: DocListItemComponent,
+ height:
+ view === 'list'
+ ? 42
+ : view === 'grid'
+ ? 280
+ : calcCardHeightById(docId),
+ 'data-view': view,
+ className: styles.docItem,
+ };
+ }),
+ } satisfies MasonryGroup;
+ });
+ return items;
+ }, [groups, view]);
+
const collectionRulesService = useService(CollectionRulesService);
useEffect(() => {
const subscription = collectionRulesService
@@ -43,7 +178,26 @@ export const AllPage = () => {
return () => {
subscription.unsubscribe();
};
- }, [collectionRulesService, explorerPreference]);
+ }, [
+ collectionRulesService,
+ explorerPreference.filters,
+ explorerPreference.groupBy,
+ explorerPreference.orderBy,
+ ]);
+
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setSelectMode(false);
+ setSelectedDocIds([]);
+ setPrevCheckAnchorId(null);
+ }
+ };
+ document.addEventListener('keydown', onKeyDown);
+ return () => {
+ document.removeEventListener('keydown', onKeyDown);
+ };
+ }, []);
const handleFilterChange = useCallback((filters: FilterParams[]) => {
setExplorerPreference(prev => ({
@@ -51,8 +205,92 @@ export const AllPage = () => {
filters,
}));
}, []);
+
+ const toggleGroupCollapse = useCallback((groupId: string) => {
+ setCollapsedGroups(prev => {
+ return prev.includes(groupId)
+ ? prev.filter(id => id !== groupId)
+ : [...prev, groupId];
+ });
+ }, []);
+ const toggleDocSelect = useCallback((docId: string) => {
+ setSelectMode(true);
+ setSelectedDocIds(prev => {
+ return prev.includes(docId)
+ ? prev.filter(id => id !== docId)
+ : [...prev, docId];
+ });
+ }, []);
+ const onSelect = useCallback(
+ (...args: Parameters) => {
+ setSelectMode(true);
+ setSelectedDocIds(...args);
+ },
+ []
+ );
+ const handleCloseFloatingToolbar = useCallback(() => {
+ setSelectMode(false);
+ setSelectedDocIds([]);
+ }, []);
+ const handleMultiDelete = useCallback(() => {
+ if (selectedDocIds.length === 0) {
+ return;
+ }
+
+ openConfirmModal({
+ title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
+ number: selectedDocIds.length.toString(),
+ }),
+ description: t[
+ 'com.affine.moveToTrash.confirmModal.description.multiple'
+ ]({
+ number: selectedDocIds.length.toString(),
+ }),
+ cancelText: t['com.affine.confirmModal.button.cancel'](),
+ confirmText: t.Delete(),
+ confirmButtonOptions: {
+ variant: 'error',
+ },
+ onConfirm: () => {
+ for (const docId of selectedDocIds) {
+ const doc = docsService.list.doc$(docId).value;
+ doc?.moveToTrash();
+ }
+ },
+ });
+ }, [docsService.list, openConfirmModal, selectedDocIds, t]);
+
+ const explorerContextValue = useMemo(
+ () =>
+ ({
+ ...explorerPreference,
+ view,
+ groups,
+ collapsed: collapsedGroups,
+ selectedDocIds,
+ selectMode,
+ prevCheckAnchorId,
+ setPrevCheckAnchorId,
+ onToggleCollapse: toggleGroupCollapse,
+ onToggleSelect: toggleDocSelect,
+ onSelect,
+ }) satisfies DocExplorerContextType,
+ [
+ collapsedGroups,
+ explorerPreference,
+ groups,
+ onSelect,
+ prevCheckAnchorId,
+ selectMode,
+ selectedDocIds,
+ toggleDocSelect,
+ toggleGroupCollapse,
+ view,
+ ]
+ );
+
return (
- <>
+
@@ -60,6 +298,12 @@ export const AllPage = () => {
+
{
onChange={setExplorerPreference}
/>
-
{JSON.stringify(explorerPreference, null, 2)}
-
{JSON.stringify(groups, null, 2)}
+
+ (w > 500 ? 24 : w > 393 ? 20 : 16),
+ []
+ )}
+ />
+
+
+
+ {{ count: selectedDocIds.length } as any}
+
+ selected
+
+ }
+ />
- >
+
);
};
diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts
index 89ec23d298..1898a3d531 100644
--- a/packages/frontend/core/src/modules/cloud/index.ts
+++ b/packages/frontend/core/src/modules/cloud/index.ts
@@ -38,8 +38,6 @@ export type { ServerConfig } from './types';
// eslint-disable-next-line simple-import-sort/imports
import { type Framework } from '@toeverything/infra';
-import { DocScope } from '../doc/scopes/doc';
-import { DocService } from '../doc/services/doc';
import { GlobalCache, GlobalState } from '../storage/providers/global';
import { GlobalStateService } from '../storage/services/global';
import { UrlService } from '../url';
@@ -101,7 +99,7 @@ import { DocCreatedByService } from './services/doc-created-by';
import { DocUpdatedByService } from './services/doc-updated-by';
import { DocCreatedByUpdatedBySyncService } from './services/doc-created-by-updated-by-sync';
import { WorkspacePermissionService } from '../permissions';
-import { DocsService } from '../doc';
+import { DocScope, DocService, DocsService } from '../doc';
import { DocCreatedByUpdatedBySyncStore } from './stores/doc-created-by-updated-by-sync';
export function configureCloudModule(framework: Framework) {
diff --git a/packages/frontend/core/src/modules/cloud/views/public-user.tsx b/packages/frontend/core/src/modules/cloud/views/public-user.tsx
index 29af2028fc..bc676cb589 100644
--- a/packages/frontend/core/src/modules/cloud/views/public-user.tsx
+++ b/packages/frontend/core/src/modules/cloud/views/public-user.tsx
@@ -7,7 +7,15 @@ import { useLayoutEffect, useMemo } from 'react';
import { PublicUserService } from '../services/public-user';
import * as styles from './public-user.css';
-export const PublicUserLabel = ({ id }: { id: string }) => {
+export const PublicUserLabel = ({
+ id,
+ size = 20,
+ showName = true,
+}: {
+ id: string;
+ size?: number;
+ showName?: boolean;
+}) => {
const serverService = useCurrentServerService();
const publicUser = useMemo(() => {
return serverService?.scope.get(PublicUserService);
@@ -28,10 +36,16 @@ export const PublicUserLabel = ({ id }: { id: string }) => {
}
if (user?.removed) {
- return (
+ return showName ? (
{t['Unknown User']()}
+ ) : (
+
);
}
@@ -40,10 +54,10 @@ export const PublicUserLabel = ({ id }: { id: string }) => {
- {user?.name}
+ {showName && user?.name}
);
};
diff --git a/packages/frontend/core/src/modules/collection-rules/impls/filters/property.ts b/packages/frontend/core/src/modules/collection-rules/impls/filters/property.ts
index 412ac19b47..1da0649a82 100644
--- a/packages/frontend/core/src/modules/collection-rules/impls/filters/property.ts
+++ b/packages/frontend/core/src/modules/collection-rules/impls/filters/property.ts
@@ -25,7 +25,7 @@ export class PropertyFilterProvider extends Service implements FilterProvider {
FilterProvider('property:' + type)
);
if (!provider) {
- throw new Error('Unsupported property type');
+ throw new Error(`Unsupported property type: ${type}`);
}
return provider.filter$(params);
})
diff --git a/packages/frontend/core/src/modules/collection-rules/impls/filters/trash.ts b/packages/frontend/core/src/modules/collection-rules/impls/filters/trash.ts
new file mode 100644
index 0000000000..afc7012965
--- /dev/null
+++ b/packages/frontend/core/src/modules/collection-rules/impls/filters/trash.ts
@@ -0,0 +1,21 @@
+import type { DocsService } from '@affine/core/modules/doc';
+import { Service } from '@toeverything/infra';
+import { map, type Observable } from 'rxjs';
+
+import type { FilterProvider } from '../../provider';
+import type { FilterParams } from '../../types';
+
+export class TrashFilterProvider extends Service implements FilterProvider {
+ constructor(private readonly docsService: DocsService) {
+ super();
+ }
+ filter$(params: FilterParams): Observable> {
+ if (params.value === 'true') {
+ return this.docsService.allTrashDocIds$().pipe(map(ids => new Set(ids)));
+ } else {
+ return this.docsService
+ .allNonTrashDocIds$()
+ .pipe(map(ids => new Set(ids)));
+ }
+ }
+}
diff --git a/packages/frontend/core/src/modules/collection-rules/index.ts b/packages/frontend/core/src/modules/collection-rules/index.ts
index 08b651ede9..35646485c1 100644
--- a/packages/frontend/core/src/modules/collection-rules/index.ts
+++ b/packages/frontend/core/src/modules/collection-rules/index.ts
@@ -14,6 +14,7 @@ import { PropertyFilterProvider } from './impls/filters/property';
import { SystemFilterProvider } from './impls/filters/system';
import { TagsFilterProvider } from './impls/filters/tags';
import { TextPropertyFilterProvider } from './impls/filters/text';
+import { TrashFilterProvider } from './impls/filters/trash';
import { UpdatedAtFilterProvider } from './impls/filters/updated-at';
import { UpdatedByFilterProvider } from './impls/filters/updated-by';
import { CheckboxPropertyGroupByProvider } from './impls/group-by/checkbox';
@@ -79,6 +80,7 @@ export function configureCollectionRulesModule(framework: Framework) {
DocPrimaryModeFilterProvider,
[DocsService]
)
+ .impl(FilterProvider('system:trash'), TrashFilterProvider, [DocsService])
.impl(FilterProvider('property:date'), DatePropertyFilterProvider, [
DocsService,
])
diff --git a/packages/frontend/core/src/modules/doc/services/docs.ts b/packages/frontend/core/src/modules/doc/services/docs.ts
index 106286ba6c..a19270305d 100644
--- a/packages/frontend/core/src/modules/doc/services/docs.ts
+++ b/packages/frontend/core/src/modules/doc/services/docs.ts
@@ -69,6 +69,14 @@ export class DocsService extends Service {
return this.store.watchAllDocTagIds();
}
+ allNonTrashDocIds$() {
+ return this.store.watchNonTrashDocIds();
+ }
+
+ allTrashDocIds$() {
+ return this.store.watchTrashDocIds();
+ }
+
constructor(
private readonly store: DocsStore,
private readonly docPropertiesStore: DocPropertiesStore,
diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json
index 4ad4315d86..e15dcdce81 100644
--- a/packages/frontend/i18n/src/i18n-completenesses.json
+++ b/packages/frontend/i18n/src/i18n-completenesses.json
@@ -1,26 +1,26 @@
{
- "ar": 97,
+ "ar": 96,
"ca": 4,
"da": 4,
- "de": 97,
- "el-GR": 97,
+ "de": 96,
+ "el-GR": 96,
"en": 100,
- "es-AR": 97,
+ "es-AR": 96,
"es-CL": 98,
- "es": 97,
- "fa": 97,
- "fr": 97,
+ "es": 96,
+ "fa": 96,
+ "fr": 96,
"hi": 2,
- "it-IT": 97,
+ "it-IT": 96,
"it": 1,
- "ja": 97,
+ "ja": 96,
"ko": 56,
- "pl": 97,
- "pt-BR": 97,
- "ru": 97,
- "sv-SE": 97,
- "uk": 97,
+ "pl": 96,
+ "pt-BR": 96,
+ "ru": 96,
+ "sv-SE": 96,
+ "uk": 96,
"ur": 2,
- "zh-Hans": 97,
- "zh-Hant": 97
+ "zh-Hans": 96,
+ "zh-Hant": 96
}
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index 513fbf6e34..94dd07d0d7 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -593,6 +593,18 @@ export function useAFFiNEI18N(): {
* `current`
*/
current(): string;
+ /**
+ * `created at {{time}}`
+ */
+ ["created at"](options: {
+ readonly time: string;
+ }): string;
+ /**
+ * `last updated at {{time}}`
+ */
+ ["updated at"](options: {
+ readonly time: string;
+ }): string;
/**
* `Automatically check for new updates periodically.`
*/
@@ -6908,6 +6920,46 @@ export function useAFFiNEI18N(): {
* `Inactive workspace`
*/
["com.affine.inactive-workspace"](): string;
+ /**
+ * `Display Properties`
+ */
+ ["com.affine.all-docs.display.properties"](): string;
+ /**
+ * `List view options`
+ */
+ ["com.affine.all-docs.display.list-view"](): string;
+ /**
+ * `Icon`
+ */
+ ["com.affine.all-docs.display.list-view.icon"](): string;
+ /**
+ * `Body`
+ */
+ ["com.affine.all-docs.display.list-view.body"](): string;
+ /**
+ * `Quick actions`
+ */
+ ["com.affine.all-docs.quick-actions"](): string;
+ /**
+ * `Favorite`
+ */
+ ["com.affine.all-docs.quick-action.favorite"](): string;
+ /**
+ * `Move to trash`
+ */
+ ["com.affine.all-docs.quick-action.trash"](): string;
+ /**
+ * `Open in split view`
+ */
+ ["com.affine.all-docs.quick-action.split"](): string;
+ /**
+ * `Open in new tab`
+ */
+ ["com.affine.all-docs.quick-action.tab"](): string;
+ /**
+ * `Select checkbox`
+ */
+ ["com.affine.all-docs.quick-action.select"](): string;
/**
* `core`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 5d84a5b294..59688baa6f 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -137,6 +137,8 @@
"Deleted User": "Deleted User",
"all": "all",
"current": "current",
+ "created at": "created at {{time}}",
+ "updated at": "last updated at {{time}}",
"com.affine.aboutAFFiNE.autoCheckUpdate.description": "Automatically check for new updates periodically.",
"com.affine.aboutAFFiNE.autoCheckUpdate.title": "Check for updates automatically",
"com.affine.aboutAFFiNE.autoDownloadUpdate.description": "Automatically download updates (to this device).",
@@ -1725,6 +1727,16 @@
"com.affine.inactive": "Inactive",
"com.affine.inactive-member": "Inactive member",
"com.affine.inactive-workspace": "Inactive workspace",
+ "com.affine.all-docs.display.properties": "Display Properties",
+ "com.affine.all-docs.display.list-view": "List view options",
+ "com.affine.all-docs.display.list-view.icon": "Icon",
+ "com.affine.all-docs.display.list-view.body": "Body",
+ "com.affine.all-docs.quick-actions": "Quick actions",
+ "com.affine.all-docs.quick-action.favorite": "Favorite",
+ "com.affine.all-docs.quick-action.trash": "Move to trash",
+ "com.affine.all-docs.quick-action.split": "Open in split view",
+ "com.affine.all-docs.quick-action.tab": "Open in new tab",
+ "com.affine.all-docs.quick-action.select": "Select checkbox",
"core": "core",
"dark": "Dark",
"invited you to join": "invited you to join",