diff --git a/TODO.md b/TODO.md index 3b7b4f3..102f2cc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,7 @@ # TODO push offers -listen / notify - on offers - notify admins - +add last synced time to want contact options on landing page diff --git a/api/src/discogs.ts b/api/src/discogs.ts index 77e6002..5aa662b 100644 --- a/api/src/discogs.ts +++ b/api/src/discogs.ts @@ -48,7 +48,7 @@ export default function createDiscogs(log: Log, pg: Pg) { // @ts-ignore const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value); - log.info('want list additions', additions); + log.info('want list additions', { additions: additions.length }); return additions; } diff --git a/api/src/events.ts b/api/src/events.ts index 534656f..d3d901d 100644 --- a/api/src/events.ts +++ b/api/src/events.ts @@ -15,7 +15,8 @@ export enum Events { ListingsUpdated = 'ListingsUpdated', Connection = 'Connection', - OffersUpdated = 'OffersUpdated' + OfferUpdated = 'OfferUpdated', + WantUpdated = 'WantUpdated', } export default function createEvents(log: Log, pg: Pg, discogs, offers) { @@ -43,7 +44,7 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) { async function updateWantListings(wants: Want[]) { for (const want of wants) { const listings = await populateListings(want); - log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added'); + log.info({ listings: listings.length, ...wantDetails(want) }, 'listings found'); events.emit(Events.ListingsUpdated); } @@ -74,11 +75,18 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) { connections.set(id, rpc); }) - events.on(Events.OffersUpdated, async function onOffersUpdated(offer: Offer) { + events.on(Events.OfferUpdated, async function onOfferUpdated(offer: Offer) { const rpc = connections.get(offer.userId); if (!rpc) return null; - log.debug({ event: Events.OffersUpdated, rpc, offer }); - return rpc.sendUserOffers(); + log.debug({ event: Events.OfferUpdated, rpc, offer }); + return rpc.pushUserOffers(); + }); + + events.on(Events.WantUpdated, async function onWantUpdated(want: Want) { + const rpc = connections.get(want.userId); + if (!rpc) return null; + log.debug({ event: Events.WantUpdated, rpc, want }); + return rpc.pushUserWants(); }); return events; diff --git a/api/src/listen.notify.ts b/api/src/listen.notify.ts deleted file mode 100644 index 79663d0..0000000 --- a/api/src/listen.notify.ts +++ /dev/null @@ -1,20 +0,0 @@ -import createSubscriber from 'pg-listen' -import type Log from 'bunyan'; - -import { Events } from './events'; - -export default function createNotify(log: Log, events) { - const CHANNEL = 'events'; - const subscriber = createSubscriber({ connectionString: process.env.DATABASE_URL }); - - subscriber.notifications.on(CHANNEL, ({ table, action, record }) => { - if (table === 'offers') return events.emit(Events.OffersUpdated, record); - }) - - async function connect() { - await subscriber.connect(); - return subscriber.listenTo(CHANNEL); - } - - return { connect }; -} \ No newline at end of file diff --git a/api/src/notify.ts b/api/src/notify.ts index a353b2e..f6c9bdf 100644 --- a/api/src/notify.ts +++ b/api/src/notify.ts @@ -18,22 +18,26 @@ export default function createNotify(log: Log, pg: Pg, events) { async function handleOffers(payload) { const { action, id } = payload; - log.info({ channel: OFFERS_CHANNEL, action, id }); + log.debug({ channel: OFFERS_CHANNEL, action, id }); + if (action === 'DELETE') return null; + + const offer = await pg.offerFindId(id); + // events.emit(Events.OfferUpdated, offer); } async function handleUsers(payload) { const { action, id } = payload; - log.info({ channel: USERS_CHANNEL, action, id }); + log.debug({ channel: USERS_CHANNEL, action, id }); } async function handleListings(payload) { const { action, id } = payload; - log.info({ channel: LISTINGS_CHANNEL, action, id }); + log.debug({ channel: LISTINGS_CHANNEL, action, id }); } async function handleWants(payload) { const { action, id } = payload; - log.info({ channel: WANTS_CHANNEL, action, id }); + log.debug({ channel: WANTS_CHANNEL, action, id }); } subscriber.notifications.on(OFFERS_CHANNEL, handleOffers); diff --git a/api/src/rpc.ts b/api/src/rpc.ts index 7456ceb..a007f03 100644 --- a/api/src/rpc.ts +++ b/api/src/rpc.ts @@ -12,6 +12,76 @@ import { Status } from './offers'; import { isAdmin } from './utils'; import { Events } from './events'; +export type Rpc = ReturnType; + +export enum RpcMessageType { + Ping = 'Ping', + Pong = 'Pong', + + // points out user is anon + WhoDis = 'WhoDis', + + Bundles = 'Bundles', + Offers = 'Offers', + List = 'List', + Wants = 'Wants', + User = 'User', + + Error = 'Error', + + OfferSamples = 'OfferSamples', + OfferStatusSet = 'OfferStatusSet', + + AcpOfferStatusSet = 'AcpOfferStatusSet', + AcpOffers = 'AcpOffers', + AcpListingsSync = 'AcpListingsSync', + + WantRm = 'WantRm', +} + +type ErrorMessage = [RpcMessageType.Error, string]; +type PongMessage = [RpcMessageType.Pong, []]; +type PingMessage = [RpcMessageType.Ping, []]; +type WhoDisMessage = [RpcMessageType.WhoDis, []]; +type UserMessage = [RpcMessageType.User, User]; +type WantsMessage = [RpcMessageType.Wants, Want[]]; +type OffersMessage = [RpcMessageType.Offers, Offer[]]; +type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]]; +type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]]; + +type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]]; +type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]]; +type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []]; + +export type RpcMessage = + PingMessage | + PongMessage | + WhoDisMessage | + + UserMessage | + + WantsMessage | + + OffersMessage | + OfferSamplesMessage | + OfferStatusSetMessage | + + AcpOfferStatusSetMessage | + AcpOffersMessage | + AcpListingsSyncMessage | + + ErrorMessage; + + +type OfferStatusSetParams = { + id: UUID, + status: Status, +}; + +type WantRmParams = { + id: UUID, +}; + function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType { if (RpcMessageType[msgType]) { return true; @@ -134,11 +204,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers const userMsg: UserMessage = [RpcMessageType.User, user]; send(userMsg); - await sendUserOffers(); - - const wants = await pg.userWants(user); - const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants]; - send(wantsMsg); + await pushUserOffers(); + await pushUserWants(); if (isAdmin(user)) return adminConnection(); } @@ -151,10 +218,12 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers )) } - async function sendUserOffers() { - const userOffers = await offers.userOffers(user); - const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers]; - send(offersMsg); + async function pushUserOffers() { + send(await userOffers()); + } + + async function pushUserWants() { + send(await userWants()); } return { @@ -162,76 +231,7 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers send, onConnection, - sendUserOffers, + pushUserOffers, + pushUserWants, } } - -export type Rpc = ReturnType; - -export enum RpcMessageType { - Ping = 'Ping', - Pong = 'Pong', - - // points out user is anon - WhoDis = 'WhoDis', - - Bundles = 'Bundles', - Offers = 'Offers', - List = 'List', - Wants = 'Wants', - User = 'User', - - Error = 'Error', - - OfferSamples = 'OfferSamples', - OfferStatusSet = 'OfferStatusSet', - - AcpOfferStatusSet = 'AcpOfferStatusSet', - AcpOffers = 'AcpOffers', - AcpListingsSync = 'AcpListingsSync', - - WantRm = 'WantRm', -} - -type ErrorMessage = [RpcMessageType.Error, string]; -type PongMessage = [RpcMessageType.Pong, []]; -type PingMessage = [RpcMessageType.Ping, []]; -type WhoDisMessage = [RpcMessageType.WhoDis, []]; -type UserMessage = [RpcMessageType.User, User]; -type WantsMessage = [RpcMessageType.Wants, Want[]]; -type OffersMessage = [RpcMessageType.Offers, Offer[]]; -type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]]; -type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]]; - -type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]]; -type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]]; -type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []]; - -export type RpcMessage = - PingMessage | - PongMessage | - WhoDisMessage | - - UserMessage | - - WantsMessage | - - OffersMessage | - OfferSamplesMessage | - OfferStatusSetMessage | - - AcpOfferStatusSetMessage | - AcpOffersMessage | - AcpListingsSyncMessage | - - ErrorMessage; - - -type OfferStatusSetParams = { - id: UUID, - status: Status, -}; - -type WantRmParams = { - id: UUID, -}; diff --git a/client/src/components/want.tsx b/client/src/components/want.tsx index 503e916..aafde16 100644 --- a/client/src/components/want.tsx +++ b/client/src/components/want.tsx @@ -1,12 +1,10 @@ import { h } from 'preact'; -import { useState, useCallback } from 'preact/hooks'; +import { useState } from 'preact/hooks'; import { ExternalLink } from 'react-feather'; import { useSelector } from 'react-redux' import { State } from './../store' -import http from './../http'; - function Want(want) { const r = want.data; @@ -14,10 +12,13 @@ function Want(want) { const [confirm, setConfirm] = useState(false); function onClick() { - if (confirm) return ws.sendWantRm(want.id); + if (confirm) { + setConfirm(false); + return ws.sendWantRm(want.id); + } setTimeout(() => setConfirm(false), 2500); return setConfirm(true); - }; + } const text = confirm ? 'confirm' diff --git a/client/src/routes/list.tsx b/client/src/routes/list.tsx index d154df2..4122079 100644 --- a/client/src/routes/list.tsx +++ b/client/src/routes/list.tsx @@ -10,7 +10,6 @@ import Want from './../components/want'; const List = () => { const wants = useSelector((state: State) => state.wants); - const user = useSelector((state: State) => state.user); if (!wants) { return
loading...
; diff --git a/client/src/style/index.less b/client/src/style/index.less index 4141623..5baa384 100644 --- a/client/src/style/index.less +++ b/client/src/style/index.less @@ -378,19 +378,16 @@ a { } } -nav { - a { - font-size: var(--small-font-size); - margin-right: var(--spacing); - } -} - - header { display: grid; grid-template-columns: 1fr 1fr min-content; padding-bottom: var(--spacing); + a { + font-size: var(--small-font-size); + margin-right: var(--spacing); + } + a { white-space: nowrap; } diff --git a/client/src/toast.tsx b/client/src/toast.tsx index 81fe9f2..e2ad0a2 100644 --- a/client/src/toast.tsx +++ b/client/src/toast.tsx @@ -3,7 +3,7 @@ import toast from 'izitoast'; export function info(title, message) { toast.info({ title, - message, + message: message || '', position: 'bottomCenter', color: '#1f2128', titleColor: '#F4F1D0', diff --git a/client/src/ws.ts b/client/src/ws.ts index c48f2fd..0a238ee 100644 --- a/client/src/ws.ts +++ b/client/src/ws.ts @@ -1,5 +1,6 @@ -import { wantsState, wsState, userState, offersState, acpOffersState } from './store' +import debounce from 'lodash/debounce'; +import { wantsState, userState, offersState, acpOffersState } from './store' import * as toast from './toast'; const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`; @@ -116,6 +117,7 @@ export default function createSocket(dispatch) { // Connection opened function onOpen() { console.log('websocket connected', SOCKET_URL) + toast.info('connected', 'connected to server'); sendPing(); }