From 41c579ded7f72af5a7fbbefa8f9a5a90f8b35cb1 Mon Sep 17 00:00:00 2001 From: kitce Date: Mon, 18 Apr 2022 22:52:23 +0800 Subject: [PATCH 01/58] refactor(typings): add type for `unregister()` --- src/cloud.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cloud.ts b/src/cloud.ts index 5981ca03..f6e0d443 100644 --- a/src/cloud.ts +++ b/src/cloud.ts @@ -8,7 +8,9 @@ import { selectSync } from './store/selectors'; import store from './store/store'; import { EventAction, EventCategory } from './types/ga'; -let unregister: (() => void) | null = null; +type TUnregister = (() => void) | null; + +let unregister: TUnregister = null; export const sync = async (auth: gapi.auth2.GoogleAuth) => { const signedIn = auth.isSignedIn.get(); From 6cf7a9ceb8968f2b785f011e55cb9b2a3a76a2fb Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 01:49:13 +0800 Subject: [PATCH 02/58] chore(models): comment unused methods --- src/models/Subscription/Subscription.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/models/Subscription/Subscription.ts b/src/models/Subscription/Subscription.ts index 325f8f7b..cd1758b1 100644 --- a/src/models/Subscription/Subscription.ts +++ b/src/models/Subscription/Subscription.ts @@ -44,15 +44,15 @@ class Subscription extends RemoteSubscription implements ISubscription { return { name, version, url, enabled }; } - enable () { - this.enabled = true; - return this; - } - - disable () { - this.enabled = false; - return this; - } + // enable () { + // this.enabled = true; + // return this; + // } + + // disable () { + // this.enabled = false; + // return this; + // } } export default Subscription; From 74720b1b0787b90f5405a59c62697a757784c712 Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 01:51:31 +0800 Subject: [PATCH 03/58] chore(store): revise imports --- src/store/middleware/subscription.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/store/middleware/subscription.ts b/src/store/middleware/subscription.ts index 45401b68..52a5a707 100644 --- a/src/store/middleware/subscription.ts +++ b/src/store/middleware/subscription.ts @@ -5,8 +5,7 @@ import * as LIHKG from '../../helpers/lihkg'; import { notification } from '../../templates/subscription'; import { selectSubscriptions } from '../selectors'; import { actions as subscriptionsActions } from '../slices/subscriptions'; -import type { TRootState } from '../store'; -import { TStore } from './../store'; +import type { TRootState, TStore } from '../store'; type TToggleAction = ReturnType; From 98f1e36424f758f59a4a3548960052b7330a37f8 Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 01:51:46 +0800 Subject: [PATCH 04/58] chore(config): revise imports --- config/webpack/webpack.config.prod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/webpack/webpack.config.prod.ts b/config/webpack/webpack.config.prod.ts index fa9bf58a..56e99fd9 100644 --- a/config/webpack/webpack.config.prod.ts +++ b/config/webpack/webpack.config.prod.ts @@ -2,7 +2,7 @@ import '../env'; // load the environment variables at the beginning import path from 'path'; import type webpack from 'webpack'; import merge from 'webpack-merge'; -import { Directory } from './../config'; +import { Directory } from '../config'; import egg from './webpack.config.egg'; import main from './webpack.config.main'; From ade1265e629cd1c6ddee4aad217c4be306e46bba Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 01:55:00 +0800 Subject: [PATCH 05/58] build(config): update source map --- config/webpack/webpack.config.dev.ts | 2 +- config/webpack/webpack.config.prod.ts | 1 + tsconfig.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/webpack/webpack.config.dev.ts b/config/webpack/webpack.config.dev.ts index 3a8503f2..e0b99481 100644 --- a/config/webpack/webpack.config.dev.ts +++ b/config/webpack/webpack.config.dev.ts @@ -8,7 +8,7 @@ import { dev as main } from './webpack.config.main'; const dev: webpack.Configuration = { mode: 'development', - devtool: 'eval-cheap-source-map', + devtool: 'eval-source-map', output: { path: path.join(process.cwd(), Directory.Build), } diff --git a/config/webpack/webpack.config.prod.ts b/config/webpack/webpack.config.prod.ts index 56e99fd9..36cdbca9 100644 --- a/config/webpack/webpack.config.prod.ts +++ b/config/webpack/webpack.config.prod.ts @@ -8,6 +8,7 @@ import main from './webpack.config.main'; const prod: webpack.Configuration = { mode: 'production', + devtool: 'source-map', output: { path: path.join(process.cwd(), Directory.Dist) } diff --git a/tsconfig.json b/tsconfig.json index deb8dae6..0518883a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "esnext" ], "jsx": "react-jsx", + "sourceMap": true, "esModuleInterop": true, // "allowSyntheticDefaultImports": true, "skipLibCheck": true, From 6cde8e4e11ccf2669c8fb816d61d17c5163bc39a Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 02:57:14 +0800 Subject: [PATCH 06/58] chore(store): revise imports --- src/store/slices/config.ts | 2 +- src/store/slices/meta.ts | 2 +- src/store/slices/personal.ts | 2 +- src/store/slices/subscriptions.ts | 2 +- src/store/slices/sync.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/store/slices/config.ts b/src/store/slices/config.ts index 82678474..c4e1ab62 100644 --- a/src/store/slices/config.ts +++ b/src/store/slices/config.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; -import { ActionType } from 'typesafe-actions'; +import type { ActionType } from 'typesafe-actions'; import storage from '../../helpers/storage'; import Config, { IConfig } from '../../models/Config'; import type { IBaseSubscription } from '../../models/Subscription'; diff --git a/src/store/slices/meta.ts b/src/store/slices/meta.ts index b2b84f67..ca32c71b 100644 --- a/src/store/slices/meta.ts +++ b/src/store/slices/meta.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; -import { ActionType } from 'typesafe-actions'; +import type { ActionType } from 'typesafe-actions'; import storage from '../../helpers/storage'; import Meta, { IMeta } from '../../models/Meta'; diff --git a/src/store/slices/personal.ts b/src/store/slices/personal.ts index 734ab518..cdddc7fa 100644 --- a/src/store/slices/personal.ts +++ b/src/store/slices/personal.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createTransform } from 'redux-persist'; -import { ActionType } from 'typesafe-actions'; +import type { ActionType } from 'typesafe-actions'; import type { ISource } from '../../models/Label'; import Personal, { IPersonal, ISerializedPersonal } from '../../models/Personal'; diff --git a/src/store/slices/subscriptions.ts b/src/store/slices/subscriptions.ts index e9b6f6dd..6d7c3fd0 100644 --- a/src/store/slices/subscriptions.ts +++ b/src/store/slices/subscriptions.ts @@ -1,6 +1,6 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createTransform } from 'redux-persist'; -import { ActionType } from 'typesafe-actions'; +import type { ActionType } from 'typesafe-actions'; import * as TEXTS from '../../constants/texts'; import Subscription, { IBaseRemoteSubscription, ISerializedSubscription } from '../../models/Subscription'; import { selectSubscriptions } from '../selectors'; diff --git a/src/store/slices/sync.ts b/src/store/slices/sync.ts index 24fff08c..b48ff57d 100644 --- a/src/store/slices/sync.ts +++ b/src/store/slices/sync.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ActionType } from 'typesafe-actions'; +import type { ActionType } from 'typesafe-actions'; interface IState { loading: boolean; From 374ce91c1784a10f248e6bbf4f0977010ed0abb4 Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 02:57:43 +0800 Subject: [PATCH 07/58] refactor(lihkg): enhance typings --- src/actions/lihkg.ts | 7 ++ .../UnlockIconMapToggleButton.tsx | 2 +- src/helpers/lihkg.ts | 14 ++- src/helpers/redux.ts | 4 +- src/types/lihkg.ts | 115 ++++++++++++++---- 5 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/actions/lihkg.ts b/src/actions/lihkg.ts index ef46a33d..53f1dd80 100644 --- a/src/actions/lihkg.ts +++ b/src/actions/lihkg.ts @@ -1,4 +1,5 @@ import { createAction } from '@reduxjs/toolkit'; +import type { ActionType } from 'typesafe-actions'; import type { IIconMap, TNotification } from '../types/lihkg'; enum Action { @@ -10,3 +11,9 @@ enum Action { export const setIconMap = createAction(Action.SetIconMap); export const showNotification = createAction(Action.ShowNotification); export const removeNotification = createAction(Action.RemoveNotification); + +export type TActions = ActionType< + typeof setIconMap + | typeof showNotification + | typeof removeNotification +>; diff --git a/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx b/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx index a04effb5..7187d5b0 100644 --- a/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx +++ b/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx @@ -26,7 +26,7 @@ const UnlockIconMapToggleButton = () => { if (isIconMapUnlocked) { const state = getState(); if (!originalIconMap) { - originalIconMap = state.app.iconMap as IIconMap; + originalIconMap = state.app.iconMap; } if (!unlockedIconMap) { unlockedIconMap = LIHKG.unlockIconMap(originalIconMap); diff --git a/src/helpers/lihkg.ts b/src/helpers/lihkg.ts index 769ca4d8..72f420a7 100644 --- a/src/helpers/lihkg.ts +++ b/src/helpers/lihkg.ts @@ -2,10 +2,11 @@ import produce from 'immer'; import { dev } from '../../config/config'; import { displayName } from '../../package.json'; +import type { TActions } from '../actions/lihkg'; import * as lihkgActions from '../actions/lihkg'; import { ISource } from '../models/Label'; import lihkgSelectors from '../stylesheets/variables/lihkg/selectors.module.scss'; -import { IIconMap, ILocalNotifcation, ILocalNotifcationPayload, IUser, NotificationType, TNotification } from '../types/lihkg'; +import { IIconMap, ILocalNotifcation, ILocalNotifcationPayload, IState, IUser, NotificationType, TNotification } from '../types/lihkg'; import { counter } from './counter'; import { waitForElement } from './dom'; import { findReduxStore, IReactRootElement } from './redux'; @@ -15,10 +16,6 @@ enum ShareType { Reply = 2 } -/** - * create a notification object - * @private - */ type TCreateNotification = { /** * create a local notification object @@ -56,7 +53,7 @@ export const waitForRightPanelContainer = async () => { */ export const getStore = () => { const app = document.querySelector(lihkgSelectors.app); - const store = findReduxStore(app as IReactRootElement); + const store = findReduxStore(app as IReactRootElement); return store; }; @@ -109,6 +106,11 @@ export const getShareID = (source: ISource) => { return C(parseInt(e, 10), 'abcdefghijkmnopqrstuvwxyz'); }; +/** + * create a notification object + * @private + * @see TCreateNotification + */ const createNotification: TCreateNotification = (type, body, duration = 3000) => { const { value: id } = notificationIdCount.next(); const defaultPayload: Partial = { title: displayName }; diff --git a/src/helpers/redux.ts b/src/helpers/redux.ts index 52a2165b..713eeaa3 100644 --- a/src/helpers/redux.ts +++ b/src/helpers/redux.ts @@ -1,11 +1,11 @@ -import { Store } from '@reduxjs/toolkit'; +import type { Action, Store } from '@reduxjs/toolkit'; // import retry from './retry'; export interface IReactRootElement extends Element { _reactRootContainer: any; } -export const findReduxStore = (root: IReactRootElement): Store | null => { +export const findReduxStore = (root: IReactRootElement): Store | null => { let base; try { base = root._reactRootContainer._internalRoot.current; diff --git a/src/types/lihkg.ts b/src/types/lihkg.ts index 75de5e45..ced75ef2 100644 --- a/src/types/lihkg.ts +++ b/src/types/lihkg.ts @@ -9,7 +9,7 @@ interface IQuoteListResponse { item_data: IPost[]; thread: IThread; parent_post: IPost; - me: IMe; + me: IMeUser; } export interface IReplyListResponseData { @@ -21,7 +21,7 @@ export interface IReplyListResponseData { interface IReplyListResponse extends IThread { page: string; item_data: IPost[]; - me: IMe; + me: IMeUser; } export interface IPost { @@ -34,7 +34,7 @@ export interface IPost { dislike_count: string; vote_score: string; no_of_quote: string; - remark: unknown[] | IRemark; + remark: unknown[] | IPostRemark; status: string; reply_time: number; msg_num: string; @@ -48,7 +48,7 @@ export interface IPost { quote?: IPost; } -interface IRemark { +interface IPostRemark { is_newbie?: boolean; is_not_push_post?: boolean; } @@ -63,7 +63,7 @@ interface IThreadListResponse { category: ICategory; is_pagination: boolean; items: IThread[]; - me: IMe; + me: IMeUser; } export interface IThread { @@ -86,7 +86,7 @@ export interface IThread { last_reply_time: number; status: string; is_adu: boolean; - remark: IRemark; + remark: IThreadRemark; last_reply_user_id: string; max_reply: string; total_page: number; @@ -127,7 +127,7 @@ interface IQuery { sub_cat_id: string; } -interface IRemark { +interface IThreadRemark { last_reply_count: number; author_pin_post_id?: string; no_of_uni_not_push_post: number; @@ -135,6 +135,77 @@ interface IRemark { cover_img?: string; } +/** + * LIHKG state type + * @todo add other properties + */ +export interface IState { + app: IApp; +} + +type TVisitedThread = [threadId: number, lastMessageNumber: number, lastVisitedTime: number, page: number, messageNumber: number]; + +type TOfficeMode = 0 | 1 | 2; + +interface IApp { + officeMode: 0 | 1 | 2; + modeSettings: { + [key in TOfficeMode]: IModeSetting + }; + isMobile: boolean; + isHoverable: boolean; + visitedThreads: { + [threadId: string]: TVisitedThread; + }; + drafts: IDraft[]; + currentUser: ICurrentUser | null; + twoFaIsEnabled: boolean | null; + autoLogout: IAutoLogout | null; + keywordFilterList: any[]; + keywordFilterRegexStr: string; + customCatIds: any[]; + iconMap: IIconMap; + flatIconMap: IconSetData; + editorShowIcon: number; + editorRecentIcons: any[]; + notifyCount: number; + pushSupported: boolean; + pushNotification: boolean; + featuredPushNotification: boolean; + pushSetting: IPushSetting | null; + preferredUploadProvider: string[]; + plusRenewable: boolean; + alertNotice: any[]; +} + +interface ICurrentUser { + token: string; + user_id: string; + nickname: string; + level: string; +} + +interface IModeSetting { + darkMode: boolean; + splitMode: boolean; + limitContainerSize: boolean; + showFullTimestamp: boolean; + fontSize: number; + autoLoadImage: boolean; + linkHoverPreview: boolean; + shrinkQuoteImages: boolean; + openImageInLightbox: boolean; + includeLinkImages: boolean; + filterSpoilerTitle: boolean; + showIcons: boolean; + staticIcons: boolean; + youtubePreview: boolean; + showNotification: boolean; + minimizeReply: boolean; + previewBeforeReply: boolean; + imageProxy: boolean; +} + enum Gender { F = 'F', M = 'M', @@ -160,16 +231,16 @@ export interface IUser { is_newbie: boolean; } -interface IMe extends IUser { +interface IMeUser extends IUser { email: string; plus_expiry_time: number; last_login_time: number; is_disappear: boolean; is_plus_user: boolean; - meta_data: IMetaData; + meta_data: IUserMetaData; } -interface IMetaData { +interface IUserMetaData { custom_cat: unknown[]; keyword_filter: string; login_count: number; @@ -209,22 +280,22 @@ export interface IIconMap { } interface IIconSet { - icons: { [key: string]: string; }; - special?: IIconSetSpecial; - showOn?: IIconSetShowOn; + icons: IconSetData; + special?: IconSetData; + showOn?: { + start_time?: number; + end_time?: number; + keywords?: string[]; + user_id?: number[]; + cat_id?: number[]; + }; + isPinTop?: boolean; } -interface IIconSetShowOn { - start_time?: number; - end_time?: number; - keywords?: string[]; - user_id?: number[]; - cat_id?: number[]; +interface IconSetData { + [url: string]: string; } -interface IIconSetSpecial { - [path: string]: string; -} export enum NotificationType { Local = 'local' From 4c26433f62f1a8c18f9d58110aaf73d2fb0ebd6f Mon Sep 17 00:00:00 2001 From: kitce Date: Thu, 21 Apr 2022 23:54:57 +0800 Subject: [PATCH 08/58] fix(label-info): missing margin-top for buttons --- src/components/LabelInfo/LabelInfo.module.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/LabelInfo/LabelInfo.module.scss b/src/components/LabelInfo/LabelInfo.module.scss index 06872ae7..ba63b2e8 100644 --- a/src/components/LabelInfo/LabelInfo.module.scss +++ b/src/components/LabelInfo/LabelInfo.module.scss @@ -24,14 +24,11 @@ @apply text-current; @apply ml-2; } - - & + .reason { - @apply mt-2; - } } - .reason { - & + .buttons { + .reason, + .buttons { + &:not(:first-child) { @apply mt-2; } } From 6ac9706dd60310a9915ebcb8fbf170e997378848 Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 22 Apr 2022 03:28:31 +0800 Subject: [PATCH 09/58] build(webpack): enable live reload for `egg` --- config/webpack/webpack.config.egg.ts | 9 +++++++-- config/webpack/webpack.config.main.ts | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config/webpack/webpack.config.egg.ts b/config/webpack/webpack.config.egg.ts index abdd13e3..b8450d88 100644 --- a/config/webpack/webpack.config.egg.ts +++ b/config/webpack/webpack.config.egg.ts @@ -9,8 +9,13 @@ import base from './webpack.config.base'; const devServer: DevServerConfiguration = { hot: false, port, - client: false, - liveReload: false, + host: '127.0.0.1', + allowedHosts: 'all', + // client: false, + liveReload: true, + devMiddleware: { + writeToDisk: true + }, headers: { 'Access-Control-Allow-Origin': '*' }, diff --git a/config/webpack/webpack.config.main.ts b/config/webpack/webpack.config.main.ts index ab3aa3dd..de664766 100644 --- a/config/webpack/webpack.config.main.ts +++ b/config/webpack/webpack.config.main.ts @@ -10,6 +10,8 @@ import base from './webpack.config.base'; const devServer: DevServerConfiguration = { hot: false, port, + host: '127.0.0.1', + allowedHosts: 'all', client: false, liveReload: false, devMiddleware: { From dd99979478959e1b796b342ad10391d18a78e6b9 Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 02:13:50 +0800 Subject: [PATCH 10/58] refactor(typings): update `lihkg` types --- src/models/App.tsx | 10 +++--- src/models/Cache.ts | 8 ++--- src/types/lihkg.ts | 76 +++++++++++++++++++++++---------------------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/models/App.tsx b/src/models/App.tsx index 7cdb629e..f1cd235e 100644 --- a/src/models/App.tsx +++ b/src/models/App.tsx @@ -9,7 +9,7 @@ import { addedNodeMutationHandlerFactory, handleDataPostIdAttributeMutation, ren import { checkUpdate } from '../helpers/version'; import { intercept } from '../helpers/xhr'; import type { TStore } from '../store/store'; -import type { IQuoteListResponseData, IReplyListResponseData, IThreadListResponseData } from '../types/lihkg'; +import type { APIv2 } from '../types/lihkg'; import type Cache from './Cache'; class App { @@ -94,15 +94,15 @@ class App { const isQuoteList = REGEXES.QUOTE_LIST_API.test(responseURL); const isReplyList = REGEXES.REPLY_LIST_API.test(responseURL); if (isThreadList || isQuoteList || isReplyList) { - const data = JSON.parse(this.responseText) as IThreadListResponseData | IQuoteListResponseData | IReplyListResponseData; + const data = JSON.parse(this.responseText) as APIv2.IThreadListResponseBody | APIv2.IQuoteListResponseBody | APIv2.IReplyListResponseBody; if (data.success === 1) { if (isThreadList) { - cache.addThreads(data as IThreadListResponseData); + cache.addThreads(data as APIv2.IThreadListResponseBody); } if (isQuoteList || isReplyList) { - cache.addReplies(data as IQuoteListResponseData | IReplyListResponseData); + cache.addReplies(data as APIv2.IQuoteListResponseBody | APIv2.IReplyListResponseBody); } - cache.addUsers(data as IThreadListResponseData & IQuoteListResponseData & IReplyListResponseData); + cache.addUsers(data as APIv2.IThreadListResponseBody & APIv2.IQuoteListResponseBody & APIv2.IReplyListResponseBody); } } }); diff --git a/src/models/Cache.ts b/src/models/Cache.ts index 09345d36..b97bc7ee 100644 --- a/src/models/Cache.ts +++ b/src/models/Cache.ts @@ -1,4 +1,4 @@ -import type { IPost, IQuoteListResponseData, IReplyListResponseData, IThread, IThreadListResponseData, IUser } from '../types/lihkg'; +import type { APIv2, IPost, IThread, IUser } from '../types/lihkg'; type TThreads = { [thread: string]: IThread | undefined; }; type TReplies = { [post: string]: IPost | undefined; }; @@ -21,7 +21,7 @@ class Cache { return this.users[id]; } - addThreads (data: IThreadListResponseData) { + addThreads (data: APIv2.IThreadListResponseBody) { const { items } = data.response; for (const item of items) { const { thread_id: threadID } = item; @@ -29,7 +29,7 @@ class Cache { } }; - addReplies (data: IReplyListResponseData | IQuoteListResponseData) { + addReplies (data: APIv2.IReplyListResponseBody | APIv2.IQuoteListResponseBody) { const { item_data: items } = data.response; for (const item of items) { const { post_id: postID } = item; @@ -37,7 +37,7 @@ class Cache { } } - addUsers (data: IThreadListResponseData & IReplyListResponseData & IQuoteListResponseData) { + addUsers (data: APIv2.IThreadListResponseBody & APIv2.IReplyListResponseBody & APIv2.IQuoteListResponseBody) { const items = data.response.items || data.response.item_data; for (const item of items) { const { user } = item; diff --git a/src/types/lihkg.ts b/src/types/lihkg.ts index ced75ef2..981aa101 100644 --- a/src/types/lihkg.ts +++ b/src/types/lihkg.ts @@ -1,27 +1,42 @@ -export interface IQuoteListResponseData { - success: number; - server_time: number; - response: IQuoteListResponse; -} - -interface IQuoteListResponse { - page: string; - item_data: IPost[]; - thread: IThread; - parent_post: IPost; - me: IMeUser; -} - -export interface IReplyListResponseData { - success: number; - server_time: number; - response: IReplyListResponse; -} - -interface IReplyListResponse extends IThread { - page: string; - item_data: IPost[]; - me: IMeUser; +export module APIv2 { + export interface IQuoteListResponseBody { + success: number; + server_time: number; + response: IQuoteListResponse; + } + + interface IQuoteListResponse { + page: string; + item_data: IPost[]; + thread: IThread; + parent_post: IPost; + me: IMeUser; + } + + export interface IReplyListResponseBody { + success: number; + server_time: number; + response: IReplyListResponse; + } + + interface IReplyListResponse extends IThread { + page: string; + item_data: IPost[]; + me: IMeUser; + } + + export interface IThreadListResponseBody { + success: number; + server_time: number; + response: IThreadListResponse; + } + + interface IThreadListResponse { + category: ICategory; + is_pagination: boolean; + items: IThread[]; + me: IMeUser; + } } export interface IPost { @@ -53,19 +68,6 @@ interface IPostRemark { is_not_push_post?: boolean; } -export interface IThreadListResponseData { - success: number; - server_time: number; - response: IThreadListResponse; -} - -interface IThreadListResponse { - category: ICategory; - is_pagination: boolean; - items: IThread[]; - me: IMeUser; -} - export interface IThread { thread_id: string; cat_id: string; From 0f1c315325bd9871b7ebb98beb624f57a682c37b Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 02:56:47 +0800 Subject: [PATCH 11/58] refactor(typings): update `findReduxStore` typings --- src/helpers/lihkg.ts | 6 +++--- src/helpers/redux.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/helpers/lihkg.ts b/src/helpers/lihkg.ts index 72f420a7..7ac4f88f 100644 --- a/src/helpers/lihkg.ts +++ b/src/helpers/lihkg.ts @@ -9,7 +9,7 @@ import lihkgSelectors from '../stylesheets/variables/lihkg/selectors.module.scss import { IIconMap, ILocalNotifcation, ILocalNotifcationPayload, IState, IUser, NotificationType, TNotification } from '../types/lihkg'; import { counter } from './counter'; import { waitForElement } from './dom'; -import { findReduxStore, IReactRootElement } from './redux'; +import { findReduxStore } from './redux'; enum ShareType { Thread = 1, @@ -52,8 +52,8 @@ export const waitForRightPanelContainer = async () => { * get the original LIHKG redux store */ export const getStore = () => { - const app = document.querySelector(lihkgSelectors.app); - const store = findReduxStore(app as IReactRootElement); + const app = document.querySelector(lihkgSelectors.app)!; + const store = findReduxStore(app); return store; }; diff --git a/src/helpers/redux.ts b/src/helpers/redux.ts index 713eeaa3..05dadbd8 100644 --- a/src/helpers/redux.ts +++ b/src/helpers/redux.ts @@ -1,14 +1,14 @@ import type { Action, Store } from '@reduxjs/toolkit'; // import retry from './retry'; -export interface IReactRootElement extends Element { - _reactRootContainer: any; +interface IReactRootElement extends Element { + _reactRootContainer?: any; } -export const findReduxStore = (root: IReactRootElement): Store | null => { +export const findReduxStore = (root: E): Store | null => { let base; try { - base = root._reactRootContainer._internalRoot.current; + base = root._reactRootContainer?._internalRoot.current; } catch (err) { // do nothing } From cb910c76f0c610bd663b5064cb92274707b06500 Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 02:58:55 +0800 Subject: [PATCH 12/58] chore(helpers): housekeeping --- src/helpers/merge.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/merge.test.ts b/src/helpers/merge.test.ts index 31960bf9..008729a1 100644 --- a/src/helpers/merge.test.ts +++ b/src/helpers/merge.test.ts @@ -3,7 +3,6 @@ import type { ISerializedDataSet } from '../models/DataSet'; import type { ISerializedSubscription } from '../models/Subscription'; import { mergeConfig, mergeDataSet, mergeSubscriptions } from './merge'; - describe('mergeConfig', () => { it('should unlock icon map (do nothing)', () => { const configA: ISerializedConfig = { From 9b588f3d51b57d5f59d03b9a192851f80a7bed4f Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 03:41:17 +0800 Subject: [PATCH 13/58] feat(helpers/lihkg): add `mapBlockedUsersToDataSet()` --- package.json | 2 ++ pnpm-lock.yaml | 18 +++++++++++--- src/constants/texts.ts | 3 +++ src/helpers/lihkg.test.ts | 50 +++++++++++++++++++++++++++++++++++++++ src/helpers/lihkg.ts | 23 ++++++++++++++++-- src/types/lihkg.ts | 31 ++++++++++++++++++++---- 6 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 src/helpers/lihkg.test.ts diff --git a/package.json b/package.json index da048e90..67622750 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "devDependencies": { "@svgr/webpack": "^6.2.1", + "@types/chance": "^1.1.3", "@types/debug": "^4.1.7", "@types/gapi": "^0.0.41", "@types/gapi.auth2": "^0.0.55", @@ -81,6 +82,7 @@ "@types/semver": "^7.3.6", "@types/tailwindcss": "^3.0.10", "@types/webpack-env": "^1.16.0", + "chance": "^1.1.8", "css-loader": "^5.2.0", "cssnano": "^5.1.4", "dotenv": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e31f855..acc8de83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@fortawesome/react-fontawesome': ^0.1.18 '@reduxjs/toolkit': ^1.5.1 '@svgr/webpack': ^6.2.1 + '@types/chance': ^1.1.3 '@types/debug': ^4.1.7 '@types/gapi': ^0.0.41 '@types/gapi.auth2': ^0.0.55 @@ -27,6 +28,7 @@ specifiers: '@types/semver': ^7.3.6 '@types/tailwindcss': ^3.0.10 '@types/webpack-env': ^1.16.0 + chance: ^1.1.8 classnames: ^2.3.1 css-loader: ^5.2.0 cssnano: ^5.1.4 @@ -110,6 +112,7 @@ dependencies: devDependencies: '@svgr/webpack': 6.2.1 + '@types/chance': 1.1.3 '@types/debug': 4.1.7 '@types/gapi': 0.0.41 '@types/gapi.auth2': 0.0.55 @@ -128,6 +131,7 @@ devDependencies: '@types/semver': 7.3.9 '@types/tailwindcss': 3.0.10 '@types/webpack-env': 1.16.3 + chance: 1.1.8 css-loader: 5.2.7_webpack@5.72.0 cssnano: 5.1.4_postcss@8.3.11 dotenv: 10.0.0 @@ -1776,8 +1780,8 @@ packages: resolution: {integrity: sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==} dev: true - /@maxim_mazurok/gapi.client.drive/3.0.20220410: - resolution: {integrity: sha512-rVIsk2MAkBxE+tYvQmd0BXJNb6Kq529OT7HWCY8icVv6Hc+vIouLyzhfdjtf91USNPAAFV1epCg5akQfjladBw==} + /@maxim_mazurok/gapi.client.drive/3.0.20220417: + resolution: {integrity: sha512-UDyqIGhUls+zSgMhSvpFavZxWEEPECXkEs+WExKTWkRKNaa2EMVRLfHs9SQ0rAe9LqwyEsdh3glAgpa/7KAfMQ==} dependencies: '@types/gapi.client': 1.0.5 dev: true @@ -2081,6 +2085,10 @@ packages: browserslist: 4.20.2 dev: true + /@types/chance/1.1.3: + resolution: {integrity: sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==} + dev: true + /@types/connect-history-api-fallback/1.3.5: resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} dependencies: @@ -2144,7 +2152,7 @@ packages: /@types/gapi.client.drive/3.0.13: resolution: {integrity: sha512-JFkHqQj9RA8MpvfvwdX8VU0Sbu7FMGqBR9dAmkocekWdD/y5Z/a/A6Y37EEqKW/dT8t7F3RpgUDUPlhp3snAlw==} dependencies: - '@maxim_mazurok/gapi.client.drive': 3.0.20220410 + '@maxim_mazurok/gapi.client.drive': 3.0.20220417 dev: true /@types/gapi.client/1.0.5: @@ -3099,6 +3107,10 @@ packages: supports-color: 7.2.0 dev: true + /chance/1.1.8: + resolution: {integrity: sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg==} + dev: true + /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} diff --git a/src/constants/texts.ts b/src/constants/texts.ts index 8b4460fe..ccef5a5c 100644 --- a/src/constants/texts.ts +++ b/src/constants/texts.ts @@ -18,6 +18,9 @@ export const BUTTON_TEXT_LABEL_SOURCE = '來源'; export const BUTTON_TEXT_LABEL_IMAGE = '相關圖片'; export const BUTTON_TEXT_SNIPE = '一鍵狙擊'; +/* label */ +export const BLOCKED_USER_DEFAULT_LABEL_TEXT = '封鎖會員'; + /* label form */ export const LABEL_FORM_MODAL_TITLE_ADD_LABEL = '新增標籤'; export const LABEL_FORM_MODAL_TITLE_EDIT_LABEL = '修改標籤'; diff --git a/src/helpers/lihkg.test.ts b/src/helpers/lihkg.test.ts new file mode 100644 index 00000000..84323e15 --- /dev/null +++ b/src/helpers/lihkg.test.ts @@ -0,0 +1,50 @@ +import Chance from 'chance'; +import times from 'lodash/times'; +import * as TEXTS from '../constants/texts'; +import { Gender, IBlockedUser, LevelName } from '../types/lihkg'; +import { mapBlockedUsersToDataSet } from './lihkg'; + +const chance = new Chance(); + +const generateBlockedUser = (id: string) => { + const nickname = chance.name(); + const blockedUser: IBlockedUser = { + user_id: id, + nickname, + level: '10', + gender: Gender.M, + status: '1', + create_time: chance.timestamp(), + level_name: LevelName.Normal, + is_following: chance.bool(), + is_blocked: true, + is_disappear: chance.bool(), + is_newbie: chance.bool(), + blocked_time: Math.round(chance.timestamp() / 1000), + block_remark: { + nickname, + reason: chance.string() + } + }; + return blockedUser; +}; + +describe('mapBlockedUsersToDataSet', () => { + it('should map blocked users to data set', () => { + const blockedUsers = times(100, (index) => generateBlockedUser(`${index}`)); + const dataSet = mapBlockedUsersToDataSet(blockedUsers); + const users = Object.keys(dataSet.data); + expect(users.length).toEqual(blockedUsers.length); + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const blockedUser = blockedUsers[i] + const labels = dataSet.data[user]!; + expect(labels.length).toEqual(1); + const label = labels[0]; + expect(label.id).toEqual('1'); + expect(label.text).toEqual(TEXTS.BLOCKED_USER_DEFAULT_LABEL_TEXT); + expect(blockedUser.blocked_time * 1000).toEqual(label.date); + expect(blockedUser.block_remark.reason).toEqual(label.reason); + } + }); +}); diff --git a/src/helpers/lihkg.ts b/src/helpers/lihkg.ts index 7ac4f88f..bd3030bb 100644 --- a/src/helpers/lihkg.ts +++ b/src/helpers/lihkg.ts @@ -4,9 +4,11 @@ import { dev } from '../../config/config'; import { displayName } from '../../package.json'; import type { TActions } from '../actions/lihkg'; import * as lihkgActions from '../actions/lihkg'; -import { ISource } from '../models/Label'; +import * as TEXTS from '../constants/texts'; +import Label, { ISource } from '../models/Label'; +import Personal from '../models/Personal'; import lihkgSelectors from '../stylesheets/variables/lihkg/selectors.module.scss'; -import { IIconMap, ILocalNotifcation, ILocalNotifcationPayload, IState, IUser, NotificationType, TNotification } from '../types/lihkg'; +import { IBlockedUser, IIconMap, ILocalNotifcation, ILocalNotifcationPayload, IState, IUser, NotificationType, TNotification } from '../types/lihkg'; import { counter } from './counter'; import { waitForElement } from './dom'; import { findReduxStore } from './redux'; @@ -154,6 +156,23 @@ export const removeNotification = (id: number) => { dispatch(lihkgActions.removeNotification(id)); }; +/** + * convert blocked users to personal data set + * @param {IBlockedUser[]} blockedUsers the blocked user list from LIHKG + */ +export const mapBlockedUsersToDataSet = (blockedUsers: IBlockedUser[]) => { + const dataSet = Personal.factory(); + const { data } = dataSet; + for (const blockedUser of blockedUsers) { + const { user_id, blocked_time, block_remark } = blockedUser; + const { reason } = block_remark; + const date = blocked_time * 1000; + const label = new Label('1', TEXTS.BLOCKED_USER_DEFAULT_LABEL_TEXT, reason, undefined, date); + data[user_id] = [label]; + } + return dataSet; +}; + /* debug */ if (dev) { (window as any).__LIBEL_DEBUG_LIHKG_GET_STORE__ = getStore; diff --git a/src/types/lihkg.ts b/src/types/lihkg.ts index 981aa101..839bef72 100644 --- a/src/types/lihkg.ts +++ b/src/types/lihkg.ts @@ -37,6 +37,17 @@ export module APIv2 { items: IThread[]; me: IMeUser; } + + export interface IBlockedUserResponseBody { + success: number; + server_time: number; + response: IBlockedUserResponse; + } + + interface IBlockedUserResponse { + blocked_user_list: IBlockedUser[]; + me: IMeUser; + } } export interface IPost { @@ -137,6 +148,16 @@ interface IThreadRemark { cover_img?: string; } +export interface IBlockedUser extends IUser { + blocked_time: number; + block_remark: IBlockUserRemark; +} + +interface IBlockUserRemark { + nickname?: string; + reason: string; +} + /** * LIHKG state type * @todo add other properties @@ -208,15 +229,15 @@ interface IModeSetting { imageProxy: boolean; } -enum Gender { +export enum Gender { F = 'F', M = 'M', } -enum LevelName { - 新手會員 = '新手會員', - 普通會員 = '普通會員', - 站長 = '站長', +export enum LevelName { + Newbie = '新手會員', + Normal = '普通會員', + Admin = '站長', } export interface IUser { From 73d50138c59bc5e0cbdb1f96ee27259fcadf8c27 Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 03:41:32 +0800 Subject: [PATCH 14/58] feat(apis/lihkg): add `fetchBlockedUser()` --- src/apis/lihkg.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/apis/lihkg.ts diff --git a/src/apis/lihkg.ts b/src/apis/lihkg.ts new file mode 100644 index 00000000..7e8f0826 --- /dev/null +++ b/src/apis/lihkg.ts @@ -0,0 +1,10 @@ +import type { APIv2 } from '../types/lihkg'; + +const baseURL = 'https://lihkg.com/api_v2'; + +export const fetchBlockedUser = async () => { + const url = `${baseURL}/me/blocked-user`; + const response = await fetch(url); + const json = await response.json(); + return json as APIv2.IBlockedUserResponseBody; +}; From 93500169d3241348dd2d58959311371a27b7d4eb Mon Sep 17 00:00:00 2001 From: kitce Date: Sun, 24 Apr 2022 03:44:13 +0800 Subject: [PATCH 15/58] style(helpers/lihkg): missing semi-colon --- src/helpers/lihkg.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/lihkg.test.ts b/src/helpers/lihkg.test.ts index 84323e15..80630eba 100644 --- a/src/helpers/lihkg.test.ts +++ b/src/helpers/lihkg.test.ts @@ -37,7 +37,7 @@ describe('mapBlockedUsersToDataSet', () => { expect(users.length).toEqual(blockedUsers.length); for (let i = 0; i < users.length; i++) { const user = users[i]; - const blockedUser = blockedUsers[i] + const blockedUser = blockedUsers[i]; const labels = dataSet.data[user]!; expect(labels.length).toEqual(1); const label = labels[0]; From 8f92fd81f433aae24bdb5f32a241592e6f549fcd Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 29 Apr 2022 00:56:04 +0800 Subject: [PATCH 16/58] refactor(settings): separate the buttons in `ManageDataSection` into standalone components --- .../FileInput/FileInput.module.scss | 2 +- .../ManageDataSection/EditDataSetButton.tsx | 91 ++++++++ .../ManageDataSection/ExportFileButton.tsx | 40 ++++ .../ManageDataSection/ImportFileButton.tsx | 82 +++++++ .../MakeSubscriptionButton.tsx | 60 +++++ .../ManageDataSection.module.scss | 19 -- .../ManageDataSection/ManageDataSection.tsx | 218 ++---------------- 7 files changed, 288 insertions(+), 224 deletions(-) create mode 100644 src/components/Settings/ManageDataSection/EditDataSetButton.tsx create mode 100644 src/components/Settings/ManageDataSection/ExportFileButton.tsx create mode 100644 src/components/Settings/ManageDataSection/ImportFileButton.tsx create mode 100644 src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx delete mode 100644 src/components/Settings/ManageDataSection/ManageDataSection.module.scss diff --git a/src/components/FileInput/FileInput.module.scss b/src/components/FileInput/FileInput.module.scss index 149207d8..f625ed42 100644 --- a/src/components/FileInput/FileInput.module.scss +++ b/src/components/FileInput/FileInput.module.scss @@ -4,7 +4,7 @@ label { &:hover { @apply cursor-pointer; - @apply underline; + // @apply underline; } } diff --git a/src/components/Settings/ManageDataSection/EditDataSetButton.tsx b/src/components/Settings/ManageDataSection/EditDataSetButton.tsx new file mode 100644 index 00000000..d327ca6c --- /dev/null +++ b/src/components/Settings/ManageDataSection/EditDataSetButton.tsx @@ -0,0 +1,91 @@ +import debugFactory from 'debug'; +import React, { useCallback, useState } from 'react'; +import * as TEXTS from '../../../constants/texts'; +import * as LIHKG from '../../../helpers/lihkg'; +import useSettingsModalFocusTrap from '../../../hooks/useSettingsModalFocusTrap'; +import { selectPersonal } from '../../../store/selectors'; +import { actions as personalActions } from '../../../store/slices/personal'; +import { useTypedDispatch, useTypedSelector } from '../../../store/store'; +import type { IProps as IDataSetEditorProps } from '../../DataSetEditor/DataSetEditor'; +import DataSetEditorModal from '../../DataSetEditorModal/DataSetEditorModal'; +import SettingOptionButton from '../SettingOptionButton/SettingOptionButton'; + +const debug = debugFactory('libel:component:EditDataSetButton'); + +const EditDataSetButton: React.FunctionComponent = () => { + const dispatch = useTypedDispatch(); + const personal = useTypedSelector(selectPersonal); + const [open, setOpen] = useState(false); + const [dirty, setDirty] = useState(false); + const settingsModalFocusTrap = useSettingsModalFocusTrap(); + + const handleClick: React.MouseEventHandler = useCallback((event) => { + event.preventDefault(); + const users = Object.keys(personal.data); + if (users.length > 0) { + settingsModalFocusTrap?.pause(); + window.requestAnimationFrame(() => { + setOpen(true); + setDirty(false); + }); + } else { + const notification = LIHKG.createLocalNotification(TEXTS.DATA_SET_EDITOR_MESSAGE_EMPTY_DATA_SET); + LIHKG.showNotification(notification); + } + }, [settingsModalFocusTrap, personal]); + + const handleClose = useCallback(() => { + if (dirty) { + const users = Object.keys(personal.data); // empty data set + if (users.length > 0) { + const yes = window.confirm(TEXTS.DATA_SET_EDITOR_MESSAGE_CLOSE_CONFIRMATION); + if (!yes) { + return; + } + } + } + settingsModalFocusTrap?.unpause(); + window.requestAnimationFrame(() => { + setOpen(false); + }); + }, [personal, dirty, settingsModalFocusTrap]); + + const handleChange: IDataSetEditorProps['onChange'] = useCallback(() => { + setDirty(true); + }, []); + + const handleSubmit: IDataSetEditorProps['onSubmit'] = useCallback((dataSet) => { + const confirmed = window.confirm(TEXTS.DATA_SET_EDITOR_MESSAGE_SAVE_CONFIRMATION); + if (confirmed) { + debug('handleDataSetEditorSubmit', dataSet); + dispatch(personalActions.update(dataSet)); + settingsModalFocusTrap?.unpause(); + window.requestAnimationFrame(() => { + setOpen(false); + }); + const notification = LIHKG.createLocalNotification(TEXTS.DATA_SET_EDITOR_MESSAGE_SAVE_SUCCESS); + LIHKG.showNotification(notification); + } + }, [settingsModalFocusTrap]); + + return ( + + + {TEXTS.BUTTON_TEXT_EDIT_DATA_SET} + + + + ); +}; + +EditDataSetButton.displayName = 'EditDataSetButton'; + +export default EditDataSetButton; diff --git a/src/components/Settings/ManageDataSection/ExportFileButton.tsx b/src/components/Settings/ManageDataSection/ExportFileButton.tsx new file mode 100644 index 00000000..67b68fc4 --- /dev/null +++ b/src/components/Settings/ManageDataSection/ExportFileButton.tsx @@ -0,0 +1,40 @@ +// import debugFactory from 'debug'; +import { render } from 'mustache'; +import React, { useCallback } from 'react'; +import * as TEXTS from '../../../constants/texts'; +import { _export } from '../../../helpers/file'; +import * as gtag from '../../../helpers/gtag'; +import * as LIHKG from '../../../helpers/lihkg'; +import * as messages from '../../../templates/messages'; +import { EventAction } from '../../../types/ga'; +import SettingOptionButton from '../SettingOptionButton/SettingOptionButton'; + +// const debug = debugFactory('libel:component:ExportFileButton'); + +const ExportFileButton: React.FunctionComponent = () => { + const handleExport: React.MouseEventHandler = useCallback(async (event) => { + event.preventDefault(); + try { + const data = await _export(); + const { personal, subscriptions } = data; + const { users, labels } = personal.aggregate(); + const message = render(messages.success.export, { users, labels, subscriptions }); + const notification = LIHKG.createLocalNotification(message); + LIHKG.showNotification(notification); + // analytics + gtag.event(EventAction.Export); + } catch (err) { + // analytics + gtag.event(EventAction.Error, { event_category: EventAction.Export }); + } + }, []); + return ( + + {TEXTS.BUTTON_TEXT_EXPORT_FILE} + + ); +}; + +ExportFileButton.displayName = 'ExportFileButton'; + +export default ExportFileButton; diff --git a/src/components/Settings/ManageDataSection/ImportFileButton.tsx b/src/components/Settings/ManageDataSection/ImportFileButton.tsx new file mode 100644 index 00000000..d8a14ad4 --- /dev/null +++ b/src/components/Settings/ManageDataSection/ImportFileButton.tsx @@ -0,0 +1,82 @@ +// import debugFactory from 'debug'; +import { render } from 'mustache'; +import React, { useCallback } from 'react'; +import * as TEXTS from '../../../constants/texts'; +import { _import } from '../../../helpers/file'; +import * as gtag from '../../../helpers/gtag'; +import * as LIHKG from '../../../helpers/lihkg'; +import { mergeConfig, mergeDataSet, mergeSubscriptions } from '../../../helpers/merge'; +import Personal from '../../../models/Personal'; +import type { ISerializedStorage } from '../../../models/Storage'; +import { selectConfig, selectPersonal, selectSubscriptions } from '../../../store/selectors'; +import { loadDataIntoStore, useTypedSelector } from '../../../store/store'; +import lihkgCssClasses from '../../../stylesheets/variables/lihkg/classes.module.scss'; +import * as messages from '../../../templates/messages'; +import { EventAction } from '../../../types/ga'; +import FileInput from '../../FileInput/FileInput'; + +// const debug = debugFactory('libel:component:ImportFileButton'); + +const accepts = [ + 'text/plain', + '.json', + '.txt' +]; + +const ImportFileButton: React.FunctionComponent = () => { + const config = useTypedSelector(selectConfig); + const personal = useTypedSelector(selectPersonal); + const subscriptions = useTypedSelector(selectSubscriptions); + + const handleImport: React.ChangeEventHandler = useCallback(async (event) => { + event.preventDefault(); + const { files } = event.target; + const file = files?.item(0); + if (file) { + try { + const incoming = await _import(file); + const storage: ISerializedStorage = { + config: mergeConfig(config, incoming.config, false), + // CAVEAT: ignore `meta` here + personal: mergeDataSet(personal.plain(), incoming.personal, false), + subscriptions: mergeSubscriptions(subscriptions, incoming.subscriptions, false) + }; + // load the merged data into the store + await loadDataIntoStore(storage); + const { users, labels } = Personal.aggregate(incoming.personal); + const _message = render(messages.success.import, { users, labels, subscriptions: incoming.subscriptions }); + const notification = LIHKG.createLocalNotification(_message); + LIHKG.showNotification(notification); + // analytics + gtag.event(EventAction.Import); + } catch (err) { + if (typeof err === 'string') { + const notification = LIHKG.createLocalNotification(err); + LIHKG.showNotification(notification); + } else { + console.error(err); + const notification = LIHKG.createLocalNotification(TEXTS.IMPORT_FILE_ERROR_GENERIC_ERROR); + LIHKG.showNotification(notification); + } + // analytics + gtag.event(EventAction.Error, { event_category: EventAction.Import }); + } + } + event.target.value = ''; + }, [config, personal, subscriptions]); + + return ( + + + {TEXTS.BUTTON_TEXT_IMPORT_FILE} + + + ); +}; + +ImportFileButton.displayName = 'ImportFileButton'; + +export default ImportFileButton; diff --git a/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx b/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx new file mode 100644 index 00000000..04fb9a1c --- /dev/null +++ b/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx @@ -0,0 +1,60 @@ +// import debugFactory from 'debug'; +import React, { useCallback, useState } from 'react'; +import * as TEXTS from '../../../constants/texts'; +import { download } from '../../../helpers/file'; +import useSettingsModalFocusTrap from '../../../hooks/useSettingsModalFocusTrap'; +import { selectPersonal } from '../../../store/selectors'; +import { useTypedSelector } from '../../../store/store'; +import type { IProps as ISubscriptionMakerProps } from '../../SubscriptionMaker/SubscriptionMaker'; +import SubscriptionMakerModal from '../../SubscriptionMakerModal/SubscriptionMakerModal'; +import SettingOptionButton from '../SettingOptionButton/SettingOptionButton'; + +// const debug = debugFactory('libel:component:MakeSubscriptionButton'); + +const MakeSubscriptionButton: React.FunctionComponent = () => { + const personal = useTypedSelector(selectPersonal); + const [open, setOpen] = useState(false); + const settingsModalFocusTrap = useSettingsModalFocusTrap(); + + const handleClick: React.MouseEventHandler = useCallback((event) => { + event.preventDefault(); + settingsModalFocusTrap?.pause(); + window.requestAnimationFrame(() => { + setOpen(true); + }); + }, [settingsModalFocusTrap]); + + const handleClose = useCallback(() => { + settingsModalFocusTrap?.unpause(); + window.requestAnimationFrame(() => { + setOpen(false); + }); + }, [settingsModalFocusTrap]); + + const handleSubmit: ISubscriptionMakerProps['onSubmit'] = useCallback((subscription) => { + const filename = `${subscription.name}.json`; + const json = JSON.stringify(subscription, null, 2); + download(filename, json, 'text/plain'); + // debug('handleSubscriptionMakerSubmit', json); + }, []); + + return ( + + + {TEXTS.BUTTON_TEXT_MAKE_SUBSCRIPTION} + + + + ); +}; + +MakeSubscriptionButton.displayName = 'MakeSubscriptionButton'; + +export default MakeSubscriptionButton; diff --git a/src/components/Settings/ManageDataSection/ManageDataSection.module.scss b/src/components/Settings/ManageDataSection/ManageDataSection.module.scss deleted file mode 100644 index b0e90648..00000000 --- a/src/components/Settings/ManageDataSection/ManageDataSection.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -.import { - // BaseInput label - > span { - @apply mr-0; - line-height: normal; - } - - // BaseInput - .input { - @apply hidden; - } - - .label { - &:hover { - @apply cursor-pointer; - @apply underline; - } - } -} diff --git a/src/components/Settings/ManageDataSection/ManageDataSection.tsx b/src/components/Settings/ManageDataSection/ManageDataSection.tsx index f5625c80..99971adf 100644 --- a/src/components/Settings/ManageDataSection/ManageDataSection.tsx +++ b/src/components/Settings/ManageDataSection/ManageDataSection.tsx @@ -1,225 +1,35 @@ -import classNames from 'classnames'; -import debugFactory from 'debug'; -import { render } from 'mustache'; -import React, { useCallback, useMemo, useState } from 'react'; +// import debugFactory from 'debug'; +import type React from 'react'; import * as TEXTS from '../../../constants/texts'; -import { download, _export, _import } from '../../../helpers/file'; -import * as gtag from '../../../helpers/gtag'; -import * as LIHKG from '../../../helpers/lihkg'; -import { mergeConfig, mergeDataSet, mergeSubscriptions } from '../../../helpers/merge'; -import useSettingsModalFocusTrap from '../../../hooks/useSettingsModalFocusTrap'; -import Personal from '../../../models/Personal'; -import type { ISerializedStorage } from '../../../models/Storage'; -import { selectConfig, selectPersonal, selectSubscriptions } from '../../../store/selectors'; -import { actions as personalActions } from '../../../store/slices/personal'; -import { loadDataIntoStore, useTypedDispatch, useTypedSelector } from '../../../store/store'; import lihkgCssClasses from '../../../stylesheets/variables/lihkg/classes.module.scss'; -import * as messages from '../../../templates/messages'; -import { EventAction } from '../../../types/ga'; -import type { IProps as IDataSetEditorProps } from '../../DataSetEditor/DataSetEditor'; -import DataSetEditorModal from '../../DataSetEditorModal/DataSetEditorModal'; -import FileInput from '../../FileInput/FileInput'; -import type { IProps as ISubscriptionMakerProps } from '../../SubscriptionMaker/SubscriptionMaker'; -import SubscriptionMakerModal from '../../SubscriptionMakerModal/SubscriptionMakerModal'; -import SettingOptionButton from '../SettingOptionButton/SettingOptionButton'; -import styles from './ManageDataSection.module.scss'; +import EditDataSetButton from './EditDataSetButton'; +import ExportFileButton from './ExportFileButton'; +import ImportFileButton from './ImportFileButton'; +import MakeSubscriptionButton from './MakeSubscriptionButton'; -const debug = debugFactory('libel:component:ManageDataSection'); - -const importInputAccepts = [ - 'text/plain', - '.json', - '.txt' -]; +// const debug = debugFactory('libel:component:ManageDataSection'); const ManageDataSection: React.FunctionComponent = () => { - const dispatch = useTypedDispatch(); - const config = useTypedSelector(selectConfig); - const personal = useTypedSelector(selectPersonal); - const subscriptions = useTypedSelector(selectSubscriptions); - const [isDataSetEditorModalOpened, setIsDataSetEditorModalOpened] = useState(false); - const [isSubscriptionMakerModalOpened, setIsSubscriptionMakerModalOpened] = useState(false); - const [isDataSetEditorDirty, setIsDataSetEditorDirty] = useState(false); - const settingsModalFocusTrap = useSettingsModalFocusTrap(); - - const personalDataUsers = useMemo(() => Object.keys(personal.data), [personal]); - - /* data set editor */ - const handleEditDataSetButtonClick: React.MouseEventHandler = useCallback((event) => { - event.preventDefault(); - if (personalDataUsers.length > 0) { - settingsModalFocusTrap?.pause(); - window.requestAnimationFrame(() => { - setIsDataSetEditorModalOpened(true); - setIsDataSetEditorDirty(false); - }); - } else { - const notification = LIHKG.createLocalNotification(TEXTS.DATA_SET_EDITOR_MESSAGE_EMPTY_DATA_SET); - LIHKG.showNotification(notification); - } - }, [settingsModalFocusTrap, personalDataUsers]); - - const handleDataSetEditorModalClose = useCallback(() => { - if (isDataSetEditorDirty) { - const users = Object.keys(personal.data); // empty data set - if (users.length > 0) { - const yes = window.confirm(TEXTS.DATA_SET_EDITOR_MESSAGE_CLOSE_CONFIRMATION); - if (!yes) { - return; - } - } - } - settingsModalFocusTrap?.unpause(); - window.requestAnimationFrame(() => { - setIsDataSetEditorModalOpened(false); - }); - }, [personal, isDataSetEditorDirty, settingsModalFocusTrap]); - - const handleDataSetEditorChange: IDataSetEditorProps['onChange'] = useCallback(() => { - setIsDataSetEditorDirty(true); - }, []); - - const handleDataSetEditorSubmit: IDataSetEditorProps['onSubmit'] = useCallback((dataSet) => { - const confirmed = window.confirm(TEXTS.DATA_SET_EDITOR_MESSAGE_SAVE_CONFIRMATION); - if (confirmed) { - debug('handleDataSetEditorSubmit', dataSet); - dispatch(personalActions.update(dataSet)); - settingsModalFocusTrap?.unpause(); - window.requestAnimationFrame(() => { - setIsDataSetEditorModalOpened(false); - }); - const notification = LIHKG.createLocalNotification(TEXTS.DATA_SET_EDITOR_MESSAGE_SAVE_SUCCESS); - LIHKG.showNotification(notification); - } - }, [settingsModalFocusTrap]); - - /* subscription maker */ - const handleMakeSubscriptionButtonClick: React.MouseEventHandler = useCallback((event) => { - event.preventDefault(); - settingsModalFocusTrap?.pause(); - window.requestAnimationFrame(() => { - setIsSubscriptionMakerModalOpened(true); - }); - }, [settingsModalFocusTrap]); - - const handleSubscriptionMakerModalClose = useCallback(() => { - settingsModalFocusTrap?.unpause(); - window.requestAnimationFrame(() => { - setIsSubscriptionMakerModalOpened(false); - }); - }, [settingsModalFocusTrap]); - - const handleSubscriptionMakerSubmit: ISubscriptionMakerProps['onSubmit'] = useCallback((subscription) => { - const filename = `${subscription.name}.json`; - const json = JSON.stringify(subscription, null, 2); - download(filename, json, 'text/plain'); - debug('handleSubscriptionMakerSubmit', json); - }, []); - - const handleExport: React.MouseEventHandler = useCallback(async (event) => { - event.preventDefault(); - try { - const data = await _export(); - // analytics - gtag.event(EventAction.Export); - const { personal, subscriptions } = data; - const { users, labels } = personal.aggregate(); - const message = render(messages.success.export, { users, labels, subscriptions }); - const notification = LIHKG.createLocalNotification(message); - LIHKG.showNotification(notification); - } catch (err) { - // analytics - gtag.event(EventAction.Error, { event_category: EventAction.Export }); - } - }, []); - - const handleImport: React.ChangeEventHandler = useCallback(async (event) => { - event.preventDefault(); - const { files } = event.target; - const file = files?.item(0); - if (file) { - try { - const incoming = await _import(file); - const storage: ISerializedStorage = { - config: mergeConfig(config, incoming.config, false), - // CAVEAT: ignore `meta` here - personal: mergeDataSet(personal.plain(), incoming.personal, false), - subscriptions: mergeSubscriptions(subscriptions, incoming.subscriptions, false) - }; - // load the merged data into the store - await loadDataIntoStore(storage); - // analytics - gtag.event(EventAction.Import); - const { users, labels } = Personal.aggregate(incoming.personal); - const _message = render(messages.success.import, { users, labels, subscriptions: incoming.subscriptions }); - const notification = LIHKG.createLocalNotification(_message); - LIHKG.showNotification(notification); - } catch (err) { - if (typeof err === 'string') { - const notification = LIHKG.createLocalNotification(err); - LIHKG.showNotification(notification); - } else { - console.error(err); - const notification = LIHKG.createLocalNotification(TEXTS.IMPORT_FILE_ERROR_GENERIC_ERROR); - LIHKG.showNotification(notification); - } - // analytics - gtag.event(EventAction.Error, { event_category: EventAction.Import }); - } - } - event.target.value = ''; - }, [config, personal, subscriptions]); - return ( - + <> {TEXTS.SETTINGS_TITLE_MANAGE_DATA}
  • - - {TEXTS.BUTTON_TEXT_EDIT_DATA_SET} - +
  • - - {TEXTS.BUTTON_TEXT_MAKE_SUBSCRIPTION} - +
  • - - {TEXTS.BUTTON_TEXT_EXPORT_FILE} - +
  • -
  • - - - {TEXTS.BUTTON_TEXT_IMPORT_FILE} - - +
  • +
- - -
+ ); }; From aad49d38b585e1859b16516435919ba0dabde1bb Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 29 Apr 2022 01:04:25 +0800 Subject: [PATCH 17/58] chore(components): revise imports --- .../AddLabelButton/AddLabelButton.tsx | 7 +- src/components/Announcement/Announcement.tsx | 3 +- .../BaseIconButton/BaseIconButton.tsx | 28 ++++--- src/components/BaseInput/BaseInput.tsx | 5 +- .../DataSetEditor/DataSetEditor.tsx | 75 +++++++++---------- .../DataSetEditor/Filter/Filter.tsx | 3 +- .../UserLabelsEditor/Loading.tsx | 6 +- .../UserLabelsEditor/UserLabelsEditor.tsx | 9 ++- .../DataSetEditorModal/DataSetEditorModal.tsx | 3 +- .../EditLabelButton/EditLabelButton.tsx | 7 +- src/components/LabelForm/LabelForm.tsx | 3 +- .../LabelFormModal/LabelFormModal.tsx | 3 +- src/components/LabelInfo/LabelInfo.tsx | 3 +- src/components/LabelItem/LabelItem.tsx | 5 +- .../GroupedLabelItem/GroupedLabelItem.tsx | 7 +- .../LabelList/LabelInfoList/LabelInfoList.tsx | 5 +- src/components/LabelList/LabelList.tsx | 3 +- src/components/Modal/Body.tsx | 3 +- src/components/Modal/Header.tsx | 3 +- src/components/Modal/IDsContext.ts | 4 +- src/components/Modal/Modal.tsx | 3 +- .../RemoveLabelButton/RemoveLabelButton.tsx | 3 +- src/components/Select/Select.tsx | 3 +- .../ClearDataSection/ClearDataSection.tsx | 7 +- .../CloudSyncSection/CloudSyncSection.tsx | 6 +- .../SyncWithGoogleDrive.tsx | 15 ++-- .../ManageDataSection/EditDataSetButton.tsx | 7 +- .../ManageDataSection/ExportFileButton.tsx | 3 +- .../ManageDataSection/ImportFileButton.tsx | 3 +- .../MakeSubscriptionButton.tsx | 7 +- .../AddSubscriptionButton.tsx | 3 +- .../ReloadSubscriptionButton.tsx | 3 +- .../RemoveSubscriptionButton.tsx | 3 +- .../SubscriptionItem/SubscriptionItem.tsx | 3 +- .../SubscriptionSection.tsx | 7 +- .../ToggleSubscriptionButton.tsx | 3 +- .../SettingsModal/SettingsModal.tsx | 3 +- .../SettingsModalFocusTrapContext.ts | 4 +- .../SettingsModalToggleButton.tsx | 7 +- src/components/SnipeButton/SnipeButton.tsx | 3 +- .../SubmissionForm/SubmissionForm.tsx | 2 +- .../SubscriptionMaker/SubscriptionMaker.tsx | 3 +- .../SubscriptionMakerModal.tsx | 3 +- src/components/TextInput/TextInput.tsx | 5 +- .../UnlockIconMapToggleButton.tsx | 3 +- 45 files changed, 166 insertions(+), 131 deletions(-) diff --git a/src/components/AddLabelButton/AddLabelButton.tsx b/src/components/AddLabelButton/AddLabelButton.tsx index 548486eb..a8177284 100644 --- a/src/components/AddLabelButton/AddLabelButton.tsx +++ b/src/components/AddLabelButton/AddLabelButton.tsx @@ -1,6 +1,7 @@ import { faTag } from '@fortawesome/free-solid-svg-icons/faTag'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import { uploadImage } from '../../apis/nacx'; import * as ATTRIBUTES from '../../constants/attributes'; import * as TEXTS from '../../constants/texts'; @@ -84,7 +85,7 @@ const AddLabelButton: React.FunctionComponent = (props) => { }, [user, post, handleLabelFormModalClose]); return ( - + <> } @@ -108,7 +109,7 @@ const AddLabelButton: React.FunctionComponent = (props) => { /> ) } - + ); }; diff --git a/src/components/Announcement/Announcement.tsx b/src/components/Announcement/Announcement.tsx index c9589e5e..2c7e630f 100644 --- a/src/components/Announcement/Announcement.tsx +++ b/src/components/Announcement/Announcement.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import max from 'lodash/max'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-use'; import logo from '../../../assets/logos/libel.png'; import { displayName } from '../../../package.json'; diff --git a/src/components/BaseIconButton/BaseIconButton.tsx b/src/components/BaseIconButton/BaseIconButton.tsx index 42be9113..9518a608 100644 --- a/src/components/BaseIconButton/BaseIconButton.tsx +++ b/src/components/BaseIconButton/BaseIconButton.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React from 'react'; +import type React from 'react'; import Button, { TProps as TButtonProps } from '../Button/Button'; import Icon from '../Icon/Icon'; import type { IconName } from '../Icon/types'; @@ -18,20 +18,18 @@ const BaseIconButton: React.FunctionComponent = (props) => { {...otherProps} className={classNames(className, styles.baseIconButton)} > - - { - typeof icon === 'string' ? ( - - ) : ( - {icon} - ) - } - { - children && ( - {children} - ) - } - + { + typeof icon === 'string' ? ( + + ) : ( + {icon} + ) + } + { + children && ( + {children} + ) + } ); }; diff --git a/src/components/BaseInput/BaseInput.tsx b/src/components/BaseInput/BaseInput.tsx index ac503afc..f6322a48 100644 --- a/src/components/BaseInput/BaseInput.tsx +++ b/src/components/BaseInput/BaseInput.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useId } from 'react'; +import type React from 'react'; +import { forwardRef, useId } from 'react'; import ErrorMessage from '../ErrorMessage/ErrorMessage'; import styles from './BaseInput.module.scss'; @@ -11,7 +12,7 @@ type TComponentProps = React.ComponentPropsWithoutRef<'input'>; export type TProps = IProps & TComponentProps; -const BaseInput = React.forwardRef((props, ref) => { +const BaseInput = forwardRef((props, ref) => { const { id, className, error, ...otherProps } = props; const _id = id || useId(); diff --git a/src/components/DataSetEditor/DataSetEditor.tsx b/src/components/DataSetEditor/DataSetEditor.tsx index 6ff9e270..9bd76b14 100644 --- a/src/components/DataSetEditor/DataSetEditor.tsx +++ b/src/components/DataSetEditor/DataSetEditor.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; import debugFactory from 'debug'; import produce from 'immer'; -import React, { useCallback, useMemo, useState } from 'react'; +import type React from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { namespace } from '../../../package.json'; import * as TEXTS from '../../constants/texts'; import { filterLabelsGroupsByKeyword, findLabelsGroupByUser, mapDataSetToLabelsGroupsGroupedByUser, mapLabelsGroupsGroupedByUserToDataSet } from '../../helpers/dataSetEditor'; @@ -125,46 +126,44 @@ const DataSetEditor: React.FunctionComponent = (props) => { } onSubmit={handleSubmit} > - - -
- { - filteredLabelsGroups.length > 0 ? ( -
    - { - filteredLabelsGroups.map(({ user, items }, index) => ( -
  1. - -
  2. - )) - } -
- ) : ( - TEXTS.DATA_SET_EDITOR_FILTER_MESSAGE_EMPTY_RESULT - ) - } -
+ +
{ - !!error && ( - - {error} - + filteredLabelsGroups.length > 0 ? ( +
    + { + filteredLabelsGroups.map(({ user, items }, index) => ( +
  1. + +
  2. + )) + } +
+ ) : ( + TEXTS.DATA_SET_EDITOR_FILTER_MESSAGE_EMPTY_RESULT ) } - +
+ { + !!error && ( + + {error} + + ) + } ); }; diff --git a/src/components/DataSetEditor/Filter/Filter.tsx b/src/components/DataSetEditor/Filter/Filter.tsx index 617cf6b5..d05109b5 100644 --- a/src/components/DataSetEditor/Filter/Filter.tsx +++ b/src/components/DataSetEditor/Filter/Filter.tsx @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import { Key } from 'ts-key-enum'; import useFocus from '../../../hooks/useFocus'; import { IconName } from '../../Icon/types'; diff --git a/src/components/DataSetEditor/UserLabelsEditor/Loading.tsx b/src/components/DataSetEditor/UserLabelsEditor/Loading.tsx index 61c122f3..c48a1f41 100644 --- a/src/components/DataSetEditor/UserLabelsEditor/Loading.tsx +++ b/src/components/DataSetEditor/UserLabelsEditor/Loading.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type React from 'react'; import Placeholder from '../../Placeholder/Placeholder'; import styles from './Loading.module.scss'; @@ -8,10 +8,10 @@ type TProps = IProps; const Loading: React.FunctionComponent = () => { return ( - + <> - + ); }; diff --git a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx index 34a2465c..9997bd3c 100644 --- a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx +++ b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import debugFactory from 'debug'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import type React from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Key } from 'ts-key-enum'; import * as TEXTS from '../../../constants/texts'; import { ILabelsGroupItem, mapLabelsGroupItemsToErrorStates } from '../../../helpers/dataSetEditor'; @@ -47,7 +48,7 @@ type TProps = IProps & TComponentProps; const debug = debugFactory('libel:component:DataSetEditor:UserLabelsEditor'); -const UserLabelsEditor: React.FunctionComponent = React.memo((props) => { +const UserLabelsEditor: React.FunctionComponent = memo((props) => { const { className, user, items, autoScrollItemIndex = -1, onChange, onRemove, onScroll } = props; const [style, setStyle] = useState({}); @@ -115,7 +116,7 @@ const UserLabelsEditor: React.FunctionComponent = React.memo((props) =>
{ visible ? ( - + <>
@@ -181,7 +182,7 @@ const UserLabelsEditor: React.FunctionComponent = React.memo((props) => )) } - + ) : ( ) diff --git a/src/components/DataSetEditorModal/DataSetEditorModal.tsx b/src/components/DataSetEditorModal/DataSetEditorModal.tsx index d8ad8050..ddabc53e 100644 --- a/src/components/DataSetEditorModal/DataSetEditorModal.tsx +++ b/src/components/DataSetEditorModal/DataSetEditorModal.tsx @@ -1,5 +1,6 @@ // import debugFactory from 'debug'; -import React, { useId } from 'react'; +import type React from 'react'; +import { useId } from 'react'; import * as TEXTS from '../../constants/texts'; import Button from '../Button/Button'; import DataSetEditor, { TProps as TDataSetEditorProps } from '../DataSetEditor/DataSetEditor'; diff --git a/src/components/EditLabelButton/EditLabelButton.tsx b/src/components/EditLabelButton/EditLabelButton.tsx index 23947217..28daa8aa 100644 --- a/src/components/EditLabelButton/EditLabelButton.tsx +++ b/src/components/EditLabelButton/EditLabelButton.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import * as TEXTS from '../../constants/texts'; import * as gtag from '../../helpers/gtag'; import type { ILabel } from '../../models/Label'; @@ -47,7 +48,7 @@ const EditLabelButton: React.FunctionComponent = (props) => { }, [user, index, handleModalClose]); return ( - + <> = (props) => { onClose={handleModalClose} onSubmit={handleLabelFormSubmit} /> - + ); }; diff --git a/src/components/LabelForm/LabelForm.tsx b/src/components/LabelForm/LabelForm.tsx index 0e88def7..e256ebec 100644 --- a/src/components/LabelForm/LabelForm.tsx +++ b/src/components/LabelForm/LabelForm.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import joi from 'joi'; -import React, { useCallback, useId, useMemo, useState } from 'react'; +import type React from 'react'; +import { useCallback, useId, useMemo, useState } from 'react'; import { namespace } from '../../../package.json'; import cache from '../../cache'; import { SCREENSHOT_WIDTH } from '../../constants/label'; diff --git a/src/components/LabelFormModal/LabelFormModal.tsx b/src/components/LabelFormModal/LabelFormModal.tsx index d047e73c..dd334723 100644 --- a/src/components/LabelFormModal/LabelFormModal.tsx +++ b/src/components/LabelFormModal/LabelFormModal.tsx @@ -1,4 +1,5 @@ -import React, { useId } from 'react'; +import type React from 'react'; +import { useId } from 'react'; import * as TEXTS from '../../constants/texts'; import Button from '../Button/Button'; import LabelForm, { TProps as TLabelFormProps } from '../LabelForm/LabelForm'; diff --git a/src/components/LabelInfo/LabelInfo.tsx b/src/components/LabelInfo/LabelInfo.tsx index 4cb3b169..82a96d45 100644 --- a/src/components/LabelInfo/LabelInfo.tsx +++ b/src/components/LabelInfo/LabelInfo.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useMemo } from 'react'; +import type React from 'react'; +import { useMemo } from 'react'; import { getShareURL } from '../../helpers/label'; import type { IDataSet } from '../../models/DataSet'; import type { ILabel } from '../../models/Label'; diff --git a/src/components/LabelItem/LabelItem.tsx b/src/components/LabelItem/LabelItem.tsx index 66272a79..15c44db4 100644 --- a/src/components/LabelItem/LabelItem.tsx +++ b/src/components/LabelItem/LabelItem.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React from 'react'; +import type React from 'react'; +import { forwardRef } from 'react'; import styles from './LabelItem.module.scss'; interface IProps { } @@ -8,7 +9,7 @@ type TComponentProps = React.ComponentPropsWithoutRef<'div'>; type TProps = IProps & TComponentProps; -const LabelItem = React.forwardRef((props, ref) => { +const LabelItem = forwardRef((props, ref) => { const { className, children, ...otherProps } = props; return (
= (props) => { }, [refs.reference, refs.floating, update]); return ( - + <> {text} @@ -72,7 +73,7 @@ const GroupedLabelItem: React.FunctionComponent = (props) => { style={labelInfoListStyle} items={items} /> - + ); }; diff --git a/src/components/LabelList/LabelInfoList/LabelInfoList.tsx b/src/components/LabelList/LabelInfoList/LabelInfoList.tsx index b7513905..a39a9dd4 100644 --- a/src/components/LabelList/LabelInfoList/LabelInfoList.tsx +++ b/src/components/LabelList/LabelInfoList/LabelInfoList.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React from 'react'; +import type React from 'react'; +import { forwardRef } from 'react'; import type { TLabelsGroupItem } from '../../../helpers/labelList'; import LabelInfo from '../../LabelInfo/LabelInfo'; import styles from './LabelInfoList.module.scss'; @@ -12,7 +13,7 @@ type TComponentProps = React.ComponentPropsWithoutRef<'ul'>; type TProps = IProps & TComponentProps; -const LabelInfoList = React.forwardRef((props, ref) => { +const LabelInfoList = forwardRef((props, ref) => { const { className, items, ...otherProps } = props; return (
    diff --git a/src/components/LabelList/LabelList.tsx b/src/components/LabelList/LabelList.tsx index 7d0811a7..73c986b3 100644 --- a/src/components/LabelList/LabelList.tsx +++ b/src/components/LabelList/LabelList.tsx @@ -1,4 +1,5 @@ -import React, { useMemo } from 'react'; +import type React from 'react'; +import { useMemo } from 'react'; import { mapDataSetsToLabelsGroupsGroupedByText } from '../../helpers/labelList'; import { createUserPersonalLabelsSelector, createUserPersonalSelector, createUserSubscriptionLabelsSelector, createUserSubscriptionsSelector } from '../../store/selectors'; import { useTypedSelector } from '../../store/store'; diff --git a/src/components/Modal/Body.tsx b/src/components/Modal/Body.tsx index 578f6a52..bdaf90a2 100644 --- a/src/components/Modal/Body.tsx +++ b/src/components/Modal/Body.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useContext } from 'react'; +import type React from 'react'; +import { useContext } from 'react'; import styles from './Body.module.scss'; import IDsContext from './IDsContext'; diff --git a/src/components/Modal/Header.tsx b/src/components/Modal/Header.tsx index 4031c723..cd3c854a 100644 --- a/src/components/Modal/Header.tsx +++ b/src/components/Modal/Header.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useContext } from 'react'; +import type React from 'react'; +import { useContext } from 'react'; import { IconName } from '../Icon/types'; import IconButton from '../IconButton/IconButton'; import styles from './Header.module.scss'; diff --git a/src/components/Modal/IDsContext.ts b/src/components/Modal/IDsContext.ts index 6fcad1ba..d6465a59 100644 --- a/src/components/Modal/IDsContext.ts +++ b/src/components/Modal/IDsContext.ts @@ -1,11 +1,11 @@ -import React from 'react'; +import { createContext } from 'react'; interface IContext { title: string; body: string; } -const Context = React.createContext({ +const Context = createContext({ title: '', body: '' }); diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index c7ee69c5..4dca4d02 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,6 +1,7 @@ import classNames from 'classNames'; import FocusTrap from 'focus-trap-react'; -import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { Key } from 'ts-key-enum'; import Body from './Body'; diff --git a/src/components/RemoveLabelButton/RemoveLabelButton.tsx b/src/components/RemoveLabelButton/RemoveLabelButton.tsx index 13b26a79..6d8288eb 100644 --- a/src/components/RemoveLabelButton/RemoveLabelButton.tsx +++ b/src/components/RemoveLabelButton/RemoveLabelButton.tsx @@ -1,5 +1,6 @@ import { render } from 'mustache'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import cache from '../../cache'; import * as TEXTS from '../../constants/texts'; import * as gtag from '../../helpers/gtag'; diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index aa92c3dd..20af7d85 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useId } from 'react'; +import type React from 'react'; +import { useId } from 'react'; import ErrorMessage from '../ErrorMessage/ErrorMessage'; import Icon from '../Icon/Icon'; import { IconName } from '../Icon/types'; diff --git a/src/components/Settings/ClearDataSection/ClearDataSection.tsx b/src/components/Settings/ClearDataSection/ClearDataSection.tsx index e7631a4d..dd879063 100644 --- a/src/components/Settings/ClearDataSection/ClearDataSection.tsx +++ b/src/components/Settings/ClearDataSection/ClearDataSection.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import { displayName } from '../../../../package.json'; import * as TEXTS from '../../../constants/texts'; import * as cloud from '../../../helpers/cloud'; @@ -64,7 +65,7 @@ const ClearDataSection: React.FunctionComponent = () => { }, []); return ( - + <> {TEXTS.SETTINGS_TITLE_CLEAR_DATA} @@ -95,7 +96,7 @@ const ClearDataSection: React.FunctionComponent = () => { ) }
- + ); }; diff --git a/src/components/Settings/CloudSyncSection/CloudSyncSection.tsx b/src/components/Settings/CloudSyncSection/CloudSyncSection.tsx index 77123f35..e957967e 100644 --- a/src/components/Settings/CloudSyncSection/CloudSyncSection.tsx +++ b/src/components/Settings/CloudSyncSection/CloudSyncSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type React from 'react'; import * as TEXTS from '../../../constants/texts'; import lihkgCssClasses from '../../../stylesheets/variables/lihkg/classes.module.scss'; import SyncWithGoogleDrive from './SyncWithGoogleDrive/SyncWithGoogleDrive'; @@ -9,7 +9,7 @@ type TProps = IProps; const CloudSyncSection: React.FunctionComponent = () => { return ( - + <> {TEXTS.SETTINGS_TITLE_CLOUD_SYNC} @@ -18,7 +18,7 @@ const CloudSyncSection: React.FunctionComponent = () => { - + ); }; diff --git a/src/components/Settings/CloudSyncSection/SyncWithGoogleDrive/SyncWithGoogleDrive.tsx b/src/components/Settings/CloudSyncSection/SyncWithGoogleDrive/SyncWithGoogleDrive.tsx index efe29625..a52bf45e 100644 --- a/src/components/Settings/CloudSyncSection/SyncWithGoogleDrive/SyncWithGoogleDrive.tsx +++ b/src/components/Settings/CloudSyncSection/SyncWithGoogleDrive/SyncWithGoogleDrive.tsx @@ -3,7 +3,8 @@ import add from 'date-fns/add'; import formatRelative from 'date-fns/formatRelative'; import { zhHK } from 'date-fns/locale'; import { render } from 'mustache'; -import React, { useCallback, useMemo } from 'react'; +import type React from 'react'; +import { useCallback, useMemo } from 'react'; import logo from '../../../../../assets/logos/google/google-drive.png'; import * as cloud from '../../../../cloud'; import { SYNC_INTERVAL } from '../../../../constants/sync'; @@ -56,14 +57,14 @@ const SyncWithGoogleDrive: React.FunctionComponent = () => { }, [signOut]); return ( - + <>
{TEXTS.CLOUD_SYNC_LABEL_GOOGLE_DRIVE} { user && signedIn && ( - + <> @@ -80,7 +81,7 @@ const SyncWithGoogleDrive: React.FunctionComponent = () => { ) : ( - + <> { !!infoHints && ( @@ -118,10 +119,10 @@ const SyncWithGoogleDrive: React.FunctionComponent = () => { ) } - + ) } - + ) }
@@ -133,7 +134,7 @@ const SyncWithGoogleDrive: React.FunctionComponent = () => { TEXTS.BUTTON_TEXT_GOOGLE_AUTHORIZE } - + ); }; diff --git a/src/components/Settings/ManageDataSection/EditDataSetButton.tsx b/src/components/Settings/ManageDataSection/EditDataSetButton.tsx index d327ca6c..41121b0f 100644 --- a/src/components/Settings/ManageDataSection/EditDataSetButton.tsx +++ b/src/components/Settings/ManageDataSection/EditDataSetButton.tsx @@ -1,5 +1,6 @@ import debugFactory from 'debug'; -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import * as TEXTS from '../../../constants/texts'; import * as LIHKG from '../../../helpers/lihkg'; import useSettingsModalFocusTrap from '../../../hooks/useSettingsModalFocusTrap'; @@ -69,7 +70,7 @@ const EditDataSetButton: React.FunctionComponent = () => { }, [settingsModalFocusTrap]); return ( - + <> {TEXTS.BUTTON_TEXT_EDIT_DATA_SET} @@ -82,7 +83,7 @@ const EditDataSetButton: React.FunctionComponent = () => { fragile={false} onClose={handleClose} /> - + ); }; diff --git a/src/components/Settings/ManageDataSection/ExportFileButton.tsx b/src/components/Settings/ManageDataSection/ExportFileButton.tsx index 67b68fc4..fff0f4fa 100644 --- a/src/components/Settings/ManageDataSection/ExportFileButton.tsx +++ b/src/components/Settings/ManageDataSection/ExportFileButton.tsx @@ -1,6 +1,7 @@ // import debugFactory from 'debug'; import { render } from 'mustache'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../constants/texts'; import { _export } from '../../../helpers/file'; import * as gtag from '../../../helpers/gtag'; diff --git a/src/components/Settings/ManageDataSection/ImportFileButton.tsx b/src/components/Settings/ManageDataSection/ImportFileButton.tsx index d8a14ad4..ee0602c9 100644 --- a/src/components/Settings/ManageDataSection/ImportFileButton.tsx +++ b/src/components/Settings/ManageDataSection/ImportFileButton.tsx @@ -1,6 +1,7 @@ // import debugFactory from 'debug'; import { render } from 'mustache'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../constants/texts'; import { _import } from '../../../helpers/file'; import * as gtag from '../../../helpers/gtag'; diff --git a/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx b/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx index 04fb9a1c..94e3d79b 100644 --- a/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx +++ b/src/components/Settings/ManageDataSection/MakeSubscriptionButton.tsx @@ -1,5 +1,6 @@ // import debugFactory from 'debug'; -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import * as TEXTS from '../../../constants/texts'; import { download } from '../../../helpers/file'; import useSettingsModalFocusTrap from '../../../hooks/useSettingsModalFocusTrap'; @@ -39,7 +40,7 @@ const MakeSubscriptionButton: React.FunctionComponent = () => { }, []); return ( - + <> {TEXTS.BUTTON_TEXT_MAKE_SUBSCRIPTION} @@ -51,7 +52,7 @@ const MakeSubscriptionButton: React.FunctionComponent = () => { fragile={false} onClose={handleClose} /> - + ); }; diff --git a/src/components/Settings/SubscriptionSection/AddSubscriptionButton/AddSubscriptionButton.tsx b/src/components/Settings/SubscriptionSection/AddSubscriptionButton/AddSubscriptionButton.tsx index 8801eead..b19e5f1b 100644 --- a/src/components/Settings/SubscriptionSection/AddSubscriptionButton/AddSubscriptionButton.tsx +++ b/src/components/Settings/SubscriptionSection/AddSubscriptionButton/AddSubscriptionButton.tsx @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../../constants/texts'; import * as gtag from '../../../../helpers/gtag'; import { prompt } from '../../../../helpers/subscription'; diff --git a/src/components/Settings/SubscriptionSection/ReloadSubscriptionButton/ReloadSubscriptionButton.tsx b/src/components/Settings/SubscriptionSection/ReloadSubscriptionButton/ReloadSubscriptionButton.tsx index 1e3d280c..1be8ee33 100644 --- a/src/components/Settings/SubscriptionSection/ReloadSubscriptionButton/ReloadSubscriptionButton.tsx +++ b/src/components/Settings/SubscriptionSection/ReloadSubscriptionButton/ReloadSubscriptionButton.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../../constants/texts'; import * as gtag from '../../../../helpers/gtag'; import type { ISubscription } from '../../../../models/Subscription'; diff --git a/src/components/Settings/SubscriptionSection/RemoveSubscriptionButton/RemoveSubscriptionButton.tsx b/src/components/Settings/SubscriptionSection/RemoveSubscriptionButton/RemoveSubscriptionButton.tsx index 9bf777fa..50f6e239 100644 --- a/src/components/Settings/SubscriptionSection/RemoveSubscriptionButton/RemoveSubscriptionButton.tsx +++ b/src/components/Settings/SubscriptionSection/RemoveSubscriptionButton/RemoveSubscriptionButton.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import { render } from 'mustache'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../../constants/texts'; import * as gtag from '../../../../helpers/gtag'; import type { ISubscription } from '../../../../models/Subscription'; diff --git a/src/components/Settings/SubscriptionSection/SubscriptionItem/SubscriptionItem.tsx b/src/components/Settings/SubscriptionSection/SubscriptionItem/SubscriptionItem.tsx index 208c2ded..68041fcd 100644 --- a/src/components/Settings/SubscriptionSection/SubscriptionItem/SubscriptionItem.tsx +++ b/src/components/Settings/SubscriptionSection/SubscriptionItem/SubscriptionItem.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useMemo } from 'react'; +import type React from 'react'; +import { useMemo } from 'react'; import * as TEXTS from '../../../../constants/texts'; import type { ISubscription } from '../../../../models/Subscription'; import Icon from '../../../Icon/Icon'; diff --git a/src/components/Settings/SubscriptionSection/SubscriptionSection.tsx b/src/components/Settings/SubscriptionSection/SubscriptionSection.tsx index 9a1da12d..ede2aae2 100644 --- a/src/components/Settings/SubscriptionSection/SubscriptionSection.tsx +++ b/src/components/Settings/SubscriptionSection/SubscriptionSection.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; import debugFactory from 'debug'; import produce from 'immer'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import { DndProvider as DragAndDropProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import * as TEXTS from '../../../constants/texts'; @@ -30,7 +31,7 @@ const SubscriptionSection: React.FunctionComponent = () => { }, [subscriptions]); return ( - + <> {TEXTS.SETTINGS_TITLE_SUBSCRIPTION} @@ -62,7 +63,7 @@ const SubscriptionSection: React.FunctionComponent = () => {
) } -
+ ); }; diff --git a/src/components/Settings/SubscriptionSection/ToggleSubscriptionButton/ToggleSubscriptionButton.tsx b/src/components/Settings/SubscriptionSection/ToggleSubscriptionButton/ToggleSubscriptionButton.tsx index c9b26438..229f0380 100644 --- a/src/components/Settings/SubscriptionSection/ToggleSubscriptionButton/ToggleSubscriptionButton.tsx +++ b/src/components/Settings/SubscriptionSection/ToggleSubscriptionButton/ToggleSubscriptionButton.tsx @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../../../constants/texts'; import type { ISubscription } from '../../../../models/Subscription'; import { actions as subscriptionsActions } from '../../../../store/slices/subscriptions'; diff --git a/src/components/SettingsModal/SettingsModal.tsx b/src/components/SettingsModal/SettingsModal.tsx index a041c684..40687224 100644 --- a/src/components/SettingsModal/SettingsModal.tsx +++ b/src/components/SettingsModal/SettingsModal.tsx @@ -1,4 +1,5 @@ -import React, { useMemo, useState } from 'react'; +import type React from 'react'; +import { useMemo, useState } from 'react'; import { displayName } from '../../../package.json'; import Modal, { TProps as TModalProps } from '../Modal/Modal'; import Settings from '../Settings/Settings'; diff --git a/src/components/SettingsModal/SettingsModalFocusTrapContext.ts b/src/components/SettingsModal/SettingsModalFocusTrapContext.ts index af189ad4..c22e2c6f 100644 --- a/src/components/SettingsModal/SettingsModalFocusTrapContext.ts +++ b/src/components/SettingsModal/SettingsModalFocusTrapContext.ts @@ -1,10 +1,10 @@ -import React from 'react'; +import { createContext } from 'react'; export interface IValue { unpause: () => void; pause: () => void; } -const Context = React.createContext(null); +const Context = createContext(null); export default Context; diff --git a/src/components/SettingsModalToggleButton/SettingsModalToggleButton.tsx b/src/components/SettingsModalToggleButton/SettingsModalToggleButton.tsx index 894050e8..95729935 100644 --- a/src/components/SettingsModalToggleButton/SettingsModalToggleButton.tsx +++ b/src/components/SettingsModalToggleButton/SettingsModalToggleButton.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import type React from 'react'; +import { useCallback, useState } from 'react'; import logo from '../../../assets/logos/libel.png'; import { displayName } from '../../../package.json'; import IconButton from '../IconButton/IconButton'; @@ -21,7 +22,7 @@ const SettingsModalToggleButton: React.FunctionComponent = () => { }, []); return ( - + <> { open={open} onClose={handleClose} /> - + ); }; diff --git a/src/components/SnipeButton/SnipeButton.tsx b/src/components/SnipeButton/SnipeButton.tsx index 8e6a2f90..4f9794c7 100644 --- a/src/components/SnipeButton/SnipeButton.tsx +++ b/src/components/SnipeButton/SnipeButton.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import * as TEXTS from '../../constants/texts'; import * as gtag from '../../helpers/gtag'; import { waitForSubmissionForm } from '../../helpers/lihkg'; diff --git a/src/components/SubmissionForm/SubmissionForm.tsx b/src/components/SubmissionForm/SubmissionForm.tsx index c249b062..2777e366 100644 --- a/src/components/SubmissionForm/SubmissionForm.tsx +++ b/src/components/SubmissionForm/SubmissionForm.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type React from 'react'; /** * dummy LIHKG SubmissionForm component diff --git a/src/components/SubscriptionMaker/SubscriptionMaker.tsx b/src/components/SubscriptionMaker/SubscriptionMaker.tsx index b450f4ec..56630411 100644 --- a/src/components/SubscriptionMaker/SubscriptionMaker.tsx +++ b/src/components/SubscriptionMaker/SubscriptionMaker.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; import debugFactory from 'debug'; import joi from 'joi'; -import React, { useCallback, useEffect, useId, useState } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useId, useState } from 'react'; import { namespace } from '../../../package.json'; import * as TEXTS from '../../constants/texts'; import * as gtag from '../../helpers/gtag'; diff --git a/src/components/SubscriptionMakerModal/SubscriptionMakerModal.tsx b/src/components/SubscriptionMakerModal/SubscriptionMakerModal.tsx index 8c51cac6..3a9b0d14 100644 --- a/src/components/SubscriptionMakerModal/SubscriptionMakerModal.tsx +++ b/src/components/SubscriptionMakerModal/SubscriptionMakerModal.tsx @@ -1,4 +1,5 @@ -import React, { useId } from 'react'; +import type React from 'react'; +import { useId } from 'react'; import * as TEXTS from '../../constants/texts'; import Button from '../Button/Button'; import Modal, { TProps as TModalProps } from '../Modal/Modal'; diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx index 0982757c..5aaf7611 100644 --- a/src/components/TextInput/TextInput.tsx +++ b/src/components/TextInput/TextInput.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import React, { useId } from 'react'; +import type React from 'react'; +import { forwardRef, useId } from 'react'; import BaseInput, { TProps as TBaseInputProps } from '../BaseInput/BaseInput'; import Icon from '../Icon/Icon'; import { IconName } from '../Icon/types'; @@ -16,7 +17,7 @@ interface IProps { export type TProps = IProps & TBaseInputProps; -const TextInput = React.forwardRef((props, ref) => { +const TextInput = forwardRef((props, ref) => { const { id, className, label, icon, error, invalid, onClear, ...otherProps } = props; const _id = id || useId(); diff --git a/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx b/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx index 7187d5b0..5a44f952 100644 --- a/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx +++ b/src/components/UnlockIconMapToggleButton/UnlockIconMapToggleButton.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useCallback, useEffect } from 'react'; import * as lihkgActions from '../../actions/lihkg'; import * as TEXTS from '../../constants/texts'; import * as LIHKG from '../../helpers/lihkg'; From ac2abd84e17859fe307f021e3a4eee024bee47cd Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 29 Apr 2022 01:43:03 +0800 Subject: [PATCH 18/58] refactor(hooks): update typings and memoization --- .../UserLabelsEditor/UserLabelsEditor.tsx | 34 +++++++++---------- src/hooks/useLazyRender.ts | 29 +++++++++------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx index 9997bd3c..076b690e 100644 --- a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx +++ b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx @@ -1,12 +1,12 @@ import classNames from 'classnames'; -import debugFactory from 'debug'; +// import debugFactory from 'debug'; import type React from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Key } from 'ts-key-enum'; import * as TEXTS from '../../../constants/texts'; import { ILabelsGroupItem, mapLabelsGroupItemsToErrorStates } from '../../../helpers/dataSetEditor'; import { getShareURL } from '../../../helpers/label'; -import useLazyRender, { IOptions as IUseLazyRenderOptions } from '../../../hooks/useLazyRender'; +import useLazyRender, { UseLazyRender } from '../../../hooks/useLazyRender'; import type { ILabel } from '../../../models/Label'; import ColorPicker from '../../ColorPicker/ColorPicker'; import Icon from '../../Icon/Icon'; @@ -46,29 +46,29 @@ type TComponentProps = Omit, 'onChange' | type TProps = IProps & TComponentProps; -const debug = debugFactory('libel:component:DataSetEditor:UserLabelsEditor'); +// const debug = debugFactory('libel:component:DataSetEditor:UserLabelsEditor'); const UserLabelsEditor: React.FunctionComponent = memo((props) => { const { className, user, items, autoScrollItemIndex = -1, onChange, onRemove, onScroll } = props; + /** validation error for each item */ + const errors = useMemo(() => mapLabelsGroupItemsToErrorStates(items), [items]); + const [style, setStyle] = useState({}); /** lazy rendering */ - const useLazyRenderOptions: IUseLazyRenderOptions = useMemo(() => ({ - onVisibilityChange: (element, visible) => { - if (visible) { - setStyle({}); - } else { - // occupy the space when invisible - const { height } = element.getBoundingClientRect(); - setStyle({ height }); - } + const handleVisibilityChange: UseLazyRender.VisibilityChangeEventHandler = useCallback((element, visible) => { + if (visible) { + setStyle({}); + } else { + // occupy the space when invisible + const { height } = element.getBoundingClientRect(); + setStyle({ height }); } - }), []); - const [ref, visible] = useLazyRender(useLazyRenderOptions); - - /** validation error for each item */ - const errors = useMemo(() => mapLabelsGroupItemsToErrorStates(items), [items]); + }, []); + const [ref, visible] = useLazyRender({ + onVisibilityChange: handleVisibilityChange + }); const handleInputChange: React.ChangeEventHandler = useCallback((event) => { const { name, value } = event.currentTarget; diff --git a/src/hooks/useLazyRender.ts b/src/hooks/useLazyRender.ts index da29e2f2..0812723d 100644 --- a/src/hooks/useLazyRender.ts +++ b/src/hooks/useLazyRender.ts @@ -1,21 +1,24 @@ -import { RefObject, useLayoutEffect, useRef, useState } from 'react'; - -/** - * `useLazyRender` hook options - * @extends IntersectionObserverInit - */ -export interface IOptions extends IntersectionObserverInit { - onVisibilityChange?: (element: T, visible: boolean) => void; +import type React from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; + +export namespace UseLazyRender { + /** + * `useLazyRender` hook options + * @extends IntersectionObserverInit + */ + export interface Options extends IntersectionObserverInit { + onVisibilityChange?: VisibilityChangeEventHandler; + } + export type VisibilityChangeEventHandler = (element: T, visible: boolean) => void; + export type Result = [React.RefObject, boolean]; } -type TUseLazyRenderResult = [RefObject, boolean]; +const useLazyRender = (options: UseLazyRender.Options = {}): UseLazyRender.Result => { + const { root, rootMargin, threshold, onVisibilityChange } = options; -const useLazyRender = (options: IOptions = {}): TUseLazyRenderResult => { const ref = useRef(null); const [visible, setVisible] = useState(false); - const { root, rootMargin, threshold, onVisibilityChange } = options; - useLayoutEffect(() => { if (ref.current) { const options = { root, rootMargin, threshold }; @@ -35,7 +38,7 @@ const useLazyRender = (options: IOptions = {}): TUseL observer.disconnect(); }; } - }, [root, rootMargin, threshold, ref]); + }, [root, rootMargin, threshold, onVisibilityChange]); return [ref, visible]; }; From e997077efcb92e53567d9663e4a6aa8417a33efc Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 29 Apr 2022 02:18:08 +0800 Subject: [PATCH 19/58] refactor(typings): update hooks types --- .../UserLabelsEditor/UserLabelsEditor.tsx | 2 +- src/components/LabelForm/LabelForm.tsx | 16 +++++----- src/hooks/useFadeoutScroll.ts | 15 ++++++---- src/hooks/useFocus.ts | 15 ++++++---- src/hooks/useGoogleAuthorization.ts | 21 +++++++++----- src/hooks/useLazyRender.ts | 16 ++++++---- src/hooks/useScreenshot.ts | 29 ++++++++++++------- 7 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx index 076b690e..1613c8e8 100644 --- a/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx +++ b/src/components/DataSetEditor/UserLabelsEditor/UserLabelsEditor.tsx @@ -57,7 +57,7 @@ const UserLabelsEditor: React.FunctionComponent = memo((props) => { const [style, setStyle] = useState({}); /** lazy rendering */ - const handleVisibilityChange: UseLazyRender.VisibilityChangeEventHandler = useCallback((element, visible) => { + const handleVisibilityChange: UseLazyRender.TVisibilityChangeEventHandler = useCallback((element, visible) => { if (visible) { setStyle({}); } else { diff --git a/src/components/LabelForm/LabelForm.tsx b/src/components/LabelForm/LabelForm.tsx index e256ebec..df24983b 100644 --- a/src/components/LabelForm/LabelForm.tsx +++ b/src/components/LabelForm/LabelForm.tsx @@ -8,7 +8,7 @@ import { SCREENSHOT_WIDTH } from '../../constants/label'; import * as TEXTS from '../../constants/texts'; import * as gtag from '../../helpers/gtag'; import { mapValidationError } from '../../helpers/validation'; -import useScreenshot, { IResult as IUseScreenshotResult, TOptions as TUseScreenshotOptions } from '../../hooks/useScreenshot'; +import useScreenshot, { UseScreenshot } from '../../hooks/useScreenshot'; import type { ILabel } from '../../models/Label'; import { color, image, reason, text } from '../../schemas/label'; import { EventAction, EventCategory } from '../../types/ga'; @@ -24,7 +24,7 @@ type TLabelData = Pick; type TFormData = TLabelData & { meta: { - screenshot?: IUseScreenshotResult; + screenshot?: UseScreenshot.IResult; }; }; @@ -98,12 +98,12 @@ const LabelForm: React.FunctionComponent = (props) => { const [inputErrors, setInputErrors] = useState({}); const [formError, setFormError] = useState(''); - const useScreenshotOptions: TUseScreenshotOptions = useMemo(() => ({ - onclone: (document, element) => { - element.style.width = SCREENSHOT_WIDTH; - } - }), []); - const screenshot = useScreenshot(toggleButtonState.useScreenshot, target, useScreenshotOptions); + const handleClone: UseScreenshot.TCloneEventHandler = useCallback((document, element) => { + element.style.width = SCREENSHOT_WIDTH; + }, []); + const screenshot = useScreenshot(toggleButtonState.useScreenshot, target, { + onclone: handleClone + }); const previewImageStyle = useMemo(() => ({ backgroundImage: `url(${screenshot.url})` diff --git a/src/hooks/useFadeoutScroll.ts b/src/hooks/useFadeoutScroll.ts index f943b854..3fcb539d 100644 --- a/src/hooks/useFadeoutScroll.ts +++ b/src/hooks/useFadeoutScroll.ts @@ -2,14 +2,19 @@ import { MutableRefObject, useMemo, useRef } from 'react'; import { useScroll } from 'react-use'; -type TUseScrollFadeResult = [ - MutableRefObject, - React.CSSProperties -]; +module UseFadeoutScroll { + /** + * `useFadeoutScroll` hook result + */ + export type TResult = [ + MutableRefObject, + React.CSSProperties + ]; +} // const debug = debugFactory('libel:hook:useFadeoutScroll'); -const useFadeoutScroll = (fadingRate = 1): TUseScrollFadeResult => { +const useFadeoutScroll = (fadingRate = 1): UseFadeoutScroll.TResult => { const ref = useRef(null); const { y } = useScroll(ref); const style = useMemo(() => { diff --git a/src/hooks/useFocus.ts b/src/hooks/useFocus.ts index c031f182..8c1bbc47 100644 --- a/src/hooks/useFocus.ts +++ b/src/hooks/useFocus.ts @@ -1,11 +1,16 @@ import { useCallback, useRef } from 'react'; -type TUseFocusResult = [ - React.RefObject, - (options?: FocusOptions) => void -]; +module UseFocus { + /** + * `useFocus` hook result + */ + export type TResult = [ + React.RefObject, + (options?: FocusOptions) => void + ]; +} -const useFocus = (): TUseFocusResult => { +const useFocus = (): UseFocus.TResult => { const ref = useRef(null); const focus = useCallback((options?: FocusOptions) => { ref.current?.focus(options); diff --git a/src/hooks/useGoogleAuthorization.ts b/src/hooks/useGoogleAuthorization.ts index a49720c4..f7298e4f 100644 --- a/src/hooks/useGoogleAuthorization.ts +++ b/src/hooks/useGoogleAuthorization.ts @@ -5,15 +5,20 @@ import * as gtag from '../helpers/gtag'; import * as LIHKG from '../helpers/lihkg'; import { EventAction, EventCategory } from '../types/ga'; -type TState = [ - gapi.auth2.GoogleAuth | undefined, - gapi.auth2.GoogleUser | undefined, - (options?: gapi.auth2.SigninOptions | gapi.auth2.SigninOptionsBuilder) => Promise, - () => void, - boolean -]; +module UseGoogleAuthorization { + /** + * `useGoogleAuthorization` hook result + */ + export type TResult = [ + gapi.auth2.GoogleAuth | undefined, + gapi.auth2.GoogleUser | undefined, + (options?: gapi.auth2.SigninOptions | gapi.auth2.SigninOptionsBuilder) => Promise, + () => void, + boolean + ]; +} -const useGoogleAuthorization = (): TState => { +const useGoogleAuthorization = (): UseGoogleAuthorization.TResult => { const [auth, setAuth] = useState(); const [user, setUser] = useState(); const [signedIn, setSignedIn] = useState(false); diff --git a/src/hooks/useLazyRender.ts b/src/hooks/useLazyRender.ts index 0812723d..5bab28a1 100644 --- a/src/hooks/useLazyRender.ts +++ b/src/hooks/useLazyRender.ts @@ -1,19 +1,23 @@ import type React from 'react'; import { useLayoutEffect, useRef, useState } from 'react'; -export namespace UseLazyRender { +export module UseLazyRender { /** * `useLazyRender` hook options * @extends IntersectionObserverInit */ - export interface Options extends IntersectionObserverInit { - onVisibilityChange?: VisibilityChangeEventHandler; + export interface IOptions extends IntersectionObserverInit { + onVisibilityChange?: TVisibilityChangeEventHandler; } - export type VisibilityChangeEventHandler = (element: T, visible: boolean) => void; - export type Result = [React.RefObject, boolean]; + /** + * `useLazyRender` hook result + */ + export type TResult = [React.RefObject, boolean]; + /* event handlers */ + export type TVisibilityChangeEventHandler = (element: T, visible: boolean) => void; } -const useLazyRender = (options: UseLazyRender.Options = {}): UseLazyRender.Result => { +const useLazyRender = (options: UseLazyRender.IOptions = {}): UseLazyRender.TResult => { const { root, rootMargin, threshold, onVisibilityChange } = options; const ref = useRef(null); diff --git a/src/hooks/useScreenshot.ts b/src/hooks/useScreenshot.ts index 4162f177..b9751472 100644 --- a/src/hooks/useScreenshot.ts +++ b/src/hooks/useScreenshot.ts @@ -1,15 +1,26 @@ import { useEffect, useState } from 'react'; import { toCanvas, toImageURL, TToCanvasOptions } from '../helpers/canvas'; -export interface IResult { - loading: boolean; - error: unknown | null; - url: string | null; - blob: Blob | null; - canvas: HTMLCanvasElement | null; +export namespace UseScreenshot { + /** + * `useScreenshot` hook result + */ + export type TOptions = TToCanvasOptions; + /** + * `useScreenshot` hook result + */ + export interface IResult { + loading: boolean; + error: unknown | null; + url: string | null; + blob: Blob | null; + canvas: HTMLCanvasElement | null; + } + /* event handlers */ + export type TCloneEventHandler = Required['onclone']; } -const initialResult: IResult = { +const initialResult: UseScreenshot.IResult = { loading: false, error: null, url: null, @@ -17,7 +28,7 @@ const initialResult: IResult = { canvas: null }; -const useScreenshot = (enabled: boolean, element?: E, options?: TToCanvasOptions): IResult => { +const useScreenshot = (enabled: boolean, element?: E, options?: UseScreenshot.TOptions): UseScreenshot.IResult => { const [result, setResult] = useState(initialResult); useEffect(() => { (async () => { @@ -39,5 +50,3 @@ const useScreenshot = (enabled: boolean, element?: E, op }; export default useScreenshot; - -export type { TToCanvasOptions as TOptions }; From 821387f2e613138b1dd45e3914aeda5db223f021 Mon Sep 17 00:00:00 2001 From: kitce Date: Fri, 29 Apr 2022 02:39:48 +0800 Subject: [PATCH 20/58] refactor(hooks): change `useSettingsModalFocusTrap` to `useFocusTrap` --- src/components/Modal/Modal.tsx | 62 ++++++++++--------- .../ManageDataSection/EditDataSetButton.tsx | 16 ++--- .../MakeSubscriptionButton.tsx | 12 ++-- .../SettingsModal/SettingsModal.tsx | 27 +++----- src/hooks/useFocusTrap.ts | 14 +++++ src/hooks/useSettingsModalFocusTrap.ts | 8 --- 6 files changed, 69 insertions(+), 70 deletions(-) create mode 100644 src/hooks/useFocusTrap.ts delete mode 100644 src/hooks/useSettingsModalFocusTrap.ts diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 4dca4d02..3a5565fb 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -4,6 +4,7 @@ import type React from 'react'; import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { Key } from 'ts-key-enum'; +import { Context as FocusTrapContext, IContextValue as IFocusTrapContextValue } from '../../hooks/useFocusTrap'; import Body from './Body'; import Footer from './Footer'; import Header from './Header'; @@ -33,10 +34,6 @@ interface IProps { * allow to click on backdrop to dismiss the modal, default: true */ fragile?: boolean; - /** - * indicate the focus trap paused state - */ - paused?: boolean; /** * close the modal */ @@ -58,11 +55,11 @@ const Modal: TModal = (props) => { backdrop = true, escape = true, fragile = true, - paused, onClose } = props; const [initialFocus, setInitialFocus] = useState(); + const [paused, setPaused] = useState(false); const ref = useRef(null); const backdropRef = useRef(null); @@ -74,6 +71,11 @@ const Modal: TModal = (props) => { body: `${_id}-body` }; + const focusTrapContextValue: IFocusTrapContextValue = useMemo(() => ({ + unpause: () => { setPaused(false); }, + pause: () => { setPaused(true); } + }), []); + const focusTrapOptions: FocusTrap.Props['focusTrapOptions'] = useMemo(() => ({ escapeDeactivates: escape, initialFocus @@ -121,31 +123,33 @@ const Modal: TModal = (props) => { return ( ReactDOM.createPortal( - -