Seed match likes/chats for new joiners and split likes received tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-04 06:30:25 -05:00
parent 681879b28e
commit 5525d04744
7 changed files with 332 additions and 18 deletions

View File

@ -619,6 +619,72 @@ DISCUSSION_REPLIES = {
"like_count": 15,
},
],
"health-insurance": [
{
"id": "r1",
"author": "Sofia",
"author_emoji": "🏥",
"content": "30 岁以下 SafetyWing 够用;有慢性病建议加本地门诊险。",
"created_at": "2026-08-28",
"like_count": 10,
},
{
"id": "r2",
"author": "Ken",
"author_emoji": "🇯🇵",
"content": "在日本长期停留我叠了本地国民健康保险,报销更稳。",
"created_at": "2026-08-28",
"like_count": 7,
},
],
"meetup-feedback": [
{
"id": "r1",
"author": "Lina",
"author_emoji": "📷",
"content": "大理 + 清迈希望能固定每月一场创作局。",
"created_at": "2026-08-29",
"like_count": 18,
},
{
"id": "r2",
"author": "Omar",
"author_emoji": "🏙️",
"content": "迪拜适合枢纽月见面,建议放周末早午餐。",
"created_at": "2026-08-29",
"like_count": 9,
},
],
"berlin-winter": [
{
"id": "r1",
"author": "Devon",
"author_emoji": "🖥️",
"content": "冬天把联合办公当社交主场,再加一周一次室内攀岩/跑步局。",
"created_at": "2026-09-02",
"like_count": 11,
},
],
"dubai-summer": [
{
"id": "r1",
"author": "Nina",
"author_emoji": "🍷",
"content": "夏天基本只在空调空间办公,预算里把交通和水电预留高一点。",
"created_at": "2026-09-04",
"like_count": 6,
},
],
"seoul-visa": [
{
"id": "r1",
"author": "Jin",
"author_emoji": "🇰🇷",
"content": "最近材料重点是收入证明和保险;审批大约 3–6 周,建议提前办。",
"created_at": "2026-09-05",
"like_count": 8,
},
],
}
NOMAD_ROUTES = [

View File

@ -568,6 +568,9 @@ _JSON_PATH = Path(__file__).resolve().parents[1] / "data" / "community_store.jso
_j: dict[str, Any] = {}
_SEED_VERSION = 3
def _jload() -> None:
global _j
if _j:
@ -580,19 +583,27 @@ def _jload() -> None:
needs_seed = not _j.get("seeded") or not (
(_j.get("meetups") or []) and (_j.get("discussions") or []) and (_j.get("gigs") or [])
)
if needs_seed:
version_bump = int(_j.get("seed_version") or 0) < _SEED_VERSION
if needs_seed or version_bump:
keep_notif = _j.get("notifications") or {}
keep_rsvp = _j.get("rsvps") or {}
keep_apps = _j.get("gig_apps") or []
keep_feedback = _j.get("feedback") or []
keep_views = _j.get("views") or {}
keep_likes = _j.get("discussion_likes") or {}
_j.update({
"meetups": deepcopy(community_data.MEETUPS),
"discussions": deepcopy(community_data.DISCUSSIONS),
"replies": deepcopy(community_data.DISCUSSION_REPLIES),
"gigs": deepcopy(getattr(community_data, "GIGS", [])),
"discussion_likes": _j.get("discussion_likes") or {},
"rsvps": _j.get("rsvps") or {},
"gig_apps": _j.get("gig_apps") or [],
"notifications": _j.get("notifications") or {},
"feedback": _j.get("feedback") or [],
"views": _j.get("views") or {},
"discussion_likes": keep_likes,
"rsvps": keep_rsvp,
"gig_apps": keep_apps,
"notifications": keep_notif,
"feedback": keep_feedback,
"views": keep_views,
"seeded": True,
"seed_version": _SEED_VERSION,
})
_jsave()

View File

@ -140,7 +140,7 @@ def get_or_create_profile(user_id: str, name: str, **extra: Any) -> dict:
def join_member(user_id: str, name: str, payload: dict) -> dict:
return get_or_create_profile(
profile = get_or_create_profile(
user_id,
name,
location=payload.get("city", "全球"),
@ -151,6 +151,11 @@ def join_member(user_id: str, name: str, payload: dict) -> dict:
lookingFor=payload.get("lookingFor", ["friends", "explore"]),
photo=payload.get("photo", "🧑‍💻"),
)
try:
seed_demo_social_activity(user_id)
except Exception:
pass
return profile
def get_public_profile(user_id: str) -> dict | None:
@ -670,6 +675,178 @@ def mark_order_paid(order_id: str) -> dict | None:
return order
def seed_demo_social_activity(user_id: str) -> None:
"""Give a real user a few incoming likes + one chat so Connect feels alive."""
if not user_id:
return
my_pid = f"user-{user_id}"
peers = [p for p in CANDIDATE_PROFILES if p.get("userId")][:4]
if not peers:
return
buddy = peers[0]
buddy_uid = buddy["userId"]
already = False
if use_pb():
already = bool(safe_first("swipes", filter=f"userId={q(buddy_uid)} && profileId={q(my_pid)}"))
else:
_jload()
already = bool((_j.get("demo_seeded") or {}).get(user_id)) or any(
s.get("userId") == buddy_uid and s.get("profileId") == my_pid for s in (_j.get("swipes") or [])
)
if already:
return
# Incoming likes from candidates toward this user
for p in peers[:3]:
peer_uid = p["userId"]
if use_pb():
if not safe_first("swipes", filter=f"userId={q(peer_uid)} && profileId={q(my_pid)}"):
safe_create(
"swipes",
{
"userId": peer_uid,
"profileId": my_pid,
"action": "like",
"intent": "friends",
"swipeDate": _today_key(),
},
)
else:
_jload()
swipes = _j.setdefault("swipes", [])
if not any(s.get("userId") == peer_uid and s.get("profileId") == my_pid for s in swipes):
swipes.append(
{
"id": secrets.token_hex(6),
"userId": peer_uid,
"profileId": my_pid,
"action": "like",
"intent": "friends",
"date": _today_key(),
"createdAt": _now_iso(),
}
)
# Mutual match + messages with first peer
buddy_pid = buddy["id"]
if use_pb():
if not safe_first("swipes", filter=f"userId={q(user_id)} && profileId={q(buddy_pid)}"):
safe_create(
"swipes",
{
"userId": user_id,
"profileId": buddy_pid,
"action": "like",
"intent": "friends",
"swipeDate": _today_key(),
},
)
else:
_jload()
swipes = _j.setdefault("swipes", [])
if not any(s.get("userId") == user_id and s.get("profileId") == buddy_pid for s in swipes):
swipes.append(
{
"id": secrets.token_hex(6),
"userId": user_id,
"profileId": buddy_pid,
"action": "like",
"intent": "friends",
"date": _today_key(),
"createdAt": _now_iso(),
}
)
match = _ensure_match(user_id, buddy, "friends")
conv_id = (match or {}).get("conversationId") or ""
if not conv_id:
conv = _ensure_conversation(user_id, buddy_uid, intent="friends", match=True)
conv_id = conv.get("id", "")
hello = f"嗨!我在{buddy.get('location', '旅途中')},看到你也在用 nomadro,交个朋友呀 👋"
reply = "你好!我也刚到,有机会一起去联合办公或 meetup~"
if conv_id:
if use_pb():
msgs = safe_list("messages", filter=f"conversationId={q(conv_id)}")
if not msgs:
safe_create("messages", {"conversationId": conv_id, "senderId": buddy_uid, "body": hello})
safe_create("messages", {"conversationId": conv_id, "senderId": user_id, "body": reply})
safe_update(
"conversations",
conv_id,
{
"lastMessageAt": _now_iso(),
"lastMessagePreview": reply[:80],
"readState": {user_id: _now_iso(), buddy_uid: _now_iso()},
},
)
else:
_jload()
msgs = [m for m in (_j.get("messages") or []) if m.get("conversationId") == conv_id]
if not msgs:
_j.setdefault("messages", []).extend(
[
{
"id": secrets.token_hex(8),
"conversationId": conv_id,
"senderId": buddy_uid,
"body": hello,
"createdAt": _now_iso(),
},
{
"id": secrets.token_hex(8),
"conversationId": conv_id,
"senderId": user_id,
"body": reply,
"createdAt": _now_iso(),
},
]
)
for c in _j.get("conversations") or []:
if c.get("id") == conv_id:
c["lastMessageAt"] = _now_iso()
c["lastMessagePreview"] = reply[:80]
break
try:
from app.services import community_store
from app.services.notify import notify_user
notify_user(
user_id,
"有人喜欢了你",
f"{buddy.get('name', '游民')} 等人对你感兴趣,去看看吧",
"/dating/likes",
"match",
)
if conv_id:
notify_user(
user_id,
"新消息",
f"{buddy.get('name', '游民')}: {hello[:40]}",
f"/chat/{conv_id}",
"chat",
)
community_store.add_notification(
user_id,
"欢迎来到匹配",
"左滑跳过、右滑喜欢、上滑超级喜欢。匹配成功可私信。",
"/dating",
"system",
)
except Exception:
pass
if not use_pb():
_jload()
_j.setdefault("demo_seeded", {})[user_id] = True
_jsave()
else:
pass
# ── JSON fallback ──────────────────────────────────────────────────────
def _jload() -> dict:

View File

@ -8,6 +8,14 @@ export const metadata: Metadata = {
};
const LOGS = [
{
date: "2026-09-04",
tag: "匹配滑动 · 喜欢我的",
items: [
"匹配卡片支持左右/上滑手势与方向键;喜欢页区分「喜欢我的 / 我喜欢的」",
"加入匹配时自动注入演示私信与来赞,讨论回复补全",
],
},
{
date: "2026-09-04",
tag: "匹配页修复",

View File

@ -9,11 +9,15 @@ import type { MatchProfile } from "@/lib/types";
import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings";
type Tab = "liked" | "received";
export default function DatingLikesClient() {
const { user, token } = useAuth();
const { t } = useI18n();
const rings = useRingSteps();
const [likes, setLikes] = useState<MatchProfile[]>([]);
const [tab, setTab] = useState<Tab>("received");
const [liked, setLiked] = useState<MatchProfile[]>([]);
const [received, setReceived] = useState<MatchProfile[]>([]);
const [loading, setLoading] = useState(Boolean(token));
useEffect(() => {
@ -22,13 +26,20 @@ export default function DatingLikesClient() {
return;
}
setLoading(true);
api
.getMatchLikes(token)
.then(setLikes)
.catch(() => setLikes([]))
Promise.all([
api.getMatchLikes(token).catch(() => [] as MatchProfile[]),
api.getMatchLikesReceived(token).catch(() => [] as MatchProfile[]),
])
.then(([mine, incoming]) => {
setLiked(mine);
setReceived(incoming);
if (incoming.length === 0 && mine.length > 0) setTab("liked");
})
.finally(() => setLoading(false));
}, [token]);
const list = tab === "liked" ? liked : received;
if (!user) {
return (
<div className="dating-page">
@ -49,16 +60,40 @@ export default function DatingLikesClient() {
<div className="container">
<nav className="detail-nav">
<Link href="/dating">{t.dating.backSwipe}</Link>
<span> · </span>
<Link href="/chat">{t.nav.chat}</Link>
</nav>
<div className="section-header reveal">
<span className="section-tag">{t.dating.tag}</span>
<h1>{t.dating.likesTitle}</h1>
<p>{t.dating.likesSubtitle}</p>
<p>{tab === "liked" ? t.dating.likesSubtitle : t.dating.likesReceivedSubtitle}</p>
</div>
<div className="dating-intents reveal">
<button
type="button"
className={`filter-btn${tab === "received" ? " active" : ""}`}
onClick={() => setTab("received")}
>
{t.dating.likesReceivedTab}
{received.length ? ` (${received.length})` : ""}
</button>
<button
type="button"
className={`filter-btn${tab === "liked" ? " active" : ""}`}
onClick={() => setTab("liked")}
>
{t.dating.likesMineTab}
{liked.length ? ` (${liked.length})` : ""}
</button>
</div>
{loading && <p className="dest-empty">{t.common.loading}</p>}
{!loading && likes.length === 0 && (
{!loading && list.length === 0 && (
<div className="notif-empty reveal">
<p className="dest-empty">{t.dating.likesEmpty}</p>
<p className="dest-empty">
{tab === "liked" ? t.dating.likesEmpty : t.dating.likesReceivedEmpty}
</p>
<div className="dating-empty-actions">
<Link href="/dating" className="btn btn-primary btn-sm">
{t.dating.goSwipe}
@ -71,11 +106,11 @@ export default function DatingLikesClient() {
)}
<div className="dating-likes-grid">
{!loading &&
likes.map((p) => {
list.map((p) => {
const photo = p.photo || "";
const href = p.userId ? `/members/${p.userId}` : "/dating";
return (
<Link key={p.id} href={href} className="dating-like-card reveal">
<Link key={p.id || p.userId || p.name} href={href} className="dating-like-card reveal">
<div className="dating-like-photo">
{photo.startsWith("http") ? (
// eslint-disable-next-line @next/next/no-img-element
@ -86,6 +121,9 @@ export default function DatingLikesClient() {
</div>
<strong>{p.name}</strong>
<p className="dating-meta">{p.location || t.dating.globalNomad}</p>
{tab === "received" && (
<span className="meetup-tag">{t.dating.likesYou}</span>
)}
</Link>
);
})}

View File

@ -351,6 +351,10 @@ export const api = {
getMemberProfile: (userId: string) => fetchAPI<MemberProfile>(`/members/${userId}`),
getMatchLikes: (token: string) =>
fetchAPI<MatchProfile[]>("/social/matches/likes", { headers: { Authorization: `Bearer ${token}` } }),
getMatchLikesReceived: (token: string) =>
fetchAPI<MatchProfile[]>("/social/matches/likes/received", {
headers: { Authorization: `Bearer ${token}` },
}),
undoSwipe: (token: string) =>
fetchAPI<{ ok: boolean }>("/social/matches/swipes/undo", {
method: "POST",

View File

@ -452,6 +452,11 @@ export const zh = {
swipeLeft: "跳过",
swipeRight: "喜欢",
swipeUp: "超级喜欢",
likesReceivedTab: "喜欢我的",
likesMineTab: "我喜欢的",
likesReceivedSubtitle: "对你右滑的游民会出现在这里",
likesReceivedEmpty: "还没有人喜欢你,先去完善资料并滑动匹配",
likesYou: "喜欢了你",
},
chat: {
tag: "✉️ MESSAGES",
@ -1712,6 +1717,11 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
swipeLeft: "Pass",
swipeRight: "Like",
swipeUp: "Super like",
likesReceivedTab: "Liked you",
likesMineTab: "You liked",
likesReceivedSubtitle: "People who swiped right on you",
likesReceivedEmpty: "No incoming likes yet — keep your profile fresh and swipe",
likesYou: "Liked you",
},
chat: {
tag: "✉️ MESSAGES",