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:
Dessalines 2024-04-17 08:37:58 -04:00 committed by GitHub
parent accf1b2d72
commit 9dcaff4301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 6198 additions and 4445 deletions

@ -1 +1 @@
Subproject commit a94ef775f3f923067b48c1719cda206dbcf1a9e5
Subproject commit f9783d686637197a389b8f10a907e0533c55b688

View file

@ -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": {

File diff suppressed because it is too large Load diff

View 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}`;
}

View file

@ -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 = {

View file

@ -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();
}
}

View file

@ -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>
);
}

View file

@ -67,6 +67,7 @@ export enum PersonDetailsView {
Comments = "Comments",
Posts = "Posts",
Saved = "Saved",
Uploads = "Uploads",
}
export enum PurgeType {