feat: files draft
This commit is contained in:
@@ -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<LoginConfig, void>({
|
||||
query: () => ({ url: `/admin/login/config` })
|
||||
}),
|
||||
getFiles: builder.query<File[], GetFilesDTO>({
|
||||
getFiles: builder.query<VoceChatFile[], GetFilesDTO>({
|
||||
query: (params) => ({
|
||||
url: `/admin/system/files?${new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
|
||||
@@ -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<VoceChatFile[]>) {
|
||||
return action.payload;
|
||||
},
|
||||
addFile(state, action: PayloadAction<VoceChatFile>) {
|
||||
state.push(action.payload);
|
||||
},
|
||||
deleteFile(state, action: PayloadAction<string>) {
|
||||
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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Props> = ({ select = 0, updateFilter }) => {
|
||||
const { channels } = useFilteredChannels();
|
||||
const handleClick = (gid?: number) => {
|
||||
updateFilter({ channel: gid });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg p-1 pt-0 bg-white dark:bg-gray-800 overflow-auto max-h-[400px] flex flex-col items-start relative drop-shadow">
|
||||
<ul className="w-full flex flex-col gap-4 p-2">
|
||||
<li
|
||||
className="relative cursor-pointer flex items-center gap-2"
|
||||
onClick={handleClick.bind(null, undefined)}
|
||||
>
|
||||
<ChannelIcon />
|
||||
<span className="text-gray-500 dark:text-gray-100 font-semibold text-sm">
|
||||
Any Channel
|
||||
</span>
|
||||
{!select && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{channels.map(({ gid, is_public, name }) => {
|
||||
return (
|
||||
<li
|
||||
key={gid}
|
||||
className="relative cursor-pointer flex items-center gap-2"
|
||||
onClick={handleClick.bind(null, gid)}
|
||||
>
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className="text-gray-500 dark:text-gray-100 font-semibold text-sm">{name}</span>
|
||||
{select == gid && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Channel;
|
||||
@@ -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<Props> = ({ select = "", updateFilter }) => {
|
||||
const handleClick = (dur?: string) => {
|
||||
updateFilter({ date: dur });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-white dark:bg-gray-800 min-w-[200px] overflow-auto rounded-lg flex flex-col items-start relative drop-shadow">
|
||||
<ul className="w-full flex flex-col gap-4">
|
||||
<li
|
||||
className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm"
|
||||
onClick={handleClick.bind(null, undefined)}
|
||||
>
|
||||
Any Time
|
||||
{!select && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{Object.entries(Dates).map(([_key, { title }]) => {
|
||||
return (
|
||||
<li
|
||||
key={title}
|
||||
className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm"
|
||||
onClick={handleClick.bind(null, _key)}
|
||||
>
|
||||
{title}
|
||||
{select == _key && (
|
||||
<CheckSign className="absolute right-0 -top-1/2 -translate-y-1/2" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default DateFilter;
|
||||
@@ -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<Props> = ({ select = "", updateFilter }) => {
|
||||
const { input, updateInput, users } = useFilteredUsers();
|
||||
const handleClick = (uid?: number) => {
|
||||
updateFilter({ from: uid });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg p-1 pt-0 bg-white dark:bg-gray-800 overflow-auto max-h-[300px] flex flex-col items-start relative drop-shadow">
|
||||
<div className="bg-white dark:bg-gray-800 sticky top-0 z-10 w-full">
|
||||
<Search embed={true} value={input} updateSearchValue={updateInput} />
|
||||
</div>
|
||||
<ul className="w-full flex flex-col">
|
||||
<li
|
||||
className="relative cursor-pointer p-2.5 font-semibold text-sm text-gray-500"
|
||||
onClick={handleClick.bind(null, undefined)}
|
||||
>
|
||||
Anyone
|
||||
{!select && <CheckSign className="absolute right-1.5 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{users.map(({ uid }) => {
|
||||
return (
|
||||
<li key={uid} className="relative cursor-pointer" onClick={handleClick.bind(null, uid)}>
|
||||
<User uid={uid} interactive={true} />
|
||||
{select == uid && (
|
||||
<CheckSign className="absolute right-1.5 top-1/2 -translate-y-1/2" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default From;
|
||||
@@ -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: <IconDoc className="w-4 h-auto" />
|
||||
},
|
||||
pdf: {
|
||||
title: "PDFs",
|
||||
icon: <IconPdf className="w-4 h-auto" />
|
||||
},
|
||||
image: {
|
||||
title: "Images",
|
||||
icon: <IconImage className="w-4 h-auto" />
|
||||
},
|
||||
audio: {
|
||||
title: "Audio",
|
||||
icon: <IconAudio className="w-4 h-auto" />
|
||||
},
|
||||
video: {
|
||||
title: "Videos",
|
||||
icon: <IconVideo className="w-4 h-auto" />
|
||||
},
|
||||
code: {
|
||||
title: "Code Snippets",
|
||||
icon: <IconCode className="w-4 h-auto" />
|
||||
},
|
||||
unknown: {
|
||||
title: "Unknown Files",
|
||||
icon: <IconUnknown className="w-4 h-auto" />
|
||||
}
|
||||
};
|
||||
type Props = {
|
||||
select: number;
|
||||
updateFilter: (param: { type?: string }) => void;
|
||||
};
|
||||
const Type: FC<Props> = ({ select = "", updateFilter }) => {
|
||||
const handleClick = (type?: string) => {
|
||||
updateFilter({ type });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-white dark:bg-gray-800 min-w-[180px] overflow-auto shadow-md rounded-lg flex flex-col items-start relative">
|
||||
<ul className="w-full flex flex-col gap-4">
|
||||
<li
|
||||
className="relative cursor-pointer flex items-center gap-4 text-gray-500 dark:text-gray-300 font-semibold text-sm"
|
||||
onClick={handleClick.bind(null, undefined)}
|
||||
>
|
||||
Any Type
|
||||
{!select && <CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />}
|
||||
</li>
|
||||
{Object.entries(FileTypes).map(([type, { title, icon }]) => {
|
||||
return (
|
||||
<li
|
||||
key={title}
|
||||
className="relative cursor-pointer flex items-center gap-2 text-sm text-gray-500 dark:text-gray-300 font-semibold"
|
||||
onClick={handleClick.bind(null, type)}
|
||||
>
|
||||
{icon} {title}
|
||||
{select == type && (
|
||||
<CheckSign className="absolute right-0 top-1/2 -translate-y-1/2" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Type;
|
||||
@@ -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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tippy
|
||||
interactive
|
||||
onClickOutside={toggleFilterVisible.bind(null, { from: false })}
|
||||
visible={fromVisible}
|
||||
placement="bottom-start"
|
||||
content={<FilterFrom select={filter.from} updateFilter={handleUpdateFilter} />}
|
||||
>
|
||||
<div className={getClass(from)} onClick={toggleFilterVisible.bind(null, { from: true })}>
|
||||
{from && (
|
||||
<Avatar
|
||||
width={16}
|
||||
height={16}
|
||||
className="rounded-full w-4 h-4"
|
||||
name={userMap[from].name}
|
||||
src={userMap[from].avatar}
|
||||
/>
|
||||
)}
|
||||
<span className="txt">
|
||||
{t("from")} {from && userMap[from].name}
|
||||
</span>
|
||||
<ArrowDown className="dark:stroke-gray-100" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
interactive
|
||||
onClickOutside={toggleFilterVisible.bind(null, { channel: false })}
|
||||
visible={channelVisible}
|
||||
placement="bottom-start"
|
||||
content={<FilterChannel select={filter.channel} updateFilter={handleUpdateFilter} />}
|
||||
>
|
||||
<div
|
||||
className={getClass(channel)}
|
||||
onClick={toggleFilterVisible.bind(null, { channel: true })}
|
||||
>
|
||||
<span className="txt">{channel ? `In ${channelMap[channel].name}` : t("channel")}</span>
|
||||
<ArrowDown className="dark:stroke-gray-100" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
interactive
|
||||
onClickOutside={toggleFilterVisible.bind(null, { type: false })}
|
||||
visible={typeVisible}
|
||||
placement="bottom-start"
|
||||
content={<FilterType select={filter.type} updateFilter={handleUpdateFilter} />}
|
||||
>
|
||||
<div className={getClass(type)} onClick={toggleFilterVisible.bind(null, { type: true })}>
|
||||
<span className="txt">{type ? FileTypes[type].title : t("type")}</span>
|
||||
<ArrowDown className="dark:stroke-gray-100" />
|
||||
</div>
|
||||
</Tippy>
|
||||
<Tippy
|
||||
interactive
|
||||
onClickOutside={toggleFilterVisible.bind(null, { date: false })}
|
||||
visible={dateVisible}
|
||||
placement="bottom-start"
|
||||
content={<FilterDate select={filter.date} updateFilter={handleUpdateFilter} />}
|
||||
>
|
||||
<div className={getClass(date)} onClick={toggleFilterVisible.bind(null, { date: true })}>
|
||||
<span className="txt">{date ? Dates[date].title : t("date")}</span>
|
||||
<ArrowDown className="dark:stroke-gray-100" />
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Props> = ({ value = "", updateSearchValue = null, embed = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (updateSearchValue) {
|
||||
updateSearchValue(evt.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`hidden md:block relative w-full py-1.5 px-4 shadow`,
|
||||
embed && "py-2 shadow-none"
|
||||
)}
|
||||
>
|
||||
<IconSearch className="absolute left-6 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className="bg-black/5 dark:bg-black/20 rounded-full text-sm text-gray-500 py-2.5 pl-9"
|
||||
placeholder={`${t("action.search")}...`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -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<HTMLLIElement>) => {
|
||||
const { view: clickView } = evt.currentTarget.dataset;
|
||||
if (clickView == view) return;
|
||||
dispatch(updateFileListView(view == "item" ? "grid" : "item"));
|
||||
};
|
||||
const isGrid = view == "grid";
|
||||
return (
|
||||
<ul
|
||||
className={`hidden md:flex border border-solid dark:border-gray-400 shadow rounded-lg box-border`}
|
||||
>
|
||||
<li className={getClass(!isGrid)} data-view={"item"} onClick={handleChangeView}>
|
||||
<IconList className={`${!isGrid ? "fill-primary-400" : ""} dark:fill-gray-400`} />
|
||||
</li>
|
||||
<li className={getClass(isGrid)} data-view={"grid"} onClick={handleChangeView}>
|
||||
<IconGrid className={`${isGrid ? "fill-primary-400" : ""} dark:fill-gray-400`} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLDivElement>();
|
||||
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 (
|
||||
<div className="h-screen md:overflow-y-scroll flex flex-col items-start my-2 mr-6 rounded-2xl bg-white dark:bg-gray-700">
|
||||
<Search value={filter.name} updateSearchValue={handleUpdateSearch} />
|
||||
<div className="flex justify-between w-full px-4 py-5">
|
||||
<Filter filter={filter} updateFilter={updateFilter} />
|
||||
<View view={view} />
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
`h-full w-full px-4 overflow-y-scroll no-scrollbar`,
|
||||
view == "item" && "flex gap-2 flex-col",
|
||||
view == "grid" && "col-count-2 md:col-count-3 lg:col-count-5"
|
||||
)}
|
||||
style={{
|
||||
columnGap: "5px"
|
||||
}}
|
||||
ref={listContainerRef}
|
||||
>
|
||||
{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 (
|
||||
// <div key={mid} className="grid-box mb-2">
|
||||
<FileBox
|
||||
cla
|
||||
preview={view == "grid"}
|
||||
flex={view == "item"}
|
||||
key={mid}
|
||||
file_type={content_type}
|
||||
content={url}
|
||||
created_at={created_at}
|
||||
from_uid={from_uid}
|
||||
size={size}
|
||||
name={name}
|
||||
/>
|
||||
// </div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Files;
|
||||
@@ -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={
|
||||
<LazyIt>
|
||||
<ResourceManagement />
|
||||
{compareVersion(version, "0.3.11") > -1 ? <FilesPage /> : <ResourceManagement />}
|
||||
</LazyIt>
|
||||
}
|
||||
></Route>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user