diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index 25e85ebc..3d6a2854 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -5,7 +5,6 @@ import {from, join, limit, orderBy, renderSql, select, where,} from 'shared/supa import {getCompatibleLovers} from 'api/compatible-lovers' import {intersection} from 'lodash' import {MAX_INT, MIN_INT} from "common/constants"; -import {Lover} from "common/love/lover"; export type profileQueryType = { limit?: number | undefined, @@ -22,6 +21,7 @@ export type profileQueryType = { is_smoker?: boolean | undefined, geodbCityIds?: String[] | undefined, compatibleWithUserId?: string | undefined, + skipId?: string | undefined, orderBy?: string | undefined, } @@ -43,6 +43,7 @@ export const loadProfiles = async (props: profileQueryType) => { geodbCityIds, compatibleWithUserId, orderBy: orderByParam, + skipId, } = props const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : [] @@ -50,7 +51,10 @@ export const loadProfiles = async (props: profileQueryType) => { // compatibility. TODO: do this in sql if (orderByParam === 'compatibility_score') { - if (!compatibleWithUserId) return {status: 'fail', lovers: []} + if (!compatibleWithUserId) { + console.error('Incompatible with user ID') + throw Error('Incompatible with user ID') + } const {compatibleLovers} = await getCompatibleLovers(compatibleWithUserId) const lovers = compatibleLovers.filter( @@ -72,6 +76,7 @@ export const loadProfiles = async (props: profileQueryType) => { (has_kids == 0 && !l.has_kids) || (l.has_kids && l.has_kids > 0)) && (!is_smoker || l.is_smoker === is_smoker) && + (l.id.toString() != skipId) && (!geodbCityIds || (l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) ) @@ -135,6 +140,8 @@ export const loadProfiles = async (props: profileQueryType) => { geodbCityIds?.length && where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}), + skipId && where(`user_id != $(skipId)`, {skipId}), + orderBy(`${orderByParam} desc`), after && where( @@ -151,6 +158,10 @@ export const loadProfiles = async (props: profileQueryType) => { } export const getProfiles: APIHandler<'get-profiles'> = async (props, _auth) => { - const lovers = await loadProfiles(props) - return {status: 'success', lovers: lovers as Lover[]} + try { + const lovers = await loadProfiles(props) + return {status: 'success', lovers: lovers} + } catch { + return {status: 'fail', lovers: []} + } } diff --git a/backend/api/src/send-search-notifications.ts b/backend/api/src/send-search-notifications.ts index af7e0012..7c6d8c62 100644 --- a/backend/api/src/send-search-notifications.ts +++ b/backend/api/src/send-search-notifications.ts @@ -1,12 +1,23 @@ import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from "shared/supabase/init"; -import {convertRow} from "shared/love/supabase"; -import {from, join, renderSql, select, where} from "shared/supabase/sql-builder"; +import {from, renderSql, select} from "shared/supabase/sql-builder"; +import {loadProfiles, profileQueryType} from "api/get-profiles"; +import {Row} from "common/supabase/utils"; +import {sendSearchAlertsEmail} from "email/functions/helpers"; +import {MatchesByUserType} from "common/love/bookmarked_searches"; +import {keyBy} from "lodash"; export function convertSearchRow(row: any): any { return row } + +export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => { + for (const [_, value] of Object.entries(matches)) { + await sendSearchAlertsEmail(value.user, value.privateUser, value.matches) + } +} + export const sendSearchNotifications: APIHandler<'send-search-notifications'> = async (_, auth) => { const pg = createSupabaseDirectClient() @@ -14,20 +25,58 @@ export const sendSearchNotifications: APIHandler<'send-search-notifications'> = select('bookmarked_searches.*'), from('bookmarked_searches'), ) - const searches = pg.map(search_query, [], convertSearchRow) + const searches = await pg.map(search_query, [], convertSearchRow) as Row<'bookmarked_searches'>[] + console.log(`Running ${searches.length} bookmarked searches`) - const query = renderSql( - select('lovers.*, name, username, users.data as user'), - from('lovers'), - join('users on users.id = lovers.user_id'), - where('looking_for_matches = true'), - where( - `(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)` + const _users = await pg.map( + renderSql( + select('users.*'), + from('users'), ), - where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`), - ) + [], + convertSearchRow + ) as Row<'users'>[] + const users = keyBy(_users, 'id') + console.log('users', users) - const profiles = await pg.map(query, [], convertRow) + const _privateUsers = await pg.map( + renderSql( + select('private_users.*'), + from('private_users'), + ), + [], + convertSearchRow + ) as Row<'private_users'>[] + const privateUsers = keyBy(_privateUsers, 'id') + console.log('privateUsers', privateUsers) - return {status: 'success', lovers: profiles} + const matches: MatchesByUserType = {} + + for (const row of searches) { + if (typeof row.search_filters !== 'object') continue; + const props = {...row.search_filters, skipId: row.creator_id} + const profiles = await loadProfiles(props as profileQueryType) + console.log(profiles.map((item: any) => item.name)) + if (!profiles.length) continue + if (!(row.creator_id in matches)) { + if (!privateUsers[row.creator_id]) continue + matches[row.creator_id] = { + user: users[row.creator_id], + privateUser: privateUsers[row.creator_id]['data'], + matches: [], + } + } + matches[row.creator_id].matches.push({ + id: row.creator_id, + description: {filters: row.search_filters, location: row.location}, + matches: [profiles.map((item: any) => ({ + name: item.name, + username: item.username, + }))], + }) + } + console.log(JSON.stringify(matches, null, 2)) + await notifyBookmarkedSearch(matches) + + return {status: 'success'} } \ No newline at end of file diff --git a/backend/email/emails/functions/helpers.tsx b/backend/email/emails/functions/helpers.tsx index e8b2102f..0a1c39de 100644 --- a/backend/email/emails/functions/helpers.tsx +++ b/backend/email/emails/functions/helpers.tsx @@ -1,12 +1,14 @@ -import { PrivateUser, User } from 'common/user' -import { getNotificationDestinationsForUser } from 'common/user-notification-preferences' -import { sendEmail } from './send-email' -import { NewMatchEmail } from '../new-match' -import { NewMessageEmail } from '../new-message' -import { NewEndorsementEmail } from '../new-endorsement' -import { Test } from '../test' -import { getLover } from 'shared/love/supabase' +import {PrivateUser, User} from 'common/user' +import {getNotificationDestinationsForUser} from 'common/user-notification-preferences' +import {sendEmail} from './send-email' +import {NewMatchEmail} from '../new-match' +import {NewMessageEmail} from '../new-message' +import {NewEndorsementEmail} from '../new-endorsement' +import {Test} from '../test' +import {getLover} from 'shared/love/supabase' import {renderToStaticMarkup} from "react-dom/server"; +import {MatchesType} from "common/love/bookmarked_searches"; +import NewSearchAlertsEmail from "email/new-search_alerts"; const from = 'Compass ' @@ -14,7 +16,7 @@ export const sendNewMatchEmail = async ( privateUser: PrivateUser, matchedWithUser: User ) => { - const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser( + const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, 'new_match' ) @@ -44,7 +46,7 @@ export const sendNewMessageEmail = async ( toUser: User, channelId: number ) => { - const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser( + const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, 'new_message' ) @@ -57,22 +59,6 @@ export const sendNewMessageEmail = async ( return } - console.log({ - from, - subject: `${fromUser.name} sent you a message!`, - to: privateUser.email, - html: renderToStaticMarkup( - - ), - }) - return await sendEmail({ from, subject: `${fromUser.name} sent you a message!`, @@ -90,13 +76,40 @@ export const sendNewMessageEmail = async ( }) } +export const sendSearchAlertsEmail = async ( + toUser: User, + privateUser: PrivateUser, + matches: MatchesType[], +) => { + const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( + privateUser, + 'new_search_alerts' + ) + const email = privateUser.email; + if (!email || !sendToEmail) return + + return await sendEmail({ + from, + subject: `Some people recently matched your bookmarked searches!`, + to: email, + html: renderToStaticMarkup( + + ), + }) +} + export const sendNewEndorsementEmail = async ( privateUser: PrivateUser, fromUser: User, onUser: User, text: string ) => { - const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser( + const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, 'new_endorsement' ) @@ -123,6 +136,6 @@ export const sendTestEmail = async (toEmail: string) => { from, subject: 'Test email from Compass', to: toEmail, - html: renderToStaticMarkup(), + html: renderToStaticMarkup(), }) } diff --git a/backend/email/emails/new-search_alerts.tsx b/backend/email/emails/new-search_alerts.tsx new file mode 100644 index 00000000..3e681f99 --- /dev/null +++ b/backend/email/emails/new-search_alerts.tsx @@ -0,0 +1,129 @@ +import { + Body, + Button, + Container, + Head, + Html, + Img, + Link, + Preview, + Section, + Text, +} from '@react-email/components' +import {type User} from 'common/user' +import {type LoverRow} from 'common/love/lover' +import { + jamesLover, + jamesUser, + sinclairLover, + sinclairUser, +} from './functions/mock' +import {DOMAIN} from 'common/envs/constants' +import {getLoveOgImageUrl} from 'common/love/og-image' +import {button, container, content, Footer, imageContainer, main, paragraph, profileImage} from "email/utils"; +import {sendSearchAlertsEmail} from "email/functions/helpers"; +import {MatchesType} from "common/love/bookmarked_searches"; +import {formatFilters, locationType} from "common/searches" +import {FilterFields} from "common/filters"; + +interface NewMessageEmailProps { + toUser: User + matches: MatchesType[] + unsubscribeUrl: string + email?: string +} + +export const NewSearchAlertsEmail = ({ + toUser, + unsubscribeUrl, + matches, + email, + }: NewMessageEmailProps) => { + const name = toUser.name.split(' ')[0] + + return ( + + + Some people recently matched your bookmarked searches! + + +
+ Hi {name}, + The following people matched your bookmarked searches in the past 24h: + +
    + {(matches || []).map((match) => ( +
  1. + {formatFilters(match.description.filters as Partial, match.description.location as locationType)} + {match.matches.map(p => p.toString()).join(', ')} +
  2. + ))} +
+ +
+
+