Skip to content

Commit

Permalink
feat: account moderation
Browse files Browse the repository at this point in the history
  • Loading branch information
mary-ext committed Apr 1, 2024
1 parent b63643b commit 1526599
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 80 deletions.
40 changes: 40 additions & 0 deletions app/api/mutations/upsert-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { XRPCError } from '@mary/bluesky-client/xrpc';
import type { AppBskyActorProfile, At } from '../atp-schema';
import { multiagent } from '../globals/agent';

export const upsertProfile = async (
uid: At.DID,
updater: (existing: AppBskyActorProfile.Record | undefined) => AppBskyActorProfile.Record,
) => {
const agent = await multiagent.connect(uid);

const existing = await agent.rpc
.get('com.atproto.repo.getRecord', {
params: {
repo: uid,
collection: 'app.bsky.actor.profile',
rkey: 'self',
},
})
.catch((err) => {
if (err instanceof XRPCError) {
if (err.kind === 'InvalidRequest') {
return undefined;
}
}

return Promise.reject(err);
});

const updated = updater(existing?.data.value as AppBskyActorProfile.Record | undefined);

await agent.rpc.call('com.atproto.repo.putRecord', {
data: {
repo: uid,
collection: 'app.bsky.actor.profile',
rkey: 'self',
record: updated,
swapRecord: existing?.data.cid,
},
});
};
40 changes: 40 additions & 0 deletions app/com/blocks/view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { JSX } from 'solid-js';

export interface ViewHeaderProps {
children?: JSX.Element;
}

export const ViewHeader = (props: ViewHeaderProps) => {
return (
<div class="flex h-13 min-w-0 shrink-0 items-center gap-2 border-b border-divider px-4">
{props.children}
</div>
);
};

export interface ViewHeadingProps {
title: string;
subtitle?: string;
}

export const ViewHeading = (props: ViewHeadingProps) => {
return (
<div class="flex min-w-0 grow flex-col gap-0.5">
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-base font-bold leading-5">
{props.title}
</p>

<p class="overflow-hidden text-ellipsis whitespace-nowrap text-xs text-muted-fg empty:hidden">
{props.subtitle}
</p>
</div>
);
};

export interface ViewBodyProps {
children?: JSX.Element;
}

export const ViewBody = (props: ViewBodyProps) => {
return <div class="flex min-h-0 grow flex-col overflow-y-auto">{props.children}</div>;
};
9 changes: 9 additions & 0 deletions app/com/icons/outline-people.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createIcon } from './_icon';

const PeopleOutlinedIcon = createIcon([
[
'M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5M4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5S5.5 6.57 5.5 8.5S7.07 12 9 12m0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7m7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44M15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35c.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35',
],
]);

export default PeopleOutlinedIcon;
61 changes: 2 additions & 59 deletions app/desktop/components/panes/dialogs/ProfileSettingsPaneDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createMutation, useQueryClient } from '@pkg/solid-query';

import TextareaAutosize from 'solid-textarea-autosize';

import type { AppBskyActorProfile, Brand, ComAtprotoLabelDefs } from '~/api/atp-schema';
import type { AppBskyActorProfile } from '~/api/atp-schema';
import { multiagent } from '~/api/globals/agent';
import { formatQueryError } from '~/api/utils/misc';

Expand All @@ -18,14 +18,12 @@ import { graphemeLen } from '~/api/richtext/intl';

import { formatLong } from '~/utils/intl/number';
import { model } from '~/utils/input';
import { mapDefined } from '~/utils/misc';

import { Button } from '~/com/primitives/button';
import { Input } from '~/com/primitives/input';
import { Textarea } from '~/com/primitives/textarea';

import AddPhotoButton from '~/com/components/inputs/AddPhotoButton';
import Checkbox from '~/com/components/inputs/Checkbox';
import BlobImage from '~/com/components/BlobImage';

import { usePaneModalState } from '../PaneContext';
Expand All @@ -39,8 +37,6 @@ export interface ProfileSettingsPaneDialogProps {

const MAX_DESC_LENGTH = 300;

const NoUnauthenticatedLabel = '!no-unauthenticated';

const profileRecordType = 'app.bsky.actor.profile';

type ProfileRecord = AppBskyActorProfile.Record;
Expand All @@ -60,10 +56,6 @@ const ProfileSettingsPaneDialog = (props: ProfileSettingsPaneDialogProps) => {
const actualDesc = createMemo(() => desc().replace(EOF_WS_RE, ''));
const length = createMemo(() => graphemeLen(actualDesc()));

const [labels, setLabels] = signal(
mapDefined(prof.labels.value, (x) => (x.src === prof.did ? x.val : undefined)),
);

const profileMutation = createMutation(() => ({
mutationFn: async () => {
let prev: ProfileRecord | undefined;
Expand All @@ -75,7 +67,6 @@ const ProfileSettingsPaneDialog = (props: ProfileSettingsPaneDialogProps) => {
const $banner = banner();
const $name = name();
const $description = actualDesc();
const $labels = labels();

const agent = await multiagent.connect(uid);

Expand Down Expand Up @@ -117,26 +108,20 @@ const ProfileSettingsPaneDialog = (props: ProfileSettingsPaneDialogProps) => {
? await uploadBlob<any>(uid, $banner)
: prev?.banner;

const nextLabels: Brand.Union<ComAtprotoLabelDefs.SelfLabels> | undefined =
$labels.length > 0
? { $type: 'com.atproto.label.defs#selfLabels', values: $labels.map((val) => ({ val: val })) }
: undefined;

let record: ProfileRecord | undefined = prev;

if (record) {
record.avatar = nextAvatar;
record.banner = nextBanner;
record.displayName = $name;
record.description = $description;
record.labels = nextLabels;
record.labels = record.labels;
} else {
record = {
avatar: nextAvatar,
banner: nextBanner,
displayName: $name,
description: $description,
labels: nextLabels,
};
}

Expand Down Expand Up @@ -258,52 +243,10 @@ const ProfileSettingsPaneDialog = (props: ProfileSettingsPaneDialogProps) => {
class={/* @once */ Textarea()}
/>
</label>

<hr class="mt-4 border-divider" />

<div class="px-4 py-3">
<label class="flex min-w-0 justify-between gap-4">
<span class="text-sm">Request limited visibility of my account</span>

<Checkbox
checked={labels().includes(NoUnauthenticatedLabel)}
onChange={(ev) => {
const next = ev.target.checked;
const array = labels();

if (next) {
setLabels(array.concat(NoUnauthenticatedLabel));
} else {
setLabels(removeItem(array, NoUnauthenticatedLabel));
}
}}
/>
</label>

<p class="mr-8 text-de text-muted-fg">
This option tells every app, including Bluesky app, that you don't want your account to be seen
by users who aren't currently signed in to an account.
</p>

<p class="mr-8 mt-1 text-de font-bold text-muted-fg">
Honoring this request is voluntary — your profile and posts will remain publicly available, and
some apps may show your account regardless.
</p>
</div>
</fieldset>
</form>
</PaneDialog>
);
};

export default ProfileSettingsPaneDialog;

const removeItem = <T,>(array: T[], item: T): T[] => {
const index = array.indexOf(item);

if (index !== -1) {
return array.toSpliced(index, 1);
}

return array;
};
5 changes: 1 addition & 4 deletions app/desktop/components/settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ComponentProps, type JSX, createSignal, Suspense } from 'solid-js';
import { type ComponentProps, type JSX, Suspense, createSignal } from 'solid-js';

import { clsx } from '~/utils/misc';

Expand All @@ -14,11 +14,9 @@ import CircularProgress from '~/com/components/CircularProgress';
import AccessibilityIcon from '~/com/icons/baseline-accessibility';
import CloseIcon from '~/com/icons/baseline-close';
import ColorLensIcon from '~/com/icons/baseline-color-lens';
import FilterAltIcon from '~/com/icons/baseline-filter-alt';
import InfoIcon from '~/com/icons/baseline-info';
import LanguageIcon from '~/com/icons/baseline-language';
import PeopleIcon from '~/com/icons/baseline-people';
import VisibilityIcon from '~/com/icons/baseline-visibility';

import {
type RouterState,
Expand All @@ -30,7 +28,6 @@ import {
VIEW_ACCOUNTS,
VIEW_APPEARANCE,
VIEW_MODERATION,
VIEW_KEYWORD_FILTERS,
VIEW_LANGAUGE,
useViewRouter,
} from './settings-views/_router';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import DefaultLabelerAvatar from '~/com/assets/default-labeler-avatar.svg?url';
import { preferences } from '../../../globals/settings';

import {
VIEW_ACCOUNT_MODERATION,
VIEW_HIDDEN_REPOSTERS,
VIEW_KEYWORD_FILTERS,
VIEW_LABELER_CONFIG,
Expand All @@ -52,11 +53,11 @@ const ModerationView = () => {
<div class={ListGroup}>
<p class={ListGroupHeader}>Account moderation</p>

<fieldset disabled class={ListBox}>
<fieldset class={ListBox}>
<For each={multiagent.accounts}>
{(account) => (
<button
onClick={() => router.move({ type: VIEW_KEYWORD_FILTERS })}
onClick={() => router.move({ type: VIEW_ACCOUNT_MODERATION, did: account.did })}
class={ListBoxItemInteractive}
>
<img
Expand All @@ -65,7 +66,7 @@ const ModerationView = () => {
/>

<div class="flex min-w-0 grow flex-col text-sm">
<p class="overflow-hidden text-ellipsis whitespace-nowrap font-bold empty:hidden">
<p class="overflow-hidden text-ellipsis whitespace-nowrap font-medium empty:hidden">
{account.profile?.displayName}
</p>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-muted-fg">
Expand All @@ -89,7 +90,7 @@ const ModerationView = () => {
class={ListBoxItemInteractive}
>
<FilterAltOutlinedIcon class={ListBoxItemIcon} />
<span class="grow">Keyword filters</span>
<span class="grow font-medium">Keyword filters</span>
<ChevronRightIcon class={ListBoxItemChevron} />
</button>

Expand All @@ -98,7 +99,7 @@ const ModerationView = () => {
class={ListBoxItemInteractive}
>
<VisibilityOffOutlinedIcon class={ListBoxItemIcon} />
<span class="grow">Silenced users</span>
<span class="grow font-medium">Silenced users</span>
<ChevronRightIcon class={ListBoxItemChevron} />
</button>

Expand All @@ -107,7 +108,7 @@ const ModerationView = () => {
class={ListBoxItemInteractive}
>
<RepeatOffIcon class={ListBoxItemIcon} />
<span class="grow">Hidden reposters</span>
<span class="grow font-medium">Hidden reposters</span>
<ChevronRightIcon class={ListBoxItemChevron} />
</button>
</div>
Expand Down Expand Up @@ -160,7 +161,7 @@ const ModerationView = () => {
/>

<div class="flex min-w-0 grow flex-col text-sm">
<p class="overflow-hidden text-ellipsis whitespace-nowrap font-bold empty:hidden">
<p class="overflow-hidden text-ellipsis whitespace-nowrap font-medium empty:hidden">
{profile.displayName}
</p>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-muted-fg">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import {
type ViewType,
VIEW_ABOUT,
VIEW_ACCESSIBILITY,
VIEW_ACCOUNT_MODERATION,
VIEW_ACCOUNTS,
VIEW_ADDITIONAL_LANGUAGE,
VIEW_APPEARANCE,
VIEW_MODERATION,
VIEW_EXCLUDED_TRANSLATION,
VIEW_HIDDEN_REPOSTERS,
VIEW_KEYWORD_FILTER_FORM,
VIEW_KEYWORD_FILTERS,
VIEW_LABELER_CONFIG,
VIEW_LANGAUGE,
VIEW_MODERATION,
VIEW_TEMPORARY_MUTES,
useViewRouter,
} from './_router';
Expand All @@ -22,9 +23,11 @@ const AboutView = lazy(() => import('./AboutView'));
const AccessibilityView = lazy(() => import('./AccessibilityView'));
const AccountsView = lazy(() => import('./AccountsView'));
const AppearanceView = lazy(() => import('./AppearanceView'));
const ModerationView = lazy(() => import('./ModerationView'));
const KeywordFiltersView = lazy(() => import('./KeywordFiltersView'));
const LanguageView = lazy(() => import('./LanguageView'));
const ModerationView = lazy(() => import('./ModerationView'));

const AccountModerationView = lazy(() => import('./moderation/AccountModerationView'))

const HiddenRepostersView = lazy(() => import('./content-filters/HiddenRepostersView'));
const LabelConfigView = lazy(() => import('./content-filters/LabelerConfigView'));
Expand All @@ -44,6 +47,8 @@ const views: Record<ViewType, Component> = {
[VIEW_KEYWORD_FILTERS]: KeywordFiltersView,
[VIEW_LANGAUGE]: LanguageView,

[VIEW_ACCOUNT_MODERATION]: AccountModerationView,

[VIEW_HIDDEN_REPOSTERS]: HiddenRepostersView,
[VIEW_LABELER_CONFIG]: LabelConfigView,
[VIEW_TEMPORARY_MUTES]: TemporaryMutesView,
Expand Down
Loading

0 comments on commit 1526599

Please sign in to comment.