Add anonymous and no-photo privacy toggles for visibility and DMs

This commit is contained in:
Ryan Salazar
2026-06-26 22:28:38 -04:00
parent 5828147341
commit 684de38ffd
11 changed files with 417 additions and 11 deletions

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import paramiko
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect("10.10.0.10", username="localadministrator", password="Bbt9115xty9176!", timeout=20, allow_agent=False, look_for_keys=False)
_, o, e = c.exec_command(
"docker exec coolify-db psql -U coolify -d coolify -t -A -c "
'"SELECT id,status FROM application_deployment_queues ORDER BY id DESC LIMIT 3;"',
timeout=60,
)
o.channel.recv_exit_status()
print((o.read() + e.read()).decode())
_, o, e = c.exec_command(
"docker exec coolify-db psql -U coolify -d coolify -t -A -c "
'"SELECT logs FROM application_deployment_queues ORDER BY id DESC LIMIT 1;"',
timeout=120,
)
o.channel.recv_exit_status()
raw = (o.read() + e.read()).decode()
for needle in ["Failed to compile", "Type error", "error TS", "Module not found", "Cannot find", "Error:"]:
idx = raw.rfind(needle)
if idx >= 0:
print("\n===", needle, "===\n")
print(raw[idx : idx + 3500])
c.close()

43
deploy/redeploy-poll.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import paramiko
import time
HOST = "10.10.0.10"
USER = "localadministrator"
PASS = "Bbt9115xty9176!"
APP = "ira4bw4nhbvm87o7s3fgqu6v"
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect(HOST, username=USER, password=PASS, timeout=20, allow_agent=False, look_for_keys=False)
def run(cmd, t=300):
print("$", cmd[:160])
_, o, e = c.exec_command(cmd, timeout=t)
code = o.channel.recv_exit_status()
out = (o.read() + e.read()).decode(errors="replace")
print(out[-4000:])
print("exit", code, "\n")
return out, code
run("cd /tmp/exposedgays && git fetch origin && git reset --hard origin/main && git log -1 --oneline")
run(
"TOKEN=$(cat /home/localadministrator/.coolify-token | tail -1); "
f"curl -sS -X POST 'http://127.0.0.1:8000/api/v1/deploy?uuid={APP}&force_rebuild=true' "
'-H "Authorization: Bearer $TOKEN"'
)
for i in range(180):
out, _ = run("docker exec coolify php artisan queue:work --queue=high,default --once --timeout=900 2>&1", t=920)
if "ApplicationDeploymentJob" in out:
if "DONE" in out or "finished" in out.lower():
print("=== DEPLOY SUCCESS ===")
break
if "FAIL" in out:
print("=== DEPLOY FAILED ===")
break
time.sleep(2)
run('docker exec coolify-db psql -U coolify -d coolify -t -c "SELECT id,status FROM application_deployment_queues ORDER BY id DESC LIMIT 1;"')
run("curl -sS https://exposedgays.com/ 2>&1 | head -c 400")
c.close()

View File

@@ -28,6 +28,7 @@ import { useAccess, useFeedPreferences, useAudienceView } from "@/components/Pro
import { usePrivacy } from "@/contexts/PrivacyContext"; import { usePrivacy } from "@/contexts/PrivacyContext";
import HideFromTraitsPanel from "@/components/HideFromTraitsPanel"; import HideFromTraitsPanel from "@/components/HideFromTraitsPanel";
import BlockListPanel from "@/components/BlockListPanel"; import BlockListPanel from "@/components/BlockListPanel";
import PrivacyAudienceToggles from "@/components/PrivacyAudienceToggles";
import { BODY_TYPE_OPTIONS } from "@/lib/privacy-rules"; import { BODY_TYPE_OPTIONS } from "@/lib/privacy-rules";
import { import {
US_STATES, US_STATES,
@@ -51,6 +52,8 @@ export default function ProfilePage() {
unblockProfile, unblockProfile,
unblockByUsername, unblockByUsername,
unblockAll, unblockAll,
privacySettings,
setPrivacySettings,
} = usePrivacy(); } = usePrivacy();
const [authOpen, setAuthOpen] = useState(false); const [authOpen, setAuthOpen] = useState(false);
const [idModalOpen, setIdModalOpen] = useState(false); const [idModalOpen, setIdModalOpen] = useState(false);
@@ -277,6 +280,11 @@ export default function ProfilePage() {
</div> </div>
)} )}
<PrivacyAudienceToggles
settings={privacySettings}
onChange={setPrivacySettings}
/>
<HideFromTraitsPanel <HideFromTraitsPanel
rules={hideRules} rules={hideRules}
onAdd={addHideRule} onAdd={addHideRule}

View File

@@ -11,6 +11,13 @@ import { CannedMessagesPanel } from "@/components/CannedMessagesPanel";
import { LocalGuidePanel } from "@/components/LocalGuidePanel"; import { LocalGuidePanel } from "@/components/LocalGuidePanel";
import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner"; import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner";
import { useAccess } from "@/components/Providers"; import { useAccess } from "@/components/Providers";
import { useAuth } from "@/lib/auth/context";
import { usePrivacy } from "@/contexts/PrivacyContext";
import { MOCK_PROFILES } from "@/lib/mock-data";
import { canViewerMessageTarget, guestViewerProfile } from "@/lib/visibility";
import { loadHideFromRules } from "@/lib/privacy-rules";
import { loadBlockList } from "@/lib/block-list";
import { loadPrivacySettings, messageBlockReason } from "@/lib/privacy-settings";
interface Message { interface Message {
id: string; id: string;
@@ -72,6 +79,8 @@ interface ChatPanelProps {
export function ChatPanel({ dmUserId }: ChatPanelProps) { export function ChatPanel({ dmUserId }: ChatPanelProps) {
const { capabilities } = useAccess(); const { capabilities } = useAccess();
const { user, profile: authProfile } = useAuth();
const { blockList } = usePrivacy();
const [cityMessages, setCityMessages] = useState<Message[]>(MOCK_CITY_MESSAGES); const [cityMessages, setCityMessages] = useState<Message[]>(MOCK_CITY_MESSAGES);
const [dmMessages, setDmMessages] = useState<Message[]>(MOCK_DM_MESSAGES); const [dmMessages, setDmMessages] = useState<Message[]>(MOCK_DM_MESSAGES);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
@@ -105,7 +114,29 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
}; };
const messages = activeTab === "city" ? cityMessages : dmMessages; const messages = activeTab === "city" ? cityMessages : dmMessages;
const showComposer = activeTab === "city" || activeTab === "dm";
const dmTarget = dmUserId
? MOCK_PROFILES.find((p) => p.id === dmUserId) ?? null
: null;
const viewer = authProfile ?? guestViewerProfile();
const viewerId = user?.id ?? "guest";
const dmAllowed =
!dmTarget ||
canViewerMessageTarget(
viewer,
viewerId,
dmTarget,
loadHideFromRules(dmTarget.id),
blockList,
loadBlockList(dmTarget.id)
);
const dmBlockNote = dmTarget
? messageBlockReason(viewer, loadPrivacySettings(dmTarget.id))
: null;
const showComposer =
(activeTab === "city" || activeTab === "dm") &&
(activeTab !== "dm" || dmAllowed);
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
@@ -139,12 +170,21 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
<TabsContent value="dm" className="flex-1 flex flex-col mt-0 overflow-hidden data-[state=inactive]:hidden"> <TabsContent value="dm" className="flex-1 flex flex-col mt-0 overflow-hidden data-[state=inactive]:hidden">
<div className="px-3 sm:px-4 py-2 border-b border-border"> <div className="px-3 sm:px-4 py-2 border-b border-border">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{dmUserId ? `Chat with User #${dmUserId}` : "Private Messages"} {dmTarget
? dmTarget.display_name || dmTarget.username || `User #${dmUserId}`
: dmUserId
? `Chat with User #${dmUserId}`
: "Private Messages"}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Use quick replies for address, directions & hosting notes Use quick replies for address, directions & hosting notes
</p> </p>
</div> </div>
{!dmAllowed && dmBlockNote && (
<div className="mx-3 sm:mx-4 mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
{dmBlockNote}
</div>
)}
<MessageList messages={dmMessages} /> <MessageList messages={dmMessages} />
</TabsContent> </TabsContent>

View File

@@ -20,6 +20,11 @@ import { Button } from "@/components/ui/button";
import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner"; import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner";
import { useAccess } from "@/components/Providers"; import { useAccess } from "@/components/Providers";
import { usePrivacy } from "@/contexts/PrivacyContext"; import { usePrivacy } from "@/contexts/PrivacyContext";
import { useAuth } from "@/lib/auth/context";
import { canViewerMessageTarget, guestViewerProfile } from "@/lib/visibility";
import { loadHideFromRules } from "@/lib/privacy-rules";
import { loadBlockList } from "@/lib/block-list";
import { loadPrivacySettings, messageBlockReason } from "@/lib/privacy-settings";
import type { Profile } from "@/types"; import type { Profile } from "@/types";
import { formatStatsLine, formatDistance } from "@/lib/profile-stats"; import { formatStatsLine, formatDistance } from "@/lib/profile-stats";
import { timeAgo } from "@/lib/utils"; import { timeAgo } from "@/lib/utils";
@@ -86,7 +91,8 @@ export function CruiserProfileView({
vanillaMode, vanillaMode,
}: CruiserProfileViewProps) { }: CruiserProfileViewProps) {
const { capabilities, blurExplicit } = useAccess(); const { capabilities, blurExplicit } = useAccess();
const { blockProfile, isBlocked, unblockProfile } = usePrivacy(); const { user, profile: authProfile } = useAuth();
const { blockProfile, isBlocked, unblockProfile, blockList } = usePrivacy();
const [tab, setTab] = useState<Tab>("about"); const [tab, setTab] = useState<Tab>("about");
const [blockMenuOpen, setBlockMenuOpen] = useState(false); const [blockMenuOpen, setBlockMenuOpen] = useState(false);
const [activePhoto, setActivePhoto] = useState(0); const [activePhoto, setActivePhoto] = useState(0);
@@ -123,9 +129,25 @@ export function CruiserProfileView({
const shouldBlurPhoto = (explicit: boolean) => const shouldBlurPhoto = (explicit: boolean) =>
vanillaMode || blurExplicit || (explicit && !capabilities.canViewExplicit); vanillaMode || blurExplicit || (explicit && !capabilities.canViewExplicit);
const viewer = authProfile ?? guestViewerProfile();
const viewerId = user?.id ?? "guest";
const canMessage = canViewerMessageTarget(
viewer,
viewerId,
profile,
loadHideFromRules(profile.id),
blockList,
loadBlockList(profile.id)
);
const messageBlockedNote = messageBlockReason(
viewer,
loadPrivacySettings(profile.id)
);
const handleChat = () => { const handleChat = () => {
if (!capabilities.canSendMessages) return; if (!capabilities.canSendMessages) return;
if (isBlocked(profile.id)) return; if (isBlocked(profile.id)) return;
if (!canMessage) return;
onChat(profile); onChat(profile);
}; };
@@ -464,8 +486,14 @@ export function CruiserProfileView({
size="icon" size="icon"
className="rounded-full h-11 w-11 shrink-0" className="rounded-full h-11 w-11 shrink-0"
onClick={handleChat} onClick={handleChat}
disabled={!capabilities.canSendMessages} disabled={!capabilities.canSendMessages || !canMessage || isBlocked(profile.id)}
title={capabilities.canSendMessages ? "Chat" : "Sign up to chat"} title={
!capabilities.canSendMessages
? "Sign up to chat"
: !canMessage && messageBlockedNote
? messageBlockedNote
: "Chat"
}
> >
<MessageCircle className="h-5 w-5" /> <MessageCircle className="h-5 w-5" />
</Button> </Button>

View File

@@ -20,7 +20,9 @@ import { profileMatchesAudienceView } from "@/lib/audience";
import { MOCK_PROFILES, MOCK_SPOTS } from "@/lib/mock-data"; import { MOCK_PROFILES, MOCK_SPOTS } from "@/lib/mock-data";
import { useAuth } from "@/lib/auth/context"; import { useAuth } from "@/lib/auth/context";
import { usePrivacy } from "@/contexts/PrivacyContext"; import { usePrivacy } from "@/contexts/PrivacyContext";
import { filterVisibleProfiles, guestViewerProfile } from "@/lib/visibility"; import { canViewerMessageTarget, filterVisibleProfiles, guestViewerProfile } from "@/lib/visibility";
import { loadHideFromRules } from "@/lib/privacy-rules";
import { loadBlockList } from "@/lib/block-list";
import { distanceMiles } from "@/lib/profile-stats"; import { distanceMiles } from "@/lib/profile-stats";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -183,10 +185,22 @@ export function CruisingMap({ vanillaMode, onActiveCountChange }: MapProps) {
onActiveCountChange?.(filteredProfiles.filter((p) => p.status !== "offline").length); onActiveCountChange?.(filteredProfiles.filter((p) => p.status !== "offline").length);
}, [filteredProfiles, onActiveCountChange]); }, [filteredProfiles, onActiveCountChange]);
const handleChat = useCallback((profile: Profile) => { const handleChat = useCallback(
if (!capabilities.canSendMessages) return; (profile: Profile) => {
window.location.href = `/chat?user=${profile.id}`; if (!capabilities.canSendMessages) return;
}, [capabilities.canSendMessages]); const allowed = canViewerMessageTarget(
viewer,
viewerId,
profile,
loadHideFromRules(profile.id),
blockList,
loadBlockList(profile.id)
);
if (!allowed) return;
window.location.href = `/chat?user=${profile.id}`;
},
[capabilities.canSendMessages, viewer, viewerId, blockList]
);
const togglePosition = (pos: Position) => { const togglePosition = (pos: Position) => {
setFilters((f) => ({ setFilters((f) => ({

View File

@@ -0,0 +1,96 @@
'use client';
import { Ghost, CameraOff } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import type { PrivacySettings } from '@/lib/privacy-settings';
interface PrivacyAudienceTogglesProps {
settings: PrivacySettings;
onChange: (settings: PrivacySettings) => void;
}
function ToggleRow({
title,
description,
hideLabel,
messageLabel,
hideOn,
messagesOn,
onHideChange,
onMessagesChange,
}: {
title: string;
description: string;
hideLabel: string;
messageLabel: string;
hideOn: boolean;
messagesOn: boolean;
onHideChange: (v: boolean) => void;
onMessagesChange: (v: boolean) => void;
}) {
return (
<div className="rounded-xl border border-zinc-700/80 bg-zinc-900/50 p-4 space-y-3">
<div>
<p className="text-sm font-semibold text-white">{title}</p>
<p className="text-xs text-zinc-400 mt-1 leading-relaxed">{description}</p>
</div>
<div className="flex items-center justify-between gap-3 py-1">
<Label className="text-sm text-zinc-300 font-normal">{hideLabel}</Label>
<Switch checked={hideOn} onCheckedChange={onHideChange} />
</div>
<div className="flex items-center justify-between gap-3 py-1 border-t border-zinc-800 pt-3">
<Label className="text-sm text-zinc-300 font-normal">{messageLabel}</Label>
<Switch checked={messagesOn} onCheckedChange={onMessagesChange} />
</div>
</div>
);
}
export default function PrivacyAudienceToggles({
settings,
onChange,
}: PrivacyAudienceTogglesProps) {
const patch = (partial: Partial<PrivacySettings>) =>
onChange({ ...settings, ...partial });
return (
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-zinc-500">
Audience filters
</p>
<ToggleRow
title="Anonymous profiles"
description="Browsing anonymously or without a display name. You can block visibility and messages separately."
hideLabel="Hide me from anonymous users"
messageLabel="Block messages from anonymous users"
hideOn={settings.hide_from_anonymous}
messagesOn={settings.block_messages_from_anonymous}
onHideChange={(v) => patch({ hide_from_anonymous: v })}
onMessagesChange={(v) => patch({ block_messages_from_anonymous: v })}
/>
<div className="flex gap-2 text-xs text-zinc-500 px-1">
<Ghost className="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>Applies to guests and anyone with &quot;Browse anonymously&quot; turned on.</span>
</div>
<ToggleRow
title="Profiles without photos"
description="No profile photo and no gallery uploads. Separate from anonymous — someone can be named but photo-less."
hideLabel="Hide me from users without photos"
messageLabel="Block messages from users without photos"
hideOn={settings.hide_from_no_photo}
messagesOn={settings.block_messages_from_no_photo}
onHideChange={(v) => patch({ hide_from_no_photo: v })}
onMessagesChange={(v) => patch({ block_messages_from_no_photo: v })}
/>
<div className="flex gap-2 text-xs text-zinc-500 px-1">
<CameraOff className="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>Upload at least one photo in your gallery so others know you&apos;re not in this group.</span>
</div>
</div>
);
}

View File

@@ -25,10 +25,18 @@ import {
type BlockEntry, type BlockEntry,
} from '@/lib/block-list'; } from '@/lib/block-list';
import type { Profile } from '@/types'; import type { Profile } from '@/types';
import {
DEFAULT_PRIVACY_SETTINGS,
loadPrivacySettings,
savePrivacySettings,
type PrivacySettings,
} from '@/lib/privacy-settings';
interface PrivacyContextValue { interface PrivacyContextValue {
hideRules: HideFromRule[]; hideRules: HideFromRule[];
blockList: BlockEntry[]; blockList: BlockEntry[];
privacySettings: PrivacySettings;
setPrivacySettings: (settings: PrivacySettings) => void;
addHideRule: (rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => void; addHideRule: (rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => void;
removeHideRule: (id: string) => void; removeHideRule: (id: string) => void;
blockProfile: (profile: Profile) => void; blockProfile: (profile: Profile) => void;
@@ -47,10 +55,14 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
const [hideRules, setHideRules] = useState<HideFromRule[]>([]); const [hideRules, setHideRules] = useState<HideFromRule[]>([]);
const [blockList, setBlockList] = useState<BlockEntry[]>([]); const [blockList, setBlockList] = useState<BlockEntry[]>([]);
const [privacySettings, setPrivacySettingsState] = useState<PrivacySettings>(
DEFAULT_PRIVACY_SETTINGS
);
const refresh = useCallback(() => { const refresh = useCallback(() => {
setHideRules(loadHideFromRules(userId)); setHideRules(loadHideFromRules(userId));
setBlockList(loadBlockList(userId)); setBlockList(loadBlockList(userId));
setPrivacySettingsState(loadPrivacySettings(userId));
}, [userId]); }, [userId]);
useEffect(() => { useEffect(() => {
@@ -60,14 +72,25 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
const onHide = () => refresh(); const onHide = () => refresh();
const onBlock = () => refresh(); const onBlock = () => refresh();
const onSettings = () => refresh();
window.addEventListener('eg-hide-rules', onHide); window.addEventListener('eg-hide-rules', onHide);
window.addEventListener('eg-block-list', onBlock); window.addEventListener('eg-block-list', onBlock);
window.addEventListener('eg-privacy-settings', onSettings);
return () => { return () => {
window.removeEventListener('eg-hide-rules', onHide); window.removeEventListener('eg-hide-rules', onHide);
window.removeEventListener('eg-block-list', onBlock); window.removeEventListener('eg-block-list', onBlock);
window.removeEventListener('eg-privacy-settings', onSettings);
}; };
}, [refresh]); }, [refresh]);
const setPrivacySettings = useCallback(
(settings: PrivacySettings) => {
savePrivacySettings(settings, userId);
setPrivacySettingsState(settings);
},
[userId]
);
const addHideRule = useCallback( const addHideRule = useCallback(
(rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => { (rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => {
const next = addHideFromRule(rule, userId); const next = addHideFromRule(rule, userId);
@@ -122,6 +145,8 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
() => ({ () => ({
hideRules, hideRules,
blockList, blockList,
privacySettings,
setPrivacySettings,
addHideRule, addHideRule,
removeHideRule, removeHideRule,
blockProfile, blockProfile,
@@ -134,6 +159,8 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
[ [
hideRules, hideRules,
blockList, blockList,
privacySettings,
setPrivacySettings,
addHideRule, addHideRule,
removeHideRule, removeHideRule,
blockProfile, blockProfile,

View File

@@ -0,0 +1,91 @@
import type { Profile } from '@/types';
const LS_SETTINGS = 'eg_privacy_settings';
const LS_SETTINGS_USER = (userId: string) => `eg_privacy_settings_${userId}`;
export interface PrivacySettings {
/** Anonymous / unnamed browsers cannot see you on map, search, or rails. */
hide_from_anonymous: boolean;
/** Anonymous users cannot send you DMs. */
block_messages_from_anonymous: boolean;
/** Profiles with no real photo cannot see you. */
hide_from_no_photo: boolean;
/** Profiles with no real photo cannot message you. */
block_messages_from_no_photo: boolean;
}
export const DEFAULT_PRIVACY_SETTINGS: PrivacySettings = {
hide_from_anonymous: false,
block_messages_from_anonymous: false,
hide_from_no_photo: false,
block_messages_from_no_photo: false,
};
const PLACEHOLDER_HINTS = ['placehold.co', 'text=?', 'text=%3f', 'text=%3F'];
export function profileHasPhoto(profile: Profile): boolean {
if (profile.gallery?.some((g) => g.url?.trim())) return true;
const url = profile.avatar_url?.trim();
if (!url) return false;
const lower = url.toLowerCase();
return !PLACEHOLDER_HINTS.some((hint) => lower.includes(hint));
}
/** Guest browse + profiles marked anonymous. */
export function isAnonymousProfile(profile: Profile): boolean {
return profile.id === 'guest' || profile.is_anonymous;
}
export function loadPrivacySettings(userId?: string | null): PrivacySettings {
if (typeof window === 'undefined') return { ...DEFAULT_PRIVACY_SETTINGS };
try {
const key = userId ? LS_SETTINGS_USER(userId) : LS_SETTINGS;
const raw = localStorage.getItem(key);
if (!raw) return { ...DEFAULT_PRIVACY_SETTINGS };
const parsed = JSON.parse(raw) as Partial<PrivacySettings>;
return { ...DEFAULT_PRIVACY_SETTINGS, ...parsed };
} catch {
return { ...DEFAULT_PRIVACY_SETTINGS };
}
}
export function savePrivacySettings(
settings: PrivacySettings,
userId?: string | null
): void {
if (typeof window === 'undefined') return;
const key = userId ? LS_SETTINGS_USER(userId) : LS_SETTINGS;
localStorage.setItem(key, JSON.stringify(settings));
window.dispatchEvent(new CustomEvent('eg-privacy-settings', { detail: settings }));
}
export function viewerHiddenByPrivacySettings(
viewer: Profile,
settings: PrivacySettings
): boolean {
if (settings.hide_from_anonymous && isAnonymousProfile(viewer)) return true;
if (settings.hide_from_no_photo && !profileHasPhoto(viewer)) return true;
return false;
}
export function viewerMessagesBlockedByPrivacySettings(
viewer: Profile,
settings: PrivacySettings
): boolean {
if (settings.block_messages_from_anonymous && isAnonymousProfile(viewer)) return true;
if (settings.block_messages_from_no_photo && !profileHasPhoto(viewer)) return true;
return false;
}
export function messageBlockReason(
viewer: Profile,
settings: PrivacySettings
): string | null {
if (settings.block_messages_from_anonymous && isAnonymousProfile(viewer)) {
return 'This cruiser does not accept messages from anonymous profiles.';
}
if (settings.block_messages_from_no_photo && !profileHasPhoto(viewer)) {
return 'This cruiser only accepts messages from profiles with photos.';
}
return null;
}

View File

@@ -3,6 +3,11 @@ import type { BlockEntry } from "@/lib/block-list";
import { isBlocked, loadBlockList } from "@/lib/block-list"; import { isBlocked, loadBlockList } from "@/lib/block-list";
import type { HideFromRule } from "@/lib/privacy-rules"; import type { HideFromRule } from "@/lib/privacy-rules";
import { loadHideFromRules, viewerHiddenByRules } from "@/lib/privacy-rules"; import { loadHideFromRules, viewerHiddenByRules } from "@/lib/privacy-rules";
import {
loadPrivacySettings,
viewerHiddenByPrivacySettings,
viewerMessagesBlockedByPrivacySettings,
} from "@/lib/privacy-settings";
export interface VisibilityContext { export interface VisibilityContext {
/** The browsing user's profile (or guest stand-in). */ /** The browsing user's profile (or guest stand-in). */
@@ -36,6 +41,28 @@ export function canViewerSeeTarget(
if (viewer && viewerHiddenByRules(viewer, myHideRules)) return false; if (viewer && viewerHiddenByRules(viewer, myHideRules)) return false;
const targetPrivacy = loadPrivacySettings(target.id);
if (viewer && viewerHiddenByPrivacySettings(viewer, targetPrivacy)) return false;
return true;
}
/** Can `viewer` open a DM thread with `target`? */
export function canViewerMessageTarget(
viewer: Profile | null,
viewerId: string | null,
target: Profile,
myHideRules: HideFromRule[],
myBlockList: BlockEntry[],
targetBlockList: BlockEntry[] = []
): boolean {
if (!canViewerSeeTarget(viewer, viewerId, target, myHideRules, myBlockList, targetBlockList)) {
return false;
}
if (viewer) {
const targetPrivacy = loadPrivacySettings(target.id);
if (viewerMessagesBlockedByPrivacySettings(viewer, targetPrivacy)) return false;
}
return true; return true;
} }

View File

@@ -3,7 +3,11 @@
ALTER TABLE profiles ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS height_in integer, ADD COLUMN IF NOT EXISTS height_in integer,
ADD COLUMN IF NOT EXISTS weight_lb integer, ADD COLUMN IF NOT EXISTS weight_lb integer,
ADD COLUMN IF NOT EXISTS body_type text; ADD COLUMN IF NOT EXISTS body_type text,
ADD COLUMN IF NOT EXISTS hide_from_anonymous boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS block_messages_from_anonymous boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS hide_from_no_photo boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS block_messages_from_no_photo boolean NOT NULL DEFAULT false;
CREATE TABLE IF NOT EXISTS hide_from_rules ( CREATE TABLE IF NOT EXISTS hide_from_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),