Compare commits
8 Commits
9e6675b8d2
...
d41b2e14f1
Author | SHA1 | Date | |
---|---|---|---|
|
d41b2e14f1 | ||
|
15c6843d80 | ||
|
ecedbd1527 | ||
|
fbd3f8efed | ||
|
fa5adac297 | ||
|
01de4e7ba7 | ||
|
d7ecbbae3a | ||
|
0552736dc7 |
31
.github/pull_request_template.md
vendored
31
.github/pull_request_template.md
vendored
@ -1,58 +1,37 @@
|
||||
## Description
|
||||
|
||||
<!--
|
||||
|
||||
A clear and concise description of what you changed and why.
|
||||
|
||||
Bullet points can be enough.
|
||||
|
||||
You can use the following PRs as inspiration:
|
||||
- https://github.com/stackernews/stacker.news/pull/227 (feature)
|
||||
- https://github.com/stackernews/stacker.news/pull/915 (feature)
|
||||
- https://github.com/stackernews/stacker.news/pull/871 (fix)
|
||||
- <your PR could be here>
|
||||
|
||||
Don't forget to mention which tickets this closes (if any).
|
||||
Use following syntax to close them automatically on merge: closes #<NUMBER>
|
||||
|
||||
Use following syntax to close them automatically on merge: closes #<number>
|
||||
-->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!--
|
||||
|
||||
If your changes are user facing, please add screenshots of the new UI.
|
||||
|
||||
You can also create a video to showcase your changes (useful to show UX).
|
||||
|
||||
-->
|
||||
|
||||
## Additional Context
|
||||
|
||||
<!--
|
||||
|
||||
You can mention here anything that you think is relevant for this PR. Some examples:
|
||||
|
||||
* You encountered something that you didn't understand while working on this PR
|
||||
* You were not sure about something you did but did not find a better way
|
||||
* You initially had a different approach but went with a different approach for some reason
|
||||
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Examples for backwards incompatible changes:
|
||||
- dropping database columns
|
||||
- changing GraphQL type definitions to make a field mandatory -->
|
||||
- [ ] Are your changes backwards compatible?
|
||||
|
||||
<!-- If your PR is not ready for review yet, please mark your PR as a draft.
|
||||
If changes were requested, request a new review when you incorporated the feedback. -->
|
||||
<!--
|
||||
If your PR is not ready for review yet, please mark your PR as a draft.
|
||||
If changes were requested, request a new review when you incorporated the feedback.
|
||||
-->
|
||||
- [ ] Did you QA this? Could we deploy this straight to production?
|
||||
|
||||
<!-- You should be able to use the mobile browser emulator in your browser to test this. -->
|
||||
- [ ] For frontend changes: Tested on mobile?
|
||||
|
||||
<!-- New env vars need to be called out
|
||||
so they can be properly configured for prod. -->
|
||||
- [ ] Did you introduce any new environment variables? If so, call them out explicitly in the PR description.
|
||||
|
@ -176,7 +176,10 @@ export default {
|
||||
let sitems = null
|
||||
let termQueries = []
|
||||
|
||||
if (!q) {
|
||||
// short circuit: return empty result if either:
|
||||
// 1. no query provided, or
|
||||
// 2. searching bookmarks without being authed
|
||||
if (!q || (what === 'bookmarks' && !me)) {
|
||||
return {
|
||||
items: [],
|
||||
cursor: null
|
||||
@ -191,6 +194,11 @@ export default {
|
||||
case 'comments':
|
||||
whatArr.push({ bool: { must: { exists: { field: 'parentId' } } } })
|
||||
break
|
||||
case 'bookmarks':
|
||||
if (me?.id) {
|
||||
whatArr.push({ match: { bookmarkedBy: me?.id } })
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -55,3 +55,10 @@ benalleng,pr,#1068,#1067,good-first-issue,,,,20k,???,???
|
||||
abhiShandy,helpfulness,#1068,#1067,good-first-issue,,,,2k,abhishandy@stacker.news,2024-04-14
|
||||
bumi,pr,#1076,,,,,,20k,bumi@getalby.com,2024-04-16
|
||||
benalleng,pr,#1079,#977,easy,,,,100k,???,???
|
||||
felipebueno,pr,#1024,,,,,,20k,felipe@stacker.news,2024-04-21
|
||||
SatsAllDay,pr,#1075,#1064,medium-hard,,1,,450k,weareallsatoshi@getalby.com,2024-04-21
|
||||
aChrisYouKnow,issue,#1075,#1064,medium-hard,,1,,45k,ACYK@stacker.news,2024-04-22
|
||||
SatsAllDay,pr,#1098,,,,,,20k,weareallsatoshi@getalby.com,2024-04-21
|
||||
SatsAllDay,pr,#1095,#728,medium,,,,250k,weareallsatoshi@getalby.com,2024-04-21
|
||||
benalleng,pr,#1090,#1077,good-first-issue,,,,20k,???,???
|
||||
benalleng,helpfulness,#1087,,,,,informed fix,20k,???,???
|
||||
|
|
@ -29,6 +29,8 @@ import { AWS_S3_URL_REGEXP } from '@/lib/constants'
|
||||
import { whenRange } from '@/lib/time'
|
||||
import { useFeeButton } from './fee-button'
|
||||
import Thumb from '@/svgs/thumb-up-fill.svg'
|
||||
import Eye from '@/svgs/eye-fill.svg'
|
||||
import EyeClose from '@/svgs/eye-close-line.svg'
|
||||
import Info from './info'
|
||||
|
||||
export function SubmitButton ({
|
||||
@ -1052,5 +1054,35 @@ function Client (Component) {
|
||||
}
|
||||
}
|
||||
|
||||
function PasswordHider ({ onClick, showPass }) {
|
||||
return (
|
||||
<InputGroup.Text
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!showPass
|
||||
? <EyeClose
|
||||
fill='var(--bs-body-color)' height={20} width={20}
|
||||
/>
|
||||
: <Eye
|
||||
fill='var(--bs-body-color)' height={20} width={20}
|
||||
/>}
|
||||
</InputGroup.Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function PasswordInput ({ newPass, ...props }) {
|
||||
const [showPass, setShowPass] = useState(false)
|
||||
|
||||
return (
|
||||
<ClientInput
|
||||
{...props}
|
||||
type={showPass ? 'text' : 'password'}
|
||||
autoComplete={newPass ? 'new-password' : 'current-password'}
|
||||
append={<PasswordHider showPass={showPass} onClick={() => setShowPass(!showPass)} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClientInput = Client(Input)
|
||||
export const ClientCheckbox = Client(Checkbox)
|
||||
|
@ -1,15 +1,17 @@
|
||||
import Container from 'react-bootstrap/Container'
|
||||
import styles from './search.module.css'
|
||||
import SearchIcon from '@/svgs/search-line.svg'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Form, Input, Select, DatePicker, SubmitButton } from './form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { whenToFrom } from '@/lib/time'
|
||||
import { useMe } from './me'
|
||||
|
||||
export default function Search ({ sub }) {
|
||||
const router = useRouter()
|
||||
const [q, setQ] = useState(router.query.q || '')
|
||||
const inputRef = useRef(null)
|
||||
const me = useMe()
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
@ -50,6 +52,7 @@ export default function Search ({ sub }) {
|
||||
const what = router.pathname.startsWith('/stackers') ? 'stackers' : router.query.what || 'all'
|
||||
const sort = router.query.sort || 'zaprank'
|
||||
const when = router.query.when || 'forever'
|
||||
const whatItemOptions = useMemo(() => (['all', 'posts', 'comments', me ? 'bookmarks' : undefined, 'stackers'].filter(item => !!item)), [me])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -86,7 +89,7 @@ export default function Search ({ sub }) {
|
||||
name='what'
|
||||
size='sm'
|
||||
overrideValue={what}
|
||||
items={['all', 'posts', 'comments', 'stackers']}
|
||||
items={whatItemOptions}
|
||||
/>
|
||||
{what !== 'stackers' &&
|
||||
<>
|
||||
|
@ -110,7 +110,7 @@ export const ToastProvider = ({ children }) => {
|
||||
// Only clear toasts with no cancel function on page navigation
|
||||
// since navigation should not interfere with being able to cancel an action.
|
||||
useEffect(() => {
|
||||
const handleRouteChangeStart = () => setToasts(toasts => toasts.length > 0 ? toasts.filter(({ onCancel, onUndo }) => onCancel || onUndo) : toasts)
|
||||
const handleRouteChangeStart = () => setToasts(toasts => toasts.length > 0 ? toasts.filter(({ onCancel, onUndo, persistOnNavigate }) => onCancel || onUndo || persistOnNavigate) : toasts)
|
||||
router.events.on('routeChangeStart', handleRouteChangeStart)
|
||||
|
||||
return () => {
|
||||
|
@ -40,6 +40,7 @@ export const toastDeleteScheduled = (toaster, upsertResponseData, dataKey, isEdi
|
||||
}[dataKey] ?? 'item'
|
||||
|
||||
const message = `${itemType === 'comment' ? 'your comment' : isEdit ? `this ${itemType}` : `your new ${itemType}`} will be deleted at ${deleteScheduledAt.toLocaleString()}`
|
||||
toaster.success(message)
|
||||
// only persist this on navigation for posts, not comments
|
||||
toaster.success(message, { persistOnNavigate: itemType !== 'comment' })
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
import { Form, ClientInput, ClientCheckbox } from '@/components/form'
|
||||
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||
import { lnbitsSchema } from '@/lib/validate'
|
||||
@ -51,12 +51,12 @@ export default function LNbits () {
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<ClientInput
|
||||
<PasswordInput
|
||||
initialValue={adminKey}
|
||||
type='password'
|
||||
autoComplete='false'
|
||||
label='admin key'
|
||||
name='adminKey'
|
||||
newPass
|
||||
required
|
||||
/>
|
||||
<ClientCheckbox
|
||||
disabled={!enabled || isDefault || enabledProviders.length === 1}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
import { Form, ClientInput, ClientCheckbox } from '@/components/form'
|
||||
import { Form, ClientCheckbox, PasswordInput } from '@/components/form'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||
import { nwcSchema } from '@/lib/validate'
|
||||
@ -43,12 +43,11 @@ export default function NWC () {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ClientInput
|
||||
<PasswordInput
|
||||
initialValue={nwcUrl}
|
||||
label='connection'
|
||||
name='nwcUrl'
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
newPass
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
|
@ -0,0 +1,46 @@
|
||||
CREATE OR REPLACE FUNCTION index_bookmarked_item() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- if a bookmark was created or updated, `NEW` will be used
|
||||
IF NEW IS NOT NULL THEN
|
||||
INSERT INTO pgboss.job (name, data) VALUES ('indexItem', jsonb_build_object('id', NEW."itemId"));
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
-- if a bookmark was deleted, `OLD` will be used
|
||||
IF OLD IS NOT NULL THEN
|
||||
-- include `updatedAt` in the `indexItem` job as `now()` to indicate when the indexed item should think it was updated
|
||||
-- this is to facilitate the fact that deleted bookmarks do not show up when re-indexing the item, and therefore
|
||||
-- we don't have a reliable way to calculate a more recent index version, to displace the prior version
|
||||
INSERT INTO pgboss.job (name, data) VALUES ('indexItem', jsonb_build_object('id', OLD."itemId", 'updatedAt', now()));
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
-- This should never be reached
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Re-index the bookmarked item when a bookmark changes, so new bookmarks are searchable
|
||||
DROP TRIGGER IF EXISTS index_bookmarked_item ON "Bookmark";
|
||||
CREATE TRIGGER index_bookmarked_item
|
||||
AFTER INSERT OR UPDATE OR DELETE ON "Bookmark"
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE index_bookmarked_item();
|
||||
|
||||
-- hack ... prisma doesn't know about our other schemas (e.g. pgboss)
|
||||
-- and this is only really a problem on their "shadow database"
|
||||
-- so we catch the exception it throws and ignore it
|
||||
CREATE OR REPLACE FUNCTION reindex_all_current_bookmarked_items() RETURNS void AS $$
|
||||
BEGIN
|
||||
-- Re-index all existing bookmarked items so these bookmarks are searchable
|
||||
INSERT INTO pgboss.job (name, data, priority, startafter, expirein)
|
||||
SELECT 'indexItem', jsonb_build_object('id', "itemId"), -100, now() + interval '10 minutes', interval '1 day'
|
||||
FROM "Bookmark"
|
||||
GROUP BY "itemId";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- catch the exception for prisma dev execution, but do nothing with it
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- execute the function once
|
||||
SELECT reindex_all_current_bookmarked_items();
|
||||
-- then drop it since we don't need it anymore
|
||||
DROP FUNCTION reindex_all_current_bookmarked_items();
|
@ -171,8 +171,7 @@ Have a great weekend!
|
||||
|
||||
##### Top Posts
|
||||
${top.data.items.items.slice(0, 10).map((item, i) =>
|
||||
`${i + 1}. [@${item.user.name}](https://stacker.news/${item.user.name}) [${item.title}](https://stacker.news/items/${item.id})
|
||||
- [${item.title}](https://stacker.news/items/${item.id})
|
||||
`${i + 1}. [${item.title}](https://stacker.news/items/${item.id})
|
||||
- ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')}
|
||||
|
||||
##### Don't miss
|
||||
|
7
sndev
7
sndev
@ -13,7 +13,12 @@ docker__compose() {
|
||||
COMPOSE_PROFILES="images,search,payments,email,capture"
|
||||
fi
|
||||
|
||||
CURRENT_UID=$(id -u) CURRENT_GID=$(id -g) COMPOSE_PROFILES=$COMPOSE_PROFILES command docker compose --env-file .env.development --env-file .env.local "$@"
|
||||
ENV_LOCAL=
|
||||
if [ -f .env.local ]; then
|
||||
ENV_LOCAL='--env-file .env.local'
|
||||
fi
|
||||
|
||||
CURRENT_UID=$(id -u) CURRENT_GID=$(id -g) COMPOSE_PROFILES=$COMPOSE_PROFILES command docker compose --env-file .env.development $ENV_LOCAL "$@"
|
||||
}
|
||||
|
||||
docker__exec() {
|
||||
|
@ -35,7 +35,7 @@ const ITEM_SEARCH_FIELDS = gql`
|
||||
ncomments
|
||||
}`
|
||||
|
||||
async function _indexItem (item, { models }) {
|
||||
async function _indexItem (item, { models, updatedAt }) {
|
||||
console.log('indexing item', item.id)
|
||||
// HACK: modify the title for jobs so that company/location are searchable
|
||||
// and highlighted without further modification
|
||||
@ -60,11 +60,33 @@ async function _indexItem (item, { models }) {
|
||||
|
||||
itemcp.wvotes = itemdb.weightedVotes - itemdb.weightedDownVotes
|
||||
|
||||
const bookmarkedBy = await models.bookmark.findMany({
|
||||
where: { itemId: Number(item.id) },
|
||||
select: { userId: true, createdAt: true },
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: 'desc'
|
||||
}
|
||||
]
|
||||
})
|
||||
itemcp.bookmarkedBy = bookmarkedBy.map(bookmark => bookmark.userId)
|
||||
|
||||
// use the latest of:
|
||||
// 1. an explicitly-supplied updatedAt value, used when a bookmark to this item was removed
|
||||
// 2. when the item itself was updated
|
||||
// 3. or when it was last bookmarked
|
||||
// to determine the latest version of the indexed version
|
||||
const latestUpdatedAt = Math.max(
|
||||
updatedAt ? new Date(updatedAt).getTime() : 0,
|
||||
new Date(item.updatedAt).getTime(),
|
||||
bookmarkedBy[0] ? new Date(bookmarkedBy[0].createdAt).getTime() : 0
|
||||
)
|
||||
|
||||
try {
|
||||
await search.index({
|
||||
id: item.id,
|
||||
index: process.env.OPENSEARCH_INDEX,
|
||||
version: new Date(item.updatedAt).getTime(),
|
||||
version: new Date(latestUpdatedAt).getTime(),
|
||||
versionType: 'external_gte',
|
||||
body: itemcp
|
||||
})
|
||||
@ -79,7 +101,9 @@ async function _indexItem (item, { models }) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function indexItem ({ data: { id }, apollo, models }) {
|
||||
// `data.updatedAt` is an explicit updatedAt value for the use case of a bookmark being removed
|
||||
// this is consulted to generate the index version
|
||||
export async function indexItem ({ data: { id, updatedAt }, apollo, models }) {
|
||||
// 1. grab item from database
|
||||
// could use apollo to avoid duping logic
|
||||
// when grabbing sats and user name, etc
|
||||
@ -94,7 +118,7 @@ export async function indexItem ({ data: { id }, apollo, models }) {
|
||||
})
|
||||
|
||||
// 2. index it with external version based on updatedAt
|
||||
await _indexItem(item, { models })
|
||||
await _indexItem(item, { models, updatedAt })
|
||||
}
|
||||
|
||||
export async function indexAllItems ({ apollo, models }) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user