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-router": "^8.2.3",
"inferno-server": "^8.2.3", "inferno-server": "^8.2.3",
"jwt-decode": "^4.0.0", "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", "lodash.isequal": "^4.5.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-bidi": "^0.1.0", "markdown-it-bidi": "^0.1.0",
@ -139,7 +139,7 @@
"sortpack" "sortpack"
] ]
}, },
"packageManager": "pnpm@8.14.3", "packageManager": "pnpm@9.0.1+sha256.46d50ee2afecb42b185ebbd662dc7bdd52ef5be56bf035bb615cab81a75345df",
"engineStrict": true, "engineStrict": true,
"importSort": { "importSort": {
".js, .jsx, .ts, .tsx": { ".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> { export default class Tabs extends Component<TabsProps, TabsState> {
constructor(props: TabsProps, context) { constructor(props: TabsProps, context: any) {
super(props, context); super(props, context);
this.state = { this.state = {

View file

@ -13,6 +13,7 @@ import {
GetFederatedInstancesResponse, GetFederatedInstancesResponse,
GetSiteResponse, GetSiteResponse,
LemmyHttp, LemmyHttp,
ListMediaResponse,
PersonView, PersonView,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
@ -37,10 +38,14 @@ import { TaglineForm } from "./tagline-form";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { MediaUploads } from "../common/media-uploads";
import { Paginator } from "../common/paginator";
import { snapToTop } from "@utils/browser";
type AdminSettingsData = RouteDataResponse<{ type AdminSettingsData = RouteDataResponse<{
bannedRes: BannedPersonsResponse; bannedRes: BannedPersonsResponse;
instancesRes: GetFederatedInstancesResponse; instancesRes: GetFederatedInstancesResponse;
uploadsRes: ListMediaResponse;
}>; }>;
interface AdminSettingsState { interface AdminSettingsState {
@ -50,6 +55,8 @@ interface AdminSettingsState {
instancesRes: RequestState<GetFederatedInstancesResponse>; instancesRes: RequestState<GetFederatedInstancesResponse>;
bannedRes: RequestState<BannedPersonsResponse>; bannedRes: RequestState<BannedPersonsResponse>;
leaveAdminTeamRes: RequestState<GetSiteResponse>; leaveAdminTeamRes: RequestState<GetSiteResponse>;
uploadsRes: RequestState<ListMediaResponse>;
uploadsPage: number;
loading: boolean; loading: boolean;
themeList: string[]; themeList: string[];
isIsomorphic: boolean; isIsomorphic: boolean;
@ -76,13 +83,19 @@ export class AdminSettings extends Component<
bannedRes: EMPTY_REQUEST, bannedRes: EMPTY_REQUEST,
instancesRes: EMPTY_REQUEST, instancesRes: EMPTY_REQUEST,
leaveAdminTeamRes: EMPTY_REQUEST, leaveAdminTeamRes: EMPTY_REQUEST,
uploadsRes: EMPTY_REQUEST,
uploadsPage: 1,
loading: false, loading: false,
themeList: [], themeList: [],
isIsomorphic: false, isIsomorphic: false,
}; };
loadingSettled() { 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) { constructor(props: any, context: any) {
@ -92,15 +105,17 @@ export class AdminSettings extends Component<
this.handleEditEmoji = this.handleEditEmoji.bind(this); this.handleEditEmoji = this.handleEditEmoji.bind(this);
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this); this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
this.handleCreateEmoji = this.handleCreateEmoji.bind(this); this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
this.handleUploadsPageChange = this.handleUploadsPageChange.bind(this);
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const { bannedRes, instancesRes } = this.isoData.routeData; const { bannedRes, instancesRes, uploadsRes } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
bannedRes, bannedRes,
instancesRes, instancesRes,
uploadsRes,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -115,6 +130,7 @@ export class AdminSettings extends Component<
return { return {
bannedRes: await client.getBannedPersons(), bannedRes: await client.getBannedPersons(),
instancesRes: await client.getFederatedInstances(), instancesRes: await client.getFederatedInstances(),
uploadsRes: await client.listAllMedia(),
}; };
} }
@ -256,6 +272,21 @@ export class AdminSettings extends Component<
</div> </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> </div>
@ -266,22 +297,34 @@ export class AdminSettings extends Component<
this.setState({ this.setState({
bannedRes: LOADING_REQUEST, bannedRes: LOADING_REQUEST,
instancesRes: LOADING_REQUEST, instancesRes: LOADING_REQUEST,
uploadsRes: LOADING_REQUEST,
themeList: [], themeList: [],
}); });
const [bannedRes, instancesRes, themeList] = await Promise.all([ const [bannedRes, instancesRes, uploadsRes, themeList] = await Promise.all([
HttpService.client.getBannedPersons(), HttpService.client.getBannedPersons(),
HttpService.client.getFederatedInstances(), HttpService.client.getFederatedInstances(),
HttpService.client.listAllMedia({
page: this.state.uploadsPage,
}),
fetchThemeList(), fetchThemeList(),
]); ]);
this.setState({ this.setState({
bannedRes, bannedRes,
instancesRes, instancesRes,
uploadsRes,
themeList, themeList,
}); });
} }
async fetchUploadsOnly() {
const uploadsRes = await HttpService.client.listAllMedia({
page: this.state.uploadsPage,
});
this.setState({ uploadsRes });
}
admins() { admins() {
return ( 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) { async handleEditSite(form: EditSite) {
this.setState({ loading: true }); this.setState({ loading: true });
@ -397,4 +464,10 @@ export class AdminSettings extends Component<
updateEmojiDataModel(res.data.custom_emoji); 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, GetPersonDetailsResponse,
GetSiteResponse, GetSiteResponse,
LemmyHttp, LemmyHttp,
ListMedia,
ListMediaResponse,
LockPost, LockPost,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
@ -95,13 +97,16 @@ import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing"; import { PersonListing } from "./person-listing";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { MediaUploads } from "../common/media-uploads";
type ProfileData = RouteDataResponse<{ type ProfileData = RouteDataResponse<{
personResponse: GetPersonDetailsResponse; personRes: GetPersonDetailsResponse;
uploadsRes: ListMediaResponse;
}>; }>;
interface ProfileState { interface ProfileState {
personRes: RequestState<GetPersonDetailsResponse>; personRes: RequestState<GetPersonDetailsResponse>;
uploadsRes: RequestState<ListMediaResponse>;
personBlocked: boolean; personBlocked: boolean;
banReason?: string; banReason?: string;
banExpireDays?: number; banExpireDays?: number;
@ -188,6 +193,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
private isoData = setIsoData<ProfileData>(this.context); private isoData = setIsoData<ProfileData>(this.context);
state: ProfileState = { state: ProfileState = {
personRes: EMPTY_REQUEST, personRes: EMPTY_REQUEST,
uploadsRes: EMPTY_REQUEST,
personBlocked: false, personBlocked: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showBanDialog: false, showBanDialog: false,
@ -240,10 +246,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { 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 = {
...this.state, ...this.state,
personRes, personRes,
uploadsRes,
isIsomorphic: true, isIsomorphic: true,
personBlocked: isPersonBlocked(personRes), personBlocked: isPersonBlocked(personRes),
}; };
@ -267,10 +275,21 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
page, page,
limit: fetchLimit, limit: fetchLimit,
}); });
this.setState({ this.setState({
personRes, personRes,
personBlocked: isPersonBlocked(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() { get amCurrentUser() {
@ -298,6 +317,16 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
new LemmyHttp(getHttpBaseInternal(), { headers }), 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 = { const form: GetPersonDetails = {
username: username, username: username,
sort, sort,
@ -305,9 +334,11 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
page, page,
limit: fetchLimit, limit: fetchLimit,
}; };
const personRes = await client.getPersonDetails(form);
return { return {
personResponse: await client.getPersonDetails(form), personRes,
uploadsRes,
}; };
} }
@ -319,6 +350,25 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
: siteName; : 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() { renderPersonRes() {
switch (this.state.personRes.state) { switch (this.state.personRes.state) {
case "loading": case "loading":
@ -349,6 +399,8 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
{this.selects} {this.selects}
{this.renderUploadsRes()}
<PersonDetails <PersonDetails
personRes={personRes} personRes={personRes}
admins={siteRes.admins} admins={siteRes.admins}
@ -414,11 +466,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
get viewRadios() { get viewRadios() {
return ( 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.Overview)}
{this.getRadio(PersonDetailsView.Comments)} {this.getRadio(PersonDetailsView.Comments)}
{this.getRadio(PersonDetailsView.Posts)} {this.getRadio(PersonDetailsView.Posts)}
{this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)} {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
{this.getRadio(PersonDetailsView.Uploads)}
</div> </div>
); );
} }
@ -457,18 +510,22 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`; const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`;
return ( return (
<div className="mb-2"> <div className="row align-items-center mb-3 g-3">
<span className="me-3">{this.viewRadios}</span> <div className="col-auto">{this.viewRadios}</div>
<SortSelect <div className="col-auto">
sort={sort} <SortSelect
onChange={this.handleSortChange} sort={sort}
hideHot onChange={this.handleSortChange}
hideMostComments hideHot
/> hideMostComments
<a href={profileRss} rel={relTags} title="RSS"> />
<Icon icon="rss" classes="text-muted small mx-2" /> </div>
</a> <div className="col-auto">
<link rel="alternate" type="application/atom+xml" href={profileRss} /> <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> </div>
); );
} }

View file

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