Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions api/resolvers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,51 @@ export default {
users
}
},
userMutedUsers: async (parent, { name, cursor }, { models, me }) => {
const user = await models.user.findUnique({ where: { name } })
if (!user) {
throw new GqlInputError('no such user')
}
if (!user.showMutedUsers && (!me || me.id !== user.id)) {
return null
}
const decodedCursor = decodeCursor(cursor)
const users = await models.$queryRaw`
SELECT users.*
FROM "Mute"
JOIN users ON "Mute"."mutedId" = users.id
WHERE "Mute"."muterId" = ${user.id}
OFFSET ${decodedCursor.offset}
LIMIT ${LIMIT}
`
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
userSubscribedUsers: async (parent, { name, cursor }, { models, me }) => {
const user = await models.user.findUnique({ where: { name } })
if (!user) {
throw new GqlInputError('no such user')
}
if (!user.showSubscribedUsers && (!me || me.id !== user.id)) {
return null
}
const decodedCursor = decodeCursor(cursor)
const users = await models.$queryRaw`
SELECT users.*
FROM "UserSubscription"
JOIN users ON "UserSubscription"."followeeId" = users.id
WHERE "UserSubscription"."followerId" = ${user.id}
AND ("UserSubscription"."postsSubscribedAt" IS NOT NULL OR "UserSubscription"."commentsSubscribedAt" IS NOT NULL)
OFFSET ${decodedCursor.offset}
LIMIT ${LIMIT}
`
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
topCowboys: async (parent, { cursor }, { models, me }) => {
const { users, cursor: topCowboysCursor } = await topUsers(parent, { cursor, when: 'forever', by: 'streak', limit: LIMIT }, { models, me })
const cowboys = users.map(u => (u?.hideCowboyHat && (!me || me.id !== u.id)) ? null : u).filter(u => u?.streak !== null)
Expand Down Expand Up @@ -857,6 +902,26 @@ export default {

return await isMuted({ models, muterId: me.id, mutedId: user.id })
},
showSubscribedUsers: user => user.showSubscribedUsers ?? false,
showMutedUsers: user => user.showMutedUsers ?? false,
nsubscribed: async (user, args, { models }) => {
if (typeof user.nsubscribed !== 'undefined') return user.nsubscribed
return await models.userSubscription.count({
where: {
followerId: user.id,
OR: [
{ postsSubscribedAt: { not: null } },
{ commentsSubscribedAt: { not: null } }
]
}
})
},
nmuted: async (user, args, { models }) => {
if (typeof user.nmuted !== 'undefined') return user.nmuted
return await models.mute.count({
where: { muterId: user.id }
})
},
since: async (user, args, { models }) => {
// get the user's first item
const item = await models.item.findFirst({
Expand Down
8 changes: 8 additions & 0 deletions api/typeDefs/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default gql`
hasNewNotes: Boolean!
mySubscribedUsers(cursor: String): Users!
myMutedUsers(cursor: String): Users!
userMutedUsers(name: String!, cursor: String): Users
userSubscribedUsers(name: String!, cursor: String): Users
}

type UsersNullable {
Expand Down Expand Up @@ -75,6 +77,10 @@ export default gql`
meMute: Boolean!
meSubscriptionPosts: Boolean!
meSubscriptionComments: Boolean!
showSubscribedUsers: Boolean!
showMutedUsers: Boolean!
nsubscribed: Int!
nmuted: Int!
}

input SettingsInput {
Expand All @@ -91,6 +97,8 @@ export default gql`
hideInvoiceDesc: Boolean!
imgproxyOnly: Boolean!
showImagesAndVideos: Boolean!
showMutedUsers: Boolean!
showSubscribedUsers: Boolean!
nostrCrossposting: Boolean!
nostrPubkey: String
nostrRelays: [String!]
Expand Down
43 changes: 42 additions & 1 deletion components/user-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,25 @@ const MEDIA_URL = process.env.NEXT_PUBLIC_MEDIA_URL || `https://${process.env.NE

export default function UserHeader ({ user }) {
const router = useRouter()
const { me } = useMe()

const pathParts = router.asPath.split('/')
const activeKey = pathParts[2] === 'territories' ? 'territories' : pathParts.length === 2 ? 'bio' : 'items'
let activeKey = 'bio'
if (pathParts[2] === 'territories') {
activeKey = 'territories'
} else if (pathParts[2] === 'subscribed') {
activeKey = 'subscribed'
} else if (pathParts[2] === 'muted') {
activeKey = 'muted'
} else if (pathParts.length > 2) {
activeKey = 'items'
}

const isMe = me?.name === user.name
const showTerritoriesTab = activeKey === 'territories' || user.nterritories > 0
// Show subscribed/muted tabs only if: viewing own profile OR list is public
const showSubscribedTab = isMe || user.showSubscribedUsers === true
const showMutedTab = isMe || user.showMutedUsers === true

return (
<>
Expand Down Expand Up @@ -77,6 +92,32 @@ export default function UserHeader ({ user }) {
</Link>
</Nav.Item>
)}
{showSubscribedTab && (
<Nav.Item>
<Link href={'/' + user.name + '/subscribed'} passHref legacyBehavior>
<Nav.Link eventKey='subscribed'>
{numWithUnits(user.nsubscribed, {
abbreviate: false,
unitSingular: 'subscription',
unitPlural: 'subscriptions'
})}
</Nav.Link>
</Link>
</Nav.Item>
)}
{showMutedTab && (
<Nav.Item>
<Link href={'/' + user.name + '/muted'} passHref legacyBehavior>
<Nav.Link eventKey='muted'>
{numWithUnits(user.nmuted, {
abbreviate: false,
unitSingular: 'muted',
unitPlural: 'muted'
})}
</Nav.Link>
</Link>
</Nav.Item>
)}
</Nav>
</>
)
Expand Down
50 changes: 50 additions & 0 deletions fragments/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ ${STREAK_FIELDS}

export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User {
showSubscribedUsers
showMutedUsers
privates {
tipDefault
tipRandom
Expand Down Expand Up @@ -170,6 +172,10 @@ export const USER_FIELDS = gql`
meSubscriptionPosts
meSubscriptionComments
meMute
showSubscribedUsers
showMutedUsers
nsubscribed
nmuted

optional {
stacked
Expand Down Expand Up @@ -345,6 +351,50 @@ export const MY_SUBSCRIBED_SUBS = gql`
}
`

export const USER_SUBSCRIBED_USERS = gql`
${USER_FIELDS}
${STREAK_FIELDS}
query UserSubscribedUsers($name: String!, $cursor: String) {
user(name: $name) {
...UserFields
}
userSubscribedUsers(name: $name, cursor: $cursor) {
users {
id
name
photoId
meSubscriptionPosts
meSubscriptionComments
meMute
...StreakFields
}
cursor
}
}
`

export const USER_MUTED_USERS = gql`
${USER_FIELDS}
${STREAK_FIELDS}
query UserMutedUsers($name: String!, $cursor: String) {
user(name: $name) {
...UserFields
}
userMutedUsers(name: $name, cursor: $cursor) {
users {
id
name
photoId
meSubscriptionPosts
meSubscriptionComments
meMute
...StreakFields
}
cursor
}
}
`

export const SET_DIAGNOSTICS = gql`
mutation setDiagnostics($diagnostics: Boolean!) {
setDiagnostics(diagnostics: $diagnostics)
Expand Down
4 changes: 3 additions & 1 deletion lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,9 @@ export const settingsSchema = object().shape({
hideTwitter: boolean(),
noReferralLinks: boolean(),
satsFilter: intValidator.required('required').min(0, 'must be at least 0').max(1000, 'must be at most 1000'),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0'),
showMutedUsers: boolean(),
showSubscribedUsers: boolean()
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
}, [['tipRandomMax', 'tipRandomMin']])

Expand Down
51 changes: 51 additions & 0 deletions pages/[name]/muted.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useMemo } from 'react'
import { getGetServerSideProps } from '@/api/ssrApollo'
import { useRouter } from 'next/router'
import { USER_MUTED_USERS } from '@/fragments/users'
import { useQuery } from '@apollo/client'
import PageLoading from '@/components/page-loading'
import { UserLayout } from '.'
import UserList from '@/components/user-list'
import { useMe } from '@/components/me'
import { MuteUserContextProvider } from '@/components/mute'

export const getServerSideProps = getGetServerSideProps({ query: USER_MUTED_USERS })

export default function UserMuted ({ ssrData }) {
const router = useRouter()
const variables = { ...router.query }
const { me } = useMe()
const { data } = useQuery(USER_MUTED_USERS, { variables })
const muteContextValue = useMemo(() => ({ refetchQueries: ['UserMutedUsers'] }), [])
if (!data && !ssrData) return <PageLoading />
const { user } = data || ssrData
const mutedData = data?.userMutedUsers || ssrData?.userMutedUsers
const isPrivate = mutedData === null
const isMe = me?.name === user.name
return (
<UserLayout user={user}>
<div className='mt-2'>
{isPrivate && !isMe
? (
<div className='text-center text-muted mt-5'>
<h4>Private List</h4>
<p>@{user.name} has chosen to keep their muted stackers private.</p>
</div>
)
: (
<MuteUserContextProvider value={muteContextValue}>
<UserList
ssrData={ssrData}
query={USER_MUTED_USERS}
variables={variables}
destructureData={data => data.userMutedUsers || { users: [], cursor: null }}
rank
nymActionDropdown
statCompsProp={[]}
/>
</MuteUserContextProvider>
)}
</div>
</UserLayout>
)
}
51 changes: 51 additions & 0 deletions pages/[name]/subscribed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useMemo } from 'react'
import { getGetServerSideProps } from '@/api/ssrApollo'
import { useRouter } from 'next/router'
import { USER_SUBSCRIBED_USERS } from '@/fragments/users'
import { useQuery } from '@apollo/client'
import PageLoading from '@/components/page-loading'
import { UserLayout } from '.'
import UserList from '@/components/user-list'
import { useMe } from '@/components/me'
import { SubscribeUserContextProvider } from '@/components/subscribeUser'

export const getServerSideProps = getGetServerSideProps({ query: USER_SUBSCRIBED_USERS })

export default function UserSubscribed ({ ssrData }) {
const router = useRouter()
const variables = { ...router.query }
const { me } = useMe()
const { data } = useQuery(USER_SUBSCRIBED_USERS, { variables })
const subscribeContextValue = useMemo(() => ({ refetchQueries: ['UserSubscribedUsers'] }), [])
if (!data && !ssrData) return <PageLoading />
const { user } = data || ssrData
const subscribedData = data?.userSubscribedUsers || ssrData?.userSubscribedUsers
const isPrivate = subscribedData === null
const isMe = me?.name === user.name
return (
<UserLayout user={user}>
<div className='mt-2'>
{isPrivate && !isMe
? (
<div className='text-center text-muted mt-5'>
<h4>Private List</h4>
<p>@{user.name} has chosen to keep their subscribed stackers private.</p>
</div>
)
: (
<SubscribeUserContextProvider value={subscribeContextValue}>
<UserList
ssrData={ssrData}
query={USER_SUBSCRIBED_USERS}
variables={variables}
destructureData={data => data.userSubscribedUsers || { users: [], cursor: null }}
rank
nymActionDropdown
statCompsProp={[]}
/>
</SubscribeUserContextProvider>
)}
</div>
</UserLayout>
)
}
Loading