fix: open duplicate entrance and fixed some issues (#6216)

This commit is contained in:
Kilu.He 2024-09-07 13:35:51 +08:00 committed by GitHub
parent 2b9b8c19a9
commit a1793b53dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 574 additions and 291 deletions

View file

@ -128,7 +128,7 @@ const createServer = async (req: Request) => {
try {
if (metaData && metaData.view) {
const view = metaData.view;
const emoji = view.icon.value;
const emoji = view.icon?.ty === 0 && view.icon?.value;
const titleList = [];
if (emoji) {

View file

@ -74,6 +74,7 @@
"react-datepicker": "^4.23.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.52.2",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
@ -129,6 +130,7 @@
"@types/react-custom-scrollbars": "^4.0.13",
"@types/react-datepicker": "^4.19.3",
"@types/react-dom": "^18.2.22",
"@types/react-helmet": "^6.1.11",
"@types/react-katex": "^3.0.0",
"@types/react-measure": "^2.0.12",
"@types/react-transition-group": "^4.4.6",

View file

@ -155,6 +155,9 @@ dependencies:
react-error-boundary:
specifier: ^4.0.13
version: 4.0.13(react@18.2.0)
react-helmet:
specifier: ^6.1.0
version: 6.1.0(react@18.2.0)
react-hook-form:
specifier: ^7.52.2
version: 7.52.2(react@18.2.0)
@ -316,6 +319,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18.2.22
version: 18.2.22
'@types/react-helmet':
specifier: ^6.1.11
version: 6.1.11
'@types/react-katex':
specifier: ^3.0.0
version: 3.0.0
@ -4216,6 +4222,12 @@ packages:
dependencies:
'@types/react': 18.2.66
/@types/react-helmet@6.1.11:
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
dependencies:
'@types/react': 18.2.66
dev: true
/@types/react-katex@3.0.0:
resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==}
dependencies:
@ -9541,6 +9553,18 @@ packages:
/react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
/react-helmet@6.1.0(react@18.2.0):
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
peerDependencies:
react: '>=16.3.0'
dependencies:
object-assign: 4.1.1
prop-types: 15.8.1
react: 18.2.0
react-fast-compare: 3.2.2
react-side-effect: 2.1.2(react@18.2.0)
dev: false
/react-hook-form@7.52.2(react@18.2.0):
resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==}
engines: {node: '>=18.0.0'}
@ -9747,6 +9771,14 @@ packages:
react: 18.2.0
dev: false
/react-side-effect@2.1.2(react@18.2.0):
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-swipeable-views-core@0.14.0:
resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==}
engines: {node: '>=6.0.0'}

View file

@ -4,6 +4,7 @@ import { ViewMeta } from '@/application/db/tables/view_metas';
import { View } from '@/application/types';
import { useService } from '@/components/app/app.hooks';
import { notify } from '@/components/_shared/notify';
import { findView } from '@/components/publish/header/utils';
import { useLiveQuery } from 'dexie-react-hooks';
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@ -33,11 +34,6 @@ export const PublishProvider = ({
publishName: string;
isTemplateThumb?: boolean;
}) => {
const viewMeta = useLiveQuery(async () => {
const name = `${namespace}_${publishName}`;
return db.view_metas.get(name);
}, [namespace, publishName]);
const [outline, setOutline] = useState<View>();
@ -49,6 +45,19 @@ export const PublishProvider = ({
};
}, []);
const viewMeta = useLiveQuery(async () => {
const name = `${namespace}_${publishName}`;
const view = await db.view_metas.get(name);
if (!view) return;
return {
...view,
name: findView(outline?.children || [], view.view_id)?.name || view.name,
};
}, [namespace, publishName, outline]);
useEffect(() => {
db.view_metas.hook('creating', (primaryKey, obj) => {
const subscriber = subscribers.get(primaryKey);
@ -87,7 +96,7 @@ export const PublishProvider = ({
const res = await service?.getPublishInfo(viewId);
if (!res) {
throw new Error('Not found');
throw new Error('View has not been published yet');
}
const { namespace: viewNamespace, publishName } = res;
@ -106,7 +115,6 @@ export const PublishProvider = ({
const loadOutline = useCallback(async () => {
if (!service || !namespace) return;
console.log('loadOutline', namespace);
try {
const res = await service?.getPublishOutline(namespace);

View file

@ -39,8 +39,13 @@ export interface DuplicatePublishView {
viewId: string;
}
export enum ViewIconType {
Emoji = 0,
Icon = 1,
}
export interface ViewIcon {
ty: number;
ty: ViewIconType;
value: string;
}

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.5C11.59 1.5 14.5 4.41 14.5 8C14.5 11.59 11.59 14.5 8 14.5C4.41 14.5 1.5 11.59 1.5 8C1.5 4.41 4.41 1.5 8 1.5ZM7.5 8.5L5.51067 8.50033C5.52467 8.82033 5.55167 9.13167 5.59067 9.43267L5.62167 9.656L5.66033 9.89433C5.969 11.6583 6.68733 12.984 7.50033 13.3787L7.5 8.5ZM10.4893 8.50033L8.5 8.5V13.3787C9.29467 12.9923 9.999 11.7167 10.3183 10.0113L10.3397 9.89433L10.3783 9.65633C10.4357 9.27323 10.4728 8.88736 10.4893 8.50033ZM4.50967 8.50033H2.52233C2.70433 10.517 3.97533 12.2203 5.74267 13.017C5.397 12.4813 5.112 11.823 4.90267 11.079L4.85533 10.9057L4.80033 10.6843L4.74967 10.4583C4.61437 9.81364 4.53408 9.15862 4.50967 8.50033ZM13.4777 8.50033H11.49C11.4679 9.08847 11.4017 9.67411 11.292 10.2523L11.25 10.4583L11.1993 10.6843L11.1443 10.906C10.931 11.7207 10.6293 12.4403 10.257 13.017C12.0243 12.2203 13.2953 10.517 13.477 8.50033H13.4777ZM5.74267 2.98267L5.698 3.003C3.95333 3.80833 2.70267 5.5 2.52233 7.5H4.50967C4.53367 6.89 4.60133 6.30167 4.70767 5.74767L4.74967 5.54167L4.80033 5.31567L4.85533 5.094C5.06867 4.27933 5.37033 3.55967 5.74267 2.983V2.98267ZM7.5 2.62133C6.70767 3.006 6.005 4.276 5.68433 5.97433L5.66033 6.10567L5.62167 6.34367C5.56424 6.72688 5.52719 7.11286 5.51067 7.5H7.5V2.62133ZM10.2573 2.983L10.2973 3.04633C10.624 3.56867 10.8947 4.20233 11.0957 4.91433L11.1447 5.09433L11.1997 5.31567L11.2503 5.54167C11.381 6.155 11.4633 6.81367 11.4903 7.5H13.4777C13.296 5.483 12.025 3.77967 10.2573 2.98333V2.983ZM8.5 2.62133V7.5H10.4893C10.4755 7.17927 10.4477 6.8593 10.406 6.541L10.3783 6.34367L10.3397 6.10567C10.031 4.342 9.313 3.01633 8.50033 2.62133H8.5Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -15,7 +15,7 @@ function AppFlowyPower ({
width,
boxShadow: 'var(--bg-footer) 0px -4px 14px 13px',
}}
className={'flex bg-bg-body sticky bottom-[-0.5px] w-full flex-col items-center justify-center'}
className={'flex rounded-[16px] transform-gpu bg-bg-body sticky bottom-[-0.5px] w-full flex-col items-center justify-center'}
>
{divider && <Divider className={'w-full my-0'} />}

View file

@ -97,7 +97,7 @@ function GalleryPreview ({
return (
<Portal container={document.body}>
<div className={'fixed inset-0 bg-black bg-opacity-80 z-50'} onClick={onClose}>
<div className={'fixed inset-0 bg-black bg-opacity-80 z-[1400]'} onClick={onClose}>
<TransformWrapper
initialScale={1}

View file

@ -0,0 +1,59 @@
import { ViewIcon, ViewIconType } from '@/application/types';
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
function ViewHelmet ({
name,
icon,
}: {
name?: string;
icon?: ViewIcon
}) {
useEffect(() => {
const setFavicon = async () => {
try {
let url = '/appflowy.svg';
if (icon && icon.ty === ViewIconType.Emoji && icon.value) {
const emojiCode = icon?.value?.codePointAt(0)?.toString(16); // Convert emoji to hex code
const baseUrl = 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u';
const response = await fetch(`${baseUrl}${emojiCode}.svg`);
const svgText = await response.text();
const blob = new Blob([svgText], { type: 'image/svg+xml' });
url = URL.createObjectURL(blob);
}
const link = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement || document.createElement('link');
link.type = 'image/svg+xml';
link.rel = 'icon';
link.href = url;
document.getElementsByTagName('head')[0].appendChild(link);
} catch (error) {
console.error('Error setting favicon:', error);
}
};
void setFavicon();
return () => {
const link = document.querySelector('link[rel*=\'icon\']');
if (link) {
document.getElementsByTagName('head')[0].removeChild(link);
}
};
}, [icon]);
if (!name) return null;
return (
<Helmet>
<title>{name} | AppFlowy</title>
</Helmet>
);
}
export default ViewHelmet;

View file

@ -1,13 +1,17 @@
import React from 'react';
import { Skeleton, Box } from '@mui/material';
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
export const BreadcrumbsSkeleton = () => {
return (
<Box display="flex" alignItems="center">
<Box display="flex" alignItems="center" gap={1}>
<Skeleton variant="circular" width={20} height={20} />
<Skeleton variant="text" width={60} height={20} />
<Skeleton variant="text" width={20} height={20} sx={{ mx: 1 }} />
<RightIcon className={'h-5 w-5 text-text-caption'} />
<Skeleton variant="circular" width={20} height={20} />
<Skeleton variant="text" width={80} height={20} />
<Skeleton variant="text" width={20} height={20} sx={{ mx: 1 }} />
<RightIcon className={'h-5 w-5 text-text-caption'} />
<Skeleton variant="circular" width={20} height={20} />
<Skeleton variant="text" width={100} height={20} />
</Box>
);

View file

@ -1,20 +1,23 @@
import { FC, useMemo } from 'react';
import { ReactComponent as CircleIcon } from '@/assets/bulleted_list_icon_1.svg';
export interface TagProps {
color?: string;
label?: string;
size?: 'small' | 'medium';
badge?: string;
}
export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
export const Tag: FC<TagProps> = ({ color, size = 'small', label, badge }) => {
const className = useMemo(() => {
const classList = ['rounded-md', 'font-medium', 'leading-[18px]'];
const classList = ['rounded-[8px]', 'font-medium', 'leading-[1.5em]', 'flex items-center gap-1 max-w-full'];
if (color) classList.push(`text-text-title`);
if (size === 'small') classList.push('px-2', 'py-[2px]');
if (size === 'small') classList.push('px-2', 'py-1');
if (size === 'medium') classList.push('px-3', 'py-1');
if (badge) classList.push('pr-4');
return classList.join(' ');
}, [color, size]);
}, [color, size, badge]);
return (
<div
@ -23,7 +26,16 @@ export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
}}
className={className}
>
{label}
{badge &&
<CircleIcon
style={{
color: `var(${badge})`,
}}
className={`w-5 h-5`}
/>
}
<div className={'truncate'}>{label}</div>
</div>
);
};

View file

@ -43,7 +43,7 @@ export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardPro
style={{
minHeight: '38px',
}}
className="relative flex flex-col gap-2 rounded-[8px] border border-line-divider p-3 text-xs"
className="relative shadow-sm flex flex-col gap-2 overflow-hidden rounded-[8px] border border-line-divider p-3 text-xs"
>
{showFields.map((field, index) => {
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;

View file

@ -32,7 +32,7 @@ export const Column = memo(
rowId: row.id,
};
}) || [],
[rows]
[rows],
);
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows });
@ -54,7 +54,7 @@ export const Column = memo(
return <ListItem fieldId={fieldId} onResize={onResizeCallback} item={item} style={style} />;
},
[fieldId, onResize]
[fieldId, onResize],
);
const getItemSize = useCallback(
@ -65,13 +65,18 @@ export const Column = memo(
if (!row) return 0;
return rowHeight(index);
},
[rowHeight, rows]
[rowHeight, rows],
);
const rowCount = rows?.length || 0;
return (
<div key={id} className='column flex w-[230px] flex-col gap-4'>
<div className='column-header flex h-[24px] items-center text-sm font-medium'>{header}</div>
<div key={id} className="column flex w-[230px] flex-col gap-4">
<div
className="column-header flex overflow-hidden items-center gap-2 text-sm font-medium whitespace-nowrap"
>
<div className={'max-w-[180px] w-auto overflow-hidden'}>{header}</div>
<span className={'text-text-caption font-medium'}>{rowCount}</span>
</div>
<div className={'w-full flex-1 overflow-hidden'}>
<AutoSizer>
@ -95,5 +100,5 @@ export const Column = memo(
</div>
);
},
(prev, next) => JSON.stringify(prev) === JSON.stringify(next)
(prev, next) => JSON.stringify(prev) === JSON.stringify(next),
);

View file

@ -1,13 +1,14 @@
import { YjsDatabaseKey } from '@/application/collab.type';
import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs';
import { Tag } from '@/components/_shared/tag';
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
import { SelectOptionBadgeColorMap, SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
import { Tooltip } from '@mui/material';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
export function useRenderColumn(id: string, fieldId: string) {
export function useRenderColumn (id: string, fieldId: string) {
const { field } = useFieldSelector(fieldId);
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
const fieldName = field?.get(YjsDatabaseKey.name) || '';
@ -34,11 +35,19 @@ export function useRenderColumn(id: string, fieldId: string) {
if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) {
const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id);
const label = option?.name || `No ${fieldName}`;
return (
<Tag
label={option?.name || `No ${fieldName}`}
color={option?.color ? SelectOptionColorMap[option?.color] : 'transparent'}
/>
<Tooltip title={label} enterNextDelay={1000} enterDelay={1000}>
<span>
<Tag
label={label}
color={option?.color ? SelectOptionColorMap[option?.color] : 'transparent'}
badge={option?.color ? SelectOptionBadgeColorMap[option?.color] : undefined}
/>
</span>
</Tooltip>
);
}

View file

@ -11,3 +11,16 @@ export const SelectOptionColorMap = {
[SelectOptionColor.Aqua]: '--tint-aqua',
[SelectOptionColor.Blue]: '--tint-blue',
};
export const SelectOptionBadgeColorMap = {
[SelectOptionColor.Purple]: '--badge-purple',
[SelectOptionColor.Pink]: '--badge-pink',
[SelectOptionColor.LightPink]: '--badge-red',
[SelectOptionColor.Orange]: '--badge-orange',
[SelectOptionColor.Yellow]: '--badge-yellow',
[SelectOptionColor.Lime]: '--badge-lime',
[SelectOptionColor.Green]: '--badge-green',
[SelectOptionColor.Aqua]: '--badge-aqua',
[SelectOptionColor.Blue]: '--badge-blue',
};

View file

@ -8,10 +8,15 @@ import {
} from '@/application/database-yjs';
import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { notify } from '@/components/_shared/notify';
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
import React, { useCallback, useContext, useEffect, useState } from 'react';
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
function RelationItems ({ style, cell, fieldId }: {
cell: RelationCell;
fieldId: string;
style?: React.CSSProperties
}) {
const viewId = useContext(DatabaseContext)?.iidIndex;
const { field } = useFieldSelector(fieldId);
const relatedDatabaseId = field ? parseRelationTypeOption(field).database_id : null;
@ -28,7 +33,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
const [rowIds, setRowIds] = useState([] as string[]);
// const navigateToRow = useNavigateToRow();
const navigateToView = useContext(DatabaseContext)?.navigateToView;
useEffect(() => {
if (!viewId) return;
@ -50,7 +55,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
setRowIds(ids.filter((id) => rows?.has(id)));
},
[cell.data]
[cell.data],
);
useEffect(() => {
@ -108,11 +113,18 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
return (
<div
key={rowId}
// onClick={(e) => {
// e.stopPropagation();
// navigateToRow?.(rowId);
// }}
className={'underline'}
onClick={async (e) => {
if (!relatedViewId) return;
e.stopPropagation();
try {
await navigateToView?.(relatedViewId);
// eslint-disable-next-line
} catch (e: any) {
notify.error(e.message);
}
}}
className={`underline ${relatedViewId ? 'cursor-pointer hover:text-content-blue-400' : ''}`}
>
<RelationPrimaryValue fieldId={relatedFieldId} rowDoc={rowDoc} />
</div>

View file

@ -23,8 +23,12 @@ export const Document = ({
viewMeta,
isTemplateThumb,
}: DocumentProps) => {
return (
<div className={'mb-16 flex h-full w-full flex-col items-center min-h-[500px]'}>
<div style={{
minHeight: `calc(100vh - 48px)`,
}} className={'mb-16 flex h-full w-full flex-col items-center'}
>
<ViewMetaPreview {...viewMeta} />
<Suspense fallback={<DocumentSkeleton />}>
<div className={'flex justify-center w-full'}>

View file

@ -100,7 +100,11 @@ export const DatabaseBlock = memo(
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
{children}
</div>
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col`}>
<div
contentEditable={false}
style={style}
className={`container-bg appflowy-scroller overflow-y-auto overflow-x-hidden relative flex w-full flex-col`}
>
{selectedViewId && doc ? (
<>
<Database
@ -151,7 +155,7 @@ export const DatabaseBlock = memo(
</>
);
}),
(prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id
(prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id,
);
export default DatabaseBlock;

View file

@ -42,12 +42,14 @@ function Comment ({ comment }: CommentProps) {
<div className={'flex items-center gap-2'}>
<div className={'flex items-center gap-4'}>
<Avatar {...avatar} className={`h-8 w-8`} />
<div className={'font-semibold'}>{comment.user?.name}</div>
<Tooltip title={comment.user?.name} enterNextDelay={500} enterDelay={1000} placement={'top-start'}>
<div className={'font-semibold max-w-[200px] truncate'}>{comment.user?.name}</div>
</Tooltip>
</div>
<Tooltip title={timeFormat} enterNextDelay={500} enterDelay={1000} placement={'top-start'}>
<div className={'flex items-center gap-2 text-text-caption'}>
<BulletedListIcon className={'h-3 w-3'} />
<div className={'text-sm'}>{time}</div>
<div className={'text-sm whitespace-nowrap'}>{time}</div>
</div>
</Tooltip>
</div>

View file

@ -67,7 +67,7 @@ function LoginProvider ({ redirectTo }: { redirectTo: string }) {
variant={'outlined'}
onClick={() => handleClick(option.value)}
className={
`flex h-[46px] w-[380px] items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium max-sm:w-full text-text-title`
`flex h-[46px] w-full items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium max-sm:w-full text-text-title`
}
>
<option.Icon className={'w-[24px] h-[24px]'} />
@ -90,8 +90,8 @@ function LoginProvider ({ redirectTo }: { redirectTo: string }) {
<Divider className={'flex-1'} />
</Button>}
<Collapse in={expand}>
<div className={'gap-[10px] flex-col flex'}>{options.slice(2).map(renderOption)}</div>
<Collapse className={'w-full'} in={expand}>
<div className={'gap-[10px] w-full flex-col flex'}>{options.slice(2).map(renderOption)}</div>
</Collapse>
</div>
);

View file

@ -1,5 +1,6 @@
import { GetViewRowsMap, LoadView, LoadViewMeta, ViewLayout, YDoc } from '@/application/collab.type';
import { usePublishContext } from '@/application/publish';
import ViewHelmet from '@/components/_shared/helmet/ViewHelmet';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import { Document } from '@/components/document';
import DatabaseView from '@/components/publish/DatabaseView';
@ -60,27 +61,32 @@ function CollabView ({ doc }: CollabViewProps) {
}
return (
<div
style={style}
className={className}
>
<View
doc={doc}
loadViewMeta={loadViewMeta}
getViewRowsMap={getViewRowsMap}
navigateToView={navigateToView}
loadView={loadView}
isTemplateThumb={isTemplateThumb}
viewMeta={{
icon,
cover,
viewId,
name,
layout: layout || ViewLayout.Document,
visibleViewIds: visibleViewIds || [],
}}
/>
</div>
<>
<ViewHelmet icon={icon} name={name} />
<div
style={style}
className={className}
>
<View
doc={doc}
loadViewMeta={loadViewMeta}
getViewRowsMap={getViewRowsMap}
navigateToView={navigateToView}
loadView={loadView}
isTemplateThumb={isTemplateThumb}
viewMeta={{
icon,
cover,
viewId,
name,
layout: layout || ViewLayout.Document,
visibleViewIds: visibleViewIds || [],
}}
/>
</div>
</>
);
}

View file

@ -1,13 +1,14 @@
import { View } from '@/application/types';
import { AFScroller } from '@/components/_shared/scroller';
import BreadcrumbItem, { Crumb } from '@/components/publish/header/BreadcrumbItem';
import BreadcrumbItem from '@/components/publish/header/BreadcrumbItem';
import React, { useMemo } from 'react';
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
export function Breadcrumb({ crumbs }: { crumbs: Crumb[] }) {
export function Breadcrumb ({ crumbs }: { crumbs: View[] }) {
const renderCrumb = useMemo(() => {
return crumbs?.map((crumb, index) => {
const isLast = index === crumbs.length - 1;
const key = crumb.rowId ? `${crumb.viewId}-${crumb.rowId}` : `${crumb.viewId}`;
const key = `${crumb.view_id}-${index}`;
return (
<div className={`${isLast ? 'text-text-title' : 'text-text-caption'} flex items-center gap-2`} key={key}>

View file

@ -1,5 +1,6 @@
import { ViewLayout } from '@/application/collab.type';
import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
import { usePublishContext } from '@/application/publish';
import { View } from '@/application/types';
import { notify } from '@/components/_shared/notify';
import { ViewIcon } from '@/components/_shared/view-icon';
import SpaceIcon from '@/components/publish/header/SpaceIcon';
@ -9,80 +10,66 @@ import { Tooltip } from '@mui/material';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export interface Crumb {
viewId: string;
rowId?: string;
name: string;
icon: string;
layout: ViewLayout;
extra?: string | null;
}
function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) {
const { viewId, icon, name, layout, extra } = crumb;
const extraObj: {
is_space?: boolean;
space_icon?: string;
space_icon_color?: string;
} = useMemo(() => {
try {
return extra ? JSON.parse(extra) : {};
} catch (e) {
return {};
}
}, [extra]);
function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: View; disableClick?: boolean }) {
const { view_id, icon, name, layout, extra, is_published } = crumb;
const { t } = useTranslation();
const onNavigateToView = usePublishContext()?.toView;
const isFlag = useMemo(() => {
return icon ? isFlagEmoji(icon) : false;
return icon ? isFlagEmoji(icon.value) : false;
}, [icon]);
return (
<Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
<div
className={`flex items-center gap-1.5 text-sm ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`}
onClick={async () => {
if (disableClick) return;
try {
await onNavigateToView?.(viewId);
} catch (e) {
if (extraObj.is_space) {
notify.warning(t('publish.spaceHasNotBeenPublished'));
return;
}
notify.warning(t('publish.hasNotBeenPublished'));
}
}}
>
{extraObj && extraObj.is_space ? (
<span
className={'icon h-4 w-4'}
style={{
backgroundColor: extraObj.space_icon_color ? renderColor(extraObj.space_icon_color) : 'rgb(163, 74, 253)',
borderRadius: '4px',
}}
>
<SpaceIcon value={extraObj.space_icon || ''} char={extraObj.space_icon ? undefined : name.slice(0, 1)} />
<div
className={`flex items-center gap-1.5 text-sm ${!disableClick && is_published ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`}
onClick={async () => {
if (disableClick || !is_published) return;
try {
await onNavigateToView?.(view_id);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
notify.error(e.message);
}
}}
>
{extra && extra.is_space ? (
<span
className={'icon h-4 w-4'}
style={{
backgroundColor: extra.space_icon_color ? renderColor(extra.space_icon_color) : 'rgb(163, 74, 253)',
borderRadius: '4px',
}}
>
<SpaceIcon value={extra.space_icon || ''} char={extra.space_icon ? undefined : name.slice(0, 1)} />
</span>
) : (
<span className={`${isFlag ? 'icon' : ''} flex h-5 w-5 items-center justify-center`}>
{icon || <ViewIcon layout={layout} size={'small'} />}
) : (
<span className={`${isFlag ? 'icon' : ''} flex h-5 w-5 items-center justify-center`}>
{icon?.value || <ViewIcon layout={layout} size={'small'} />}
</span>
)}
)}
<Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
<span
className={
'max-w-[250px] overflow-hidden truncate ' +
(!disableClick ? 'hover:text-text-title hover:underline' : 'flex-1')
(!disableClick && is_published ? 'hover:text-text-title hover:underline' : 'flex-1')
}
>
{name || t('menuAppHeader.defaultNewPageName')}
</span>
</div>
</Tooltip>
</Tooltip>
{!is_published && !extra?.is_space && (<Tooltip
disableInteractive
title={extra?.is_space ? t('publish.spaceHasNotBeenPublished') : t('publish.hasNotBeenPublished')}
>
<div
className={'text-text-caption cursor-pointer hover:bg-fill-list-hover rounded h-5 w-5 flex items-center justify-center'}
>
<PublishIcon className={'h-4 w-4'} />
</div>
</Tooltip>)}
</div>
);
}

View file

@ -1,6 +1,8 @@
import { PublishViewInfo } from '@/application/collab.type';
import { usePublishContext } from '@/application/publish';
import { useCurrentUser } from '@/components/app/app.hooks';
import { openOrDownload } from '@/components/publish/header/utils';
import { View } from '@/application/types';
import BreadcrumbSkeleton from '@/components/_shared/skeleton/BreadcrumbSkeleton';
import { findAncestors, findView, openOrDownload } from '@/components/publish/header/utils';
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
import { getPlatform } from '@/utils/platform';
import { Divider, IconButton, Tooltip } from '@mui/material';
@ -26,29 +28,38 @@ export function PublishViewHeader ({
}) {
const { t } = useTranslation();
const viewMeta = usePublishContext()?.viewMeta;
const outline = usePublishContext()?.outline;
const crumbs = useMemo(() => {
const ancestors = viewMeta?.ancestor_views.slice(1) || [];
if (!viewMeta || !outline) return [];
const ancestors = findAncestors(outline.children, viewMeta?.view_id);
return ancestors.map((ancestor) => {
let icon;
if (ancestors) return ancestors;
if (!viewMeta?.ancestor_views) return [];
const parseToView = (ancestor: PublishViewInfo): View => {
let extra = null;
try {
const extra = ancestor?.extra ? JSON.parse(ancestor.extra) : {};
icon = extra.icon?.value || ancestor.icon?.value;
extra = ancestor.extra ? JSON.parse(ancestor.extra) : null;
} catch (e) {
// ignore
// do nothing
}
return {
viewId: ancestor.view_id,
view_id: ancestor.view_id,
name: ancestor.name,
icon: icon,
icon: ancestor.icon,
layout: ancestor.layout,
extra: ancestor.extra,
extra,
is_published: true,
children: [],
};
});
}, [viewMeta]);
};
const currentView = parseToView(viewMeta);
return viewMeta?.ancestor_views.slice(1).map(item => findView(outline.children, item.view_id) || parseToView(item)) || [currentView];
}, [viewMeta, outline]);
const [openPopover, setOpenPopover] = React.useState(false);
const isMobile = useMemo(() => {
return getPlatform().isMobile;
@ -97,10 +108,6 @@ export function PublishViewHeader ({
return debounce(handleOpenPopover, 100);
}, [handleOpenPopover, debounceClosePopover]);
const currentUser = useCurrentUser();
const isAppFlowyUser = currentUser?.email?.endsWith('@appflowy.io');
return (
<div
style={{
@ -108,12 +115,12 @@ export function PublishViewHeader ({
background: 'var(--bg-header)',
height: HEADER_HEIGHT,
}}
className={'appflowy-top-bar sticky top-0 z-10 flex px-5'}
className={'appflowy-top-bar transform-gpu sticky top-0 z-10 flex px-5'}
>
<div className={'flex w-full items-center justify-between gap-4 overflow-hidden'}>
{!openDrawer && (
{!openDrawer && !isMobile && (
<OutlinePopover
{...isMobile ? undefined : {
{...{
onMouseEnter: handleOpenPopover,
onMouseLeave: debounceClosePopover,
}}
@ -122,11 +129,7 @@ export function PublishViewHeader ({
drawerWidth={drawerWidth}
>
<IconButton
{...isMobile ? {
onTouchEnd: () => {
setOpenPopover(prev => !prev);
},
} : {
{...{
onMouseEnter: debounceOpenPopover,
onMouseLeave: debounceClosePopover,
onClick: () => {
@ -136,19 +139,20 @@ export function PublishViewHeader ({
}}
>
<SideOutlined className={'h-4 w-4'} />
<SideOutlined className={'h-4 w-4 text-text-caption'} />
</IconButton>
</OutlinePopover>
)}
<div className={'h-full flex-1 overflow-hidden'}>
<Breadcrumb crumbs={crumbs} />
{!viewMeta ? <div className={'h-[48px] flex items-center'}><BreadcrumbSkeleton /></div> : <Breadcrumb
crumbs={crumbs}
/>}
</div>
<div className={'flex items-center gap-2'}>
<MoreActions />
{isAppFlowyUser && <Duplicate />}
<Duplicate />
<Divider
orientation={'vertical'}
className={'mx-2'}

View file

@ -1,10 +1,12 @@
import React, { useCallback } from 'react';
import { Button } from '@mui/material';
import { getPlatform } from '@/utils/platform';
import React, { useCallback, useMemo } from 'react';
import { Button, IconButton, Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { LoginModal } from '@/components/login';
import { useSearchParams } from 'react-router-dom';
import { useDuplicate } from '@/components/publish/header/duplicate/useDuplicate';
import DuplicateModal from '@/components/publish/header/duplicate/DuplicateModal';
import { ReactComponent as CopyIcon } from '@/assets/copy.svg';
export function Duplicate () {
const { t } = useTranslation();
@ -17,16 +19,31 @@ export function Duplicate () {
});
}, [setSearch]);
const isMobile = useMemo(() => {
return getPlatform().isMobile;
}, []);
return (
<>
<Button
{isMobile ? (
<Tooltip title={t('publish.saveThisPage')}>
<IconButton
onClick={handleClick}
size={'small'}
color={'inherit'}
>
<CopyIcon className={'w-5 h-5'} />
</IconButton>
</Tooltip>
) : <Button
onClick={handleClick}
size={'small'}
variant={'outlined'}
color={'inherit'}
variant={'contained'}
color={'primary'}
>
{t('publish.saveThisPage')}
</Button>
</Button>}
<LoginModal
redirectTo={url}
open={loginOpen}

View file

@ -47,6 +47,8 @@ export function useDuplicate () {
}
export function useLoadWorkspaces () {
const currentUser = useContext(AFConfigContext)?.currentUser;
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated && Boolean(currentUser) || false;
const [spaceLoading, setSpaceLoading] = useState<boolean>(false);
const [workspaceLoading, setWorkspaceLoading] = useState<boolean>(false);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(() => {
@ -61,6 +63,7 @@ export function useLoadWorkspaces () {
const service = useContext(AFConfigContext)?.service;
const loadWorkspaces = useCallback(async () => {
if (!isAuthenticated) return;
setWorkspaceLoading(true);
try {
const workspaces = await service?.getWorkspaces();
@ -80,10 +83,11 @@ export function useLoadWorkspaces () {
} finally {
setWorkspaceLoading(false);
}
}, [service]);
}, [service, isAuthenticated]);
const loadSpaces = useCallback(
async (selectedWorkspaceId: string) => {
if (!isAuthenticated) return;
setSpaceLoading(true);
try {
const folder = await service?.getWorkspaceFolder(selectedWorkspaceId);
@ -113,7 +117,7 @@ export function useLoadWorkspaces () {
setSpaceLoading(false);
}
},
[service],
[service, isAuthenticated],
);
return {

View file

@ -1,99 +1,51 @@
export function openOrDownload() {
const getDeviceType = () => {
const ua = navigator.userAgent;
import { View } from '@/application/types';
import { getOS, openAppOrDownload } from '@/utils/open_schema';
import { iosDownloadLink, androidDownloadLink, desktopDownloadLink, openAppFlowySchema } from '@/utils/url';
if (/(iPad|iPhone|iPod)/g.test(ua)) {
return 'iOS';
} else if (/Android/g.test(ua)) {
return 'Android';
} else {
return 'Desktop';
}
};
export function openOrDownload () {
const os = getOS();
const downloadUrl = os === 'ios' ? iosDownloadLink : os === 'android' ? androidDownloadLink : desktopDownloadLink;
const deviceType = getDeviceType();
const isMobile = deviceType !== 'Desktop';
const getFallbackLink = () => {
if (deviceType === 'iOS') {
return 'https://testflight.apple.com/join/6CexvkDz';
} else if (deviceType === 'Android') {
return 'https://play.google.com/store/apps/details?id=io.appflowy.appflowy';
} else {
return 'https://appflowy.io/download/#pop';
}
};
const getDuration = () => {
switch (deviceType) {
case 'iOS':
return 250;
default:
return 1500;
}
};
const APPFLOWY_SCHEME = 'appflowy-flutter://';
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = APPFLOWY_SCHEME;
const openSchema = () => {
if (isMobile) return (window.location.href = APPFLOWY_SCHEME);
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 1000);
};
const openAppFlowy = () => {
openSchema();
const initialTime = Date.now();
let interactTime = initialTime;
let waitTime = 0;
const duration = getDuration();
const updateInteractTime = () => {
interactTime = Date.now();
};
document.removeEventListener('mousemove', updateInteractTime);
document.removeEventListener('mouseenter', updateInteractTime);
const checkOpen = setInterval(() => {
waitTime = Date.now() - initialTime;
if (waitTime > duration) {
clearInterval(checkOpen);
if (isMobile || Date.now() - interactTime < duration) {
window.open(getFallbackLink(), '_current');
}
}
}, 20);
if (!isMobile) {
document.addEventListener('mouseenter', updateInteractTime);
document.addEventListener('mousemove', updateInteractTime);
}
document.addEventListener('visibilitychange', () => {
const isHidden = document.hidden;
if (isHidden) {
clearInterval(checkOpen);
}
});
window.onpagehide = () => {
clearInterval(checkOpen);
};
window.onbeforeunload = () => {
clearInterval(checkOpen);
};
};
openAppFlowy();
return openAppOrDownload({
appScheme: openAppFlowySchema,
downloadUrl,
});
}
export function findAncestors (data: View[], targetId: string, currentPath: View[] = []): View[] | null {
for (const item of data) {
const newPath = [...currentPath, item];
if (item.view_id === targetId) {
return newPath;
}
if (item.children && item.children.length > 0) {
const result = findAncestors(item.children, targetId, newPath);
if (result) {
return result;
}
}
}
return null;
}
export function findView (data: View[], targetId: string): View | null {
for (const item of data) {
if (item.view_id === targetId) {
return item;
}
if (item.children && item.children.length > 0) {
const result = findView(item.children, targetId);
if (result) {
return result;
}
}
}
return null;
}

View file

@ -36,7 +36,7 @@ export function OutlineDrawer ({ open, width, onClose }: { open: boolean; width:
<div className={'flex h-full relative min-h-full flex-col overflow-y-auto overflow-x-hidden appflowy-scroller'}>
<div style={{
backdropFilter: 'blur(4px)',
}} className={'flex z-10 h-[48px] sticky top-0 items-center justify-between p-4'}
}} className={'flex transform-gpu z-10 h-[48px] sticky top-0 items-center justify-between p-4'}
>
<div
className={'flex cursor-pointer items-center gap-1 text-text-title'}

View file

@ -1,12 +1,13 @@
import { PublishContext, usePublishContext } from '@/application/publish';
import { View } from '@/application/types';
import { notify } from '@/components/_shared/notify';
import { ViewIcon } from '@/components/_shared/view-icon';
import SpaceIcon from '@/components/publish/header/SpaceIcon';
import { renderColor } from '@/utils/color';
import { isFlagEmoji } from '@/utils/emoji';
import { Tooltip } from '@mui/material';
import React, { useCallback, useContext, useEffect } from 'react';
import { ReactComponent as ChevronDownIcon } from '@/assets/chevron_down.svg';
import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
import { useTranslation } from 'react-i18next';
function OutlineItem ({ view, level = 0, width }: { view: View; width: number; level?: number }) {
@ -53,18 +54,9 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
const { t } = useTranslation();
const navigateToView = useContext(PublishContext)?.toView;
const [hovered, setHovered] = React.useState(false);
const toastWarning = useCallback((item: View) => {
if (!item.is_published) {
if (item.extra?.is_space) {
notify.warning(t('publish.spaceHasNotBeenPublished'));
return;
}
notify.warning(t('publish.hasNotBeenPublished'));
}
}, [t]);
const renderItem = (item: View) => {
const renderItem = useCallback((item: View) => {
const { icon, layout, name, view_id, extra } = item;
const isSpace = extra?.is_space;
@ -81,23 +73,26 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
}
>
{item.children?.length ? getIcon() : null}
<div
onClick={async () => {
if (!item.is_published) {
toastWarning(item);
if (isSpace || !item.is_published) {
setIsExpanded(prev => !prev);
return;
}
try {
await navigateToView?.(view_id);
} catch (e) {
toastWarning(item);
// do nothing
}
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
paddingLeft: item.children?.length ? 0 : 1.125 * (level + 1) + 'rem',
}}
className={'flex flex-1 cursor-pointer items-center gap-1.5 overflow-hidden'}
className={`flex flex-1 cursor-pointer select-none items-center gap-1.5 overflow-hidden`}
>
{isSpace && extra ?
<span
@ -116,15 +111,33 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
</div>
}
<div className={'flex-1 truncate'}>{name}</div>
<Tooltip title={name} enterDelay={1000} enterNextDelay={1000}>
<div className={'flex-1 truncate'}>{name}</div>
</Tooltip>
{hovered && !item.is_published && !isSpace && (
<Tooltip
disableInteractive
title={isSpace ? t('publish.spaceHasNotBeenPublished') : t('publish.hasNotBeenPublished')}
>
<div
className={'text-text-caption ml-2 mr-4 cursor-pointer hover:bg-fill-list-hover rounded h-5 w-5 flex items-center justify-center'}
>
<PublishIcon className={'h-4 w-4'} />
</div>
</Tooltip>
)}
</div>
</div>
</div>
);
};
}, [hovered, getIcon, level, navigateToView, selected, t, width]);
const children = view.children || [];
if (!children.length && !view.is_published) {
return null;
}
return (
<div className={'flex h-fit w-full flex-col'}>
{renderItem(view)}

View file

@ -23,13 +23,11 @@ export function OutlinePopover ({
drawerWidth: number;
}) {
const viewMeta = usePublishContext()?.viewMeta;
const content = useMemo(() => {
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={'flex h-fit max-h-[590px] flex-col overflow-y-auto overflow-x-hidden appflowy-scroller'}
>
<Outline width={drawerWidth} />

View file

@ -12,7 +12,7 @@
--icon-disabled: #525A69;
--icon-on-toolbar: white;
--line-border: #59647a;
--line-divider: #252F41;
--line-divider: #384967;
--line-on-toolbar: #99a6b8;
--fill-default: #00bcf0;
--fill-hover: #005174;
@ -43,14 +43,23 @@
--function-success-opacity: rgba(59, 168, 86, 0.1);
--function-info: #2e9dbb;
--tint-red: #56363F;
--badge-red: #D32772;
--tint-green: #3C5133;
--badge-green: #3BA856;
--tint-purple: #4D4078;
--badge-purple: #5749CA;
--tint-blue: #2C3B58;
--badge-blue: #00BCF0;
--tint-yellow: #695E3E;
--badge-yellow: #E9B320;
--tint-pink: #5E3C5E;
--badge-pink: #BB4A97;
--tint-lime: #394027;
--badge-lime: #B6D94C;
--tint-aqua: #1B3849;
--badge-aqua: #00B8E5;
--tint-orange: #5E3C3C;
--badge-orange: #FF6D00;
--shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.3);
--scrollbar-track: #252F41;
--scrollbar-thumb: #3c4557;

View file

@ -46,14 +46,23 @@
--function-success-opacity: rgba(102, 207, 128, 0.1);
--function-info: #00bcf0;
--tint-purple: #e8e0ff;
--badge-purple: #5749ca;
--tint-pink: #ffe7ee;
--badge-pink: #bb4a97;
--tint-red: #ffdddd;
--badge-red: #f44336;
--tint-lime: #f5ffdc;
--badge-lime: #b6d94c;
--tint-green: #ddffd6;
--badge-green: #66cf80;
--tint-aqua: #defff1;
--badge-aqua: #00b8e5;
--tint-blue: #e1fbff;
--badge-blue: #00bcf0;
--tint-orange: #ffefe3;
--badge-orange: #ff8c00;
--tint-yellow: #fff2cd;
--badge-yellow: #ffcc00;
--shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
--scrollbar-thumb: #bdbdbd;
--scrollbar-track: #e5e5e5;

View file

@ -0,0 +1,92 @@
type OS = 'ios' | 'android' | 'other';
interface AppConfig {
appScheme: string;
universalLink?: string;
intentUrl?: string;
downloadUrl: string;
timeout?: number;
}
export const getOS = (): OS => {
const ua = navigator.userAgent;
if (/iPad|iPhone|iPod/.test(ua)) return 'ios';
if (/Android/.test(ua)) return 'android';
return 'other';
};
const isWebView = (): boolean => {
const ua = navigator.userAgent.toLowerCase();
return /(webview|wv)/i.test(ua);
};
const createHiddenIframe = (): HTMLIFrameElement => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
return iframe;
};
const removeIframe = (iframe: HTMLIFrameElement): void => {
document.body.removeChild(iframe);
};
const redirectToUrl = (url: string): void => {
window.location.href = url;
};
export const openAppOrDownload = (config: AppConfig): void => {
const { appScheme, universalLink, intentUrl, downloadUrl, timeout = 3000 } = config;
const os = getOS();
const iframe = createHiddenIframe();
const timer = setTimeout(() => {
removeIframe(iframe);
redirectToUrl(downloadUrl);
}, timeout);
const handleVisibilityChange = (): void => {
if (!document.hidden) {
clearTimeout(timer);
removeIframe(iframe);
redirectToUrl(downloadUrl);
document.removeEventListener('visibilitychange', handleVisibilityChange);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
const openApp = () => {
switch (os) {
case 'ios':
if (isWebView() || !universalLink) {
iframe.src = appScheme;
} else {
redirectToUrl(universalLink);
}
break;
case 'android':
if (isWebView() || !intentUrl) {
iframe.src = appScheme;
} else {
redirectToUrl(intentUrl);
}
break;
default:
iframe.src = appScheme;
}
};
openApp();
iframe.onload = () => {
clearTimeout(timer);
removeIframe(iframe);
redirectToUrl(downloadUrl);
};
};

View file

@ -7,13 +7,18 @@ export const downloadPage = 'https://appflowy.io/download';
export const openAppFlowySchema = 'appflowy-flutter://';
export function isValidUrl(input: string) {
export const iosDownloadLink = 'https://testflight.apple.com/join/6CexvkDz';
export const androidDownloadLink = 'https://play.google.com/store/apps/details?id=io.appflowy.appflowy';
export const desktopDownloadLink = 'https://appflowy.io/download/#pop';
export function isValidUrl (input: string) {
return isURL(input, { require_protocol: true, require_host: false });
}
// Process the URL to make sure it's a valid URL
// If it's not a valid URL(eg: 'appflowy.io' or '192.168.1.2'), we'll add 'https://' to the URL
export function processUrl(input: string) {
export function processUrl (input: string) {
let processedUrl = input;
if (isValidUrl(input)) {
@ -40,7 +45,7 @@ export function processUrl(input: string) {
return;
}
export async function openUrl(url: string, target: string = '_current') {
export async function openUrl (url: string, target: string = '_current') {
const platform = getPlatform();
const newUrl = processUrl(url);

View file

@ -1,7 +1,7 @@
/**
* Do not edit directly
* Generated on Fri, 06 Sep 2024 04:24:51 GMT
* Generated on Fri, 06 Sep 2024 10:13:48 GMT
* Generated from $pnpm css:variables
*/

View file

@ -1,7 +1,7 @@
/**
* Do not edit directly
* Generated on Fri, 06 Sep 2024 04:24:51 GMT
* Generated on Fri, 06 Sep 2024 10:13:48 GMT
* Generated from $pnpm css:variables
*/
@ -76,6 +76,17 @@ module.exports = {
"orange": "var(--tint-orange)",
"yellow": "var(--tint-yellow)"
},
"badge": {
"purple": "var(--badge-purple)",
"pink": "var(--badge-pink)",
"red": "var(--badge-red)",
"lime": "var(--badge-lime)",
"green": "var(--badge-green)",
"aqua": "var(--badge-aqua)",
"blue": "var(--badge-blue)",
"orange": "var(--badge-orange)",
"yellow": "var(--badge-yellow)"
},
"scrollbar": {
"thumb": "var(--scrollbar-thumb)",
"track": "var(--scrollbar-track)"