Add anonymous and no-photo privacy toggles for visibility and DMs
This commit is contained in:
28
deploy/check-deploy-err.py
Normal file
28
deploy/check-deploy-err.py
Normal 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
43
deploy/redeploy-poll.py
Normal 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()
|
||||
@@ -28,6 +28,7 @@ import { useAccess, useFeedPreferences, useAudienceView } from "@/components/Pro
|
||||
import { usePrivacy } from "@/contexts/PrivacyContext";
|
||||
import HideFromTraitsPanel from "@/components/HideFromTraitsPanel";
|
||||
import BlockListPanel from "@/components/BlockListPanel";
|
||||
import PrivacyAudienceToggles from "@/components/PrivacyAudienceToggles";
|
||||
import { BODY_TYPE_OPTIONS } from "@/lib/privacy-rules";
|
||||
import {
|
||||
US_STATES,
|
||||
@@ -51,6 +52,8 @@ export default function ProfilePage() {
|
||||
unblockProfile,
|
||||
unblockByUsername,
|
||||
unblockAll,
|
||||
privacySettings,
|
||||
setPrivacySettings,
|
||||
} = usePrivacy();
|
||||
const [authOpen, setAuthOpen] = useState(false);
|
||||
const [idModalOpen, setIdModalOpen] = useState(false);
|
||||
@@ -277,6 +280,11 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PrivacyAudienceToggles
|
||||
settings={privacySettings}
|
||||
onChange={setPrivacySettings}
|
||||
/>
|
||||
|
||||
<HideFromTraitsPanel
|
||||
rules={hideRules}
|
||||
onAdd={addHideRule}
|
||||
|
||||
@@ -11,6 +11,13 @@ import { CannedMessagesPanel } from "@/components/CannedMessagesPanel";
|
||||
import { LocalGuidePanel } from "@/components/LocalGuidePanel";
|
||||
import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner";
|
||||
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 {
|
||||
id: string;
|
||||
@@ -72,6 +79,8 @@ interface ChatPanelProps {
|
||||
|
||||
export function ChatPanel({ dmUserId }: ChatPanelProps) {
|
||||
const { capabilities } = useAccess();
|
||||
const { user, profile: authProfile } = useAuth();
|
||||
const { blockList } = usePrivacy();
|
||||
const [cityMessages, setCityMessages] = useState<Message[]>(MOCK_CITY_MESSAGES);
|
||||
const [dmMessages, setDmMessages] = useState<Message[]>(MOCK_DM_MESSAGES);
|
||||
const [input, setInput] = useState("");
|
||||
@@ -105,7 +114,29 @@ export function ChatPanel({ dmUserId }: ChatPanelProps) {
|
||||
};
|
||||
|
||||
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 (
|
||||
<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">
|
||||
<div className="px-3 sm:px-4 py-2 border-b border-border">
|
||||
<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 className="text-xs text-muted-foreground">
|
||||
Use quick replies for address, directions & hosting notes
|
||||
</p>
|
||||
</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} />
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { GuestUpgradeBanner } from "@/components/GuestUpgradeBanner";
|
||||
import { useAccess } from "@/components/Providers";
|
||||
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 { formatStatsLine, formatDistance } from "@/lib/profile-stats";
|
||||
import { timeAgo } from "@/lib/utils";
|
||||
@@ -86,7 +91,8 @@ export function CruiserProfileView({
|
||||
vanillaMode,
|
||||
}: CruiserProfileViewProps) {
|
||||
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 [blockMenuOpen, setBlockMenuOpen] = useState(false);
|
||||
const [activePhoto, setActivePhoto] = useState(0);
|
||||
@@ -123,9 +129,25 @@ export function CruiserProfileView({
|
||||
const shouldBlurPhoto = (explicit: boolean) =>
|
||||
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 = () => {
|
||||
if (!capabilities.canSendMessages) return;
|
||||
if (isBlocked(profile.id)) return;
|
||||
if (!canMessage) return;
|
||||
onChat(profile);
|
||||
};
|
||||
|
||||
@@ -464,8 +486,14 @@ export function CruiserProfileView({
|
||||
size="icon"
|
||||
className="rounded-full h-11 w-11 shrink-0"
|
||||
onClick={handleChat}
|
||||
disabled={!capabilities.canSendMessages}
|
||||
title={capabilities.canSendMessages ? "Chat" : "Sign up to chat"}
|
||||
disabled={!capabilities.canSendMessages || !canMessage || isBlocked(profile.id)}
|
||||
title={
|
||||
!capabilities.canSendMessages
|
||||
? "Sign up to chat"
|
||||
: !canMessage && messageBlockedNote
|
||||
? messageBlockedNote
|
||||
: "Chat"
|
||||
}
|
||||
>
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
@@ -20,7 +20,9 @@ import { profileMatchesAudienceView } from "@/lib/audience";
|
||||
import { MOCK_PROFILES, MOCK_SPOTS } from "@/lib/mock-data";
|
||||
import { useAuth } from "@/lib/auth/context";
|
||||
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 { cn } from "@/lib/utils";
|
||||
|
||||
@@ -183,10 +185,22 @@ export function CruisingMap({ vanillaMode, onActiveCountChange }: MapProps) {
|
||||
onActiveCountChange?.(filteredProfiles.filter((p) => p.status !== "offline").length);
|
||||
}, [filteredProfiles, onActiveCountChange]);
|
||||
|
||||
const handleChat = useCallback((profile: Profile) => {
|
||||
if (!capabilities.canSendMessages) return;
|
||||
window.location.href = `/chat?user=${profile.id}`;
|
||||
}, [capabilities.canSendMessages]);
|
||||
const handleChat = useCallback(
|
||||
(profile: Profile) => {
|
||||
if (!capabilities.canSendMessages) return;
|
||||
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) => {
|
||||
setFilters((f) => ({
|
||||
|
||||
96
src/components/PrivacyAudienceToggles.tsx
Normal file
96
src/components/PrivacyAudienceToggles.tsx
Normal 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 "Browse anonymously" 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're not in this group.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,10 +25,18 @@ import {
|
||||
type BlockEntry,
|
||||
} from '@/lib/block-list';
|
||||
import type { Profile } from '@/types';
|
||||
import {
|
||||
DEFAULT_PRIVACY_SETTINGS,
|
||||
loadPrivacySettings,
|
||||
savePrivacySettings,
|
||||
type PrivacySettings,
|
||||
} from '@/lib/privacy-settings';
|
||||
|
||||
interface PrivacyContextValue {
|
||||
hideRules: HideFromRule[];
|
||||
blockList: BlockEntry[];
|
||||
privacySettings: PrivacySettings;
|
||||
setPrivacySettings: (settings: PrivacySettings) => void;
|
||||
addHideRule: (rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => void;
|
||||
removeHideRule: (id: string) => void;
|
||||
blockProfile: (profile: Profile) => void;
|
||||
@@ -47,10 +55,14 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const [hideRules, setHideRules] = useState<HideFromRule[]>([]);
|
||||
const [blockList, setBlockList] = useState<BlockEntry[]>([]);
|
||||
const [privacySettings, setPrivacySettingsState] = useState<PrivacySettings>(
|
||||
DEFAULT_PRIVACY_SETTINGS
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setHideRules(loadHideFromRules(userId));
|
||||
setBlockList(loadBlockList(userId));
|
||||
setPrivacySettingsState(loadPrivacySettings(userId));
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,14 +72,25 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
const onHide = () => refresh();
|
||||
const onBlock = () => refresh();
|
||||
const onSettings = () => refresh();
|
||||
window.addEventListener('eg-hide-rules', onHide);
|
||||
window.addEventListener('eg-block-list', onBlock);
|
||||
window.addEventListener('eg-privacy-settings', onSettings);
|
||||
return () => {
|
||||
window.removeEventListener('eg-hide-rules', onHide);
|
||||
window.removeEventListener('eg-block-list', onBlock);
|
||||
window.removeEventListener('eg-privacy-settings', onSettings);
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const setPrivacySettings = useCallback(
|
||||
(settings: PrivacySettings) => {
|
||||
savePrivacySettings(settings, userId);
|
||||
setPrivacySettingsState(settings);
|
||||
},
|
||||
[userId]
|
||||
);
|
||||
|
||||
const addHideRule = useCallback(
|
||||
(rule: Omit<HideFromRule, 'id' | 'created_at' | 'label'>) => {
|
||||
const next = addHideFromRule(rule, userId);
|
||||
@@ -122,6 +145,8 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
|
||||
() => ({
|
||||
hideRules,
|
||||
blockList,
|
||||
privacySettings,
|
||||
setPrivacySettings,
|
||||
addHideRule,
|
||||
removeHideRule,
|
||||
blockProfile,
|
||||
@@ -134,6 +159,8 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
|
||||
[
|
||||
hideRules,
|
||||
blockList,
|
||||
privacySettings,
|
||||
setPrivacySettings,
|
||||
addHideRule,
|
||||
removeHideRule,
|
||||
blockProfile,
|
||||
|
||||
91
src/lib/privacy-settings.ts
Normal file
91
src/lib/privacy-settings.ts
Normal 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;
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import type { BlockEntry } from "@/lib/block-list";
|
||||
import { isBlocked, loadBlockList } from "@/lib/block-list";
|
||||
import type { HideFromRule } from "@/lib/privacy-rules";
|
||||
import { loadHideFromRules, viewerHiddenByRules } from "@/lib/privacy-rules";
|
||||
import {
|
||||
loadPrivacySettings,
|
||||
viewerHiddenByPrivacySettings,
|
||||
viewerMessagesBlockedByPrivacySettings,
|
||||
} from "@/lib/privacy-settings";
|
||||
|
||||
export interface VisibilityContext {
|
||||
/** The browsing user's profile (or guest stand-in). */
|
||||
@@ -36,6 +41,28 @@ export function canViewerSeeTarget(
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS height_in 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 (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
Reference in New Issue
Block a user