Add Opt-in to Display Linked Accounts in Profile (#826)

* Add display linked accounts to settings

* Apply suggestions from code review

Co-authored-by: ekzyis <ek@stacker.news>

* small styling enhancements

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
Alex Lewin 2024-02-14 14:33:31 -05:00 committed by GitHub
parent 5c3c7fb185
commit b3498fe277
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 211 additions and 8 deletions

View File

@ -503,10 +503,15 @@ export default {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
}
await models.account.delete({ where: { id: account.id } })
if (authType === 'twitter') {
await models.user.update({ where: { id: me.id }, data: { hideTwitter: true, twitterId: null } })
} else {
await models.user.update({ where: { id: me.id }, data: { hideGithub: true, githubId: null } })
}
} else if (authType === 'lightning') {
user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
} else if (authType === 'nostr') {
user = await models.user.update({ where: { id: me.id }, data: { nostrAuthPubkey: null } })
user = await models.user.update({ where: { id: me.id }, data: { hideNostr: true, nostrAuthPubkey: null } })
} else if (authType === 'email') {
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
} else {
@ -811,6 +816,24 @@ export default {
}
}
})
},
githubId: async (user, args, { me }) => {
if ((!me || me.id !== user.id) && user.hideGithub) {
return null
}
return user.githubId
},
twitterId: async (user, args, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideTwitter) {
return null
}
return user.twitterId
},
nostrAuthPubkey: async (user, args, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideNostr) {
return null
}
return user.nostrAuthPubkey
}
}
}

View File

@ -60,6 +60,9 @@ export default gql`
greeterMode: Boolean!
hideBookmarks: Boolean!
hideCowboyHat: Boolean!
hideGithub: Boolean!
hideNostr: Boolean!
hideTwitter: Boolean!
hideFromTopUsers: Boolean!
hideInvoiceDesc: Boolean!
hideIsContributor: Boolean!
@ -119,6 +122,9 @@ export default gql`
greeterMode: Boolean!
hideBookmarks: Boolean!
hideCowboyHat: Boolean!
hideGithub: Boolean!
hideNostr: Boolean!
hideTwitter: Boolean!
hideFromTopUsers: Boolean!
hideInvoiceDesc: Boolean!
hideIsContributor: Boolean!
@ -156,5 +162,8 @@ export default gql`
streak: Int
maxStreak: Int
isContributor: Boolean
githubId: String
twitterId: String
nostrAuthPubkey: String
}
`

View File

@ -24,6 +24,10 @@ import CodeIcon from '../svgs/terminal-box-fill.svg'
import MuteDropdownItem from './mute'
import copy from 'clipboard-copy'
import { useToast } from './toast'
import { hexToBech32 } from '../lib/nostr'
import NostrIcon from '../svgs/nostr.svg'
import GithubIcon from '../svgs/github-fill.svg'
import TwitterIcon from '../svgs/twitter-fill.svg'
export default function UserHeader ({ user }) {
const router = useRouter()
@ -180,8 +184,36 @@ function HeaderNym ({ user, isMe }) {
: <NymView user={user} isMe={isMe} setEditting={setEditting} />
}
function SocialLink ({ name, id }) {
const className = `${styles.social} text-reset`
if (name === 'Nostr') {
const npub = hexToBech32(id)
return (
<Link className={className} target='_blank' href={`https://nostr.com/${npub}`} rel='noreferrer'>
<NostrIcon width={20} height={20} className='me-1' />
{npub.slice(0, 10)}...{npub.slice(-10)}
</Link>
)
} else if (name === 'Github') {
return (
<Link className={className} target='_blank' href={`https://github.com/${id}`} rel='noreferrer'>
<GithubIcon width={20} height={20} className='me-1' />
{id}
</Link>
)
} else if (name === 'Twitter') {
return (
<Link className={className} target='_blank' href={`https://twitter.com/${id}`} rel='noreferrer'>
<TwitterIcon width={20} height={20} className='me-1' />
@{id}
</Link>
)
}
}
function HeaderHeader ({ user }) {
const me = useMe()
const showModal = useShowModal()
const toaster = useToast()
@ -229,8 +261,24 @@ function HeaderHeader ({ user }) {
? <Link href={`/items/${user.since}`} className='ms-1'>#{user.since}</Link>
: <span>never</span>}
</small>
{user.optional.maxStreak !== null && <small className='text-muted d-flex-inline'>longest cowboy streak: {user.optional.maxStreak}</small>}
{user.optional.isContributor && <small className='text-muted d-flex align-items-center'><CodeIcon className='me-1' height={16} width={16} /> verified stacker.news contributor</small>}
{user.optional.maxStreak !== null &&
<small className='text-muted d-flex-inline'>longest cowboy streak: {user.optional.maxStreak}</small>}
{user.optional.isContributor &&
<small className='text-muted d-flex align-items-center'>
<CodeIcon className='me-1' height={16} width={16} /> verified stacker.news contributor
</small>}
{user.optional.nostrAuthPubkey !== null && !me.privates?.hideNostr &&
<small className='text-muted d-flex-inline'>
<SocialLink name='Nostr' id={user.optional.nostrAuthPubkey} />
</small>}
{user.optional.githubId !== null && !me?.privates?.hideGithub &&
<small className='text-muted d-flex-inline'>
<SocialLink name='Github' id={user.optional.githubId} />
</small>}
{user.optional.twitterId !== null && !me?.privates?.hideTwitter &&
<small className='text-muted d-flex-inline'>
<SocialLink name='Twitter' id={user.optional.twitterId} />
</small>}
</div>
</div>
</div>

View File

@ -31,3 +31,9 @@
font-size: 150%;
line-height: 1.25rem;
}
.social {
font-weight: bold;
display: flex;
align-items: center;
}

View File

@ -15,6 +15,9 @@ export const ME = gql`
greeterMode
hideCowboyHat
hideFromTopUsers
hideGithub
hideNostr
hideTwitter
hideInvoiceDesc
hideIsContributor
hideWalletBalance
@ -47,6 +50,9 @@ export const ME = gql`
isContributor
stacked
streak
githubId
nostrAuthPubkey
twitterId
}
}
}`
@ -73,6 +79,9 @@ export const SETTINGS_FIELDS = gql`
hideFromTopUsers
hideCowboyHat
hideBookmarks
hideGithub
hideNostr
hideTwitter
hideIsContributor
imgproxyOnly
hideWalletBalance
@ -182,6 +191,9 @@ export const USER_FIELDS = gql`
streak
maxStreak
isContributor
githubId
nostrAuthPubkey
twitterId
}
}`

View File

@ -476,6 +476,9 @@ export const settingsSchema = object({
).max(NOSTR_MAX_RELAY_NUM,
({ max, value }) => `${Math.abs(max - value.length)} too many`),
hideBookmarks: boolean(),
hideGithub: boolean(),
hideNostr: boolean(),
hideTwitter: boolean(),
hideWalletBalance: boolean(),
diagnostics: boolean(),
hideIsContributor: boolean()

View File

@ -12,6 +12,30 @@ import { NodeNextRequest } from 'next/dist/server/base-http/node'
import { schnorr } from '@noble/curves/secp256k1'
import { sendUserNotification } from '../../../api/webPush'
/**
* Stores userIds in user table
* @returns {Partial<import('next-auth').EventCallbacks>}
* */
function getEventCallbacks () {
return {
async linkAccount ({ user, profile, account }) {
if (account.provider === 'github') {
await prisma.user.update({ where: { id: user.id }, data: { githubId: profile.name } })
} else if (account.provider === 'twitter') {
await prisma.user.update({ where: { id: user.id }, data: { twitterId: profile.name } })
}
},
async signIn ({ user, profile, account, isNewUser }) {
if (account.provider === 'github') {
await prisma.user.update({ where: { id: user.id }, data: { githubId: profile.name } })
} else if (account.provider === 'twitter') {
await prisma.user.update({ where: { id: user.id }, data: { twitterId: profile.name } })
}
}
}
}
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
function getCallbacks (req) {
return {
/**
@ -136,6 +160,7 @@ async function nostrEventAuth (event) {
return { k1, pubkey }
}
/** @type {import('next-auth/providers').Provider[]} */
const providers = [
CredentialsProvider({
id: 'lightning',
@ -164,7 +189,7 @@ const providers = [
url: 'https://github.com/login/oauth/authorize',
params: { scope: '' }
},
profile: profile => {
profile (profile) {
return {
id: profile.id,
name: profile.login
@ -174,7 +199,7 @@ const providers = [
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
profile: profile => {
profile (profile) {
return {
id: profile.id,
name: profile.screen_name
@ -188,6 +213,7 @@ const providers = [
})
]
/** @returns {import('next-auth').AuthOptions} */
export const getAuthOptions = req => ({
callbacks: getCallbacks(req),
providers,
@ -199,7 +225,8 @@ export const getAuthOptions = req => ({
signIn: '/login',
verifyRequest: '/email',
error: '/auth/error'
}
},
events: getEventCallbacks()
})
export default async (req, res) => {

View File

@ -3,7 +3,7 @@ import Alert from 'react-bootstrap/Alert'
import Button from 'react-bootstrap/Button'
import InputGroup from 'react-bootstrap/InputGroup'
import { CenterLayout } from '../../components/layout'
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { gql, useMutation, useQuery } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import LoginButton from '../../components/login-button'
@ -52,7 +52,7 @@ export default function Settings ({ ssrData }) {
const logger = useLogger()
const { data } = useQuery(SETTINGS)
const { settings: { privates: settings } } = data || ssrData
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
if (!data && !ssrData) return <PageLoading />
return (
@ -79,6 +79,9 @@ export default function Settings ({ ssrData }) {
autoDropBolt11s: settings?.autoDropBolt11s,
hideFromTopUsers: settings?.hideFromTopUsers,
hideCowboyHat: settings?.hideCowboyHat,
hideGithub: settings?.hideGithub,
hideNostr: settings?.hideNostr,
hideTwitter: settings?.hideTwitter,
imgproxyOnly: settings?.imgproxyOnly,
wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode,
@ -284,6 +287,66 @@ export default function Settings ({ ssrData }) {
name='hideBookmarks'
groupClassName='mb-0'
/>
<Checkbox
disabled={me.optional.githubId === null}
label={
<div className='d-flex align-items-center'>hide my linked github profile
<Info>
<ul className='fw-bold'>
<li>Linked accounts are hidden from your profile by default</li>
<li>uncheck this to display your github on your profile</li>
{me.optional.githubId === null &&
<div className='my-2'>
<li><i>You don't seem to have a linked github account</i></li>
<ul><li>If this is wrong, try unlinking/relinking</li></ul>
</div>}
</ul>
</Info>
</div>
}
name='hideGithub'
groupClassName='mb-0'
/>
<Checkbox
disabled={me.optional.nostrAuthPubkey === null}
label={
<div className='d-flex align-items-center'>hide my linked nostr profile
<Info>
<ul className='fw-bold'>
<li>Linked accounts are hidden from your profile by default</li>
<li>Uncheck this to display your npub on your profile</li>
{me.optional.nostrAuthPubkey === null &&
<div className='my-2'>
<li>You don't seem to have a linked nostr account</li>
<ul><li>If this is wrong, try unlinking/relinking</li></ul>
</div>}
</ul>
</Info>
</div>
}
name='hideNostr'
groupClassName='mb-0'
/>
<Checkbox
disabled={me.optional.twitterId === null}
label={
<div className='d-flex align-items-center'>hide my linked twitter profile
<Info>
<ul className='fw-bold'>
<li>Linked accounts are hidden from your profile by default</li>
<li>Uncheck this to display your twitter on your profile</li>
{me.optional.twitterId === null &&
<div className='my-2'>
<i>You don't seem to have a linked twitter account</i>
<ul><li>If this is wrong, try unlinking/relinking</li></ul>
</div>}
</ul>
</Info>
</div>
}
name='hideTwitter'
groupClassName='mb-0'
/>
{me.optional?.isContributor &&
<Checkbox
label={<>hide that I'm a stacker.news contributor</>}

View File

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "hideGithub" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "hideNostr" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "hideTwitter" BOOLEAN NOT NULL DEFAULT true;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "githubId" TEXT,
ADD COLUMN "twitterId" TEXT;

View File

@ -92,6 +92,11 @@ model User {
Session Session[]
itemForwards ItemForward[]
hideBookmarks Boolean @default(false)
hideGithub Boolean @default(true)
hideNostr Boolean @default(true)
hideTwitter Boolean @default(true)
githubId String?
twitterId String?
followers UserSubscription[] @relation("follower")
followees UserSubscription[] @relation("followee")
hideWelcomeBanner Boolean @default(false)