mirror of https://github.com/usememos/memos
refactor: introducing `use{Module}Store` instead of service (#768)
* refactor: introducing `useEditorStore` * refactor: update * chore: updatepull/769/head
parent
bd00fa798d
commit
ef621a444f
@ -1,6 +0,0 @@
|
||||
# Services
|
||||
|
||||
What should service do?
|
||||
|
||||
- request data api and throw error;
|
||||
- dispatch state actions;
|
@ -1,30 +0,0 @@
|
||||
import store from "../store";
|
||||
import { setEditMemoId, setMemoVisibility, setResourceList } from "../store/modules/editor";
|
||||
|
||||
const editorStateService = {
|
||||
getState: () => {
|
||||
return store.getState().editor;
|
||||
},
|
||||
|
||||
setEditMemoWithId: (editMemoId: MemoId) => {
|
||||
store.dispatch(setEditMemoId(editMemoId));
|
||||
},
|
||||
|
||||
clearEditMemo: () => {
|
||||
store.dispatch(setEditMemoId());
|
||||
},
|
||||
|
||||
setMemoVisibility: (memoVisibility: Visibility) => {
|
||||
store.dispatch(setMemoVisibility(memoVisibility));
|
||||
},
|
||||
|
||||
setResourceList: (resourceList: Resource[]) => {
|
||||
store.dispatch(setResourceList(resourceList));
|
||||
},
|
||||
|
||||
clearResourceList: () => {
|
||||
store.dispatch(setResourceList([]));
|
||||
},
|
||||
};
|
||||
|
||||
export default editorStateService;
|
@ -1,51 +0,0 @@
|
||||
import store from "../store";
|
||||
import * as api from "../helpers/api";
|
||||
import * as storage from "../helpers/storage";
|
||||
import { setAppearance, setGlobalState, setLocale } from "../store/modules/global";
|
||||
|
||||
const globalService = {
|
||||
getState: () => {
|
||||
return store.getState().global;
|
||||
},
|
||||
|
||||
initialState: async () => {
|
||||
const defaultGlobalState = {
|
||||
locale: "en" as Locale,
|
||||
appearance: "system" as Appearance,
|
||||
systemStatus: {
|
||||
allowSignUp: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
} as SystemStatus,
|
||||
};
|
||||
|
||||
const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]);
|
||||
if (storageLocale) {
|
||||
defaultGlobalState.locale = storageLocale;
|
||||
}
|
||||
if (storageAppearance) {
|
||||
defaultGlobalState.appearance = storageAppearance;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = (await api.getSystemStatus()).data;
|
||||
if (data) {
|
||||
defaultGlobalState.systemStatus = data;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
|
||||
store.dispatch(setGlobalState(defaultGlobalState));
|
||||
},
|
||||
|
||||
setLocale: (locale: Locale) => {
|
||||
store.dispatch(setLocale(locale));
|
||||
},
|
||||
|
||||
setAppearance: (appearance: Appearance) => {
|
||||
store.dispatch(setAppearance(appearance));
|
||||
},
|
||||
};
|
||||
|
||||
export default globalService;
|
@ -1,9 +0,0 @@
|
||||
import globalService from "./globalService";
|
||||
import editorStateService from "./editorStateService";
|
||||
import locationService from "./locationService";
|
||||
import memoService from "./memoService";
|
||||
import shortcutService from "./shortcutService";
|
||||
import userService from "./userService";
|
||||
import resourceService from "./resourceService";
|
||||
|
||||
export { globalService, editorStateService, locationService, memoService, shortcutService, userService, resourceService };
|
@ -1,131 +0,0 @@
|
||||
import { stringify } from "qs";
|
||||
import store from "../store";
|
||||
import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../store/modules/location";
|
||||
|
||||
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
|
||||
// avoid pathname confusion when entering from non-home page
|
||||
store.dispatch(updatePathnameStateWithLocation());
|
||||
|
||||
const { query, pathname, hash } = store.getState().location;
|
||||
let queryString = stringify(query);
|
||||
if (queryString) {
|
||||
queryString = "?" + queryString;
|
||||
} else {
|
||||
queryString = "";
|
||||
}
|
||||
|
||||
if (method === "replace") {
|
||||
window.history.replaceState(null, "", pathname + hash + queryString);
|
||||
} else {
|
||||
window.history.pushState(null, "", pathname + hash + queryString);
|
||||
}
|
||||
store.dispatch(updateStateWithLocation());
|
||||
};
|
||||
|
||||
const locationService = {
|
||||
getState: () => {
|
||||
return store.getState().location;
|
||||
},
|
||||
|
||||
updateStateWithLocation: () => {
|
||||
store.dispatch(updateStateWithLocation());
|
||||
},
|
||||
|
||||
setPathname: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
pushHistory: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl("push");
|
||||
},
|
||||
|
||||
replaceHistory: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl("replace");
|
||||
},
|
||||
|
||||
setQuery: (query: Query) => {
|
||||
store.dispatch(setQuery(query));
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
clearQuery: () => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
tag: undefined,
|
||||
type: undefined,
|
||||
duration: undefined,
|
||||
text: undefined,
|
||||
shortcutId: undefined,
|
||||
visibility: undefined,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setMemoTypeQuery: (type?: MemoSpecType) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
type: type,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setMemoShortcut: (shortcutId?: ShortcutId) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
shortcutId: shortcutId,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setTextQuery: (text?: string) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
text: text,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setTagQuery: (tag?: string) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
tag: tag,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setFromAndToQuery: (from?: number, to?: number) => {
|
||||
let duration = undefined;
|
||||
if (from && to && from < to) {
|
||||
duration = {
|
||||
from,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
duration,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
|
||||
setMemoVisibilityQuery: (visibility?: Visibility) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
visibility: visibility,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
};
|
||||
|
||||
export default locationService;
|
@ -1,143 +0,0 @@
|
||||
import { uniqBy } from "lodash";
|
||||
import * as api from "../helpers/api";
|
||||
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../store/modules/memo";
|
||||
import store from "../store";
|
||||
import userService from "./userService";
|
||||
|
||||
export const DEFAULT_MEMO_LIMIT = 30;
|
||||
|
||||
const convertResponseModelMemo = (memo: Memo): Memo => {
|
||||
return {
|
||||
...memo,
|
||||
createdTs: memo.createdTs * 1000,
|
||||
updatedTs: memo.updatedTs * 1000,
|
||||
displayTs: memo.displayTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const memoService = {
|
||||
getState: () => {
|
||||
return store.getState().memo;
|
||||
},
|
||||
|
||||
fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => {
|
||||
store.dispatch(setIsFetching(true));
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "NORMAL",
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
if (userService.isVisitorMode()) {
|
||||
memoFind.creatorId = userService.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getMemoList(memoFind)).data;
|
||||
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
|
||||
if (offset === 0) {
|
||||
store.dispatch(setMemos([]));
|
||||
}
|
||||
const memos = memoService.getState().memos;
|
||||
store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id")));
|
||||
store.dispatch(setIsFetching(false));
|
||||
|
||||
return fetchedMemos;
|
||||
},
|
||||
|
||||
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "NORMAL",
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
const { data } = (await api.getAllMemos(memoFind)).data;
|
||||
const memos = data.map((m) => convertResponseModelMemo(m));
|
||||
return memos;
|
||||
},
|
||||
|
||||
fetchArchivedMemos: async () => {
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "ARCHIVED",
|
||||
};
|
||||
if (userService.isVisitorMode()) {
|
||||
memoFind.creatorId = userService.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getMemoList(memoFind)).data;
|
||||
const archivedMemos = data.map((m) => {
|
||||
return convertResponseModelMemo(m);
|
||||
});
|
||||
return archivedMemos;
|
||||
},
|
||||
|
||||
fetchMemoById: async (memoId: MemoId) => {
|
||||
const { data } = (await api.getMemoById(memoId)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
|
||||
return memo;
|
||||
},
|
||||
|
||||
getMemoById: async (memoId: MemoId) => {
|
||||
for (const m of memoService.getState().memos) {
|
||||
if (m.id === memoId) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
return await memoService.fetchMemoById(memoId);
|
||||
},
|
||||
|
||||
updateTagsState: async () => {
|
||||
const tagFind: TagFind = {};
|
||||
if (userService.isVisitorMode()) {
|
||||
tagFind.creatorId = userService.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getTagList(tagFind)).data;
|
||||
store.dispatch(setTags(data));
|
||||
},
|
||||
|
||||
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
|
||||
const { memos } = memoService.getState();
|
||||
const regex = new RegExp(`[@(.+?)](${memoId})`);
|
||||
return memos.filter((m) => m.content.match(regex));
|
||||
},
|
||||
|
||||
createMemo: async (memoCreate: MemoCreate) => {
|
||||
const { data } = (await api.createMemo(memoCreate)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
store.dispatch(createMemo(memo));
|
||||
return memo;
|
||||
},
|
||||
|
||||
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
|
||||
const { data } = (await api.patchMemo(memoPatch)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
store.dispatch(patchMemo(memo));
|
||||
return memo;
|
||||
},
|
||||
|
||||
pinMemo: async (memoId: MemoId) => {
|
||||
await api.pinMemo(memoId);
|
||||
store.dispatch(
|
||||
patchMemo({
|
||||
id: memoId,
|
||||
pinned: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
unpinMemo: async (memoId: MemoId) => {
|
||||
await api.unpinMemo(memoId);
|
||||
store.dispatch(
|
||||
patchMemo({
|
||||
id: memoId,
|
||||
pinned: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
deleteMemoById: async (memoId: MemoId) => {
|
||||
await api.deleteMemo(memoId);
|
||||
store.dispatch(deleteMemo(memoId));
|
||||
},
|
||||
};
|
||||
|
||||
export default memoService;
|
@ -1,54 +0,0 @@
|
||||
import * as api from "../helpers/api";
|
||||
import store from "../store";
|
||||
import { patchResource, setResources, deleteResource } from "../store/modules/resource";
|
||||
|
||||
const convertResponseModelResource = (resource: Resource): Resource => {
|
||||
return {
|
||||
...resource,
|
||||
createdTs: resource.createdTs * 1000,
|
||||
updatedTs: resource.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const resourceService = {
|
||||
getState: () => {
|
||||
return store.getState().resource;
|
||||
},
|
||||
|
||||
async fetchResourceList(): Promise<Resource[]> {
|
||||
const { data } = (await api.getResourceList()).data;
|
||||
const resourceList = data.map((m) => convertResponseModelResource(m));
|
||||
store.dispatch(setResources(resourceList));
|
||||
return resourceList;
|
||||
},
|
||||
|
||||
async upload(file: File): Promise<Resource> {
|
||||
const { name: filename, size } = file;
|
||||
|
||||
if (size > 64 << 20) {
|
||||
return Promise.reject("overload max size: 8MB");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, filename);
|
||||
const { data } = (await api.uploadFile(formData)).data;
|
||||
const resource = convertResponseModelResource(data);
|
||||
const resourceList = resourceService.getState().resources;
|
||||
store.dispatch(setResources([resource, ...resourceList]));
|
||||
return resource;
|
||||
},
|
||||
|
||||
async deleteResourceById(id: ResourceId) {
|
||||
await api.deleteResourceById(id);
|
||||
store.dispatch(deleteResource(id));
|
||||
},
|
||||
|
||||
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
|
||||
const { data } = (await api.patchResource(resourcePatch)).data;
|
||||
const resource = convertResponseModelResource(data);
|
||||
store.dispatch(patchResource(resource));
|
||||
return resource;
|
||||
},
|
||||
};
|
||||
|
||||
export default resourceService;
|
@ -1,52 +0,0 @@
|
||||
import * as api from "../helpers/api";
|
||||
import store from "../store/";
|
||||
import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut";
|
||||
|
||||
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
|
||||
return {
|
||||
...shortcut,
|
||||
createdTs: shortcut.createdTs * 1000,
|
||||
updatedTs: shortcut.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const shortcutService = {
|
||||
getState: () => {
|
||||
return store.getState().shortcut;
|
||||
},
|
||||
|
||||
getMyAllShortcuts: async () => {
|
||||
const { data } = (await api.getShortcutList()).data;
|
||||
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
|
||||
store.dispatch(setShortcuts(shortcuts));
|
||||
},
|
||||
|
||||
getShortcutById: (id: ShortcutId) => {
|
||||
for (const s of shortcutService.getState().shortcuts) {
|
||||
if (s.id === id) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
createShortcut: async (shortcutCreate: ShortcutCreate) => {
|
||||
const { data } = (await api.createShortcut(shortcutCreate)).data;
|
||||
const shortcut = convertResponseModelShortcut(data);
|
||||
store.dispatch(createShortcut(shortcut));
|
||||
},
|
||||
|
||||
patchShortcut: async (shortcutPatch: ShortcutPatch) => {
|
||||
const { data } = (await api.patchShortcut(shortcutPatch)).data;
|
||||
const shortcut = convertResponseModelShortcut(data);
|
||||
store.dispatch(patchShortcut(shortcut));
|
||||
},
|
||||
|
||||
deleteShortcutById: async (shortcutId: ShortcutId) => {
|
||||
await api.deleteShortcutById(shortcutId);
|
||||
store.dispatch(deleteShortcut(shortcutId));
|
||||
},
|
||||
};
|
||||
|
||||
export default shortcutService;
|
@ -1,148 +0,0 @@
|
||||
import { globalService, locationService } from ".";
|
||||
import * as api from "../helpers/api";
|
||||
import * as storage from "../helpers/storage";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import store from "../store";
|
||||
import { setUser, patchUser, setHost, setOwner } from "../store/modules/user";
|
||||
import { getSystemColorScheme } from "../helpers/utils";
|
||||
|
||||
const defaultSetting: Setting = {
|
||||
locale: "en",
|
||||
appearance: getSystemColorScheme(),
|
||||
memoVisibility: "PRIVATE",
|
||||
memoDisplayTsOption: "created_ts",
|
||||
};
|
||||
|
||||
const defaultLocalSetting: LocalSetting = {
|
||||
enableFoldMemo: true,
|
||||
};
|
||||
|
||||
export const convertResponseModelUser = (user: User): User => {
|
||||
const setting: Setting = {
|
||||
...defaultSetting,
|
||||
};
|
||||
const { localSetting: storageLocalSetting } = storage.get(["localSetting"]);
|
||||
const localSetting: LocalSetting = {
|
||||
...defaultLocalSetting,
|
||||
...storageLocalSetting,
|
||||
};
|
||||
|
||||
if (user.userSettingList) {
|
||||
for (const userSetting of user.userSettingList) {
|
||||
(setting as any)[userSetting.key] = JSON.parse(userSetting.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
setting,
|
||||
localSetting,
|
||||
createdTs: user.createdTs * 1000,
|
||||
updatedTs: user.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const userService = {
|
||||
getState: () => {
|
||||
return store.getState().user;
|
||||
},
|
||||
|
||||
initialState: async () => {
|
||||
const { systemStatus } = globalService.getState();
|
||||
if (systemStatus.host) {
|
||||
store.dispatch(setHost(convertResponseModelUser(systemStatus.host)));
|
||||
}
|
||||
|
||||
const ownerUserId = userService.getUserIdFromPath();
|
||||
if (ownerUserId) {
|
||||
const { data: owner } = (await api.getUserById(ownerUserId)).data;
|
||||
if (owner) {
|
||||
store.dispatch(setOwner(convertResponseModelUser(owner)));
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = (await api.getMyselfUser()).data;
|
||||
if (data) {
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(setUser(user));
|
||||
if (user.setting.locale) {
|
||||
globalService.setLocale(user.setting.locale);
|
||||
}
|
||||
if (user.setting.appearance) {
|
||||
globalService.setAppearance(user.setting.appearance);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentUserId: () => {
|
||||
if (userService.isVisitorMode()) {
|
||||
return userService.getUserIdFromPath() || UNKNOWN_ID;
|
||||
} else {
|
||||
return userService.getState().user?.id || UNKNOWN_ID;
|
||||
}
|
||||
},
|
||||
|
||||
isVisitorMode: () => {
|
||||
return !(userService.getUserIdFromPath() === undefined);
|
||||
},
|
||||
|
||||
getUserIdFromPath: () => {
|
||||
const userIdRegex = /^\/u\/(\d+).*/;
|
||||
const result = locationService.getState().pathname.match(userIdRegex);
|
||||
if (result && result.length === 2) {
|
||||
return Number(result[1]);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
doSignIn: async () => {
|
||||
const { data: user } = (await api.getMyselfUser()).data;
|
||||
if (user) {
|
||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
||||
} else {
|
||||
userService.doSignOut();
|
||||
}
|
||||
return user;
|
||||
},
|
||||
|
||||
doSignOut: async () => {
|
||||
store.dispatch(setUser());
|
||||
await api.signout();
|
||||
},
|
||||
|
||||
getUserById: async (userId: UserId) => {
|
||||
const { data: user } = (await api.getUserById(userId)).data;
|
||||
if (user) {
|
||||
return convertResponseModelUser(user);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
upsertUserSetting: async (key: keyof Setting, value: any) => {
|
||||
await api.upsertUserSetting({
|
||||
key: key as any,
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
await userService.doSignIn();
|
||||
},
|
||||
|
||||
upsertLocalSetting: async (key: keyof LocalSetting, value: any) => {
|
||||
storage.set({ localSetting: { [key]: value } });
|
||||
store.dispatch(patchUser({ localSetting: { [key]: value } }));
|
||||
},
|
||||
|
||||
patchUser: async (userPatch: UserPatch): Promise<void> => {
|
||||
const { data } = (await api.patchUser(userPatch)).data;
|
||||
if (userPatch.id === store.getState().user.user?.id) {
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(patchUser(user));
|
||||
}
|
||||
},
|
||||
|
||||
deleteUser: async (userDelete: UserDelete) => {
|
||||
await api.deleteUser(userDelete);
|
||||
},
|
||||
};
|
||||
|
||||
export default userService;
|
@ -0,0 +1,28 @@
|
||||
import store, { useAppSelector } from "..";
|
||||
import { setEditMemoId, setMemoVisibility, setResourceList } from "../reducer/editor";
|
||||
|
||||
export const useEditorStore = () => {
|
||||
const state = useAppSelector((state) => state.editor);
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().editor;
|
||||
},
|
||||
setEditMemoWithId: (editMemoId: MemoId) => {
|
||||
store.dispatch(setEditMemoId(editMemoId));
|
||||
},
|
||||
clearEditMemo: () => {
|
||||
store.dispatch(setEditMemoId());
|
||||
},
|
||||
setMemoVisibility: (memoVisibility: Visibility) => {
|
||||
store.dispatch(setMemoVisibility(memoVisibility));
|
||||
},
|
||||
setResourceList: (resourceList: Resource[]) => {
|
||||
store.dispatch(setResourceList(resourceList));
|
||||
},
|
||||
clearResourceList: () => {
|
||||
store.dispatch(setResourceList([]));
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
import * as api from "../../helpers/api";
|
||||
import * as storage from "../../helpers/storage";
|
||||
import store, { useAppSelector } from "../";
|
||||
import { setAppearance, setGlobalState, setLocale } from "../reducer/global";
|
||||
|
||||
export const initialGlobalState = async () => {
|
||||
const defaultGlobalState = {
|
||||
locale: "en" as Locale,
|
||||
appearance: "system" as Appearance,
|
||||
systemStatus: {
|
||||
allowSignUp: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
} as SystemStatus,
|
||||
};
|
||||
|
||||
const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]);
|
||||
if (storageLocale) {
|
||||
defaultGlobalState.locale = storageLocale;
|
||||
}
|
||||
if (storageAppearance) {
|
||||
defaultGlobalState.appearance = storageAppearance;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = (await api.getSystemStatus()).data;
|
||||
if (data) {
|
||||
defaultGlobalState.systemStatus = data;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
|
||||
store.dispatch(setGlobalState(defaultGlobalState));
|
||||
};
|
||||
|
||||
export const useGlobalStore = () => {
|
||||
const state = useAppSelector((state) => state.global);
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().global;
|
||||
},
|
||||
setLocale: (locale: Locale) => {
|
||||
store.dispatch(setLocale(locale));
|
||||
},
|
||||
setAppearance: (appearance: Appearance) => {
|
||||
store.dispatch(setAppearance(appearance));
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export * from "./editor";
|
||||
export * from "./global";
|
||||
export * from "./location";
|
||||
export * from "./memo";
|
||||
export * from "./resource";
|
||||
export * from "./shortcut";
|
||||
export * from "./user";
|
@ -0,0 +1,122 @@
|
||||
import { stringify } from "qs";
|
||||
import store, { useAppSelector } from "../";
|
||||
import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../reducer/location";
|
||||
|
||||
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
|
||||
// avoid pathname confusion when entering from non-home page
|
||||
store.dispatch(updatePathnameStateWithLocation());
|
||||
|
||||
const { query, pathname, hash } = store.getState().location;
|
||||
let queryString = stringify(query);
|
||||
if (queryString) {
|
||||
queryString = "?" + queryString;
|
||||
} else {
|
||||
queryString = "";
|
||||
}
|
||||
|
||||
if (method === "replace") {
|
||||
window.history.replaceState(null, "", pathname + hash + queryString);
|
||||
} else {
|
||||
window.history.pushState(null, "", pathname + hash + queryString);
|
||||
}
|
||||
store.dispatch(updateStateWithLocation());
|
||||
};
|
||||
|
||||
export const useLocationStore = () => {
|
||||
const state = useAppSelector((state) => state.location);
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().location;
|
||||
},
|
||||
updateStateWithLocation: () => {
|
||||
store.dispatch(updateStateWithLocation());
|
||||
},
|
||||
setPathname: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl();
|
||||
},
|
||||
pushHistory: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl("push");
|
||||
},
|
||||
replaceHistory: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl("replace");
|
||||
},
|
||||
setQuery: (query: Query) => {
|
||||
store.dispatch(setQuery(query));
|
||||
updateLocationUrl();
|
||||
},
|
||||
clearQuery: () => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
tag: undefined,
|
||||
type: undefined,
|
||||
duration: undefined,
|
||||
text: undefined,
|
||||
shortcutId: undefined,
|
||||
visibility: undefined,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setMemoTypeQuery: (type?: MemoSpecType) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
type: type,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setMemoShortcut: (shortcutId?: ShortcutId) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
shortcutId: shortcutId,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setTextQuery: (text?: string) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
text: text,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setTagQuery: (tag?: string) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
tag: tag,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setFromAndToQuery: (from?: number, to?: number) => {
|
||||
let duration = undefined;
|
||||
if (from && to && from < to) {
|
||||
duration = {
|
||||
from,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
duration,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
setMemoVisibilityQuery: (visibility?: Visibility) => {
|
||||
store.dispatch(
|
||||
setQuery({
|
||||
visibility: visibility,
|
||||
})
|
||||
);
|
||||
updateLocationUrl();
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,135 @@
|
||||
import { uniqBy } from "lodash";
|
||||
import * as api from "../../helpers/api";
|
||||
import { DEFAULT_MEMO_LIMIT } from "../../helpers/consts";
|
||||
import { useUserStore } from "./";
|
||||
import store, { useAppSelector } from "../";
|
||||
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../reducer/memo";
|
||||
|
||||
const convertResponseModelMemo = (memo: Memo): Memo => {
|
||||
return {
|
||||
...memo,
|
||||
createdTs: memo.createdTs * 1000,
|
||||
updatedTs: memo.updatedTs * 1000,
|
||||
displayTs: memo.displayTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
export const useMemoStore = () => {
|
||||
const state = useAppSelector((state) => state.memo);
|
||||
const userStore = useUserStore();
|
||||
|
||||
const fetchMemoById = async (memoId: MemoId) => {
|
||||
const { data } = (await api.getMemoById(memoId)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
|
||||
return memo;
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().memo;
|
||||
},
|
||||
fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => {
|
||||
store.dispatch(setIsFetching(true));
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "NORMAL",
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
if (userStore.isVisitorMode()) {
|
||||
memoFind.creatorId = userStore.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getMemoList(memoFind)).data;
|
||||
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
|
||||
if (offset === 0) {
|
||||
store.dispatch(setMemos([]));
|
||||
}
|
||||
const memos = state.memos;
|
||||
store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id")));
|
||||
store.dispatch(setIsFetching(false));
|
||||
|
||||
return fetchedMemos;
|
||||
},
|
||||
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "NORMAL",
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
const { data } = (await api.getAllMemos(memoFind)).data;
|
||||
const memos = data.map((m) => convertResponseModelMemo(m));
|
||||
return memos;
|
||||
},
|
||||
fetchArchivedMemos: async () => {
|
||||
const memoFind: MemoFind = {
|
||||
rowStatus: "ARCHIVED",
|
||||
};
|
||||
if (userStore.isVisitorMode()) {
|
||||
memoFind.creatorId = userStore.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getMemoList(memoFind)).data;
|
||||
const archivedMemos = data.map((m) => {
|
||||
return convertResponseModelMemo(m);
|
||||
});
|
||||
return archivedMemos;
|
||||
},
|
||||
fetchMemoById,
|
||||
getMemoById: async (memoId: MemoId) => {
|
||||
for (const m of state.memos) {
|
||||
if (m.id === memoId) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
return await fetchMemoById(memoId);
|
||||
},
|
||||
updateTagsState: async () => {
|
||||
const tagFind: TagFind = {};
|
||||
if (userStore.isVisitorMode()) {
|
||||
tagFind.creatorId = userStore.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getTagList(tagFind)).data;
|
||||
store.dispatch(setTags(data));
|
||||
},
|
||||
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
|
||||
const regex = new RegExp(`[@(.+?)](${memoId})`);
|
||||
return state.memos.filter((m) => m.content.match(regex));
|
||||
},
|
||||
createMemo: async (memoCreate: MemoCreate) => {
|
||||
const { data } = (await api.createMemo(memoCreate)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
store.dispatch(createMemo(memo));
|
||||
return memo;
|
||||
},
|
||||
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
|
||||
const { data } = (await api.patchMemo(memoPatch)).data;
|
||||
const memo = convertResponseModelMemo(data);
|
||||
store.dispatch(patchMemo(memo));
|
||||
return memo;
|
||||
},
|
||||
pinMemo: async (memoId: MemoId) => {
|
||||
await api.pinMemo(memoId);
|
||||
store.dispatch(
|
||||
patchMemo({
|
||||
id: memoId,
|
||||
pinned: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
unpinMemo: async (memoId: MemoId) => {
|
||||
await api.unpinMemo(memoId);
|
||||
store.dispatch(
|
||||
patchMemo({
|
||||
id: memoId,
|
||||
pinned: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
deleteMemoById: async (memoId: MemoId) => {
|
||||
await api.deleteMemo(memoId);
|
||||
store.dispatch(deleteMemo(memoId));
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import store, { useAppSelector } from "../";
|
||||
import { patchResource, setResources, deleteResource } from "../reducer/resource";
|
||||
import * as api from "../../helpers/api";
|
||||
|
||||
const convertResponseModelResource = (resource: Resource): Resource => {
|
||||
return {
|
||||
...resource,
|
||||
createdTs: resource.createdTs * 1000,
|
||||
updatedTs: resource.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
export const useResourceStore = () => {
|
||||
const state = useAppSelector((state) => state.resource);
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().resource;
|
||||
},
|
||||
async fetchResourceList(): Promise<Resource[]> {
|
||||
const { data } = (await api.getResourceList()).data;
|
||||
const resourceList = data.map((m) => convertResponseModelResource(m));
|
||||
store.dispatch(setResources(resourceList));
|
||||
return resourceList;
|
||||
},
|
||||
async upload(file: File): Promise<Resource> {
|
||||
const { name: filename, size } = file;
|
||||
|
||||
if (size > 64 << 20) {
|
||||
return Promise.reject("overload max size: 8MB");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, filename);
|
||||
const { data } = (await api.uploadFile(formData)).data;
|
||||
const resource = convertResponseModelResource(data);
|
||||
const resourceList = state.resources;
|
||||
store.dispatch(setResources([resource, ...resourceList]));
|
||||
return resource;
|
||||
},
|
||||
async deleteResourceById(id: ResourceId) {
|
||||
await api.deleteResourceById(id);
|
||||
store.dispatch(deleteResource(id));
|
||||
},
|
||||
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
|
||||
const { data } = (await api.patchResource(resourcePatch)).data;
|
||||
const resource = convertResponseModelResource(data);
|
||||
store.dispatch(patchResource(resource));
|
||||
return resource;
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import store, { useAppSelector } from "../";
|
||||
import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../reducer/shortcut";
|
||||
import * as api from "../../helpers/api";
|
||||
|
||||
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
|
||||
return {
|
||||
...shortcut,
|
||||
createdTs: shortcut.createdTs * 1000,
|
||||
updatedTs: shortcut.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
export const useShortcutStore = () => {
|
||||
const state = useAppSelector((state) => state.shortcut);
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().shortcut;
|
||||
},
|
||||
getMyAllShortcuts: async () => {
|
||||
const { data } = (await api.getShortcutList()).data;
|
||||
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
|
||||
store.dispatch(setShortcuts(shortcuts));
|
||||
},
|
||||
getShortcutById: (id: ShortcutId) => {
|
||||
for (const s of state.shortcuts) {
|
||||
if (s.id === id) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
createShortcut: async (shortcutCreate: ShortcutCreate) => {
|
||||
const { data } = (await api.createShortcut(shortcutCreate)).data;
|
||||
const shortcut = convertResponseModelShortcut(data);
|
||||
store.dispatch(createShortcut(shortcut));
|
||||
},
|
||||
patchShortcut: async (shortcutPatch: ShortcutPatch) => {
|
||||
const { data } = (await api.patchShortcut(shortcutPatch)).data;
|
||||
const shortcut = convertResponseModelShortcut(data);
|
||||
store.dispatch(patchShortcut(shortcut));
|
||||
},
|
||||
deleteShortcutById: async (shortcutId: ShortcutId) => {
|
||||
await api.deleteShortcutById(shortcutId);
|
||||
store.dispatch(deleteShortcut(shortcutId));
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,151 @@
|
||||
import store, { useAppSelector } from "..";
|
||||
import * as api from "../../helpers/api";
|
||||
import * as storage from "../../helpers/storage";
|
||||
import { UNKNOWN_ID } from "../../helpers/consts";
|
||||
import { getSystemColorScheme } from "../../helpers/utils";
|
||||
import { setAppearance, setLocale } from "../reducer/global";
|
||||
import { setUser, patchUser, setHost, setOwner } from "../reducer/user";
|
||||
|
||||
const defaultSetting: Setting = {
|
||||
locale: "en",
|
||||
appearance: getSystemColorScheme(),
|
||||
memoVisibility: "PRIVATE",
|
||||
memoDisplayTsOption: "created_ts",
|
||||
};
|
||||
|
||||
const defaultLocalSetting: LocalSetting = {
|
||||
enableFoldMemo: true,
|
||||
};
|
||||
|
||||
export const convertResponseModelUser = (user: User): User => {
|
||||
const setting: Setting = {
|
||||
...defaultSetting,
|
||||
};
|
||||
const { localSetting: storageLocalSetting } = storage.get(["localSetting"]);
|
||||
const localSetting: LocalSetting = {
|
||||
...defaultLocalSetting,
|
||||
...storageLocalSetting,
|
||||
};
|
||||
|
||||
if (user.userSettingList) {
|
||||
for (const userSetting of user.userSettingList) {
|
||||
(setting as any)[userSetting.key] = JSON.parse(userSetting.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
setting,
|
||||
localSetting,
|
||||
createdTs: user.createdTs * 1000,
|
||||
updatedTs: user.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
export const initialUserState = async () => {
|
||||
const { systemStatus } = store.getState().global;
|
||||
|
||||
if (systemStatus.host) {
|
||||
store.dispatch(setHost(convertResponseModelUser(systemStatus.host)));
|
||||
}
|
||||
|
||||
const ownerUserId = getUserIdFromPath();
|
||||
if (ownerUserId) {
|
||||
const { data: owner } = (await api.getUserById(ownerUserId)).data;
|
||||
if (owner) {
|
||||
store.dispatch(setOwner(convertResponseModelUser(owner)));
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = (await api.getMyselfUser()).data;
|
||||
if (data) {
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(setUser(user));
|
||||
if (user.setting.locale) {
|
||||
store.dispatch(setLocale(user.setting.locale));
|
||||
}
|
||||
if (user.setting.appearance) {
|
||||
store.dispatch(setAppearance(user.setting.appearance));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getUserIdFromPath = () => {
|
||||
const { pathname } = store.getState().location;
|
||||
const userIdRegex = /^\/u\/(\d+).*/;
|
||||
const result = pathname.match(userIdRegex);
|
||||
if (result && result.length === 2) {
|
||||
return Number(result[1]);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const doSignIn = async () => {
|
||||
const { data: user } = (await api.getMyselfUser()).data;
|
||||
if (user) {
|
||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
||||
} else {
|
||||
doSignOut();
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
const doSignOut = async () => {
|
||||
store.dispatch(setUser());
|
||||
await api.signout();
|
||||
};
|
||||
|
||||
export const useUserStore = () => {
|
||||
const state = useAppSelector((state) => state.user);
|
||||
|
||||
const isVisitorMode = () => {
|
||||
return !(getUserIdFromPath() === undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().user;
|
||||
},
|
||||
isVisitorMode,
|
||||
getUserIdFromPath,
|
||||
doSignIn,
|
||||
doSignOut,
|
||||
getCurrentUserId: () => {
|
||||
if (isVisitorMode()) {
|
||||
return getUserIdFromPath() || UNKNOWN_ID;
|
||||
} else {
|
||||
return state.user?.id || UNKNOWN_ID;
|
||||
}
|
||||
},
|
||||
getUserById: async (userId: UserId) => {
|
||||
const { data: user } = (await api.getUserById(userId)).data;
|
||||
if (user) {
|
||||
return convertResponseModelUser(user);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
upsertUserSetting: async (key: keyof Setting, value: any) => {
|
||||
await api.upsertUserSetting({
|
||||
key: key as any,
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
await doSignIn();
|
||||
},
|
||||
upsertLocalSetting: async (key: keyof LocalSetting, value: any) => {
|
||||
storage.set({ localSetting: { [key]: value } });
|
||||
store.dispatch(patchUser({ localSetting: { [key]: value } }));
|
||||
},
|
||||
patchUser: async (userPatch: UserPatch): Promise<void> => {
|
||||
const { data } = (await api.patchUser(userPatch)).data;
|
||||
if (userPatch.id === store.getState().user.user?.id) {
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(patchUser(user));
|
||||
}
|
||||
},
|
||||
deleteUser: async (userDelete: UserDelete) => {
|
||||
await api.deleteUser(userDelete);
|
||||
},
|
||||
};
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface State {
|
||||
editMemoId?: MemoId;
|
||||
memoVisibility: Visibility;
|
||||
resourceList: Resource[];
|
||||
editMemoId?: MemoId;
|
||||
}
|
||||
|
||||
const editorSlice = createSlice({
|
Loading…
Reference in New Issue