diff --git a/config/index.ts b/config/index.ts index 7e3a91b..dd77f7a 100644 --- a/config/index.ts +++ b/config/index.ts @@ -20,6 +20,6 @@ export const OSS_MATATAKI_FEUSE = 'https://ssimg.frontenduse.top'; export const OSS_MATATAKI = 'https://smartsignature-img.oss-cn-hongkong.aliyuncs.com'; // Gun Config -export const GUN_PEERS = ['http://gun-manhattan.herokuapp.com/gun']; +export const GUN_PEERS = ['https://gun-manhattan.herokuapp.com/gun']; export const KEY_GUN_ROOT = 'meta.io_v7'; export const KEY_GUN_ROOT_DRAFT = 'cms_draft'; diff --git a/src/app.tsx b/src/app.tsx index 07eb500..98b3d0c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -15,6 +15,7 @@ import { queryCurrentUser, queryInvitations, refreshTokens } from './services/ap import type { SiderMenuProps } from '@ant-design/pro-layout/lib/components/SiderMenu/SiderMenu'; import { getDefaultSiteConfigAPI } from './helpers/index'; import { FetchPostsStorageParamsState } from './services/constants'; +import MenuFeedbackButton from './components/menu/MenuFeedbackButton'; const { Text } = Typography; @@ -195,6 +196,10 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => { history.push('/user/login'); } }, - links: [, ], + links: [ + , + , + , + ], }; }; diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 195fbf3..52557b7 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -33,7 +33,12 @@ const Editor: React.FC = React.memo(function Editor({ asyncContentToDB }) }, []); const init = useCallback(() => { + const _height = + window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; + const vditor = new Vditor('vditor', { + width: '100%', + height: _height - 206, cache: { enable: false, }, diff --git a/src/components/Guide/StoreSetting.tsx b/src/components/Guide/StoreSetting.tsx index 9294c45..fa50344 100644 --- a/src/components/Guide/StoreSetting.tsx +++ b/src/components/Guide/StoreSetting.tsx @@ -56,8 +56,12 @@ export default () => { return Promise.resolve(); }; const updateRepoSettings = async (values: { storeRepo: string; publishRepo: string }) => { - setStoreSetting((prev) => ({ ...prev, repos: values })); - message.success(intl.formatMessage({ id: 'messages.store.setRepoName' }, values)); + if (values.storeRepo !== values.publishRepo) { + setStoreSetting((prev) => ({ ...prev, repos: values })); + message.success(intl.formatMessage({ id: 'messages.store.setRepoName' }, values)); + } else { + message.error(intl.formatMessage({ id: 'messages.store.form.sameRepoName' }, values)); + } }; return ( diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 0000000..16ebe0b --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import Icon from '@ant-design/icons'; + +const MatrixSvg = () => ( + + + + + +); +export const MatrixIcon: React.FC = (props) => ; + +const TelegramSvg = () => ( + + + +); +export const TelegramIcon: React.FC = (props) => ; + +const DiscordSvg = () => ( + + + + +); +export const DiscordIcon: React.FC = (props) => ; +const MetaLogoSvg = () => ( + + + + +); +export const MetaLogoIcon: React.FC = (props) => ; + +const ElementSvg = () => ( + + + +); +export const ElementIcon: React.FC = (props) => ; diff --git a/src/components/menu/MenuFeedbackButton/index.tsx b/src/components/menu/MenuFeedbackButton/index.tsx new file mode 100644 index 0000000..839eb57 --- /dev/null +++ b/src/components/menu/MenuFeedbackButton/index.tsx @@ -0,0 +1,18 @@ +import { MatrixIcon } from '../../Icon/index'; +import { Tooltip } from 'antd'; + +const feedbackLink = 'https://forms.gle/1HAZ8puQ9vhBSqMGA'; + +export default () => { + return ( +
{ + window.open(feedbackLink, '_blank'); + }} + > + + + +
+ ); +}; diff --git a/src/components/menu/MenuLanguageSwitch/index.tsx b/src/components/menu/MenuLanguageSwitch/index.tsx index 9c6820e..f15c628 100644 --- a/src/components/menu/MenuLanguageSwitch/index.tsx +++ b/src/components/menu/MenuLanguageSwitch/index.tsx @@ -1,7 +1,8 @@ +import { setLocale } from 'umi'; +import { useState } from 'react'; import { GlobalOutlined } from '@ant-design/icons'; import { Dropdown, Menu, Typography } from 'antd'; import style from './index.less'; -import { setLocale } from 'umi'; const languages = [ { @@ -29,13 +30,25 @@ const menu = ( ); -export default () => ( - - - -); +export default () => { + const [visible, setVisible] = useState(false); + + return ( +
{ + setVisible((v) => !v); + return; + }} + > + setVisible(isVisible)} + overlayClassName={style.menuLanguageSwitch} + > + + +
+ ); +}; diff --git a/src/components/menu/MenuMoreInfo/index.tsx b/src/components/menu/MenuMoreInfo/index.tsx index 87ae443..998b00f 100644 --- a/src/components/menu/MenuMoreInfo/index.tsx +++ b/src/components/menu/MenuMoreInfo/index.tsx @@ -1,96 +1,126 @@ +import { Dropdown, Menu } from 'antd'; +import { Fragment, useState } from 'react'; +import { FormattedMessage } from 'umi'; import { - GithubOutlined, - LinkOutlined, + TwitterOutlined, MediumOutlined, + LinkOutlined, QuestionOutlined, - TwitterOutlined, + YoutubeOutlined, } from '@ant-design/icons'; -import { Dropdown, Menu } from 'antd'; -import { FormattedMessage } from 'umi'; +import { TelegramIcon, DiscordIcon, ElementIcon, MetaLogoIcon } from '../../Icon/index'; import style from './index.less'; +const menuJson = [ + { + title: , + item: [ + { + url: 'https://matrix.to/#/!jrjmzTFiYYIuKnRpEg:matrix.org?via=matrix.org', + icon: , + name: 'Matrix Group', + }, + { + url: 'https://discord.com/invite/59cXXWCWUT', + icon: , + name: 'Discord', + }, + { + url: 'https://t.me/metanetwork', + icon: , + name: 'Telegram', + }, + { + url: 'https://twitter.com/realMetaNetwork', + icon: , + name: 'Twitter', + }, + { + url: 'https://medium.com/meta-network', + icon: , + name: 'Medium', + }, + { + url: 'https://www.youtube.com/channel/UC-rNon6FUm3blTnSrXta2gw', + icon: , + name: 'Youtube', + }, + ], + }, + { + title: , + item: [ + { + url: 'https://www.meta.io', + icon: , + name: 'Meta.io', + }, + { + url: 'https://www.matataki.io', + icon: , + name: 'Matataki', + }, + { + url: 'https://home.metanetwork.online', + icon: , + name: 'home', + }, + ], + }, + { + title: , + item: [ + { + url: 'https://metanetwork.online/terms', + icon: , + name: , + }, + { + url: 'https://metanetwork.online/privacy', + icon: , + name: , + }, + ], + }, +]; + const menu = ( - - - - }> - - Twitter - - - }> - - Telegram - - - }> - - Discord - - - }> - - Medium - - - }> - - Github - - - - - - - - - - - - - - - - - - Meta.io - - - - - Meta Space - - - - - Meta Network - - - - - - - - - - - - - - - - + {menuJson.map((i, idx) => ( + + {i.title} + {i.item.map((j, idxJ) => ( + + + {j.name} + + + ))} + {idx < menuJson.length - 1 ? : null} + + ))} ); -export default () => ( - - - -); +export default () => { + const [visible, setVisible] = useState(false); + + return ( +
{ + setVisible((v) => !v); + return; + }} + > + setVisible(isVisible)} + overlayClassName={style.menuMoreInfo} + > + + +
+ ); +}; diff --git a/src/db/Posts.d.ts b/src/db/Posts.d.ts deleted file mode 100644 index 36c4c65..0000000 --- a/src/db/Posts.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Posts { - id?: number; - cover: string; - title: string; - summary: string; - content: string; - hash: string; - status: 'pending' | 'publish'; - timestamp: number; - delete: boolean; - post: CMS.Post | null; - draft: CMS.Draft | null; - tags: string[]; - license: string; - userId: number; - createdAt: string; - updatedAt: string; -} diff --git a/src/db/db.ts b/src/db/db.ts index 7bb744b..1bc2ebc 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,12 +1,11 @@ import type { Table, Transaction } from 'dexie'; import Dexie from 'dexie'; -import type { Posts } from './Posts'; import type { Metadatas, MetadataTempDataState } from './Metadatas'; import moment from 'moment'; import { License } from '../../config'; export class StoreDB extends Dexie { - posts!: Table; + posts!: Table; metadatas!: Table; constructor() { super('StoreDB'); @@ -19,7 +18,7 @@ export class StoreDB extends Dexie { .upgrade((tx: Transaction | any) => { const time = moment().toISOString(); // TODO: modify typescript - tx.posts.toCollection().modify((post: Posts) => { + tx.posts.toCollection().modify((post: PostType.Posts) => { // console.log('post', post); post.cover = post.cover || ''; post.title = post.title || ''; @@ -62,7 +61,7 @@ export const dbPostsUpdate = async (id: number, data: T): Promise => * @param data * @returns */ -export const dbPostsAdd = async (data: Posts): Promise => { +export const dbPostsAdd = async (data: PostType.Posts): Promise => { return await db.posts.add(data); }; @@ -71,7 +70,7 @@ export const dbPostsAdd = async (data: Posts): Promise => { * @param id * @returns */ -export const dbPostsGet = async (id: number): Promise => { +export const dbPostsGet = async (id: number): Promise => { return await db.posts.get(id); }; @@ -95,7 +94,7 @@ export const dbPostsDeleteAll = async (): Promise => { * @param userId * @returns */ -export const dbPostsAll = async (userId: number): Promise => { +export const dbPostsAll = async (userId: number): Promise => { return await db.posts .filter((i) => !i.delete && i.userId === userId) .reverse() @@ -122,14 +121,14 @@ export const dbPostsWhereExist = async (id: number): Promise => { return result.some((post) => post.post && Number(post.post.id) === id); }; -export const dbPostsWhereByID = async (id: number): Promise => { +export const dbPostsWhereByID = async (id: number): Promise => { // 草稿删除了 允许重新编辑 const result = await db.posts.filter((i) => !i.delete).toArray(); return result.find((post) => post.post && Number(post.post.id) === id); }; // post data temp -export const PostTempData = (): Posts => ({ +export const PostTempData = (): PostType.Posts => ({ cover: '', title: '', summary: '', diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts index b601235..dbc57d2 100644 --- a/src/locales/en-US/component.ts +++ b/src/locales/en-US/component.ts @@ -22,7 +22,6 @@ export default { 'component.button.regenerate': 'regenerate', 'component.status.alreadyBound': 'Already bound', 'component.status.notBound': 'Not bound', - 'component.badge.invitationAvailable': 'Invitation code available', - 'component.badge.invitationUsed': 'Used invitation code', + 'component.status.used': 'Used', 'component.full.tip': 'If the page does not respond, you can manually close it!', }; diff --git a/src/locales/en-US/editor.ts b/src/locales/en-US/editor.ts index a28d6e1..c2d69fc 100644 --- a/src/locales/en-US/editor.ts +++ b/src/locales/en-US/editor.ts @@ -46,7 +46,7 @@ export default { 'editor.publish.item.gateway.label': 'Backup storage', 'editor.publish.item.gateway.description': 'Store all and cannot be deleted', 'editor.tips.content': - 'When you submit an article and choose to directly store your metadata in IPFS, it will not go through any backend or database of Meta Network, reducing the possibility of man-in-the-middle attacks. If you want the content to be displayed in your Meta Space, you need to return to the CMS for publishing after submission.', + 'When you submit an article and choose to directly store your metadata in IPFS, it will reducing the possibility of man-in-the-middle attacks as much as possible. If you want the content to be displayed in your Meta Space, you need to return to the CMS for publishing after submission.', 'editor.originalLink.title': 'Original link', 'editor.originalLink.noOriginalLink': 'No original link', 'editor.learn.content': 'Quickly learn the Meta Space editor', diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 2064fdd..56d65e9 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -22,7 +22,7 @@ export default { 'menu.moreInfo.policy': 'Policy', 'menu.moreInfo.terms': 'Terms', 'menu.moreInfo.privacyPolicy': 'Privacy Policy', - 'menu.moreInfo.versions': 'Version record', + 'menu.moreInfo.home': 'Home', 'menu.post.create': 'Create', 'menu.settings': 'Settings', }; diff --git a/src/locales/en-US/messages.ts b/src/locales/en-US/messages.ts index a3a898c..481268f 100644 --- a/src/locales/en-US/messages.ts +++ b/src/locales/en-US/messages.ts @@ -49,14 +49,16 @@ export default { 'The storage repository already exists, please set a new name', 'messages.store.form.repoName': 'Please set the name of the storage repository', 'messages.store.form.repoNameAvailable': 'This name is available!', + 'messages.store.form.sameRepoName': + 'The names of the repositories should not be the same, please set a new name', 'messages.invitation.title': 'Invitation code management', 'messages.invitation.description': 'Manage the invitation codes you have here.\nYou can edit the invitation information below to customize the information that the other party will accept after sending.', - 'messages.invitation.cardTitle': 'Invitation code #{id}', + 'messages.invitation.inviteMessage': + 'Hi, Meta Network Natives are inviting you to be their neighbors!,visit https://home.metanetwork.online,join us with your invitation code! ', 'messages.info.updateSuccess': 'Information update completed', 'messages.info.updateFailed': 'Information update failed', 'messages.info.copySuccess': 'Copy successfully', - 'messages.invitation.cardLabel': 'Invitation code', 'messages.invitation.editInfoHere': 'You can edit the information here', 'messages.invitation.inviteeLabel': 'Invited people', 'messages.invitation.inviteeName': 'The name of the invitee', @@ -180,7 +182,6 @@ export default { 'messages.editor.submit.generateKey.fail': 'Generated failed', 'messages.editor.submit.bindStorage': 'Please bind GitHub', 'messages.editor.submit.uploadMetadata.fail': 'Failed to upload metadata, please try again!', - 'messages.invitation.card.title': 'Invitation code #{codeId}', 'messages.delete.success': 'deleted successfully', 'messages.delete.fail': 'delete failed', 'messages.dashboard.lastPublishDate': 'Last publish date: {time, time, ::yyyyMMddHHmmss}', diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts index 7fe1628..d063c83 100644 --- a/src/locales/zh-CN/component.ts +++ b/src/locales/zh-CN/component.ts @@ -22,7 +22,6 @@ export default { 'component.button.regenerate': '重新生成', 'component.status.alreadyBound': '已绑定', 'component.status.notBound': '未绑定', - 'component.badge.invitationAvailable': '邀请码可用', - 'component.badge.invitationUsed': '邀请码已使用', + 'component.status.used': '已使用', 'component.full.tip': '如遇到页面失去反馈,可手动关闭!', }; diff --git a/src/locales/zh-CN/editor.ts b/src/locales/zh-CN/editor.ts index d0cfe6e..e30ed10 100644 --- a/src/locales/zh-CN/editor.ts +++ b/src/locales/zh-CN/editor.ts @@ -41,7 +41,7 @@ export default { 'editor.publish.item.gateway.label': '备份存储', 'editor.publish.item.gateway.description': '存储全文,且无法删除', 'editor.tips.content': - '当您提交文章并选择在 IPFS 直接存证您的元数据时,不会经过 Meta Network 的任何后台或数据库,减少中间人攻击的可能性。若要让内容在您的 Meta Space 中显示,则需要您在提交完成后返回 CMS 进行发布。', + '当您提交文章并选择在 IPFS 直接存证您的元数据时,会最大可能减少中间人攻击的可能性。若要让内容在您的 Meta Space 中显示,则需要您在提交完成后返回 CMS 进行发布。', 'editor.originalLink.title': '原文链接', 'editor.originalLink.noOriginalLink': '没有原文链接', 'editor.learn.content': '快速学习 Meta Space 编辑器', diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 849199f..686bb7a 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -21,7 +21,7 @@ export default { 'menu.moreInfo.policy': '政策', 'menu.moreInfo.terms': '条款', 'menu.moreInfo.privacyPolicy': '隐私政策', - 'menu.moreInfo.versions': '版本记录', + 'menu.moreInfo.home': '首页', 'menu.post.create': '创作', 'menu.settings': '设置', }; diff --git a/src/locales/zh-CN/messages.ts b/src/locales/zh-CN/messages.ts index 84b54b8..40d7d6e 100644 --- a/src/locales/zh-CN/messages.ts +++ b/src/locales/zh-CN/messages.ts @@ -39,14 +39,13 @@ export default { 'messages.store.repoNameAlreadyExists': '该存储仓库已存在,请设定一个新的名称', 'messages.store.form.repoName': '请设定存储仓库的名称', 'messages.store.form.repoNameAvailable': '可以使用该名称!', + 'messages.store.form.sameRepoName': '仓库名称不可以重复,请重新设定名称', 'messages.invitation.title': '邀请码管理', 'messages.invitation.description': '在这里管理你拥有的邀请码。\n你可以在下方编辑邀请信息,定制发送后对方会接受的信息。', - 'messages.invitation.cardTitle': '邀请码 #{id}', 'messages.info.updateSuccess': '信息更新完成', 'messages.info.updateFailed': '信息更新失败', 'messages.info.copySuccess': '复制成功', - 'messages.invitation.cardLabel': '邀请代码', 'messages.invitation.editInfoHere': '可以在此处编辑信息', 'messages.invitation.inviteeLabel': '受邀人', 'messages.invitation.inviteeName': '受邀人的称呼', @@ -161,7 +160,6 @@ export default { 'messages.editor.submit.generateKey.fail': '生成失败', 'messages.editor.submit.bindStorage': '请绑定 GitHub', 'messages.editor.submit.uploadMetadata.fail': '上传 metadata 失败, 请重试!', - 'messages.invitation.card.title': '邀请码 #{codeId}', 'messages.delete.success': '删除成功', 'messages.delete.fail': '删除失败', 'messages.dashboard.lastPublishDate': '上次发布时间:{time, time, ::yyyyMMddHHmmss}', @@ -170,4 +168,6 @@ export default { 'messages.dashboard.publishing': '正在发布中...', 'messages.dashboard.submitted': '已提交', 'messages.dashboard.settings': '设置项', + 'messages.invitation.inviteMessage': + 'Hi,Meta Network原住民正在邀请您来做邻居,快去 https://home.metanetwork.online,通过您的邀请码来入驻吧!', }; diff --git a/src/pages/Invitation.less b/src/pages/Invitation.less new file mode 100644 index 0000000..1fd8942 --- /dev/null +++ b/src/pages/Invitation.less @@ -0,0 +1,7 @@ +.lessMargin > form > div { + margin: 1vh 0; +} + +.flexedListElement { + margin: 0 1vw; +} diff --git a/src/pages/Invitation.tsx b/src/pages/Invitation.tsx index f1ba549..d44a4a9 100644 --- a/src/pages/Invitation.tsx +++ b/src/pages/Invitation.tsx @@ -1,14 +1,16 @@ import { useIntl, useRequest } from 'umi'; +import { List, notification } from 'antd'; import { CopyOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; -import { List, Badge, Card, Divider, notification } from 'antd'; -import ProForm, { ProFormText } from '@ant-design/pro-form'; +import ProFrom, { ProFormText } from '@ant-design/pro-form'; +import { queryInvitations } from '@/services/api/meta-ucenter'; import FormattedDescription from '@/components/FormattedDescription'; -import { queryInvitations, updateInvitation } from '@/services/api/meta-ucenter'; + +import styles from './Invitation.less'; export default () => { - const intl = useIntl(); const { data, loading } = useRequest(() => queryInvitations()); + const intl = useIntl(); const list = data || []; return ( @@ -18,110 +20,48 @@ export default () => { content={} > { - const isUnused: boolean = item.invitee_user_id === 0; - return ( - - + null, + }} > - - { - const body = { - sub: values.sub, - message: values.message, - } as GLOBAL.InvitationInfo; - const result = await updateInvitation(item.signature, body); - if (result.message === 'ok') { - notification.success({ - message: intl.formatMessage({ id: 'messages.info.updateSuccess' }), - }); - } else { - notification.error({ - message: intl.formatMessage({ id: 'messages.info.updateFailed' }), - description: result.message, - }); - } - }} - > - { - const sigArea = document.querySelector( - `#signature_${item.id}`, - ) as HTMLInputElement; - sigArea?.select(); - document.execCommand('copy'); - notification.success({ - message: intl.formatMessage({ id: 'messages.info.copySuccess' }), - }); - }} - > - - - ) : ( - - ), - }} - initialValue={item.signature} - label={intl.formatMessage({ id: 'messages.invitation.cardLabel' })} - width="md" - name="signature" - disabled={!isUnused} - /> - -

{intl.formatMessage({ id: 'messages.invitation.editInfoHere' })}

- - -
-
-
+ { + await navigator.clipboard.writeText( + intl.formatMessage({ id: 'messages.invitation.inviteMessage' }) + + item.signature, + ); + notification.success({ + message: intl.formatMessage({ id: 'messages.info.copySuccess' }), + }); + }} + > + + + ), + }} + initialValue={item.signature} + wrapperCol={{ span: 24 }} + width="md" + name="signature" + disabled={item.invitee_user_id !== 0} // invitation code used + /> + + + {intl.formatMessage({ id: 'component.status.used' })} +
); }} diff --git a/src/pages/Settings/components/DeleteLocalDraft/index.less b/src/pages/Settings/components/DeleteLocalDraft/index.less index f134448..e125135 100644 --- a/src/pages/Settings/components/DeleteLocalDraft/index.less +++ b/src/pages/Settings/components/DeleteLocalDraft/index.less @@ -1,5 +1,5 @@ .list { - max-width: 600px; + max-width: 700px; :global { .ant-list-item-meta-avatar .anticon { font-size: 22px; diff --git a/src/pages/Settings/components/DeleteLocalDraft/index.tsx b/src/pages/Settings/components/DeleteLocalDraft/index.tsx index ba8a464..d591946 100644 --- a/src/pages/Settings/components/DeleteLocalDraft/index.tsx +++ b/src/pages/Settings/components/DeleteLocalDraft/index.tsx @@ -3,29 +3,36 @@ import { useIntl, useModel } from 'umi'; import { Typography, Button, Popconfirm, message, Space, Input, List, Dropdown } from 'antd'; import { DeleteOutlined, CloudSyncOutlined } from '@ant-design/icons'; import { dbPostsDeleteAll, dbMetadatasDeleteAll } from '@/db/db'; -import { fetchGunDraftsAndUpdateLocal, deleteDraft, twoWaySyncDrafts } from '@/utils/gun'; -import { storeGet, storeSet } from '@/utils/store'; -import { KEY_META_CMS_GUN_SEED, KEY_META_CMS_GUN_PAIR } from '../../../../../config'; +import { + fetchGunDraftsAndUpdateLocal, + deleteDraft, + twoWaySyncDrafts, + generateSeedAndPair, + getSeedAndPair, + saveSeedAndPair, +} from '@/utils/gun'; import styles from './index.less'; import type { KeyPair } from '@metaio/meta-signature-util'; import { generateKeys } from '@metaio/meta-signature-util'; +import { useMount } from 'ahooks'; const { Text } = Typography; -const ImportSeedAndPairComponents = () => { +interface ImportSeedAndPairComponentsState { + getSeedAndPairFn: () => void; +} + +const ImportSeedAndPairComponents: React.FC = ({ + getSeedAndPairFn, +}) => { const [seedAndPairInput, setSeedAndPairInput] = useState(''); // handle import - const handleImport = useCallback(() => { - if (!seedAndPairInput) { - return; - } - - const [seed, pair] = JSON.parse(seedAndPairInput); - storeSet(KEY_META_CMS_GUN_SEED, seed); - storeSet(KEY_META_CMS_GUN_PAIR, pair); + const handleImport = useCallback(async () => { + await saveSeedAndPair(seedAndPairInput); + getSeedAndPairFn(); message.success('导入成功'); - }, [seedAndPairInput]); + }, [seedAndPairInput, getSeedAndPairFn]); return ( setSeedAndPairInput(e.target.value)} value={seedAndPairInput} /> @@ -37,7 +44,8 @@ const ImportSeedAndPairComponents = () => { export default () => { const intl = useIntl(); const { initialState } = useModel('@@initialState'); - const [syncDraftsLoading, setSyncDraftsLoading] = useState(false); + const [syncDraftsLoading, setSyncDraftsLoading] = useState(false); + const [seedAndPair, setSeedAndPair] = useState(''); /** * handle delete all local draft @@ -75,27 +83,38 @@ export default () => { } setSyncDraftsLoading(true); - await twoWaySyncDrafts(initialState.currentUser); - setSyncDraftsLoading(false); + try { + await twoWaySyncDrafts(initialState.currentUser); + } catch (e) { + console.error('e', e); + } finally { + setSyncDraftsLoading(false); + } }, [initialState]); // sync seed and pair - const seedAndPair = useMemo(() => { - // TODO: 复制出来的格式并不好看,可以考虑加密成一串字符然后导入再解密 待考虑 - const seed = storeGet(KEY_META_CMS_GUN_SEED); - const pair = storeGet(KEY_META_CMS_GUN_PAIR); - return JSON.stringify([seed, pair]); + const getSeedAndPairFn = useCallback(() => { + const result = getSeedAndPair(); + setSeedAndPair(result); }, []); // sync seed public key const seedPublicKey = useMemo(() => { - const seed = JSON.parse(storeGet(KEY_META_CMS_GUN_SEED) || '[]'); - if (!seed.length) { + const _seedAndPair = JSON.parse(seedAndPair || '[]'); + if (!_seedAndPair.length) { return ''; } + const seed = JSON.parse(_seedAndPair[0] || '[]'); const keys: KeyPair = generateKeys(seed); return keys.public; - }, []); + }, [seedAndPair]); + + // generate seed pair fn + const generateSeedAndPairFn = useCallback(async () => { + await generateSeedAndPair(); + getSeedAndPairFn(); + message.success('生成成功'); + }, [getSeedAndPairFn]); const list = useMemo( () => [ @@ -134,15 +153,15 @@ export default () => { {seedPublicKey.slice(0, 6)}****{seedPublicKey.slice(-4)} , - } trigger={['click']}> + } + trigger={['click']} + > , { 同步 , + + + , ], }, ], [ handleDeleteAllLocalDraft, twoWaySyncDraftsFn, + generateSeedAndPairFn, + getSeedAndPairFn, intl, seedAndPair, seedPublicKey, @@ -167,6 +200,10 @@ export default () => { ], ); + useMount(() => { + getSeedAndPairFn(); + }); + return ( { return ( } > diff --git a/src/pages/content/SyncCenter.tsx b/src/pages/content/SyncCenter.tsx index 610474c..b50e351 100644 --- a/src/pages/content/SyncCenter.tsx +++ b/src/pages/content/SyncCenter.tsx @@ -5,6 +5,7 @@ import { fetchPostSync, getSourceStatus, publishPosts, + decryptMatatakiPost, } from '@/services/api/meta-cms'; import { useIntl, useModel, history } from 'umi'; import ProTable from '@ant-design/pro-table'; @@ -130,7 +131,11 @@ export default () => { } try { - const postResult: { content: string } = await fetchIpfs(_post.source); + let postResult = await fetchIpfs(_post.source); + if (!postResult.content && postResult.iv && postResult.encryptedData) { + postResult = await decryptMatatakiPost(postResult.iv, postResult.encryptedData); + } + if (!postResult.content) { message.success(intl.formatMessage({ id: 'messages.syncCenter.getContentFail' })); return; diff --git a/src/pages/content/drafts/Edit.less b/src/pages/content/drafts/Edit.less index 808c5a8..48f1944 100644 --- a/src/pages/content/drafts/Edit.less +++ b/src/pages/content/drafts/Edit.less @@ -15,10 +15,10 @@ .edit { box-sizing: border-box; - max-width: 1020px; + max-width: 2080px; margin: 0 auto; - padding: 100px 10px 0 10px; + padding: 100px 40px 0 40px; @media screen and (max-width: 1420px) { - padding-top: 35px; + // padding-top: 35px; } } diff --git a/src/pages/content/drafts/Edit.tsx b/src/pages/content/drafts/Edit.tsx index 17d9883..1deed72 100644 --- a/src/pages/content/drafts/Edit.tsx +++ b/src/pages/content/drafts/Edit.tsx @@ -14,7 +14,6 @@ import { dbMetadatasAdd, MetadataTempData, } from '@/db/db'; -import type { Posts } from '@/db/Posts.d'; import { imageUploadByUrlAPI, getDefaultSiteConfigAPI } from '@/helpers'; import { assign, cloneDeep } from 'lodash'; // import type Vditor from 'vditor'; @@ -52,7 +51,7 @@ import { storeGet } from '@/utils/store'; const Edit: React.FC = () => { const intl = useIntl(); - const [postData, setPostData] = useState({} as Posts); + const [postData, setPostData] = useState({} as PostType.Posts); const [cover, setCover] = useState(''); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); @@ -108,7 +107,7 @@ const Edit: React.FC = () => { // 解密 const msg = await Gun.SEA.verify(data, pair.pub); - const gunDraft = (await Gun.SEA.decrypt(msg, pair)) as Posts; + const gunDraft = (await Gun.SEA.decrypt(msg, pair)) as PostType.Posts; // 如果文章变动 if ( diff --git a/src/pages/content/drafts/List.tsx b/src/pages/content/drafts/List.tsx index 4ae10d0..d6c1aa6 100644 --- a/src/pages/content/drafts/List.tsx +++ b/src/pages/content/drafts/List.tsx @@ -1,16 +1,15 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { history, useIntl, useModel } from 'umi'; import { PageContainer } from '@ant-design/pro-layout'; import { Table, Tag, Button, Image, Space, Popconfirm, message } from 'antd'; import { dbPostsUpdate, dbMetadatasUpdateByPostId } from '@/db/db'; -import type { Posts } from '@/db/Posts'; import { strSlice } from '@/utils'; import { fetchGunDraftsAndUpdateLocal, deleteDraft } from '@/utils/gun'; -import type { GunDraft } from '@/utils/gun'; +import moment from 'moment'; export default () => { const intl = useIntl(); - const [postsList, setPostsList] = useState([]); + const [postsList, setPostsList] = useState([]); const { initialState } = useModel('@@initialState'); /** handle delete */ @@ -39,124 +38,129 @@ export default () => { const fetchPosts = useCallback(async () => { if (initialState?.currentUser) { const response = await fetchGunDraftsAndUpdateLocal(initialState.currentUser); - setPostsList(response); + const responseSort = response.sort((a, b) => + Number(moment(a.updatedAt).isBefore(b.updatedAt)), + ); + setPostsList(responseSort); } }, [initialState]); - const columns = [ - { - title: 'COVER', - dataIndex: 'cover', - key: 'cover', - width: 100, - render: (val: string) => ( - <> - {val ? ( - e.stopPropagation()} - width={100} - height={50} - src={val} - style={{ objectFit: 'cover' }} - /> - ) : ( -
- )} - - ), - }, - { - title: 'TITLE', - dataIndex: 'title', - key: 'title', - render: (val: string) => {val}, - }, - { - title: 'SUMMARY', - dataIndex: 'summary', - key: 'summary', - render: (val: string) => {strSlice(val, 40)}, - }, - { - title: 'STATUS', - dataIndex: 'status', - key: 'status', - width: 100, - render: (_: any, record: Posts) => ( - - {record.post - ? record.post.state === 'drafted' - ? intl.formatMessage({ - id: 'posts.table.status.cloudDraft', - }) - : record.post.state === 'pending' - ? intl.formatMessage({ - id: 'posts.table.status.pending', - }) - : record.post.state === 'published' - ? intl.formatMessage({ - id: 'posts.table.status.published', - }) + const columns = useMemo(() => { + return [ + { + title: 'COVER', + dataIndex: 'cover', + key: 'cover', + width: 100, + render: (val: string) => ( + <> + {val ? ( + e.stopPropagation()} + width={100} + height={50} + src={val} + style={{ objectFit: 'cover' }} + /> + ) : ( +
+ )} + + ), + }, + { + title: 'TITLE', + dataIndex: 'title', + key: 'title', + render: (val: string) => {val}, + }, + { + title: 'SUMMARY', + dataIndex: 'summary', + key: 'summary', + render: (val: string) => {strSlice(val, 40)}, + }, + { + title: 'STATUS', + dataIndex: 'status', + key: 'status', + width: 100, + render: (_: any, record: PostType.Posts) => ( + + {record.post + ? record.post.state === 'drafted' + ? intl.formatMessage({ + id: 'posts.table.status.cloudDraft', + }) + : record.post.state === 'pending' + ? intl.formatMessage({ + id: 'posts.table.status.pending', + }) + : record.post.state === 'published' + ? intl.formatMessage({ + id: 'posts.table.status.published', + }) + : intl.formatMessage({ + id: 'posts.table.status.localDraft', + }) : intl.formatMessage({ id: 'posts.table.status.localDraft', - }) - : intl.formatMessage({ - id: 'posts.table.status.localDraft', + })} + + ), + }, + { + title: 'ACTION', + dataIndex: 'status', + key: 'status', + width: 180, + render: (val: string, record: GunType.GunDraft) => ( + + {val === 'pending' ? ( + + ) : null} + - ), - }, - { - title: 'ACTION', - dataIndex: 'status', - key: 'status', - width: 180, - render: (val: string, record: GunDraft) => ( - - {val === 'pending' ? ( - - ) : null} - { - e?.stopPropagation(); - console.log(record); - await handleDelete(Number(record.id), record?.key); - await fetchPosts(); - }} - onCancel={(e) => e?.stopPropagation()} - okText={intl.formatMessage({ - id: 'component.button.yes', - })} - cancelText={intl.formatMessage({ - id: 'component.button.no', - })} - > - - - - ), - }, - ]; + > + + + + ), + }, + ]; + }, [fetchPosts, handleDelete, intl]); useEffect(() => { fetchPosts(); @@ -177,7 +181,7 @@ export default () => { } > String(record.id)} + rowKey={(record: PostType.Posts) => `${String(record.id)}}`} onRow={() => { return {}; }} diff --git a/src/pages/manage/Source/index.tsx b/src/pages/manage/Source/index.tsx index 7858ba5..842ac77 100644 --- a/src/pages/manage/Source/index.tsx +++ b/src/pages/manage/Source/index.tsx @@ -1,17 +1,19 @@ -import FormattedDescription from '@/components/FormattedDescription'; +import { useIntl } from 'umi'; +import { useEffect, useState } from 'react'; +import { Button, List, message, Spin, Tag } from 'antd'; +import { GridContent, PageContainer } from '@ant-design/pro-layout'; import { getSourceStatus } from '@/services/api/meta-cms'; -import { deleteSourcePlatformToken } from '@/services/api/meta-ucenter'; import syncPostsRequest from '@/utils/sync-posts-request'; -import { GridContent, PageContainer } from '@ant-design/pro-layout'; -import { Button, List, message, Spin, Tag } from 'antd'; -import { useEffect, useState } from 'react'; -import { useIntl } from 'umi'; +import FormattedDescription from '@/components/FormattedDescription'; +import { deleteSourcePlatformToken } from '@/services/api/meta-ucenter'; import styles from './index.less'; -const status: GLOBAL.SourcePlatforms = { +/* The default status for every platform */ +const status: GLOBAL.SourcePlatformStatus = { matataki: { name: 'matataki', active: false, + username: '', }, }; @@ -20,9 +22,40 @@ export default () => { const [pageLoading, setPageLoading] = useState(true); const [syncLoading, setSyncLoading] = useState(false); const [unbindLoading, setUnbindLoading] = useState(false); - const [sourceStatus, setSourceStatus] = useState(status); + const [sourceStatus, setSourceStatus] = useState(status); + + const getStatus = (platform: GLOBAL.SourcePlatformStatusProperties) => + platform.active ? ( + + {intl.formatMessage({ id: 'component.status.alreadyBound' })} + + ) : ( + + {intl.formatMessage({ id: 'component.status.notBound' })} + + ); + + const loadSourceStatus = async () => { + const result = await getSourceStatus(); + setSourceStatus((source: GLOBAL.SourcePlatformStatus) => { + const newStatus = { ...source }; + result.data.forEach((service: CMS.SourceStatusResponse) => { + if (newStatus[service.platform]) { + newStatus[service.platform].active = service.active; + newStatus[service.platform].username = service.username; + } + }); + return newStatus; + }); + setPageLoading(false); + }; + + useEffect(() => { + loadSourceStatus(); + }, []); - const actions = { + /* Actions on page for every platform */ + const actionsOnPage = { matataki: { sync: ( , + , ], }; } @@ -62,7 +62,7 @@ export default () => { , ], actions: [ - , ], @@ -108,9 +108,11 @@ export default () => { rowKey={(record) => record.name} dataSource={stores} renderItem={(store) => { - if (store.actions.length && store.title.length) { + if (store.title.length) { return ( - + // + // TODO: no actions are supported now + (`https://ipfs.io/ipfs/${hash}`, { + return request(`https://ipfs.io/ipfs/${hash}`, { method: 'GET', }); } diff --git a/src/services/api/meta-cms.ts b/src/services/api/meta-cms.ts index 88efbec..7684d80 100644 --- a/src/services/api/meta-cms.ts +++ b/src/services/api/meta-cms.ts @@ -367,3 +367,18 @@ export async function imageUploadByUrl(url: string) { data: { url }, }); } + +/** + * Decrypt restricted matataki post + */ +export async function decryptMatatakiPost(iv: string, encryptedData: string) { + const response = await request>( + `/post/decrypt/matataki`, + { + method: 'POST', + data: { iv, encryptedData }, + }, + ); + + return response.data; +} diff --git a/src/services/typings.d.ts b/src/services/typings.d.ts index d21412c..6bb66e4 100644 --- a/src/services/typings.d.ts +++ b/src/services/typings.d.ts @@ -4,6 +4,7 @@ declare namespace CMS { userId: number; createdAt: Date; updatedAt: Date; + username: string; platform: string; accessToken: string; active: boolean; @@ -333,13 +334,14 @@ declare namespace GLOBAL { }; }; - type SourcePlatformProperties = { + type SourcePlatformStatusProperties = { name: string; + username: string; active: boolean; }; - type SourcePlatforms = { - matataki: SourcePlatformProperties; + type SourcePlatformStatus = { + matataki: SourcePlatformStatusProperties; }; type StoreProvider = 'GitHub' | 'Gitee'; @@ -384,3 +386,14 @@ declare namespace NETWORK { inviterUserId: number; }; } + +declare namespace MATATAKI { + type GenericPostMetadata = { + content?: string; + iv?: string; + encryptedData?: string; + }; + type PostMetadata = { + content: string; + }; +} diff --git a/src/utils/gun.ts b/src/utils/gun.ts index 52d3bcf..55cf7cc 100644 --- a/src/utils/gun.ts +++ b/src/utils/gun.ts @@ -2,7 +2,6 @@ import { assign, cloneDeep } from 'lodash'; import moment from 'moment'; import Gun from 'gun'; import 'gun/sea'; -import type { Posts } from '@/db/Posts.d'; import { dbPostsUpdate, dbPostsAdd, dbPostsAll, dbPostsGet } from '@/db/db'; import { KEY_GUN_ROOT, @@ -15,41 +14,32 @@ import { storeGet, storeSet } from './store'; import { generateSeed, generateKeys } from '@metaio/meta-signature-util'; import type { KeyPair } from '@metaio/meta-signature-util'; -export type GunDraft = Posts & { key?: string }; -type FetchGunDraftsArgs = { - gunDraft: any; - scope: string; - userId: number; -}; -type SyncLocalDraftsArgs = { - drafts: Posts[]; - gunDrafts: GunDraft[]; -}; -type SyncGunDraftsArgs = { - drafts: GunDraft[]; - userId: number; -}; -type SyncNewDraftArgs = { - id: number; - userId: number; -}; -type SyncDraftArgs = { - userId: number; - key: string; - data: any; -}; -type DeleteDraftArgs = { - userId: number; - key: string; -}; - -export const signIn = (gun: any): Promise => { +export const signIn = (gun: any): Promise => { /** * 判断本地是否生成了用户 * 有 用户登录 * 没有 用户注册, 生成 pair */ + // TODO: 后面再来接入 + // const userGun = gun.user().recall({ sessionStorage: true }); + + const createUser = (user: string, pass: string): Promise => { + return new Promise((resolve, reject) => { + gun.user().create(user, pass, (ack: GunType.GunCreateCb) => { + if (ack.err) { + console.error('ack', ack); + reject(ack.err); + } else if (ack.ok === 0 || ack.pub) { + console.log('success'); + resolve('success'); + } else { + reject('未知错误'); + } + }); + }); + }; + return new Promise(async (resolve, reject) => { // pair const gunPair = JSON.parse(storeGet(KEY_META_CMS_GUN_PAIR) || '""'); @@ -57,7 +47,6 @@ export const signIn = (gun: any): Promise => { // } else { const pair = await Gun.SEA.pair(); - storeSet(KEY_META_CMS_GUN_PAIR, JSON.stringify(pair)); } @@ -65,23 +54,38 @@ export const signIn = (gun: any): Promise => { if (gunSeed.length) { const keys: KeyPair = generateKeys(gunSeed); - const user = gun.user().auth(keys.public, keys.private); - if (user.is) { - console.log('success'); - resolve(); - } else { - console.log('fail'); - reject(); - } + gun.user().auth(keys.public, keys.private, (at: GunType.GunAuthCb) => { + if (at.err) { + console.error('at', at); + if (at.err.includes('Wrong user or password.')) { + createUser(keys.public, keys.private) + .then((response) => { + resolve(response); + }) + .catch((e) => { + reject(e); + }); + } else { + reject(at.err); + } + } else { + console.log('auth success'); + resolve('success'); + } + }); } else { const seed: string[] = generateSeed(); const keys: KeyPair = generateKeys(seed); storeSet(KEY_META_CMS_GUN_SEED, JSON.stringify(seed)); - gun.user().create(keys.public, keys.private, () => { - resolve(); - }); + createUser(keys.public, keys.private) + .then((response) => { + resolve(response); + }) + .catch((e) => { + reject(e); + }); } }); }; @@ -105,7 +109,7 @@ export const fetchGunDrafts = ({ gunDraft, scope, userId, -}: FetchGunDraftsArgs): Promise => { +}: GunType.FetchGunDraftsArgs): Promise => { return new Promise((resolve, reject) => { try { gunDraft.get(scope).once(async (data: any) => { @@ -125,7 +129,7 @@ export const fetchGunDrafts = ({ if (data[key]) { // 解密 const msg = await Gun.SEA.verify(data[key], pair.pub); - const dec = (await Gun.SEA.decrypt(msg, pair)) as GunDraft; + const dec = (await Gun.SEA.decrypt(msg, pair)) as GunType.GunDraft; if (dec) { dec.key = key; @@ -149,7 +153,10 @@ export const fetchGunDrafts = ({ * @param param0 * @returns */ -export const syncLocalDrafts = ({ drafts, gunDrafts }: SyncLocalDraftsArgs): Promise => { +export const syncLocalDrafts = ({ + drafts, + gunDrafts, +}: GunType.SyncLocalDraftsArgs): Promise => { return new Promise(async (resolve, reject) => { try { const _draftsClone = cloneDeep(drafts); @@ -212,7 +219,7 @@ export const syncLocalDrafts = ({ drafts, gunDrafts }: SyncLocalDraftsArgs): Pro * @param param0 * @returns */ -export const syncNewDraft = async ({ id, userId }: SyncNewDraftArgs): Promise => { +export const syncNewDraft = async ({ id, userId }: GunType.SyncNewDraftArgs): Promise => { await signIn((window as any).gun); const userScope = `user_${userId}`; @@ -235,7 +242,7 @@ export const syncNewDraft = async ({ id, userId }: SyncNewDraftArgs): Promise => { +export const syncDraft = async ({ userId, key, data }: GunType.SyncDraftArgs): Promise => { await signIn((window as any).gun); const userScope = `user_${userId}`; @@ -258,7 +265,7 @@ export const syncDraft = async ({ userId, key, data }: SyncDraftArgs): Promise => { +export const syncGunDrafts = ({ drafts, userId }: GunType.SyncGunDraftsArgs): Promise => { return new Promise(async (resolve) => { const draftsClone = cloneDeep(drafts); for (let i = 0; i < draftsClone.length; i++) { @@ -286,7 +293,7 @@ export const syncGunDrafts = ({ drafts, userId }: SyncGunDraftsArgs): Promise => { +): Promise => { /** * 获取所有本地文章 * 获取 gun.js 所有文章 @@ -318,7 +325,7 @@ export const fetchGunDraftsAndUpdateLocal = async ( return allDrafts; }; -export const twoWaySyncDrafts = async (user: GLOBAL.CurrentUser): Promise => { +export const twoWaySyncDrafts = async (user: GLOBAL.CurrentUser): Promise => { /** * 获取所有本地文章 * 获取 gun.js 所有文章 @@ -361,10 +368,51 @@ export const twoWaySyncDrafts = async (user: GLOBAL.CurrentUser): Promise { +export const deleteDraft = async ({ userId, key }: GunType.DeleteDraftArgs) => { await signIn((window as any).gun); const userScope = `user_${userId}`; const _gun = (window as any).gun.user().get(KEY_GUN_ROOT).get(KEY_GUN_ROOT_DRAFT); _gun.get(userScope).get(key).put(null); }; + +/** + * generate seed pair + */ +export const generateSeedAndPair = async () => { + const seed: string[] = generateSeed(); + const pair = await Gun.SEA.pair(); + + storeSet(KEY_META_CMS_GUN_SEED, JSON.stringify(seed)); + storeSet(KEY_META_CMS_GUN_PAIR, JSON.stringify(pair)); + + sessionStorage.clear(); + await signIn((window as any).gun); +}; + +/** + * get seed pair + * @returns + */ +export const getSeedAndPair = () => { + // TODO: 复制出来的格式并不好看,可以考虑加密成一串字符然后导入再解密 待考虑 + const seed = storeGet(KEY_META_CMS_GUN_SEED); + const pair = storeGet(KEY_META_CMS_GUN_PAIR); + return JSON.stringify([seed, pair]); +}; + +/** + * save seed pair + */ +export const saveSeedAndPair = async (seedAndPair: string) => { + if (!seedAndPair) { + return; + } + + const [seed, pair] = JSON.parse(seedAndPair); + storeSet(KEY_META_CMS_GUN_SEED, seed); + storeSet(KEY_META_CMS_GUN_PAIR, pair); + + sessionStorage.clear(); + await signIn((window as any).gun); +};