mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: replace wasm with axios (#5856)
* fix: replace wasm with axios * fix: login redirect * fix: flag emoji on windows
This commit is contained in:
parent
04556252e1
commit
cb60488bbe
@ -54,10 +54,7 @@ http {
|
||||
root /usr/share/nginx/html;
|
||||
expires 30d;
|
||||
access_log off;
|
||||
location ~* \.wasm$ {
|
||||
types { application/wasm wasm; }
|
||||
default_type application/wasm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
location /appflowy.svg {
|
||||
|
@ -68,14 +68,25 @@ const createServer = async (req: Request) => {
|
||||
|
||||
logger.info(`Request URL: ${hostname}${reqUrl.pathname}`);
|
||||
|
||||
if (reqUrl.pathname === '/after-payment') {
|
||||
if (['/after-payment', '/login'].includes(reqUrl.pathname)) {
|
||||
timer();
|
||||
const htmlData = fs.readFileSync(indexPath, 'utf8');
|
||||
const $ = load(htmlData);
|
||||
|
||||
$('title').text('Payment Success | AppFlowy');
|
||||
$('link[rel="icon"]').attr('href', '/appflowy.svg');
|
||||
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', 'Payment success on AppFlowy');
|
||||
let title, description;
|
||||
|
||||
if (reqUrl.pathname === '/after-payment') {
|
||||
title = 'Payment Success | AppFlowy';
|
||||
description = 'Payment success on AppFlowy';
|
||||
}
|
||||
|
||||
if (reqUrl.pathname === '/login') {
|
||||
title = 'Login | AppFlowy';
|
||||
description = 'Login to AppFlowy';
|
||||
}
|
||||
|
||||
if (title) $('title').text(title);
|
||||
if (description) setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description);
|
||||
|
||||
return new Response($.html(), {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
|
@ -24,7 +24,6 @@
|
||||
"coverage": "pnpm run test:unit && pnpm run test:components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appflowyinc/client-api-wasm": "0.1.4",
|
||||
"@atlaskit/primitives": "^5.5.3",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
@ -5,9 +5,6 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@appflowyinc/client-api-wasm':
|
||||
specifier: 0.1.4
|
||||
version: 0.1.4
|
||||
'@atlaskit/primitives':
|
||||
specifier: ^5.5.3
|
||||
version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
|
||||
@ -454,10 +451,6 @@ packages:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
/@appflowyinc/client-api-wasm@0.1.4:
|
||||
resolution: {integrity: sha512-3uBpy3n+aIG0fapPAroMfL8JLdAPtqPAkpV+LOxlRnMW4Au2JQcW8TW0P3K1YAe16tDZ62ZIZPoG6Bi40RDRoQ==}
|
||||
dev: false
|
||||
|
||||
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-iO6+hIp09dF4iAZQarVz3vKY1kM5Ij5CExYcK9jgc2q+OH8nv8n+BPFeJTdzGOGopmbUZn5Opj9pYQvge1Gr4Q==}
|
||||
peerDependencies:
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { expect } from '@jest/globals';
|
||||
import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '../fetch';
|
||||
import { APIService } from '@/application/services/js-services/wasm';
|
||||
import { APIService } from '@/application/services/js-services/http';
|
||||
|
||||
jest.mock('@/application/services/js-services/wasm', () => {
|
||||
jest.mock('@/application/services/js-services/http', () => {
|
||||
return {
|
||||
APIService: {
|
||||
getPublishView: jest.fn(),
|
||||
|
@ -5,7 +5,7 @@ import { fetchViewInfo } from '@/application/services/js-services/fetch';
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import { getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
|
||||
|
||||
jest.mock('@/application/services/js-services/wasm/client_api', () => {
|
||||
jest.mock('@/application/services/js-services/http/http_api', () => {
|
||||
return {
|
||||
initAPIService: jest.fn(),
|
||||
};
|
||||
|
@ -114,7 +114,7 @@ export async function getPublishViewMeta<
|
||||
|
||||
export async function getPublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
data: Uint8Array;
|
||||
rows?: Record<RowId, number[]>;
|
||||
visibleViewIds?: ViewId[];
|
||||
relations?: Record<DatabaseId, ViewId>;
|
||||
@ -219,7 +219,7 @@ export async function revalidatePublishViewMeta<
|
||||
|
||||
export async function revalidatePublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
data: Uint8Array;
|
||||
rows?: Record<RowId, number[]>;
|
||||
visibleViewIds?: ViewId[];
|
||||
relations?: Record<DatabaseId, ViewId>;
|
||||
@ -260,10 +260,9 @@ export async function revalidatePublishView<
|
||||
}
|
||||
}
|
||||
|
||||
console.log('====', rows);
|
||||
const state = new Uint8Array(data);
|
||||
console.log('====', data);
|
||||
|
||||
applyYDoc(collab, state);
|
||||
applyYDoc(collab, data);
|
||||
}
|
||||
|
||||
export async function deleteViewMeta(name: string) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { APIService } from '@/application/services/js-services/wasm';
|
||||
import { APIService } from '@/application/services/js-services/http';
|
||||
|
||||
const pendingRequests = new Map();
|
||||
|
||||
|
@ -0,0 +1,94 @@
|
||||
import { refreshToken as refreshSessionToken } from '@/application/session/token';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
let axiosInstance: AxiosInstance | null = null;
|
||||
|
||||
export function initGrantService(baseURL: string) {
|
||||
if (axiosInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
axiosInstance = axios.create({
|
||||
baseURL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
Object.assign(config.headers, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshToken(refresh_token: string) {
|
||||
const response = await axiosInstance?.post<{
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
refresh_token: string;
|
||||
}>('/token?grant_type=refresh_token', {
|
||||
refresh_token,
|
||||
});
|
||||
|
||||
const newToken = response?.data;
|
||||
|
||||
if (newToken) {
|
||||
refreshSessionToken(JSON.stringify(newToken));
|
||||
}
|
||||
|
||||
return newToken;
|
||||
}
|
||||
|
||||
export async function signInWithMagicLink(email: string, authUrl: string) {
|
||||
const res = await axiosInstance?.post(
|
||||
'/magiclink',
|
||||
{
|
||||
code_challenge: '',
|
||||
code_challenge_method: '',
|
||||
data: {},
|
||||
email,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Redirect_to: authUrl,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
export async function settings() {
|
||||
const res = await axiosInstance?.get('/settings');
|
||||
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
export function signInGoogle(authUrl: string) {
|
||||
const provider = 'google';
|
||||
const redirectTo = encodeURIComponent(authUrl);
|
||||
const accessType = 'offline';
|
||||
const prompt = 'consent';
|
||||
const baseURL = axiosInstance?.defaults.baseURL;
|
||||
const url = `${baseURL}/authorize?provider=${provider}&redirect_to=${redirectTo}&access_type=${accessType}&prompt=${prompt}`;
|
||||
|
||||
window.open(url, '_current');
|
||||
}
|
||||
|
||||
export function signInGithub(authUrl: string) {
|
||||
const provider = 'github';
|
||||
const redirectTo = encodeURIComponent(authUrl);
|
||||
const baseURL = axiosInstance?.defaults.baseURL;
|
||||
const url = `${baseURL}/authorize?provider=${provider}&redirect_to=${redirectTo}`;
|
||||
|
||||
window.open(url, '_current');
|
||||
}
|
||||
|
||||
export function signInDiscord(authUrl: string) {
|
||||
const provider = 'discord';
|
||||
const redirectTo = encodeURIComponent(authUrl);
|
||||
const baseURL = axiosInstance?.defaults.baseURL;
|
||||
const url = `${baseURL}/authorize?provider=${provider}&redirect_to=${redirectTo}`;
|
||||
|
||||
window.open(url, '_current');
|
||||
}
|
@ -0,0 +1,489 @@
|
||||
import { DatabaseId, RowId, ViewId, ViewLayout } from '@/application/collab.type';
|
||||
import { GlobalComment, Reaction } from '@/application/comment.type';
|
||||
import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue';
|
||||
import { blobToBytes } from '@/application/services/js-services/http/utils';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
import { getTokenParsed, invalidToken } from '@/application/session/token';
|
||||
import { FolderView, User, Workspace } from '@/application/types';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export * from './gotrue';
|
||||
|
||||
let axiosInstance: AxiosInstance | null = null;
|
||||
|
||||
export function initAPIService(config: AFCloudConfig) {
|
||||
if (axiosInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
axiosInstance = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
});
|
||||
|
||||
initGrantService(config.gotrueURL);
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = getTokenParsed();
|
||||
|
||||
Object.assign(config.headers, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const isExpired = dayjs().isAfter(dayjs.unix(token.expires_at));
|
||||
|
||||
let access_token = token.access_token;
|
||||
const refresh_token = token.refresh_token;
|
||||
|
||||
if (isExpired) {
|
||||
const newToken = await refreshToken(refresh_token);
|
||||
|
||||
access_token = newToken?.access_token || '';
|
||||
}
|
||||
|
||||
if (access_token) {
|
||||
Object.assign(config.headers, {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(async (response) => {
|
||||
const status = response.status;
|
||||
|
||||
if (status === 401) {
|
||||
const token = getTokenParsed();
|
||||
|
||||
if (!token) {
|
||||
invalidToken();
|
||||
return response;
|
||||
}
|
||||
|
||||
const refresh_token = token.refresh_token;
|
||||
|
||||
try {
|
||||
await refreshToken(refresh_token);
|
||||
} catch (e) {
|
||||
invalidToken();
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
export async function signInWithUrl(url: string) {
|
||||
const hash = new URL(url).hash;
|
||||
|
||||
if (!hash) {
|
||||
return Promise.reject('No hash found');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(hash.slice(1));
|
||||
const refresh_token = params.get('refresh_token');
|
||||
|
||||
if (!refresh_token) {
|
||||
return Promise.reject('No access_token found');
|
||||
}
|
||||
|
||||
await refreshToken(refresh_token);
|
||||
}
|
||||
|
||||
export async function verifyToken(accessToken: string) {
|
||||
const url = `/api/user/verify/${accessToken}`;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
is_new: boolean;
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User> {
|
||||
const url = '/api/user/profile';
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
uid: number;
|
||||
uuid: string;
|
||||
email: string;
|
||||
name: string;
|
||||
metadata: {
|
||||
icon_url: string;
|
||||
};
|
||||
encryption_sign: null;
|
||||
latest_workspace_id: string;
|
||||
updated_at: number;
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
const { uid, uuid, email, name, metadata } = data.data;
|
||||
|
||||
return {
|
||||
uid: String(uid),
|
||||
uuid,
|
||||
email,
|
||||
name,
|
||||
avatar: metadata.icon_url,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function getPublishViewMeta(namespace: string, publishName: string) {
|
||||
const url = `/api/workspace/published/${namespace}/${publishName}`;
|
||||
const response = await axiosInstance?.get(url);
|
||||
|
||||
return response?.data;
|
||||
}
|
||||
|
||||
export async function getPublishViewBlob(namespace: string, publishName: string) {
|
||||
const url = `/api/workspace/published/${namespace}/${publishName}/blob`;
|
||||
const response = await axiosInstance?.get(url, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
return blobToBytes(response?.data);
|
||||
}
|
||||
|
||||
export async function getPublishView(publishNamespace: string, publishName: string) {
|
||||
const meta = await getPublishViewMeta(publishNamespace, publishName);
|
||||
const blob = await getPublishViewBlob(publishNamespace, publishName);
|
||||
|
||||
if (meta.view.layout === ViewLayout.Document) {
|
||||
return {
|
||||
data: blob,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
const jsonStr = decoder.decode(blob);
|
||||
|
||||
const res = JSON.parse(jsonStr) as {
|
||||
database_collab: Uint8Array;
|
||||
database_row_collabs: Record<RowId, number[]>;
|
||||
database_row_document_collabs: Record<string, number[]>;
|
||||
visible_database_view_ids: ViewId[];
|
||||
database_relations: Record<DatabaseId, ViewId>;
|
||||
};
|
||||
|
||||
return {
|
||||
data: new Uint8Array(res.database_collab),
|
||||
rows: res.database_row_collabs,
|
||||
visibleViewIds: res.visible_database_view_ids,
|
||||
relations: res.database_relations,
|
||||
meta,
|
||||
};
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublishInfoWithViewId(viewId: string) {
|
||||
const url = `/api/workspace/published-info/${viewId}`;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
namespace: string;
|
||||
publish_name: string;
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function getPublishViewComments(viewId: string): Promise<GlobalComment[]> {
|
||||
const url = `/api/workspace/published-info/${viewId}/comment`;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
comments: {
|
||||
comment_id: string;
|
||||
user: {
|
||||
uuid: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
content: string;
|
||||
created_at: string;
|
||||
last_updated_at: string;
|
||||
reply_comment_id: string | null;
|
||||
is_deleted: boolean;
|
||||
can_be_deleted: boolean;
|
||||
}[];
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
const { comments } = data.data;
|
||||
|
||||
return comments.map((comment) => {
|
||||
return {
|
||||
commentId: comment.comment_id,
|
||||
user: {
|
||||
uuid: comment.user?.uuid || '',
|
||||
name: comment.user?.name || '',
|
||||
avatarUrl: comment.user?.avatar_url || null,
|
||||
},
|
||||
content: comment.content,
|
||||
createdAt: comment.created_at,
|
||||
lastUpdatedAt: comment.last_updated_at,
|
||||
replyCommentId: comment.reply_comment_id,
|
||||
isDeleted: comment.is_deleted,
|
||||
canDeleted: comment.can_be_deleted,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function getReactions(viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
|
||||
let url = `/api/workspace/published-info/${viewId}/reaction`;
|
||||
|
||||
if (commentId) {
|
||||
url += `?comment_id=${commentId}`;
|
||||
}
|
||||
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
reactions: {
|
||||
reaction_type: string;
|
||||
react_users: {
|
||||
uuid: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
}[];
|
||||
comment_id: string;
|
||||
}[];
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
const { reactions } = data.data;
|
||||
const reactionsMap: Record<string, Reaction[]> = {};
|
||||
|
||||
for (const reaction of reactions) {
|
||||
if (!reactionsMap[reaction.comment_id]) {
|
||||
reactionsMap[reaction.comment_id] = [];
|
||||
}
|
||||
|
||||
reactionsMap[reaction.comment_id].push({
|
||||
reactionType: reaction.reaction_type,
|
||||
commentId: reaction.comment_id,
|
||||
reactUsers: reaction.react_users.map((user) => ({
|
||||
uuid: user.uuid,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatar_url,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return reactionsMap;
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function createGlobalCommentOnPublishView(viewId: string, content: string, replyCommentId?: string) {
|
||||
const url = `/api/workspace/published-info/${viewId}/comment`;
|
||||
const response = await axiosInstance?.post<{ code: number; message: string }>(url, {
|
||||
content,
|
||||
reply_comment_id: replyCommentId,
|
||||
});
|
||||
|
||||
if (response?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(response?.data.message);
|
||||
}
|
||||
|
||||
export async function deleteGlobalCommentOnPublishView(viewId: string, commentId: string) {
|
||||
const url = `/api/workspace/published-info/${viewId}/comment`;
|
||||
const response = await axiosInstance?.delete<{ code: number; message: string }>(url, {
|
||||
data: {
|
||||
comment_id: commentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(response?.data.message);
|
||||
}
|
||||
|
||||
export async function addReaction(viewId: string, commentId: string, reactionType: string) {
|
||||
const url = `/api/workspace/published-info/${viewId}/reaction`;
|
||||
const response = await axiosInstance?.post<{ code: number; message: string }>(url, {
|
||||
comment_id: commentId,
|
||||
reaction_type: reactionType,
|
||||
});
|
||||
|
||||
if (response?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(response?.data.message);
|
||||
}
|
||||
|
||||
export async function removeReaction(viewId: string, commentId: string, reactionType: string) {
|
||||
const url = `/api/workspace/published-info/${viewId}/reaction`;
|
||||
const response = await axiosInstance?.delete<{ code: number; message: string }>(url, {
|
||||
data: {
|
||||
comment_id: commentId,
|
||||
reaction_type: reactionType,
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(response?.data.message);
|
||||
}
|
||||
|
||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||
const query = new URLSearchParams({
|
||||
include_member_count: 'true',
|
||||
});
|
||||
|
||||
const url = `/api/workspace?${query.toString()}`;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
workspace_id: string;
|
||||
workspace_name: string;
|
||||
member_count: number;
|
||||
icon: string;
|
||||
}[];
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
return data.data.map((workspace) => {
|
||||
return {
|
||||
id: workspace.workspace_id,
|
||||
name: workspace.workspace_name,
|
||||
memberCount: workspace.member_count,
|
||||
icon: workspace.icon,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export interface WorkspaceFolder {
|
||||
view_id: string;
|
||||
icon: string | null;
|
||||
name: string;
|
||||
is_space: boolean;
|
||||
is_private: boolean;
|
||||
extra: {
|
||||
is_space: boolean;
|
||||
space_created_at: number;
|
||||
space_icon: string;
|
||||
space_icon_color: string;
|
||||
space_permission: number;
|
||||
};
|
||||
|
||||
children: WorkspaceFolder[];
|
||||
}
|
||||
|
||||
function iterateFolder(folder: WorkspaceFolder): FolderView {
|
||||
return {
|
||||
id: folder.view_id,
|
||||
name: folder.name,
|
||||
icon: folder.icon,
|
||||
isSpace: folder.is_space,
|
||||
extra: folder.extra ? JSON.stringify(folder.extra) : null,
|
||||
isPrivate: folder.is_private,
|
||||
children: folder.children.map((child: WorkspaceFolder) => {
|
||||
return iterateFolder(child);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWorkspaceFolder(workspaceId: string): Promise<FolderView> {
|
||||
const url = `/api/workspace/${workspaceId}/folder`;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: WorkspaceFolder;
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
return iterateFolder(data.data);
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export interface DuplicatePublishViewPayload {
|
||||
published_collab_type: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
published_view_id: string;
|
||||
dest_view_id: string;
|
||||
}
|
||||
|
||||
export async function duplicatePublishView(workspaceId: string, payload: DuplicatePublishViewPayload) {
|
||||
const url = `/api/workspace/${workspaceId}/published-duplicate`;
|
||||
|
||||
const res = await axiosInstance?.post<{
|
||||
code: number;
|
||||
message: string;
|
||||
}>(url, payload);
|
||||
|
||||
if (res?.data.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(res?.data.message);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * as APIService from './http_api';
|
@ -0,0 +1,17 @@
|
||||
export function blobToBytes(blob: Blob): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
reject(new Error('Failed to convert blob to bytes'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(new Uint8Array(reader.result));
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
@ -8,24 +8,8 @@ import {
|
||||
} from '@/application/services/js-services/cache';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch';
|
||||
import {
|
||||
initAPIService,
|
||||
signInGoogle,
|
||||
signInWithMagicLink,
|
||||
signInGithub,
|
||||
signInDiscord,
|
||||
signInWithUrl,
|
||||
createGlobalCommentOnPublishView,
|
||||
deleteGlobalCommentOnPublishView,
|
||||
getPublishViewComments,
|
||||
getWorkspaces,
|
||||
getWorkspaceFolder,
|
||||
getCurrentUser,
|
||||
duplicatePublishView,
|
||||
getReactions,
|
||||
addReaction,
|
||||
removeReaction,
|
||||
} from '@/application/services/js-services/wasm/client_api';
|
||||
import { APIService } from '@/application/services/js-services/http';
|
||||
|
||||
import { AFService, AFServiceConfig } from '@/application/services/services.type';
|
||||
import { emit, EventType } from '@/application/session';
|
||||
import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in';
|
||||
@ -53,11 +37,7 @@ export class AFClientService implements AFService {
|
||||
private cacheDatabaseRowFolder: Map<string, Y.Map<YDoc>> = new Map();
|
||||
|
||||
constructor(config: AFServiceConfig) {
|
||||
initAPIService({
|
||||
...config.cloudConfig,
|
||||
deviceId: this.deviceId,
|
||||
clientId: this.clientId,
|
||||
});
|
||||
APIService.initAPIService(config.cloudConfig);
|
||||
}
|
||||
|
||||
getClientId() {
|
||||
@ -182,7 +162,7 @@ export class AFClientService implements AFService {
|
||||
async loginAuth(url: string) {
|
||||
try {
|
||||
console.log('loginAuth', url);
|
||||
await signInWithUrl(url);
|
||||
await APIService.signInWithUrl(url);
|
||||
emit(EventType.SESSION_VALID);
|
||||
afterAuth();
|
||||
return;
|
||||
@ -194,51 +174,45 @@ export class AFClientService implements AFService {
|
||||
|
||||
@withSignIn()
|
||||
async signInMagicLink({ email }: { email: string; redirectTo: string }) {
|
||||
return await signInWithMagicLink(email, AUTH_CALLBACK_URL);
|
||||
return await APIService.signInWithMagicLink(email, AUTH_CALLBACK_URL);
|
||||
}
|
||||
|
||||
@withSignIn()
|
||||
async signInGoogle(_: { redirectTo: string }) {
|
||||
return await signInGoogle(AUTH_CALLBACK_URL);
|
||||
return APIService.signInGoogle(AUTH_CALLBACK_URL);
|
||||
}
|
||||
|
||||
@withSignIn()
|
||||
async signInGithub(_: { redirectTo: string }) {
|
||||
return await signInGithub(AUTH_CALLBACK_URL);
|
||||
return APIService.signInGithub(AUTH_CALLBACK_URL);
|
||||
}
|
||||
|
||||
@withSignIn()
|
||||
async signInDiscord(_: { redirectTo: string }) {
|
||||
return await signInDiscord(AUTH_CALLBACK_URL);
|
||||
return APIService.signInDiscord(AUTH_CALLBACK_URL);
|
||||
}
|
||||
|
||||
async getWorkspaces() {
|
||||
const data = getWorkspaces();
|
||||
const data = APIService.getWorkspaces();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getWorkspaceFolder(workspaceId: string) {
|
||||
const data = await getWorkspaceFolder(workspaceId);
|
||||
const data = await APIService.getWorkspaceFolder(workspaceId);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
const data = await getCurrentUser();
|
||||
const data = await APIService.getCurrentUser();
|
||||
|
||||
return {
|
||||
uid: data.uid,
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
avatar: data.icon_url,
|
||||
uuid: data.uuid,
|
||||
};
|
||||
await APIService.getWorkspaces();
|
||||
return data;
|
||||
}
|
||||
|
||||
async duplicatePublishView(params: DuplicatePublishView) {
|
||||
return duplicatePublishView({
|
||||
workspace_id: params.workspaceId,
|
||||
return APIService.duplicatePublishView(params.workspaceId, {
|
||||
dest_view_id: params.spaceViewId,
|
||||
published_view_id: params.viewId,
|
||||
published_collab_type: params.collabType,
|
||||
@ -246,26 +220,26 @@ export class AFClientService implements AFService {
|
||||
}
|
||||
|
||||
createCommentOnPublishView(viewId: string, content: string, replyCommentId: string | undefined): Promise<void> {
|
||||
return createGlobalCommentOnPublishView(viewId, content, replyCommentId);
|
||||
return APIService.createGlobalCommentOnPublishView(viewId, content, replyCommentId);
|
||||
}
|
||||
|
||||
deleteCommentOnPublishView(viewId: string, commentId: string): Promise<void> {
|
||||
return deleteGlobalCommentOnPublishView(viewId, commentId);
|
||||
return APIService.deleteGlobalCommentOnPublishView(viewId, commentId);
|
||||
}
|
||||
|
||||
getPublishViewGlobalComments(viewId: string): Promise<GlobalComment[]> {
|
||||
return getPublishViewComments(viewId);
|
||||
return APIService.getPublishViewComments(viewId);
|
||||
}
|
||||
|
||||
getPublishViewReactions(viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
|
||||
return getReactions(viewId, commentId);
|
||||
return APIService.getReactions(viewId, commentId);
|
||||
}
|
||||
|
||||
addPublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise<void> {
|
||||
return addReaction(viewId, commentId, reactionType);
|
||||
return APIService.addReaction(viewId, commentId, reactionType);
|
||||
}
|
||||
|
||||
removePublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise<void> {
|
||||
return removeReaction(viewId, commentId, reactionType);
|
||||
return APIService.removeReaction(viewId, commentId, reactionType);
|
||||
}
|
||||
}
|
||||
|
@ -1,236 +0,0 @@
|
||||
import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token';
|
||||
import { ClientAPI, WorkspaceFolder, DuplicatePublishViewPayload } from '@appflowyinc/client-api-wasm';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type';
|
||||
import { FolderView } from '@/application/types';
|
||||
import { GlobalComment, Reaction } from '@/application/comment.type';
|
||||
|
||||
let client: ClientAPI;
|
||||
|
||||
export function initAPIService(
|
||||
config: AFCloudConfig & {
|
||||
deviceId: string;
|
||||
clientId: string;
|
||||
}
|
||||
) {
|
||||
if (client) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.refresh_token = refreshToken;
|
||||
|
||||
window.invalid_token = invalidToken;
|
||||
|
||||
client = ClientAPI.new({
|
||||
base_url: config.baseURL,
|
||||
ws_addr: config.wsURL,
|
||||
gotrue_url: config.gotrueURL,
|
||||
device_id: config.deviceId,
|
||||
client_id: config.clientId,
|
||||
configuration: {
|
||||
compression_quality: 8,
|
||||
compression_buffer_size: 10240,
|
||||
},
|
||||
});
|
||||
|
||||
if (isTokenValid()) {
|
||||
client.restore_token(getToken() || '');
|
||||
}
|
||||
|
||||
client.subscribe();
|
||||
}
|
||||
|
||||
export async function getPublishView(publishNamespace: string, publishName: string) {
|
||||
const data = await client.get_publish_view(publishNamespace, publishName);
|
||||
|
||||
const meta = JSON.parse(data.meta.data) as PublishViewMetaData;
|
||||
|
||||
if (meta.view.layout === ViewLayout.Document) {
|
||||
return {
|
||||
data: data.data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
const jsonStr = decoder.decode(new Uint8Array(data.data));
|
||||
|
||||
const res = JSON.parse(jsonStr) as {
|
||||
database_collab: number[];
|
||||
database_row_collabs: Record<RowId, number[]>;
|
||||
database_row_document_collabs: Record<string, number[]>;
|
||||
visible_database_view_ids: ViewId[];
|
||||
database_relations: Record<DatabaseId, ViewId>;
|
||||
};
|
||||
|
||||
return {
|
||||
data: res.database_collab,
|
||||
rows: res.database_row_collabs,
|
||||
visibleViewIds: res.visible_database_view_ids,
|
||||
relations: res.database_relations,
|
||||
meta,
|
||||
};
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublishInfoWithViewId(viewId: string) {
|
||||
return client.get_publish_info(viewId);
|
||||
}
|
||||
|
||||
export async function getPublishViewMeta(publishNamespace: string, publishName: string) {
|
||||
const data = await client.get_publish_view_meta(publishNamespace, publishName);
|
||||
const metadata = JSON.parse(data.data) as PublishViewMetaData;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function signInWithUrl(url: string) {
|
||||
return client.sign_in_with_url(url);
|
||||
}
|
||||
|
||||
export async function signInWithMagicLink(email: string, redirectTo: string) {
|
||||
return client.sign_in_with_magic_link(email, redirectTo);
|
||||
}
|
||||
|
||||
export async function signInGoogle(redirectTo: string) {
|
||||
return signInProvider('google', redirectTo);
|
||||
}
|
||||
|
||||
export async function signInProvider(provider: string, redirectTo: string) {
|
||||
try {
|
||||
const { url } = await client.generate_oauth_url_with_provider(provider, redirectTo);
|
||||
|
||||
window.open(url, '_current');
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function signInGithub(redirectTo: string) {
|
||||
return signInProvider('github', redirectTo);
|
||||
}
|
||||
|
||||
export async function signInDiscord(redirectTo: string) {
|
||||
return signInProvider('discord', redirectTo);
|
||||
}
|
||||
|
||||
export async function getWorkspaces() {
|
||||
try {
|
||||
const { data } = await client.get_workspaces();
|
||||
|
||||
return data.map((workspace) => ({
|
||||
id: workspace.workspace_id,
|
||||
name: workspace.workspace_name,
|
||||
icon: workspace.icon,
|
||||
memberCount: workspace.member_count || 0,
|
||||
}));
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkspaceFolder(workspaceId: string): Promise<FolderView> {
|
||||
try {
|
||||
const data = await client.get_folder(workspaceId);
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function iterateFolder(folder: WorkspaceFolder): FolderView {
|
||||
return {
|
||||
id: folder.view_id,
|
||||
name: folder.name,
|
||||
icon: folder.icon,
|
||||
isSpace: folder.is_space,
|
||||
extra: folder.extra,
|
||||
isPrivate: folder.is_private,
|
||||
children: folder.children.map((child: WorkspaceFolder) => {
|
||||
return iterateFolder(child);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return iterateFolder(data);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentUser() {
|
||||
return client.get_user();
|
||||
}
|
||||
|
||||
export function duplicatePublishView(payload: DuplicatePublishViewPayload) {
|
||||
return client.duplicate_publish_view(payload);
|
||||
}
|
||||
|
||||
export async function getPublishViewComments(viewId: string): Promise<GlobalComment[]> {
|
||||
try {
|
||||
const { comments } = await client.get_publish_view_comments(viewId);
|
||||
|
||||
return comments.map((comment) => {
|
||||
return {
|
||||
commentId: comment.comment_id,
|
||||
user: {
|
||||
uuid: comment.user?.uuid || '',
|
||||
name: comment.user?.name || '',
|
||||
avatarUrl: comment.user?.avatar_url || null,
|
||||
},
|
||||
content: comment.content,
|
||||
createdAt: comment.created_at,
|
||||
lastUpdatedAt: comment.last_updated_at,
|
||||
replyCommentId: comment.reply_comment_id,
|
||||
isDeleted: comment.is_deleted,
|
||||
canDeleted: comment.can_be_deleted,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGlobalCommentOnPublishView(viewId: string, content: string, replyCommentId?: string) {
|
||||
return client.create_comment_on_publish_view(viewId, content, replyCommentId);
|
||||
}
|
||||
|
||||
export async function deleteGlobalCommentOnPublishView(viewId: string, commentId: string) {
|
||||
return client.delete_comment_on_publish_view(viewId, commentId);
|
||||
}
|
||||
|
||||
export async function getReactions(viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
|
||||
try {
|
||||
const { reactions } = await client.get_reactions(viewId, commentId);
|
||||
|
||||
const reactionsMap: Record<string, Reaction[]> = {};
|
||||
|
||||
for (const reaction of reactions) {
|
||||
if (!reactionsMap[reaction.comment_id]) {
|
||||
reactionsMap[reaction.comment_id] = [];
|
||||
}
|
||||
|
||||
reactionsMap[reaction.comment_id].push({
|
||||
reactionType: reaction.reaction_type,
|
||||
commentId: reaction.comment_id,
|
||||
reactUsers: reaction.react_users.map((user) => ({
|
||||
uuid: user.uuid,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatar_url,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return reactionsMap;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addReaction(viewId: string, commentId: string, reactionType: string) {
|
||||
return client.create_reaction(viewId, commentId, reactionType);
|
||||
}
|
||||
|
||||
export async function removeReaction(viewId: string, commentId: string, reactionType: string) {
|
||||
return client.delete_reaction(viewId, commentId, reactionType);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * as APIService from './client_api';
|
@ -18,3 +18,21 @@ export function isTokenValid() {
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
export function getTokenParsed(): {
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
refresh_token: string;
|
||||
} | null {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(token);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { MAX_FREQUENTLY_ROW_COUNT, PER_ROW_EMOJI_COUNT } from '@/components/_shared/emoji-picker/const';
|
||||
import { loadEmojiData } from '@/utils/emoji';
|
||||
import { EmojiMartData } from '@emoji-mart/data';
|
||||
import { PopoverProps } from '@mui/material/Popover';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import { FrequentlyUsed, getEmojiDataFromNative, init, Store } from 'emoji-mart';
|
||||
import chunk from 'lodash-es/chunk';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@ -22,12 +20,12 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [emojiCategories, setEmojiCategories] = useState<EmojiCategory[]>([]);
|
||||
const [skin, setSkin] = useState<number>(() => {
|
||||
return Number(Store.get('skin')) || 0;
|
||||
return Number(localStorage.getItem('emoji-mart.skin')) || 0;
|
||||
});
|
||||
|
||||
const onSkinChange = useCallback((val: number) => {
|
||||
setSkin(val);
|
||||
Store.set('skin', String(val));
|
||||
localStorage.setItem('emoji-mart.skin', String(val));
|
||||
}, []);
|
||||
|
||||
const searchEmojiData = useCallback(
|
||||
@ -70,10 +68,6 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await init({
|
||||
maxFrequentRows: MAX_FREQUENTLY_ROW_COUNT,
|
||||
perLine: PER_ROW_EMOJI_COUNT,
|
||||
});
|
||||
await searchEmojiData();
|
||||
})();
|
||||
}, [searchEmojiData]);
|
||||
@ -85,17 +79,6 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
||||
const onSelect = useCallback(
|
||||
async (native: string) => {
|
||||
onEmojiSelect(native);
|
||||
if (!native) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getEmojiDataFromNative(native);
|
||||
|
||||
FrequentlyUsed.add(data);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
},
|
||||
[onEmojiSelect]
|
||||
);
|
||||
@ -156,6 +139,7 @@ export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize:
|
||||
id: string;
|
||||
type: 'category' | 'emojis';
|
||||
emojis?: Emoji[];
|
||||
category?: string;
|
||||
}[] = [];
|
||||
|
||||
emojiCategories.forEach((category) => {
|
||||
@ -165,6 +149,7 @@ export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize:
|
||||
});
|
||||
chunk(category.emojis, rowSize).forEach((chunk, index) => {
|
||||
rows.push({
|
||||
category: category.id,
|
||||
type: 'emojis',
|
||||
emojis: chunk,
|
||||
id: `${category.id}-${index}`,
|
||||
|
@ -67,17 +67,36 @@ function EmojiPickerCategories({
|
||||
const renderRow = useCallback(
|
||||
({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||
const item = rows[index];
|
||||
const tagName = getCategoryName(item.id);
|
||||
const isFlags = item.category === 'flags';
|
||||
|
||||
return (
|
||||
<div style={style} data-index={index}>
|
||||
{item.type === 'category' ? (
|
||||
<div className={'pt-2 text-base font-medium text-text-caption'}>{getCategoryName(item.id)}</div>
|
||||
<div className={'pt-2 text-base font-medium text-text-caption'}>{tagName}</div>
|
||||
) : null}
|
||||
<div className={'flex'}>
|
||||
{item.emojis?.map((emoji, columnIndex) => {
|
||||
const isSelected = selectCell.row === index && selectCell.column === columnIndex;
|
||||
|
||||
const isDefaultEmoji = defaultEmoji === emoji.native;
|
||||
const classList = [
|
||||
'flex cursor-pointer items-center justify-center rounded text-[20px] hover:bg-fill-list-hover',
|
||||
];
|
||||
|
||||
if (isSelected) {
|
||||
classList.push('bg-fill-list-hover');
|
||||
} else {
|
||||
classList.push('hover:bg-transparent');
|
||||
}
|
||||
|
||||
if (isDefaultEmoji) {
|
||||
classList.push('bg-fill-list-active');
|
||||
}
|
||||
|
||||
if (isFlags) {
|
||||
classList.push('icon');
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip key={emoji.id} title={emoji.name} placement={'top'} enterDelay={500} disableInteractive={true}>
|
||||
@ -105,9 +124,7 @@ function EmojiPickerCategories({
|
||||
mouseX.current = e.clientX;
|
||||
mouseY.current = e.clientY;
|
||||
}}
|
||||
className={`flex cursor-pointer items-center justify-center rounded text-[20px] hover:bg-fill-list-hover ${
|
||||
isSelected ? 'bg-fill-list-hover' : 'hover:bg-transparent'
|
||||
} ${isDefaultEmoji ? 'bg-fill-list-active' : ''}`}
|
||||
className={classList.join(' ')}
|
||||
>
|
||||
{emoji.native}
|
||||
</div>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { lazy } from 'react';
|
||||
// import { lazy } from 'react';
|
||||
//
|
||||
// export const EmojiPicker = lazy(() => import('./EmojiPicker'));
|
||||
|
||||
export const EmojiPicker = lazy(() => import('./EmojiPicker'));
|
||||
export * from './EmojiPicker';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ViewLayout, ViewMetaIcon } from '@/application/collab.type';
|
||||
import { ViewIcon } from '@/components/_shared/view-icon';
|
||||
import React from 'react';
|
||||
import { isFlagEmoji } from '@/utils/emoji';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function DatabaseHeader({
|
||||
@ -14,6 +15,9 @@ function DatabaseHeader({
|
||||
layout?: ViewLayout;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isFlag = useMemo(() => {
|
||||
return icon ? isFlagEmoji(icon.value) : false;
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -23,7 +27,7 @@ function DatabaseHeader({
|
||||
>
|
||||
<div className={'relative'}>
|
||||
{icon?.value ? (
|
||||
<div className={'view-icon'}>{icon?.value}</div>
|
||||
<div className={`view-icon ${isFlag ? 'icon' : ''}`}>{icon?.value}</div>
|
||||
) : (
|
||||
<ViewIcon layout={layout || ViewLayout.Grid} size={10} />
|
||||
)}
|
||||
|
@ -2,6 +2,7 @@ import { ViewLayout } from '@/application/collab.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import { ViewIcon } from '@/components/_shared/view-icon';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { isFlagEmoji } from '@/utils/emoji';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -32,6 +33,10 @@ function MentionPage({ pageId }: { pageId: string }) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isFlag = useMemo(() => {
|
||||
return icon ? isFlagEmoji(icon.value) : false;
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
@ -44,7 +49,7 @@ function MentionPage({ pageId }: { pageId: string }) {
|
||||
<span className={'mention-unpublished cursor-text font-semibold text-text-caption'}>No Access</span>
|
||||
) : (
|
||||
<>
|
||||
<span className={'mention-icon icon'}>
|
||||
<span className={`mention-icon ${isFlag ? 'icon' : ''}`}>
|
||||
{icon?.value || <ViewIcon layout={meta?.layout || ViewLayout.Document} size={'small'} />}
|
||||
</span>
|
||||
|
||||
|
@ -185,7 +185,7 @@ export function useCommentRender(comment: GlobalComment) {
|
||||
}, [comment]);
|
||||
|
||||
const timeFormat = useMemo(() => {
|
||||
const time = dayjs.unix(Number(comment.lastUpdatedAt));
|
||||
const time = dayjs(comment.lastUpdatedAt);
|
||||
|
||||
return time.format('YYYY-MM-DD HH:mm:ss');
|
||||
}, [comment.lastUpdatedAt]);
|
||||
@ -193,7 +193,7 @@ export function useCommentRender(comment: GlobalComment) {
|
||||
const time = useMemo(() => {
|
||||
if (!comment.lastUpdatedAt) return '';
|
||||
const now = dayjs();
|
||||
const past = dayjs.unix(Number(comment.lastUpdatedAt));
|
||||
const past = dayjs(comment.lastUpdatedAt);
|
||||
const diffSec = now.diff(past, 'second');
|
||||
const diffMin = now.diff(past, 'minute');
|
||||
const diffHour = now.diff(past, 'hour');
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Reaction as ReactionType } from '@/application/comment.type';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { isFlagEmoji } from '@/utils/emoji';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import React, { memo, useContext, useMemo } from 'react';
|
||||
@ -68,6 +69,10 @@ function Reaction({ reaction, onClick }: { reaction: ReactionType; onClick: (rea
|
||||
return getPlatform().isMobile;
|
||||
}, []);
|
||||
|
||||
const isFlag = useMemo(() => {
|
||||
return isFlagEmoji(reaction.reactionType);
|
||||
}, [reaction.reactionType]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
@ -91,7 +96,7 @@ function Reaction({ reaction, onClick }: { reaction: ReactionType; onClick: (rea
|
||||
'flex cursor-pointer items-center gap-1 rounded-full border border-transparent bg-fill-list-hover px-1 py-0.5 text-sm'
|
||||
}
|
||||
>
|
||||
<span className={''}>{reaction.reactionType}</span>
|
||||
<span className={`${isFlag ? 'icon' : ''}`}>{reaction.reactionType}</span>
|
||||
{<div className={'text-xs font-medium'}>{reactCount}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -4,6 +4,7 @@ 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, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -34,6 +35,9 @@ function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disable
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onNavigateToView = usePublishContext()?.toView;
|
||||
const isFlag = useMemo(() => {
|
||||
return icon ? isFlagEmoji(icon) : false;
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
|
||||
@ -59,7 +63,7 @@ function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disable
|
||||
<SpaceIcon value={extraObj.space_icon || ''} />
|
||||
</span>
|
||||
) : (
|
||||
<span className={'icon flex h-5 w-5 items-center justify-center'}>
|
||||
<span className={`${isFlag ? 'icon' : ''} flex h-5 w-5 items-center justify-center`}>
|
||||
{icon || <ViewIcon layout={layout} size={'small'} />}
|
||||
</span>
|
||||
)}
|
||||
|
@ -93,9 +93,7 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer:
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<MoreActions />
|
||||
{/*<Suspense fallback={null}>*/}
|
||||
{/* <Duplicate />*/}
|
||||
{/*</Suspense>*/}
|
||||
{/*<Duplicate />*/}
|
||||
<Divider orientation={'vertical'} className={'mx-2'} flexItem />
|
||||
<Tooltip title={t('publish.downloadApp')}>
|
||||
<button onClick={openOrDownload}>
|
||||
|
@ -5,6 +5,7 @@ import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png';
|
||||
import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png';
|
||||
import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png';
|
||||
import ViewCover, { CoverType } from '@/components/view-meta/ViewCover';
|
||||
import { isFlagEmoji } from '@/utils/emoji';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ViewLayout, ViewMetaIcon } from '@/application/collab.type';
|
||||
@ -54,6 +55,10 @@ export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) {
|
||||
}, [coverType, cover?.value]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isFlag = useMemo(() => {
|
||||
return icon ? isFlagEmoji(icon.value) : false;
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-col items-center'}>
|
||||
{cover && <ViewCover coverType={coverType} coverValue={coverValue} />}
|
||||
@ -63,7 +68,7 @@ export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) {
|
||||
'flex gap-4 overflow-hidden whitespace-pre-wrap break-words break-all px-16 text-[2.25rem] font-bold leading-[1.5em] max-md:px-4 max-sm:text-[7vw]'
|
||||
}
|
||||
>
|
||||
{icon?.value ? <div className={'view-icon'}>{icon?.value}</div> : null}
|
||||
{icon?.value ? <div className={`view-icon ${isFlag ? 'icon' : ''}`}>{icon?.value}</div> : null}
|
||||
|
||||
<div className={'relative top-1.5'}>
|
||||
{name || <span className={'text-text-placeholder'}>{t('menuAppHeader.defaultNewPageName')}</span>}
|
||||
|
@ -9,8 +9,8 @@ function LoginPage() {
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && redirectTo && encodeURIComponent(redirectTo) !== window.location.href) {
|
||||
window.location.href = redirectTo;
|
||||
if (isAuthenticated && redirectTo && decodeURIComponent(redirectTo) !== window.location.href) {
|
||||
window.location.href = decodeURIComponent(redirectTo);
|
||||
}
|
||||
}, [isAuthenticated, redirectTo]);
|
||||
return (
|
||||
|
@ -69,7 +69,6 @@ body {
|
||||
|
||||
.view-icon {
|
||||
@apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em];
|
||||
font-family: 'Apple Color Emoji', 'Noto Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
||||
line-height: 1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -12,3 +12,7 @@ export async function randomEmoji(skin = 0) {
|
||||
export async function loadEmojiData() {
|
||||
return import('@emoji-mart/data/sets/15/native.json');
|
||||
}
|
||||
|
||||
export function isFlagEmoji(emoji: string) {
|
||||
return /\uD83C[\uDDE6-\uDDFF]/.test(emoji);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import usePluginImport from 'vite-plugin-importer';
|
||||
import { totalBundleSize } from 'vite-plugin-total-bundle-size';
|
||||
@ -14,7 +13,6 @@ const isDev = process.env.NODE_ENV === 'development';
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
wasm(),
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
prettier: false,
|
||||
@ -117,7 +115,8 @@ export default defineConfig({
|
||||
id.includes('/react-custom-scrollbars') ||
|
||||
id.includes('/dayjs') ||
|
||||
id.includes('/smooth-scroll-into-view-if-needed') ||
|
||||
id.includes('/react-virtualized-auto-sizer')
|
||||
id.includes('/react-virtualized-auto-sizer') ||
|
||||
id.includes('/react-window')
|
||||
) {
|
||||
return 'common';
|
||||
}
|
||||
@ -141,13 +140,6 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-katex',
|
||||
// 'react-custom-scrollbars-2',
|
||||
// 'react-window',
|
||||
// 'react-virtualized-auto-sizer',
|
||||
],
|
||||
include: ['react', 'react-dom', 'react-katex'],
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user