mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 19:01:26 +00:00
Adding image upload views for admins and profiles. (#2424)
* Adding image upload views for admins and profiles. * Upgraded lemmy-js-client dep. * Removing this. * Upgrade to pnpm v9.0.1 --------- Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
This commit is contained in:
parent
accf1b2d72
commit
9dcaff4301
8 changed files with 6198 additions and 4445 deletions
|
@ -1 +1 @@
|
|||
Subproject commit a94ef775f3f923067b48c1719cda206dbcf1a9e5
|
||||
Subproject commit f9783d686637197a389b8f10a907e0533c55b688
|
|
@ -60,7 +60,7 @@
|
|||
"inferno-router": "^8.2.3",
|
||||
"inferno-server": "^8.2.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lemmy-js-client": "0.19.4-alpha.16",
|
||||
"lemmy-js-client": "0.19.4-alpha.18",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-bidi": "^0.1.0",
|
||||
|
@ -139,7 +139,7 @@
|
|||
"sortpack"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@8.14.3",
|
||||
"packageManager": "pnpm@9.0.1+sha256.46d50ee2afecb42b185ebbd662dc7bdd52ef5be56bf035bb615cab81a75345df",
|
||||
"engineStrict": true,
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
|
|
10360
pnpm-lock.yaml
10360
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
106
src/shared/components/common/media-uploads.tsx
Normal file
106
src/shared/components/common/media-uploads.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||
import { ListMediaResponse, LocalImage } from "lemmy-js-client";
|
||||
import { HttpService, I18NextService } from "../../services";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
import { MomentTime } from "./moment-time";
|
||||
import { PictrsImage } from "./pictrs-image";
|
||||
import { getHttpBase } from "@utils/env";
|
||||
import { toast } from "../../toast";
|
||||
|
||||
interface Props {
|
||||
uploads: ListMediaResponse;
|
||||
showUploader?: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class MediaUploads extends Component<Props, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & Props>,
|
||||
): void {
|
||||
if (this.props !== nextProps) {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const images = this.props.uploads.images;
|
||||
|
||||
return (
|
||||
<div className="media-uploads table-responsive">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
{this.props.showUploader && (
|
||||
<th>{I18NextService.i18n.t("uploader")}</th>
|
||||
)}
|
||||
<th colSpan={3}>{I18NextService.i18n.t("time")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{images.map(i => (
|
||||
<tr key={i.local_image.pictrs_alias}>
|
||||
{this.props.showUploader && (
|
||||
<td>
|
||||
<PersonListing person={i.person} />
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<MomentTime published={i.local_image.published} />
|
||||
</td>
|
||||
<td>
|
||||
<PictrsImage
|
||||
src={buildImageUrl(i.local_image.pictrs_alias)}
|
||||
/>
|
||||
</td>
|
||||
<td>{this.deleteImageBtn(i.local_image)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
deleteImageBtn(image: LocalImage) {
|
||||
return (
|
||||
<button
|
||||
onClick={linkEvent(image, this.handleDeleteImage)}
|
||||
className="btn btn-danger"
|
||||
>
|
||||
{I18NextService.i18n.t("delete")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
async handleDeleteImage(image: LocalImage) {
|
||||
const form = {
|
||||
token: image.pictrs_delete_token,
|
||||
filename: image.pictrs_alias,
|
||||
};
|
||||
const res = await HttpService.client.deleteImage(form);
|
||||
const filename = image.pictrs_alias;
|
||||
if (res.state === "success") {
|
||||
const deletePictureText = I18NextService.i18n.t("picture_deleted", {
|
||||
filename,
|
||||
});
|
||||
toast(deletePictureText);
|
||||
} else if (res.state === "failed") {
|
||||
const failedDeletePictureText = I18NextService.i18n.t(
|
||||
"failed_to_delete_picture",
|
||||
{
|
||||
filename,
|
||||
},
|
||||
);
|
||||
toast(failedDeletePictureText, "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildImageUrl(pictrsAlias: string): string {
|
||||
return `${getHttpBase()}/pictrs/image/${pictrsAlias}`;
|
||||
}
|
|
@ -20,7 +20,7 @@ function handleSwitchTab({ ctx, tab }: { ctx: Tabs; tab: string }) {
|
|||
}
|
||||
|
||||
export default class Tabs extends Component<TabsProps, TabsState> {
|
||||
constructor(props: TabsProps, context) {
|
||||
constructor(props: TabsProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
GetFederatedInstancesResponse,
|
||||
GetSiteResponse,
|
||||
LemmyHttp,
|
||||
ListMediaResponse,
|
||||
PersonView,
|
||||
} from "lemmy-js-client";
|
||||
import { InitialFetchRequest } from "../../interfaces";
|
||||
|
@ -37,10 +38,14 @@ import { TaglineForm } from "./tagline-form";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { MediaUploads } from "../common/media-uploads";
|
||||
import { Paginator } from "../common/paginator";
|
||||
import { snapToTop } from "@utils/browser";
|
||||
|
||||
type AdminSettingsData = RouteDataResponse<{
|
||||
bannedRes: BannedPersonsResponse;
|
||||
instancesRes: GetFederatedInstancesResponse;
|
||||
uploadsRes: ListMediaResponse;
|
||||
}>;
|
||||
|
||||
interface AdminSettingsState {
|
||||
|
@ -50,6 +55,8 @@ interface AdminSettingsState {
|
|||
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||
bannedRes: RequestState<BannedPersonsResponse>;
|
||||
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
||||
uploadsRes: RequestState<ListMediaResponse>;
|
||||
uploadsPage: number;
|
||||
loading: boolean;
|
||||
themeList: string[];
|
||||
isIsomorphic: boolean;
|
||||
|
@ -76,13 +83,19 @@ export class AdminSettings extends Component<
|
|||
bannedRes: EMPTY_REQUEST,
|
||||
instancesRes: EMPTY_REQUEST,
|
||||
leaveAdminTeamRes: EMPTY_REQUEST,
|
||||
uploadsRes: EMPTY_REQUEST,
|
||||
uploadsPage: 1,
|
||||
loading: false,
|
||||
themeList: [],
|
||||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.bannedRes, this.state.instancesRes]);
|
||||
return resourcesSettled([
|
||||
this.state.bannedRes,
|
||||
this.state.instancesRes,
|
||||
this.state.uploadsRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -92,15 +105,17 @@ export class AdminSettings extends Component<
|
|||
this.handleEditEmoji = this.handleEditEmoji.bind(this);
|
||||
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
|
||||
this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
|
||||
this.handleUploadsPageChange = this.handleUploadsPageChange.bind(this);
|
||||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const { bannedRes, instancesRes } = this.isoData.routeData;
|
||||
const { bannedRes, instancesRes, uploadsRes } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
bannedRes,
|
||||
instancesRes,
|
||||
uploadsRes,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
|
@ -115,6 +130,7 @@ export class AdminSettings extends Component<
|
|||
return {
|
||||
bannedRes: await client.getBannedPersons(),
|
||||
instancesRes: await client.getFederatedInstances(),
|
||||
uploadsRes: await client.listAllMedia(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -256,6 +272,21 @@ export class AdminSettings extends Component<
|
|||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "uploads",
|
||||
label: I18NextService.i18n.t("uploads"),
|
||||
getNode: isSelected => (
|
||||
<div
|
||||
className={classNames("tab-pane", {
|
||||
active: isSelected,
|
||||
})}
|
||||
role="tabpanel"
|
||||
id="uploads-tab-pane"
|
||||
>
|
||||
{this.uploads()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
@ -266,22 +297,34 @@ export class AdminSettings extends Component<
|
|||
this.setState({
|
||||
bannedRes: LOADING_REQUEST,
|
||||
instancesRes: LOADING_REQUEST,
|
||||
uploadsRes: LOADING_REQUEST,
|
||||
themeList: [],
|
||||
});
|
||||
|
||||
const [bannedRes, instancesRes, themeList] = await Promise.all([
|
||||
const [bannedRes, instancesRes, uploadsRes, themeList] = await Promise.all([
|
||||
HttpService.client.getBannedPersons(),
|
||||
HttpService.client.getFederatedInstances(),
|
||||
HttpService.client.listAllMedia({
|
||||
page: this.state.uploadsPage,
|
||||
}),
|
||||
fetchThemeList(),
|
||||
]);
|
||||
|
||||
this.setState({
|
||||
bannedRes,
|
||||
instancesRes,
|
||||
uploadsRes,
|
||||
themeList,
|
||||
});
|
||||
}
|
||||
|
||||
async fetchUploadsOnly() {
|
||||
const uploadsRes = await HttpService.client.listAllMedia({
|
||||
page: this.state.uploadsPage,
|
||||
});
|
||||
this.setState({ uploadsRes });
|
||||
}
|
||||
|
||||
admins() {
|
||||
return (
|
||||
<>
|
||||
|
@ -341,6 +384,30 @@ export class AdminSettings extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
uploads() {
|
||||
switch (this.state.uploadsRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
);
|
||||
case "success": {
|
||||
const uploadsRes = this.state.uploadsRes.data;
|
||||
return (
|
||||
<div>
|
||||
<MediaUploads showUploader uploads={uploadsRes} />
|
||||
<Paginator
|
||||
page={this.state.uploadsPage}
|
||||
onChange={this.handleUploadsPageChange}
|
||||
nextDisabled={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleEditSite(form: EditSite) {
|
||||
this.setState({ loading: true });
|
||||
|
||||
|
@ -397,4 +464,10 @@ export class AdminSettings extends Component<
|
|||
updateEmojiDataModel(res.data.custom_emoji);
|
||||
}
|
||||
}
|
||||
|
||||
async handleUploadsPageChange(val: number) {
|
||||
this.setState({ uploadsPage: val });
|
||||
snapToTop();
|
||||
await this.fetchUploadsOnly();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ import {
|
|||
GetPersonDetailsResponse,
|
||||
GetSiteResponse,
|
||||
LemmyHttp,
|
||||
ListMedia,
|
||||
ListMediaResponse,
|
||||
LockPost,
|
||||
MarkCommentReplyAsRead,
|
||||
MarkPersonMentionAsRead,
|
||||
|
@ -95,13 +97,16 @@ import { PersonDetails } from "./person-details";
|
|||
import { PersonListing } from "./person-listing";
|
||||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { MediaUploads } from "../common/media-uploads";
|
||||
|
||||
type ProfileData = RouteDataResponse<{
|
||||
personResponse: GetPersonDetailsResponse;
|
||||
personRes: GetPersonDetailsResponse;
|
||||
uploadsRes: ListMediaResponse;
|
||||
}>;
|
||||
|
||||
interface ProfileState {
|
||||
personRes: RequestState<GetPersonDetailsResponse>;
|
||||
uploadsRes: RequestState<ListMediaResponse>;
|
||||
personBlocked: boolean;
|
||||
banReason?: string;
|
||||
banExpireDays?: number;
|
||||
|
@ -188,6 +193,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
private isoData = setIsoData<ProfileData>(this.context);
|
||||
state: ProfileState = {
|
||||
personRes: EMPTY_REQUEST,
|
||||
uploadsRes: EMPTY_REQUEST,
|
||||
personBlocked: false,
|
||||
siteRes: this.isoData.site_res,
|
||||
showBanDialog: false,
|
||||
|
@ -240,10 +246,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const personRes = this.isoData.routeData.personResponse;
|
||||
const personRes = this.isoData.routeData.personRes;
|
||||
const uploadsRes = this.isoData.routeData.uploadsRes;
|
||||
this.state = {
|
||||
...this.state,
|
||||
personRes,
|
||||
uploadsRes,
|
||||
isIsomorphic: true,
|
||||
personBlocked: isPersonBlocked(personRes),
|
||||
};
|
||||
|
@ -267,10 +275,21 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
page,
|
||||
limit: fetchLimit,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
personRes,
|
||||
personBlocked: isPersonBlocked(personRes),
|
||||
});
|
||||
|
||||
if (view === PersonDetailsView.Uploads) {
|
||||
this.setState({ uploadsRes: LOADING_REQUEST });
|
||||
const form: ListMedia = {
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
const uploadsRes = await HttpService.client.listMedia(form);
|
||||
this.setState({ uploadsRes });
|
||||
}
|
||||
}
|
||||
|
||||
get amCurrentUser() {
|
||||
|
@ -298,6 +317,16 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
new LemmyHttp(getHttpBaseInternal(), { headers }),
|
||||
);
|
||||
|
||||
let uploadsRes: RequestState<ListMediaResponse> = EMPTY_REQUEST;
|
||||
|
||||
if (view === PersonDetailsView.Uploads) {
|
||||
const form: ListMedia = {
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
uploadsRes = await client.listMedia(form);
|
||||
}
|
||||
|
||||
const form: GetPersonDetails = {
|
||||
username: username,
|
||||
sort,
|
||||
|
@ -305,9 +334,11 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
const personRes = await client.getPersonDetails(form);
|
||||
|
||||
return {
|
||||
personResponse: await client.getPersonDetails(form),
|
||||
personRes,
|
||||
uploadsRes,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -319,6 +350,25 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
: siteName;
|
||||
}
|
||||
|
||||
renderUploadsRes() {
|
||||
switch (this.state.uploadsRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
);
|
||||
case "success": {
|
||||
const uploadsRes = this.state.uploadsRes.data;
|
||||
return (
|
||||
<div>
|
||||
<MediaUploads uploads={uploadsRes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderPersonRes() {
|
||||
switch (this.state.personRes.state) {
|
||||
case "loading":
|
||||
|
@ -349,6 +399,8 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
|
||||
{this.selects}
|
||||
|
||||
{this.renderUploadsRes()}
|
||||
|
||||
<PersonDetails
|
||||
personRes={personRes}
|
||||
admins={siteRes.admins}
|
||||
|
@ -414,11 +466,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
|
||||
get viewRadios() {
|
||||
return (
|
||||
<div className="btn-group btn-group-toggle flex-wrap mb-2" role="group">
|
||||
<div className="btn-group btn-group-toggle flex-wrap" role="group">
|
||||
{this.getRadio(PersonDetailsView.Overview)}
|
||||
{this.getRadio(PersonDetailsView.Comments)}
|
||||
{this.getRadio(PersonDetailsView.Posts)}
|
||||
{this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
|
||||
{this.getRadio(PersonDetailsView.Uploads)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -457,18 +510,22 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`;
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<span className="me-3">{this.viewRadios}</span>
|
||||
<SortSelect
|
||||
sort={sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
hideMostComments
|
||||
/>
|
||||
<a href={profileRss} rel={relTags} title="RSS">
|
||||
<Icon icon="rss" classes="text-muted small mx-2" />
|
||||
</a>
|
||||
<link rel="alternate" type="application/atom+xml" href={profileRss} />
|
||||
<div className="row align-items-center mb-3 g-3">
|
||||
<div className="col-auto">{this.viewRadios}</div>
|
||||
<div className="col-auto">
|
||||
<SortSelect
|
||||
sort={sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
hideMostComments
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<a href={profileRss} rel={relTags} title="RSS">
|
||||
<Icon icon="rss" classes="text-muted small ps-0" />
|
||||
</a>
|
||||
<link rel="alternate" type="application/atom+xml" href={profileRss} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ export enum PersonDetailsView {
|
|||
Comments = "Comments",
|
||||
Posts = "Posts",
|
||||
Saved = "Saved",
|
||||
Uploads = "Uploads",
|
||||
}
|
||||
|
||||
export enum PurgeType {
|
||||
|
|
Loading…
Reference in a new issue