mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-01 01:59:56 +00:00
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:
parent
d9fe7d1488
commit
f0ccf93735
6 changed files with 206 additions and 7 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit de9de2c53bee034d3824ecaa9a2104f8f341332e
|
Subproject commit 18da10858d8c63750beb06247947f25d91944741
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
9
src/shared/utils/app/instance-to-choice.ts
Normal file
9
src/shared/utils/app/instance-to-choice.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
27
src/shared/utils/app/update-instance-block.ts
Normal file
27
src/shared/utils/app/update-instance-block.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue