mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Merge commit '5c6bffd6de' into swarmforge-architect
This commit is contained in:
@@ -11,6 +11,7 @@ import MemoDb from '../../../services/memo-db'
|
||||
import MemoFollow from '../../../services/memo-follow'
|
||||
import MemoMute from '../../../services/memo-mute'
|
||||
import ProfilePage from '../../../services/profile-page'
|
||||
import { getViewerAddress } from '../../../services/profile-wallet'
|
||||
import PostReplyCount from '../../post-reply-count'
|
||||
import LikeButton from '../../post-feed/like-button'
|
||||
import PostThreadModal from '../../post-thread-modal'
|
||||
@@ -75,6 +76,10 @@ function Profile (props) {
|
||||
setThreadTxid(null)
|
||||
}
|
||||
|
||||
const wallet = appData?.wallet || null
|
||||
const appProfiles = appData?.profiles || null
|
||||
const myAddr = getViewerAddress(appData)
|
||||
|
||||
const runAction = async (method, failMsg) => {
|
||||
if (!profilePage || busy) return
|
||||
setBusy(true)
|
||||
@@ -98,12 +103,11 @@ function Profile (props) {
|
||||
|
||||
try {
|
||||
const memoDb = new MemoDb()
|
||||
const myAddr = appData?.wallet?.walletInfo?.cashAddress || null
|
||||
const memoFollow = myAddr
|
||||
? new MemoFollow({ wallet: appData.wallet, profiles: appData.profiles })
|
||||
const memoFollow = myAddr && wallet
|
||||
? new MemoFollow({ wallet, profiles: appProfiles })
|
||||
: null
|
||||
const memoMute = myAddr
|
||||
? new MemoMute({ wallet: appData.wallet, profiles: appData.profiles })
|
||||
const memoMute = myAddr && wallet
|
||||
? new MemoMute({ wallet, profiles: appProfiles })
|
||||
: null
|
||||
const page = new ProfilePage({ memoDb, addr, myAddr, memoFollow, memoMute })
|
||||
|
||||
@@ -132,7 +136,7 @@ function Profile (props) {
|
||||
setError('Missing profile address')
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addr, appData?.wallet, appData?.profiles])
|
||||
}, [addr, myAddr, wallet, appProfiles])
|
||||
|
||||
const showFollowButton = profilePage && profilePage.canFollow() && !profilePage.isFollowing()
|
||||
const showUnfollowButton = profilePage && profilePage.canFollow() && profilePage.isFollowing()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Wallet address selection for the profile page.
|
||||
|
||||
The React app stores the authenticated wallet address in two places:
|
||||
`appData.bchWalletState.cashAddress` (reactive state updated as the wallet
|
||||
initializes) and `appData.wallet.walletInfo.cashAddress` (the wallet object
|
||||
itself). Prefer the reactive state so the profile page re-loads when the
|
||||
address becomes available.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
function getViewerAddress (appData) {
|
||||
return appData?.bchWalletState?.cashAddress ||
|
||||
appData?.wallet?.walletInfo?.cashAddress ||
|
||||
null
|
||||
}
|
||||
|
||||
module.exports = { getViewerAddress }
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Property tests for the profile page wallet-address selector.
|
||||
|
||||
The unit tests check getViewerAddress at a few fixed shapes. These
|
||||
properties pin the precedence invariant over a broad input space:
|
||||
|
||||
- precedence: whenever the reactive bchWalletState.cashAddress is a truthy
|
||||
value it is returned, regardless of the wallet-field address.
|
||||
- fallback: when the reactive state carries no truthy address, the wallet
|
||||
object's walletInfo.cashAddress is used when present.
|
||||
- absence: when neither source has an address, the result is null, never a
|
||||
partial/empty object.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const { seededRandom, forAll, intGen } = require('./harness')
|
||||
const { getViewerAddress } = require('../../src/services/profile-wallet')
|
||||
|
||||
const rng = seededRandom(20260903)
|
||||
const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
||||
|
||||
// Random cash-address-shaped string (never empty so it passes as truthy).
|
||||
function addressGen () {
|
||||
const len = intGen(rng, 40, 60)()
|
||||
let out = 'bitcoincash:q'
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += CHARS[Math.floor(rng() * CHARS.length)]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Pick a "reactive state address" which may be falsy (undefined/null/'') or a
|
||||
// real address string.
|
||||
function stateAddrGen () {
|
||||
const roll = rng()
|
||||
if (roll < 0.2) return undefined
|
||||
if (roll < 0.4) return null
|
||||
if (roll < 0.55) return ''
|
||||
return addressGen()
|
||||
}
|
||||
|
||||
// Build a wallet field: absent, present without an address, or present with a
|
||||
// randomly chosen address.
|
||||
function walletGen () {
|
||||
const roll = rng()
|
||||
if (roll < 0.25) return undefined
|
||||
if (roll < 0.4) return { walletInfo: {} }
|
||||
return { walletInfo: { cashAddress: addressGen() } }
|
||||
}
|
||||
|
||||
function buildAppData (stateAddr, wallet) {
|
||||
return {
|
||||
bchWalletState: { cashAddress: stateAddr },
|
||||
wallet,
|
||||
profiles: {}
|
||||
}
|
||||
}
|
||||
|
||||
function expectedGet (stateAddr, wallet) {
|
||||
if (stateAddr) return stateAddr
|
||||
if (wallet && wallet.walletInfo && wallet.walletInfo.cashAddress) {
|
||||
return wallet.walletInfo.cashAddress
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function fixtureGen () {
|
||||
return () => ({
|
||||
stateAddr: stateAddrGen(),
|
||||
wallet: walletGen()
|
||||
})
|
||||
}
|
||||
|
||||
const gen = fixtureGen()
|
||||
|
||||
test('getViewerAddress matches the reactive-then-wallet precedence rule', async () => {
|
||||
await forAll(
|
||||
gen,
|
||||
({ stateAddr, wallet }) => {
|
||||
const appData = buildAppData(stateAddr, wallet)
|
||||
return getViewerAddress(appData) === expectedGet(stateAddr, wallet)
|
||||
},
|
||||
{ label: 'getViewerAddress precedence' }
|
||||
)
|
||||
})
|
||||
|
||||
test('getViewerAddress prefers the reactive state whenever it is truthy', async () => {
|
||||
await forAll(
|
||||
gen,
|
||||
({ stateAddr, wallet }) => {
|
||||
if (!stateAddr) return true
|
||||
const appData = buildAppData(stateAddr, wallet)
|
||||
return getViewerAddress(appData) === stateAddr
|
||||
},
|
||||
{ label: 'getViewerAddress reactive preference invariant' }
|
||||
)
|
||||
})
|
||||
|
||||
test('getViewerAddress returns null when no wallet address is available', async () => {
|
||||
await forAll(
|
||||
gen,
|
||||
({ stateAddr, wallet }) => {
|
||||
if (expectedGet(stateAddr, wallet) !== null) return true
|
||||
const appData = buildAppData(stateAddr, wallet)
|
||||
return getViewerAddress(appData) === null
|
||||
},
|
||||
{ label: 'getViewerAddress null-on-absent invariant' }
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Unit tests for the profile wallet address selector.
|
||||
|
||||
The profile page must show follow/mute controls only when the viewer's
|
||||
wallet address is known. The React app tracks the loaded address both in the
|
||||
wallet object and in `bchWalletState`; the selector prefers the reactive
|
||||
state so the profile reloads when the address becomes available.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const { getViewerAddress } = require('../../src/services/profile-wallet')
|
||||
|
||||
function makeAppData ({ bchCashAddress, walletCashAddress } = {}) {
|
||||
return {
|
||||
bchWalletState: {
|
||||
cashAddress: bchCashAddress
|
||||
},
|
||||
wallet: walletCashAddress
|
||||
? { walletInfo: { cashAddress: walletCashAddress } }
|
||||
: null,
|
||||
profiles: {}
|
||||
}
|
||||
}
|
||||
|
||||
test('getViewerAddress prefers bchWalletState.cashAddress', () => {
|
||||
const appData = makeAppData({
|
||||
bchCashAddress: 'bitcoincash:state-address',
|
||||
walletCashAddress: 'bitcoincash:wallet-address'
|
||||
})
|
||||
|
||||
assert.equal(getViewerAddress(appData), 'bitcoincash:state-address')
|
||||
})
|
||||
|
||||
test('getViewerAddress falls back to wallet.walletInfo.cashAddress', () => {
|
||||
const appData = makeAppData({
|
||||
bchCashAddress: undefined,
|
||||
walletCashAddress: 'bitcoincash:wallet-address'
|
||||
})
|
||||
|
||||
assert.equal(getViewerAddress(appData), 'bitcoincash:wallet-address')
|
||||
})
|
||||
|
||||
test('getViewerAddress returns null when no wallet is loaded', () => {
|
||||
const appData = makeAppData({})
|
||||
|
||||
assert.equal(getViewerAddress(appData), null)
|
||||
})
|
||||
|
||||
test('getViewerAddress returns null when wallet lacks an address', () => {
|
||||
const appData = {
|
||||
bchWalletState: {},
|
||||
wallet: { walletInfo: {} },
|
||||
profiles: {}
|
||||
}
|
||||
|
||||
assert.equal(getViewerAddress(appData), null)
|
||||
})
|
||||
+3
-2
@@ -359,6 +359,7 @@ At the end of each session, update this file:
|
||||
- Note the current `master` HEAD commit.
|
||||
- State the next feature to work on.
|
||||
|
||||
Current `master` HEAD: `d3903fa` (backlog cleared of all previously listed
|
||||
features; direction set to front-end improvements to `psf-memo-client`).
|
||||
Current `master` HEAD: `d99eda3` (merged architect's notifications refactor +
|
||||
startup bloat check; verified db 315 passing + lint clean, client 239 passing +
|
||||
lint clean + build succeeds).
|
||||
Next action: **ask the user for the next front-end improvement to spec**.
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper for the Babashka APS gherkin-parser task.
|
||||
# Delegates to the project-local APS checkout under .swarmforge/tools/aps.
|
||||
set -euo pipefail
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "usage: gherkin-parser <feature-file> <json-output>" >&2
|
||||
exit 2
|
||||
fi
|
||||
FEATURE_FILE="$(realpath -m "$1")"
|
||||
JSON_OUTPUT="$(realpath -m "$2")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APS_DIR="$(cd "$SCRIPT_DIR/../../.swarmforge/tools/aps" && pwd)"
|
||||
cd "$APS_DIR"
|
||||
exec bb gherkin-parser "$FEATURE_FILE" "$JSON_OUTPUT"
|
||||
Reference in New Issue
Block a user