/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import React, { useCallback, useMemo } from "react";
import {
useDefaultInteractionPoliciesQuery,
useResetDefaultInteractionPoliciesMutation,
useUpdateDefaultInteractionPoliciesMutation,
} from "../../../../lib/query/user";
import Loading from "../../../../components/loading";
import { Error } from "../../../../components/error";
import MutationButton from "../../../../components/form/mutation-button";
import {
DefaultInteractionPolicies,
InteractionPolicy,
InteractionPolicyEntry,
InteractionPolicyValue,
PolicyValueAuthor,
PolicyValueFollowers,
PolicyValueFollowing,
PolicyValueMentioned,
PolicyValuePublic,
} from "../../../../lib/types/interaction";
import { useTextInput } from "../../../../lib/form";
import { Select } from "../../../../components/form/inputs";
import { TextFormInputHook } from "../../../../lib/form/types";
import { useBasicFor } from "./basic";
import { PolicyFormSomethingElse, useSomethingElseFor } from "./something-else";
import { Action, PolicyFormSub, SomethingElseValue, Visibility } from "./types";
export default function InteractionPolicySettings() {
const {
data: defaultPolicies,
isLoading,
isFetching,
isError,
error,
} = useDefaultInteractionPoliciesQuery();
if (isLoading || isFetching) {
return ;
}
if (isError) {
return ;
}
if (!defaultPolicies) {
throw "default policies undefined";
}
return (
);
}
interface InteractionPoliciesFormProps {
defaultPolicies: DefaultInteractionPolicies;
}
function InteractionPoliciesForm({ defaultPolicies }: InteractionPoliciesFormProps) {
// Sub-form for visibility "public".
const formPublic = useFormForVis(defaultPolicies.public, "public");
const assemblePublic = useCallback(() => {
return {
can_favourite: assemblePolicyEntry("public", "favourite", formPublic),
can_reply: assemblePolicyEntry("public", "reply", formPublic),
can_reblog: assemblePolicyEntry("public", "reblog", formPublic),
};
}, [formPublic]);
// Sub-form for visibility "unlisted".
const formUnlisted = useFormForVis(defaultPolicies.unlisted, "unlisted");
const assembleUnlisted = useCallback(() => {
return {
can_favourite: assemblePolicyEntry("unlisted", "favourite", formUnlisted),
can_reply: assemblePolicyEntry("unlisted", "reply", formUnlisted),
can_reblog: assemblePolicyEntry("unlisted", "reblog", formUnlisted),
};
}, [formUnlisted]);
// Sub-form for visibility "private".
const formPrivate = useFormForVis(defaultPolicies.private, "private");
const assemblePrivate = useCallback(() => {
return {
can_favourite: assemblePolicyEntry("private", "favourite", formPrivate),
can_reply: assemblePolicyEntry("private", "reply", formPrivate),
can_reblog: assemblePolicyEntry("private", "reblog", formPrivate),
};
}, [formPrivate]);
const selectedVis = useTextInput("selectedVis", { defaultValue: "public" });
const [updatePolicies, updateResult] = useUpdateDefaultInteractionPoliciesMutation();
const [resetPolicies, resetResult] = useResetDefaultInteractionPoliciesMutation();
const onSubmit = (e) => {
e.preventDefault();
updatePolicies({
public: assemblePublic(),
unlisted: assembleUnlisted(),
private: assemblePrivate(),
// Always use the
// default for direct.
direct: null,
});
};
return (
);
}
// A tablist of tab buttons, one for each visibility.
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
return (
);
}
interface TabProps {
thisVisibility: string;
label: string,
selectedVis: TextFormInputHook
}
// One tab in a tablist, corresponding to the given thisVisibility.
function Tab({ thisVisibility, label, selectedVis }: TabProps) {
const selected = useMemo(() => {
return selectedVis.value === thisVisibility;
}, [selectedVis, thisVisibility]);
return (
);
}
interface PolicyPanelProps {
policyForm: PolicyForm;
forVis: Visibility;
isActive: boolean;
}
// Tab panel for one policy form of the given visibility.
function PolicyPanel({ policyForm, forVis, isActive }: PolicyPanelProps) {
return (
{ forVis !== "private" &&
}
);
}
interface PolicyComponentProps {
form: {
basic: PolicyFormSub;
somethingElse: PolicyFormSomethingElse;
};
forAction: Action;
}
// A component of one policy of the given
// visibility, corresponding to the given action.
function PolicyComponent({ form, forAction }: PolicyComponentProps) {
const legend = useLegend(forAction);
return (
);
}
/*
UTILITY FUNCTIONS
*/
// useLegend returns an appropriate
// fieldset legend for the given action.
function useLegend(action: Action) {
return useMemo(() => {
switch (action) {
case "favourite":
return (
<>
Like
>
);
case "reply":
return (
<>
Reply
>
);
case "reblog":
return (
<>
Boost
>
);
}
}, [action]);
}
// Form encapsulating the different
// actions for one visibility.
interface PolicyForm {
favourite: {
basic: PolicyFormSub,
somethingElse: PolicyFormSomethingElse,
}
reply: {
basic: PolicyFormSub,
somethingElse: PolicyFormSomethingElse,
}
reblog: {
basic: PolicyFormSub,
somethingElse: PolicyFormSomethingElse,
}
}
// Return a PolicyForm for the given visibility,
// set already to whatever the defaultPolicies value is.
function useFormForVis(
currentPolicy: InteractionPolicy,
forVis: Visibility,
): PolicyForm {
return {
favourite: {
basic: useBasicFor(
forVis,
"favourite",
currentPolicy.can_favourite.always,
currentPolicy.can_favourite.with_approval,
),
somethingElse: useSomethingElseFor(
forVis,
"favourite",
currentPolicy.can_favourite.always,
currentPolicy.can_favourite.with_approval,
),
},
reply: {
basic: useBasicFor(
forVis,
"reply",
currentPolicy.can_reply.always,
currentPolicy.can_reply.with_approval,
),
somethingElse: useSomethingElseFor(
forVis,
"reply",
currentPolicy.can_reply.always,
currentPolicy.can_reply.with_approval,
),
},
reblog: {
basic: useBasicFor(
forVis,
"reblog",
currentPolicy.can_reblog.always,
currentPolicy.can_reblog.with_approval,
),
somethingElse: useSomethingElseFor(
forVis,
"reblog",
currentPolicy.can_reblog.always,
currentPolicy.can_reblog.with_approval,
),
},
};
}
function assemblePolicyEntry(
forVis: Visibility,
forAction: Action,
policyForm: PolicyForm,
): InteractionPolicyEntry {
const basic = policyForm[forAction].basic;
// If this is followers visibility then
// "anyone" only means followers, not public.
const anyone: InteractionPolicyValue =
(forVis === "private")
? PolicyValueFollowers
: PolicyValuePublic;
// If this is a reply action then "just me"
// must include mentioned accounts as well,
// since they can always reply.
const justMe: InteractionPolicyValue[] =
(forAction === "reply")
? [PolicyValueAuthor, PolicyValueMentioned]
: [PolicyValueAuthor];
switch (basic.field.value) {
case "anyone":
return {
// Anyone can do this.
always: [anyone],
with_approval: [],
};
case "anyone_with_approval":
return {
// Author and maybe mentioned can do
// this, everyone else needs approval.
always: justMe,
with_approval: [anyone],
};
case "just_me":
return {
// Only author and maybe
// mentioned can do this.
always: justMe,
with_approval: [],
};
}
// Something else!
const somethingElse = policyForm[forAction].somethingElse;
// Start with basic "always"
// and "with_approval" values.
let always: InteractionPolicyValue[] = justMe;
let withApproval: InteractionPolicyValue[] = [];
// Add PolicyValueFollowers depending on choices made.
switch (somethingElse.followers.field.value as SomethingElseValue) {
case "always":
always.push(PolicyValueFollowers);
break;
case "with_approval":
withApproval.push(PolicyValueFollowers);
break;
}
// Add PolicyValueFollowing depending on choices made.
switch (somethingElse.following.field.value as SomethingElseValue) {
case "always":
always.push(PolicyValueFollowing);
break;
case "with_approval":
withApproval.push(PolicyValueFollowing);
break;
}
// Add PolicyValueMentioned depending on choices made.
// Note: mentioned can always reply, and that's already
// included above, so only do this if action is not reply.
if (forAction !== "reply") {
switch (somethingElse.mentioned.field.value as SomethingElseValue) {
case "always":
always.push(PolicyValueMentioned);
break;
case "with_approval":
withApproval.push(PolicyValueMentioned);
break;
}
}
// Add anyone depending on choices made.
switch (somethingElse.everyoneElse.field.value as SomethingElseValue) {
case "with_approval":
withApproval.push(anyone);
break;
}
// Simplify a bit after
// all the parsing above.
if (always.includes(anyone)) {
always = [anyone];
}
if (withApproval.includes(anyone)) {
withApproval = [anyone];
}
return {
always: always,
with_approval: withApproval,
};
}