feat: Block instance (#2144)

* Start modifying settings

* feat: Finish making block instance setting

* feat: Add translations

* fix: Handle first load fetch

* chore: Fix linting error

* fix: Fix broken import
This commit is contained in:
SleeplessOne1917 2023-09-27 18:11:08 +00:00 committed by GitHub
parent d9fe7d1488
commit f0ccf93735
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 7 deletions

@ -1 +1 @@
Subproject commit de9de2c53bee034d3824ecaa9a2104f8f341332e Subproject commit 18da10858d8c63750beb06247947f25d91944741

View file

@ -3,32 +3,38 @@ import {
fetchCommunities, fetchCommunities,
fetchThemeList, fetchThemeList,
fetchUsers, fetchUsers,
instanceToChoice,
myAuth, myAuth,
personToChoice, personToChoice,
setIsoData, setIsoData,
setTheme, setTheme,
showLocal, showLocal,
updateCommunityBlock, updateCommunityBlock,
updateInstanceBlock,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
import { capitalizeFirstLetter, debounce } from "@utils/helpers"; import { capitalizeFirstLetter, debounce } from "@utils/helpers";
import { Choice } from "@utils/types"; import { Choice, RouteDataResponse } from "@utils/types";
import classNames from "classnames"; import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockCommunityResponse, BlockCommunityResponse,
BlockInstanceResponse,
BlockPersonResponse, BlockPersonResponse,
CommunityBlockView, CommunityBlockView,
DeleteAccountResponse, DeleteAccountResponse,
GetFederatedInstancesResponse,
GetSiteResponse, GetSiteResponse,
Instance,
InstanceBlockView,
ListingType, ListingType,
LoginResponse, LoginResponse,
PersonBlockView, PersonBlockView,
SortType, SortType,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { elementUrl, emDash, relTags } from "../../config"; import { elementUrl, emDash, relTags } from "../../config";
import { UserService } from "../../services"; import { FirstLoadService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { I18NextService, languages } from "../../services/I18NextService"; import { I18NextService, languages } from "../../services/I18NextService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
@ -45,11 +51,17 @@ import { SortSelect } from "../common/sort-select";
import Tabs from "../common/tabs"; import Tabs from "../common/tabs";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
import { PersonListing } from "./person-listing"; import { PersonListing } from "./person-listing";
import { InitialFetchRequest } from "../../interfaces";
type SettingsData = RouteDataResponse<{
instancesRes: GetFederatedInstancesResponse;
}>;
interface SettingsState { interface SettingsState {
saveRes: RequestState<LoginResponse>; saveRes: RequestState<LoginResponse>;
changePasswordRes: RequestState<LoginResponse>; changePasswordRes: RequestState<LoginResponse>;
deleteAccountRes: RequestState<DeleteAccountResponse>; deleteAccountRes: RequestState<DeleteAccountResponse>;
instancesRes: RequestState<GetFederatedInstancesResponse>;
// TODO redo these forms // TODO redo these forms
saveUserSettingsForm: { saveUserSettingsForm: {
show_nsfw?: boolean; show_nsfw?: boolean;
@ -86,6 +98,7 @@ interface SettingsState {
}; };
personBlocks: PersonBlockView[]; personBlocks: PersonBlockView[];
communityBlocks: CommunityBlockView[]; communityBlocks: CommunityBlockView[];
instanceBlocks: InstanceBlockView[];
currentTab: string; currentTab: string;
themeList: string[]; themeList: string[];
deleteAccountShowConfirm: boolean; deleteAccountShowConfirm: boolean;
@ -94,22 +107,24 @@ interface SettingsState {
searchCommunityOptions: Choice[]; searchCommunityOptions: Choice[];
searchPersonLoading: boolean; searchPersonLoading: boolean;
searchPersonOptions: Choice[]; searchPersonOptions: Choice[];
searchInstanceOptions: Choice[];
isIsomorphic: boolean;
} }
type FilterType = "user" | "community"; type FilterType = "user" | "community" | "instance";
const Filter = ({ const Filter = ({
filterType, filterType,
options, options,
onChange, onChange,
onSearch, onSearch,
loading, loading = false,
}: { }: {
filterType: FilterType; filterType: FilterType;
options: Choice[]; options: Choice[];
onSearch: (text: string) => void; onSearch: (text: string) => void;
onChange: (choice: Choice) => void; onChange: (choice: Choice) => void;
loading: boolean; loading?: boolean;
}) => ( }) => (
<div className="mb-3 row"> <div className="mb-3 row">
<label <label
@ -133,17 +148,19 @@ const Filter = ({
); );
export class Settings extends Component<any, SettingsState> { export class Settings extends Component<any, SettingsState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<SettingsData>(this.context);
state: SettingsState = { state: SettingsState = {
saveRes: { state: "empty" }, saveRes: { state: "empty" },
deleteAccountRes: { state: "empty" }, deleteAccountRes: { state: "empty" },
changePasswordRes: { state: "empty" }, changePasswordRes: { state: "empty" },
instancesRes: { state: "empty" },
saveUserSettingsForm: {}, saveUserSettingsForm: {},
changePasswordForm: {}, changePasswordForm: {},
deleteAccountShowConfirm: false, deleteAccountShowConfirm: false,
deleteAccountForm: {}, deleteAccountForm: {},
personBlocks: [], personBlocks: [],
communityBlocks: [], communityBlocks: [],
instanceBlocks: [],
currentTab: "settings", currentTab: "settings",
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
themeList: [], themeList: [],
@ -151,6 +168,8 @@ export class Settings extends Component<any, SettingsState> {
searchCommunityOptions: [], searchCommunityOptions: [],
searchPersonLoading: false, searchPersonLoading: false,
searchPersonOptions: [], searchPersonOptions: [],
searchInstanceOptions: [],
isIsomorphic: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -172,6 +191,7 @@ export class Settings extends Component<any, SettingsState> {
this.handleBlockPerson = this.handleBlockPerson.bind(this); this.handleBlockPerson = this.handleBlockPerson.bind(this);
this.handleBlockCommunity = this.handleBlockCommunity.bind(this); this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
this.handleBlockInstance = this.handleBlockInstance.bind(this);
const mui = UserService.Instance.myUserInfo; const mui = UserService.Instance.myUserInfo;
if (mui) { if (mui) {
@ -232,11 +252,40 @@ export class Settings extends Component<any, SettingsState> {
}, },
}; };
} }
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
const { instancesRes } = this.isoData.routeData;
this.state = {
...this.state,
instancesRes,
isIsomorphic: true,
};
}
} }
async componentDidMount() { async componentDidMount() {
setupTippy(); setupTippy();
this.setState({ themeList: await fetchThemeList() }); this.setState({ themeList: await fetchThemeList() });
if (!this.state.isIsomorphic) {
this.setState({
instancesRes: { state: "loading" },
});
this.setState({
instancesRes: await HttpService.client.getFederatedInstances(),
});
}
}
static async fetchInitialData({
client,
}: InitialFetchRequest): Promise<SettingsData> {
return {
instancesRes: await client.getFederatedInstances(),
};
} }
get documentTitle(): string { get documentTitle(): string {
@ -315,6 +364,11 @@ export class Settings extends Component<any, SettingsState> {
<div className="card-body">{this.blockCommunityCard()}</div> <div className="card-body">{this.blockCommunityCard()}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-6">
<div className="card border-secondary mb-3">
<div className="card-body">{this.blockInstanceCard()}</div>
</div>
</div>
</div> </div>
</div> </div>
); );
@ -459,6 +513,49 @@ export class Settings extends Component<any, SettingsState> {
); );
} }
blockInstanceCard() {
const { searchInstanceOptions } = this.state;
return (
<div>
<Filter
filterType="instance"
onChange={this.handleBlockInstance}
onSearch={this.handleInstanceSearch}
options={searchInstanceOptions}
/>
{this.blockedInstancesList()}
</div>
);
}
blockedInstancesList() {
return (
<>
<h2 className="h5">{I18NextService.i18n.t("blocked_instances")}</h2>
<ul className="list-unstyled mb-0">
{this.state.instanceBlocks.map(ib => (
<li key={ib.instance.id}>
<span>
{ib.instance.domain}
<button
className="btn btn-sm"
onClick={linkEvent(
{ ctx: this, instanceId: ib.instance.id },
this.handleUnblockInstance,
)}
data-tippy-content={I18NextService.i18n.t("unblock_instance")}
>
<Icon icon="x" classes="icon-inline" />
</button>
</span>
</li>
))}
</ul>
</>
);
}
saveUserSettingsHtmlForm() { saveUserSettingsHtmlForm() {
const selectedLangs = this.state.saveUserSettingsForm.discussion_languages; const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
@ -987,6 +1084,27 @@ export class Settings extends Component<any, SettingsState> {
}); });
}); });
handleInstanceSearch = debounce(async (text: string) => {
let searchInstanceOptions: Instance[] = [];
if (this.state.instancesRes.state === "success") {
searchInstanceOptions =
this.state.instancesRes.data.federated_instances?.linked.filter(
instance =>
instance.domain.toLowerCase().includes(text.toLowerCase()) ||
!this.state.instanceBlocks.some(
blockedIntance => blockedIntance.instance.id === instance.id,
),
) ?? [];
}
this.setState({
searchInstanceOptions: searchInstanceOptions
.slice(0, 30)
.map(instanceToChoice),
});
});
async handleBlockPerson({ value }: Choice) { async handleBlockPerson({ value }: Choice) {
if (value !== "0") { if (value !== "0") {
const res = await HttpService.client.blockPerson({ const res = await HttpService.client.blockPerson({
@ -1031,6 +1149,31 @@ export class Settings extends Component<any, SettingsState> {
} }
} }
async handleBlockInstance({ value }: Choice) {
if (value !== "0") {
const id = Number(value);
const res = await HttpService.client.blockInstance({
block: true,
instance_id: id,
});
this.instanceBlock(id, res);
}
}
async handleUnblockInstance({
ctx,
instanceId,
}: {
ctx: Settings;
instanceId: number;
}) {
const res = await HttpService.client.blockInstance({
block: false,
instance_id: instanceId,
});
ctx.instanceBlock(instanceId, res);
}
handleShowNsfwChange(i: Settings, event: any) { handleShowNsfwChange(i: Settings, event: any) {
i.setState( i.setState(
s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s), s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s),
@ -1315,4 +1458,19 @@ export class Settings extends Component<any, SettingsState> {
} }
} }
} }
instanceBlock(id: number, res: RequestState<BlockInstanceResponse>) {
if (
res.state === "success" &&
this.state.instancesRes.state === "success"
) {
const linkedInstances =
this.state.instancesRes.data.federated_instances?.linked ?? [];
updateInstanceBlock(res.data, id, linkedInstances);
const mui = UserService.Instance.myUserInfo;
if (mui) {
this.setState({ instanceBlocks: mui.instance_blocks });
}
}
}
} }

View file

@ -95,6 +95,7 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
{ {
path: `/settings`, path: `/settings`,
component: Settings, component: Settings,
fetchInitialData: Settings.fetchInitialData,
}, },
{ {
path: `/modlog/:communityId`, path: `/modlog/:communityId`,

View file

@ -52,6 +52,8 @@ import showScores from "./show-scores";
import siteBannerCss from "./site-banner-css"; import siteBannerCss from "./site-banner-css";
import updateCommunityBlock from "./update-community-block"; import updateCommunityBlock from "./update-community-block";
import updatePersonBlock from "./update-person-block"; import updatePersonBlock from "./update-person-block";
import instanceToChoice from "./instance-to-choice";
import updateInstanceBlock from "./update-instance-block";
export { export {
buildCommentsTree, buildCommentsTree,
@ -108,4 +110,6 @@ export {
siteBannerCss, siteBannerCss,
updateCommunityBlock, updateCommunityBlock,
updatePersonBlock, updatePersonBlock,
instanceToChoice,
updateInstanceBlock,
}; };

View file

@ -0,0 +1,9 @@
import { Choice } from "@utils/types";
import { Instance } from "lemmy-js-client";
export default function instanceToChoice({ id, domain }: Instance): Choice {
return {
value: id.toString(),
label: domain,
};
}

View file

@ -0,0 +1,27 @@
import { BlockInstanceResponse, Instance, MyUserInfo } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { toast } from "../../toast";
export default function updateInstanceBlock(
data: BlockInstanceResponse,
id: number,
linkedInstances: Instance[],
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo,
) {
if (myUserInfo) {
const instance = linkedInstances.find(i => i.id === id)!;
if (data.blocked) {
myUserInfo.instance_blocks.push({
person: myUserInfo.local_user_view.person,
instance,
});
toast(`${I18NextService.i18n.t("blocked")} ${instance.domain}`);
} else {
myUserInfo.instance_blocks = myUserInfo.instance_blocks.filter(
i => i.instance.id !== id,
);
toast(`${I18NextService.i18n.t("unblocked")} ${instance.domain}`);
}
}
}