Compare commits

...

8 Commits

Author SHA1 Message Date
Keyan d41b2e14f1
Update awards.csv 2024-04-22 10:08:33 -05:00
Keyan 15c6843d80
Update awards.csv 2024-04-21 17:44:45 -05:00
Ben Allen ecedbd1527
Add PasswordInput component (#1090)
* feat: add PasswordHider

* feat: add PasswordInput

* fix typo and require requirement

* merge state

* use ...props and lnbits password required

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-04-21 17:28:57 -05:00
SatsAllDay fbd3f8efed
introduce `persistOnNavigate` option for toasts (#1095)
ensure that post auto-delete success toasts are preserved
when navigating back to the prior page
2024-04-21 17:25:48 -05:00
keyan fa5adac297 improve newsletter top stories format 2024-04-21 14:12:29 -05:00
SatsAllDay 01de4e7ba7
fix sndev to make .env.local optional in docker compose files (#1098) 2024-04-20 16:50:43 -05:00
SatsAllDay d7ecbbae3a
Search bookmarks (#1075)
* Support `is:bookmarked` search option to search my bookmarked items

* Update the worker search module to include `bookmarkedBy: Array<Number>` which
contains the list of user ids which have bookmarked a given item

* Add a trigger on the `Bookmark` DB table to re-index the corresponding item when
a bookmark is added/removed

* Update the Search resolver to check for a `is:bookmarked` query option. If provided,
include it as an option in the search request. This updates search to look for items
which are bookmarked by the current user. By default, this preserves stacker privacy
so you can only search your own bookmarks

* Update the search page UI to show how to invoke searching your own bookmarks

* undo `is:bookmarked` support, add `bookmarks` item in search select

* short circuit return empty payload for anon requesting bookmarks

* remove console.log for debugging

* fix indexing a new item that has yet to be bookmarked

* update db migration to re-index all existing bookmarked items one time

* fix the case where deleting a bookmark doesn't trigger a new index of items

explictly specify a `updatedAt` value when deleting a bookmark, to ensure that
deleting a bookmark results in a new indexed version of the bookmarked item

* update search indexer to use the latest of all three choices for the latest version

* give bookmark index jobs longer expiration

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-19 13:24:48 -05:00
ekzyis 0552736dc7
More concise PR template (#1092)
The big HTML comments were getting in the way.
2024-04-19 10:56:08 -05:00
13 changed files with 150 additions and 47 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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,???,???

1 name type pr id issue ids difficulty priority changes requested notes amount receive method date paid
55 abhiShandy helpfulness #1068 #1067 good-first-issue 2k abhishandy@stacker.news 2024-04-14
56 bumi pr #1076 20k bumi@getalby.com 2024-04-16
57 benalleng pr #1079 #977 easy 100k ??? ???
58 felipebueno pr #1024 20k felipe@stacker.news 2024-04-21
59 SatsAllDay pr #1075 #1064 medium-hard 1 450k weareallsatoshi@getalby.com 2024-04-21
60 aChrisYouKnow issue #1075 #1064 medium-hard 1 45k ACYK@stacker.news 2024-04-22
61 SatsAllDay pr #1098 20k weareallsatoshi@getalby.com 2024-04-21
62 SatsAllDay pr #1095 #728 medium 250k weareallsatoshi@getalby.com 2024-04-21
63 benalleng pr #1090 #1077 good-first-issue 20k ??? ???
64 benalleng helpfulness #1087 informed fix 20k ??? ???

View File

@ -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)

View File

@ -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' &&
<>

View File

@ -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 () => {

View File

@ -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' })
}
}

View File

@ -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}

View File

@ -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
/>

View File

@ -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();

View File

@ -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
View File

@ -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() {

View File

@ -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 }) {