From 51c2067bd15a7ccf0a8dd67bef1100d684d1a79d Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Tue, 1 Aug 2023 20:48:21 +0800 Subject: [PATCH] feat: files draft --- src/app/services/server.ts | 4 +- src/app/slices/files.ts | 28 +++++++ src/app/store.ts | 2 + src/assets/index.css | 19 +++++ src/routes/files/Filter/Channel.tsx | 47 +++++++++++ src/routes/files/Filter/Date.tsx | 60 ++++++++++++++ src/routes/files/Filter/From.tsx | 45 ++++++++++ src/routes/files/Filter/Type.tsx | 79 ++++++++++++++++++ src/routes/files/Filter/index.tsx | 123 ++++++++++++++++++++++++++++ src/routes/files/Search.tsx | 39 +++++++++ src/routes/files/View.tsx | 37 +++++++++ src/routes/files/index.tsx | 108 ++++++++++++++++++++++++ src/routes/index.tsx | 13 ++- src/types/resource.ts | 21 +++++ 14 files changed, 616 insertions(+), 9 deletions(-) create mode 100644 src/app/slices/files.ts create mode 100644 src/routes/files/Filter/Channel.tsx create mode 100644 src/routes/files/Filter/Date.tsx create mode 100644 src/routes/files/Filter/From.tsx create mode 100644 src/routes/files/Filter/Type.tsx create mode 100644 src/routes/files/Filter/index.tsx create mode 100644 src/routes/files/Search.tsx create mode 100644 src/routes/files/View.tsx create mode 100644 src/routes/files/index.tsx diff --git a/src/app/services/server.ts b/src/app/services/server.ts index eacc6dd6..9eb3d016 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -32,7 +32,7 @@ import { updateInfo } from "../slices/server"; import { updateCallInfo, upsertVoiceList } from "../slices/voice"; import { RootState } from "../store"; import baseQuery from "./base.query"; -import { File, GetFilesDTO } from "@/types/resource"; +import { GetFilesDTO, VoceChatFile } from "@/types/resource"; export const serverApi = createApi({ reducerPath: "serverApi", @@ -239,7 +239,7 @@ export const serverApi = createApi({ getLoginConfig: builder.query({ query: () => ({ url: `/admin/login/config` }) }), - getFiles: builder.query({ + getFiles: builder.query({ query: (params) => ({ url: `/admin/system/files?${new URLSearchParams( params as Record diff --git a/src/app/slices/files.ts b/src/app/slices/files.ts new file mode 100644 index 00000000..35ea70bb --- /dev/null +++ b/src/app/slices/files.ts @@ -0,0 +1,28 @@ +import { VoceChatFile } from "@/types/resource"; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +const initialState: VoceChatFile[] = []; + +const filesSlice = createSlice({ + name: `files`, + initialState, + reducers: { + fillFiles(state, action: PayloadAction) { + return action.payload; + }, + addFile(state, action: PayloadAction) { + state.push(action.payload); + }, + deleteFile(state, action: PayloadAction) { + const content = action.payload; + const idx = state.findIndex((f) => f.content == content); + if (idx > -1) { + state.splice(idx, 1); + } + } + } +}); + +export const { addFile, deleteFile, fillFiles } = filesSlice.actions; + +export default filesSlice.reducer; diff --git a/src/app/store.ts b/src/app/store.ts index a32d2034..138f3a39 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -13,6 +13,7 @@ import channelsReducer from "./slices/channels"; import favoritesReducer from "./slices/favorites"; import footprintReducer from "./slices/footprint"; import messageReducer from "./slices/message"; +import filesReducer from "./slices/files"; import archiveMsgReducer from "./slices/message.archive"; import channelMsgReducer from "./slices/message.channel"; import fileMsgReducer from "./slices/message.file"; @@ -38,6 +39,7 @@ const reducer = combineReducers({ fileMessage: fileMsgReducer, archiveMessage: archiveMsgReducer, message: messageReducer, + files: filesReducer, [authApi.reducerPath]: authApi.reducer, [messageApi.reducerPath]: messageApi.reducer, [userApi.reducerPath]: userApi.reducer, diff --git a/src/assets/index.css b/src/assets/index.css index a8bd84f0..971c5004 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -7,6 +7,25 @@ } } @layer utilities { + .col-count-2 { + column-count: 2; + } + .col-count-3 { + column-count: 3; + } + .col-count-4 { + column-count: 4; + } + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } /* word break */ .wb { word-break: break-word; diff --git a/src/routes/files/Filter/Channel.tsx b/src/routes/files/Filter/Channel.tsx new file mode 100644 index 00000000..8882c3a2 --- /dev/null +++ b/src/routes/files/Filter/Channel.tsx @@ -0,0 +1,47 @@ +import { FC } from "react"; + +import ChannelIcon from "@/components/ChannelIcon"; +import useFilteredChannels from "@/hooks/useFilteredChannels"; +import CheckSign from "@/assets/icons/check.sign.svg"; + +type Props = { + select: number; + updateFilter: (param: { channel?: number }) => void; +}; +const Channel: FC = ({ select = 0, updateFilter }) => { + const { channels } = useFilteredChannels(); + const handleClick = (gid?: number) => { + updateFilter({ channel: gid }); + }; + + return ( +
+
    +
  • + + + Any Channel + + {!select && } +
  • + {channels.map(({ gid, is_public, name }) => { + return ( +
  • + + {name} + {select == gid && } +
  • + ); + })} +
+
+ ); +}; +export default Channel; diff --git a/src/routes/files/Filter/Date.tsx b/src/routes/files/Filter/Date.tsx new file mode 100644 index 00000000..11d401c8 --- /dev/null +++ b/src/routes/files/Filter/Date.tsx @@ -0,0 +1,60 @@ +import { FC } from "react"; + +import CheckSign from "@/assets/icons/check.sign.svg"; + +export const Dates = { + today: { + title: "Today", + duration: 2222 + }, + in7d: { + title: "Last 7 Days" + }, + in30d: { + title: "Last 30 Days" + }, + in3m: { + title: "Last 3 months" + }, + in12m: { + title: "Last 12 months" + } +}; +type Props = { + select: number; + updateFilter: (param: { date?: string }) => void; +}; +const DateFilter: FC = ({ select = "", updateFilter }) => { + const handleClick = (dur?: string) => { + updateFilter({ date: dur }); + }; + + return ( +
+
    +
  • + Any Time + {!select && } +
  • + {Object.entries(Dates).map(([_key, { title }]) => { + return ( +
  • + {title} + {select == _key && ( + + )} +
  • + ); + })} +
+
+ ); +}; +export default DateFilter; diff --git a/src/routes/files/Filter/From.tsx b/src/routes/files/Filter/From.tsx new file mode 100644 index 00000000..eedf5ba7 --- /dev/null +++ b/src/routes/files/Filter/From.tsx @@ -0,0 +1,45 @@ +import { FC } from "react"; + +import User from "@/components/User"; +import useFilteredUsers from "@/hooks/useFilteredUsers"; +import CheckSign from "@/assets/icons/check.sign.svg"; +import Search from "../Search"; + +type Props = { + select: number; + updateFilter: (param: { from?: number }) => void; +}; +const From: FC = ({ select = "", updateFilter }) => { + const { input, updateInput, users } = useFilteredUsers(); + const handleClick = (uid?: number) => { + updateFilter({ from: uid }); + }; + + return ( +
+
+ +
+
    +
  • + Anyone + {!select && } +
  • + {users.map(({ uid }) => { + return ( +
  • + + {select == uid && ( + + )} +
  • + ); + })} +
+
+ ); +}; +export default From; diff --git a/src/routes/files/Filter/Type.tsx b/src/routes/files/Filter/Type.tsx new file mode 100644 index 00000000..5da3c0a9 --- /dev/null +++ b/src/routes/files/Filter/Type.tsx @@ -0,0 +1,79 @@ +import { FC } from "react"; + +import CheckSign from "@/assets/icons/check.sign.svg"; +import IconAudio from "@/assets/icons/file.audio.svg"; +import IconCode from "@/assets/icons/file.code.svg"; +import IconDoc from "@/assets/icons/file.doc.svg"; +import IconImage from "@/assets/icons/file.image.svg"; +import IconPdf from "@/assets/icons/file.pdf.svg"; +import IconUnknown from "@/assets/icons/file.unknown.svg"; +import IconVideo from "@/assets/icons/file.video.svg"; + +export const FileTypes = { + doc: { + title: "Documents", + icon: + }, + pdf: { + title: "PDFs", + icon: + }, + image: { + title: "Images", + icon: + }, + audio: { + title: "Audio", + icon: + }, + video: { + title: "Videos", + icon: + }, + code: { + title: "Code Snippets", + icon: + }, + unknown: { + title: "Unknown Files", + icon: + } +}; +type Props = { + select: number; + updateFilter: (param: { type?: string }) => void; +}; +const Type: FC = ({ select = "", updateFilter }) => { + const handleClick = (type?: string) => { + updateFilter({ type }); + }; + + return ( +
+
    +
  • + Any Type + {!select && } +
  • + {Object.entries(FileTypes).map(([type, { title, icon }]) => { + return ( +
  • + {icon} {title} + {select == type && ( + + )} +
  • + ); + })} +
+
+ ); +}; +export default Type; diff --git a/src/routes/files/Filter/index.tsx b/src/routes/files/Filter/index.tsx new file mode 100644 index 00000000..1ce3af1c --- /dev/null +++ b/src/routes/files/Filter/index.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import Tippy from "@tippyjs/react"; +import clsx from "clsx"; + +import { useAppSelector } from "@/app/store"; +import Avatar from "@/components/Avatar"; +import ArrowDown from "@/assets/icons/arrow.down.svg"; +import FilterChannel from "./Channel"; +import FilterDate, { Dates } from "./Date"; +import FilterFrom from "./From"; +import FilterType, { FileTypes } from "./Type"; + +const getClass = (selected: boolean) => { + return clsx( + `cursor-pointer flex items-center gap-1 md:gap-2 shadow rounded-lg p-1 md:py-2 md:px-3 text-xs text-gray-900 dark:text-gray-200`, + selected + ? "text-white bg-primary-400" + : "border border-solid border-gray-300 dark:border-gray-400 " + ); +}; +export default function Filter({ filter, updateFilter }) { + const { t } = useTranslation("file"); + const [filtersVisible, setFiltersVisible] = useState({ + channel: false, + date: false, + from: false, + type: false + }); + + const toggleFilterVisible = (obj: any) => { + setFiltersVisible((prev) => { + return { ...prev, ...obj }; + }); + }; + + const handleUpdateFilter = (data: any) => { + updateFilter(data); + let _key = Object.keys(data)[0]; + let tmp = { + [_key]: false + }; + toggleFilterVisible(tmp); + }; + const { userMap, channelMap } = useAppSelector((store) => { + return { userMap: store.users.byId, channelMap: store.channels.byId }; + }); + + const { from, channel, type, date } = filter; + const { + channel: channelVisible, + date: dateVisible, + type: typeVisible, + from: fromVisible + } = filtersVisible; + + return ( +
+ } + > +
+ {from && ( + + )} + + {t("from")} {from && userMap[from].name} + + +
+
+ } + > +
+ {channel ? `In ${channelMap[channel].name}` : t("channel")} + +
+
+ } + > +
+ {type ? FileTypes[type].title : t("type")} + +
+
+ } + > +
+ {date ? Dates[date].title : t("date")} + +
+
+
+ ); +} diff --git a/src/routes/files/Search.tsx b/src/routes/files/Search.tsx new file mode 100644 index 00000000..361e7156 --- /dev/null +++ b/src/routes/files/Search.tsx @@ -0,0 +1,39 @@ +import { ChangeEvent, FC } from "react"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import IconSearch from "@/assets/icons/search.svg"; + +interface Props { + value?: string; + updateSearchValue?: (value: string) => void; + embed?: boolean; +} + +const Search: FC = ({ value = "", updateSearchValue = null, embed = false }) => { + const { t } = useTranslation(); + const handleChange = (evt: ChangeEvent) => { + if (updateSearchValue) { + updateSearchValue(evt.target.value); + } + }; + + return ( +
+ + +
+ ); +}; + +export default Search; diff --git a/src/routes/files/View.tsx b/src/routes/files/View.tsx new file mode 100644 index 00000000..5296bb51 --- /dev/null +++ b/src/routes/files/View.tsx @@ -0,0 +1,37 @@ +import { MouseEvent } from "react"; +import { useDispatch } from "react-redux"; +import clsx from "clsx"; + +import { updateFileListView } from "@/app/slices/ui"; +import IconGrid from "@/assets/icons/file.grid.svg"; +import IconList from "@/assets/icons/file.list.svg"; + +const getClass = (selected: boolean) => + clsx( + `cursor-pointer p-2 box-border flex-center`, + selected && `border border-solid border-primary-400 shadow rounded-lg` + ); +type Props = { + view?: "item" | "grid"; +}; +export default function View({ view = "item" }: Props) { + const dispatch = useDispatch(); + const handleChangeView = (evt: MouseEvent) => { + const { view: clickView } = evt.currentTarget.dataset; + if (clickView == view) return; + dispatch(updateFileListView(view == "item" ? "grid" : "item")); + }; + const isGrid = view == "grid"; + return ( +
    +
  • + +
  • +
  • + +
  • +
+ ); +} diff --git a/src/routes/files/index.tsx b/src/routes/files/index.tsx new file mode 100644 index 00000000..11226a1b --- /dev/null +++ b/src/routes/files/index.tsx @@ -0,0 +1,108 @@ +// @ts-nocheck +import { useEffect, useRef, useState } from "react"; +import clsx from "clsx"; +import BASE_URL from "@/app/config"; + +import { useAppSelector } from "@/app/store"; +import FileBox from "@/components/FileBox"; +import Filter from "./Filter"; +import Search from "./Search"; +import View from "./View"; +import { useLazyGetFilesQuery } from "@/app/services/server"; + +const checkFilter = (data, filter, channelMessage) => { + let selected = true; + const { mid, from_uid, properties } = data; + const { name: nameFilter, from: fromFilter, channel: channelFilter } = filter; + const name = properties ? properties.name : ""; + if (fromFilter && fromFilter != from_uid) { + selected = false; + } + if (channelFilter && channelMessage[channelFilter].findIndex((id) => id == mid) == -1) { + selected = false; + } + if (nameFilter) { + let str = ["", ...nameFilter.toLowerCase(), ""].join(".*"); + let reg = new RegExp(str); + if (!reg.test(name)) { + selected = false; + } + } + return selected; +}; + +function Files() { + const [getFiles, { data }] = useLazyGetFilesQuery(); + const listContainerRef = useRef(); + const [filter, setFilter] = useState({}); + const { view } = useAppSelector((store) => { + return { + view: store.ui.fileListView + }; + }); + + const updateFilter = (data) => { + setFilter((prev) => { + return { ...prev, ...data }; + }); + }; + + const handleUpdateSearch = (val) => { + setFilter((prev) => { + return { ...prev, name: val }; + }); + }; + useEffect(() => { + getFiles(); + }, []); + if (!data) return null; + // return null; + const nonExpiredFiles = data.filter((item) => !item.expired); + console.log("nonExpiredFiles", nonExpiredFiles); + return ( +
+ +
+ + +
+
+ {nonExpiredFiles.map((file) => { + const { mid, thumbnail, content, created_at, from_uid, properties } = file; + const { name, content_type, size } = properties ? JSON.parse(properties) : {}; + const url = `${BASE_URL}/resource/file?file_path=${encodeURIComponent( + thumbnail || content + )}`; + return ( + //
+ + //
+ ); + })} +
+
+ ); +} + +export default Files; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index dbf701c7..5e5f39ce 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -10,7 +10,7 @@ import useDeviceToken from "@/components/Notification/useDeviceToken"; import RequireAuth from "@/components/RequireAuth"; import RequireNoAuth from "@/components/RequireNoAuth"; import RequireSingleTab from "@/components/RequireSingleTab"; -import { isElectronContext } from "@/utils"; +import { compareVersion, isElectronContext } from "@/utils"; import { vapidKey } from "../app/config"; import store, { useAppSelector } from "../app/store"; import NotFoundPage from "./404"; @@ -18,9 +18,6 @@ import InvitePrivate from "./invitePrivate"; import LazyIt from "./lazy"; import InviteInMobile from "./reg/InviteInMobile"; -// import Welcome from './Welcome' -// const HomePage = lazy(() => import("./home")); -// const ChatPage = lazy(() => import("./chat")); const RegBasePage = lazy(() => import("./reg")); const RegWithUsernamePage = lazy(() => import("./reg/RegWithUsername")); const SendMagicLinkPage = lazy(() => import("./sendMagicLink")); @@ -35,6 +32,7 @@ const SettingChannelPage = lazy(() => import("./settingChannel")); const SettingDMPage = lazy(() => import("./settingDM")); const SettingPage = lazy(() => import("./setting")); const ResourceManagement = lazy(() => import("./resources")); +const FilesPage = lazy(() => import("./files")); const GuestLogin = lazy(() => import("./guest")); const ChatPage = lazy(() => import("./chat")); const HomePage = lazy(() => import("./home")); @@ -42,9 +40,10 @@ const HomePage = lazy(() => import("./home")); let toastId: string; const PageRoutes = () => { const { - ui: { online } + ui: { online }, + version } = useAppSelector((store) => { - return { ui: store.ui }; + return { ui: store.ui, version: store.server.version }; }, isEqual); // 提前获取device token useDeviceToken(vapidKey); @@ -284,7 +283,7 @@ const PageRoutes = () => { path="files" element={ - + {compareVersion(version, "0.3.11") > -1 ? : } } > diff --git a/src/types/resource.ts b/src/types/resource.ts index 0ad7f3b5..b5e8a49e 100644 --- a/src/types/resource.ts +++ b/src/types/resource.ts @@ -63,3 +63,24 @@ export interface OG { locale_alternate?: []; site_name?: string; } +export interface VoceChatFile { + mid: number; + from_uid: number; + gid: number; + ext: string; + content_type: string; + content: string; + properties: string; + created_at: number; + expired: boolean; +} +export type FileType = "Doc" | "PDF" | "Image" | "Audio" | "Video"; +export type FileCreateTime = "Day1" | "Day7" | "Day30" | "Day90" | "Day180"; +export interface GetFilesDTO { + uid?: number; + gid?: number; + file_type?: FileType; + creation_time_type?: FileCreateTime; + page?: number; + page_size?: number; +}